读懂反向传播算法(bp算法)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读懂反向传播算法(bp算法)相关的知识,希望对你有一定的参考价值。

参考技术A 反向传播算法可以说是神经网络最基础也是最重要的知识点。基本上所以的优化算法都是在反向传播算出梯度之后进行改进的。同时,也因为反向传播算法是一个递归的形式,一层一层的向后传播误差即可,很容易实现(这部分听不懂没关系,下面介绍)。不要被反向传播吓到,掌握其核心思想就很容易自己手推出来。

我们知道神经网络都是有一个loss函数的。这个函数根据不同的任务有不同的定义方式,但是这个loss函数的目的就是计算出当前神经网络建模出来输出的数据和理想数据之间的距离。计算出loss之后,根据反向传播算法就可以更新网络中的各种参数以此使loss不断下降,即可使输出的数据更加理想。
所以,现在的任务是,已知一个网络的loss之后,如何根据loss来更新参数呢?具体点即如何更新网络节点中的权重w和偏差b的值呢?

这里我们采用的是全连接神经网络进行说明。
要想把这个过程说清楚,首先需要将神经网络中各个参数用文字表达清楚。定义的就是w和b在网络中的准确位置。

对于 表示的是神经网络中第 层第k个节点到神经网络中第 层第j个节点之间的权重。注意w的下标是首位表示的是节点后层节点的位置,末尾表示是前层节点的位置。理解这样的表达方式在后面的计算中会很好理解。
同理,对于b的表示:

b的表示相比于w要简单一些,符号 表示第l层网络在第j个节点的偏置。无论w还是b的表示,上标都是表示层数。并且 和 表示都是第l层网络第j个节点的参数。所以该节点的输出可以表示为:

神经网络输出之后会经过一个激活函数,这用激活函数用 表示,则经过激活函数输出为:

至此,根据上面符号 、 、 、 。我们可以对于神经网络里面每一个数据准确的表示了。

给定一个损失函数之后,用 表示,说白了反向传播就是求∂C/∂w和∂C/∂b,然后将这个值乘以和对应的w,b进行相减就可以实现一次的参数更新了。为什么这样的操作就可以优化网络,减小loss值呢?

来源于导数的概念和速度相关。∂C/∂w和∂C/∂b相当于loss值C相对于w和v变化的速度。如果∂C/∂w是正的,则增大w,C也会增大,如果希望C减小的话,应该减小w;并且∂C/∂w的绝对值越大,表示w对C的值影响越大,w稍微有一点变化,C就会有大幅变化。如果要优化C变小,w应该对应的减少多少呢?也没有一个确定的答案。这里通过变化的速度和学习率相乘作为一个减小的值。通过多轮迭代。最终是希望c达到最小点。而当函数落入最小值的时候,无论是局部最小还是全局最小,其周围一定是平滑的。所以此时∂C/∂w和∂C/∂b将会变得很小甚至为0,即参数不在更新了。当函数在局部最小点处参数不在更新出现梯度消失的问题时,目前也有各种trick进行解决。不是这里的重点。

为了好说明,这里定义一个很简单的损失函数C:

接下来就是有意思的阶段了。这里还是利用上一节中∂C/∂w和∂C/∂b的解释。如果我们想要求出∂C/∂w和∂C/∂b的值,即具体的 、 对C影响速率的值,我们找一个中间变量∂C/∂ 。因为我们知道:

我们定义:

当我们知道了 值之后,我们根据 式子可以很容易求出 。
利用导数的链式法则:

很容易推出来不是?同理可以求出:

可以看出通过媒介 很容易求出∂C/∂w和∂C/∂b。那么我们现在来理解一下 到底是什么意思,以及如何求出来每一个l层j节点的 值。

根据定义:

可以看出来 就是 对于C的影响大小(联系之前说的导数和速率的关系)。而 是第 层第 个神经元未进过激活函数之前的输出。所以我们可以理解 为网络中第 层第 个神经元对loss的影响。所以很直观的看法就是我们先求出单个神经元对loss值得影响,然后再计算该神经元内部参数对于loss的影响。

ok,如果我们已经理解了为什么要引入 变量以及如何利用该变量计算具体参数的梯度后,接下来我们就可以看看如何获得 值。反向传播的名字我想也就是通过计算 的方式而来的。是一层一层递归而来的。

既然说是递归的方式,我们来思考一下 和 之间有什么关系,如果找到这个关系之后,我们就可以默认我们如果知道最后一层网络节点的 值,我们就可以获得倒数第二层网络节点的 值,倒数第三层,倒数第四层,……以此推类即可获得整个网络的每个节点的 值。至此我们的反向传播也基本完成了。
所以最重要的有两点:

