《GoMLX从入门到精通》第7章:自动微分——梯度是怎样"算"出来的
第7章:自动微分——梯度是怎样"算"出来的
---
7.1 为什么你必须理解自动微分
第 1 章说过:训练 = 在参数空间里搜索使误差最小的那组参数。第 1 章的类比是"你在山里找最低点,感受坡度往下走"。
这个"坡度",在数学里就是梯度。如果你不知道梯度怎么算出来的、算得对不对——那你调的所有学习率、改的所有损失函数、写的所有自定义层,都建立在盲信之上。
自动微分得信任。而信任一件事的最好方式,是亲手验证它。
这一章,就来做这件事。
---
7.2 三种求梯度的方法
求 f(x, y) = x² + xy 在 (x=2, y=3) 处的梯度。正确答案是 ∂f/∂x = 2x + y = 7,∂f/∂y = x = 2。
方法一:手动推导(精确但不通用)
∂f/∂x = 2x + y → 在 (2,3) 处 = 7
∂f/∂y = x → 在 (2,3) 处 = 2
优点:精确。缺点:每换一个函数,你得重新求一遍导。深度学习模型有几百层嵌套运算——手算不现实。
方法二:数值微分(通用但有误差)
func numericalGradient(f func(x, y float64) float64, x, y, h float64) (float64, float64) {
dfdx := (f(x+h, y) - f(x-h, y)) / (2 * h)
dfdy := (f(x, y+h) - f(x, y-h)) / (2 * h)
return dfdx, dfdy
}
f := func(x, y float64) float64 { return x*x + x*y }
gx, gy := numericalGradient(f, 2.0, 3.0, 1e-5)
// gx ≈ 7.0000000001, gy ≈ 2.0000000001
优点:任何函数都能用。缺点:精度依赖 h 的选择——h 太小有浮点舍入误差,h 太大有截断误差。200 万个参数?400 万次函数调用。一次。
方法三:自动微分(既精确又通用)
这就是 GoMLX 的 Gradient()。
import "github.com/gomlx/gomlx/pkg/core/graph"
func main() {
backend, _ := backends.New()
modelFn := func(x, y *graph.Node) (output *graph.Node) {
xSquared := graph.Square(x)
xy := graph.Mul(x, y)
return graph.Add(xSquared, xy)
}
gradFn := func(x, y *graph.Node) (output, gradX, gradY *graph.Node) {
output = modelFn(x, y)
reduced := graph.ReduceAllSum(output)
grads := graph.Gradient(reduced, x, y)
gradX, gradY = grads[0], grads[1]
return
}
exec := graph.MustNewExec(backend, gradFn)
results := exec.MustExec(2.0, 3.0)
fmt.Printf("f(2,3) = %v\n", results[0].Value()) // 2² + 2×3 = 10
fmt.Printf("∂f/∂x = %v\n", results[1].Value()) // 7.0
fmt.Printf("∂f/∂y = %v\n", results[2].Value()) // 2.0
}
输出:
f(2,3) = 10
∂f/∂x = 7
∂f/∂y = 2
和手工推导一模一样。不是近似——是精确的。
---
7.3 自动微分在背后做了什么
GoMLX 的 Gradient(output, x, y) 基于反向模式自动微分——也就是"反向传播"。
分两步走。
第一步:前向遍历(记录运算)。在你调用 MustNewExec 时,GoMLX 已经构建了一张 DAG:
[x] ──→ [Square] ──→ [x²]
\
→ [Add] → output
/
[y] ──→ [Mul] ──→ [xy]
↑ ↑
└── x ─────────┘
每个节点都记录了它的运算类型和输入节点。
第二步:反向遍历(应用链式法则)。Gradient() 从 output 节点开始,反向遍历整张图,为每个节点计算 VJP(Vector-Jacobian Product)。用 Go 直觉来理解:
遍历到 Add 节点:
grad[x²] = grad[output] × 1 = 1.0
grad[xy] = grad[output] × 1 = 1.0
遍历到 Square 节点:
∂(x²)/∂x = 2x = 4(在 x=2 处)
grad[x] = grad[x²] × 4 = 1.0 × 4 = 4.0
遍历到 Mul 节点:
∂(xy)/∂x = y = 3,∂(xy)/∂y = x = 2
grad[x] += grad[xy] × 3 = 1.0 × 3 = 3.0 → grad[x] = 7.0
grad[y] = grad[xy] × 2 = 2.0
最终:grad[x] = 7.0, grad[y] = 2.0
这就是反向传播的完整计算过程。每一步都是精确的算术——不涉及 h 的选择、没有截断误差、不需要手动求导公式。它是对链式法则的机械化应用。
---
7.4 为什么 output 必须是标量
你可能注意到:Gradient(output, ...) 要求 output 是标量。
原因:梯度是一个"output 标量对每个输入节点的偏导数"。如果 output 不是标量,那"output 对 x 的导数"就不是一个标量,而是一个矩阵(雅可比矩阵)。GoMLX 目前不支持雅可比矩阵。
在实践中,损失函数总是返回一个标量(平均误差或总和)。你总是先把输出 ReduceAllSum 或 ReduceAllMean 成标量,然后传给 Gradient。
---
7.5 自动微分的关键属性
精确。 自动微分算出来的梯度,和手工推导的解析解在小数点后十几位都是一致的。
通用。 Gradient 不需要知道你写了什么函数。无论你的图里是简单的 Square 还是复杂的 Conv + BatchNorm + Dropout + Attention,反向传播逻辑自动沿图走。
计算开销可控。 反向传播的计算量大约是前向传播的 1-3 倍。这意味着训练一步(前向 + 反向)的成本大约是推理一步的 2-4 倍——可以接受。
---
7.6 当前限制
GoMLX v0.27.3 的 Gradient 只支持"标量输出 → 多输入梯度"。不支持雅可比矩阵和黑塞矩阵。
这两个限制对 99% 的训练场景不构成障碍——因为损失函数总是标量。
---
7.7 收束
你在这一章学到最重要的东西,不是一个 API 怎么调用,而是一个认识:
> 自动微分不是数值近似的魔法。它就是链式法则在计算图上的机械化演绎。每一步都是精确的算术。你可以信任它。
第 8 章,把这把刀用在真实的模型上——从零手写一个线性回归。
---
> 作者:步子哥,资深AI研究员 | 连载中:《GoMLX从入门到精通》 > 上一章:第6章 张量 > 下一章预告:第8章 你的第一个模型——线性回归从头写
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens