评估指标 + 过拟合 + 正则化:让模型不犯傻的工程实战
会训模型不算啥。会判断模型"行不行"、知道为啥不行、怎么修——才是真正的 ML 工程师。
到这里你已经会跑 5 个算法了。但跑通不是 ML 工程师的能力——
真正的能力是:知道你的模型”行不行”,知道为什么不行,知道怎么修。
这一篇讲三个交织在一起的话题:评估指标、过拟合、正则化。
一、过拟合:ML 工程师的头号公敌
什么是过拟合
模型在训练数据上表现极好,在新数据上表现很差。
它”背”了训练数据的细节、噪声、巧合——没学到”通用规律”。
一个图像化的直觉
假设你要拟合 6 个点。三种模型:
模型 A: 一条直线
✓ ●○●○●○○● ← 线(学得有点粗)
说明:欠拟合(underfitting)
模型 B: 一条 S 形曲线
✓ ✓✓✓✓✓✓✓ ← 曲线穿过/接近所有点
说明:刚刚好
模型 C: 极其曲折的多项式
✓✓✓✓✓✓ ← 精确穿过所有点,但形状离谱
说明:过拟合(overfitting)
模型 A 太简单(欠拟合),模型 C 太复杂(过拟合)。
诊断准则:
- 训练准确率低 + 测试准确率低 → 欠拟合(模型不够复杂或特征不够)
- 训练准确率高 + 测试准确率低 → 过拟合(模型太复杂或数据太少)
- 训练准确率高 + 测试准确率高 → 达到 sweet spot
为什么会过拟合
3 个主要原因:
- 模型太复杂(参数太多 vs 样本太少)
- 训练数据太少 / 不够多样
- 训练时间太长(神经网络)
二、避免过拟合的招数
1. 训练/验证/测试集切分
永远不要用同一份数据训练和评估!
from sklearn.model_selection import train_test_split
# 经典 80/20 切分
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 更严格:先切出最终测试集(碰都不能碰),剩下再切验证集
X_full_train, X_test, y_full_train, y_test = train_test_split(X, y, test_size=0.2)
X_train, X_val, y_train, y_val = train_test_split(X_full_train, y_full_train, test_size=0.2)
测试集是”最终考试”——只在所有调参完成后看一次。否则你会无意识地把测试集信息泄露到训练里。
2. 交叉验证(Cross-Validation)
不只切一次,切 K 次取平均。
5 折交叉验证:
原数据 ━━━━━━━━━━━━━━━━━━━━━━━
│1│2│3│4│5│
第 1 轮: 用 1 验证,2-5 训练 → 准确率 92%
第 2 轮: 用 2 验证,其余训练 → 准确率 90%
第 3 轮: 用 3 验证,其余训练 → 准确率 91%
第 4 轮: 用 4 验证,其余训练 → 准确率 89%
第 5 轮: 用 5 验证,其余训练 → 准确率 93%
平均 = 91% ± 1.5%
代码:
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
scores = cross_val_score(model, X, y, cv=5)
print(f"准确率: {scores.mean():.3f} ± {scores.std():.3f}")
好处:
- 不浪费任何数据
- 评估更稳定
- 能看出方差(标准差大 = 模型不稳定)
3. 正则化(Regularization)
加约束让模型”不要太复杂”。
L1 正则化(Lasso)
损失里加:
效果:让一些权重变成 0——自动特征选择。
L2 正则化(Ridge)
损失里加:
效果:让所有权重变小但不为 0——减少模型对单个特征的依赖。
实际用
from sklearn.linear_model import Lasso, Ridge
# alpha 越大正则越强
lasso = Lasso(alpha=0.1).fit(X_train, y_train)
ridge = Ridge(alpha=1.0).fit(X_train, y_train)
# 查看权重
print(f"Lasso 非零权重数: {(lasso.coef_ != 0).sum()}") # 通常远小于总数
print(f"Ridge 最大权重: {ridge.coef_.max():.3f}") # 通常被压得不大
4. 更多数据
最暴力的招——数据量是过拟合的终极克星。
这就是为什么 GPT-3 / GPT-4 不严重过拟合——它们的数据量大到几乎不可能”背诵”。
实际工业中加数据的方式:
- 数据增强(图像翻转/旋转)
- 合成数据(GAN 生成)
- 用预训练模型(迁移学习)
5. Dropout(神经网络专用)
训练时随机”关掉”一部分神经元。让模型不能依赖单个神经元——必须分散学习。
import torch.nn as nn
model = nn.Sequential(
nn.Linear(100, 64),
nn.ReLU(),
nn.Dropout(0.5), # 50% 概率丢弃
nn.Linear(64, 10)
)
6. Early Stopping(神经网络专用)
监控验证损失。一旦它开始上升(开始过拟合),停止训练。
best_val_loss = float('inf')
patience = 0
for epoch in range(100):
train_one_epoch()
val_loss = evaluate(val_set)
if val_loss < best_val_loss:
best_val_loss = val_loss
save_model()
patience = 0
else:
patience += 1
if patience > 5:
print("Early stopping!")
break
三、评估指标全集
不同任务用不同指标。用错指标比模型本身错更可怕。
分类指标
准确率(Accuracy)
对的 / 总数。最直观,但类别不均衡时极不可靠。
混淆矩阵
预测为正 预测为负
真实为正 TP FN
真实为负 FP TN
- TP(True Positive):真阳性(确实有病,预测有病)
- FP(False Positive):假阳性(其实没病,被误诊有病)
- FN(False Negative):假阴性(确实有病,被漏诊)
- TN(True Negative):真阴性
精确率(Precision)
“预测为正的样本里有多少真是正”——关心误报用它(比如反垃圾邮件,别把正常邮件误判垃圾)。
召回率(Recall)
“真正例里有多少被找出来”——关心漏报用它(比如癌症筛查,宁可多查不可漏)。
F1 分数
精确率和召回率的调和平均。两者都关心时用。
ROC AUC
不同阈值下的 TP rate 和 FP rate 画曲线,曲线下面积 = AUC。
- AUC = 1.0:完美
- AUC = 0.5:和瞎猜一样
- AUC > 0.9:很好
- AUC > 0.7:可用
AUC 阈值无关——在你不确定要用什么阈值时用 AUC 评估”模型本身的判别能力”。
回归指标
| 指标 | 公式 | 解读 |
|---|---|---|
| MSE | 大误差被放大 | |
| RMSE | MSE 的平方根,单位和 y 一致 | |
| MAE | 对异常值不敏感 | |
| R² | 0-1,越接近 1 越好 |
用 sklearn 一键计算
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score, roc_auc_score,
confusion_matrix, classification_report
)
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1] # 正例概率
print(f"准确率: {accuracy_score(y_test, y_pred):.3f}")
print(f"精确率: {precision_score(y_test, y_pred):.3f}")
print(f"召回率: {recall_score(y_test, y_pred):.3f}")
print(f"F1: {f1_score(y_test, y_pred):.3f}")
print(f"AUC: {roc_auc_score(y_test, y_prob):.3f}")
# 一份完整报告
print(classification_report(y_test, y_pred))
一个真实的诊断流程
新模型训完,依次跑这几步:
# 1. 训练 vs 测试准确率对比
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
gap = train_acc - test_acc
print(f"训练: {train_acc:.3f}, 测试: {test_acc:.3f}, gap: {gap:.3f}")
# gap > 0.1:可能过拟合
# 都很低:可能欠拟合
# 2. 交叉验证看稳定性
cv = cross_val_score(model, X, y, cv=5)
print(f"CV: {cv.mean():.3f} ± {cv.std():.3f}")
# std > 0.05:模型不稳定,可能数据少
# 3. 混淆矩阵看具体错在哪
print(confusion_matrix(y_test, y_pred))
# 4. 看不同阈值下的表现
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, y_prob)
# 画 P-R 曲线 找合适阈值
一个真实业务陷阱
信用卡欺诈检测:99% 正常交易 + 1% 欺诈。
❌ 错误做法:用准确率评估。
- 模型输出”全部正常”→ 准确率 99%!
- 但毫无用处——它没找到任何欺诈
✅ 正确做法:
- 用 召回率(找到了多少欺诈)和 精确率(标记的欺诈里多少真的是)
- 业务上接受 5% 的假阳性(误标)换取 90% 召回率
- 调整阈值实现这个权衡
数据不均衡时,准确率是个陷阱——它能让烂模型看起来很好。
- 不要相信训练准确率——它几乎总是过分乐观
- 永远用测试集做最终评估——而且只用一次
- 选指标看业务关心什么——医疗看 Recall,反垃圾看 Precision
- 盯紧训练 vs 测试的差距——大差距 = 过拟合警报
- 交叉验证看稳定性——CV 标准差大 = 模型/数据有问题
到这里,L2 路径起步部分(7 篇)完结。再后面的 L2-08 (SVM)、L2-10 (特征工程)、L2-11 (端到端项目) 我们留给后续章节。
L1 + L2 学完,你已经具备真正的 ML 工程师入门能力——能跑通完整项目、解决真实业务问题。
下一步推荐:L3 路径(深度学习核心)—— 已经写好了几篇关键的(Attention、CNN)。
读到这里说明你认真在学 🎯
订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。
讨论区
· 用 GitHub 账号登录评论src/components/Comments.astro 顶部填入
仓库 ID 和分类 ID(见组件注释里的配置步骤)。