哈喽,大家好~
为什么训练集准确率越来越高,测试集反而变差了?
这是机器学习面试里很常见的问题。很多时候,问题不在于模型学得不够,而在于模型已经“学过头”了。
模型复杂度到底是什么?
我们可以把模型理解成一条“根据输入预测输出的规律”。
比如,我们想根据房屋面积预测房价:
这里, 是输入特征, 是模型预测值, 是模型需要学习的参数。
如果我们使用一次函数:
模型只能学到一条直线,表达能力比较弱。
如果我们使用多项式:
其中 是多项式的次数。 越大,模型能够画出的曲线越复杂,模型复杂度也越高。
你可以这样理解:
-
低复杂度模型:像拿直尺画趋势,容易欠拟合。 -
高复杂度模型:像拿一根很灵活的曲线去贴数据,容易过拟合。
复杂度并不只由参数数量决定。决策树的深度、神经网络的层数和参数量、正则化强度,都可能影响模型复杂度。
复杂度为什么会影响泛化?
模型训练时,通常会尽量降低训练误差:
模型越复杂,通常越容易把训练集拟合得很好。但训练数据里不仅有真正的规律,也包含噪声。
当模型复杂度过高时,它可能把噪声也当成规律记住了。
模型的期望误差,可以简单拆成:
Bias 是偏差,表示模型本身过于简单;Variance 是方差,表示模型对训练数据变化过于敏感;Noise 是数据中无法消除的噪声。
一句话概括:模型太简单,学不到规律;模型太复杂,连噪声也学进去了。我们真正想找的是复杂度和泛化能力之间的平衡点。
完整案例
我们构造一组带噪声的非线性数据,真实规律大致是:
其中 表示随机噪声。
接着分别使用 1 次、3 次和 15 次多项式进行拟合,再观察训练误差和测试误差的变化。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
np.random.seed(42)
# 构造带噪声的非线性数据
X = np.linspace(-3, 3, 190).reshape(-1, 1)
y = np.sin(X[:, 0]) + 0.15 * X[:, 0] ** 2
y += np.random.normal(0, 0.18, size=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, 19)
train_errors, test_errors = [], []
# 计算不同复杂度下的误差
for degree in degrees:
model = make_pipeline(
PolynomialFeatures(degree),
LinearRegression()
)
model.fit(X_train, y_train)
train_errors.append(mean_squared_error(
y_train, model.predict(X_train)
))
test_errors.append(mean_squared_error(
y_test, model.predict(X_test)
))
# 绘图
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左图:不同复杂度的拟合效果
colors = ["#2563EB", "#F97316", "#DC2626"]
for degree, color in zip([1, 3, 15], colors):
model = make_pipeline(
PolynomialFeatures(degree),
LinearRegression()
)
model.fit(X_train, y_train)
x_line = np.linspace(-3, 3, 500).reshape(-1, 1)
y_line = model.predict(x_line)
axes[0].plot(
x_line, y_line, color=color, linewidth=2.5,
label=f"degree={degree}"
)
axes[0].scatter(
X_train, y_train, color="#06B6D4",
edgecolor="white", s=45, label="train data"
)
axes[0].scatter(
X_test, y_test, color="#FACC15",
edgecolor="#111827", s=55, label="test data"
)
axes[0].set_title("Model Fits with Different Complexity")
axes[0].set_xlabel("x")
axes[0].set_ylabel("y")
axes[0].legend()
# 右图:复杂度与误差
axes[1].plot(
degrees, train_errors, marker="o",
color="#16A34A", linewidth=2.5, label="train MSE"
)
axes[1].plot(
degrees, test_errors, marker="o",
color="#E11D48", linewidth=2.5, label="test MSE"
)
axes[1].set_title("Complexity vs Error")
axes[1].set_xlabel("Polynomial Degree")
axes[1].set_ylabel("MSE")
axes[1].legend()
plt.tight_layout()
plt.show()
左图主要看模型画出的曲线。
1 次多项式只能画直线,无法跟随真实的波动,属于欠拟合。3 次多项式能够抓住整体趋势,通常是比较合适的复杂度。15 次多项式会出现明显弯折,甚至为了穿过训练样本而剧烈摆动,这就是过拟合。
右图更直观。随着多项式次数增加,训练误差通常持续下降;但测试误差往往先下降、后上升。
测试误差最低的位置,就是当前数据下更值得选择的复杂度。注意,这个位置不是固定的,它会受到样本数量、噪声大小和特征质量影响。
实际项目中如何控制复杂度?
模型复杂度不是越低越好,也不是越高越好。我们通常通过验证集或交叉验证选择合适的复杂度,而不是直接根据训练集表现做判断。
常见方法包括:
-
限制决策树最大深度、叶子节点数量。 -
降低神经网络层数或参数量。 -
使用 L1、L2 正则化。 -
增加训练数据,降低模型对单批数据的敏感程度。 -
使用交叉验证比较不同超参数。
以正则化为例,目标函数可以写成:
越大,对复杂参数的惩罚越强,模型通常越平滑; 太大又可能让模型变得过于简单。
最后
模型复杂度决定了模型表达规律的能力。复杂度太低会欠拟合,复杂度太高会过拟合,测试集误差最低的区域通常才是我们真正关心的地方。
你可以继续尝试两个方向:把上面的 LinearRegression 换成带 L2 正则化的 Ridge,观察正则化如何抑制高次多项式;或者增加数据噪声和样本数量,看看复杂度曲线会发生什么变化。

