乐趣区

关于深度学习:手把手推导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 的简略状况

做了一个简略的网络,这能够对应到链式法则的第一种状况,如下图所示:


图 1

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

这个例子中,咱们是想更新 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 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 的各种属性如下:

如下图所示:


图 2

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


图 3

在这个简略的网络中,z 节点示意一个 avg-pooling 的操作,kernel 是 2 ×2,loss 采纳均方误差,上面是对应的公式:

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

上面是求 w1、w2、w3 梯度的过程相似,间接写出后果:

最初再依照上面公式来更新参数即可:

用 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/~…
2.https://speech.ee.ntu.edu.tw/…
3.https://www.youtube.com/c/Hun…

欢送下载体验 OneFlow v0.7.0 最新版本:

https://github.com/Oneflow-In…

退出移动版