神经网络理论到实践(2):理解并实现反向传播及验证神经网络是否正确

描述

专栏中《零神经网络实战》系列持续更新介绍神经元怎么工作,最后使用python从0到1不调用任何依赖神经网络框架(不使用tensorflow等框架)来实现神经网络,梯度下降、反向传播、卷积神经网络CNN、循环神经网络RNN。从0基础角度进行神经网络实战。
上一篇:零基础神经网络实战(1):单个神经元+随机梯度下降学习逻辑与规则
作者:司南牧

实例介绍反向传播,为何说深度学习离不开反向传播?

之前介绍了单个神经元如何利用随机梯度下降自己调节权重。深度学习指的是数以百千层的层数很深的神经网络,每层又有数以百千个神经元。那么深度学习是否也可以使用这种形式的梯度下降来进行调节权重呢?答:很难。为什么?主要原因是太深了。为何“深”用梯度下降解会有问题呢?主要是因为链式法则导致计算损失函数对前面层权重的导数时,损失函数对后面层权重的导数总是被重复计算,反向传播就是将那些计算的值保存减少重复计算。不明白?那这篇文章就看对了。接下来将解释这个重复计算过程。反向传播就是梯度下降中的求导环节,它从后往前计算导数重复利用计算过的导数而已。。梯度下降不懂或者神经网络不懂请先阅读这个文章单个神经元+随机梯度下降学习逻辑与规则

文章结构:

  1. 为何要使用反向传播?
  2. 反向传播优化神经网络权重参数实践。

为何要使用反向传播?

我们用一个最简单的三层神经网络(无激活函数)来进行解释为何需要使用反向传播,所使用的三层神经网络如下所示:

 

 

这里我们有三个要优化的参数:w1,w2,w3。

我们先用传统的梯度下降方法来看看哪些地方重复计算了,而反向传播就是将这些重复计算的地方计算结果保存,并且向前面层传播这个值以减少运算复杂度。 梯度下降需要先得求损失函数对w1,w2,w3的导数,然后根据导数来不断迭代更新w1,w2,w3的值。

第1层:求损失函数L(w1,w2,w3)对w1的导数(更规范的讲是偏导数)。

我们先看看损失函数L(w1,w2,w3)和w1之间的关系是什么。

$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z
z=w2 * y
y=w1 * x

所以这是个复合函数的求导。根据高中学习到的复合函数求导法则(也是大家可能经常听到的链式法则),复合函数求导等于各级复合函数导数的乘积,也就是这样

第2层:求损失函数L(w1,w2,w3)对w2的导数。
我们再看看损失函数L(w1,w2,w3)和w2之间的关系是什么。

$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z
z=w2 * y

根据复合函数求导可得:

第3层:求损失函数L(w1,w2,w3)对w3的导数。
我们再看看损失函数L(w1,w2,w3)和w3之间的关系是什么。

$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z

根据复合函数求导可得:

我们将这三层的损失函数对相应层权重导数列在一起看看哪儿重复计算了:


 

我们再看看这个图:

所搭建的最简单的三层神经网络

你会发现,如果将损失函数也看做是一层的话。即我们认为e->L(w1,w2,w3)这也算一层。

找规律

 

反向传播过程理解

前面我们提到了可以从后面往前面计算,将公共部分的导数往前传。这只是解决了求导问题,那怎么进行参数更新呢?答:参数更新跟梯度下降完全一样,都是这个公式。反向传播就是梯度下降中的求导环节,它重复利用计算过的导数而已。

我们看看反向传播版的使用梯度下降进行参数更新是怎样的。

损失函数L(w1,w2,w3)是这样:$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$

其他的几层函数如下所示:

第3层:e=w3 * z
第2层:z=w2 * y
第1层:y=w1 * x

在这篇文章“单个神经元+随机梯度下降学习逻辑与规则”介绍了,权重更新是一个不断猜(迭代更新)的过程。下一次权重值 = 本次权重值 - 学习率* 损失函数对该权重的导数。定义学习率为α. 接下来我们只需要知道怎么利用后面层数传递过来的值来求“损失函数对当前层权重wi的导数”即可。

则各层网络更新权重的步骤为如下所示:

1.更新第3层权重参数w3.

 

2.更新第2层权重参数w2

 

3.更新第1层权重参数w1.

 

所以,将上面3步用伪代码写可以写成下面这样。

 

终于可以告别可怕的公式了,越难的东西你坚持了,你的不可替代性就越强。加油你不是一个人在奋斗。

反向传播实践

我们将上面的伪代码转成Python代码。我们希望这个神经网络能自己学习到的功能是输入x输出的e=-x.

