写点什么

手把手推导 Back Propagation

作者:OneFlow
  • 2022 年 5 月 13 日
  • 本文字数:3456 字

    阅读完需:约 11 分钟

手把手推导Back Propagation

撰文|月踏


BP(Back Propagation)是深度学习神经网络的理论核心,本文通过两个例子展示手动推导 BP 的过程。


1、链式法则


链式法则是 BP 的核心,分两种情况:


(1)一元方程


在一元方程的情况下,链式法则比较简单,假设存在下面两个函数:



那么 x 的变化最终会影响到 z 的值,用数学符号表示如下:



z 对 x 的微分可以表示如下:



(2)多元方程


在多元方程的情况下,链式法则稍微复杂一些,假设存在下面三个函数:



因为 s 的微小变化会通过 g(s)和 h(s)两条路径来影响 z 的结果,这时 z 对 s 的微分可以表示如下:



这就是链式法则的全部内容,后面用实际例子来推导 BP 的具体过程。


2、只有一个 weight 的简单情况


做了一个简单的网络,这可以对应到链式法则的第一种情况,如下图所示:



其中圆形表示叶子节点,方块表示非叶子节点,每个非叶子节点的定义如下,训练过程中的前向过程会根据这些公式进行计算:



这个例子中,我们是想更新 w1、b1、w2 三个参数值,假如用 lr 表示 learning rate,那么它们的更新公式如下:



在训练开始之前,b1、w1、w2 都会被初始化成某个值,在训练开始之后,参数根据下面两个步骤来进行更新:


  1. 先进行一次前向计算,这样可以得到 y1、y2、y3、loss 的值

  2. 再进行一次反向计算,得到每个参数的梯度值,进而根据上面的公式(13)、(14)、(15)来更新参数值


下面看下反向传播时的梯度的计算过程,因为梯度值是从后往前计算的,所以先看 w2 的梯度计算:



再继续看 w1 的梯度计算:



最后看 b1 的梯度计算:



把 w2、w1、b1 的梯度计算出来之后,就可以按照公式(13)、(14)、(15)来更新参数值了,下面用 OneFlow 按照图 1 搭建一个对应的网络做实验,代码如下:


import oneflow as ofimport oneflow.nn as nnimport oneflow.optim as optim
class Sample(nn.Module): def __init__(self): super(Sample, self).__init__() self.w1 = of.tensor(10.0, dtype=of.float, requires_grad=True) self.b1 = of.tensor(1.0, dtype=of.float, requires_grad=True) self.w2 = of.tensor(20.0, dtype=of.float, requires_grad=True) self.loss = nn.MSELoss()
def parameters(self): return [self.w1, self.b1, self.w2]
def forward(self, x, label): y1 = self.w1 * x + self.b1 y2 = y1 * self.w2 y3 = 2 * y2 return self.loss(y3, label)
model = Sample()
optimizer = optim.SGD(model.parameters(), lr=0.005)data = of.tensor(1.0, dtype=of.float)label = of.tensor(500.0, dtype=of.float)
loss = model(data, label)print("------------before backward()---------------")print("w1 =", model.w1)print("b1 =", model.b1)print("w2 =", model.w2)print("w1.grad =", model.w1.grad)print("b1.grad =", model.b1.grad)print("w2.grad =", model.w2.grad)loss.backward()print("------------after backward()---------------")print("w1 =", model.w1)print("b1 =", model.b1)print("w2 =", model.w2)print("w1.grad =", model.w1.grad)print("b1.grad =", model.b1.grad)print("w2.grad =", model.w2.grad)optimizer.step()print("------------after step()---------------")print("w1 =", model.w1)print("b1 =", model.b1)print("w2 =", model.w2)print("w1.grad =", model.w1.grad)print("b1.grad =", model.b1.grad)print("w2.grad =", model.w2.grad)optimizer.zero_grad()print("------------after zero_grad()---------------")print("w1 =", model.w1)print("b1 =", model.b1)print("w2 =", model.w2)print("w1.grad =", model.w1.grad)print("b1.grad =", model.b1.grad)print("w2.grad =", model.w2.grad)
复制代码


这段代码只跑了一次 forward 和一次 backward,然后调用 step 更新了参数信息,最后调用 zero_grad 来对这一轮 backward 算出来的梯度信息进行了清零,运行结果如下:


