PyTorch 训练流程完全理解:loss.backward() 到底干了些啥?

🧠 PyTorch 训练流程完全理解:loss.backward() 到底干了些啥?

📓 本文其实就是我自己学 PyTorch 的时候,搞了很久才真正明白的一些点。
这几行代码明明天天写,但到底谁干了啥,谁动了谁,早期是真没整太明白。
所以干脆趁我这回稍微想清楚了,写篇笔记,分享也顺便帮自己梳理。
如果你刚好也在学,那希望这些啰嗦点的解释能帮上你。


🌱 为什么写这个?

老规矩,先说说背景。

在我学 PyTorch 的时候,经常看到这样的训练代码块:

for data in dataloader:
    outputs = model(inputs)
    loss = loss_fn(outputs, targets)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

说实话,一开始看的时候,我脑子里只有两个问题:

  • ❓ loss.backward() 是干嘛的?我看不到“参与计算”的地方啊
  • ❓ 为什么梯度还得“清空”?它不是应该每次都重算吗?

于是我一边查文档一边打断点,甚至手算了一些梯度,才慢慢捋清楚这一套到底是怎么回事。以下就是我个人的理解,一步一讲。


🧩 从最基本的流程说起

训练过程其实就是这五步:

1. 前向传播 → 2. 计算损失 → 3. 反向传播 → 4. 参数更新 → 5. 清空梯度

PyTorch 对应的代码写法:

outputs = model(inputs)             # 1. 前向传播
loss = loss_fn(outputs, targets)    # 2. 计算损失(比如 CrossEntropy)
loss.backward()                     # 3. 反向传播(计算各参数的梯度)
optimizer.step()                    # 4. 用梯度来更新参数
optimizer.zero_grad()               # 5. 清除旧的梯度

说简单点就是:

模型猜答案 → 算错了 → 看哪错了 → 改一下 → 擦干净继续猜下一题

但“loss.backward() 到底怎么让模型知道‘哪错了’?”,这才是最关键的点。


🧪 举个超小例子:不通过完整模型,就用张量来解释

咱别一上来就整 CNN、ResNet、DataLoader,那玩意刚开始理解真挺乱。
我们拿一个最简单的例子,用 3 类分类任务说明 loss.backward() 这一套事。

import torch
import torch.nn as nn

# 假设模型输出了某张图对应的 3 类得分
outputs = torch.tensor([[2.0, 0.5, 0.3]], requires_grad=True)

# 真实标签是类别 0(比如这是一只猫)
targets = torch.tensor([0])

# 定义交叉熵损失
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(outputs, targets)

# 反向传播,算梯度
loss.backward()

🔍 到底发生了什么?逐行拆解

➤ 第一步:outputs 是啥?

outputs = torch.tensor([[2.0, 0.5, 0.3]], requires_grad=True)

假装这是网络输出的 logits,也就是没经过 softmax 的原始得分。
维度是 [1, 3],表示这个 batch 有一张图,它对三类的判断分数。


➤ 第二步:targets 是啥?

targets = torch.tensor([0])

整型张量,表示这张图真实是第 0 类。注意 CrossEntropyLoss 不接受 one-hot 向量,而是直接要类别编号。


➤ 第三步:loss 怎么算的?

loss = loss_fn(outputs, targets)

这一行其实干了三件事

  1. 先对 outputs 做 softmax,变成每类的概率
  2. 然后取 log(因为交叉熵是对 log 概率求负)
  3. 最后只挑真实类别那一项,做负对数

我们手动算一下看看:

softmax = exp(x_i) / sum(exp(x)) 
        ≈ [0.711, 0.159, 0.130]

loss = -log(softmax[0]) = -log(0.711) ≈ 0.341

➤ 第四步:loss.backward() 真正干了啥?

loss.backward()

这一步最核心,它干的事就是:

从 loss 开始,反向传播整个计算图,把每一个参数对 loss 的导数(也叫梯度)都算出来,存在每个张量的 .grad 属性里。

你可以验证:

print(outputs.grad)

输出是:

tensor([[-0.289, 0.159, 0.130]])

这个东西就是:
softmax i − onehot i ( 对 o n e − h o t 不理解的可以翻翻之前的帖子 ) \text{softmax}_i - \text{onehot}_i(对one-hot不理解的可以翻翻之前的帖子) softmaxionehoti(onehot不理解的可以翻翻之前的帖子)
也就是说,如果真实类别是 0,那对应那一项:

0.711 - 1 = -0.289(-0.289 就是 梯度(grad),是通过 loss.backward() 这一步自动计算出来的)

其他类别:

0.159 - 0 = 0.159(同上)
0.130 - 0 = 0.130(同上)

这表示:

  • 第 0 类你猜高了,减点
  • 第 1、2 类你猜低了,加点

⚙️ 梯度计算完了,怎么更新参数?

这个就轮到 optimizer.step() 登场了:

optimizer.step()

这行代码的作用是:
读取所有参数的 .grad 值,然后按照梯度下降算法更新这些参数

比如有个参数是:

w = 2.0(再pytorch中由pytorch自动生成)
grad = 0.5(像-0.289 就是 梯度(grad),是通过 loss.backward() 这一步自动计算出来的)
lr = 0.01(自定义)

那更新就是:

w = w - lr * grad = 2.0 - 0.01 * 0.5 = 1.995

PyTorch 内部就是自动做了这件事。


🧹 那为啥还得 zero_grad()?不写不行吗?

这玩意你不写真的要命!

默认情况下,PyTorch 每次算完梯度 是会累加在上一次梯度上的!

也就是说:

loss1.backward()  # grad = A
loss2.backward()  # grad = A + B
loss3.backward()  # grad = A + B + C

你不清零,梯度就像账单越记越厚,结果模型更新方向完全错乱,loss 越训练越大。

所以每次必须清理:

optimizer.zero_grad()

就像打游戏前存档,先归零,不然这盘就废了。


🔚 总结一下:一句话记住整套流程

步骤作用内部发生了啥
loss.backward()算梯度每个参与计算的变量都会生成 .grad
.grad梯度值存着“该往哪调”的方向
optimizer.step()用梯度更新参数按照 w -= lr * grad
optimizer.zero_grad()清空上一次的梯度防止梯度累加出错

🙋‍♂️ 结尾唠几句

说真的,我刚接触 PyTorch 的时候,一直把这几行代码当仪式感一样背下来写。但每次调模型、改网络结构的时候,总感觉像黑盒一样用着没底气。

后来我慢慢翻源码、打断点、手推导公式,才慢慢理解这几行代码是啥。

loss 不只是一个值,它是反过来牵引模型学习的导向;.grad 是神经网络学习的方向盘;而 zero_grad() 就像写字前先擦黑板,不擦就糊了。

这篇笔记就写到这,写的在我理解的范围内尽量详细,一方面希望在自己回头看的时候能不至于像太极一样忘得一干净,也希望对这块有疑问的读者能够在这里能狗理解的更深,少浪费些时间,觉得有帮助的希望能换个赞,三克油

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值