12-PaddlePaddle, 飞桨, 分类模型, 训练循环, 损失函数, 优化器, cross_entropy, Adam

概述

前面已经有了数据读取、预处理和 MLP 模型。第 12 篇把重点放在训练本身:分类模型如何通过 loss、backward 和 optimizer 一步步学会预测。

一个完整训练循环包含:

text 复制代码
取 batch
    |
前向计算 logits
    |
计算 loss
    |
反向传播
    |
优化器更新参数
    |
清理梯度
    |
记录指标

这些步骤缺一不可。只写模型结构,参数不会自动变好;只计算 loss,不调用 backward()step(),参数也不会更新。

读完本文,你应该能从零写出分类模型训练循环,并理解损失函数、优化器、梯度清理和训练日志各自的作用。

分类任务的数据形状

分类模型最常见的输入输出形状是:

text 复制代码
features: [batch_size, feature_dim]
labels:   [batch_size]
logits:   [batch_size, num_classes]

例如二分类:

text 复制代码
batch_x: [32, 2]
batch_y: [32]
logits:  [32, 2]

标签不是 one-hot,而是类别 ID:

text 复制代码
[0, 1, 1, 0, ...]

并且 dtype 通常是 int64

损失函数:cross_entropy 为什么常用于分类

分类模型最后输出 logits:

python 复制代码
logits = model(batch_x)

logits 是未归一化分数,不是概率。训练时通常直接使用:

python 复制代码
loss = paddle.nn.functional.cross_entropy(logits, batch_y)

交叉熵会衡量模型给真实类别的分数是否足够高。直觉上:

  • 真实类别分数越高,loss 越小。
  • 真实类别分数越低,loss 越大。
  • loss 越大,梯度会推动参数向更正确方向更新。

训练时不要先手动 softmax 再交给 cross_entropy,因为很多交叉熵实现已经融合了 softmax 和 log 计算,数值更稳定。

优化器:参数更新的执行者

自动微分负责算梯度,优化器负责根据梯度更新参数。

常见写法:

python 复制代码
optimizer = paddle.optimizer.Adam(
    learning_rate=0.01,
    parameters=model.parameters(),
)

关键参数:

  • learning_rate:每次参数更新的步幅。
  • parameters:要更新的模型参数。

训练步骤:

python 复制代码
loss.backward()
optimizer.step()
optimizer.clear_grad()

含义:

代码 作用
loss.backward() 根据 loss 计算参数梯度
optimizer.step() 根据梯度更新参数
optimizer.clear_grad() 清理梯度,避免累积

完整训练脚本

