大数跨境

导师:“模型训练集都快学满分了,你还高兴!” 我:“说明学得好啊!” 导师:“错,这种时候最该先查过拟合和正则化!”

导师:“模型训练集都快学满分了,你还高兴!” 我:“说明学得好啊!” 导师:“错,这种时候最该先查过拟合和正则化!” 机器学习和人工智能AI
2026-10-25
3

哈喽,大家好~

在做机器学习时,我们常常会遇到一个尴尬的现象:模型在训练集上表现很好(损失很低),但是到了验证集或真实数据上表现很差,也就是“过拟合”。

直观上来看,模型“记住”了训练数据里的噪声或偶然性,反而学不到普适的规律。

那么,正则化(Regularization)就是为了解决过拟合的一类方法。

简单来说,正则化就是“给模型加点惩罚或约束”,不让模型学得太自由、太复杂,从而让模型学到更稳健、更能泛化的规律。

常见的正则化手段包括:

  • 在目标函数里加入权重惩罚(L2 / L1),比如把模型参数的尺寸越大就越“罚款”;
  • 在神经网络中使用 Dropout,训练时随机丢掉一部分神经元;
  • 早停(Early stopping):当验证误差不再下降就停;
  • 数据增广(Data augmentation)也能被视为一种正则化手段;
  • 限制模型结构(比如降低网络宽度、深度)等。

核心逻辑可以用一句话总结:在损失函数里加一个“复杂度项”,目标变成在拟合数据和保持模型简单之间找到平衡。

数学上常写成:

其中   是原始的经验损失(如 MSE),  是正则化项(如   或  ),  是超参数,用来控制“罚多罚少”。

  • L2 正则化(Ridge / weight decay): ,鼓励参数较小且均匀分布。
  • L1 正则化(Lasso): ,鼓励参数稀疏(很多参数变成零),常用于特征选择。
  • Dropout:在训练时随机让一部份神经元失活,相当于训练时在子网络上做训练,最后平均效果能降低过拟合。

完整案例

我们构造一个简单问题:真实函数是平滑的  ,但我们用高阶多项式(比如 degree = 15)去拟合,从而易发生过拟合。

这里,咱们使用 PyTorch 训练几种模型并比较。

生成数据(训练/验证)和高阶多项式特征:

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="whitegrid")
torch.manual_seed(0)
np.random.seed(0)
device = torch.device("cpu")

# 数据集
def make_data(n_samples=100, noise_std=0.15):
    x = np.random.uniform(-1, 1, size=(n_samples, 1))
    y = np.sin(2 * np.pi * x) + np.random.normal(0, noise_std, size=(n_samples, 1))
    return x.astype(np.float32), y.astype(np.float32)

# 多项式特征(degree 从 0 到 deg)
def poly_features(x, deg):
    # x: (N,1)
    N = x.shape[0]
    X = np.concatenate([x**i for i in range(deg+1)], axis=1)  # 包含常数项
    return X.astype(np.float32)

# 准备训练 / 验证 / 绘图网格
deg = 15
x_train, y_train = make_data(80)
x_val, y_val = make_data(80)
X_train = poly_features(x_train, deg)
X_val = poly_features(x_val, deg)

# 用于绘图的密集网格
x_plot = np.linspace(-1.2, 1.2, 400).reshape(-1,1).astype(np.float32)
X_plot = poly_features(x_plot, deg)

模型 1:线性回归(高阶多项式特征)- 不正则化

class PolyLinear(nn.Module):
    def __init__(self, in_dim):
        super().__init__()
        self.linear = nn.Linear(in_dim, 1, bias=True)
    def forward(self, x):
        return self.linear(x)

def train_model(model, Xtr, Ytr, Xval, Yval, l1_lambda=0.0, weight_decay=0.0, epochs=600, lr=0.01):
    model.to(device)
    optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)  # weight_decay 即 L2
    criterion = nn.MSELoss()
    train_loss_hist = []
    val_loss_hist = []
    for epoch in range(epochs):
        model.train()
        inputs = torch.from_numpy(Xtr).to(device)
        targets = torch.from_numpy(Ytr).to(device)
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        # L1 正则项手动加上(如果需要)
        if l1_lambda > 0:
            l1 = 0.0
            for name, p in model.named_parameters():
                if'weight'in name:
                    l1 = l1 + p.abs().sum()
            loss = loss + l1_lambda * l1
        loss.backward()
        optimizer.step()

        # 记录
        model.eval()
        with torch.no_grad():
            train_loss = criterion(model(torch.from_numpy(Xtr)), torch.from_numpy(Ytr)).item()
            val_loss = criterion(model(torch.from_numpy(Xval)), torch.from_numpy(Yval)).item()
        train_loss_hist.append(train_loss)
        val_loss_hist.append(val_loss)
    return model, train_loss_hist, val_loss_hist