先看问题1,直接根据求导的链式法则就可以找出两个的关系,具体公式如下,可以多看看手写一下,思路上也很简单。

觉得这样的链式公式还是很直观的,如果不好理解,可以自己画一个神经网络图,连上节点与节点之间的线,标上参数,然后推一下应该就能理解了。
这里的 都表示的未经过激活函数的神经元的输出。 表示激活函数。因为:

所以:

带入上式就可以得出:

至此就找出了 和 之间的关系了。
(还能简化,根据最开始我们定义的 )。

理解起来就是网络中前面一层某一个神经元对于loss的影响与该层的后一层所有的神经元对loss的影响、该神经元的输出大小、该神经元与后一层神经元连接的权重有关系的,并且是一个累加的效应。这样的理解也是非常直观合乎常理的。

现在万事具备,只差问题2了。即假设最后一层网络是L,最后一层 如何计算得出。最后一层的 值就像一个导火索,一旦有了开始,就可以利用我们之前推出来的: 公式进行反向传播了(反向传播还是很形象的不是?)。现在解决这个问题。这个问题就是和损失函数具体怎么定义有关系了。不过我们先不考虑C的具体形式,根据通用的链式法则我们可以得到:

这里需要注意的是最后一层激活函数使用的是哪种。最后一层激活函数在计算某一个神经元的输出时可能会结合其他节点的输出来计算。比如softmax激活函数,其输出的是一个概率值【0,1】。输出大小就是结合输出所有的值。

现在我们来考虑两个具体的损失函数,并且采用之前定义的均方误差损失函数 :

求导为:
因为sigmoid输出的值仅仅和输入的x值有关 。所以 当 时值为0.所以:

根据上面,BP推导有三部曲,先求出 ,再根据 分别求出 、 。总结公式如下:

启动上面反传的导火索是最后一层的 值,计算公式为:

根据最后一层不同类型的激活函数不同对待。

手写BP(反向传播)算法

BP算法为深度学习中参数更新的重要角色,一般基于loss对参数的偏导进行更新。

一些根据均方误差,每层默认激活函数sigmoid(不同激活函数,则更新公式不一样)

假设网络如图所示:

技术图片

则更新公式为:

技术图片

以上列举了最后2层的参数更新方式,第一层的更新公式类似,即上一层的误差来自于下一层所有的神经元,e的更新就是不断建立在旧的e上(这里g可以当做初始的e)

下面上代码:

1,BP算法

# 手写BP算法
import numpy as np

# 先更新参数,再继续传播
# layers:包括从输入层到输出层,每层参数为:连接权重w,阈值b,输出y。类型为np.array
# 对于输入层,w和b随便是啥,反正不用,只需y即原始输入
# 基于激活函数sigmoid
# loss为均方误差
def bp(layers,labels,lr=0.001):
#     翻转layers,反向传播
    reversed_layers=layers[::-1]
#     输出层
    output_w,output_b,output_y=reversed_layers[0]
    g=np.array([output_y[j]*(1-output_y[j])*(labels[j]-output_y[j]) for j in range(len(labels))])
#     最后一层更新较为特殊,先进行更新
    delta_w=np.empty(shape=(output_w.shape[0],output_w.shape[1]))
#     上一层输出y
    last_y=reversed_layers[1][2]
    for h in range(output_w.shape[0]):
        for j in range(output_w.shape[1]):
            delta_w[h,j]=lr*g[j]*last_y[h]
    delta_b=-lr*g
    new_w=output_w+delta_w
    new_b=output_b+delta_b
    reversed_layers[0][0]=new_w
    reversed_layers[0][1]=new_b

    #     从倒数第二层到第二层进行更新,每次取3层进行计算,由公式知,需用到上一层输出即下一层权重
    for i in range(1,len(reversed_layers)-1):
#         下一层w
        next_w=reversed_layers[i-1][0]
        out_w,out_b,out_y=reversed_layers[i]
#         上一层y
        last_y=reversed_layers[i+1][2]
#         更新辅助量,意思即上一层每个神经元的误差都由下一层所有神经元的误差反向传播,体现在这里内循环
        e=np.empty(shape=(len(out_b),1))
        for h in range(len(out_b)):
            temp=0
            for j in range(next_w.shape[1]):
                temp+=next_w[h,j]*g[j]
            e[h]=out_y[h]*(1-out_y[h])*temp
        delta_w=np.empty(shape=(out_w.shape[0],out_w.shape[1]))
        for h in range(out_w.shape[0]):
            for j in range(out_w.shape[1]):
                delta_w[h,j]=lr*e[j]*last_y[h]
        delta_b=-lr*e
        out_new_w=out_w+delta_w
        out_new_b=out_b+delta_b
        reversed_layers[i][0]=out_new_w
        reversed_layers[i][1]=out_new_b
        g=np.copy(e)
    return layers

