6 minutes
模型评估与调优
训练一个模型只是第一步。如何可靠地评估模型的表现,如何找到最好的参数组合,如何比较不同模型哪个更优,这些才是机器学习实践中的核心挑战。本文将全面介绍模型评估和调优的方法论。
交叉验证:更可靠的评估
单次划分训练集和测试集存在一个问题:结果可能因为随机划分而产生很大波动。交叉验证通过多次划分取平均值,提供更稳定的评估结果。
K-Fold 交叉验证
将数据平均分为 K 份,轮流用 K-1 份训练、1 份验证。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_diabetes
from sklearn.model_selection import (cross_val_score, cross_validate,
KFold, StratifiedKFold,
LeaveOneOut, TimeSeriesSplit,
learning_curve, validation_curve,
GridSearchCV, RandomizedSearchCV)
from sklearn.linear_model import Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score, make_scorer
import warnings
warnings.filterwarnings('ignore')
# 加载数据
diabetes = load_diabetes()
X, y = diabetes.data, diabetes.target
# 5 折交叉验证
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(Ridge(alpha=1.0), X, y, cv=kfold, scoring='r2')
print(f"5 折交叉验证 R² 分数: {scores}")
print(f"平均 R²: {scores.mean():.4f} (±{scores.std():.4f})")
Stratified K-Fold
对于分类问题,分层 K 折确保每折的类别比例与原数据一致。
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
iris = load_iris()
X_iris, y_iris = iris.data, iris.target
# 分层 K 折
stratified_kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_strat = cross_val_score(LogisticRegression(max_iter=200),
X_iris, y_iris,
cv=stratified_kfold, scoring='accuracy')
print(f"分层 K 折准确率: {scores_strat}")
print(f"平均准确率: {scores_strat.mean():.4f}")
Leave-One-Out (LOO)
每个样本单独作为验证集,适合小数据集(但计算量大)。
loo = LeaveOneOut()
# LOO 在小数据集上演示
scores_loo = cross_val_score(Ridge(alpha=1.0), X[:50], y[:50],
cv=loo, scoring='r2')
print(f"LOO 平均 R²: {scores_loo.mean():.4f}")
时间序列交叉验证
时间序列数据不能随机划分(不能使用未来数据预测过去)。时间序列交叉验证使用逐步向前的方式。
# 模拟时间序列数据
np.random.seed(42)
n = 100
X_ts = np.random.randn(n, 3)
y_ts = np.sin(np.arange(n) * 0.1) + np.random.randn(n) * 0.1
# 时间序列分割
tscv = TimeSeriesSplit(n_splits=5)
for i, (train_idx, test_idx) in enumerate(tscv.split(X_ts)):
print(f"折 {i+1}: 训练 {len(train_idx)} 个样本, 测试 {len(test_idx)} 个样本")
print(f" 训练索引范围: {train_idx[0]}...{train_idx[-1]}")
print(f" 测试索引范围: {test_idx[0]}...{test_idx[-1]}")
cross_validate:获取更多信息
cross_val_score 只返回分数,而 cross_validate 可以返回更多信息。
# cross_validate 获取详细结果
cv_results = cross_validate(
Ridge(alpha=1.0), X, y,
cv=5,
scoring={'r2': 'r2', 'neg_mse': 'neg_mean_squared_error'},
return_train_score=True
)
print("交叉验证详细结果:")
for metric in ['r2', 'neg_mse']:
train_mean = cv_results[f'train_{metric}'].mean()
test_mean = cv_results[f'test_{metric}'].mean()
print(f" {metric} - 训练: {train_mean:.4f}, 验证: {test_mean:.4f}")
超参数调优
超参数是训练前设定的参数,不是从数据中学习的。找到最佳超参数组合是提升模型表现的关键。
GridSearchCV:网格搜索
穷举搜索所有参数组合。简单但计算量大。
from sklearn.model_selection import GridSearchCV
# 定义参数网格
param_grid = {
'alpha': [0.01, 0.1, 1.0, 10.0, 100.0],
'solver': ['auto', 'svd', 'cholesky']
}
# 网格搜索
grid_search = GridSearchCV(
Ridge(),
param_grid=param_grid,
cv=5,
scoring='r2',
n_jobs=-1,
verbose=1
)
grid_search.fit(X, y)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.4f}")
print(f"最佳模型: {grid_search.best_estimator_}")
# 查看所有组合的结果
results_df = pd.DataFrame(grid_search.cv_results_)
print("\n前5个组合的搜索结果:")
print(results_df[['param_alpha', 'param_solver', 'mean_test_score', 'std_test_score']].head())
网格搜索的注意事项
参数搜索空间随着参数数量呈指数增长。实际使用中,建议先用粗粒度搜索确定大致范围,再细化搜索。
# 分阶段搜索策略
# 第一轮: 粗粒度搜索
coarse_grid = {'alpha': [0.001, 0.01, 0.1, 1, 10, 100, 1000]}
coarse_search = GridSearchCV(Ridge(), coarse_grid, cv=5, scoring='r2')
coarse_search.fit(X, y)
print(f"粗搜索最佳 alpha: {coarse_search.best_params_['alpha']:.4f}")
# 第二轮: 在最佳值附近细化
best_alpha = coarse_search.best_params_['alpha']
fine_grid = {
'alpha': np.linspace(best_alpha * 0.5, best_alpha * 2, 10)
}
fine_search = GridSearchCV(Ridge(), fine_grid, cv=5, scoring='r2')
fine_search.fit(X, y)
print(f"精细搜索最佳 alpha: {fine_search.best_params_['alpha']:.4f}")
RandomizedSearchCV:随机搜索
当参数空间很大时,随机搜索比网格搜索更高效。它从参数分布中随机采样固定数量的组合。
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint
# 随机搜索
param_dist = {
'n_estimators': randint(50, 300),
'max_depth': randint(3, 20),
'min_samples_split': randint(2, 20),
'min_samples_leaf': randint(1, 10),
}
random_search = RandomizedSearchCV(
RandomForestRegressor(random_state=42),
param_distributions=param_dist,
n_iter=30, # 只搜索 30 种组合
cv=5,
scoring='r2',
random_state=42,
n_jobs=-1
)
random_search.fit(X, y)
print(f"随机搜索最佳参数: {random_search.best_params_}")
print(f"最佳 R²: {random_search.best_score_:.4f}")
网格搜索 vs 随机搜索
关键发现(来自 Bergstra & Bengio 2012 的研究):随机搜索在大部分参数不重要时远比网格搜索高效。
网格搜索: 固定的点阵 → 浪费大量计算在不重要的参数上
随机搜索: 从分布中采样 → 更容易覆盖最优参数所在的区域
贝叶斯优化
贝叶斯优化比随机搜索更智能,它会根据历史结果选择下一组参数。scikit-optimize 提供了现成的实现。
# 需要安装: pip install scikit-optimize
from skopt import BayesSearchCV
from skopt.space import Real, Integer
# 贝叶斯搜索
opt = BayesSearchCV(
RandomForestRegressor(random_state=42),
{
'n_estimators': Integer(50, 300),
'max_depth': Integer(3, 20),
'min_samples_split': Integer(2, 20),
},
n_iter=20,
cv=5,
scoring='r2',
random_state=42
)
opt.fit(X, y)
print(f"贝叶斯搜索最佳参数: {opt.best_params_}")
print(f"最佳 R²: {opt.best_score_:.4f}")
学习曲线与验证曲线
学习曲线
学习曲线展示了模型表现随训练数据量增加的变化趋势。它可以用来诊断偏差和方差问题。
# 学习曲线
train_sizes, train_scores, test_scores = learning_curve(
Ridge(alpha=1.0), X, y,
train_sizes=np.linspace(0.1, 1.0, 10),
cv=5, scoring='r2', n_jobs=-1
)
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
plt.figure(figsize=(10, 6))
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std,
alpha=0.2, color='blue')
plt.fill_between(train_sizes, test_mean - test_std, test_mean + test_std,
alpha=0.2, color='orange')
plt.plot(train_sizes, train_mean, 'o-', label='训练集', color='blue')
plt.plot(train_sizes, test_mean, 'o-', label='验证集', color='orange')
plt.xlabel('训练集大小')
plt.ylabel('R² 分数')
plt.title('学习曲线')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
# 解读:
# 两条曲线都低 → 高偏差(欠拟合)→ 增加复杂特征或使用更复杂模型
# 训练集高但验证集低且差距大 → 高方差(过拟合)→ 增加数据量或减少特征
# 两条曲线都高且差距小 → 理想情况
验证曲线
验证曲线展示模型表现随某个超参数的变化趋势。
# 验证曲线
param_range = np.logspace(-3, 3, 20)
train_scores_v, test_scores_v = validation_curve(
Ridge(), X, y,
param_name='alpha',
param_range=param_range,
cv=5, scoring='r2', n_jobs=-1
)
train_mean_v = np.mean(train_scores_v, axis=1)
test_mean_v = np.mean(test_scores_v, axis=1)
plt.figure(figsize=(10, 6))
plt.semilogx(param_range, train_mean_v, 'o-', label='训练集', color='blue')
plt.semilogx(param_range, test_mean_v, 'o-', label='验证集', color='orange')
plt.xlabel('Alpha (正则化强度)')
plt.ylabel('R² 分数')
plt.title('验证曲线 - Ridge 正则化')
plt.legend()
plt.grid(alpha=0.3)
# 左侧 alpha 小 → 过拟合
# 右侧 alpha 大 → 欠拟合
plt.show()
模型比较与统计显著性
比较两个模型时,不能只看单次测试的分数差异,需要统计检验来判断差异是否显著。
from sklearn.model_selection import cross_val_score
from scipy import stats
# 比较两个模型
models = {
'Ridge': Ridge(alpha=1.0),
'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42)
}
results = {}
for name, model in models.items():
scores = cross_val_score(model, X, y, cv=10, scoring='r2')
results[name] = scores
print(f"{name}: 平均 R² = {scores.mean():.4f} (±{scores.std():.4f})")
# 配对 t 检验
t_stat, p_value = stats.ttest_rel(results['Ridge'], results['Random Forest'])
print(f"\n配对 t 检验:")
print(f" t 统计量: {t_stat:.4f}")
print(f" p 值: {p_value:.4f}")
print(f" 结论: {'差异显著' if p_value < 0.05 else '差异不显著'}")
集成方法
集成学习通过组合多个模型来获得更好的预测性能,是 Kaggle 竞赛的制胜法宝。
Bagging
Bagging 通过并行训练多个模型并取平均来降低方差。随机森林是最典型的代表。
from sklearn.ensemble import BaggingRegressor
# Bagging 回归
bagging = BaggingRegressor(
estimator=Ridge(alpha=1.0),
n_estimators=50,
max_samples=0.8,
max_features=0.8,
random_state=42
)
bagging_scores = cross_val_score(bagging, X, y, cv=5, scoring='r2')
print(f"Bagging R²: {bagging_scores.mean():.4f}")
Boosting
Boosting 按顺序训练模型,每个新模型修正前一个的错误。它主要降低偏差。
from sklearn.ensemble import AdaBoostRegressor, GradientBoostingRegressor
# AdaBoost
ada = AdaBoostRegressor(n_estimators=50, random_state=42)
ada_scores = cross_val_score(ada, X, y, cv=5, scoring='r2')
print(f"AdaBoost R²: {ada_scores.mean():.4f}")
# Gradient Boosting
gbr = GradientBoostingRegressor(
n_estimators=100, max_depth=3, learning_rate=0.1, random_state=42
)
gbr_scores = cross_val_score(gbr, X, y, cv=5, scoring='r2')
print(f"Gradient Boosting R²: {gbr_scores.mean():.4f}")
XGBoost
XGBoost 是梯度提升的高效实现,速度更快、性能更好,是 Kaggle 竞赛的常胜将军。
# 需要安装: pip install xgboost
import xgboost as xgb
xgb_model = xgb.XGBRegressor(
n_estimators=100, max_depth=3, learning_rate=0.1,
subsample=0.8, colsample_bytree=0.8,
random_state=42
)
xgb_scores = cross_val_score(xgb_model, X, y, cv=5, scoring='r2')
print(f"XGBoost R²: {xgb_scores.mean():.4f}")
LightGBM
LightGBM 是微软开发的高效梯度提升框架,比 XGBoost 更快,内存占用更低。
# 需要安装: pip install lightgbm
import lightgbm as lgb
lgb_model = lgb.LGBMRegressor(
n_estimators=100, max_depth=3, learning_rate=0.1,
subsample=0.8, colsample_bytree=0.8,
random_state=42, verbose=-1
)
lgb_scores = cross_val_score(lgb_model, X, y, cv=5, scoring='r2')
print(f"LightGBM R²: {lgb_scores.mean():.4f}")
Stacking
Stacking 用"元学习器"来组合多个基模型的预测结果。
from sklearn.ensemble import StackingRegressor
from sklearn.linear_model import LinearRegression
# Stacking 集成
base_models = [
('ridge', Ridge(alpha=1.0)),
('rf', RandomForestRegressor(n_estimators=50, random_state=42)),
('gbr', GradientBoostingRegressor(n_estimators=50, random_state=42))
]
stacking = StackingRegressor(
estimators=base_models,
final_estimator=LinearRegression(),
cv=5
)
stacking_scores = cross_val_score(stacking, X, y, cv=5, scoring='r2')
print(f"Stacking R²: {stacking_scores.mean():.4f}")
集成方法对比
| 方法 | 目标 | 训练方式 | 典型算法 |
|---|---|---|---|
| Bagging | 降低方差 | 并行 | 随机森林 |
| Boosting | 降低偏差 | 顺序 | AdaBoost, GBDT, XGBoost |
| Stacking | 两者兼顾 | 分层 | 元学习器组合基模型 |
模型可解释性
复杂模型往往被称为"黑箱"。可解释性工具让我们理解模型为什么做出某个预测。
特征重要性
树模型和线性模型直接提供特征重要性。
gbr.fit(X, y)
importance_df = pd.DataFrame({
'feature': diabetes.feature_names,
'importance': gbr.feature_importances_
}).sort_values('importance', ascending=False)
print("Gradient Boosting 特征重要性:")
print(importance_df)
SHAP 值
SHAP(SHapley Additive exPlanations)基于博弈论,为每个预测计算每个特征的贡献值。
# 需要安装: pip install shap
import shap
# 训练模型
model = GradientBoostingRegressor(n_estimators=100, max_depth=3, random_state=42)
model.fit(X, y)
# 计算 SHAP 值
explainer = shap.Explainer(model, X)
shap_values = explainer(X)
# 特征重要性总结图
shap.summary_plot(shap_values, X, feature_names=diabetes.feature_names)
偏依赖图(Partial Dependence Plot)
展示某个特征对预测结果的平均边际影响。
from sklearn.inspection import PartialDependenceDisplay
# 偏依赖图
fig, ax = plt.subplots(figsize=(10, 6))
PartialDependenceDisplay.from_estimator(
model, X, features=[0, 2, (0, 2)],
feature_names=diabetes.feature_names,
ax=ax
)
plt.suptitle('偏依赖图')
plt.tight_layout()
plt.show()
LIME(Local Interpretable Model-agnostic Explanations)
LIME 在预测点附近拟合一个简单可解释模型来解释单个预测。
# 需要安装: pip install lime
import lime
from lime.lime_tabular import LimeTabularExplainer
explainer_lime = LimeTabularExplainer(
X, feature_names=diabetes.feature_names,
mode='regression', random_state=42
)
# 解释单个预测
i = 0 # 第一个测试样本
exp = explainer_lime.explain_instance(
X[i], model.predict, num_features=5
)
exp.show_in_notebook(show_table=True)
模型持久化
训练好模型后,保存到磁盘以便后续使用。
import joblib
import pickle
# 训练最终模型
final_model = GradientBoostingRegressor(
n_estimators=100, max_depth=3, learning_rate=0.1,
random_state=42
)
final_model.fit(X, y)
# 方法 1: joblib(推荐,对大对象更高效)
joblib.dump(final_model, 'model.joblib')
loaded_model_joblib = joblib.load('model.joblib')
print("joblib 保存/加载成功")
# 方法 2: pickle(Python 标准库)
with open('model.pkl', 'wb') as f:
pickle.dump(final_model, f)
with open('model.pkl', 'rb') as f:
loaded_model_pickle = pickle.load(f)
print("pickle 保存/加载成功")
# 验证保存的模型
y_pred = loaded_model_joblib.predict(X)
print(f"加载模型 R²: {r2_score(y, y_pred):.4f}")
保存模型时的建议:
- joblib:推荐用于 scikit-learn 模型,对大数组序列化更高效
- pickle:Python 标准库,通用但可能较慢
- 始终保存模型训练时的预处理 Pipeline(编码器、缩放器等),而不是只保存模型
- 记录模型版本和训练参数
# 正确做法:保存整个 Pipeline
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', GradientBoostingRegressor(n_estimators=100, random_state=42))
])
pipeline.fit(X, y)
joblib.dump(pipeline, 'full_pipeline.joblib')
print("完整 Pipeline 已保存")
实战:完整的调优流程
from sklearn.model_selection import train_test_split
# 1. 数据划分
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 2. 参数搜索
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 7],
'learning_rate': [0.01, 0.05, 0.1],
}
gbr = GradientBoostingRegressor(random_state=42)
grid = GridSearchCV(gbr, param_grid, cv=5, scoring='r2', n_jobs=-1)
grid.fit(X_train, y_train)
print(f"最佳参数: {grid.best_params_}")
print(f"最佳交叉验证 R²: {grid.best_score_:.4f}")
# 3. 测试集评估
test_r2 = grid.score(X_test, y_test)
print(f"测试集 R²: {test_r2:.4f}")
# 4. 特征重要性
best_model = grid.best_estimator_
importance = pd.DataFrame({
'feature': diabetes.feature_names,
'importance': best_model.feature_importances_
}).sort_values('importance', ascending=False)
print("\n特征重要性:")
print(importance)
# 5. 保存最终模型
joblib.dump(grid.best_estimator_, 'best_gbr_model.joblib')
print("\n最佳模型已保存为 best_gbr_model.joblib")
总结
这是本系列文章的最后一篇。我们一起走过了模型评估与调优的全流程:
- 交叉验证:K-Fold、Stratified K-Fold、LOO、时间序列分割
- 超参数调优:网格搜索、随机搜索、贝叶斯优化
- 学习曲线与验证曲线:诊断偏差方差问题
- 模型比较:统计检验判断差异显著性
- 集成方法:Bagging、Boosting(XGBoost、LightGBM)、Stacking
- 模型可解释性:特征重要性、SHAP、偏依赖图、LIME
- 模型持久化:joblib / pickle
回顾整个"高级篇",我们从机器学习概览出发,深入学习了回归、分类、聚类三大算法体系,掌握了特征工程的核心技能,最终到达模型评估与调优的终点。
机器学习的道路没有终点。这些理论和技能只是基础,真正的成长来自于在真实数据上反复实践。祝你在数据分析的道路上不断进步,从入门走向精通!