2.2 自动求导对照


2.2 自动求导对照

本节摘要:训练神经网络要算损失对参数的梯度。TensorFlow 用 tf.GradientTape 在一个上下文里录制前向运算,再 tape.gradient 取出梯度;PyTorch 用 autograd,叶子张量带 requires_grad,对标量损失调用 backward。两边都是反向模式自动微分,不是符号软件里那种「先把公式展开」。入门对照的关键是:默认盯谁、何时停止记录、梯度会不会累加、以及推理时为什么必须关掉求导。

核心问题

阅读完本节,你应当能够:

  1. 用自己的话解释反向模式自动微分在「多层复合函数」上为什么合适
  2. 对照写出二次函数求梯度的最小例子:Tape 对 backward
  3. 说明 Keras 层参数默认被 Tape 监视,而普通 tf.constant 需要 watch
  4. 解释 PyTorch 必须 zero_grad 的原因:梯度默认累加
  5. 判断何时用 tape.stop_recording / torch.no_grad / model.eval

求导系统在解决什么痛

多层网络的损失是复合函数。手工推每一层的雅可比,写一遍就会恨自己,改一层激活就要重推。自动微分把每一步基本运算的局部导数记下来,反向时用链式法则拼全局梯度。它不是数值扰动那种「每个参数加一个 epsilon」——那种方法慢且不稳;也不是把整张图编译成一个封闭公式再对符号求导。它是:前向走一遍留下计算轨迹,反向沿轨迹把梯度传回叶子。

原文在选型节把自动微分与张量并列,视为两套框架的共同核心。旧章结构却把它散落在训练流程里,没有专节对照 Tape 与 autograd。本教程把它单列,因为后文所有「为什么 fit 能改权重」和「为什么漏写 backward 准确率不动」都回到这里。

最小玩具不要上 Fashion MNIST。令 y = x * x,求 dy/dxx=3 应为 6。TensorFlow:把 x 做成 Variable 或对 constant watch,在 Tape 里算 y,再 tape.gradient(y, x)。PyTorch:x = torch.tensor(3.0, requires_grad=True)y = x * xy.backward(),读 x.grad。同一个数学事实,两套仪式。把这六行当对照课的「加法验证」升级版:安装节验证运算,本节验证求导。

💡 关键直觉:自动求导录的是「这次实际执行的那条路径」。动态图里 if 走了哪一分支,梯度就只沿那一分支回传。这正是 PyTorch 默认心智,也是 TF 即时执行下 Tape 的心智。

默认盯谁:Variable / Parameter 对普通张量

TensorFlow 的 GradientTape 默认监视 tf.Variable。Keras 层里的权重是 Variable,所以你在 Tape 里调用模型,再对 model.trainable_variables 求梯度,就能接到优化器。普通 tf.constant 默认不进监视名单,要对它求导得 tape.watch(x)。这是 SOURCE 选型段点名 GradientTape 之后,入门最容易漏的一句。自定义训练循环里对输入图像求梯度(对抗样本一类)必须显式 watch;对权重则通常不必。

PyTorch 的默认更「张量中心」:任何 requires_grad=True 的叶子都会被追踪。nn.Parameter 默认就是 True。由它们计算出来的中间张量会带 grad_fn,表示从哪条运算来。你很少对中间张量求 .grad,而是对叶子读 .gradbackward 默认要求被调用的张量是标量;批次损失要先 .mean().sum(),否则要传入 gradient 参数——入门请把损失定义成标量,少踩这个坑。

问题 GradientTape autograd
默认监视 tf.Variable requires_grad=True 的叶子
取出梯度 tape.gradient(loss, vars) loss.backward() 后读 .grad
多次反向 默认 Tape 用完释放,需 persistent 图默认释放,需 retain_graph
推理关记录 不进 Tape 即可 torch.no_grad()
累加行为 每次 gradient 返回新张量 .grad 会累加,必须清零

累加是 PyTorch 训练循环里 optimizer.zero_grad() 存在的全部理由。上一批的梯度若还留在 .grad 里,下一批 backward 会加上去,等于把学习率按批次胡乱放大。TensorFlow 的 tape.gradient 每次返回新的梯度张量,Keras fit 内部也不会让你碰到累加;一旦你写自定义循环并用 apply_gradients,仍要保证没有把旧梯度错误地再 apply 一次。对照时请把「清梯度」理解成 PyTorch 的显式步骤、Keras 的隐含步骤,而不是「TF 没有梯度」。