我们提供训练集数据(我们只有两条数据):


重复训练次数epoch = 160。

好开工实现它。

# -*- coding: UTF-8 -*-
"""
@author 知乎:@Ai酱
"""
class NeuralNetwork:

    def __init__(self):
        self.LEARNING_RATE = 0.05 # 设置学习率
        # 初始化网络各层权重(权重的初试值也会影响神经网络是否收敛)
        # 博主试了下权重初始值都为0.2333是不行的
        self.w3 = -0.52133
        self.w2 = -0.233
        self.w1 = 0.2333
        self.data = [1, -1] # 输入数据
        self.label= [-1, 1]

    def train(self):
        epoch = 160
        for _ in range(epoch):
            # 逐个样本进行训练模型
            for i in range(len(self.data)):
                x = self.data[i]
                e_real = self.label[i]

                self.y = self.w1 * x #计算第1层输出
                self.z = self.w2 * self.y # 计算第2层输出
                self.e = self.w3 * self.z # 计算第3层输出

                # 开始反向传播优化权重
                self.result3 = self.e - e_real
                self.w3 = self.w3 - self.LEARNING_RATE * self.result3 * self.z

                self.result2 = self.result3 * self.w3
                self.w2 = self.w2 - self.LEARNING_RATE * self.result2 * self.y

                self.w1 = self.w1 - self.LEARNING_RATE * self.result2 * self.w2 * x

    def predict(self,x):
        self.y = self.w1 * x #计算第1层输出
        self.z = self.w2 * self.y # 计算第2层输出
        self.e = self.w3 * self.z # 计算第3层输出
        return 1 if self.e>0 else -1
nn = NeuralNetwork()
nn.train()
print(1,',',nn.predict(1))
print(-1,',',nn.predict(-1))
'''
输出:
1 , -1
-1 , 1
'''

如何检验反向传播是否写对?

手动推导,人工判断

前面提到了反向传播本质是梯度下降,那么关键在于导数必须对。我们现在网络比较小可以直接手动计算导数比对代码中对各权重导数是否求对。

比如上面代码中三个参数的导数将代码中的result*展开表示就是:

dw3 = (self.e - e_real) * self.z
    = (self.e - e_real) * self.w2 * self.y
    =  (self.e - e_real) * self.w2 * self.w1 * x
    = (self. w3 * self.w2 * self.w1 * x - e_real) * self.w2 * self.w1 * x

dw2 = (self.e - e_real) * self.w3* self.y
    = (self.e - e_real) * self.w3* self.w1 * x
    = (self. w3 * self.w2 * self.w1 * x - e_real) * self.w3* self.w1 * x

dw3 =  self.result3 * self.z
    = (self.e - e_real) * self.w2 * self.w1 * x
    = (self. w3 * self.w2 * self.w1 * x - e_real)* self.w2 * self.w1 * x

而损失函数展开可以表示为: $$L(w1,w2,w3) = 1/2 (w3 * w2 * w1 * x - /hat e)^2$$

对各权重参数求导为:

$${dL}/ {dw3} = (w3 * w2 * w1 * x - /hat e) * w2 * w1$$

$${dL}/ {dw2} = (w3 * w2 * w1 * x - /hat e) * w3 * w1$$

$${dL}/ {dw1} = (w3 * w2 * w1 * x - /hat e) * w3 * w2$$

可以发现我们代码展开,与我们实际的公式求导是一致的证明我们代码是正确的。

但是,一旦层数很深,那么我们就不能这么做了

我们需要用代码自动判断是否反向传播写对了

代码自动判断反向传播的导函数是否正确

这个和手工判断方法类似。反向传播是否正确,关键在于dL/dwi是否计算正确。根据高中学过的导数的定义,对于位于点θ的导数有:

$$f'(/theta) = /lim_{/epsilon/to 0} {f(/theta + /epsilon) + f(/theta - /epsilon) } /{2/epsilon}$$

所以我们可以看反向传播求的导函数值和用导数定义求的导函数值是否接近。

即我们需要让代码判断这个式子是否成立:$${dL}/ {dwi} /approx {L(wi+ 10^{-4}) - L(wi- 10^{-4})} /{2* 10^{-4}}$$

左边的dL/dwi是反向传播求得,右边是导数定义求的导数值。这两个导数应当大致相同。

实践:程序自动检验导函数是否正确:

新增了一个梯度检验函数check_gradient(),如下所示:

