Session S05
Vectorized backprop · how autograd engines work向量化反向传播 · 自动微分引擎如何工作
Deep Learning — CS 4715/5715 · University of Idaho
Chain (nodes in series, \(a \to b \to L\)): multiply along the path链(串联的节点,\(a \to b \to L\)):沿路径相乘
\[\frac{\partial L}{\partial a} = \frac{\partial L}{\partial b}\,\frac{\partial b}{\partial a}\]
Reuse (one node feeding consumers \(b_1, \dots, b_k\); the reading calls this fan-out): sum across paths复用(一个节点喂给消费者 \(b_1, \dots, b_k\);阅读里叫 fan-out):跨路径求和
\[\frac{\partial L}{\partial a} = \sum_{i=1}^{k} \frac{\partial L}{\partial b_i}\,\frac{\partial b_i}{\partial a}\]
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial u}\,\frac{du}{dx} + \frac{\partial L}{\partial v}\,\frac{dv}{dx} = v \cdot 1 + u \cdot 1 = x + x = 2x\]
Linear layer \(Z = XW + b\), with \(X: (N, D)\), \(W: (D, H)\), incoming \(dZ = \partial L/\partial Z: (N, H)\):线性层 \(Z = XW + b\),其中 \(X: (N, D)\),\(W: (D, H)\),传入的 \(dZ = \partial L/\partial Z: (N, H)\):
Shape discipline: a parameter's gradient always has the parameter's shape — \((D,N)(N,H)=(D,H)\) checks the transpose; the chain rule establishes the formula形状纪律:参数的梯度永远和参数同形;\((D,N)(N,H)=(D,H)\) 帮助检查转置,公式本身由链式法则推出
Broadcast forward ⇒ sum backward: \(b\) was copied across N rows, so its gradient sums the N contributions前向广播 ⇒ 反向求和:\(b\) 被复制到 N 行,所以它的梯度把 N 份贡献加起来
2 examples · 1 input feature and 1 output each · \(b=0,\ W=[w]\)2 个样本 · 每个样本 1 个输入特征、1 个输出 · \(b=0,\ W=[w]\)
Change from \(w=2.0\):相对 \(w=2.0\) 的变化: \(\Delta w=0.0,\quad \Delta z_1=0.0,\quad \Delta z_2=0.0\)
Local derivative = input:局部导数 = 各自的输入: \(\displaystyle\frac{\partial z_1}{\partial w}=2,\quad\frac{\partial z_2}{\partial w}=3\)
At \(w=2\), take the incoming \(dZ=\partial L/\partial Z\) as given.
The remaining computation has already returned \(dZ_1=1,\ dZ_2=2\).在 \(w=2\) 处,把传入梯度 \(dZ=\partial L/\partial Z\) 作为已知量。
后续计算已传回 \(dZ_1=1,\ dZ_2=2\)。
For any weight entry:推广到任意权重元素: \(\displaystyle dW_{ij}=\sum_n X_{ni}\,dZ_{nj}=(X^\top dZ)_{ij}\)
Feature column \(i\) of \(X\) · gradient column \(j\) of \(dZ\): multiply matching rows, then sum.取 \(X\) 的特征列 \(i\) 和 \(dZ\) 的梯度列 \(j\):对应行相乘,再求和。
Same values: \(w=2,\ dZ_1=1,\ dZ_2=2\). Each input has local derivative \(w\).沿用 \(w=2,\ dZ_1=1,\ dZ_2=2\)。每个输入对应的局部导数都是 \(w\)。
| Input输入 | Incoming × local传入梯度 × 局部导数 | Its gradient各自的梯度 |
|---|---|---|
| \(x_1\) | \(1\times2\) | \(dx_1=2\) |
| \(x_2\) | \(2\times2\) | \(dx_2=4\) |
\(x_1\) and \(x_2\) are different nodes. Keep \(2\) and \(4\) separate.\(x_1\)、\(x_2\) 是不同节点,梯度 \(2\)、\(4\) 分别保存。
\[dX_{ni}=\sum_j dZ_{nj}W_{ij}=(dZ\,W^\top)_{ni}\]
With several outputs, one input reaches every output column \(j\): sum over \(j\), within the same example.有多个输出时,一个输入影响每个输出列 \(j\):在同一样本内对 \(j\) 求和。
\(z_n=x_nw+b\). Even at \(b=0\), the local derivative with respect to \(b\) is \(1\).\(z_n=x_nw+b\)。即使当前 \(b=0\),输出对 \(b\) 的局部导数仍为 \(1\)。
| Example样本 | Incoming × local传入梯度 × 局部导数 | Contribution to the same b对同一个 b 的贡献 |
|---|---|---|
| 1 | \(1\times1\) | 1 |
| 2 | \(2\times1\) | 2 |
\[db=1+2=3\]
\[db_j=\sum_n dZ_{nj},\qquad \operatorname{shape}(db)=(H,)\]
One bias per output column, reused across examples: broadcast forward, sum backward.每个输出列有一个偏置,在不同样本间共享:前向广播,反向求和。
Two trainable layers: \((W_1,b_1)\) and \((W_2,b_2)\). ReLU follows the first affine transformation.两组可训练层:\((W_1,b_1)\) 与 \((W_2,b_2)\)。第一层仿射变换之后使用 ReLU。
The diagram shows one example. Repeat the same network for all \(N\) rows; \(L\) averages their losses.图中画出一个样本。对全部 \(N\) 行使用同一个网络;\(L\) 对各行损失取平均。
\(N\): examples · \(D\): input features · \(H\): hidden units · \(C\): classes.\(N\):样本数 · \(D\):输入特征数 · \(H\):隐藏单元数 · \(C\):类别数。
| Array数组 | Definition定义 | Shape形状 |
|---|---|---|
| \(X\) | Input batch输入 batch | \((N,D)\) |
| \(Z_1\) | \(XW_1+b_1\) | \((N,H)\) |
| \(A_1\) | \(\max(0,Z_1)\) | \((N,H)\) |
| \(Z_2\) | \(A_1W_2+b_2\) | \((N,C)\) |
| \(P\) | \(\operatorname{softmax}(Z_2)\) | \((N,C)\) |
| \(L\) | \(-\frac1N\sum_n\log P_{n,y_n}\) | scalar标量 |
Parameter shapes: \(W_1:(D,H),\ b_1:(H,),\ W_2:(H,C),\ b_2:(C,)\).参数形状:\(W_1:(D,H),\ b_1:(H,),\ W_2:(H,C),\ b_2:(C,)\)。
\[P_{nc} = \frac{e^{Z_{2,nc}}}{\sum_{c'} e^{Z_{2,nc'}}} \qquad \ell_n = -\log P_{n,\,y_n} \qquad L = \frac{1}{N}\sum_{n} \ell_n\]
\[\ell_n = -Z_{2,ny_n} + \log \sum_{c'} e^{Z_{2,nc'}}\]
Backward route: \(L\ \longrightarrow\ dZ_2\ \longrightarrow\ dA_1\ \longrightarrow\ dZ_1\).反向路线:\(L\ \longrightarrow\ dZ_2\ \longrightarrow\ dA_1\ \longrightarrow\ dZ_1\)。
\[\frac{\partial\ell_n}{\partial Z_{2,nc}}=\underbrace{-\mathbf1[c=y_n]}_{\text{true-class score}}+\underbrace{P_{nc}}_{\text{log-sum-exp}}\]
\[dZ_2=\frac{\partial L}{\partial Z_2}=\frac{P-Y}{N},\qquad Y=\operatorname{onehot}(y)\]
Shape: \(P,\ Y,\ dZ_2\in\mathbb R^{N\times C}\): \(N\) rows, \(C\) columns.形状:\(P,\ Y,\ dZ_2\in\mathbb R^{N\times C}\):\(N\) 行、\(C\) 列。
Subtract 1 only at each true class. Divide by N once because the loss averages examples.只在每行的真实类别处减 1。损失对样本取平均,因此在这里除一次 N。
Local forward operation: \(Z_2=A_1W_2+b_2\). Incoming gradient: \(dZ_2\).当前的前向运算:\(Z_2=A_1W_2+b_2\)。传入梯度:\(dZ_2\)。
| Derivative of L with respect toL 对谁求导 | Formula计算公式 | Shape形状 |
|---|---|---|
| \(W_2\) | \(dW_2=A_1^\top dZ_2\) | \((H,C)\) |
| \(b_2\) | \(db_2=\sum_n(dZ_2)_{n,:}\) | \((C,)\) |
| \(A_1\) | \(dA_1=dZ_2W_2^\top\) | \((N,H)\) |
Forward: \(A_1=\max(0,Z_1)\). Backward: \(dZ_1=dA_1\odot\mathbf1[Z_1>0]\).前向:\(A_1=\max(0,Z_1)\)。反向:\(dZ_1=dA_1\odot\mathbf1[Z_1>0]\)。
| One entry of某个元素 | 1 | 2 | 3 |
|---|---|---|---|
| \(Z_1\) | −2 | 0 | 3 |
| Local derivative / mask局部导数 / 掩码 | 0 | 0 | 1 |
| \(dA_1\) | 4 | 5 | 6 |
| \(dZ_1\) | 0 | 0 | 6 |
At zero, the ordinary derivative does not exist; this backward rule chooses \(0\).零点不存在普通导数;这里的反向规则约定取 \(0\)。
All three arrays have shape \((N,H)\). \(\odot\) means elementwise multiplication.三个数组的形状都是 \((N,H)\)。\(\odot\) 表示逐元素相乘。
Local forward operation: \(Z_1=XW_1+b_1\). Incoming gradient: \(dZ_1\).当前的前向运算:\(Z_1=XW_1+b_1\)。传入梯度:\(dZ_1\)。
| Formula公式 | Shape形状 | Use用途 |
|---|---|---|
| \(dW_1=X^\top dZ_1\) | \((D,H)\) | Weight gradient权重梯度 |
| \(db_1=\sum_n(dZ_1)_{n,:}\) | \((H,)\) | Bias gradient偏置梯度 |
| \(dX=dZ_1W_1^\top\) | \((N,D)\) | If an upstream task needs it上游任务需要时计算 |
Saved values: \(X,A_1\) for weight gradients; \(Z_1\) for the mask; \(P\) for \(dZ_2\); \(W_2\) for \(dA_1\).保存值:权重梯度用 \(X,A_1\);掩码用 \(Z_1\);\(dZ_2\) 用 \(P\);\(dA_1\) 用 \(W_2\)。
The implementation below returns the four parameter gradients; its input data are fixed.接下来的实现返回四个参数梯度,输入数据视为固定。
Inside forward_backward(params, X, y); params stores W1, b1, W2, b2.位于 forward_backward(params, X, y) 内;params 保存 W1、b1、W2、b2。
W1, b1 = params["W1"], params["b1"]
W2, b2 = params["W2"], params["b2"]
N = X.shape[0]Z1 = X @ W1 + b1A1 = np.maximum(0, Z1)Z2 = A1 @ W2 + b2@ multiplies matrices; each bias broadcasts across the N rows.@ 执行矩阵乘法;每个偏置向量广播到 N 行。
\[s_{nc}=Z_{2,nc}-\max_{c'}Z_{2,nc'},\qquad \log P_{nc}=s_{nc}-\log\sum_{c'}e^{s_{nc'}}\]
Z2s = Z2 - Z2.max(axis=1, keepdims=True)
logp = Z2s - np.log(np.exp(Z2s).sum(axis=1, keepdims=True))
loss = -logp[np.arange(N), y].mean()axis=1: operate across classes in each example. keepdims=True: retain shape (N, 1) for row-wise broadcasting.axis=1:在每个样本内沿类别计算。keepdims=True:保留 (N, 1) 形状,便于逐行广播。\[dZ_2=(P-Y)/N\]
dZ2 = np.exp(logp)
dZ2[np.arange(N), y] -= 1
dZ2 /= N| Line代码行 | Meaning含义 |
|---|---|
| 1 | Create a new array \(P\) from \(\log P\).从 \(\log P\) 生成新数组 \(P\)。 |
| 2 | Subtract the one-hot labels without building a full Y matrix.不创建完整 Y 矩阵,直接在独热标签的位置减 1。 |
| 3 | Account for averaging the N losses.计入对 N 个损失取均值的因子。 |
y = [2, 0] → row indices [0, 1] → selected entries (0, 2) and (1, 0).y = [2, 0] → 行索引 [0, 1] → 选中 (0, 2) 和 (1, 0)。
The index example uses 2 rows and 3 classes. All later gradients already carry the 1/N factor.索引示例使用 2 行、3 个类别。后续梯度已经包含 1/N 因子。
grads = {}grads["W2"] = A1.T @ dZ2grads["b2"] = dZ2.sum(axis=0)dA1 = dZ2 @ W2.Tgrads stores parameter derivatives under the same keys as params.grads 使用与 params 相同的键,保存参数导数。.T transposes a 2-D array. axis=0 sums examples and keeps the class columns..T 转置二维数组。axis=0 对样本求和,保留类别列。dZ1 = dA1 * (Z1 > 0)grads["W1"] = X.T @ dZ1grads["b1"] = dZ1.sum(axis=0)return loss, gradsZ1 > 0 creates a Boolean mask. Multiplication treats False/True as 0/1.Z1 > 0 创建布尔掩码,相乘时 False/True 对应 0/1。* is elementwise; @ is matrix multiplication. The function returns a scalar loss and four parameter-gradient arrays.* 逐元素相乘,@ 做矩阵乘法。函数返回标量损失和四个参数梯度数组。Reading example: \(N=8,\ D=3,\ H=4,\ C=2\), float64, random seed 0. Keep the batch and every other parameter fixed.阅读中的例子:\(N=8,\ D=3,\ H=4,\ C=2\),float64,随机种子 0。固定 batch 和其余全部参数。
name, idx = "W1", (0, 0)
p = params[name] # the actual array, not a copy
eps = 1e-5
loss, grads = forward_backward(params, X, y)| Parameter value参数值 | Recomputed loss (rounded)重算的损失(显示值已舍入) |
|---|---|
| \(p_{00}+\epsilon\) | 1.0065300857 |
| \(p_{00}-\epsilon\) | 1.0065315136 |
\[g_{\rm num}=\frac{L(p+\epsilon)-L(p-\epsilon)}{2\epsilon}\]
analytic (backward) = -0.0713913172
numeric (two losses) = -0.0713913172def numeric_entry(p, idx):
old = p[idx]
try:
p[idx] = old + eps
lp, _ = forward_backward(params, X, y)
p[idx] = old - eps
lm, _ = forward_backward(params, X, y)
finally:
p[idx] = old
return (lp - lm) / (2 * eps)p refers to an array in params; changing p[idx] changes what forward reads.p 引用 params 中的数组;修改 p[idx] 就改变了前向读到的参数。finally restores the original value even if evaluation raises an error. No parameter update occurs.finally 即使在计算报错时也恢复原值。这里不执行参数更新。for name, p in params.items():
num = np.zeros_like(p)
for idx in np.ndindex(p.shape):
num[idx] = numeric_entry(p, idx)
ana = grads[name]
assert ana.shape == p.shape
rel = np.abs(num - ana) / (np.abs(num) + np.abs(ana) + 1e-12)
print(name, f"{rel.max():.2e}")W1 4.32e-10 b1 5.78e-10
W2 7.99e-11 b2 6.01e-12These maxima summarize all entries of each array. Also inspect absolute error when both gradients are near zero.这些最大值汇总了每个数组的全部元素。两个梯度都接近零时,还要检查绝对误差。
A small discrepancy supports this backward implementation for this forward function and these inputs.误差很小,支持这份反向实现在当前前向函数与输入上的正确性。
| Part组成 | Responsibility职责 | Interface接口 |
|---|---|---|
| A Value nodeValue 节点 | Store a number and accumulated gradient保存数值与累积梯度 | data, grad |
| An operation一个运算 | Make an output, link its inputs, attach its local rule创建输出,连接输入,保存局部规则 | _prev, _backward |
| The backward driver反向执行器 | Order nodes, then call each stored rule排列节点,再调用保存的规则 | backward() |
a, b, c = Value(2.0), Value(3.0), Value(1.0)
p = a * b
q = p + c
y = q.tanh()
y.backward()Forward builds the graph and saves rules. Backward later calls those rules in dependency order.前向创建计算图并保存规则;反向随后按依赖关系调用规则。
class Value:
def __init__(self, data, _children=()):
self.data = float(data) # forward value
self.grad = 0.0 # accumulated dL/d(self)
self._prev = set(_children) # input nodes: the parents
self._backward = lambda: None # leaf: nothing to propagate
Value(3.0) makes a leaf: empty parent set, zero gradient, a rule that does nothing.Value(3.0) 创建叶节点:父节点集合为空,梯度为零,反向规则不做任何操作。y = (a * b + c).tanh(): each operation creates a result node and replaces its empty rule. This is define-by-run.y = (a * b + c).tanh():每次运算创建一个结果节点,并设置它的反向规则。这就是 define-by-run。\[p=ab,\qquad q=p+c,\qquad y=\tanh(q)\]
def __mul__(self, other):
if not isinstance(other, Value):
other = Value(other)
out = Value(self.data * other.data,
(self, other))
# Attach the local backward rule next.| Expression表达式 | Meaning inside this method方法内部的含义 |
|---|---|
a * b | a.__mul__(b): self=a, other=b |
a * 3 | Wrap 3 in Value so every operand has data and grad把 3 包装成 Value,让每个操作数都有 data 和 grad |
out | A new node; its parents are the original operand objects一个新节点,其父节点就是原来的操作数对象 |
The forward result is ready, but its gradient is still zero. Its local backward rule has not run.前向结果已经就绪,但它的梯度仍为零,局部反向规则还没有运行。
Continuation of __mul__; self, other, and out are already defined.继续 __mul__;self、other、out 已经定义。
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out\[p=ab:\qquad \frac{\partial L}{\partial a}\mathrel{+}=b\,\frac{\partial L}{\partial p},\qquad \frac{\partial L}{\partial b}\mathrel{+}=a\,\frac{\partial L}{\partial p}\]
def creates a function; it does not run the indented updates.def 创建函数,不执行缩进中的梯度更新。return out returns the result node.不带括号的赋值保存函数;return out 返回结果节点。+= adds one operand-slot contribution, even when both operands are the same object.每条 += 都累加一个操作数槽的贡献,即使两个操作数是同一个对象。a, b = Value(2.0), Value(3.0)
p = a * b
print(p.data, a.grad, b.grad) # 6.0 0.0 0.0
p.grad = 1.0
p._backward() # call this one local rule
print(a.grad, b.grad) # 3.0 2.0| Stored rule for pp 保存的规则 | Still refers to仍然引用 |
|---|---|
self | a |
other | b |
out | p |
These are live object references. Do not mutate operand data between forward and backward.它们是对对象的引用。在前向与反向之间,不要修改操作数的 data。
def __add__(self, other):
if not isinstance(other, Value):
other = Value(other)
out = Value(self.data + other.data, (self, other))
def _backward():
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out\[q=a+b:\qquad \frac{\partial q}{\partial a}=1,\quad \frac{\partial q}{\partial b}=1\]
The constructor, parent links, closure, assignment and return follow the multiplication pattern.构造输出、连接父节点、定义闭包、保存函数与返回结果,都沿用乘法的结构。
def tanh(self):
t = math.tanh(self.data)
out = Value(t, (self,))
def _backward():
self.grad += (1.0 - t * t) * out.grad
out._backward = _backward
return out
# Also inside Value:
__radd__ = __add__
__rmul__ = __mul__\[t=\tanh(x),\qquad \frac{dt}{dx}=1-t^2\]
(self,) is a one-element tuple. The closure reuses the forward output t.(self,) 是只有一个元素的元组,闭包复用前向输出 t。1 + x and 2 * x; this shortcut uses add/multiply commutativity.反向运算符方法支持 1 + x 和 2 * x;这种简写依赖加法与乘法的交换律。= instead of += and the three deposits overwrite each other先处理 \(p\) 再处理 \(f\),传播的 \(p.\mathrm{grad}\) 还是 0。把 += 写成 =,三份存入就互相覆盖a = Value(3.0)
p = a * a
f = p + a| After this action执行之后 | f.grad | p.grad | a.grad |
|---|---|---|---|
| Forward only只有前向 | 0 | 0 | 0 |
f.grad = 1.0 | 1 | 0 | 0 |
f._backward() | 1 | 1 | 1 |
p._backward() | 1 | 1 | \(1+3+3=7\) |
_prev = {a} visits the object once. The two multiply statements still contribute twice._prev = {a} 让遍历只访问该对象一次,但乘法中的两条更新仍然贡献两次。
def backward(self):
topo, visited = [], set()
def build(v):
if v not in visited:
visited.add(v)
for parent in v._prev:
build(parent)
topo.append(v)
build(self)| Statement语句 | Reason原因 |
|---|---|
visited.add(v) | Prevent repeated traversal of shared objects避免重复遍历共享对象 |
build(parent) | Finish dependencies first先处理依赖节点 |
topo.append(v) | Put each result after its parents把结果放在父节点之后 |
build(f) on f = a*a + a produces topo = [a, p, f].对 f = a*a + a 调用 build(f),得到 topo = [a, p, f]。
Continuation of the same backward(self) method after build(self).继续同一个 backward(self) 方法,接在 build(self) 之后。
self.grad = 1.0
for node in reversed(topo):
node._backward()\[\frac{\partial f}{\partial f}=1\]
| Execution order执行顺序 | What is ready before the call调用之前已经就绪 |
|---|---|
f._backward() | f.grad = 1 |
p._backward() | p.grad = 1 |
a._backward() | Leaf: its empty rule does nothing叶节点:空规则不执行任何更新 |
All consumers must contribute to a node before its rule propagates that total into its parents.一个节点的所有使用方都贡献完梯度,它的规则才能把总和继续传给父节点。
One example: \(x=2,\ y=3\). Model \(\hat y=wx\); loss \(L=(\hat y-y)^2\).一个样本:\(x=2,\ y=3\)。模型 \(\hat y=wx\),损失 \(L=(\hat y-y)^2\)。
w = Value(1.0)
for step in range(3):
w.grad = 0.0
pred = w * 2.0
error = pred + (-3.0)
loss = error * error
loss.backward()
print(f"{step}: w={w.data:.4f}, L={loss.data:.4f}, dw={w.grad:.4f}")
w.data -= 0.1 * w.grad0: w=1.0000, L=1.0000, dw=-4.0000
1: w=1.4000, L=0.0400, dw=-0.8000
2: w=1.4800, L=0.0016, dw=-0.1600Each printed line is before the update. Rebuild intermediates; keep the parameter object.每一行都在更新之前打印。重新创建中间节点,保留参数对象。
a, b = Value(2.0), Value(3.0)
z = a * b
z.backward()
assert (a.grad, b.grad) == (3.0, 2.0)
a = Value(3.0)
f = a * a + a
f.backward()
assert a.grad == 7.0def expression(a, b, c):
d = a * b + c
e = (d + a).tanh()
return e * d + ba=0.7, b=-1.3, c=2.0. Call this function with Value nodes, then with torch tensors.a=0.7, b=-1.3, c=2.0。先传入 Value 节点,再传入 torch 张量。
a, b, c = map(Value, (0.7, -1.3, 2.0))
out = expression(a, b, c)
out.backward()import torch
ts = [torch.tensor(v, dtype=torch.float32,
requires_grad=True)
for v in (0.7, -1.3, 2.0)]
at, bt, ct = ts
outt = expression(at, bt, ct)
outt.backward() micro torch |diff|
out -0.269121 -0.269121 5.19e-08
dL/da -1.263999 -1.263999 2.42e-08
dL/db 1.742557 1.742557 4.19e-08
dL/dc 1.060796 1.060796 5.93e-08Compare out.data with outt.item(), and each Value.grad with the matching tensor.grad.item().将 out.data 与 outt.item() 比较,再将每个 Value.grad 与对应 tensor.grad.item() 比较。
This example uses Python float versus torch float32. Use tolerances; matching rounded printouts is not exact equality.这里比较 Python float 与 torch float32,应使用容差;打印后相同不代表数值完全相等。
torch.no_grad() skips recording new reverse-mode history.保存的值:保留反向所需的数据;梯度和优化器状态也占内存。torch.no_grad() 不记录新的反向模式历史。.grad. PyTorch normally fills .grad only on leaves with requires_grad=True; use retain_grad() to inspect an intermediate.梯度保存:玩具引擎为每个节点保存 .grad。PyTorch 通常只填充 requires_grad=True 的叶节点;要查看中间节点可用 retain_grad()。ReLU at 0: no ordinary derivative; choose 0 for backward. Any subgradient in \([0,1]\) is valid. Exact zeros occur at any precision.ReLU 零点无普通导数;反向约定取 0。\([0,1]\) 中的数均为合法次梯度;任何精度下都可能出现恰好为零的输入。
# x is a floating-point leaf tensor requiring gradients
bad = torch.where(x > 0, torch.sqrt(x), 0.0) # sqrt runs first
mask = x > 0
good = torch.zeros_like(x)
good[mask] = torch.sqrt(x[mask]) # sqrt sees only x > 0
inf * 0 = nan. Mask inputs before sqrt.x=0:不安全的反向可能形成 inf * 0 = nan。在 sqrt 之前筛选输入。torch.autograd.detect_anomaly(): identify a NaN-producing backward function and its forward call.用 torch.autograd.detect_anomaly() 包住前向与反向:定位产生 NaN 的反向函数及其前向调用。retain_graph=True on the earlier call. A fresh forward builds a fresh graph.保存的张量:反向通常会释放它们。如果第二次反向仍需要它们,要在前一次调用设置 retain_graph=True。重新前向会构建新图。loss/k on each fresh graph, then step once.梯度累积:在一次更新的累积组开始前清零参数梯度。k 个等大小 minibatch 各用均值损失,在各自新图上反向传播 loss/k,最后更新一次。detach(): returns a tensor disconnected from earlier history. It shares storage; it does not freeze parameters or copy data by itself.detach():返回一个与先前历史断开的张量。它共享底层存储,本身不会冻结参数,也不复制数据。no_grad(): skips new reverse-mode recording in its context. model.eval() separately changes dropout/BatchNorm behavior.no_grad():在作用域内跳过新的反向模式记录。model.eval() 另行改变 dropout/BatchNorm 的行为。+=; compose neg, sub, and div from supported operations where appropriate.任务 1–5:add、mul、pow、neg、sub、div、tanh、relu、exp、log。为基础运算编写带 += 的局部规则;neg、sub、div 可以用已有运算组合。a*a+a reuse test and a separate branching diamond test check accumulation and ordering.任务 6:排列可达节点,令输出梯度为 1,倒序调用规则。a*a+a 复用测试和另一个分支菱形测试检查累加与顺序。fit_line: clear w.grad and b.grad; rebuild predictions and mean loss; backward; update the two parameters.任务 7,fit_line:清零 w.grad 与 b.grad;重新计算预测值和均值损失;反向传播;更新两个参数。a * a + aHW1(micro-autograd)S08 截止。今天的引擎就是蓝图;用 a * a + a 这样的复用表达式来测