跳到主要内容

数据截至 (上游 commit 7bc720e951fe)

micrograd — 架构与原理

30 秒导读: micrograd 是 Andrej Karpathy 写的一个教学用自动微分引擎:一个标量版的「迷你 PyTorch」,核心代码约 150 行(引擎 ~90 行 + 神经网络库 ~60 行),外加一个训练 demo 和一组与 PyTorch 对拍的测试。它存在的意义不是拿来用,而是让你一个下午读完、从此真正理解反向传播是怎么回事


1. 这是什么(零基础也能懂)

一句话定义

micrograd 是一个标量自动微分引擎 + 迷你神经网络库:你用普通 Python 写四则运算,它自动记下计算过程,然后能替你算出「输出对每个输入的导数」。

它要解决谁的什么问题

设想你在学深度学习,看过无数遍「反向传播 = 链式法则」,但始终有两个疑问:

  • 链式法则在代码里到底长什么样?PyTorch 的 loss.backward() 那一行背后发生了什么?
  • 一个神经网络库最少需要哪些零件,才能「定义网络 → 算梯度 → 更新参数」?

这些问题在 PyTorch 几十万行 C++/CUDA 里找不到答案。micrograd 把答案压缩到一个人能通读的尺寸:整个引擎就是一个文件、一个类。

它能做什么

  • 对标量表达式(+-***/relu 及其组合)做前向计算与反向求导。
  • 搭一个多层感知机(MLP),支持 ReLU 非线性,参数量级在几百个。
  • 端到端训练一个二分类器——demo 在 sklearn 的双月数据集上画出非线性决策边界(README.md:41)。
  • 画出计算图(trace_graph.ipynb 里的 draw_dot,用 graphviz 把每个节点的 data 和 grad 可视化)。

不能做的事同样重要,见 §6 边界与局限。

用起来什么样

下面这段就是它的全部用法(摘自 README.md:18-37,已精简):

from micrograd.engine import Value

a = Value(-4.0)
b = Value(2.0)
c = a + b # 普通 Python 运算,类型被悄悄换成了 Value
d = a * b + b**3
e = c - d
g = e**2 / 2.0
g.backward() # 一行:反向传播,算出 dg/d每个输入
print(a.grad) # dg/da 的数值

没有 import torch、没有 Variable、没有 requires_grad——运算符重载让 a + b 直接返回一个「带梯度记录功能」的 Value

一句话直觉

把每个数字想成一个「带记账本的包裹」。 每做一次运算,包裹就记下两笔账:「我是从哪两个包裹算出来的」(_prev)和「如果我的梯度到了,该往两个来源各分多少」(_backward 闭包)。反向传播,就是沿着账本从最后一笔往前逐笔清账。


2. 顶层全景(它大概怎么转)

2.1 部件与依赖

整个库只有两个模块,加上两个 notebook 和一个测试文件:

engine.py nn.py
┌──────────────┐ ┌────────────────┐
│ Value │◄───────│ Neuron Layer │ nn 只是 engine 的
│ data / grad │ 只依赖 │ MLP Module │ 一层薄封装,零新增机制
│ _backward │ │ parameters() │
│ backward() │ └───────┬────────┘
└──────┬───────┘ │
│ ▼
│ demo.ipynb
│ 训练闭环:MLP(2,[16,16,1])
│ + max-margin 损失 + 裸 SGD

test/test_engine.py
与 PyTorch 对拍:同一段表达式,两边前向值和梯度必须逐一相等

怎么读这张图: 上下是依赖方向(箭头指向被依赖方);engine.py 是全部地基,不依赖任何东西;测试文件反过来依赖 PyTorch——但只用于验证,不参与运行。

2.2 部件职责

部件干什么在哪个文件
Value标量节点:存 datagrad,知道自己由谁算出(_prev)、由哪个算子算出(_op)micrograd/engine.py:2
各算子的 _backward 闭包前向创建节点时顺手定义:该算子的局部链式法则(梯度怎么分给输入)micrograd/engine.py:17-20micrograd/engine.py:28-31
Value.backward对整张子图做拓扑排序,置输出梯度为 1,倒序逐个调 _backwardmicrograd/engine.py:54-70
Module / Neuron / Layer / MLPValue 搭网络,核心贡献只有一件:parameters() 收集全部参数micrograd/nn.py:4micrograd/nn.py:13micrograd/nn.py:30micrograd/nn.py:45
loss + SGD 循环demo:max-margin 损失 + L2 正则 + 手动梯度下降demo.ipynb:116demo.ipynb:258-273
test_sanity_check / test_more_ops拿 PyTorch 当裁判,验证前向值与梯度完全一致test/test_engine.py:4test/test_engine.py:28

2.3 主线走一遍(一次训练迭代)

demo 里的一步训练,完整经过整条链:

① 前向:inputs → MLP 逐层 __call__ → 每个 w*x 都是 Value 运算
→ 计算图随表达式求值动态长成(动态图,define-by-run)


② 损失:(1 - y*score).relu() 平均 + α·Σp² —— 损失本身也是 Value


③ backward():拓扑排序 → total_loss.grad=1 → 倒序调每个节点的 _backward
→ 梯度沿图流回每个参数 p,p.grad 累积完毕


④ SGD:p.data -= lr * p.grad(徒手循环,没有 optimizer 类)


⑤ 下一步迭代先 zero_grad() 清空 p.grad,再回到 ①

