学习初稿 · 第 10 课。 本篇已开放网页阅读,用于先建立完整认识。文中手算值和示意记录不代表真实训练成绩;真实数据训练、预训练权重和板端测试以本篇具体说明为准,本篇已附代码练习,实际验证范围见文末与包内记录。
一个预测错了,应该罚多少
准确率只记录对或错。真实类别是汽车时,模型给汽车 0.49 和 0.01 都可能被判错,但后者明显更偏离目标。训练需要一个能随输出连续变化的量来表达误差,这就是损失。
本课延续单标签分类,标签为整数类别编号,网络输出为原始 logits。损失用于指导优化;最终任务还要看准确率、误报漏报和实际错误样本,不能把损失数字直接当百分比。
交叉熵先从一个样本理解
对于一个正确类别,交叉熵可以理解为对其预测概率取负对数。正确类别概率越高,损失越低。例如概率 0.8 对应约 0.223,概率 0.2 对应约 1.609。这里的对数采用自然对数。
Softmax 与交叉熵在框架中通常一起稳定计算,因此单标签 CrossEntropyLoss 直接接 logits,不先手工做 Softmax。整数目标 [N] 表示每张图的类别索引,输出 [N,C] 则包含每张图对所有类别的分数。官方接口约定
以下小实验只需 PyTorch,不加载图片,也不会更新参数:
|
手算预期损失约为 1.3133。沿负梯度方向改变 logits,会降低错误类别的分数、提高正确类别的分数。实际训练时 logits 是网络计算结果,梯度还要通过计算图传回各层参数。
梯度不是新的参数值
梯度描述当前点附近,参数小幅变化时损失如何变化。backward() 计算并累积梯度,优化器的 step() 才根据梯度和更新规则改变参数。zero_grad() 清理旧梯度,避免无意把多次计算累积在一起。
用 L=(w-3)² 手算:当 w=1,梯度为 2(w-3)=-4。学习率 0.1 时,更新为 w=1-0.1×(-4)=1.4,损失从 4 降到 2.56。学习率如果过大,则可能越过低点并离得更远。
|
需要梯度累积时可以有意不每次清理,但还要正确处理损失缩放与更新频率。初学阶段先保持每批一次清理与更新,确认基本闭环。
SGD、动量和 AdamW 的不同角色
最简单的 SGD 按当前梯度与学习率更新。带动量的版本还利用历史更新方向。Adam 类方法根据梯度的一阶、二阶统计调整更新,AdamW 将权重衰减与其自适应梯度更新解耦。
这些方法都不是“保证找到最好参数”的按钮。同一个学习率在不同优化器中含义和有效范围不能简单等价,改变优化器后应重新检查训练曲线,而不是只比较最后一次打印。
权重衰减控制参数的收缩倾向,学习率控制更新尺度,两者不能互相替代。把所有参数都用相同衰减方式是一个实现选择,较复杂模型可能会单独处理偏置与归一化参数。本课先固定简单配置,后面再做对照。
三类经常被混淆的问题
损失不下降,可能是没有调用更新、标签错位、输入尺度错误、学习率不合适,也可能只是任务更难。损失下降但验证恶化,可能是过拟合或划分存在问题。出现 NaN,则需要先检查输入、数值范围和梯度,而不是继续增加训练轮数。
调试时可先对少量固定样本反复训练,检查模型能否降低其训练误差。这是检查优化管线的工具,不能把这个结果报告成泛化性能。
练习
- 为什么训练前不需要把 logits 限制在 0~1?
L=(w-3)²,从w=1用学习率 1.2 更新,结果如何?- 有梯度但参数没有改变,首先检查哪一步?
参考答案
交叉熵接口接原始分数,并在内部稳定处理。第二题更新到 5.8,损失为 7.84,比原来的 4 更大。第三题先检查是否执行了 optimizer.step()、优化器是否包含目标参数、学习率是否为零,不能仅凭存在梯度就认定更新完成。
下一课把这些步骤放进 batch 与 epoch,计算一次实验到底进行了多少次更新。
综合实践
想把这些知识接成一套可运行的代码,可以进入实现 MLP:从损失计算、训练到 ONNX 推理。实践包含自生成数据、训练与验证、模型保存、独立推理、ONNX 导出和数值对照;遇到问题可按实践中的索引返回对应理论章节。
配套代码练习
实验:交叉熵梯度与一次 SGD 更新
查看完整代码:main.py
"""交叉熵梯度与一次 SGD 更新。运行方式与练习见 README.md。"""
import torch
import torch.nn.functional as F
torch.set_num_threads(2)
logits = torch.tensor([[2.,1.]], requires_grad=True)
target = torch.tensor([1])
loss = F.cross_entropy(logits, target)
manual = -torch.log_softmax(logits, dim=1)[0,1]
torch.testing.assert_close(loss, manual)
loss.backward()
expected_grad = logits.detach().softmax(1) - F.one_hot(target,2)
torch.testing.assert_close(logits.grad, expected_grad)
print('loss:', loss.item(), 'gradient:', logits.grad.tolist())
with torch.no_grad(): updated = logits - 0.1 * logits.grad
new_loss = F.cross_entropy(updated, target)
assert new_loss < loss
print('更新后的 loss:', new_loss.item())
在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。
cd 'content/posts/AI-深度学习/从零搭建视觉模型/10-损失函数与优化器/code' && python3 main.py
本篇已附独立实验包,在代码卡片中查看源码、下载或复制运行命令。解压后执行 python3 main.py;依赖安装见包内 README.md。
观察目标: 标签为 1 时损失约 1.3133,梯度约 [0.7311,-0.7311],更新后损失下降。
动手修改: 把标签改为 0,先预测梯度符号再运行;把学习率改成 0,检查参数和损失。
所有输入均在本篇代码中构造;运行范围和验证记录见包内 verification.md。
