三、逻辑回归模型训练与结果解释
在完成数据重编码后,接下来即可进行模型训练了,此处我们首先考虑构建可解释性较强的逻辑回归与决策树模型,并围绕最终模型输出结果进行结果解读,而在下一节,我们将继续介绍更多集成模型建模过程。
1.设置评估指标与测试集
当然,在模型训练开始前,我们需要设置模型结果评估指标。此处由于0:1类样本比例约为3:1,因此可以考虑使用准确率作为模型评估指标,同时参考混淆矩阵评估指标、f1-Score和roc-aux值。
需要知道的是,一般在二分类预测问题中,0:1在3:1左右是一个重要界限,若0:1小于3:1,则标签偏态基本可以忽略不计,不需要进行偏态样本处理(处理了也容易过拟合),同时在模型评估指标选取时也可以直接选择“中立”评估指标,如准确率或者roc-auc。而如果0:1大于3:1,则认为标签取值分布存在偏态,需要对其进行处理,如过采样、欠采样、或者模型组合训练、或者样本聚类等,并且如果此时需要重点衡量模型对1类识别能力的话,则更加推荐选择f1-Score。
当然,尽管此处数据集并未呈现明显偏态,但我们仍然会在后续介绍数据调整时介绍常用处理偏态数据的方法,不过还是需要强调的是,对偏态并不明显的数据集采用这些方法,极容易造成模型过拟合。
此外,模型训练过程我们也将模仿实际竞赛流程,即在模型训练开始之初就划分出一个确定的、全程不带入建模的测试集(竞赛中该数据集标签未知,只能通过在线提交结果后获得对应得分),而后续若要在模型训练阶段验证模型结果,则会额外在训练集中划分验证集。
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, roc_auc_score
from sklearn.model_selection import train_test_split
train, test = train_test_split(tcc, test_size=0.3, random_state=21)
其中train就是训练数据集,同时包含训练集的特征和标签。
tcc
2.逻辑回归模型训练
首先我们测试逻辑回归的建模效果。逻辑回归作为线性方程,连续变量和离散变量的数据解释是不同的,连续变量表示数值每变化1,对标签取值的影响,而分类变量则表示当该特征状态发生变化时,标签受到影响的程度。因此,对于若要带入离散变量进行逻辑回归建模,则需要对多分类离散变量进行独热编码处理。当然,也是因为我们需要先对数据集进行转化再进行训练,因此我们可以通过创建机器学习流来封装这两步。
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
- 数据转化
# 划分特征和标签
X_train = train.drop(columns=[ID_col, target]).copy()
y_train = train['Churn'].copy()
X_test = test.drop(columns=[ID_col, target]).copy()
y_test = test['Churn'].copy()
- 设置转化器与评估器
# 检验列是否划分完全
assert len(category_cols) + len(numeric_cols) == X_train.shape[1]# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])# 实例化逻辑回归评估器
logistic_model = LogisticRegression(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)
- 模型训练
logistic_pipe.fit(X_train, y_train)
# Pipeline(steps=[('columntransformer',
# ColumnTransformer(transformers=[('cat',
# OneHotEncoder(drop='if_binary'),
# ['gender', 'SeniorCitizen',
# 'Partner', 'Dependents',
# 'PhoneService',
# 'MultipleLines',
# 'InternetService',
# 'OnlineSecurity',
# 'OnlineBackup',
# 'DeviceProtection',
# 'TechSupport', 'StreamingTV',
# 'StreamingMovies',
# 'Contract',
# 'PaperlessBilling',
# 'PaymentMethod']),
# ('num', 'passthrough',
# ['tenure', 'MonthlyCharges',
# 'TotalCharges'])])),
# ('logisticregression', LogisticRegression(max_iter=100000000))])
- 查看模型结果
接下来,我们直接通过.score方法查看模型再训练集和测试集上准确率:
logistic_pipe.score(X_train, y_train)
#0.8089249492900609
logistic_pipe.score(X_test, y_test)
#0.7931850449597728
当然,关于更多评估指标的计算,我们可以通过下述函数来实现,同时计算模型的召回率、精确度、f1-Score和roc-auc值:
def result_df(model, X_train, y_train, X_test, y_test, metrics=[accuracy_score, recall_score, precision_score, f1_score, roc_auc_score]):res_train = []res_test = []col_name = []for fun in metrics:res_train.append(fun(model.predict(X_train), y_train))res_test.append(fun(model.predict(X_test), y_test)) col_name.append(fun.__name__)idx_name = ['train_eval', 'test_eval']res = pd.DataFrame([res_train, res_test], columns=col_name, index=idx_name)return res
result_df(logistic_pipe, X_train, y_train, X_test, y_test)
一般来说准确率再80%以上的模型就算是可用的模型,但同时也要综合考虑当前数据集情况(建模难度),有些场景(比赛)下80%只是模型调优前的基准线(baseline),而有时候80%的准确率却是当前数据的预测巅峰结果(比赛Top 10)。所以对于上述结果的解读,即逻辑回归模型建模效果好或不好,可能需要进一步与其他模型进行横向对比来进行判断。
此外,需要注意的是,训练集和测试集的划分方式也会影响当前的输出结果,但建议是一旦划分完训练集和测试集后,就围绕当前建模结果进行优化,而不再考虑通过调整训练集和测试集的划分方式来影响最后输出结果,这么做也是毫无意义的。
2.逻辑回归的超参数调优
- 网格搜索优化
在模型训练完毕后,首先我们可以尝试一些基本优化方法——网格搜索,即超参数选择的调优,来尝试着对上述模型进行调优。网格搜索的过程并不一定能显著提高模型效果(当然其实也没有确定一定能提高模型效果的通用方法),但却是我们训练完模型后一定要做的基本优化流程,网格搜索能够帮我们确定一组最优超参数,并且随之附带的交叉验证的过程也能够让训练集上的模型得分更具有说服力。
from sklearn.model_selection import GridSearchCV
首先,逻辑回归评估器损失函数方程如下:
而逻辑回归评估器的所有参数解释如下:
而在这些所有超参数中,对模型结果影响较大的参数主要有两类,其一是正则化项的选择,同时也包括经验风险项的系数与损失求解方法选择,第二类则是迭代限制条件,主要是max_iter和tol两个参数,当然,在数据量较小、算力允许的情况下,我们也可以直接设置较大max_iter、同时设置较小tol数值。由于我们并未考虑带入数据本身的膨胀系数(共线性),因此此处我们优先考虑围绕经验风险系数与正则化选择类参数进行搜索与优化。(加入了正则化项的逻辑回归能够很好的规避掉自变量间的共线性)
而整个网格搜索过程其实就是一个将所有参数可能的取值一一组合,然后计算每一种组合下模型在给定评估指标下的交叉验证的结果(验证集上的平均值),作为该参数组合的得分,然后通过横向比较(比较不同参数组合的得分),来选定最优参数组合。要使用网格搜索,首先我们需要设置参数空间,也就是带入哪些参数的哪些取值进行搜索。需要注意的是,由于我们现在是直接选用机器学习流进行训练,此时逻辑回归的超参数的名称会发生变化,我们可以通过机器学习流的.get_param来获取集成在机器学习流中的逻辑回归参数名称:
# 检验列是否划分完全
assert len(category_cols) + len(numeric_cols) == X_train.shape[1]# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])# 实例化逻辑回归评估器
logistic_model = LogisticRegression(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)#logistic_pipe.get_params()
然后,我们选取正则化项、经验风险权重项C、弹性网正则化中l1正则化的比例项l1_ratio、以及求解器solver作为搜索超参数,来构建超参数空间:
logistic_param = [{'logisticregression__penalty': ['l1'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['saga']}, {'logisticregression__penalty': ['l2'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']}, {'logisticregression__penalty': ['elasticnet'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__l1_ratio': np.arange(0.1, 1.1, 0.1).tolist(), 'logisticregression__solver': ['saga']}
]
接下来执行网格搜索,在网格搜索评估器的使用过程中,只需要输入搜索的评估器(也就是机器学习流)和评估器的参数空间即可,当然若想提高运行速度,可以在n_jobs中输入调用进程数,一般保守情况数值可以设置为当前电脑核数。此外,由于我们目前是以准确率作为评估指标,因此在实例化评估器时无需设置评估指标参数。
# 实例化网格搜索评估器
logistic_search = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,n_jobs = 12)
import time
# 在训练集上进行训练
s = time.time()
logistic_search.fit(X_train, y_train)
print(time.time()-s, "s")
#378.3900156021118 s
此处可以考虑拆分特征重编码和模型训练过程,可加快搜索效率
接下来查看在网格搜索中验证集的准确率的均值:
logistic_search.best_score_
#0.8044624746450305
以及搜索出的最优超参数组合:
logistic_search.best_params_
#{'logisticregression__C': 0.1,
# 'logisticregression__penalty': 'l2',
# 'logisticregression__solver': 'lbfgs'}
能够发现,搜索出来的参数结果和默认参数相差不大(默认情况下C的取值是1.0,其他没有区别),因此预计在这组最优参数下模型预测结果和默认参数差不多。
# 调用最佳参数的机器学习流评估器
logistic_search.best_estimator_
# logistic_search.best_estimator_
# Pipeline(steps=[('columntransformer',
# ColumnTransformer(transformers=[('cat',
# OneHotEncoder(drop='if_binary'),
# ['gender', 'SeniorCitizen',
# 'Partner', 'Dependents',
# 'PhoneService',
# 'MultipleLines',
# 'InternetService',
# 'OnlineSecurity',
# 'OnlineBackup',
# 'DeviceProtection',
# 'TechSupport', 'StreamingTV',
# 'StreamingMovies',
# 'Contract',
# 'PaperlessBilling',
# 'PaymentMethod']),
# ('num', 'passthrough',
# ['tenure', 'MonthlyCharges',
# 'TotalCharges'])])),
# ('logisticregression',
# LogisticRegression(C=0.1, max_iter=100000000))])
# 计算预测结果
result_df(logistic_search.best_estimator_, X_train, y_train, X_test, y_test)
需要注意的是,这里的网格搜索结果的.best_score_和训练集上准确率并不一致。我们需要清楚这里不一致的原因,以及当二者不一致时我们应该更相信哪个值。
首先是不一致的原因,需要知道的是,.best_score_返回的是在网格搜索的交叉验证过程中(默认是五折验证)验证集上准确率的平均值,而最终我们看到的训练集上准确率评分只是模型在训练集上一次运行后的整体结果,二者计算过程不一致,最终结果也自然是不一样的。
另外,如果二者不一致的话我们更应该相信哪个值呢?首先无论相信哪个值,最终的目的都是通过已知数据集上的模型得分,去判断模型当前的泛化能力,也就是去估计一下在未知数据集上模型的表现,哪个预估的更准,我们就应该更相信哪个。而对于上述两个取值,很明显经过交叉验证后的验证集平均得分更能衡量模型泛化能力,这也就是为何我们经常会发现经过交叉验证后的.best_score_会和测试集的评分更加接近的原因。
当然,更进一步的说,只要我们采用网格搜索来选取超参数,就默认我们更“相信”交叉验证后的结果(毕竟我们是根据这个平均得分选取超参数)。也就是说,在超参数选取的过程中,我们并没有其他选项。
如果更深入的来探讨,实际上如何在训练集上获得一个更加可信的得分,其实是事关模型训练成败的关键。目前我们知道了交叉验证评分可信度>训练集上单次运行得分,而在Part 5时,我们还将更进一步寻找更加可信的得分,来训练泛化能力更强的模型。
- f1-Score搜索
此外,在使用网格搜索的过程中,我们也能够规定搜索方向,即可规定超参数的调优方向。在默认情况下,搜索的目的是提升模型准确率,但我们也可以对其进行修改,例如希望搜索的结果尽可能提升模型f1-Score,则可在网格搜索实例化过程中调整scoring超参数。具体scoring参数可选列表如下:
接下来执行模型训练与网格搜索过程:
# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])# 实例化逻辑回归评估器
logistic_model = LogisticRegression(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)# 设置超参数空间
logistic_param = [{'logisticregression__penalty': ['l1'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['saga']}, {'logisticregression__penalty': ['l2'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']}, {'logisticregression__penalty': ['elasticnet'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__l1_ratio': np.arange(0.1, 1.1, 0.1).tolist(), 'logisticregression__solver': ['saga']}
]# 实例化网格搜索评估器
logistic_search_f1 = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,scoring='f1',n_jobs = 12)s = time.time()
logistic_search_f1.fit(X_train, y_train)
print(time.time()-s, "s")
#384.4797456264496 s
logistic_search_f1.best_score_
#0.6030911241536703
以及搜索出的最优超参数组合:
logistic_search_f1.best_params_
#{'logisticregression__C': 1.4000000000000001,
# 'logisticregression__penalty': 'l2',
# 'logisticregression__solver': 'lbfgs'}
# 计算预测结果
result_df(logistic_search_f1.best_estimator_, X_train, y_train, X_test, y_test)
能够发现,搜索出来的参数结果和此前围绕准确率指标的搜索结果略有不同,但对于最终测试集上的f1-Score提升效果并不明显,稍后我们会进一步介绍通过调整逻辑回归阈值来快速提升f1-Score。
- 更多超参数
当然,根据此前介绍,我们知道,对于逻辑回归来说,连续变量是存在多种可选的处理方式的,例如我们可以对其进行分箱或者归一化处理,这些不同的处理方法或许能提升模型最终效果。这里我们可以将其视作超参数,并一并纳入网格搜索调参的范围,来选取一种最佳的对连续变量的处理方式。同时,由于我们发现,在多次搜索过程中都未出现弹性网正则化的搜索结果,因此暂时将弹性网正则化的超参数从搜索空间中移除,以确保后续搜索的效率。
logistic_pipe.get_params()# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])num_pre = ['passthrough', preprocessing.StandardScaler(), preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')]# 实例化逻辑回归评估器
logistic_model = LogisticRegression(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)# 设置超参数空间
logistic_param = [{'columntransformer__num':num_pre, 'logisticregression__penalty': ['l1'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['saga']}, {'columntransformer__num':num_pre, 'logisticregression__penalty': ['l2'], 'logisticregression__C': np.arange(0.1, 2.1, 0.1).tolist(), 'logisticregression__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']},
]# 实例化网格搜索评估器
logistic_search = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,n_jobs = 12)s = time.time()
logistic_search.fit(X_train, y_train)
print(time.time()-s, "s")
#86.78103947639465 slogistic_search.best_score_
#0.8044624746450305
logistic_search.best_params_
#{'columntransformer__num': 'passthrough',
# 'logisticregression__C': 0.1,
# 'logisticregression__penalty': 'l2',
# 'logisticregression__solver': 'lbfgs'}
# 计算预测结果
result_df(logistic_search.best_estimator_, X_train, y_train, X_test, y_test)
能够发现,根据交叉验证结果,模型仍然选择了最初搜索出来的参数组。
优化没效果,十之八九。
3.逻辑回归的进阶调优策略
尽管网格搜索是一种较为暴力的搜索最优参数的方法,但具体的搜索策略仍然举要结合当前模型与数据的实际情况来进行调整。例如对于逻辑回归来说,其实有个隐藏参数——类别判别阈值。通过对逻辑回归阈值的调整,能够直接影响最终的输出结果。此外,由于当前数据集存在一定程度样本不均衡问题,因此也可以通过调整class_weight参数来进行结果优化,不过需要注意的是,对于逻辑回归模型来说,很多时候阈值移动的效果是向下兼容class_weight的。
在sklearn中逻辑回归的判别阈值并不是超参数,因此如果需要对此进行搜索调优的话,我们可以考虑手动编写一个包装在逻辑回归评估器外层的评估器,并加入阈值这一超参数,然后再带入网格搜索流程;而class_weight的搜索调优就相对简单,该参数作为逻辑回归评估器的原生参数,只需要合理设置参数空间对其搜索即可。
3.1 阈值移动调优
- 自定义评估器
要手动编写sklearn评估器,需要先导入下述辅助构造sklearn评估器的包:
from sklearn.base import BaseEstimator, TransformerMixin
然后开始编写能够实现阈值移动的逻辑回归评估器:
LogisticRegression?class logit_threshold(BaseEstimator, TransformerMixin):def __init__(self, penalty='l2', C=1.0, max_iter=1e8, solver='lbfgs', l1_ratio=None, class_weight=None, thr=0.5):self.penalty = penaltyself.C = Cself.max_iter = max_iterself.solver = solverself.l1_ratio = l1_ratioself.thr = thrself.class_weight = class_weightdef fit(self, X, y):clf = LogisticRegression(penalty = self.penalty, C = self.C, solver = self.solver, l1_ratio = self.l1_ratio,class_weight=self.class_weight, max_iter=self.max_iter)clf.fit(X, y)self.coef_ = clf.coef_self.clf = clfreturn selfdef predict(self, X):res = (self.clf.predict_proba(X)[:, 1]>=self.thr) * 1return res
需要注意的是,上述评估器只继承了部分逻辑回归评估器的核心参数,并且由于没有设置.score方法,因此如果要使用网格搜索调参,需要明确规定scoring参数。当然,在定义完评估器后,我们可以通过下述过程进行手动验证评估器的有效性:
# 创建数据集
np.random.seed(24)
X = np.random.normal(0, 1, size=(1000, 2))
y = np.array(X[:,0]+X[:, 1]**2 < 1.5, int)clf = LogisticRegression()
clf.fit(X, y)
#LogisticRegression()
# 输出阈值为0.4时的模型预测结果
res1 = (clf.predict_proba(X)[:, 1] >= 0.4) * 1
# 实例化自定义评估器,设置阈值为0.4
clf_thr = logit_threshold(thr=0.4)
clf_thr.fit(X, y)
#logit_threshold(thr=0.4)
res2 = clf_thr.predict(X)
(res1 != res2).sum()
#0
能够发现该评估器能够实现阈值调整。接下来,我们尝试带入阈值参数到搜索过程中,需要注意的是,阈值搜索对于f1-Score和roc-auc提升效果明显,一般来说对于准确率提升效果一般,因此我们考虑直接依据f1-Score分数进行搜索,以测试阈值调整的实际效果。
- 阈值搜索
# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])num_pre = ['passthrough', preprocessing.StandardScaler(), preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')]# 实例化逻辑回归评估器
logistic_model = logit_threshold(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)# 设置超参数空间
logistic_param = [{'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l1'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['saga']}, {'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l2'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['lbfgs', 'newton-cg', 'sag', 'saga']},
]# 实例化网格搜索评估器
logistic_search_f1 = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,scoring='f1',n_jobs = 12)s = time.time()
logistic_search_f1.fit(X_train, y_train)
print(time.time()-s, "s")
#369.4163067340851 s
logistic_search_f1.best_score_
#0.6341624667020883
logistic_search_f1.best_params_
#{'columntransformer__num': 'passthrough',
# 'logit_threshold__C': 0.1,
# 'logit_threshold__penalty': 'l2',
# 'logit_threshold__solver': 'lbfgs',
# 'logit_threshold__thr': 0.30000000000000004}
# 计算预测结果
result_df(logistic_search_f1.best_estimator_, X_train, y_train, X_test, y_test)
能够发现,阈值移动对于模型的f1-Score提升效果显著,约提升了4%。
3.2 class_weight调优
接下来,我们进一步调整class_weight参数。根据此前数据探索的结果我们知道,数据集中标签的0/1分布比例大致为3:1左右,而class_weight的核心作用在于能够调整不同类别样本在计算损失值时的权重,一般来说,如果是3:1的样本比例,class_weight的参数取值基本可以设置在2:1-4:1之间,当然也可以直接考虑使用不同类别样本数量的反比,也就是balanced参数。
y = tcc['Churn']
print(f'Percentage of Churn: {round(y.value_counts(normalize=True)[1]*100,2)} % --> ({y.value_counts()[1]} customer)\nPercentage of customer did not churn: {round(y.value_counts(normalize=True)[0]*100,2)} % --> ({y.value_counts()[0]} customer)')
#Percentage of Churn: 26.54 % --> (1869 customer)
#Percentage of customer did not churn: 73.46 % --> (5174 customer)
增加class_weight后的模型训练和调优过程:
# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])num_pre = ['passthrough', preprocessing.StandardScaler(), preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')]# 实例化逻辑回归评估器
logistic_model = logit_threshold(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)# 设置超参数空间
cw_l = [None, 'balanced']
#cw_l.extend([{1: x} for x in np.arange(1, 4, 0.2)])
logistic_param = [{'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l1'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['saga'], 'logit_threshold__class_weight':cw_l}, {'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l2'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['lbfgs', 'newton-cg', 'sag', 'saga'], 'logit_threshold__class_weight':cw_l},
]# 实例化网格搜索评估器
logistic_search_f1 = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,scoring='f1',n_jobs = 12)s = time.time()
logistic_search_f1.fit(X_train, y_train)
print(time.time()-s, "s")
#737.595550775528 s
logistic_search_f1.best_score_
#0.6341624667020883
logistic_search_f1.best_params_
#{'columntransformer__num': 'passthrough',
# 'logit_threshold__C': 0.1,
# 'logit_threshold__class_weight': None,
# 'logit_threshold__penalty': 'l2',
# 'logit_threshold__solver': 'lbfgs',
# 'logit_threshold__thr': 0.30000000000000004}
# 计算预测结果
result_df(logistic_search_f1.best_estimator_, X_train, y_train, X_test, y_test)
能够发现,阈值移动的效果会兼容class_weight参数调整结果。最后我们再来尝试围绕准确率进行搜索时二者参数的效果:
# 设置转化器流
logistic_pre = ColumnTransformer([('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols), ('num', 'passthrough', numeric_cols)
])num_pre = ['passthrough', preprocessing.StandardScaler(), preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')]# 实例化逻辑回归评估器
logistic_model = logit_threshold(max_iter=int(1e8))# 设置机器学习流
logistic_pipe = make_pipeline(logistic_pre, logistic_model)# 设置超参数空间
cw_l = [None, 'balanced']
#cw_l.extend([{1: x} for x in np.arange(1, 4, 0.2)])
logistic_param = [{'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l1'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['saga'], 'logit_threshold__class_weight':cw_l}, {'columntransformer__num':num_pre, 'logit_threshold__thr': np.arange(0.1, 1, 0.1).tolist(), 'logit_threshold__penalty': ['l2'], 'logit_threshold__C': np.arange(0.1, 1.1, 0.1).tolist(), 'logit_threshold__solver': ['lbfgs', 'newton-cg', 'sag', 'saga'], 'logit_threshold__class_weight':cw_l},
]# 实例化网格搜索评估器
logistic_search = GridSearchCV(estimator = logistic_pipe,param_grid = logistic_param,scoring='accuracy',n_jobs = 12)s = time.time()
logistic_search.fit(X_train, y_train)
print(time.time()-s, "s")# 计算预测结果
result_df(logistic_search.best_estimator_, X_train, y_train, X_test, y_test)
#737.222731590271 s
logistic_search.best_params_
#{'columntransformer__num': 'passthrough',
# 'logit_threshold__C': 0.30000000000000004,
# 'logit_threshold__class_weight': None,
# 'logit_threshold__penalty': 'l2',
# 'logit_threshold__solver': 'lbfgs',
# 'logit_threshold__thr': 0.5}
能够发现,在样本相对均衡、且以准确率作为调参目标进行搜索时,阈值移动和样本权重调整并未能对模型有更好效果提升。
4.逻辑回归模型解释
对于逻辑回归的模型解释,核心是需要观察线性方程中自变量的系数,通过系数大小可以判断特征重要性,并且系数的具体数值也能表示因变量如何伴随自变量变化而变化。
- 逻辑回归方程系数查看
我们可以通过如下方式在一个训练好的网格搜索评估器中查看逻辑回归方程系数:
coe = logistic_search.best_estimator_.named_steps['logit_threshold'].coef_
coe = coe.flatten()
coe
#array([-3.64734497e-02, 2.97225123e-01, -6.96677065e-02, -2.11536083e-01,
# -3.59041883e-01, -3.16598442e-01, 1.96605064e-01, -4.24434409e-02,
# -3.93451080e-01, 3.65505701e-01, -1.34491440e-01, 1.88033447e-01,
# -1.34491440e-01, -2.15978826e-01, 1.48635094e-02, -1.34491440e-01,
# -4.28088885e-02, 3.09582859e-02, -1.34491440e-01, -5.89036649e-02,
# 1.53753023e-01, -1.34491440e-01, -1.81698402e-01, -1.31620667e-01,
# -1.34491440e-01, 1.03675288e-01, -1.14052749e-01, -1.34491440e-01,
# 8.61073700e-02, 5.32238806e-01, -1.63533154e-01, -5.31142471e-01,
# 3.89129562e-01, -9.62838286e-02, -1.82584235e-01, 2.82335761e-01,
# -1.65904515e-01, -6.68822855e-02, 1.00859554e-03, 3.84181240e-04])
而根据网格搜索评估器的结果,我们发现,上述数据输出结果实际上是经过了离散变量的多分类独热编码转化,因此我们可以借助此前定义的函数来生成具体特征含义:
# 我们带入的训练数据是DataFrame
# X_train
# 定位独热编码转化器
tf = logistic_search.best_estimator_.named_steps['columntransformer'].named_transformers_['cat']
tf
#OneHotEncoder(drop='if_binary')
# 转化后离散变量列名称
category_cols_new = cate_colName(tf, category_cols)# 所有字段名称
cols_new = category_cols_new + numeric_cols# 查看特征名称数量和特征系数数量是否一致
assert len(cols_new) == len(coe)
# 创建index是列名称,取值是自变量系数的Series
weights = pd.Series(coe, index=cols_new)
weights
# gender -0.036473
# SeniorCitizen 0.297225
# Partner -0.069668
# Dependents -0.211536
# PhoneService -0.359042
# MultipleLines_No -0.316598
# MultipleLines_No phone service 0.196605
# MultipleLines_Yes -0.042443
# InternetService_DSL -0.393451
# InternetService_Fiber optic 0.365506
# InternetService_No -0.134491
# OnlineSecurity_No 0.188033
# OnlineSecurity_No internet service -0.134491
# OnlineSecurity_Yes -0.215979
# OnlineBackup_No 0.014864
# OnlineBackup_No internet service -0.134491
# OnlineBackup_Yes -0.042809
# DeviceProtection_No 0.030958
# DeviceProtection_No internet service -0.134491
# DeviceProtection_Yes -0.058904
# TechSupport_No 0.153753
# TechSupport_No internet service -0.134491
# TechSupport_Yes -0.181698
# StreamingTV_No -0.131621
# StreamingTV_No internet service -0.134491
# StreamingTV_Yes 0.103675
# StreamingMovies_No -0.114053
# StreamingMovies_No internet service -0.134491
# StreamingMovies_Yes 0.086107
# Contract_Month-to-month 0.532239
# Contract_One year -0.163533
# Contract_Two year -0.531142
# PaperlessBilling 0.389130
# PaymentMethod_Bank transfer (automatic) -0.096284
# PaymentMethod_Credit card (automatic) -0.182584
# PaymentMethod_Electronic check 0.282336
# PaymentMethod_Mailed check -0.165905
# tenure -0.066882
# MonthlyCharges 0.001009
# TotalCharges 0.000384
# dtype: float64
然后可视化展示取值最大的10个自变量系数与取值最小的10个自变量系数:
plt.figure(figsize=(16, 6), dpi=200)# 挑选正相关的前10个变量
plt.subplot(121)
weights.sort_values(ascending = False)[:10].plot(kind='bar')# 挑选负相关的前10个变量
plt.subplot(122)
weights.sort_values(ascending = False)[-10:].plot(kind='bar')
能够发现,Contract出现Month-to-month时,用户流失可能性较大,而Contract出现Two year时,用户留存可能性较大。当然,相比此前相关系数柱状图,上述根据模型生成的自变量系数可视化的结果会更加可靠一些,并且每个变量系数的取值也有对应的可解释的具体含义
- 自变量系数解释
接下来我们来进行逻辑回归模型方程系数解释。假设现在训练出来的逻辑回归线性方程为1-x,即逻辑回归方程模型如下:y=11+e−(1−x)y = \frac{1}{1+e^{-(1-x)}} y=1+e−(1−x)1据此可以进一步推导出:lny1−y=1−xln\frac{y}{1-y} = 1-xln1−yy=1−x
此时,自变量x的系数为-1,据此可以解读为x每增加1,样本属于1的概率的对数几率就减少1。
而这种基于自变量系数的可解释性不仅可以用于自变量和因变量之间的解释,还可用于自变量重要性的判别当中,例如,假设逻辑回归方程如下:lny1−y=x1+2×2−1ln\frac{y}{1-y} = x_1+2x_2-1ln1−yy=x1+2x2−1则可解读为x2x_2x2的重要性是x1x_1x1的两倍,x2x_2x2每增加1的效果(令样本为1的概率的增加)是x1x_1x1增加1效果的两倍。
据此,上述建模结果系数中,我们可以有如下解释,例如:对于Contract字段来说,Month-to-month出现时会让用户流失的对数几率增加50%(概率增加约10%),而Two year出现时会让用户流失的对数几率减少50%(概率减少约10%),其他变量也可参照该方式进行解释。
并且根据上述结果,我们不难看出,'Contract_Month-to-month’的系数是’SeniorCitizen’的两倍
weights['Contract_Month-to-month'], weights['SeniorCitizen']
#(0.5322388059587707, 0.29722512309644633)
该结果说明,在导致用户流失的因素中,'Contract_Month-to-month’的影响是’SeniorCitizen’的两倍,或用户签订协约时出现月付行为所造成的用户流失风险,是用户是老年人导致的风险的两倍。
此外,需要注意的是,在上述建模过程中,我们发现连续变量的系数普遍较小:
weights[numeric_cols]
#tenure -0.066882
#MonthlyCharges 0.001009
#TotalCharges 0.000384
#dtype: float64
其根本原因在于自变量取值范围较大,而逻辑回归方程系数实际上是在衡量自变量每增加1、因变量的对数几率变化情况,因此对于取值较大的连续变量来说,最终的系数结果较小。若想更加准确的和离散变量作比较,此处可以考虑将连续变量离散化,然后再计算离散化后的特征系数,并使用该系数和原离散变量系数进行比较,二者会有更好的可比性。
5.逻辑回归建模总结
接下来,对逻辑回归建模过程以及模型使用技巧进行总结。
- 模型性能评估
通过上述尝试,我们基本能判断逻辑回归模型在当前数据集的性能,准确率约在80%左右,准确率没有太大的超参数调优搜索空间,而f1-Score则在我们额外设置的超参数——阈值上能够有更好的搜索结果。
- 超参数搜索策略总结
sklearn中的逻辑回归超参数众多,在算力允许的情况下,建议尽量设置更多的迭代次数(max_iter)和更小的收敛条件(tol),基本的搜索参数为正则化项(penalty)+经验风险系数(C)+求解器(solver),如果算力允许,可以纳入弹性网正则化项进行搜索,并搜索l1正则化项权重系数(l1_ratio)。若样本存在样本不均衡,可带入class_weight进行搜索,若搜索目标是提升f1-Score或ROC-AUC,则可通过自定义评估器进行阈值移动,若希望进行更加精确的搜索,可以纳入连续变量的编码方式进行搜索。
- 阈值移动与样本权重调优总结
根据上面的实验结果,对于阈值调优和样本权重调优可以进行如下总结:
(1)阈值移动往往出现在f1-Score调优或ROC-AUC调优的场景中,由于阈值移动对召回率、精确度等指标调整效果显著,因此该参数的搜索往往效果要好于逻辑回归其他默认参数,类似的情况也出现在其他能够输出概率结果的模型中(如决策树、随机森林等);
(2)样本权重调节往往出现在非平衡类数据集的建模场景中,通过该参数的设置,能够让模型在训练过程中更加关注少数类样本,从而一定程度起到平衡数据集不同类别样本的目的,并且相比于其他平衡样本方法(例如过采样、欠采样、SMOTEENN等),该方法能够更好的避免过拟合,并且该参数同样也是一个通用参数,出现在sklearn集成的诸多模型中。建议如果算力允许,可以在任何指标调整过程中对该参数进行搜索;
(3)不过如果是围绕f1-Score或ROC-AUC进行调优,阈值移动和样本权重调节会有功能上的重复,此时建议优先选用阈值进行搜索。
- 结果解读
对于逻辑回归来说,模型可解释性的核心在于模型是线性方程,据此我们可以根据线性方程中自变量的系数对其进行结果解读,包括自变量变化如何影响因变量,以及自变量之间的相对关系等。
四、决策树模型训练与结果解释
1.决策树模型训练
接下来,继续测试决策树模型。我们知道,由于决策树的分类边界更加灵活,相比只能进行线性边界划分的逻辑回归来来说,大多数情况下都能取得一个更好的预测结果。当然,对于决策树来说,由于并没有类似线性方程的数值解释,因此无需对分类变量进行独热编码转化,直接进行自然数转化即可
- 默认参数模型训练
# 导入决策树评估器
from sklearn.tree import DecisionTreeClassifier# 设置转化器流
tree_pre = ColumnTransformer([('cat', preprocessing.OrdinalEncoder(), category_cols), ('num', 'passthrough', numeric_cols)
])# 实例化决策树评估器
tree_model = DecisionTreeClassifier()# 设置机器学习流
tree_pipe = make_pipeline(tree_pre, tree_model)# 模型训练
tree_pipe.fit(X_train, y_train)# 计算预测结果
result_df(tree_pipe, X_train, y_train, X_test, y_test)
能够发现,模型严重过拟合,即在训练集上表现较好,但在测试集上表现一般。此时可以考虑进行网格搜索,通过交叉验证来降低模型结构风险。
2.决策树模型优化
决策树模型的参数解释如下:
一般来说,我们可以考虑树模型生长相关的参数来构造参数空间,当然,在新版sklearn中还加入了ccp_alpha参数,该参数是决策树的结构风险系数,作用和逻辑回归中C的作用类似,但二者取值正好相反(ccp_alpha是结构风险系数,而C是经验风险系数)。此处我们选取max_depth、min_samples_split、min_samples_leaf、max_leaf_nodes和ccp_alpha进行搜索:
Rα(T)=R(T)+α∣T~∣R_\alpha(T) = R(T) + \alpha|\widetilde{T}| Rα(T)=R(T)+α∣T∣
# 设置转化器流
tree_pre = ColumnTransformer([('cat', preprocessing.OrdinalEncoder(), category_cols), ('num', 'passthrough', numeric_cols)
])# 实例化决策树评估器
tree_model = DecisionTreeClassifier()# 设置机器学习流
tree_pipe = make_pipeline(tree_pre, tree_model)
# tree_pipe.get_params()
# 构造包含阈值的参数空间
tree_param = {'decisiontreeclassifier__ccp_alpha': np.arange(0, 1, 0.1).tolist(),'decisiontreeclassifier__max_depth': np.arange(2, 8, 1).tolist(), 'decisiontreeclassifier__min_samples_split': np.arange(2, 5, 1).tolist(), 'decisiontreeclassifier__min_samples_leaf': np.arange(1, 4, 1).tolist(), 'decisiontreeclassifier__max_leaf_nodes':np.arange(6,10, 1).tolist()}
# 实例化网格搜索评估器
tree_search = GridSearchCV(estimator = tree_pipe,param_grid = tree_param,n_jobs = 12)
# 在训练集上进行训练
s = time.time()
tree_search.fit(X_train, y_train)
print(time.time()-s, "s")
#30.371087312698364 s
能够发现决策树的训练效率要比逻辑回归高很多,接下来查看搜索结果:
# 查看验证集准确率均值
tree_search.best_score_
#0.79026369168357
# 查看最优参数组
tree_search.best_params_
#{'decisiontreeclassifier__ccp_alpha': 0.0,
# 'decisiontreeclassifier__max_depth': 5,
# 'decisiontreeclassifier__max_leaf_nodes': 8,
# 'decisiontreeclassifier__min_samples_leaf': 1,
# 'decisiontreeclassifier__min_samples_split': 2}
能够发现,决策树的最优参数都在设置的范围内。这里需要注意的是,如果某些参数的最优取值达到搜索空间的边界,则需要进一步扩大该参数的搜索范围。接下来查看经过网格搜索后的模型预测结果:
# 计算预测结果
result_df(tree_search.best_estimator_, X_train, y_train, X_test, y_test)
能够发现,经过网格搜索和交叉验证后,决策树的过拟合问题已经的到解决,并且最终预测结果与逻辑回归类似。
需要知道的是,在大多数情况下,决策树的判别效力实际上是要强于逻辑回归(逻辑回归只能构建线性决策边界,而决策树可以构建折线决策边界),而此处决策树表现出了和逻辑回归类似的判别效力,则说明该数据集本身建模难度较大,极有可能是一个“上手容易、精通极难”的数据集。
在后面的建模过程中我们会陆续发现,诸多大杀四方的集成模型(XGB、LightGBM、CatBoost)在初始状态下也只能跑到80%准确率。
3.决策树模型解释
接下来进行决策树模型的模型解释,相比逻辑回归,树模型的模型解释会相对来说简单一些。树模型的模型解释主要有两点,其一是根据树模型的.feature_importances_属性来查看各个特征的重要性:
fi = tree_search.best_estimator_.named_steps['decisiontreeclassifier'].feature_importances_
fi
#array([0. , 0. , 0. , 0. , 0. ,
# 0. , 0.11262871, 0.15756862, 0. , 0. ,
# 0. , 0. , 0. , 0.5955054 , 0. ,
# 0. , 0.10555534, 0.02874193, 0. ])
需要注意的是,特征重要性为0表示该列特征并未在树模型生长过程中提供分支依据。
类似的,我们也可以以列名作为index、以特征重要性值作为数值,构建Series:
col_names = category_cols + numeric_cols
col_names
# ['gender',
# 'SeniorCitizen',
# 'Partner',
# 'Dependents',
# 'PhoneService',
# 'MultipleLines',
# 'InternetService',
# 'OnlineSecurity',
# 'OnlineBackup',
# 'DeviceProtection',
# 'TechSupport',
# 'StreamingTV',
# 'StreamingMovies',
# 'Contract',
# 'PaperlessBilling',
# 'PaymentMethod',
# 'tenure',
# 'MonthlyCharges',
# 'TotalCharges']
feature_importances = pd.Series(fi, index=col_names)
feature_importances
#gender 0.000000
#SeniorCitizen 0.000000
#Partner 0.000000
#Dependents 0.000000
#PhoneService 0.000000
#MultipleLines 0.000000
#InternetService 0.112629
#OnlineSecurity 0.157569
#OnlineBackup 0.000000
#DeviceProtection 0.000000
#TechSupport 0.000000
#StreamingTV 0.000000
#StreamingMovies 0.000000
#Contract 0.595505
#PaperlessBilling 0.000000
#PaymentMethod 0.000000
#tenure 0.105555
#MonthlyCharges 0.028742
#TotalCharges 0.000000
#dtype: float64
然后查看5个重要性不为0的特征的对比分布:
feature_importances.sort_values(ascending = False)[:5].plot(kind='bar')
当然,树模型的特征重要性只能给与一个直观的特征是否重要的感受,并不能像逻辑回归一样可以解释其数值的具体含义。此外需要注意的是,树模型的特征重要性的数值计算过程存在一定的随机性,也就是多次运行可能得到多组结果不同的特征重要性计算结果。因此很多时候树模型的特征重要性只能作为最终模型解释的参考。
若要精准的计算于解释特征重要性,可以考虑使用SHAP方法。此外,目前尚未接触到的一类特征重要性的用途是借助进行特征筛选,下一部分在讨论特征筛选时会具体讲解。
除了特征重要性以外,树模型还提供了非常直观的样本分类规则,并以树状图形式进行呈现。我们可以借助sklearn中数模块内的plot_tree函数来绘制树状图,并据此进一步提取有效的样本分类规则:
plt.figure(figsize=(16, 6), dpi=200)
tree.plot_tree(tree_search.best_estimator_.named_steps['decisiontreeclassifier'])
提取分类规则的过程是从叶节点往上进行提取,叶节点中的基尼系数代表当前数据集中样本标签的不纯度,越是有效的分类规则,对应的基尼系数越小,而样本数量越多,则说明该规则越有“普适性”。例如我们可以提取右侧分支的第三层左侧叶节点,该节点对应的分类规则可以解释为:X[13]不满足小于等于0.5时、且X[17]小于等于93.675时,在总共1674条样本中,只有58条样本是流失用户,约占比3%,说明满足该规则的用户大多都不会流失。
58/1674
#0.03464755077658303
当然我们这里可以进一步查看X[13]和X[17]所代表的特征:
cat_rules = tree_search.best_estimator_.named_steps['columntransformer'].named_transformers_['cat'].categories_
cat_rules
#[array(['Female', 'Male'], dtype=object),
# array([0, 1], dtype=int64),
# array(['No', 'Yes'], dtype=object),
# array(['No', 'Yes'], dtype=object),
# array(['No', 'Yes'], dtype=object),
# array(['No', 'No phone service', 'Yes'], dtype=object),
# array(['DSL', 'Fiber optic', 'No'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['No', 'No internet service', 'Yes'], dtype=object),
# array(['Month-to-month', 'One year', 'Two year'], dtype=object),
# array(['No', 'Yes'], dtype=object),
# array(['Bank transfer (automatic)', 'Credit card (automatic)',
# 'Electronic check', 'Mailed check'], dtype=object)]
col_names[13], cat_rules[13]
#('Contract', array(['Month-to-month', 'One year', 'Two year'], dtype=object))
col_names[17]
#'MonthlyCharges'
也就是说,大多数非按月付费用户、并且月消费金额小于92.5的用户留存率较大。此外还有类似在按月付费用户中,未购买OnlineSecurity(X[7]<=0.5)、且入网不足8个月(X[16]<=7.5)、且购买了InternetService的用户,流失用户数量是留存用户数量的3倍。其他规则我们也可以通过树状图、按照类似方法进行提取,当然,树的层数越多,相关规则的说明就越复杂,同时也就越不利于进行解释。
(col_names[7], col_names[16], col_names[6])
#('OnlineSecurity', 'tenure', 'InternetService')
当然,借助这些规则,我们其实可以进一步衍生一些业务指标(即衍生一些新的特征),例如付费形式和付费金额的交叉组合、购买服务和入网时间的交叉组合等。更多特征衍生策略,我们将在下一小节重点讲解。
至此,我们就完成了树模型的结果解释。