这条线最值得记住的一点:「建图」和「算值」是同时发生的——你写下 a * b 的那一刻,结果和「将来怎么反传」就一起确定了。这就是动态图(define-by-run)的本质,也是 PyTorch 同款设计。


3. 阅读地图(建议顺序)

micrograd 很小,三章读完连同源码不超过两小时。01 是心脏,必读;02、03 半小时可过。

顺序章节讲什么适合谁
101-engine-autograd.mdValue_backward 闭包、拓扑排序:反向传播的完整机械原理所有人必读
202-nn-library.md四层抽象怎么把一个神经元变成 MLP;parameters() 为何是全部要害想知道「神经网络库最少需要什么」的人
303-demo-training-loop.md训练循环四件套 + max-margin 损失 + 与 PyTorch 对拍的测试想把引擎用起来、并验证它没算错的人

4. 巧妙之处(可借鉴的技术)

每条先白话点出妙在哪,再给锚点;细节论证见各章节。

  1. 把链式法则折叠进闭包。 不需要「反向图」「梯度函数注册表」这类基础设施:每个算子在前向时顺手定义一个捕获了局部信息的 _backward 闭包,反向就是调闭包(micrograd/engine.py:17-20)。整个反向传播机制 = 闭包 + 拓扑排序,再无其他。
  2. 梯度累加而非赋值。 所有 _backward 写的都是 +=(micrograd/engine.py:18-19)。这一处细节让「一个变量被使用多次」的情形自动正确(梯度的多元加法法则),也是 PyTorch 同款语义;代价是必须手动 zero_grad()(micrograd/nn.py:6-8)。
  3. 运算全靠归约。 只实现 +***relu 四个算子,减、负、除全部由它们组合出来(micrograd/engine.py:72-91)。需要写 _backward 的代码量被压到最小。
  4. 用对手做测试。 测试不手写期望梯度值,而是把同一段表达式在 PyTorch 里再跑一遍,断言两边前向值与梯度逐一相等(test/test_engine.py:24-26)——对拍是验证自动微分实现最省心的方法。
  5. 动态图即调试体验。 图就是一堆普通 Python 对象,_prev 是 set、_op 是字符串(micrograd/engine.py:10-11),notebook 里 20 行就能把整张子图画出来(trace_graph.ipynbtrace / draw_dot)。

5. 边界与局限

micrograd 是刻意做小的教学件,以下每一条都是设计取舍,不是缺陷:

边界具体含义依据
只支持标量没有张量、没有广播、没有批维度;一个神经元要拆成逐元素的乘加README.md:6
性能不可用纯 Python、每算子一个闭包、无向量化;demo 的 337 参数 MLP 已是舒适区上限(inferred,纯 Python 实现的必然结果)
无优化器 / 无 autograd 之外的设施SGD 是手写循环,没有 Adam、没有学习率调度器、没有 DataLoaderdemo.ipynb:269-271
算子覆盖面极小** 只支持常数指数(assert),没有 exp / log / tanh / matmulmicrograd/engine.py:36
无设备概念没有 GPU、没有 dtype 管理;数字就是 Python floatmicrograd/engine.py:6
项目事实冻结代码主体停更于 2020-04;最近 commit(2026-08)只是 README 加了 microgpt 链接git log

想继续往前走的人,作者在 README 里指了下一步:microgpt(gist 链接,README.md:47)——同一作者、同一份引擎思路的张量化/实用化重写,能在纯 Python 里训一个 GPT。


6. 横向对比

同书架上与 micrograd 近亲关系最自然的对比对象:

项目与 micrograd 的关系取舍差异
PyTorch(非书架项目,作参照)micrograd 是它的「标量教学剪影」,API 刻意模仿(backward() / zero_grad() / parameters() / Module)PyTorch 用张量 + 静态算子库 + C++ 后端换性能;micrograd 用标量 + 闭包换可读性
verl同书架的大尺度对照:把「训练系统」做到工业级(分布式 RL,几十万行)verl 解决「成百上千张卡怎么协同」;micrograd 回答「一张卡上一次 backward 到底是什么」。一个读它的引擎要一周,micrograd 要一下午
dspy同书架、同样带教学性质,但站在栈的另一端:把「调 LLM」抽象成可编程模块DSPy 优化的「参数」是 prompt,不走梯度;micrograd 是最原味的梯度下降

结论:micrograd 在书架上的定位是「深度学习地基的最小可读样本」——理解它之后,再去看任何训练框架的 autograd 章节,都是在看同一件事的工程化版本。


7. 代码地图(导航索引)

读源码顺序:先 engine.py(从上到下,94 行),再 nn.py(60 行),最后跑通 demo.ipynb

主题文件路径符号名
标量节点(全部地基)micrograd/engine.pyValue
加法/乘法及其反向闭包micrograd/engine.pyValue.__add__Value.__mul__
幂与 ReLUmicrograd/engine.pyValue.__pow__Value.relu
反向传播驱动器(拓扑排序)micrograd/engine.pyValue.backward
由基本算子归约出的便利运算micrograd/engine.pyValue.__neg__Value.__sub__Value.__truediv__
神经网络四层抽象micrograd/nn.pyModuleNeuronLayerMLP
参数收集与梯度清零micrograd/nn.pyModule.parametersModule.zero_grad
训练闭环(前向/损失/反传/更新)demo.ipynbloss 函数、optimization 单元
与 PyTorch 对拍的正确性测试test/test_engine.pytest_sanity_checktest_more_ops
计算图可视化trace_graph.ipynbtracedraw_dot