【深度学习】蒲公英书笔记 | 算子、自动微分机制
《蒲公英书》第2章 算子和自动微分机制学习笔记
前言
神经网络之所以能够“学习”,核心在于梯度反向传播。而现代深度学习框架(如 PyTorch)能让开发者几乎不用手写梯度代码,靠的就是算子与自动微分这两大机制。
1. 算子是什么
在深度学习框架中,算子(Operator, Op)是搭建模型的最小复用单元。每个算子必须绑定一对函数:
- forward:接收输入,计算输出(前向);
- backward:接收上游传来的梯度,计算该算子对输入(以及可能包含的参数)的梯度(反向)。
这种“一个积木,正反两套计算”的设计,使得复杂模型可以通过组合基础算子来搭建,而梯度也能由链式法则自动传播。
1.1 算子基类 Op
为了让所有算子拥有统一接口,可以抽象出如下基类:
class Op:
"""算子基类:子类按需实现 forward / backward."""
def __call__(self, *args, **kwargs):
return self.forward(*args, **kwargs)
def forward(self, *args, **kwargs):
raise NotImplementedError
def backward(self, *args, **kwargs):
raise NotImplementedError
算子基类的调用与实现约定
__call__ 使得实例可以像函数一样调用 model(...);forward 和 backward 留给子类实现,签名按具体语义灵活定义。
1.2 加法算子
前向 z = x + y z = x + y z=x+y,反向时因 ∂ L ∂ x = ∂ L ∂ y = ∂ L ∂ z \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} = \frac{\partial L}{\partial z} ∂x∂L=∂y∂L=∂z∂L,加法算子直接将上游梯度原样分发给两个输入。
class add(Op):
def forward(self, x, y):
self.x = x
self.y = y
return x + y
def backward(self, grads):
return grads, grads
x, y = 1, 4
add_op = add()
z = add_op(x, y)
grads_x, grads_y = add_op.backward(grads=1)
print(f"x's grad: {grads_x}, y's grad: {grads_y}") # 均为 1
1.3 乘法算子
前向 z = x × y z = x \times y z=x×y,反向时 ∂ L ∂ x = δ z ⋅ y \frac{\partial L}{\partial x} = \delta_z \cdot y ∂x∂L=δz⋅y, ∂ L ∂ y = δ z ⋅ x \frac{\partial L}{\partial y} = \delta_z \cdot x ∂y∂L=δz⋅x。
class multiply(Op):
def forward(self, x, y):
self.x = x
self.y = y
return x * y
def backward(self, grads):
return grads * self.y, grads * self.x
1.4 指数算子
前向 z = e x z = e^{x} z=ex,反向 d z d x = e x \frac{dz}{dx} = e^{x} dxdz=ex,故 δ x = δ z ⋅ e x \delta_x = \delta_z \cdot e^{x} δx=δz⋅ex。
import math
class exponential(Op):
def forward(self, x):
self.x = x
return math.exp(x)
def backward(self, grads):
return grads * math.exp(self.x)
1.5 组合算子完成 g = e a × b + c × d g = e^{a \times b + c \times d} g=ea×b+c×d
我们可以像搭积木一样,用上述算子计算 g g g 的值:
a, b, c, d = 2, 3, 2, 2
multiply_op = multiply()
add_op = add()
exp_op = exponential()
g = exp_op(add_op(multiply_op(a, b), multiply_op(c, d)))
print('g:', g) # 22026.465794806718
2. PyTorch 的自动微分机制
主流框架几乎都内置了自动微分(Automatic Differentiation, AD)。其核心思想是:将计算拆分为原子操作,构建计算图,每个节点保存前向结果与局部导数,反向时沿图按链式法则求梯度。
PyTorch 采用的是动态计算图:在前向执行时边算边记录,调用 .backward() 时再回溯计算梯度。
2.1 前向执行:悄无声息建图
以一个简单的乘法为例:
a = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(5.0, requires_grad=False)
c = a * b
在 c.backward() 调用之前,PyTorch 已经为 a 创建了一个反向张量 a_grad,并为乘法算子记录了反向操作 MulBackward。
大致结构如下:
a:requires_grad=True,有对应的 a_grad(此时 data 为 None);
b:requires_grad=False,不参与梯度追踪;
c = a * b:生成 c_grad,其 grad_fn 指向 MulBackward,该反向算子记录了“如何从 c_grad 计算出 a_grad”。
此时反向尚未执行,所有梯度存储空间仍为空。
2.2 反向执行:按图索骥传梯度
调用 c.backward() 后,框架从 c_grad 出发,依次执行反向算子,利用链式法则将梯度回传到叶子张量:
c.backward()
print(a.grad) # tensor(5.0)
print(b.grad) # None (未要求梯度)
print(c.grad) # None (非叶子张量默认不保留梯度)
2.3 梯度累加与清零
PyTorch 每次 backward() 都会累加梯度到 .grad 中,而不是覆盖。这在需要梯度累积的场景很有用,但在标准训练循环中必须手动清零:
x = torch.tensor([1., 2., 3.], requires_grad=True)
y = (x ** 2).sum()
y.backward()
print(x.grad) # [2., 4., 6.]
y2 = (x ** 2).sum()
y2.backward()
print(x.grad) # [4., 8., 12.] ← 累加了!
x.grad.zero_() # 清零
(x ** 2).sum().backward()
print(x.grad) # [2., 4., 6.]
2.4 推理时关闭梯度
推理阶段不需要构建计算图和存储梯度,可用 torch.no_grad() 节省内存和计算:
with torch.no_grad():
pred = x * 2 # pred.requires_grad == False
若想切断某个张量的梯度追踪但共享存储,可用 .detach():
z = (x * 3).detach() # z.requires_grad == False
3. 预定义的算子与 nn.Module
PyTorch 内置了大量常用算子,既有 +、-、*、/、exp() 等基础运算,也有线性层、卷积、激活函数等高级模块。它们统一继承自 torch.nn.Module —— 整个网络本身也可看作是一个大的 Module。
开发者只需在 forward 里写好前向逻辑,反向梯度由框架自动求导。
更多推荐




所有评论(0)