# -*- coding: UTF-8 -*-
"""
@author 知乎:@Ai酱
"""
class NeuralNetwork:

    def __init__(self):
        self.LEARNING_RATE = 0.05 # 设置学习率
        # 初始化网络各层权重(权重的初试值也会影响神经网络是否收敛)
        # 博主试了下权重初始值都为0.2333是不行的
        self.w3 = -0.52133
        self.w2 = -0.233
        self.w1 = 0.2333

        self.data = [1, -1] # 输入数据
        self.label= [-1, 1]
    def L(self,w1,w2,w3,x,e_real):
        '''
        损失函数 return 1/2 * (e - e_real)^2
        '''
        return 0.5*(w1*w2*w3*x - e_real)**2

    def train(self):
        epoch = 160
        for _ in range(epoch):
            # 逐个样本进行训练模型
            for i in range(len(self.data)):
                x = self.data[i]
                e_real = self.label[i]

                self.y = self.w1 * x #计算第1层输出
                self.z = self.w2 * self.y # 计算第2层输出
                self.e = self.w3 * self.z # 计算第3层输出

                # 开始反向传播优化权重
                self.result3 = self.e - e_real
                self.w3 = self.w3 - self.LEARNING_RATE * self.result3 * self.z

                self.result2 = self.result3 * self.w3
                self.w2 = self.w2 - self.LEARNING_RATE * self.result2 * self.y

                self.w1 = self.w1 - self.LEARNING_RATE * self.result2 * self.w2 * x
                self.check_gradient(x,e_real)

    def check_gradient(self,x,e_real):
        # 反向传播所求得的损失函数对各权重的导数
        dw3 = self.result3 * self.z
        dw2 = self.result2 * self.y
        dw1 = self.result2 * self.w2 * x

        # 使用定义求损失函数对各权重的导数
        epsilon = 10**-4 # epsilon为10的4次方
        # 求损失函数在w3处的左极限和右极限
        lim_dw3_right = self.L(self.w1, self.w2, self.w3+epsilon, x, e_real)
        lim_dw3_left = self.L(self.w1, self.w2, self.w3-epsilon, x, e_real)
        # 利用左右极限求导
        lim_dw3 = (lim_dw3_right - lim_dw3_left)/(2*epsilon)

        lim_dw2_right = self.L(self.w1, self.w2+epsilon, self.w3, x, e_real)
        lim_dw2_left = self.L(self.w1, self.w2-epsilon, self.w3, x, e_real)
        lim_dw2 = (lim_dw2_right - lim_dw2_left)/(2*epsilon)

        lim_dw1_right = self.L(self.w1+epsilon, self.w2, self.w3, x, e_real)
        lim_dw1_left = self.L(self.w1-epsilon, self.w2, self.w3, x, e_real)
        lim_dw1 = (lim_dw1_right - lim_dw1_left)/(2*epsilon)

        # 比对反向传播求的导数和用定义求的导数是否接近
        print("dl/dw3反向传播求得:%f,定义求得%f"%(dw3,lim_dw3))
        print("dl/dw2反向传播求得:%f,定义求得%f"%(dw2,lim_dw2))
        print("dl/dw1反向传播求得:%f,定义求得%f"%(dw1,lim_dw1))

    def predict(self,x):
        self.y = self.w1 * x #计算第1层输出
        self.z = self.w2 * self.y # 计算第2层输出
        self.e = self.w3 * self.z # 计算第3层输出
        return 1 if self.e>0 else -1
nn = NeuralNetwork()
nn.train()
print(1,',',nn.predict(1))
print(-1,',',nn.predict(-1))
'''
输出:
dl/dw1反向传播求得:-0.026729,定义求得-0.026727
dl/dw3反向传播求得:0.003970,定义求得0.004164
dl/dw2反向传播求得:-0.032617,定义求得-0.033257
dl/dw1反向传播求得:-0.027502,定义求得-0.027499
dl/dw3反向传播求得:0.004164,定义求得0.004367
dl/dw2反向传播求得:-0.033272,定义求得-0.033932
dl/dw1反向传播求得:-0.028291,定义求得-0.028288
dl/dw3反向传播求得:0.004367,定义求得0.004579
dl/dw2反向传播求得:-0.033947,定义求得-0.034625
dl/dw1反向传播求得:-0.029097,定义求得-0.029094
... ...
1 , -1
-1 , 1
'''

可以发现反向传播求得损失函数对各参数求得的导数和我们用高中学的定义法求导数,两者基本一致,证明我们反向传播求得的导数没有问题。

附上上面的易读版代码的github代码下载地址:从本质如何理解机器学习?

框架化反向传播

每个程序员都有一个写框架的梦想,不如我们将前面的代码写个类似TensorFlow这种框架的反向传播简单框架吧。附上框架版的代码github下载地址:https://github.com/varyshare/newbie_neural_network_practice/blob/master/backpropagation/backpropagation_framework.py


