1. 先搞清楚模型在“闹脾气”:欠拟合与过拟合的本质
如果你已经跑通了几个经典的机器学习入门项目,比如用sklearn做线性回归、用逻辑回归做分类,大概率会遇到一种让人很头疼的情况:训练集上表现很好,一到测试集上就拉胯;或者反过来,训练集上误差就很大,怎么调都降不下去。这两类问题,前者叫过拟合(Overfitting),后者叫欠拟合(Underfitting)。很多新手会把它们当成“模型不行”或者“数据不行”,但实际上,这俩是机器学习里最核心、也最值得花时间搞懂的诊断问题。
先说欠拟合。它本质上就是模型能力不够,连训练集里的规律都没学明白。就好比你让一个实习生去处理一份他完全没接触过的行业报表,他连表头都看不懂,自然给不出靠谱的结果。模型也是一样,当模型复杂度太低、特征太少、或者训练轮次不够时,它在训练集和测试集上的误差都会偏高,这是它的“能力天花板”太低导致的。
过拟合则完全相反,是模型“太聪明”了,聪明到把训练集里的噪音、异常点、个别样本的偶然规律都当成了一般的规律去学习。你可以把它理解成一个考试前把参考答案逐字逐句背下来的学生——遇到原题他能考满分,但只要题目稍微变一下,他就懵了。在机器学习里,这个“变一下”就是测试集和训练集的分布差异。过拟合的典型表现是:训练集误差极低甚至为0,测试集误差却高得离谱,模型的泛化能力很差。
我见过太多初学者在这上面走弯路。有人一看训练集效果好就兴冲冲上线,结果线上效果被打回原形;有人一看到测试集效果差,就开始疯狂加数据、加特征、调参,折腾了一个月才发现问题出在模型根本没有学到有效信息上。所以,先把欠拟合和过拟合的识别搞清楚,是比调参更优先的事情。
好在,识别这两种问题并不需要什么高深的手段,最实用的方法就是画学习曲线,用训练误差和验证误差随数据量或训练轮次的变化趋势来判断。这也是这一节想带你先掌握的核心能力:不是急着优化,而是先看明白模型到底处于什么状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一招诊断模型状态:学习曲线的原理与画法
2.1 学习曲线到底在看什么
学习曲线(Learning Curve)是最直观、也最不容易出错的模型状态诊断工具。它的横轴通常是训练样本数量(也有用训练轮次的),纵轴是误差或者准确率,然后分别画出模型在训练集和验证集上的表现随数据量变化的两条曲线。
正常情况下,随着训练样本增加,训练集误差会逐渐上升(因为数据变多,模型更难把每个样本都背下来),验证集误差会逐渐下降(因为模型学到了更多真实的规律,泛化能力变强),最后两条曲线会趋近于一个相同或者接近的值,那个值就代表了这个模型在当前特征组合下的“真实水平上限”。
如果两条曲线之间一直隔着一道鸿沟——训练集误差很低、验证集误差很高——那就要警惕过拟合了。如果两条曲线都高位徘徊、怎么都不往下走,那就是欠拟合。
我在实际项目里一般会用sklearn的learning_curve函数来画,它可以自动做K折交叉验证,帮你算不同训练集大小下模型在训练集和验证集上的平均得分,比自己手动切数据要靠谱得多,因为用了交叉验证,结果更稳、更不容易被某一次随机的数据切分带偏。
2.2 三类典型状态的长什么样
把模型状态分成三类来看,最清晰:
第一类,欠拟合。 训练集和验证集的误差都很高,两条曲线在图表上方贴得很近。这种时候你加再多的训练数据也没用,因为模型根本没那个容量去学更多东西。正确的解法是提升模型复杂度,比如用多项式特征替代线性特征、增加神经网络的层数或神经元数量、减少正则化强度,或者换一个更强的模型。
第二类,过拟合。 训练集误差一路走低、低到令人发指,验证集误差却在某个点之后开始反弹或者居高不下。两条曲线之间有一个很大的“gap”。这种时候加数据是有效果的,但很多时候数据不好搞,更快的办法是正则化、Dropout、早停(Early Stopping)或者特征选择。
第三类,正常。 两条曲线最终收敛在一个较低的位置,而且差值不大。这个状态下的模型基本上就是“训练到位了”。
画学习曲线这件事,花不了十分钟,但它能帮你少走好几天的弯路。我自己的习惯是,每跑一个新模型,先画一条学习曲线看看状态,再决定下一步是加数据、加特征、加复杂度,还是上正则化。下面给出一段可以跑的代码,直接换成你自己的数据和模型就能用。
python复制import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.linear_model import LinearRegression
# 这里用波士顿房价数据做演示,实际使用时换成自己的数据集
from sklearn.datasets import load_diabetes
data = load_diabetes()
X, y = data.data, data.target
train_sizes, train_scores, valid_scores = learning_curve(
LinearRegression(), X, y,
train_sizes=np.linspace(0.1, 1.0, 10),
cv=5, scoring='neg_mean_squared_error'
)
train_errors = -train_scores.mean(axis=1)
valid_errors = -valid_scores.mean(axis=1)
plt.figure(figsize=(8, 5))
plt.plot(train_sizes, train_errors, 'o-', label='Train Error')
plt.plot(train_sizes, valid_errors, 's-', label='Validation Error')
plt.xlabel('Training Set Size')
plt.ylabel('MSE')
plt.legend()
plt.title('Learning Curve')
plt.show()
提示:
learning_curve在sklearn 1.2之前可以用scoring='neg_mean_squared_error',之后的版本记得检查sklearn的版本兼容性。另外,如果你的样本量很大,比如几十万条,学习曲线的计算会比较慢,可以适当减少train_sizes的取值个数。
如果你发现画出来的曲线趋势不明确,也有一个可能:数据划分或预处理出了问题。比如你在做多项式特征时,直接用fit_transform处理了全部数据,而不是在训练集上fit完再对测试集transform,这会导致数据泄漏,让验证集结果虚高,学习曲线的参考价值就会大打折扣。这个问题在后面的实战部分还会再次遇到,务必留意。
3. L1/L2正则化:往损失函数里加一项,为什么能治过拟合
3.1 正则项的数学直觉
在学习曲线确认了模型过拟合之后,最常用的手段之一就是正则化(Regularization)。正则化的思路非常朴素:既然模型太复杂导致它把噪音也学进去了,那我们就给它加点“惩罚”,让它不敢轻易把参数变得很大。
神经科学的类比可能不太好懂,换个生活化的说法。你请了一个非常健谈的顾问,他能把你公司过去三年的每条OKR细节、每个人的考勤异常全部复述出来,显得很专业,但真正遇到新问题时,他给出的建议全是从旧数据里翻出来的,没有一句话能落地。这时候你要在他出报告时加一条规矩:每条建议必须足够简洁、通用,用词不能太偏激,如果太“个性化”就扣钱。顾问为了不被扣钱,就不敢把话说得太满、太细,只能挑真正稳定的规律来讲。正则化干的就是这件事——它在损失函数里加了一个“扣钱项”,模
