17-正则化方法:Dropout、权重衰减与过拟合控制

概述:为什么训练准确率高不等于模型好

训练模型时,经常会看到这样的现象:

text 复制代码
train_acc 很高
val_acc 明显低
train_loss 持续下降
val_loss 先下降后上升

这通常说明模型出现了过拟合。模型把训练集里的模式甚至噪声记住了,但面对验证集或测试集时泛化不好。

正则化的目标不是让训练 loss 最低,而是让模型在未见数据上表现更稳。常见手段包括:

  • Dropout:训练时随机丢弃部分神经元输出。
  • weight_decay:约束参数不要过大。
  • 数据增强:让模型见到更多输入变化。
  • 早停:验证集长期不提升时停止训练。
  • 减小模型容量:降低过拟合能力。

读完本文,你应该能判断模型是否过拟合,并能在 Paddle 中使用 Dropoutweight_decay 和早停策略控制过拟合。

过拟合、欠拟合与正常学习

先看三种训练状态:

状态 训练集表现 验证集表现 常见原因
欠拟合 模型太小、训练不足、学习率不合适
正常学习 容量和正则化较合适
过拟合 很好 较差 模型太大、数据太少、正则不足

可以用曲线理解:

text 复制代码
正常学习:
train_loss 下降,val_loss 也下降

过拟合:
train_loss 继续下降,val_loss 开始上升

欠拟合:
train_loss 和 val_loss 都高

判断过拟合时,不要只看一轮结果,要看多个 epoch 的趋势。

Dropout:训练时随机关闭部分神经元

Paddle 中使用:

python 复制代码
dropout = paddle.nn.Dropout(p=0.5)

官方文档说明,Dropout 是一种正则化手段,会在训练过程中按照概率 p 将部分神经元输出置为 0,以减少神经元之间的共同适应。

示例:

python 复制代码
import paddle
import paddle.nn as nn

paddle.seed(2026)

x = paddle.ones([2, 8], dtype="float32")
dropout = nn.Dropout(p=0.5)

dropout.train()
y_train = dropout(x)

dropout.eval()
y_eval = dropout(x)

print("train output:")
print(y_train)
print("eval output:")
print(y_eval)

训练模式下,部分位置会被随机置零;评估模式下,Dropout 不再随机丢弃。

关键点 :Dropout 是否生效取决于模型模式,所以训练和评估时必须正确调用 model.train()model.eval()

Dropout 放在哪里

MLP 中常见放法:

python 复制代码
class MLP(nn.Layer):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(32, 2),
        )

    def forward(self, x):
        return self.net(x)

常见原则:

  • 放在隐藏层激活之后更常见。
  • 不要放在最终 logits 后面。
  • p 不是越大越好。
  • 小模型或数据充分时,Dropout 不一定带来提升。

常见尝试:

text 复制代码
p = 0.1
p = 0.3
p = 0.5

如果 p 太大,模型可能欠拟合。

weight_decay:通过优化器约束参数

权重衰减常用于限制参数过大。在 Paddle 优化器中可以设置:

python 复制代码
optimizer = paddle.optimizer.Adam(
    learning_rate=0.001,
    parameters=model.parameters(),
    weight_decay=1e-4,
)

直觉上,训练目标变成:

text 复制代码
不仅要分类正确
还希望参数不要过大

过大的参数可能让模型对训练数据过度敏感。weight_decay 会给这种倾向加一点惩罚。

常见取值:

text 复制代码
1e-5
1e-4
1e-3

如果 weight_decay 太小,效果可能不明显;太大则可能让模型学不动。

Dropout 与 weight_decay 的区别

方法 作用位置 主要效果
Dropout 网络前向中的激活值 减少神经元共同适应
weight_decay 优化器参数更新 惩罚过大权重
数据增强 输入数据 增加训练样本变化
早停 训练过程 防止继续过拟合

它们不是互斥关系,可以组合使用。

推荐顺序:

text 复制代码
先跑无正则 baseline
观察是否过拟合
加入 weight_decay
必要时加入 Dropout
再考虑数据增强和早停

不要一开始把所有正则化都加满,否则很难知道是哪一个起作用。

早停:验证集不再提升就停止

早停不是 Paddle 特有 API,而是一种训练策略。基本逻辑:

text 复制代码
记录 best_val_acc
如果验证集长期没有提升
停止训练

示例:

python 复制代码
best_val_acc = 0.0
patience = 5
bad_epochs = 0

for epoch in range(100):
    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
    val_loss, val_acc = evaluate(model, val_loader)

    if val_acc > best_val_acc:
        best_val_acc = val_acc
        bad_epochs = 0
        paddle.save(model.state_dict(), "best_model.pdparams")
    else:
        bad_epochs += 1

    print("epoch:", epoch, "val_acc:", val_acc, "bad_epochs:", bad_epochs)

    if bad_epochs >= patience:
        print("early stop at epoch:", epoch)
        break

早停适合:

  • 验证集指标已经停滞。
  • 训练时间有限。
  • 模型后期明显过拟合。

完整示例:带 Dropout 和 weight_decay 的 MLP

python 复制代码
import paddle
import paddle.nn as nn
import paddle.nn.functional as F


class CircleDataset(paddle.io.Dataset):
    def __init__(self, num_samples, seed):
        super().__init__()
        paddle.seed(seed)
        self.x = paddle.rand([num_samples, 2], dtype="float32") * 4.0 - 2.0
        radius_square = paddle.sum(self.x ** 2, axis=1)
        self.y = (radius_square > 1.0).astype("int64")

    def __len__(self):
        return self.y.shape[0]

    def __getitem__(self, index):
        return self.x[index], self.y[index]


