哈喽,大家好~
“模型在训练集上准确率都 99% 了,为什么一换数据就不行?”
这个问题,大家几乎都遇到过。因为它考察的不是某个算法,而是你是否真正理解了机器学习~
这篇文章我们就来聊聊机器学习里的一个核心能力:泛化能力。
简单说,就是模型不仅能做好见过的题,还能把学到的规律应用到没见过的新数据上。
01什么是泛化能力?
我们先用一个生活中的例子理解~
如果一个学生把课本上的 100 道题答案全部背下来,考试时原题出现,他当然能考满分。但题目稍微换个数字,他就不会了。
这就是“记住训练数据”,但没有真正学会规律。
机器学习里的模型也是一样。训练集就像平时做过的题,测试集则像真正的考试。模型在新数据上的表现,才体现了它的泛化能力。
通常我们会把数据分成:
-
训练集:用来学习模型参数; -
验证集:用来选择模型结构和超参数; -
测试集:最后评估模型的真实表现。
模型在训练集上表现很好,但在测试集上表现很差,这种情况叫做过拟合。
反过来,如果训练集和测试集表现都很差,说明模型过于简单,连训练数据中的基本规律都没有学会,这叫做欠拟合。
一句话概括:泛化能力强的模型,训练误差不一定最低,但测试误差通常比较稳定。
02泛化能力背后的原理
假设我们希望模型学习一个真实规律 ,其中 是数据本身的规律, 是噪声。
模型最终的预测误差,通常可以理解为三部分:
这里的含义是:
-
偏差:模型太简单,无法表达真实规律; -
方差:模型太复杂,过度依赖当前训练集; -
不可约误差:数据本身的噪声,模型无法消除。
比如用一条直线拟合明显弯曲的数据,偏差会很大;如果用一个次数很高的多项式,模型甚至会把每个噪声点都连起来,方差就会变大。
我们训练模型时真正关心的不是训练误差:
而是模型在未知数据上的期望误差:
前一个公式计算模型在手头数据上的平均损失,后一个公式描述模型面对真实数据分布时的平均损失。
当然,真实分布 我们拿不到,所以只能通过验证集、测试集和交叉验证来近似判断。
03观察过拟合
下面我们构造一组带噪声的正弦数据,再使用不同次数的多项式回归进行拟合。
模型复杂度较低时,可能学不出曲线;复杂度太高时,又会开始追着噪声跑。
我们用两张图观察这个过程:
-
左图比较低阶模型和高阶模型的拟合效果; -
右图展示多项式次数变化时,训练误差和测试误差的变化。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
np.random.seed(42)
# 数据:真实规律是 sin 曲线,同时加入噪声
X = np.linspace(0, 10, 160).reshape(-1, 1)
y = np.sin(X[:, 0]) + 0.25 * np.random.randn(len(X))
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=7
)
degrees = range(1, 16)
train_errors, test_errors = [], []
models = {}
for degree in degrees:
# Ridge 可以抑制过高阶多项式带来的参数爆炸
model = make_pipeline(
PolynomialFeatures(degree),
Ridge(alpha=0.01)
)
model.fit(X_train, y_train)
train_pred = model.predict(X_train)
test_pred = model.predict(X_test)
train_errors.append(mean_squared_error(y_train, train_pred))
test_errors.append(mean_squared_error(y_test, test_pred))
models[degree] = model
x_line = np.linspace(0, 10, 500).reshape(-1, 1)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 图一:低复杂度与高复杂度模型的拟合结果
axes[0].scatter(X_train, y_train, color="#00B4D8",
label="Train data", alpha=0.75)
axes[0].scatter(X_test, y_test, color="#FF006E",
label="Test data", alpha=0.85)
for degree, color in [(2, "#06D6A0"), (14, "#8338EC")]:
axes[0].plot(
x_line,
models[degree].predict(x_line),
color=color,
linewidth=3,
label=f"Degree {degree}"
)
axes[0].set_title("Model complexity and fitting")
axes[0].set_xlabel("x")
axes[0].set_ylabel("y")
axes[0].legend()
# 图二:训练误差与测试误差
axes[1].plot(degrees, train_errors, "o-",
color="#118AB2", linewidth=2.5, label="Train MSE")
axes[1].plot(degrees, test_errors, "o-",
color="#EF476F", linewidth=2.5, label="Test MSE")
best_degree = degrees[np.argmin(test_errors)]
axes[1].axvline(best_degree, color="#FF9F1C",
linestyle="--", linewidth=2,
label=f"Best degree = {best_degree}")
axes[1].set_title("Generalization error curve")
axes[1].set_xlabel("Polynomial degree")
axes[1].set_ylabel("Mean squared error")
axes[1].legend()
plt.tight_layout()
plt.show()
左图中,2 次多项式通常比较平滑,可能无法充分表达正弦曲线,属于偏差较大的情况。14 次多项式会努力穿过更多训练点,曲线可能出现剧烈波动,这就是模型开始记忆噪声。
右图更直观:随着模型复杂度增加,训练误差通常持续下降;但测试误差往往先下降、后上升。测试误差最低的位置,就是当前数据下泛化效果相对较好的复杂度。
这也是机器学习中非常重要的现象:训练误差最低的模型,不一定是泛化能力最强的模型。
04如何提升泛化能力?
我们这里可以从几个方向入手。
第一,增加有效数据。数据越丰富,模型越不容易把偶然噪声当成规律。
第二,控制模型复杂度。例如降低树的深度、减少神经网络参数,或者使用正则化。
以 Ridge 回归为例,它会在原始损失后增加参数惩罚:
其中 控制惩罚强度。 越大,模型越倾向于使用较小的参数,从而减少过拟合。
第三,使用交叉验证。把训练数据分成多份,轮流训练和验证,可以减少一次划分带来的偶然性。
第四,不要反复查看测试集再调参。测试集应该像真正的期末考试,只在最后使用一次,否则测试集也会逐渐变成“训练资料”。
总结
泛化能力,就是模型从已知数据中学习规律,并在未知数据上保持稳定表现的能力。模型太简单会欠拟合,模型太复杂会过拟合,真正合适的模型通常处在两者之间。
接下来你可以尝试修改代码中的噪声大小,观察测试误差如何变化;也可以把 Ridge 换成普通线性回归,对比正则化对泛化能力的影响。总之,评价一个模型不能只看训练集分数,更要看它面对新数据时是否依然可靠。