# -*- coding: utf-8 -*-
"""
框架化反向传播编程
@author: 知乎@Ai酱
"""

import random
class Layer(object):
    '''
    本文中,一层只有一个神经元,一个神经元只有一个输入一个输出
    '''
    def __init__(self,layer_index):
        '''
        layer_index: 第几层
        '''
        self.layer_index = layer_index
        # 初始化权重[0,1] - 0.5 = [-0.5,0.5]保证初始化有正有负
        self.w = random.random() - 0.5 
        # 当前层的输出
        self.output = 0
        
    def forward(self,input_var):
        '''
        前向传播:对输入进行运算,并将结果保存
        input_var: 当前层的输入
        '''
        self.input = input_var
        self.output = self.w * self.input
        
    
    def backward(self, public_value):
        '''
        反向传播:计算上层也会使用的导数值并保存
        假设当前层的计算规则是这样output = f(input),
        而 input == 前一层的输出,
        因此,根据链式法则损失函数对上一层权重的导数 = 后面层传过来的公共导数* f'(input) * 前一层的导数
        也就是说,后面层传过来的公共导数值* f'(input) 是需要往前传的公用的导数值。
        由于本层中对输入做的运算为:output = f(input) = w*input
        所以, f'(input) = w.
        public_value: 后面传过来的公共导数值
        '''
        # 当前层要传给前面层的公共导数值 = 后面传过来的公共导数值 * f'(input)
        self.public_value = public_value * self.w
        # 损失函数对当前层参数w的导数 = 后面传过来的公共导数值 * f'(input) * doutput/dw
        self.w_grad = self.public_value * self.input
    
    def upate(self, learning_rate):
        '''
        利用梯度下降更新参数w
        参数迭代更新规则(梯度下降): w = w - 学习率*损失函数对w的导数
        learning_rate: 学习率
        '''
        self.w = self.w - learning_rate * self.w_grad
    
    def display(self):
        print('layer',self.layer_index,'w:',self.w)

class Network(object):
    def __init__(self,layers_num):
        '''
        构造网络
        layers_num: 网络层数
        '''
        self.layers = []
        # 向网络添加层
        for i in range(layers_num):
            self.layers.append(Layer(i+1))#层编号从1开始
    
    def predict(self, sample):
        '''
        sample: 样本输入
        return 最后一层的输出
        '''
        output = sample
        for layer in self.layers:
            layer.forward(output)
            output = layer.output
        return 1 if output>0 else -1
    
    def calc_gradient(self, label):
        '''
        从后往前计算损失函数对各层的导数
        '''
        # 计算最后一层的导数
        last_layer = self.layers[-1]
        # 由于损失函数=0.5*(last_layer.output - label)^2
        # 由于backward中的public_value = 输出对输入的导数
        # 对于损失函数其输入是last_layer.output,损失函数对该输入的导数=last_layer.output - label
        # 所以 最后一层的backward的public_value = last_layer.output - label
        last_layer.backward(last_layer.output - label)
        public_value = last_layer.public_value
        for layer in self.layers:
            layer.backward(public_value) # 计算损失函数对该层参数的导数
            public_value= layer.public_value
            
    def update_weights(self, learning_rate):
        '''
        更新各层权重
        '''
        for layer in self.layers:
            layer.upate(learning_rate)
        
    
    def train_one_sample(self, label, sample, learning_rate):
        self.predict(sample) # 前向传播,使得各层的输入都有值
        self.calc_gradient(label) # 计算各层导数
        self.update_weights(learning_rate) # 更新各层参数
        
    def train(self, labels, data_set, learning_rate, epoch):
        '''
        训练神经网络
        labels: 样本标签
        data_set: 输入样本们
        learning_rate: 学习率
        epoch: 同样的样本反复训练的次数
        '''
        for _ in range(epoch):# 同样数据反复训练epoch次保证权重收敛
            for i in range(len(labels)):#逐样本更新权重
                self.train_one_sample(labels[i], data_set[i], learning_rate)

nn = Network(3)
data_set = [1,-1]
labels   = [-1,1]
learning_rate = 0.05
epoch = 160
nn.train(labels,data_set,learning_rate,epoch)
print(nn.predict(1)) # 输出 -1
print(nn.predict(-1)) # 输出 1

 

欢迎关注我的知乎专栏适合初学者的机器学习神经网络理论到实践
上一篇为零基础神经网络实战(1):单个神经元+随机梯度下降学习逻辑与规则
下一篇为适合初学者的神经网络理论到实践(3):打破概念束缚:强化学习是个啥?

审核编辑:符乾江
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分