持久 Tape 与 retain_graph 属于同一类需求:还想再反向一次,例如高阶导,或对同一前向输出分别对两套变量求导。入门分类器用不到。看到这两个参数,先问自己是不是误把训练步骤写进了双重循环。过早打开 persistent 会占内存,Tape 会把更多中间结果留下来。

停止记录的 API 也成对。TF:with tape.stop_recording() 包住不需要梯度的辅助计算(例如用当前模型做一次只读评估)。PT:torch.no_grad() 包住验证循环;另有 torch.inference_mode() 在更新版本里更彻底,但原文以 no_grad 为准,本课不另造版本故事。注意:model.eval() 不是关梯度。它切换 Dropout、BatchNorm 的行为。验证时两者常常同时出现:eval + no_grad。只写其中一个,要么统计层仍按训练态更新,要么白白录一套用不到的图。

# 概念对照:标量二次函数 import tensorflow as tf x_tf = tf.Variable(3.0) with tf.GradientTape() as tape: y_tf = x_tf * x_tf g_tf = tape.gradient(y_tf, x_tf) # 应为 6 import torch x_pt = torch.tensor(3.0, requires_grad=True) y_pt = x_pt * x_pt y_pt.backward() g_pt = x_pt.grad # 应为 6

⚠️ 常见坑:在 PyTorch 里对已经 backward 过的图再次 backward 却不 retain_graph,或反过来每次都 retain 导致显存缓慢涨死。另一高频坑:TF 自定义循环里对 tf.constant 输入求梯度却忘记 watch,得到 None 还以为是网络没连上。

和训练循环如何咬合

Keras 的 fit 把 Tape、梯度、apply 藏起来。你仍应能在脑子里展开:每个批次前向、算损失、Tape 对 trainable_variables 求导、优化器按梯度改 Variable。PyTorch 则把展开写在纸面上:zero_grad、前向、损失、backwardstep。第 4.3 节会把这两段并排。本节只要求你承认:没有求导系统,优化器只是在改随机数。

梯度为 None 或全零,诊断顺序两边通用。先看变量是否真的参与了损失:断掉的计算图(例如中途 .numpy() 再转回张量)会让 TF 得到 None、让 PT 的叶子 .grad 仍是 None。再看是否在 no_grad 里训练。再看学习率是否写成了 0。最后才怀疑层没注册进 nn.Module 或没放进 trainable_variables。对照课把「断图」当作比「算法错」更常见的事故。

内存方面,前向留下的激活是反向的原料。批次越大、层越深,峰值显存越高。验证时关掉求导,显存立刻下降一截,所以原文评估段强调 torch.no_grad()。Keras evaluate 同样不会为了训练去保留那套反向状态。你若在验证循环里忘记关,第一次只会觉得「怎么验证比训练还吃显存」。

高阶导数(梯度的梯度)在元学习、某些二阶优化里出现,入门不做。若你看到 Tape persistent=True 再对梯度求梯度,知道那是同一套系统的递归应用即可。不要为了「高级」在 Fashion MNIST 上开二阶。

和数值梯度核对是怀疑求导实现时的最后手段:对单个标量参数加微小扰动,看损失差商是否接近自动梯度。框架自身很少在入门任务上算错;错的是你的图。核对能告诉你「图断了」还是「损失写错了」(例如平均成了求和,梯度整体差一个批次因子)。批次因子这类 bug 两侧都会犯,对照时用同一损失归约(都 mean 或都 sum)才能比较学习率。

自定义循环里 Tape 怎么接到优化器

Keras fit 隐藏了本节几乎全部 API。一旦你写自定义循环,标准套路是:在 GradientTape 里做前向并算出损失,grads = tape.gradient(loss, model.trainable_variables)optimizer.apply_gradients(zip(grads, model.trainable_variables))。这与 PT 的 backward+step 一一对应,只是梯度以列表返回而不是写在 .grad 上。对照到这一步,两边的「神秘」都会消失。原文入门不强制你写 Tape 循环,但读懂这四行,才能在需要梯度裁剪时知道刀插在哪:PT 在 backwardstep 之间;TF 在 gradientapply_gradients 之间。

