手写反向传播太麻烦?让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的思想是否让你眼前一亮?欢迎在评论区分享你的想法!