class RegularizedMLP(nn.Layer):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(32, 2),
        )

    def forward(self, x):
        return self.net(x)


def accuracy(logits, labels):
    pred = paddle.argmax(logits, axis=1)
    return paddle.mean((pred == labels).astype("float32"))


def train_one_epoch(model, loader, optimizer):
    model.train()
    total_loss = 0.0
    total_acc = 0.0
    count = 0

    for batch_x, batch_y in loader:
        logits = model(batch_x)
        loss = F.cross_entropy(logits, batch_y)
        acc = accuracy(logits, batch_y)

        loss.backward()
        optimizer.step()
        optimizer.clear_grad()

        total_loss += float(loss.numpy())
        total_acc += float(acc.numpy())
        count += 1

    return total_loss / count, total_acc / count


def evaluate(model, loader):
    model.eval()
    total_acc = 0.0
    count = 0

    with paddle.no_grad():
        for batch_x, batch_y in loader:
            logits = model(batch_x)
            total_acc += float(accuracy(logits, batch_y).numpy())
            count += 1

    return total_acc / count


def main():
    paddle.seed(2026)

    train_loader = paddle.io.DataLoader(CircleDataset(512, 2026), batch_size=32, shuffle=True)
    val_loader = paddle.io.DataLoader(CircleDataset(512, 2027), batch_size=64, shuffle=False)

    model = RegularizedMLP()
    optimizer = paddle.optimizer.Adam(
        learning_rate=0.001,
        parameters=model.parameters(),
        weight_decay=1e-4,
    )

    best_val_acc = 0.0
    patience = 8
    bad_epochs = 0

    for epoch in range(100):
        train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
        val_acc = evaluate(model, val_loader)

        if val_acc > best_val_acc:
            best_val_acc = val_acc
            bad_epochs = 0
            paddle.save(model.state_dict(), "best_regularized_mlp.pdparams")
        else:
            bad_epochs += 1

        print(
            "epoch:", epoch,
            "train_loss:", train_loss,
            "train_acc:", train_acc,
            "val_acc:", val_acc,
            "best_val_acc:", best_val_acc,
        )

        if bad_epochs >= patience:
            print("early stop at epoch:", epoch)
            break


if __name__ == "__main__":
    main()

常见错误:正则化排查清单

错误一:评估时忘记 model.eval

包含 Dropout 的模型必须在验证和推理前切换:

python 复制代码
model.eval()

否则验证集结果会带随机性,指标不稳定。

错误二:Dropout p 设置过大

如果 p=0.8,大部分激活都会被丢弃,模型可能欠拟合。入门阶段先试 0.10.5

错误三:只看训练准确率

正则化的目标是提升泛化。应同时看:

text 复制代码
train_acc
val_acc
train_loss
val_loss

错误四:weight_decay 过大

如果训练集也学不好,可能正则过强。尝试降低到 1e-5 或暂时关闭。

错误五:把正则化当成万能修复

如果数据标签错了、输入 shape 错了、学习率过大,正则化解决不了根因。

调参建议:先建立 baseline

推荐流程:

  1. 无 Dropout、无 weight_decay,训练 baseline。
  2. 如果训练集和验证集都差,先解决欠拟合。
  3. 如果训练集好、验证集差,加入 weight_decay=1e-4
  4. 再加入 Dropout(p=0.1 或 0.3)
  5. 观察验证集指标,不要只看训练 loss。
  6. 保存验证集最好的模型。

建议练习:观察过拟合和正则化效果

  1. 减小训练集到 128 条,观察是否更容易过拟合。
  2. 把隐藏层改成 128,观察训练集和验证集差距。
  3. 加入 Dropout(p=0.3),观察验证集变化。
  4. 加入 weight_decay=1e-4,观察参数约束效果。
  5. 实现早停,并保存 best model。
  6. 故意评估时不调用 eval(),观察指标波动。

总结

这一篇讲了 Paddle 中常见正则化思路:

  • 过拟合表现为训练集好、验证集差。
  • Dropout 在训练时随机丢弃激活,评估时需 model.eval()
  • weight_decay 通过优化器约束权重大小。
  • 早停根据验证集指标停止训练。
  • 正则化强度不是越大越好。
  • 调参要先建立 baseline,再逐步加入策略。

如果只能记住一句话,那就是:

正则化不是为了让训练 loss 更低,而是为了让模型在新数据上更可靠。

相关推荐
2603_965148111 小时前
抖音/快手直播选品:用API快速匹配爆款与低价货源
开发语言·python·自动化·api
m0_749492221 小时前
2026年仓储安防机器人选型横评:3款室外巡逻机器人仓库场景实测对比
人工智能
用户7088769039381 小时前
RAG检索优化实战:从67%到92%,我做了这4步
python
GEOshijie1231 小时前
智能家居品牌做GEO推荐哪家供应商?品类词抢占案例复盘
人工智能·python·智能家居
HAYDENR1 小时前
数据挖掘是什么?数据挖掘在实际业务中如何落地应用?
人工智能·数据挖掘
zhangjin11201 小时前
NLP自然语言处理
人工智能·自然语言处理
天天爱吃肉82181 小时前
行业笔记|高压连接器瞬态接触退化通用检测方法论
人工智能·笔记·python·功能测试·学习·汽车
卷无止境1 小时前
用FastAPI和PyCasbin搭一套能扛住数据中台复杂权限需求的鉴权中枢
后端·python
程序员-李俞2 小时前
MiniMax H3 深度拆解:全模态视频模型来了,AI 漫剧系统该如何重构?
人工智能·ai作画·重构·aigc·音视频·ai写作·画布