前言

神经网络之所以能够“学习”,核心在于梯度反向传播。而现代深度学习框架(如 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(...)
forwardbackward 留给子类实现,签名按具体语义灵活定义。


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} xL=yL=zL,加法算子直接将上游梯度原样分发给两个输入。

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 xL=δzy ∂ L ∂ y = δ z ⋅ x \frac{\partial L}{\partial y} = \delta_z \cdot x yL=δzx

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=δzex

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 里写好前向逻辑,反向梯度由框架自动求导。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