第十六篇:如何防止过拟合------从剪枝、正则化到早停,模型到底该怎么"收一收"
上一篇我们讲了过拟合。
如果只用一句话概括,过拟合就是:
模型把训练集学得太细了,细到连噪声和偶然性都当成了规律。
所以它在训练集上看起来特别好,换到新数据上却不一定行。
讲到这里,问题其实就顺理成章了:
那既然知道模型会学过头,我们该怎么防?
很多人第一次接触这个问题时,容易下意识地觉得:
- 那是不是换个更高级的模型?
- 是不是多训练一会儿?
- 是不是把参数再调大一点?
但真正有效的方向,往往不是"继续加",而是学会收一收。
这也是机器学习里一个很有意思的地方:
模型不总是越强越好,很多时候你真正需要的是让它别太放飞。
这一篇就专门讲这个。
1. 防止过拟合,先要明白你到底在和什么对抗
如果你只是把"防过拟合"理解成几个零散技巧,比如:
- 正则化
- 早停
- 剪枝
- Dropout
- 数据增强
那学起来会很碎。
更好的方法是,先把它们背后的共同目标看清楚。
这些方法虽然长得不一样,但本质上都在做类似的事:
第一类:限制模型别太复杂
比如:
- 决策树别长太深
- 参数别太大
- 特征别太多
- 模型自由度别太高
第二类:让模型别太相信训练集里的局部细节
比如:
- 通过随机性增加稳健性
- 通过更多数据削弱偶然性
- 通过验证集及时叫停训练
第三类:让模型更关注"稳定规律",少关注"局部噪声"
比如:
- 惩罚过大的参数
- 限制树分裂得太细
- 不让每一步纠错太猛
你会发现,这些方法其实都在围绕一件事:
别让模型把训练集当成世界的全部。
2. 最朴素也最常见的一招:控制模型复杂度
很多时候,过拟合最直接的原因就是:
模型表达能力太强了。
它强到不仅能学规律,还能把训练集里的细小波动都记下来。
所以防过拟合最自然的一件事,就是先别让模型复杂得太离谱。
在决策树里
你最容易做的,就是限制树别长太深。
比如用这些参数:
max_depthmin_samples_splitmin_samples_leafmax_leaf_nodes
它们本质上都在干同一件事:
别让树为了照顾少数样本,不停往下切。
因为树一旦切得太碎,就特别容易把训练集背下来。
在随机森林里
随机森林本来就比单棵树稳,但也不是完全不会过拟合。
你可以通过这些方式控制它:
- 不让单棵树太深
- 让叶子节点别太小
- 适当控制树的数量
- 控制每次分裂可用的特征数
它们的思路还是一样的:
森林可以有很多棵树,但每棵树别太"极端"。
在 GBDT / XGBoost / LightGBM 里
这里就更明显了。
因为提升树类模型是一步步往训练集上贴的,
如果你让每一棵树都很强、每一步都改得很猛,它就特别容易过拟合。
所以这里经常会控制:
- 树深
- 学习率
- 树的数量
- 采样比例
说到底,还是那句话:
模型别太贪。
在 SVM 里
虽然 SVM 和树模型不是一类东西,但它也一样有"别太激进"的问题。
比如:
C太大,模型会特别在意每个训练点都分对gamma太大,模型会特别关注局部细节
结果边界就会变得很紧、很弯、很容易贴着训练数据跑。
所以 SVM 里所谓"防过拟合",其实也是在防:
边界不要太贴训练样本。
3. 剪枝:为什么树模型特别喜欢这一招
说到防过拟合,树模型有个特别经典的做法:剪枝(pruning)
这个词很形象。
你可以把一棵决策树想成一棵真的树。
如果它一直长,不受限制,它会长出很多细枝末节。
这些细枝,有些是必要的,有些其实只是为了迎合训练集里的个别样本。
剪枝做的事情就是:
把那些不太有必要、会让模型变得过于具体的枝条剪掉。
这样做的目的不是让树"变弱",而是让它:
- 更简洁
- 更稳定
- 更不容易记住噪声
预剪枝
预剪枝就是在树生长的过程中提前设限。
比如:
- 最多长几层
- 一个节点样本数太少就别分了
- 分裂带来的收益太小就停
它的优点是简单、训练快。
缺点是有时候会剪得太早,把本来有用的结构也挡住。
后剪枝
后剪枝是先让树长出来,再回头砍掉那些没什么必要的分支。
它的思路更像:
- 先把树长充分
- 再看哪些分支只是让训练集更漂亮,但对验证集没帮助
- 把这些分支砍掉
相比预剪枝,后剪枝通常更细一点。
不过在很多实际库里,大家往往更常直接用预剪枝参数来控制复杂度,因为实现和使用都更方便。
4. 正则化:为什么要主动惩罚"太夸张"的模型
正则化这个词,很多人第一次听会觉得有点抽象。
其实它背后的想法非常朴素:
如果模型某些参数大得离谱,或者结构复杂得离谱,那我们就额外罚它一下。
也就是说,除了原本的损失函数,我们再加一个"别太夸张"的惩罚项。
于是模型优化的就不再只是:
- 把训练误差做到最低
而是变成:
- 在误差和复杂度之间做平衡
这就是正则化的核心味道。
5. L1 和 L2,最值得抓住的直觉是什么
如果你后面会写更系统的补充篇,这里正文其实不用推太多公式,但可以把直觉讲清楚。
L2 正则化
它会惩罚参数太大。
最经典的形式像这样:
Loss=原始损失+λ∑wi2 Loss = 原始损失 + \lambda \sum w_i^2 Loss=原始损失+λ∑wi2
直觉上就是:
参数别太大,别让模型对某些特征过度依赖。
L2 往往会让模型变得更平滑、更稳一点。
L1 正则化
它惩罚的是参数绝对值:
Loss=原始损失+λ∑∣wi∣ Loss = 原始损失 + \lambda \sum |w_i| Loss=原始损失+λ∑∣wi∣
L1 的一个很特别的效果是,它更容易把一部分参数直接压到 0。
所以很多时候它不仅在防过拟合,还带一点"做特征选择"的味道。
正则化的本质,不是让模型学得更少,而是让模型别太极端。
它在提醒模型:你可以努力拟合数据,但不要把某几个局部特征用得过猛,也不要把边界搞得太紧。
6. 早停:有时候模型不是不会学,而是学太久了
这个方法特别适合接上一章。
上一章我们说过,很多模型训练时会出现一种现象:
- 训练集表现一直在变好
- 验证集表现先变好,后变差
这说明什么?
说明模型一开始确实在学习规律,
但后面慢慢开始钻训练集细节了。
这时候,一个特别实用的方法就是:早停(early stopping)
意思很简单:
别让它一直学到最后,差不多该停的时候就停。
为什么早停有效
因为很多模型在训练中并不是"越往后越聪明",而是:
- 前期学主要规律
- 后期开始记局部噪声
所以如果你盯着验证集表现,当它不再变好、甚至开始变差时,就停下来,往往会得到一个泛化更好的模型。
这在下面这些模型里特别常见:
- 神经网络
- GBDT / XGBoost / LightGBM
- boosting 类方法
它本质上在防什么
它防的不是"训练时间太长",而是:
模型开始把额外的学习能力浪费在训练集细节上。
所以早停其实也是一种"控制复杂度"的方式,只不过它控制的不是树深和参数数量,而是:
控制你让模型继续往训练集上贴的时间。
7. 数据增强:不是让模型更辛苦,而是让规律更稳定
这个方法在图像、文本、语音里特别常见,但本质不只限于深度学习。
数据增强的思路是:
给模型看更多"变化后的合理样本",让它别把原始训练样本的某种固定样子当成唯一规律。
比如在图像里:
- 翻转
- 裁剪
- 旋转
- 加一点噪声
如果一张猫的图片轻微旋转一下,它还是猫。
模型如果连这一点变化都扛不住,说明它学到的不是"猫",而是训练图片的某种固定角度和纹理。
所以数据增强本质上是在告诉模型:
你别死记这几个样本长什么样,
你要学的是更稳定的模式。
8. 更多数据,为什么往往真的有用
这一点几乎是所有防过拟合方法里最"朴素但有效"的一个。
如果训练数据太少,模型很容易把训练集里的偶然性当规律。
因为它见到的世界本来就很窄。
但如果你有更多、更丰富、更有代表性的数据,很多偶然性就会被冲淡。
你可以这样理解:
- 少量数据里,一个巧合看起来像规律
- 大量数据里,这个巧合往往会暴露出它只是巧合
所以更多数据的价值在于:
它能让真正的规律更稳定地浮出来,也能让噪声更难伪装成规律。
当然,这里不是说"数据一多就天下无敌"。
如果数据质量很差、标签很乱、分布有问题,光加数量也不一定能救。
但在很多场景里,数据量增加确实是最根本的抗过拟合手段之一。
9. 特征太多时,为什么也容易过拟合
这个点很值得单独提一下。
很多人会觉得:
特征越多,信息不是越全吗?
理论上可能是。
但现实里,特征越多,不代表有用信息就越多。
很多时候会一起增加的是:
- 冗余特征
- 噪声特征
- 偶然相关的特征
- 训练集特有的小信号
模型一旦能力比较强,就可能会从这些额外特征里"挖出"一些只在训练集上成立的假模式。
于是你表面上觉得自己在"喂更多信息",
实际上是在给模型更多机会去过拟合。
所以防过拟合的一个常见思路就是:
别什么特征都塞进去。
适当做特征选择、特征筛选、去掉明显没用或高度噪声的变量,往往反而会让模型更稳。
10. Dropout、采样、随机性,这些方法为什么也能防过拟合
这一类方法表面上看很杂,但背后的共同味道其实是:
别让模型过于依赖某个局部结构。
比如:
Dropout
在神经网络里,训练时随机让一部分神经元暂时失效。
这会迫使网络别把所有希望都押在某几个固定通路上。
随机森林里的随机采样
让每棵树看到不同样本、不同特征。
这样单棵树就更难把训练集里的某种局部巧合学成"唯一真理"。
GBDT 里的 subsample / colsample
也是类似思路,用随机性减少模型对某些细节的过度依赖。
所以随机性并不是"故意让模型变笨",而是:
防止模型太死盯某一块训练数据。
11. 防过拟合不是只靠一个技巧,而是几种方法一起配合
这一点特别值得你在文章里点出来。
很多初学者会问:
到底哪一种方法最有效?
其实很多时候,没有单一银弹。
更真实的情况通常是:
- 用合适的数据划分和交叉验证,先确认问题是不是过拟合
- 再控制模型复杂度
- 再加一点正则化
- 必要时用早停
- 同时检查数据和特征质量
也就是说,防过拟合更像是一套组合拳,而不是某一个按钮。
因为过拟合本身就不止一个来源:
- 可能是模型太强
- 可能是数据太少
- 可能是噪声太大
- 可能是训练太久
- 可能是特征太杂
所以对应地,解决方案也不太可能只有一个。
12. 什么时候你该怀疑自己需要"收一收"
这部分可以写得很实用,因为读者自己做实验时最需要这种判断感。
如果你看到这些情况,就该开始警惕:
情况一
训练集效果明显高于验证集,而且差距越来越大。
情况二
你把模型变复杂以后,训练集继续变好,但验证集不升反降。
情况三
你不断堆树、加层、增大参数,结果测试集没跟着变好。
情况四
模型特别依赖少数几个奇怪特征,或者重要性结果和常识完全冲突。
情况五
你的模型对训练集里的个别样本特别敏感,稍微变一点结果就大变。
这些都很像模型已经开始"上头"了。
这时候,与其继续加码,不如先想想:
是不是该限制一下模型自由度了。
13. 用一句话概括每种方法到底在防什么
这一段很适合放在文章后半,读者会一下子清楚很多。
- 剪枝:防树长得太碎
- 正则化:防参数太大、边界太激进
- 早停:防模型学太久,开始记噪声
- 更多数据:防偶然性伪装成规律
- 特征选择:防没用特征给模型制造假信号
- 随机采样 / Dropout:防模型过度依赖局部细节
- 交叉验证:防你误以为模型真的很好
你看,它们其实都在服务同一件事:
让模型学得稳一点,而不是学得死一点。
14. 这一篇最想传达的,其实不是"招式",而是一种训练观
如果你把前面所有方法都只当成技巧,会有点散。
但如果你把它们看成一种共同的训练观,就会顺很多。
这种训练观就是:
模型不是越努力拟合训练集越好。
真正重要的是,它学到的东西能不能离开训练集以后还成立。
所以"防止过拟合"不是在压制模型进步,
而是在帮模型把精力放到更值得学的地方。
你可以把它理解成一种纠偏:
- 当模型开始痴迷训练集细节时,把它拉回来
- 当模型开始把噪声当规律时,把它拉回来
- 当模型开始只会做旧题时,把它拉回来
这件事,其实比"把训练误差再降一点"更重要。
最后我们用一个非常常见的方法来展示:
通过限制树的深度,防止决策树过拟合。
这个例子很贴合前面写的树模型系列。
代码示例
python
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 构造数据
np.random.seed(42)
X = np.random.rand(200, 2)
y = (X[:,0] + X[:,1] > 1).astype(int)
# 加一点噪声
noise = np.random.choice(200, 20)
y[noise] = 1 - y[noise]
# 划分数据
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 两种模型
model_simple = DecisionTreeClassifier(max_depth=4)
model_complex = DecisionTreeClassifier(max_depth=10)
# 训练
model_simple.fit(X_train, y_train)
model_complex.fit(X_train, y_train)
# 预测
pred_simple_train = model_simple.predict(X_train)
pred_simple_test = model_simple.predict(X_test)
pred_complex_train = model_complex.predict(X_train)
pred_complex_test = model_complex.predict(X_test)
# 输出结果
print("简单模型")
print("训练集准确率:", accuracy_score(y_train, pred_simple_train))
print("测试集准确率:", accuracy_score(y_test, pred_simple_test))
print("\n复杂模型")
print("训练集准确率:", accuracy_score(y_train, pred_complex_train))
print("测试集准确率:", accuracy_score(y_test, pred_complex_test))
这个例子会出现什么现象
通常你会看到类似结果:
简单模型
训练集准确率:0.88
测试集准确率:0.85
训练和测试差距不大。
说明模型比较稳。
复杂模型
训练集准确率:0.99
测试集准确率:0.80
训练集几乎完美,但测试集反而更差。
说明模型开始记训练集细节。
这就是过拟合。
树太深的时候,它会试图把每一个训练样本都分对。
包括那些带噪声的样本。
结果是训练集越来越好,但模型学到的规则越来越不稳定。
通过限制树的深度,相当于告诉模型:
不要为了少数样本,把整体规律搞复杂。