手把手推导Back Propagation

f04636ce638cef876577ebff05b43b1f.png

撰文|月踏

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

1

链式法则

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

1. 一元方程

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

4d23b1459a87cad9eda2d7878016e07a.png

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

bc6dee9076cf842b5eea1d6a654234a2.png

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

c58e773c60296a819be0b3a625fade5b.png

2. 多元方程

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

98bc2ef8736ee93822d056e314fba73b.png

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

2a9cbf9def85cda72b597a8d01f53d96.png

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

2

只有一个weight的简单情况

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

f713cd2d08001d31896a413c585460f5.png4a876dfcb0155ffa81e45d5bd4faca1d.png

图1

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

b89332d88ad187846d5d16c96188e1f3.png

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

58f9e03bb5783537a97d9adea380250b.png

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

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

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

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

a1dd0486dcee52b244d0a7f5e4978ba8.png

再继续看w1的梯度计算: 

2710a64b6a9abac86b068582d2d4c58c.png

最后看b1的梯度计算: 

6d18a7591b4be5b9af26789f91d09089.png

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

import oneflow as of
import oneflow.nn as nn
import 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 = None
b1.grad = None
w2.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的各种属性如下:

9f1c9c8307320f585fb4c96ae51eb96f.png

如下图所示:

e0008abb16538b7d8508b74d7de070ce.png

图2

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

03eaae48fd0ff722b0335a51270efda2.png

图3

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

3c9dd4fa8ecfc54b1561352998171d3b.png

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

36281c220e39f810dd155dc1c9c313af.png

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

68c96d62c12ce9982bd873c6f6b7816a.png

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

c756385df8c3713429d0c62a3c426196.png

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

 
 
import oneflow as of
import oneflow.nn as nn
import 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:GitHub - Oneflow-Inc/oneflow: OneFlow is a performance-centered and open-source deep learning framework.icon-default.png?t=M4ADhttps://github.com/Oneflow-Inc/oneflow

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值