None 梯度的另一来源是布尔索引或 NumPy 函数切断了追踪。TF 里对张量调用 .numpy() 再计算,结果是普通数组,Tape 看不见。PT 里对叶子用了原地运算且未按规则来,可能报错或破坏图。调试手段是打印 tape.watched_variables() 或 PT 的 grad_fn 是否还在。不要用「再训几个 epoch」来治断图。

问题:验证时为什么有人只用 eval 不用 no_grad?

eval 切换层行为,no_grad 关闭记录。只 eval 时前向仍可能建图,浪费内存;只 no_grad 时 Dropout 仍按训练随机丢弃,验证准确率偏低且抖。两者都写,意图分离。Keras evaluate 内部会处理学习阶段,你少踩这个坑,但自定义 TF 循环时同样要保证没有在训练模式下做验证,也没有把验证批次送进 apply_gradients。对照模板把两行写成固定开头,比每次临场发挥安全。

高阶导数在入门的唯一价值是理解 persistent:Tape 默认用过即弃,再问一次梯度会报错。你若误把训练步骤写进双重 Tape,可能碰到这类报错。解决不是开 persistent 硬顶,而是删掉多余的 Tape。PT 的 retain_graph 同理。看到这两个参数,先假设自己把循环写套了。内存涨、速度慢,常常是图被留住了。Fashion MLP 不需要二阶,不要为了笔记完整去求梯度的梯度。

watch 在对抗样本或可视化显著性时会出现:输入不是 Variable,却要对输入求导。分类器训练不需要 watch 像素。若你复制了可视化代码进训练循环,像素被更新而不是权重被更新,准确率会胡闹。对照检查:优化器的变量列表应是层权重,不应包含图像批次。Keras fit 不会更新输入;手写循环有这个自由度,也有这个枪口。

本章回顾

  • 自动微分:前向留轨迹,反向链式回传;不是数值扰动,也不是纯符号展开
  • Tape 盯 Variable:常量要 watch;Keras 权重默认已是 Variable
  • autograd 盯 requires_gradbackward 写在标量损失上;叶子上读 .grad
  • 累加只在 PT 显眼:必须 zero_grad;TF 每次 gradient 返回新值
  • 关记录 ≠ 评估模式no_grad 管图,eval 管 Dropout 与 BN
  • None 梯度优先查断图:中途转 NumPy、在 no_grad 里训练、层未注册

把「梯度为 None」做成决策树贴在求导节:变量有没有参与损失,中途有没有转成 NumPy,有没有在 no_grad 里训练,层有没有登记进模块或 trainable_variables,优化器有没有拿到这些变量。五问都否,才怀疑框架实现。入门任务上框架几乎不会算错二次函数,错的是图。二次函数玩具应保留在笔记本第一格,每次改循环先跑它。能求出 6,再跑 Fashion。Tape 的 persistent 与 retain_graph 继续当成「循环写套了」的信号,而不是进阶勋章。

二次函数体检保留:x 等于 3,y 等于 x 乘 x,梯度应为 6。每次改循环先跑。Tape 监视 Variable,常量要 watch。autograd 读叶子的 grad,backward 前要清。验证两行:eval 加 no_grad。None 梯度走五问决策树。persistent 当循环写套的信号。像素不要进优化器变量列表。这些句子构成求导节的出门证。出门证不过,禁止 Fashion。能求出 6 却在 Fashion 上梯度全无,去查层登记与损失是否接到输出,不要怀疑自动微分系统本身。

把求出梯度 6 当成 2.2 出门证。Variable 默认被盯,常量要 watch。清梯度在 PT 是显式的。验证两行齐。None 走五问。persistent 当写套信号。像素不进优化器列表。出门证不过禁止 Fashion。能求出 6 但 Fashion 无梯度,查登记与损失是否接到输出。不要升级框架版本来治断图。断图是图的问题,版本是另一张表。

把梯度 6 的单元格置顶。置顶不是装饰,是每次改循环的回归测试。回归不过禁止下发 Fashion。下发了得到随机准确率,先回置顶单元格,不要先改学习率。学习率改不了断图。断图用五问。五问结束仍 None,打印 watched 变量或 grad_fn。打印比猜版本有效。

审查二次函数单元格是否在改循环之后仍然输出约 6。约的意思是浮点误差,不是 5 或 0。0 是断图。5 是写错了函数。6 才放行。放行记录写一行时间,证明回归测试发生过。没时间戳的 6,可能是上周的输出。

下一节把层与激活放到对照表上:Dense 对 Linear,Softmax 放在模型里还是放在损失里。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U