哈喽,大家好~
大家是否遇到过,为什么训练集准确率这么高,到了测试集却完全不行?
模型越复杂,效果一定越好吗?
我们就用一个简单但非常典型的例子,把机器学习里的欠拟合、正常拟合和过拟合一次讲清楚~
模型到底在学习什么?
我们先假设有一组数据:
-
输入 :房屋面积、用户年龄、图片像素等特征 -
输出 :房价、是否购买、图片类别等目标
模型要做的事情,就是找到一个函数:
让预测值 尽量接近真实值 。
比如真实数据大致符合一条曲线:
其中 是噪声,表示现实数据中不可避免的误差。
模型训练时,会不断调整参数,让损失函数变小。最常见的均方误差是:
一句话理解:预测值和真实值差得越远,损失就越大。
什么是欠拟合和过拟合?
我们用多项式模型来举例。
一次多项式只能画出一条直线:
如果真实数据是一条弯曲的曲线,直线就太简单了,连训练数据都解释不好,这就是欠拟合。
欠拟合通常表现为:
-
训练误差较高 -
测试误差也较高 -
模型过于简单,偏差较大
如果我们把模型改成 20 次多项式,它几乎可以穿过所有训练样本,训练误差非常低。但它也会把噪声当成规律,遇到新数据就表现很差,这就是过拟合。
过拟合通常表现为:
-
训练误差非常低 -
测试误差较高 -
模型过度记忆训练数据,方差较大
可以这样理解,欠拟合是“没学会”,过拟合是“背下来了”,正常拟合才是“真正学会了”。
从误差分解角度看:
模型太简单,偏差高;模型太复杂,方差高。我们真正想找的是两者之间的平衡。
Python实现
下面构造一组带噪声的正弦数据,分别训练一次、五次和十八次多项式模型。
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, StandardScaler
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
np.random.seed(42)
# 构造虚拟数据:真实规律是 sin(x),同时加入噪声
X = np.linspace(-3, 3, 80).reshape(-1, 1)
y = np.sin(X).ravel() + np.random.normal(0, 0.18, 80)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
def build_model(degree):
return make_pipeline(
PolynomialFeatures(degree=degree),
StandardScaler(),
Ridge(alpha=0.01)
)
degrees = [1, 5, 18]
x_line = np.linspace(-3, 3, 500).reshape(-1, 1)
fig, axes = plt.subplots(1, 3, figsize=(16, 4))
for ax, degree in zip(axes, degrees):
model = build_model(degree)
model.fit(X_train, y_train)
train_pred = model.predict(X_train)
test_pred = model.predict(X_test)
line_pred = model.predict(x_line)
train_mse = mean_squared_error(y_train, train_pred)
test_mse = mean_squared_error(y_test, test_pred)
ax.scatter(X_train, y_train, color="#00BFFF",
edgecolor="white", s=45, label="训练数据")
ax.scatter(X_test, y_test, color="#FF4B5C",
edgecolor="white", s=45, label="测试数据")
ax.plot(x_line, line_pred, color="#7B2CBF",
linewidth=3, label="模型曲线")
ax.set_title(f"{degree}次多项式\nTrain={train_mse:.3f}, Test={test_mse:.3f}")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(alpha=0.2)
ax.legend()
plt.suptitle("欠拟合、正常拟合与过拟合", fontsize=16)
plt.tight_layout()
plt.show()
# 比较不同复杂度下的训练误差和测试误差
all_degrees = range(1, 21)
train_errors, test_errors = [], []
for degree in all_degrees:
model = build_model(degree)
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))
)
plt.figure(figsize=(9, 5))
plt.plot(all_degrees, train_errors, "o-", color="#00C853",
linewidth=2.5, label="训练误差")
plt.plot(all_degrees, test_errors, "o-", color="#FF6D00",
linewidth=2.5, label="测试误差")
plt.axvline(5, color="#2962FF", linestyle="--",
label="较合适的复杂度")
plt.xlabel("多项式阶数")
plt.ylabel("MSE")
plt.title("模型复杂度与误差变化")
plt.xticks(list(all_degrees))
plt.grid(alpha=0.25)
plt.legend()
plt.show()
第一张图里,1 次多项式基本是一条直线,无法跟随真实曲线,这是欠拟合。5 次多项式能够抓住整体趋势,同时没有过度追踪噪声,通常是比较合适的模型。18 次多项式会产生明显的波动,训练点附近表现很好,但泛化能力下降,这就是过拟合。
第二张图展示了一个很重要的规律:随着模型复杂度增加,训练误差通常持续下降;测试误差则会先下降,达到最低点后再次上升。测试误差最低的位置,就是模型复杂度比较合适的区域。
如何解决过拟合?
实际项目中,我们可以从几个方向入手。
第一,减少模型复杂度,比如降低树的深度、减少神经网络层数、降低多项式阶数。
第二,增加训练数据。数据越丰富,模型越不容易只记住少量样本。
第三,使用正则化。以 Ridge 回归为例,它会在原损失后增加参数惩罚:
其中 是正则化强度。 越大,对复杂参数的限制越强,模型曲线也会更平滑。
不过正则化不是越大越好。太强会让模型重新变得过于简单,出现欠拟合,所以需要通过验证集或交叉验证选择参数。
小结
欠拟合说明模型太简单,过拟合说明模型太复杂。我们不能只看训练集效果,而要重点观察模型在新数据上的表现。
实际训练时,可以结合验证集、交叉验证、正则化和学习曲线,找到模型复杂度与泛化能力之间的平衡。接下来大家还可以尝试把 Ridge 换成 Lasso,或者使用决策树、随机森林,对比不同模型的过拟合表现。

