建模调参流程与逻辑回归
对于二分类问题,逻辑回归是一个可以优先尝试的模型,不仅可以作为一个基准模型,同时因其广义线性模型的特性,拥有良好的可解释性。
NOTE
本文不赘述逻辑回归的数学原理,重点介绍实战过程,以及模型调优的真实路径。
Version 1: Ordinal Encoding
首先我们从最原始的数据集开始尝试,由于模型只能接受数值型特征,我们需要对分类变量进行编码。这里通过object
类型判断分类变量,并选用OrdinalEncoder
对分类变量进行编码。
X_train = train_df.drop('Attrition', axis=1)
y_train = train_df['Attrition']
X_test = test_df
# 识别分类变量和数值变量
categorical_cols = X_train.select_dtypes(include=['object']).columns.tolist()
numerical_cols = X_train.select_dtypes(exclude=['object']).columns.tolist()
# 创建预处理管道
preprocessor = ColumnTransformer(
transformers=[
('num', 'passthrough', numerical_cols),
('cat', OrdinalEncoder(), categorical_cols)
])
# 创建逻辑回归模型
logreg = LogisticRegression(max_iter=10000000)
# 创建包含预处理和模型的管道
clf = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', logreg)
])
# 定义超参数网格
param_grid = [
{'classifier__C': [0.01, 0.1, 1, 10], 'classifier__penalty': ['l2'], 'classifier__solver': ['liblinear', 'lbfgs', 'sag', 'saga']},
{'classifier__C': [0.01, 0.1, 1, 10], 'classifier__penalty': ['l1'], 'classifier__solver': ['liblinear', 'saga']}
]
# 创建 GridSearchCV 对象
grid_search = GridSearchCV(clf, param_grid, cv=5, scoring='roc_auc', n_jobs=-1)
# 训练模型
grid_search.fit(X_train, y_train)
# 输出最佳参数
best_params = grid_search.best_params_
best_score = grid_search.best_score_
print("最佳参数:", best_params)
print("最佳交叉验证得分:\nROC AUC:", best_score)
训练结果
Output
最佳参数: {'classifier__C': 10, 'classifier__penalty': 'l1', 'classifier__solver': 'liblinear'}
最佳交叉验证得分:
ROC AUC: 0.8001219651855245
TIP
当我们仅有训练集时,评估模型效果的优劣,一般来说会采用交叉验证的平均得分作为评价标准。而不会直接使用模型在训练集上的得分,防止过拟合。
当然,如果希望看一下模型在训练集上的表现,可以使用以下代码:
# 使用最佳模型进行预测
best_clf = grid_search.best_estimator_
y_pred_train = best_clf.predict(X_train)
y_pred_train_proba = best_clf.predict_proba(X_train)[:, 1] # 获取训练集的预测概率
# 计算并输出训练集的评估结果
roc_auc = roc_auc_score(y_train, y_pred_train_proba)
print("训练集评估结果:")
print(f"ROC AUC: {roc_auc}")
print("分类报告:")
print(classification_report(y_train, y_pred_train))
print("混淆矩阵:")
print(confusion_matrix(y_train, y_pred_train))
Output
训练集评估结果:
ROC AUC: 0.8365030467163168
分类报告:
precision recall f1-score support
0 0.90 0.99 0.94 1477
1 0.67 0.20 0.30 200
accuracy 0.89 1677
macro avg 0.79 0.59 0.62 1677
weighted avg 0.87 0.89 0.87 1677
混淆矩阵:
[[1458 19]
[ 161 39]]
后续的实验中,我们将仅关注交叉验证的结果。
Version 2: One-Hot Encoding
在上一版本中,我们使用了OrdinalEncoder
对分类变量进行编码,对于逻辑回归模型来说,可能会将编码当做一种有序数据,而实际上分类变量之间并没有这种关系。
另一种可以尝试的编码方式是OneHotEncoder
,它会将每个分类变量的每个类别都转换为一个新的特征,这样可以避免有序性的问题。
python
# ...
# 创建预处理管道
preprocessor = ColumnTransformer(
transformers=[
('num', 'passthrough', numerical_cols),
- ('cat', OrdinalEncoder(), categorical_cols)
+ ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols)
])
# 其余代码不变
# ...
Output
最佳参数: {'classifier__C': 10, 'classifier__penalty': 'l2', 'classifier__solver': 'lbfgs'}
最佳交叉验证得分:
ROC AUC: 0.8137899679340357
可以看到,使用OneHotEncoder
编码后,模型的交叉验证得分略有提升。我们将历次实验的建模结果记录到下表中,以便后续对比。
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3: StandardScaler
除了对分类变量进行编码,我们还可以对数值型特征进行标准化处理,这样可以使模型更快地收敛,提高模型的稳定性。
python
# ...
# 创建预处理管道
preprocessor = ColumnTransformer(
transformers=[
- ('num', 'passthrough', numerical_cols),
+ ('num', StandardScaler(), numerical_cols),
('cat', OneHotEncoder(), categorical_cols)
])
# 其余代码不变
# ...
Output
最佳参数: {'classifier__C': 0.1, 'classifier__penalty': 'l2', 'classifier__solver': 'sag'}
最佳交叉验证得分:
ROC AUC: 0.8171719537333944
在增加了 StandardScaler
的预处理步骤后,模型的交叉验证得分进一步提升。
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3 | StandardScaler | 0.81717 |
Version 4: 自定义分类变量和数值变量
上述实验中,我们均通过字段类型是否为object
来判断分类变量和数值变量,但实际上有些存储为int
类型的字段也是分类变量,比如Education
等,这时我们可以自定义分类变量和数值变量,看一下是否能进一步提升模型效果。
# 自定义分类变量和数值变量
numerical_cols = ['Age', 'DailyRate', 'DistanceFromHome', 'HourlyRate', 'MonthlyIncome', 'MonthlyRate',
'NumCompaniesWorked', 'PercentSalaryHike', 'TotalWorkingYears', 'TrainingTimesLastYear',
'YearsAtCompany', 'YearsInCurrentRole', 'YearsSinceLastPromotion', 'YearsWithCurrManager']
categorical_cols = ['BusinessTravel', 'Department', 'Education', 'EducationField',
'EnvironmentSatisfaction', 'Gender', 'JobInvolvement', 'JobLevel', 'JobRole',
'JobSatisfaction', 'MaritalStatus', 'OverTime', 'PerformanceRating',
'RelationshipSatisfaction', 'StockOptionLevel', 'WorkLifeBalance']
# 创建预处理管道
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), numerical_cols),
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols)
])
# 其余代码不变
# ...
当使用我们自定义的分类变量和数值变量后,模型的交叉验证得分再次提升。
Output
最佳参数: {'classifier__C': 0.1, 'classifier__penalty': 'l2', 'classifier__solver': 'liblinear'}
最佳交叉验证得分:
ROC AUC: 0.8332083142464498
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3 | StandardScaler | 0.81717 |
Version 4 | 自定义分类变量和数值变量 | 0.83321 |
Version 5: Feature Engineering
在之前的文章中,我们基于探索数据集的方式手动衍生了一些新的特征,接下来看一下将这些特征加入到模型中的效果。
# 这里切换成特征工程后的数据集
X_train = fe_train_df.drop('Attrition', axis=1)
y_train = fe_train_df['Attrition']
# 其余代码不变
# ...
训练代码不再重复,不出意外,特征工程后的模型效果继续提升。
Output
最佳参数: {'classifier__C': 0.1, 'classifier__penalty': 'l2', 'classifier__solver': 'liblinear'}
最佳交叉验证得分:
ROC AUC: 0.836042143838754
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3 | StandardScaler | 0.81717 |
Version 4 | 自定义分类变量和数值变量 | 0.83321 |
Version 5 | 特征工程 | 0.83604 |
Version 6: 参数精调
以上所有实验我们均采用了根据经验的初始参数空间,接下来我们可以对模型的参数空间进行更细致的调整,以期进一步提升模型效果。
TIP
参数精调的基本思路,就是观测上一步的最佳参数,然后在其附近的参数空间进行搜索。由于逻辑回归模型的参数较少,运算速度也较快,我们可以尝试多次迭代。
在经过两次迭代之后,精调的参数空间如下:
# ...
# 定义超参数网格
param_grid = [
{'classifier__C': [0.2, 0.25, 0.3, 0.35, 0.4], 'classifier__penalty': ['l2'], 'classifier__solver': ['liblinear', 'lbfgs', 'sag', 'saga']},
{'classifier__C': [0.2, 0.25, 0.3, 0.35, 0.4], 'classifier__penalty': ['l1'], 'classifier__solver': ['liblinear', 'saga']}
]
# ...
最终的交叉验证得分如下,还是能够观察到微弱的提升。
Output
最佳参数: {'classifier__C': 0.25, 'classifier__penalty': 'l2', 'classifier__solver': 'lbfgs'}
最佳交叉验证得分:
ROC AUC: 0.836177508016491
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3 | StandardScaler | 0.81717 |
Version 4 | 自定义分类变量和数值变量 | 0.83321 |
Version 5 | 特征工程 | 0.83604 |
Version 6 | 参数精调 | 0.83618 |
Version 7: 调整交叉验证折数
此外,调整交叉验证的折数也是一个可以尝试的方向。
TIP
虽然理论上讲,更多的交叉验证的折数,可以提升模型的泛化能力,但同时也会带来计算量增加的问题。且当数据集较小时,交叉验证的折数过大会导致验证集的样本量较少和不稳定。
python
# 创建 GridSearchCV 对象
- grid_search = GridSearchCV(clf, param_grid, cv=5, scoring='roc_auc', n_jobs=-1)
+ grid_search = GridSearchCV(clf, param_grid, cv=10, scoring='roc_auc', n_jobs=-1)
最终的交叉验证得分为:
Output
最佳参数: {'classifier__C': 0.3, 'classifier__penalty': 'l2', 'classifier__solver': 'liblinear'}
最佳交叉验证得分:
ROC AUC: 0.8397028405956977
版本 | 版本说明 | 交叉验证得分 |
---|
Version 1 | Ordinal Encoding | 0.80012 |
Version 2 | One-Hot Encoding | 0.81379 |
Version 3 | StandardScaler | 0.81717 |
Version 4 | 自定义分类变量和数值变量 | 0.83321 |
Version 5 | 特征工程 | 0.83604 |
Version 6 | 参数精调 | 0.83618 |
Version 7 | 调整交叉验证折数 | 0.83970 |
总结并提交结果
至此,我们经历了一个完整的建模和调参过程,并介绍了其中逐步调优的常用方法和细节,最终将逻辑回归最佳模型的交叉验证得分逐渐由 0.8001 提升到了 0.8397。
我们使用该模型对测试集进行预测,并提交到 Kaggle 上,查看一下最终成绩。
# 使用特征工程后的测试集
X_test = fe_test_df
# 使用最佳模型进行预测
best_clf = grid_search.best_estimator_
y_pred_test_proba = best_clf.predict_proba(X_test)[:, 1]
# 将预测结果保存为提交文件
submission = pd.DataFrame({
'id': test_ids, # 使用之前保存的 id 列
'Attrition': y_pred_test_proba
})
submission.to_csv("logreg_submission.csv", index=False)
# 提交到 Kaggle
!kaggle competitions submit -c playground-series-s3e3 -f logreg_submission.csv -m "logreg_best"
# 查看提交结果
!kaggle competitions submissions -c playground-series-s3e3
Output
100%|██████████████████████████████████████| 27.5k/27.5k [00:01<00:00, 23.3kB/s]
Successfully submitted to Binary Classification with a Tabular Employee Attrition Dataset
fileName date description status publicScore privateScore
--------------------- ------------------- ------------- -------- ----------- ------------
logreg_submission.csv 2023-04-18 21:54:15 logreg_best complete 0.93962 0.88385
在 Private Score 排行榜上,这一成绩能够位列 257 名。