参考用时 · 90 分钟
用 Tensor 包住 numpy 数组,加入 grad、_backward、_prev 三个字段。
完成标志:一个对象里能找到数值、梯度和计算图信息。
18 / 一次完成一个小任务
mini-torch 的第一站是一个能检查答案的计算图,不是完整复刻训练框架。
先抓住一个具体结果
令 x=3,y=x×x,那么 y=9,dy/dx=6。先让你的反向传播得到 6,再加入更多运算,并与数值差分核对。
接下来,按需要选一项
这些入口是选择,不是必须按顺序完成的任务。
mini-torch 的第一站是一个能检查答案的计算图,不是完整复刻训练框架。
需要更多细节时,继续看完整正文 →原有正文、图解和例子都在下方。按需跳转,不必一次读完。
⏱ 5–7 天 | 难度 ★★★★☆ | 前置:第 7–11、15 章
第 8 章你手写了一个固定网络的反向传播。这个项目更进一步: 造一个通用的自动微分引擎——任意搭网络,它自动求导。
做完你会得到的东西:
import minitorch as mt
model = mt.Sequential(
mt.Linear(30, 64), mt.ReLU(),
mt.Linear(64, 1))
loss = mt.BCEWithLogits(model(x), y)
loss.backward() # ← 你自己实现的!
optimizer.step()
然后你会彻底明白:
.backward() 到底在干什么.detach() 能切断梯度这是理解深度学习框架的分水岭。 之后你用任何框架,都知道皮下是什么。
先看关系:Tensor 是共同的核心;网络层、损失函数和优化器都围绕它工作。 下面不是执行顺序,也不是目录清单。
一次运算,做两件事
__add__、__mul__、__matmul__:先算出结果,再记录这个结果如何把梯度传回父节点。
两个 backward,职责不同
._backward:只负责当前节点怎么反传。backward():先拓扑排序整张图,再从输出往输入调用各节点的 ._backward。三个模块,各管一件事
nn.Module:Linear / ReLU / Sequential / Dropout,负责搭网络、算输出。optim:SGD / Momentum / Adam,负责用梯度更新参数。一次只选一项。时间是参考,不是倒计时;较长的任务可以分几次完成。勾选表示你已核对该项结果,不代表页面替你运行了测试。
共 14 项任务,可逐项勾选。
这一阶段最关键:先让一个小计算图求对梯度,再扩展网络。
参考用时 · 90 分钟
用 Tensor 包住 numpy 数组,加入 grad、_backward、_prev 三个字段。
完成标志:一个对象里能找到数值、梯度和计算图信息。
参考用时 · 120 分钟,可按算子拆开做
实现 +、*、@ 的前向计算和反向传播。
完成标志:三个算子既能给出结果,也能把梯度传回输入。
难点:广播的反向。形状不匹配时,梯度要 sum 回原来的形状。
参考用时 · 90 分钟
实现 backward():拓扑排序整张图,从输出往输入依次调用 _backward。
完成标志:共享节点等所有下游贡献累加齐,再向前传梯度。
参考用时 · 60 分钟
逐个算子对比数值梯度与解析梯度。
完成标志:相对误差小于 1e-6;不能只凭网络能训练就跳过。
停靠点:T1–T4 通过后,你已经有了一个可检验的自动微分内核。
把已验证的算子组合起来,让搭网络不再需要手写每条连接。
参考用时 · 60 分钟
实现线性层,并加入 Xavier / He 参数初始化。
完成标志:线性层能产生正确形状的输出,参数参与计算图。
参考用时 · 45 分钟
实现 ReLU、Tanh、Sigmoid。
完成标志:三种激活都能前向计算,并传回梯度。
参考用时 · 60 分钟
实现 MSELoss 和 BCEWithLogitsLoss。
完成标志:能算出损失,并反传到预测值。
注意数值稳定:BCEWithLogitsLoss 使用 logsumexp 技巧。
参考用时 · 45 分钟
实现 Sequential 和递归收集参数的 parameters()。
完成标志:多层能顺序运行,内部可训练参数都能被收集到。
停靠点:现在能搭网络、算损失;下一阶段才加入参数更新。
让参数真正更新,观察你的框架能不能学会一个小任务。
参考用时 · 60 分钟
实现 SGD(含 momentum)和 Adam。
完成标志:调用更新方法后,参数按照对应规则改变。
参考用时 · 45 分钟
实现 zero_grad(),观察连续反传时梯度为什么默认累加。
完成标志:你能分别演示“累加梯度”和“清零后重新计算”。
参考用时 · 60 分钟
用自己的框架训练异或,再训练 load_digits 手写数字。
完成标志:留下两次训练的损失记录;手写数字的最终验收见下方通关标准。
停靠点:能训练是进展,但还不能替代与参考框架的逐项比较。
对拍就是给两个框架相同条件,比较结果,而不只是看最后的准确率。
参考用时 · 90 分钟
使用同样的网络、初始权重和输入,逐元素比较 loss 与梯度。
完成标志:误差小于 1e-5,并核对通关标准要求的前 10 步。
参考用时 · 60 分钟
加入 Dropout 和 no_grad() 上下文管理器。
完成标志:检查 Dropout 的训练与推理行为,以及 no_grad 下不记录求导图的行为。
参考用时 · 90 分钟
README 包含架构图、计算图可视化、对拍结果,以及“我从造轮子里理解了什么”。
完成标志:别人能顺着报告了解结构、复现比较,并看懂你的结论。
停靠点:保留对拍记录和报告,再逐项核对下方通关标准。
import numpy as np
class Tensor:
def __init__(self, data, _prev=(), requires_grad=True):
self.data = np.asarray(data, dtype=np.float64)
self.grad = np.zeros_like(self.data)
self._prev = set(_prev)
self._backward = lambda: None # 默认叶子节点什么都不做
self.requires_grad = requires_grad
def __add__(self, other):
other = other if isinstance(other, Tensor) else Tensor(other)
out = Tensor(self.data + other.data, (self, other))
def _backward():
# ⭐ 难点:广播的反向 —— 把梯度 sum 回原来的形状
self.grad += unbroadcast(out.grad, self.data.shape)
other.grad += unbroadcast(out.grad, other.data.shape)
out._backward = _backward
return out
def __matmul__(self, other):
out = Tensor(self.data @ other.data, (self, other))
def _backward():
self.grad += out.grad @ other.data.T
other.grad += self.data.T @ out.grad
out._backward = _backward
return out
def relu(self):
out = Tensor(np.maximum(0, self.data), (self,))
def _backward():
self.grad += out.grad * (self.data > 0)
out._backward = _backward
return out
def backward(self):
# 拓扑排序:保证调用某节点的 _backward 前,它的所有下游都已算完
topo, visited = [], set()
def build(v):
if v not in visited:
visited.add(v)
for p in v._prev: build(p)
topo.append(v)
build(self)
self.grad = np.ones_like(self.data) # 起点:dloss/dloss = 1
for node in reversed(topo):
node._backward()
def unbroadcast(grad, shape):
"""把广播后的梯度还原回原形状 —— 这是最容易写错的地方"""
while grad.ndim > len(shape):
grad = grad.sum(axis=0)
for i, s in enumerate(shape):
if s == 1:
grad = grad.sum(axis=i, keepdims=True)
return grad
| 坑 | 说明 |
|---|---|
| 广播的反向 ⭐ 最常错 | (32,64) + (64,) 的反向要把梯度 sum 到 (64,)。不处理会形状报错或静默算错 |
| 梯度累加 vs 覆盖 | 必须用 += 不是 = —— 一个节点可能被多条路径用到(这就是 PyTorch 梯度累加的由来) |
| 拓扑排序顺序 | 必须保证下游全部算完才算上游,否则梯度不全 |
| 数值稳定 | softmax 要减 max;log 要加 eps;BCE 要用 logsumexp 形式 |
| 只测网络不测算子 | 网络能训不代表每个算子都对。逐算子做梯度检验 |
| 原地修改 numpy 数组 | 会破坏计算图,调试到怀疑人生 |
| 去哪 | 为什么 |
|---|---|
| 数学原理 12 | ⭐ 手搓 autograd 之前先把链式法则的矩阵形式推一遍,否则调试时无从下手 |
| AI基础设施 09 | 做到「存不存中间值」那一步时:这就是梯度检查点,用时间换显存 |
| 智能体 18 | 同构的「手搓一个框架」项目,阶段切分方式可以直接借 |
load_digits 到测试精度 > 92%zero_grad() / no_grad() / DropoutConv2d(用 im2col 技巧)—— 难度陡增,但做出来极有成就感Tensor 需要携带哪几样东西?为什么每个运算都要记录 _backward?backward() 为什么必须先做拓扑排序?不排会怎样?zero_grad() 的那一刻,你会理解 PyTorch 的什么设计?_prev 前驱、_backward 怎么反传)。记录 _backward 是因为反向传播时需要知道"这个结果是怎么算出来的"——前向时不记,反向时就无从下手。(1, n) 的偏置广播成 (m, n) 参与运算,它的梯度必须沿 batch 维求和才能回到 (1, n)。忘了 unbroadcast 会直接形状报错,或者更糟——静默地算错。zero_grad() 是你的责任,忘了训练就废。走神救援 · 只记这四点
backward() 排好顺序再反传;共享节点的梯度要先累加齐。今天可以停下。下次回到四阶段任务区,从尚未完成的一项继续。
下一个挑战 👉 19-挑战项目B-四种解法对决.md