用三种设置训练:无正则化、L2(weight_decay)、L1(手动)。

in_dim = deg + 1
# 1) 无正则化
model_none = PolyLinear(in_dim)
model_none, tr_none, val_none = train_model(model_none, X_train, y_train, X_val, y_val,
                                           l1_lambda=0.0, weight_decay=0.0, epochs=800, lr=0.01)

# 2) L2 正则(weight decay)
model_l2 = PolyLinear(in_dim)
model_l2, tr_l2, val_l2 = train_model(model_l2, X_train, y_train, X_val, y_val,
                                     l1_lambda=0.0, weight_decay=1e-3, epochs=800, lr=0.01)

# 3) L1 正则(手动)
model_l1 = PolyLinear(in_dim)
model_l1, tr_l1, val_l1 = train_model(model_l1, X_train, y_train, X_val, y_val,
                                     l1_lambda=1e-4, weight_decay=0.0, epochs=800, lr=0.01)

另外我们再训练一个小型神经网络并在隐藏层使用 Dropout 以对比(非线性模型):

class SmallNet(nn.Module):
    def __init__(self, in_dim, hidden=64, p_drop=0.5):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden),
            nn.ReLU(),
            nn.Dropout(p_drop),
            nn.Linear(hidden, hidden),
            nn.ReLU(),
            nn.Dropout(p_drop),
            nn.Linear(hidden, 1)
        )
    def forward(self, x):
        return self.net(x)

net_dropout = SmallNet(in_dim, hidden=64, p_drop=0.5)
net_dropout, tr_drop, val_drop = train_model(net_dropout, X_train, y_train, X_val, y_val,
                                             l1_lambda=0.0, weight_decay=0.0, epochs=800, lr=0.01)

画图函数与结果展示

# 辅助:预测曲线
def predict_curve(model, Xplot):
    model.eval()
    with torch.no_grad():
        pred = model(torch.from_numpy(Xplot)).cpu().numpy()
    return pred

pred_none = predict_curve(model_none, X_plot)
pred_l2 = predict_curve(model_l2, X_plot)
pred_l1 = predict_curve(model_l1, X_plot)
pred_drop = predict_curve(net_dropout, X_plot)

# 将训练数据和真实函数画出来
plt.figure(figsize=(10,6))
plt.scatter(x_train, y_train, s=40, c='orange', edgecolor='k', label='train samples')
plt.scatter(x_val, y_val, s=40, c='lightgreen', edgecolor='k', label='val samples')
plt.plot(x_plot, np.sin(2*np.pi*x_plot), c='navy', linewidth=2, label='true function')
plt.plot(x_plot, pred_none, c='red', linewidth=2, label='no reg')
plt.plot(x_plot, pred_l2, c='purple', linewidth=2, label='L2 (weight_decay)')
plt.plot(x_plot, pred_l1, c='teal', linewidth=2, label='L1')
plt.plot(x_plot, pred_drop, c='gold', linewidth=2, label='NN + Dropout')
plt.xlim(-1.2,1.2)
plt.legend()
plt.title("拟合曲线对比")
plt.show()

训练/验证损失曲线对比(有助于观察过拟合):

plt.figure(figsize=(10,6))
plt.plot(tr_none, c='red', alpha=0.8, label='train none')
plt.plot(val_none, c='red', alpha=0.4, linestyle='--', label='val none')
plt.plot(tr_l2, c='purple', alpha=0.8, label='train L2')
plt.plot(val_l2, c='purple', alpha=0.4, linestyle='--', label='val L2')
plt.plot(tr_l1, c='teal', alpha=0.8, label='train L1')
plt.plot(val_l1, c='teal', alpha=0.4, linestyle='--', label='val L1')
plt.plot(tr_drop, c='gold', alpha=0.8, label='train dropout')
plt.plot(val_drop, c='gold', alpha=0.4, linestyle='--', label='val dropout')
plt.yscale('log')
plt.xlabel('epoch')
plt.ylabel('MSE (log scale)')
plt.title('训练/验证损失曲线对比')
plt.legend()
plt.show()

