HelloAI
L2 第 7 篇 🐣 难度 🕒 14 分钟

评估指标 + 过拟合 + 正则化:让模型不犯傻的工程实战

会训模型不算啥。会判断模型"行不行"、知道为啥不行、怎么修——才是真正的 ML 工程师。

阿莱
2026/6/30

到这里你已经会跑 5 个算法了。但跑通不是 ML 工程师的能力——

真正的能力是:知道你的模型”行不行”,知道为什么不行,知道怎么修。

这一篇讲三个交织在一起的话题:评估指标、过拟合、正则化

一、过拟合:ML 工程师的头号公敌

什么是过拟合

模型在训练数据上表现极好,在新数据上表现很差。

它”背”了训练数据的细节、噪声、巧合——没学到”通用规律”。

一个图像化的直觉

假设你要拟合 6 个点。三种模型:

模型 A: 一条直线
✓ ●○●○●○○●  ← 线(学得有点粗)
说明:欠拟合(underfitting)

模型 B: 一条 S 形曲线  
✓ ✓✓✓✓✓✓✓ ← 曲线穿过/接近所有点
说明:刚刚好

模型 C: 极其曲折的多项式  
✓✓✓✓✓✓     ← 精确穿过所有点,但形状离谱
说明:过拟合(overfitting)

模型 A 太简单(欠拟合),模型 C 太复杂(过拟合)。

诊断准则

  • 训练准确率低 + 测试准确率低 → 欠拟合(模型不够复杂或特征不够)
  • 训练准确率高 + 测试准确率低 → 过拟合(模型太复杂或数据太少)
  • 训练准确率高 + 测试准确率高 → 达到 sweet spot

为什么会过拟合

3 个主要原因:

  1. 模型太复杂(参数太多 vs 样本太少)
  2. 训练数据太少 / 不够多样
  3. 训练时间太长(神经网络)

二、避免过拟合的招数

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)

损失里加:

Ltotal=L+λiwiL_{total} = L + \lambda \sum_i |w_i|

效果:让一些权重变成 0——自动特征选择。

L2 正则化(Ridge)

损失里加:

Ltotal=L+λiwi2L_{total} = L + \lambda \sum_i w_i^2

效果:让所有权重变小但不为 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)

P=TPTP+FPP = \frac{TP}{TP + FP}

“预测为正的样本里有多少真是正”——关心误报用它(比如反垃圾邮件,别把正常邮件误判垃圾)。

召回率(Recall)

R=TPTP+FNR = \frac{TP}{TP + FN}

“真正例里有多少被找出来”——关心漏报用它(比如癌症筛查,宁可多查不可漏)。

F1 分数

F1=2PRP+RF_1 = \frac{2 PR}{P + R}

精确率和召回率的调和平均。两者都关心时用。

ROC AUC

不同阈值下的 TP rate 和 FP rate 画曲线,曲线下面积 = AUC。

  • AUC = 1.0:完美
  • AUC = 0.5:和瞎猜一样
  • AUC > 0.9:很好
  • AUC > 0.7:可用

AUC 阈值无关——在你不确定要用什么阈值时用 AUC 评估”模型本身的判别能力”。

回归指标

指标公式解读
MSE1N(yy^)2\frac{1}{N}\sum(y - \hat{y})^2大误差被放大
RMSEMSE\sqrt{MSE}MSE 的平方根,单位和 y 一致
MAE1Nyy^\frac{1}{N}\sum\mid y - \hat{y}\mid对异常值不敏感
1SSresSStot1 - \frac{SS_{res}}{SS_{tot}}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%!
  • 但毫无用处——它没找到任何欺诈

✅ 正确做法:

  1. 召回率(找到了多少欺诈)和 精确率(标记的欺诈里多少真的是)
  2. 业务上接受 5% 的假阳性(误标)换取 90% 召回率
  3. 调整阈值实现这个权衡

数据不均衡时,准确率是个陷阱——它能让烂模型看起来很好。

💡 评估的核心心法
  1. 不要相信训练准确率——它几乎总是过分乐观
  2. 永远用测试集做最终评估——而且只用一次
  3. 选指标看业务关心什么——医疗看 Recall,反垃圾看 Precision
  4. 盯紧训练 vs 测试的差距——大差距 = 过拟合警报
  5. 交叉验证看稳定性——CV 标准差大 = 模型/数据有问题

到这里,L2 路径起步部分(7 篇)完结。再后面的 L2-08 (SVM)、L2-10 (特征工程)、L2-11 (端到端项目) 我们留给后续章节。

L1 + L2 学完,你已经具备真正的 ML 工程师入门能力——能跑通完整项目、解决真实业务问题。

下一步推荐:L3 路径(深度学习核心)—— 已经写好了几篇关键的(Attention、CNN)。

📬

读到这里说明你认真在学 🎯

订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。

💬

讨论区

· 用 GitHub 账号登录评论
⚠️ Giscus 评论未配置 —— 在 src/components/Comments.astro 顶部填入 仓库 ID 和分类 ID(见组件注释里的配置步骤)。