------------before backward()---------------w1 = tensor(10., requires_grad=True)b1 = tensor(1., requires_grad=True)w2 = tensor(20., requires_grad=True)w1.grad = Noneb1.grad = Nonew2.grad = None------------after backward()---------------w1 = tensor(10., requires_grad=True)b1 = tensor(1., requires_grad=True)w2 = tensor(20., requires_grad=True)w1.grad = tensor(-4800.)b1.grad = tensor(-4800.)w2.grad = tensor(-2640.)------------after step()---------------w1 = tensor(34., requires_grad=True)b1 = tensor(25., requires_grad=True)w2 = tensor(33.2000, requires_grad=True)w1.grad = tensor(-4800.)b1.grad = tensor(-4800.)w2.grad = tensor(-2640.)------------after zero_grad()---------------w1 = tensor(34., requires_grad=True)b1 = tensor(25., requires_grad=True)w2 = tensor(33.2000, requires_grad=True)w1.grad = tensor(0.)b1.grad = tensor(0.)w2.grad = tensor(0.)
复制代码


3、以 conv 为例的含有多个 weights 的情况


用一个非常简单的 conv 来举例,这个 conv 的各种属性如下:



如下图所示:



假定这个例子中的网络结构如下图:


在这个简单的网络中,z 节点表示一个 avg-pooling 的操作,kernel 是 2x2,loss 采用均方误差,下面是对应的公式:



前传部分同上一节一样,直接看反传过程,目的是为了求 w0、w1、w2、w3 的梯度,并更新这四个参数值,以下是求 w0 梯度的过程:



下面是求 w1、w2、w3 梯度的过程类似,直接写出结果:



最后再按照下面公式来更新参数即可:



用 OneFlow 按照图 3 来搭建一个对应的网络做实验,代码如下:


import oneflow as ofimport oneflow.nn as nnimport oneflow.optim as optim
class Sample(nn.Module): def __init__(self): super(Sample, self).__init__() self.op1 = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(2,2), bias=False) self.op2 = nn.AvgPool2d(kernel_size=(2,2)) self.loss = nn.MSELoss()
def forward(self, x, label): y1 = self.op1(x) y2 = self.op2(y1) return self.loss(y2, label)
model = Sample()
optimizer = optim.SGD(model.parameters(), lr=0.005)data = of.randn(1, 1, 3, 3)label = of.randn(1, 1, 1, 1)
loss = model(data, label)print("------------before backward()---------------")param = model.parameters()print("w =", next(param))loss.backward()print("------------after backward()---------------")param = model.parameters()print("w =", next(param))optimizer.step()print("------------after step()---------------")param = model.parameters()print("w =", next(param))optimizer.zero_grad()print("------------after zero_grad()---------------")param = model.parameters()print("w =", next(param))
复制代码


输出如下(里面的 input、param、label 的值都是随机的,每次运行的结果会不一样):


------------before backward()---------------w = tensor([[[[ 0.2621, -0.2583],          [-0.1751, -0.0839]]]], dtype=oneflow.float32, grad_fn=<accumulate_grad>)------------after backward()---------------w = tensor([[[[ 0.2621, -0.2583],          [-0.1751, -0.0839]]]], dtype=oneflow.float32, grad_fn=<accumulate_grad>)------------after step()---------------w = tensor([[[[ 0.2587, -0.2642],          [-0.1831, -0.0884]]]], dtype=oneflow.float32, grad_fn=<accumulate_grad>)------------after zero_grad()---------------w = tensor([[[[ 0.2587, -0.2642],          [-0.1831, -0.0884]]]], dtype=oneflow.float32, grad_fn=<accumulate_grad>)
复制代码


参考资料:

1.http://speech.ee.ntu.edu.tw/~tlkagk/courses.html

2.https://speech.ee.ntu.edu.tw/~hylee/index.php

3.https://www.youtube.com/c/HungyiLeeNTU


其他人都在看


欢迎下载体验 OneFlow v0.7.0 最新版本:https://github.com/Oneflow-Inc/oneflow/

用户头像

OneFlow

关注

不至于成为世界上最快的深度学习框架。 2022.03.23 加入

★ OneFlow深度学习框架:github.com/Oneflow-Inc/oneflow ★ OF云平台:oneflow.cloud

评论

发布
暂无评论
手把手推导Back Propagation_人工智能_OneFlow_InfoQ写作社区