比较不同正则化下模型权重的直方图(展示“缩小”或“稀疏化”的效果):

def weight_hist(model, name, color):
    w = []
    for n, p in model.named_parameters():
        if'weight'in n:
            w.append(p.detach().cpu().numpy().flatten())
    w = np.concatenate(w)
    plt.hist(w, bins=40, alpha=0.7, color=color, label=name)

plt.figure(figsize=(10,6))
weight_hist(model_none, 'no reg', 'red')
weight_hist(model_l2, 'L2', 'purple')
weight_hist(model_l1, 'L1', 'teal')
plt.legend()
plt.title('权重分布直方图:无正则 vs L2 vs L1')
plt.show()

验证误差随正则强度变化:

l2_list = [0.0, 1e-5, 1e-4, 1e-3, 1e-2]
val_errs = []
for wd in l2_list:
    m = PolyLinear(in_dim)
    m, tr, val = train_model(m, X_train, y_train, X_val, y_val, l1_lambda=0.0, weight_decay=wd, epochs=600)
    val_errs.append(val[-1])

plt.figure(figsize=(8,5))
plt.plot(l2_list, val_errs, marker='o', color='magenta')
plt.xscale('log')
plt.xlabel('L2 weight_decay (log scale)')
plt.ylabel('Validation MSE')
plt.title('验证误差随 L2 强度变化(有 U 型曲线可能)')
plt.show()

拟合曲线对比:

展示不同方法在整个输入区间上的拟合函数。

  • “无正则化”通常在训练点附近震荡严重,尤其在区间外或数据稀疏处偏离真实函数(明显高方差)。
  • “L2”曲线更平滑,幅度受控,避免了高阶振荡,泛化通常变好。
  • “L1”在某些系数被压成零后,模型可能变得像低阶多项式,曲线更简单甚至有局部平直。
  • “NN + Dropout”作为非线性模型,能拟合复杂形态但 Dropout 帮助减少过拟合,曲线通常更稳定。

训练/验证损失曲线:

训练损失与验证损失随 epoch 的变化,以 log 纵轴更容易看清数量级差异。

  • 无正则化:训练损失会持续下降,但验证损失在达到某个点后回升(过拟合)。
  • L2/L1/Dropout:验证损失更早稳定或更低,说明泛化能力更好。
  • Early stopping:如果看到验证损失在某一轮后上升,就可以借此实施早停策略。

权重分布直方图:

展示模型参数的数值分布。

  • L2 会把权重推向 0 附近,但多数权重仍为小幅连续值(分布窄而高)。
  • L1 会倾向把许多权重变成严格的 0(稀疏)。直方图会显示更多集中在 0 的频率。
  • 无正则:权重分布通常更宽、更分散,出现一些大系数,可能导致模型不稳定。

验证误差 vs 正则强度:

展示模型泛化性能如何依赖正则化强度  。

  • 通常存在“U 型”曲线:当   很小或为 0 时过拟合(高方差),当   太大时欠拟合(高偏差)。中间有个最优值平衡偏差与方差。
  • 该图是调参的重要依据:帮助我们选择合适的正则化强度。

数学上,我们经常把带 L2 的目标写作:

其中  。当使用优化器里的 weight_decay 时,PyTorch 在参数更新时会隐含地做类似于 L2 的收敛控制,所以我们把它当作 L2 正则化来用。L1 则是

,会促使很多   变为 0,得到稀疏权重。

Dropout 的直观解释是:训练时随机丢掉神经元,相当于在训练阶段对许多不同的子网络做平均,从而在测试阶段得到更稳健的预测。它并不直接把权重变小(不像 L2),而是通过 ensemble-like 的效果减少过拟合。

总结

正则化并不是万能药:数据量大且模型合适时,可能不需要强力正则化;反之当模型过于复杂或数据稀少时,正则化很关键。

另外,L2 是“普适且稳妥”的选择(使权重小,模型平滑),通常作为首选;L1 适合希望得到稀疏解或做特征选择时使用;Dropout 更适合大型神经网络。

选择正则化强度   很重要:常用交叉验证或验证集做网格搜索(如上文“验证误差随正则强度的变化”图)。

结合早停、数据增强、减小模型复杂度等手段,通常能取得更好效果。

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 434
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读8.8k
粉丝0
内容434