当前位置:首页>python>手写反向传播太麻烦?让Python自动帮你搞定!

手写反向传播太麻烦?让Python自动帮你搞定!

  • 2026-09-21 11:53:48
手写反向传播太麻烦?让Python自动帮你搞定!

手写反向传播太麻烦?让Python自动帮你搞定!

今天我们来聊一个非常核心的话题——反向传播的自动化。如果你曾经手写过神经网络的反向传播,一定深有体会:每次换一个计算图,就要重新推导、重新编码,不仅容易出错,而且非常耗时。那么,有没有办法让Python自动帮我们完成这些无聊的工作呢?答案是肯定的!这正是深度学习框架(如PyTorch、TensorFlow)的核心机制——Define-by-Run。今天,我们就通过一个简单的框架DeZero,来揭开自动反向传播的神秘面纱。


1. 手动反向传播的痛点

先来看一个场景:假设我们有多个不同的计算图(如图7-1所示),每个图都需要单独编写反向传播代码。这就像每次换一道数学题,都要重新手算导数,不仅麻烦,而且容易出错。

图7-1 各种计算图的例子

我们希望建立一种机制:无论正向传播是什么样的计算,反向传播都能自动进行。这就是Define-by-Run的核心思想:在正向传播的同时,动态构建计算图,然后自动反向传播。


2. 建立变量与函数之间的“连接”

要实现自动反向传播,首先得让计算图“记住”计算过程。也就是说,我们需要在正向传播时,记录下每个变量是由哪个函数生成的,以及每个函数的输入是什么。

从函数的角度看变量

函数有输入变量和输出变量(图7-2左图)。

从变量的角度看函数

变量是由函数“创造”的,这个函数就是变量的creator(创造者)。如果变量没有creator,说明它是用户直接输入的(比如数据或参数)。

图7-2 函数与变量的双向关系

在代码中,我们为Variable类添加一个creator属性,以及设置它的方法set_creator:

classVariable:def__init__(self, data):        self.data = data        self.grad = None        self.creator = Nonedefset_creator(self, func):        self.creator = func

然后在Function类中,当计算输出output时,让output记住自己这个创造者:

classFunction:def__call__(self, input):        x = input.data        y = self.forward(x)        output = Variable(y)        output.set_creator(self)   # 让输出变量知道是谁创造了它        self.input = input          # 函数记住输入,方便反向传播        self.output = output        # 也记住输出,便于后续遍历return output

这样,在正向传播的过程中,我们就动态地建立了一条由变量和函数交替连接而成的链条。


3. 反向遍历计算图

有了这些连接,我们就可以从输出变量出发,反向遍历整个计算图。比如下面这个例子:

A = Square()B = Exp()C = Square()x = Variable(np.array(0.5))a = A(x)b = B(a)y = C(b)

通过creator和input,我们可以检查连接是否正确:

assert y.creator == Cassert y.creator.input == bassert y.creator.input.creator == Bassert y.creator.input.creator.input == aassert y.creator.input.creator.input.creator == Aassert y.creator.input.creator.input.creator.input == x

这些断言全部通过,说明我们成功构建了一个可回溯的计算图(如图7-3所示)。

图7-3 从y出发反向遍历计算图

这种在数据流动的同时动态建立连接的方式,就是Define-by-Run的精髓。


4. 手动反向传播演示

有了连接,我们可以手动一步步进行反向传播。例如,从y到b:

y.grad = np.array(1.0)C = y.creator          # 1. 获取函数b = C.input            # 2. 获取函数的输入b.grad = C.backward(y.grad)   # 3. 调用backward

再从a到x:

A = a.creator          # 1. 获取函数x = A.input            # 2. 获取函数的输入x.grad = A.backward(a.grad)   # 3. 调用backwardprint(x.grad)          # 输出最终梯度

这些步骤模式完全一样,完全可以自动化。


5. 让Variable自己会反向传播

我们在Variable类中添加一个backward方法,让它能够递归地调用前一个变量的backward,直到到达输入变量。

classVariable:# ... 前面代码 ...defbackward(self):        f = self.creator          # 1. 获取创造自己的函数if f isnotNone:            x = f.input            # 2. 获取函数的输入变量            x.grad = f.backward(self.grad)   # 3. 计算梯度传给输入            x.backward()           # 4. 递归调用输入变量的backward

注意:这里假设所有函数都实现了backward方法(比如Square和Exp类)。

现在,只需要设置输出的梯度为1,然后调用y.backward(),就能自动完成所有反向传播:

y.grad = np.array(1.0)y.backward()print(x.grad)   # 输出梯度,与手动计算结果一致

6. 总结与展望

至此,我们已经实现了DeZero中最核心的自动微分机制。通过动态建立变量与函数之间的连接,我们成功让反向传播自动进行。这为后续处理更复杂的计算图(如分支、循环、共享变量等)打下了坚实的基础。

在下一篇文章中,我们将继续完善这个框架,让它能够处理更复杂的计算图,并实现真正的自动梯度计算。敬请期待!


思考与互动:你是否曾在手写反向传播时被复杂的链式法则搞晕?Define-by-Run的思想是否让你眼前一亮?欢迎在评论区分享你的想法!

最新文章

随机文章