介绍
除了离散变量的重编码外,有的时候我们也需要对连续变量进行转化,以提升模型表现或模型训练效率。在之前的内容中我们曾介绍了关于连续变量标准化和归一化的相关内容,对连续变量而言,标准化可以消除量纲影响并且加快梯度下降的迭代效率,而归一化则能够对每条数据进行进行范数单位化处理,我们可以通过下面的内容进行标准化和归一化相关内容回顾。
标准化与归一化
从功能上划分,sklearn中的归一化其实是分为标准化(Standardization)和归一化(Normalization)两类。其中,此前所介绍的Z-Score标准化和0-1标准化,都属于Standardization的范畴,而在sklearn中,Normalization则特指针对单个样本(一行数据)利用其范数进行放缩的过程。不过二者都属于数据预处理范畴,都在sklearn中的Preprocessing data模块下。
需要注意的是,此前我们介绍数据归一化时有讨论过标准化和归一化名称上的区别,在大多数场景下其实我们并不会对其进行特意的区分,但sklearn中标准化和归一化则各指代一类数据处理方法,此处需要注意。
标准化 Standardization
sklearn的标准化过程,即包括Z-Score标准化,也包括0-1标准化,并且即可以通过实用函数来进行标准化处理,同时也可以利用评估器来执行标准化过程。接下来我们分不同功能以的不同实现形式来进行讨论:
实用函数进行标准化处理,尽管从代码实现角度来看清晰易懂,但却不适用于许多实际的机器学习建模场景。其一是因为在进行数据集的训练集和测试集切分后,我们首先要在训练集进行标准化、然后统计训练集上统计均值和方差再对测试集进行标准化处理,因此其实还需要一个统计训练集相关统计量的过程;其二则是因为相比实用函数,sklearn中的评估器其实会有一个非常便捷的串联的功能,sklearn中提供了Pipeline工具能够对多个评估器进行串联进而组成一个机器学习流,从而简化模型在重复调用时候所需代码量,因此通过评估器的方法进行数据标准化,其实是一种更加通用的选择。
首先是评估器导入:
from sklearn.preprocessing import StandardScaler
然后是查阅评估器参数,然后进行评估器的实例化:
# 查阅参数
StandardScaler?
...............
# 创建
scaler = StandardScaler()
然后导入数据,进行训练,此处也是使用fit函数进行训练:
X = np.arange(15).reshape(5, 3)
X_train, X_test = train_test_split(X)
X_train, X_test
#
(array([[ 9, 10, 11],
[ 6, 7, 8],
[ 0, 1, 2]]),
array([[12, 13, 14],
[ 3, 4, 5]]))
scaler.fit(X_train)
虽然同样是输入数据,但标准化的评估器和训练模型的评估器实际上是不同的计算过程。此前我们介绍的线性方程的评估器,输入数据进行训练的过程(fit过程)实际上是计算线性方程的参数,而此处标准化的评估器的训练结果实际上是对输入数据的相关统计量进行了汇总计算,也就是计算了输入数据的均值、标准差等统计量,后续将用这些统计量对各数据进行标准化计算。不过无论计算过程是否相同,评估器最终计算结果都可以通过相关属性进行调用和查看:
# 查看训练数据各列的标准差
scaler.scale_
# array([3.74165739, 3.74165739, 3.74165739])
# 查看训练数据各列的均值
scaler.mean_
# array([5., 6., 7.])
# 查看训练数据各列的方差
scaler.var_
# array([14., 14., 14.])
当然,截止目前,我们只保留了训练数据的统计量,但尚未对任何数据进行修改,输入的训练数据也是如此
接下来,我们可以通过评估器中的transform方法来进行数据标准化处理。注意,算法模型的评估器是利用predict方法进行数值预测,而标准化评估器则是利用transform方法进行数据的数值转化。
# 利用训练集的均值和方差对训练集进行标准化处理
scaler.transform(X_train)
# 效果
array([[ 1.06904497, 1.06904497, 1.06904497],
[ 0.26726124, 0.26726124, 0.26726124],
[-1.33630621, -1.33630621, -1.33630621]])
再利用训练集的均值和方差对测试集进行标准化处理:
scaler.transform(X_test)
#
array([[ 1.87082869, 1.87082869, 1.87082869],
[-0.53452248, -0.53452248, -0.53452248]])
z_score(X_train)
#
array([[ 1.06904497, 1.06904497, 1.06904497],
[ 0.26726124, 0.26726124, 0.26726124],
[-1.33630621, -1.33630621, -1.33630621]])
此外,我们还可以使用fit_transform对输入数据进行直接转化:
# 一步执行在X_train上fit和transfrom两个操作
scaler.fit_transform(X_train)
接下来,我们就能直接带入标准化后的数据进行建模了。
类似的,我们可以调用评估器进行0-1标准化。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit_transform(X)
scaler.data_min_
#
array([0., 1., 2.])
scaler.data_max_
#
array([12., 13., 14.])
此外,sklearn中还有针对稀疏矩阵的标准化(MaxAbsScaler)、针对存在异常值点特征矩阵的标准化(RobustScaler)、以及非线性变化的标准化(Non-linear transformation)等方法,相关内容待后续进行介绍。
归一化 Normalization
和标准化不同,sklearn中的归一化特指将单个样本(一行数据)放缩为单位范数(1范数或者2范数为单位范数)的过程,该操作常见于核方法或者衡量样本之间相似性的过程中。这些内容此前我们并未进行介绍,但出于为后续内容做铺垫的考虑,此处先介绍关于归一化的相关方法。同样,归一化也有函数实现和评估器实现两种方法。
- 归一化目的是将数据按比例缩放,使之落入一个小的特定区间,通常是[0, 1]或[-1, 1]。
- 将数据按其特征的均值和标准差进行缩放,使得特征符合标准正态分布(均值为0,标准差为1)。其主要目的是消除特征之间的量纲影响,使模型更加稳定和收敛更快。
from sklearn.preprocessing import Normalizer
normlize = Normalizer()
normlize.fit_transform(X)
normlize = Normalizer(norm='l1')
normlize.fit_transform(X)
- 在使用
Normalizer(norm='l1')
时,每个样本(即数据集中的每一行或每一列,取决于你的具体应用场景)都将被缩放到其L1范数为1。对于每个样本,其所有特征值的绝对值之和将被调整为1,而不改变它们之间的相对比例。这种归一化方式特别适用于那些特征值可能具有不同量纲或范围的数据集。 - 使用
Normalizer(norm='l2')
时,每个样本都将被缩放到其L2范数为1,对于每个样本,其所有特征值的平方和的平方根将被调整为1,这同样不会改变特征之间的相对比例。
连续变量特征转化的ColumnTransformer集成
上述所介绍的关于连续变量的标准化或分箱等过程,也是可以集成到ColumnTransformer中的。例如,如果同时执行离散字段的多分类独热编码和连续字段的标准化,则可以创建如下转化流:
ColumnTransformer([
('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols),
('num', preprocessing.StandardScaler(), numeric_cols)
])
类似的,如果需要同时对离散变量进行多分类独热编码、对连续字段进行基于kmeans的三分箱,则可以创建如下转化流:
ColumnTransformer([
('cat', preprocessing.OneHotEncoder(drop='if_binary'), category_cols),
('num', preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans'), numeric_cols)
])
逻辑回归模型训练与结果解释
在完成数据重编码后,接下来即可进行模型训练了,此处我们首先考虑构建可解释性较强的逻辑回归与决策树模型,并围绕最终模型输出结果进行结果解读。
设置评估指标与测试集
当然,在模型训练开始前,我们需要设置模型结果评估指标。此处由于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就是训练数据集,同时包含训练集的特征和标签。
逻辑回归模型训练
首先我们测试逻辑回归的建模效果。逻辑回归作为线性方程,连续变量和离散变量的数据解释是不同的,连续变量表示数值每变化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', preproc4essing.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)
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)。所以对于上述结果的解读,即逻辑回归模型建模效果好或不好,可能需要进一步与其他模型进行横向对比来进行判断。
此外,需要注意的是,训练集和测试集的划分方式也会影响当前的输出结果,但建议是一旦划分完训练集和测试集后,就围绕当前建模结果进行优化,而不再考虑通过调整训练集和测试集的划分方式来影响最后输出结果,这么做也是毫无意义的。
逻辑回归的超参数调优
在模型训练完毕后,首先我们可以尝试一些基本优化方法——网格搜索,即超参数选择的调优,来尝试着对上述模型进行调优。网格搜索的过程并不一定能显著提高模型效果(当然其实也没有确定一定能提高模型效果的通用方法),但却是我们训练完模型后一定要做的基本优化流程,网格搜索能够帮我们确定一组最优超参数,并且随之附带的交叉验证的过程也能够让训练集上的模型得分更具有说服力。
from sklearn.model_selection import GridSearchCV
在逻辑回归中,通常使用交叉熵损失(Cross-Entropy Loss)函数或称为对数损失(Log Loss)函数来评估模型的性能。
逻辑回归评估器的所有参数解释如下:
参数 | 解释 |
---|---|
penalty | 正则化项 |
dual | 是否求解对偶问题* |
tol | 迭代停止条件:两轮迭代损失值差值小于tol时,停止迭代 |
C | 经验风险和结构风险在损失函数中的权重 |
fit_intercept | 线性方程中是否包含截距项 |
intercept_scaling | 相当于此前讨论的特征最后一列全为1的列,当使用liblinear求解参数时用于捕获截距 |
class_weight | 各类样本权重* |
random_state | 随机数种子 |
solver | 损失函数求解方法* |
max_iter | 求解参数时最大迭代次数,迭代过程满足max_iter或tol其一即停止迭代 |
multi_class | 多分类问题时求解方法* |
verbose | 是否输出任务进程 |
warm_start | 是否使用上次训练结果作为本次运行初始参数 |
l1_ratio | 当采用弹性网正则化时,L1正则项权重,就是损失函数中的p |
而在这些所有超参数中,对模型结果影响较大的参数主要有两类,其一是正则化项的选择,同时也包括经验风险项的系数与损失求解方法选择,第二类则是迭代限制条件,主要是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)
# 在训练集上进行训练
s = time.time()
logistic_search.fit(X_train, y_train)
print(time.time()-s, "s")
此处可以考虑拆分特征重编码和模型训练过程,可加快搜索效率
接下来查看在网格搜索中验证集的准确率的均值:
logistic_search.best_score_
# 0.8044624746450305
以及搜索出的最优超参数组合:
logistic_search.best_params_
#
{'logisticregression__C': 0.1,
'logisticregression__penalty': 'l2',
'logisticregression__solver': 'lbfgs'}
能够发现,搜索出来的参数结果和默认参数相差不大(默认情况下C的取值是1.0,其他没有区别),因此预计在这组最优参数下模型预测结果和默认参数差不多。
# 计算预测结果
result_df(logistic_search.best_estimator_, X_train, y_train, X_test, y_test)
需要注意的是,这里的网格搜索结果的.best_score_和训练集上准确率并不一致。我们需要清楚这里不一致的原因,以及当二者不一致时我们应该更相信哪个值。
- 首先是不一致的原因,需要知道的是,.best_score_返回的是在网格搜索的交叉验证过程中(默认是五折验证)验证集上准确率的平均值,而最终我们看到的训练集上准确率评分只是模型在训练集上一次运行后的整体结果,二者计算过程不一致,最终结果也自然是不一样的。
- 另外,如果二者不一致的话我们更应该相信哪个值呢?首先无论相信哪个值,最终的目的都是通过已知数据集上的模型得分,去判断模型当前的泛化能力,也就是去估计一下在未知数据集上模型的表现,哪个预估的更准,我们就应该更相信哪个。而对于上述两个取值,很明显经过交叉验证后的验证集平均得分更能衡量模型泛化能力,这也就是为何我们经常会发现经过交叉验证后的.best_score_会和测试集的评分更加接近的原因。
决策树模型优化
决策树模型的参数解释如下:
Name | Description |
---|---|
criterion | 规则评估指标或损失函数,默认基尼系数,可选信息熵 |
splitter | 树模型生长方式,默认以损失函数取值减少最快方式生长,可选随机根据某条件进行划分 |
max_depth | 树的最大生长深度,类似max_iter,即总共迭代几次 |
min_samples_split | 内部节点再划分所需最小样本数 |
min_samples_leaf | 叶节点包含最少样本数 |
min_weight_fraction_leaf | 叶节点所需最小权重和 |
max_features | 在进行切分时候最多带入多少个特征进行划分规则挑选 |
random_state | 随机数种子 |
max_leaf_nodes | 叶节点最大个数 |
min_impurity_decrease | 数据集再划分至少需要降低的损失值 |
min_impurity_split | 数据集再划分所需最低不纯度,将在0.25版本中移除 |
class_weight | 各类样本权重 |
presort | 已在0.24版本中移除 |
ccp_alpha | 在执行CART树原生原理中的剪枝流程时结构复杂度惩罚因子的系数,默认情况下不使用该方法进行剪枝 |
一般来说,我们可以考虑树模型生长相关的参数来构造参数空间,当然,在新版sklearn中还加入了ccp_alpha参数,该参数是决策树的结构风险系数,作用和逻辑回归中C的作用类似,但二者取值正好相反(ccp_alpha是结构风险系数,而C是经验风险系数)。此处我们选取max_depth、min_samples_split、min_samples_leaf、max_leaf_nodes和ccp_alpha进行搜索:
#%%
# 设置转化器流
tree_pre = ColumnTransformer([
('cat', preprocessing.OrdinalEncoder(), category_cols),
('num', 'passthrough', numeric_cols)
])
# 实例化决策树评估器
tree_model = DecisionTreeClassifier()
# 设置机器学习流
tree_pipe = make_pipeline(tree_pre, tree_model)
# 构造包含阈值的参数空间
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")
能够发现决策树的训练效率要比逻辑回归高很多,接下来查看搜索结果:
# 查看验证集准确率均值
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%准确率。