模型选择与评估
模型选择与评估是机器学习工作流程中至关重要的一环。训练出模型只是第一步,如何科学地评估模型性能、选择最佳模型、调整超参数,直接影响最终的应用效果。本章将系统介绍 sklearn 中的模型评估方法和超参数调优技术。
为什么需要科学的模型评估?
训练误差与泛化误差
机器学习的目标是让模型在未见过的数据上表现良好,这称为泛化能力。如果只关注训练数据上的表现,很容易陷入过拟合的陷阱。
训练误差是模型在训练数据上的预测误差。训练误差低并不意味着模型好,因为模型可能只是"记住"了训练数据。
泛化误差是模型在未见数据上的预测误差。这才是我们真正关心的指标。
评估模型的关键是用独立的数据来估计泛化误差。简单地将数据分为训练集和测试集是一种方法,但对于更可靠的评估,我们需要交叉验证。
数据泄露问题
数据泄露是指在模型训练过程中无意中使用了不应该使用的信息。这是机器学习中最常见且最危险的错误之一。
常见的数据泄露场景:
- 预处理泄露:在整个数据集上进行标准化,然后再划分训练集和测试集。这样测试集的信息泄露到了预处理参数中。
- 特征选择泄露:在整个数据集上进行特征选择,然后再划分数据。
- 时间泄露:在时间序列预测中,使用未来的数据预测过去。
正确的做法是:任何数据转换都应该只从训练集学习参数,然后应用到测试集。使用 Pipeline 可以自动保证这一点。
交叉验证
交叉验证(Cross-Validation,简称 CV)是一种更稳健的模型评估方法,充分利用有限的数据进行训练和验证。
K 折交叉验证
K 折交叉验证将数据分成 K 份,每次用 K-1 份训练,1 份验证,重复 K 次,取平均作为最终评估结果。
为什么 K 折交叉验证更可靠?
单次划分可能导致偶然性的偏差。比如划分恰好把难分类的样本都放到了测试集,评估结果就会偏低。K 折交叉验证通过多次划分取平均,降低了这种偶然性。
from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
clf = RandomForestClassifier(n_estimators=100, random_state=42)
# 5折交叉验证
scores = cross_val_score(clf, X, y, cv=5)
print(f"各折得分: {scores}")
print(f"平均得分: {scores.mean():.3f}")
print(f"标准差: {scores.std():.3f}")
分层 K 折交叉验证
对于分类问题,如果某些类别样本很少,普通 K 折可能导致某些折中缺少某些类别。分层 K 折保证每折中各类别的比例与完整数据集相同。
from sklearn.model_selection import cross_val_score, StratifiedKFold
# 分层 K 折(推荐用于分类)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(clf, X, y, cv=skf)
print(f"分层交叉验证得分: {scores.mean():.3f} (+/- {scores.std()*2:.3f})")
分类任务应该使用 StratifiedKFold,而不是普通的 KFold。
留一法交叉验证
留一法(Leave-One-Out,LOO)是 K 折的特例,K 等于样本数。每次只留一个样本作为验证集。
优点是充分利用数据,缺点是计算量大。
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(clf, X, y, cv=loo)
print(f"留一法得分: {scores.mean():.3f}")
print(f"总测试次数: {len(scores)}")
重复交叉验证
为了获得更稳定的估计,可以多次重复 K 折交叉验证。
from sklearn.model_selection import RepeatedStratifiedKFold
# 重复 5 折交叉验证 10 次
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)
scores = cross_val_score(clf, X, y, cv=rskf)
print(f"重复交叉验证得分: {scores.mean():.3f} (+/- {scores.std()*2:.3f})")
交叉验证的注意事项
计算成本:K 折交叉验证需要训练 K 次模型,对于复杂模型或大数据集,计算成本较高。可以通过 n_jobs=-1 并行化。
数据泄露风险:如果在交叉验证外进行预处理,仍然会发生数据泄露。正确做法是使用 Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# 正确:预处理在交叉验证内部
pipeline = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression())
])
scores = cross_val_score(pipeline, X, y, cv=5)
print(f"正确做法得分: {scores.mean():.3f}")
# 错误:预处理在交叉验证外部
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 泄露了测试集信息
clf = LogisticRegression()
scores_wrong = cross_val_score(clf, X_scaled, y, cv=5)
print(f"错误做法得分: {scores_wrong.mean():.3f}")
自定义交叉验证
对于特殊情况,可以自定义交叉验证划分方式。
时间序列交叉验证:
from sklearn.model_selection import TimeSeriesSplit
import numpy as np
# 时间序列数据
X_time = np.arange(100).reshape(-1, 1)
y_time = np.random.randn(100)
tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X_time):
print(f"训练: {train_index[:5]}...{train_index[-5:]}, 测试: {test_index[:5]}...{test_index[-5:]}")
分组交叉验证:
当数据有分组结构时(如同一个患者的多次测量),需要保证同一组的数据不会同时出现在训练集和测试集中。
from sklearn.model_selection import GroupKFold
# 假设有 5 个组
groups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
gkf = GroupKFold(n_splits=5)
for train_index, test_index in gkf.split(X, y, groups):
print(f"训练组: {set(np.array(groups)[train_index])}, 测试组: {set(np.array(groups)[test_index])}")
评估指标
选择合适的评估指标对于正确评估模型至关重要。不同任务需要不同的指标。
分类评估指标
准确率(Accuracy):正确预测的比例。当类别不平衡时具有误导性。
from sklearn.metrics import accuracy_score
y_true = [0, 1, 0, 1, 0, 1, 0, 1]
y_pred = [0, 1, 0, 0, 0, 1, 1, 1]
print(f"准确率: {accuracy_score(y_true, y_pred):.3f}")
精确率、召回率、F1 分数:
from sklearn.metrics import precision_score, recall_score, f1_score
print(f"精确率: {precision_score(y_true, y_pred):.3f}")
print(f"召回率: {recall_score(y_true, y_pred):.3f}")
print(f"F1 分数: {f1_score(y_true, y_pred):.3f}")
这些指标的数学定义:
- 精确率 = TP / (TP + FP),预测为正的样本中有多少是真正的正样本
- 召回率 = TP / (TP + FN),真正的正样本有多少被正确预测
- F1 分数 = 2 × (精确率 × 召回率) / (精确率 + 召回率)
精确率-召回率权衡:
在很多分类任务中,精确率和召回率是此消彼长的。提高分类阈值会增加精确率但降低召回率,反之亦然。选择哪个指标更重要取决于业务场景:
- 疾病筛查:召回率更重要,宁可误诊也不能漏诊
- 垃圾邮件过滤:精确率更重要,误判正常邮件为垃圾邮件代价很高
分类报告:
from sklearn.metrics import classification_report
print(classification_report(y_true, y_pred, target_names=['负类', '正类']))
混淆矩阵:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
cm = confusion_matrix(y_true, y_pred)
print("混淆矩阵:")
print(cm)
# 可视化
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['负类', '正类'])
disp.plot(cmap='Blues')
ROC 曲线与 AUC:
ROC 曲线展示不同阈值下的真阳性率(TPR)和假阳性率(FPR)。AUC 是曲线下的面积。
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt
# 需要概率输出
y_prob = [0.1, 0.4, 0.35, 0.8, 0.2, 0.7, 0.6, 0.9]
fpr, tpr, thresholds = roc_curve(y_true, y_prob)
auc = roc_auc_score(y_true, y_prob)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC 曲线 (AUC = {auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('假阳性率 (FPR)')
plt.ylabel('真阳性率 (TPR)')
plt.title('ROC 曲线')
plt.legend()
plt.grid(True)
plt.show()
回归评估指标
均方误差(MSE)和均方根误差(RMSE):
from sklearn.metrics import mean_squared_error
import numpy as np
y_true = [3.0, -0.5, 2.0, 7.0]
y_pred = [2.5, 0.0, 2.0, 8.0]
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
print(f"MSE: {mse:.3f}")
print(f"RMSE: {rmse:.3f}")
平均绝对误差(MAE):
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y_true, y_pred)
print(f"MAE: {mae:.3f}")
决定系数(R²):
from sklearn.metrics import r2_score
r2 = r2_score(y_true, y_pred)
print(f"R²: {r2:.3f}")
R² 表示模型解释目标变量方差的比例。R² = 1 表示完美预测,R² = 0 表示模型等于简单预测均值。
指标选择:
| 指标 | 特点 | 适用场景 |
|---|---|---|
| MSE/RMSE | 对大误差敏感 | 需要惩罚大误差 |
| MAE | 对异常值稳健 | 数据有异常值 |
| R² | 可解释性好 | 需要解释模型拟合程度 |
使用交叉验证计算多个指标
from sklearn.model_selection import cross_validate
scoring = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
results = cross_validate(clf, X, y, cv=5, scoring=scoring)
for metric in scoring:
scores = results[f'test_{metric}']
print(f"{metric}: {scores.mean():.3f} (+/- {scores.std()*2:.3f})")
超参数调优
超参数是在训练前需要设置的参数,不同于模型训练过程中学习的参数。选择合适的超参数对模型性能至关重要。
网格搜索(GridSearchCV)
网格搜索穷举所有参数组合,找到最优解。
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# 定义参数网格
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 10, None],
'min_samples_split': [2, 5, 10]
}
# 网格搜索
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1,
verbose=1
)
grid_search.fit(X, y)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.3f}")
# 使用最佳模型
best_model = grid_search.best_estimator_
随机搜索(RandomizedSearchCV)
当参数空间很大时,随机搜索更高效。它从参数分布中随机采样固定数量的组合。
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
# 定义参数分布
param_dist = {
'n_estimators': randint(50, 300),
'max_depth': [3, 5, 10, None],
'min_samples_split': randint(2, 20),
'min_samples_leaf': randint(1, 10)
}
# 随机搜索
random_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42),
param_dist,
n_iter=50, # 尝试 50 个组合
cv=5,
scoring='accuracy',
n_jobs=-1,
random_state=42
)
random_search.fit(X, y)
print(f"最佳参数: {random_search.best_params_}")
print(f"最佳得分: {random_search.best_score_:.3f}")
嵌套交叉验证
当需要在调参的同时估计泛化误差时,使用嵌套交叉验证。外层循环评估模型,内层循环进行超参数调优。
from sklearn.model_selection import cross_val_score, GridSearchCV
# 外层交叉验证
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# 内层网格搜索
inner_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=3,
scoring='accuracy'
)
# 嵌套交叉验证
nested_scores = cross_val_score(inner_search, X, y, cv=outer_cv)
print(f"嵌套交叉验证得分: {nested_scores.mean():.3f} (+/- {nested_scores.std()*2:.3f})")
嵌套交叉验证的结果是对泛化误差的无偏估计。普通网格搜索的最佳得分往往过于乐观,因为它用验证集选了最优参数。
超参数调优的最佳实践
搜索顺序:先用粗粒度搜索确定大致范围,再用细粒度搜索精确调优。
# 第一阶段:粗搜索
param_grid_coarse = {
'n_estimators': [10, 50, 100, 200],
'max_depth': [3, 5, 10, 20, None]
}
# 第二阶段:细搜索(基于粗搜索结果)
param_grid_fine = {
'n_estimators': [80, 100, 120],
'max_depth': [8, 10, 12]
}
常用超参数优先级:
对于随机森林:
n_estimators(树的数量,通常越多越好,但有计算成本)max_depth(最大深度,控制过拟合)min_samples_split、min_samples_leaf(控制分裂条件)
对于 SVM:
C(正则化参数)kernel(核函数类型)gamma(RBF 核参数)
避免过拟合验证集:如果尝试了太多参数组合,可能会过拟合到验证集。使用嵌套交叉验证或保留一个独立的测试集进行最终评估。
学习曲线与验证曲线
学习曲线
学习曲线展示模型性能随训练数据量的变化,帮助判断模型是否需要更多数据。
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt
train_sizes, train_scores, test_scores = learning_curve(
RandomForestClassifier(n_estimators=100, random_state=42),
X, y,
cv=5,
train_sizes=np.linspace(0.1, 1.0, 10),
n_jobs=-1
)
train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
test_mean = test_scores.mean(axis=1)
test_std = test_scores.std(axis=1)
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, 'o-', label='训练得分')
plt.plot(train_sizes, test_mean, 'o-', label='验证得分')
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1)
plt.fill_between(train_sizes, test_mean - test_std, test_mean + test_std, alpha=0.1)
plt.xlabel('训练样本数')
plt.ylabel('得分')
plt.title('学习曲线')
plt.legend()
plt.grid(True)
plt.show()
如何解读学习曲线:
- 高偏差(欠拟合):训练得分和验证得分都很低,增加数据不会有帮助,需要更复杂的模型或更好的特征
- 高方差(过拟合):训练得分高,验证得分低,增加数据可能有帮助
- 理想情况:训练得分和验证得分都高且接近
验证曲线
验证曲线展示模型性能随超参数值的变化,帮助选择最优超参数。
from sklearn.model_selection import validation_curve
param_range = [1, 3, 5, 10, 20, 50, 100]
train_scores, test_scores = validation_curve(
RandomForestClassifier(n_estimators=50, random_state=42),
X, y,
param_name='max_depth',
param_range=param_range,
cv=5,
n_jobs=-1
)
train_mean = train_scores.mean(axis=1)
test_mean = test_scores.mean(axis=1)
plt.figure(figsize=(10, 6))
plt.plot(param_range, train_mean, 'o-', label='训练得分')
plt.plot(param_range, test_mean, 'o-', label='验证得分')
plt.xlabel('max_depth')
plt.ylabel('得分')
plt.title('验证曲线')
plt.legend()
plt.grid(True)
plt.xscale('log')
plt.show()
模型持久化
训练好的模型需要保存以便后续使用。
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
# 训练模型
iris = load_iris()
X, y = iris.data, iris.target
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X, y)
# 保存模型
joblib.dump(model, 'random_forest_model.joblib')
# 加载模型
loaded_model = joblib.load('random_forest_model.joblib')
# 使用模型预测
predictions = loaded_model.predict([[5.1, 3.5, 1.4, 0.2]])
print(f"预测结果: {predictions}")
保存 Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
pipeline = Pipeline([
('scaler', StandardScaler()),
('clf', RandomForestClassifier(n_estimators=100, random_state=42))
])
pipeline.fit(X, y)
# 保存整个 Pipeline
joblib.dump(pipeline, 'full_pipeline.joblib')
# 加载后会自动进行预处理和预测
loaded_pipeline = joblib.load('full_pipeline.joblib')
小结
模型选择与评估是机器学习工作流程的核心环节:
- 交叉验证:使用 K 折交叉验证获得更稳健的模型评估,分类任务使用分层 K 折
- 评估指标:根据任务特点选择合适的指标,不要只看准确率
- 超参数调优:网格搜索适合小参数空间,随机搜索更适合大参数空间
- 避免数据泄露:预处理必须在交叉验证内部进行,使用 Pipeline
- 学习曲线:帮助判断是需要更多数据还是更复杂的模型
- 模型持久化:使用 joblib 保存训练好的模型
这些技能的掌握将帮助你更科学地评估和优化机器学习模型。