下面给出一个可运行的分类训练脚本。

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 Classifier(nn.Layer):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 32),
            nn.ReLU(),
            nn.Linear(32, 16),
            nn.ReLU(),
            nn.Linear(16, 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_loss = 0.0
    total_acc = 0.0
    count = 0

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

    return total_loss / count, total_acc / count


def main():
    paddle.seed(2026)

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

    model = Classifier()
    optimizer = paddle.optimizer.Adam(
        learning_rate=0.01,
        parameters=model.parameters(),
    )

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


if __name__ == "__main__":
    main()

训练循环拆解

核心代码:

python 复制代码
for batch_x, batch_y in loader:
    logits = model(batch_x)
    loss = F.cross_entropy(logits, batch_y)
    loss.backward()
    optimizer.step()
    optimizer.clear_grad()

每一轮 batch 都会重新构建动态图并计算梯度。optimizer.clear_grad() 很重要,因为 Paddle 的梯度默认会累积。

如果忘记清梯度,下一批样本的梯度会叠加上一批的梯度,训练行为会偏离预期。

train_one_epoch:把训练一轮封装起来

把训练一轮封装成函数的好处:

  • 主流程更清楚。
  • 训练和验证逻辑分开。
  • 后续添加日志、学习率调度、混合精度更方便。
  • 单独调试训练逻辑更容易。

函数返回平均 loss 和平均 accuracy:

python 复制代码
return total_loss / count, total_acc / count

注意:这是 batch 平均。如果最后一个 batch 大小不同,严格样本加权平均会更精确。入门阶段 batch 平均足够直观。

训练日志应该看什么

每个 epoch 打印:

text 复制代码
train_loss
train_acc
val_loss
val_acc

常见判断:

现象 可能原因
train_loss 下降,val_acc 上升 正常学习
train_loss 不下降 学习率、模型、数据或 loss 有问题
train_acc 高,val_acc 低 可能过拟合
loss 变 NaN 学习率过大或数值不稳定
acc 一直接近随机 标签、axis、模型输出可能有问题

训练日志不是装饰,它是判断模型是否正常学习的第一证据。

优化器选择:SGD 与 Adam

SGD:

python 复制代码
optimizer = paddle.optimizer.SGD(
    learning_rate=0.1,
    parameters=model.parameters(),
)

Adam:

python 复制代码
optimizer = paddle.optimizer.Adam(
    learning_rate=0.01,
    parameters=model.parameters(),
)

入门建议:

  • 小实验优先用 Adam,收敛通常更快。
  • 学习优化器原理时尝试 SGD。
  • 不同优化器的合适学习率可能差很多。

不要机械地把 Adam 和 SGD 使用同一个学习率。

学习率:最先调的超参数

学习率太小:

text 复制代码
loss 下降很慢
训练很多轮仍没明显变化

学习率太大:

text 复制代码
loss 剧烈震荡
甚至出现 NaN

建议尝试:

text 复制代码
Adam: 0.001、0.01、0.03
SGD: 0.01、0.05、0.1

观察训练曲线,而不是只看最后一个 epoch。

常见错误

错误一:logits 和 labels shape 不匹配

分类常见形状:

text 复制代码
logits: [N, C]
labels: [N]

如果 labels 是 [N, 1],先检查损失函数要求,必要时 reshape。

错误二:labels dtype 不是 int64

分类标签通常:

python 复制代码
labels = labels.astype("int64")

错误三:argmax axis 写错

对于 [batch_size, num_classes]

python 复制代码
pred = paddle.argmax(logits, axis=1)

错误四:训练阶段用了 no_grad

训练前向和 loss 不能放在 paddle.no_grad() 中,否则无法反向传播。

错误五:验证阶段更新了参数

验证函数中不要调用:

python 复制代码
loss.backward()
optimizer.step()

调试方法:从单 batch 开始

如果训练不正常,先取一个 batch:

python 复制代码
batch_x, batch_y = next(iter(train_loader))
logits = model(batch_x)
loss = F.cross_entropy(logits, batch_y)

print(batch_x.shape, batch_x.dtype)
print(batch_y.shape, batch_y.dtype)
print(logits.shape, logits.dtype)
print(loss.shape, loss.dtype)

确认单 batch 正常,再跑完整 epoch。

总结:分类训练是一个明确闭环

分类模型训练的核心闭环是:

text 复制代码
batch -> logits -> loss -> backward -> step -> clear_grad

每个环节都要正确:

  • 数据 shape 和 dtype 正确。
  • 模型输出 logits。
  • 交叉熵接收 logits 和类别标签。
  • backward() 计算梯度。
  • 优化器更新参数。
  • 清理梯度避免累积。
  • 用训练和验证日志判断学习情况。

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

训练循环不是模板代码,而是模型从错误中更新参数的完整机制。

相关推荐
冬奇Lab1 小时前
AI 评测系列(08):评测 CI/CD——持续质量门控
人工智能
冬奇Lab1 小时前
每日一个开源项目(第169篇):AIRI - 开源自托管 AI 虚拟伴侣,能打 Minecraft 的 Neuro-sama 复现
人工智能·开源·资讯
MartinYeung51 小时前
[论文学习]自主性如何重塑个性化对LLM智能体隐私关切与信任的影响
人工智能·学习
LDZKKJ1 小时前
OpenAI模型“越狱“入侵Hugging Face——AI安全史上的至暗时刻
网络·人工智能·安全
鬓戈2 小时前
Hermes Agent执行流程
人工智能
工具派2 小时前
从接口文档到测试用例:我用在线 AI 工具跑通了一条生成链路(实操记录)
人工智能·测试用例
颜酱2 小时前
07 | 把字段与指标同步到 Qdrant(生成阶段)
前端·人工智能·后端
睿拓时创2 小时前
数字图像相关(DIC)领域:VIC-3D 11.4上线多款全新功能
人工智能