建模调参流程与逻辑回归

对于二分类问题,逻辑回归是一个可以优先尝试的模型,不仅可以作为一个基准模型,同时因其广义线性模型的特性,拥有良好的可解释性。

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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.81379
Version 3StandardScaler0.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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.81379
Version 3StandardScaler0.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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.81379
Version 3StandardScaler0.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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.81379
Version 3StandardScaler0.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 1Ordinal Encoding0.80012
Version 2One-Hot Encoding0.81379
Version 3StandardScaler0.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 名。