以上假设每个神经元的输出为一个实数y值

2,构建测试

构建平面上的点(x,y),将y是否大于0作为划分,进行训练。只使用了一层网络,sigmoid激活

X=[]
Y=[]
for i in range(-100,100):
    for j in range(-100,100):
        X.append([[i],[j]])
        if j>=0:
            Y.append([1])
        else:
            Y.append([0])
X=np.array(X)
Y=np.array(Y)

3,划分训练,验证集

indexs=np.random.choice(range(40000),size=30000)

x_train=np.array([X[i] for i in indexs])
y_train=np.array([Y[i] for i in indexs])

x_val=np.array([X[i] for i in np.setdiff1d(range(40000),indexs))
y_val=np.array([Y[i] for i in np.setdiff1d(range(40000),indexs))

4,训练。这里只对所有样本训练了一轮。使用随机初始化的w和b,每个样本都会改变w和b

# 使用sigmoid激活函数
def output(input_x,w,b):
    res=0
    t=np.matmul(np.transpose(w),input_x)-b
    return 1./(1+np.power(np.e,-t))

w1=np.random.normal(size=(2,1))
b1=np.array([[0]])
for i in range(len(x_train)):
    y0=x_train[i]
    l=y_train[i]
    input_layers=[]
    w0,b0=(0,0)
    input_layers.append([w0,b0,y0])
    input_layers.append([w1,b1,output(y0,w1,b1)])
    input_layers=bp(input_layers,l)
    w1=input_layers[1][0]
    b1=input_layers[1][1]
    
# w:  [[0.11213777]
#  [1.67425498]]
# b:  [[0.0001581]]
print(w: ,w1)
print(b: ,b1)

5,验证。从分出的验证集选取部分验证即可

for xx in x_val[:50]:
    print(xx.reshape((2,)),output(xx,w1,b1).reshape((1,)))

验证结果如下:

[63 68] [1.]
[-100  -99] [1.39636722e-77]
[-100  -98] [7.44936654e-77]
[63 69] [1.]
[-100  -96] [2.12011171e-75]
[-100  -94] [6.03390049e-74]
[-100  -93] [3.21897678e-73]
[63 74] [1.]
[-100  -91] [9.16130293e-72]
[63 75] [1.]
[63 76] [1.]
[63 77] [1.]
[63 78] [1.]
[-100  -86] [3.95872874e-68]
[-100  -85] [2.11191018e-67]
[63 79] [1.]
[-100  -83] [6.01055872e-66]
[-100  -82] [3.20652436e-65]
[63 82] [1.]
[-100  -80] [9.12586299e-64]
[-100  -79] [4.86848285e-63]
[63 83] [1.]
[-100  -77] [1.38558459e-61]
[-100  -76] [7.39184317e-61]
[63 89] [1.]
[-100  -74] [2.10374039e-59]
[63 91] [1.]
[-100  -72] [5.98730724e-58]
[-100  -71] [3.19412012e-57]
[-100  -70] [1.70400531e-56]
[-100  -69] [9.09056014e-56]
[-100  -68] [4.84964942e-55]
[-100  -67] [2.58720025e-54]
[-100  -66] [1.38022454e-53]
[63 99] [1.]
[-100  -64] [3.92815978e-52]
[-100  -63] [2.09560219e-51]
[  64 -100] [2.53988133e-70]
[-100  -61] [5.9641457e-50]
[ 64 -97] [3.85631522e-68]
[ 64 -96] [2.05727442e-67]
[-100  -58] [9.05539386e-48]
[ 64 -90] [4.74253371e-63]
[ 64 -89] [2.53005596e-62]
[ 64 -87] [7.20061393e-61]
[-100  -52] [2.08749548e-43]
[ 64 -84] [1.09327301e-58]
[-100  -50] [5.94107377e-42]
[ 95 -13] [1.49260907e-05]
[-100  -45] [2.56722211e-38]

6,总结:可以看出,这50个验证样本上都没问题,虽然想到的测试方案有点low,但一时找不到啥好数据。由此验证BP算法的正确性。如有可疑或不足之处,敬请告知。

 

以上是关于读懂反向传播算法(bp算法)的主要内容,如果未能解决你的问题,请参考以下文章

如何理解CNN神经网络里的反向传播backpropagation,bp算法

反向传播算法

如何理解神经网络里面的反向传播算法

反向传播算法的算法简介

一文搞懂反向传播算法

BP算法,反向传播算法