一文详解神经网络 BP 算法原理及 Python 实现

  什么是梯度下降和链式求导法则

假设我们有一个函数 J(w),如下图所示。

梯度下降示意图

现在,我们要求当 w 等于什么的时候,J(w) 能够取到最小值。从图中我们知道最小值在初始位置的左边,也就意味着如果想要使 J(w) 最小,w的值需要减小。而初始位置的切线的斜率a > 0(也即该位置对应的导数大于0),w = w – a 就能够让 w 的值减小,循环求导更新w直到 J(w) 取得最小值。如果函数J(w)包含多个变量,那么就要分别对不同变量求偏导来更新不同变量的值。

所谓的链式求导法则,就是求复合函数的导数:

链式求导法则

放个例题,会更加明白一点:

链式求导的例子

  神经网络的结构

神经网络由三部分组成,分别是最左边的输入层,隐藏层(实际应用中远远不止一层)和最右边的输出层。层与层之间用线连接在一起,每条连接线都有一个对应的权重值 w,除了输入层,一般来说每个神经元还有对应的偏置 b。

神经网络的结构图

除了输入层的神经元,每个神经元都会有加权求和得到的输入值 z 和将 z 通过 Sigmoid 函数(也即是激活函数)非线性转化后的输出值 a,他们之间的计算公式如下

神经元输出值 a 的计算公式

其中,公式里面的变量l和j表示的是第 l 层的第 j 个神经元,ij 则表示从第 i 个神经元到第 j 个神经元之间的连线,w 表示的是权重,b 表示的是偏置,后面这些符号的含义大体上与这里描述的相似,所以不会再说明。下面的 Gif 动图可以更加清楚每个神经元输入输出值的计算方式(注意,这里的动图并没有加上偏置,但使用中都会加上)

动图显示计算神经元输出值

使用激活函数的原因是因为线性模型(无法处理线性不可分的情况)的表达能力不够,所以这里通常需要加入 Sigmoid 函数来加入非线性因素得到神经元的输出值。

关于为什么线性函数模型表达能力不够,可以点击这里查看知乎上面的讨论。

sigmoid 函数

可以看到 Sigmoid 函数的值域为 (0,1) ,若对于多分类任务,输出层的每个神经元可以表示是该分类的概率。当然还存在其他的激活函数,他们的用途和优缺点也都各异。

  BP 算法执行的流程(前向传递和逆向更新)

在手工设定了神经网络的层数,每层的神经元的个数,学习率 η(下面会提到)后,BP 算法会先随机初始化每条连接线权重和偏置,然后对于训练集中的每个输入 x 和输出 y,BP 算法都会先执行前向传输得到预测值,然后根据真实值与预测值之间的误差执行逆向反馈更新神经网络中每条连接线的权重和每层的偏好。在没有到达停止条件的情况下重复上述过程。

其中,停止条件可以是下面这三条

● 权重的更新低于某个阈值的时候

● 预测的错误率低于某个阈值

● 达到预设一定的迭代次数

譬如说,手写数字识别中,一张手写数字1的图片储存了28*28 = 784个像素点,每个像素点储存着灰度值(值域为[0,255]),那么就意味着有784个神经元作为输入层,而输出层有10个神经元代表数字0~9,每个神经元取值为0~1,代表着这张图片是这个数字的概率。

每输入一张图片(也就是实例),神经网络会执行前向传输一层一层的计算到输出层神经元的值,根据哪个输出神经元的值最大来预测输入图片所代表的手写数字。

然后根据输出神经元的值,计算出预测值与真实值之间的误差,再逆向反馈更新神经网络中每条连接线的权重和每个神经元的偏好。

前向传输(Feed-Forward)

从输入层=>隐藏层=>输出层,一层一层的计算所有神经元输出值的过程。

逆向反馈(Back Propagation)

因为输出层的值与真实的值会存在误差,我们可以用均方误差来衡量预测值和真实值之间的误差。

均方误差

逆向反馈的目标就是让E函数的值尽可能的小,而每个神经元的输出值是由该点的连接线对应的权重值和该层对应的偏好所决定的,因此,要让误差函数达到最小,我们就要调整w和b值, 使得误差函数的值最小。

权重和偏置的更新公式

对目标函数 E 求 w 和 b 的偏导可以得到 w 和 b 的更新量,下面拿求 w 偏导来做推导。

其中 η 为学习率,取值通常为 0.1 ~ 0.3,可以理解为每次梯度所迈的步伐。注意到 w_hj 的值先影响到第 j 个输出层神经元的输入值a,再影响到输出值y,根据链式求导法则有:

使用链式法则展开对权重求偏导

根据神经元输出值 a 的定义有:

对函数 z 求 w 的偏导

Sigmoid 求导数的式子如下,从式子中可以发现其在计算机中实现也是非常的方便:

Sigmoid 函数求导

所以

则权重 w 的更新量为:

类似可得 b 的更新量为:

但这两个公式只能够更新输出层与前一层连接线的权重和输出层的偏置,原因是因为 δ 值依赖了真实值y这个变量,但是我们只知道输出层的真实值而不知道每层隐藏层的真实值,导致无法计算每层隐藏层的 δ 值,所以我们希望能够利用 l+1 层的 δ 值来计算 l 层的 δ 值,而恰恰通过一些列数学转换后可以做到,这也就是逆向反馈名字的由来,公式如下:

从式子中我们可以看到,我们只需要知道下一层的权重和神经元输出层的值就可以计算出上一层的 δ 值,我们只要通过不断的利用上面这个式子就可以更新隐藏层的全部权重和偏置了。

在推导之前请先观察下面这张图:

l 和 l+1 层的神经元

首先我们看到 l 层的第 i 个神经元与 l+1 层的所有神经元都有连接,那么我们可以将 δ 展开成如下的式子:

也即是说我们可以将 E 看做是 l+1 层所有神经元输入值的 z 函数,而上面式子的 n 表示的是 l+1 层神经元的数量,再进行化简后就可以得到上面所说的式子。

在这里的推导过程只解释了关键的部分,如果要查看更加详细的推导内容,可以点击此处下载我在学习过程中参考的一篇 pdf 文档,里面的推导过程非常详细。另外也参考了周志华所写的机器学习中的神经网络部分的内容和 neural networks and deep learning 的内容。

  Python 源码解析

源码来自于 Michael Nielsen 大神的深度学习在线教程,但他的内容都是英文的,我结合了自己的理解和上面的理论知识对源码进行了注释。>>点击此处查看整理的代码和数字识别实例<<

使用 Python 实现的神经网络的代码行数并不多,仅包含一个 Network 类,首先来看看该类的构造方法。

def __init__(self, sizes):
        """
        :param sizes: list类型,储存每层神经网络的神经元数目
                      譬如说:sizes = [2, 3, 2] 表示输入层有两个神经元、
                      隐藏层有3个神经元以及输出层有2个神经元
        """
        # 有几层神经网络
        self.num_layers = len(sizes)
        self.sizes = sizes
        # 除去输入层,随机产生每层中 y 个神经元的 biase 值(0 - 1)
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        # 随机产生每条连接线的 weight 值(0 - 1)
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

向前传输(FreedForward)的代码。

    

def feedforward(self, a):
        """
        前向传输计算每个神经元的值
        :param a: 输入值
        :return: 计算后每个神经元的值
        """
        for b, w in zip(self.biases, self.weights):
            # 加权求和以及加上 biase
            a = sigmoid(np.dot(w, a)+b)
        return a

源码里使用的是随机梯度下降(Stochastic Gradient Descent,简称 SGD),原理与梯度下降相似,不同的是随机梯度下降算法每次迭代只取数据集中一部分的样本来更新 w 和 b 的值,速度比梯度下降快,但是,它不一定会收敛到局部极小值,可能会在局部极小值附近徘徊。

    

def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """
        随机梯度下降
        :param training_data: 输入的训练集
        :param epochs: 迭代次数
        :param mini_batch_size: 小样本数量
        :param eta: 学习率
        :param test_data: 测试数据集
        """
        if test_data: n_test = len(test_data)
        n = len(training_data)
        for j in xrange(epochs):
            # 搅乱训练集,让其排序顺序发生变化
            random.shuffle(training_data)
            # 按照小样本数量划分训练集
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in xrange(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                # 根据每个小样本来更新 w 和 b,代码在下一段
                self.update_mini_batch(mini_batch, eta)
            # 输出测试每轮结束后,神经网络的准确度
            if test_data:
                print "Epoch {0}: {1} / {2}".format(
                    j, self.evaluate(test_data), n_test)
            else:
                print "Epoch {0} complete".format(j)

根据 backprop 方法得到的偏导数更新 w 和 b 的值。

    

def update_mini_batch(self, mini_batch, eta):
        """
        更新 w 和 b 的值
        :param mini_batch: 一部分的样本
        :param eta: 学习率
        """
        # 根据 biases 和 weights 的行列数创建对应的全部元素值为 0 的空矩阵
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            # 根据样本中的每一个输入 x 的其输出 y,计算 w 和 b 的偏导数
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            # 累加储存偏导值 delta_nabla_b 和 delta_nabla_w
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        # 更新根据累加的偏导值更新 w 和 b,这里因为用了小样本,
        # 所以 eta 要除于小样本的长度
        self.weights = [w-(eta/len(mini_batch))*nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb
                       for b, nb in zip(self.biases, nabla_b)]

下面这块代码是源码最核心的部分,也即 BP 算法的实现,包含了前向传输和逆向反馈,前向传输在 Network 里有单独一个方法(上面提到的 feedforward 方法),那个方法是用于验证训练好的神经网络的精确度的,在下面有提到该方法。

    def backprop(self, x, y):
        """
        :param x:
        :param y:
        :return:
        """
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 前向传输
        activation = x
        # 储存每层的神经元的值的矩阵,下面循环会 append 每层的神经元的值
        activations = [x]
        # 储存每个未经过 sigmoid 计算的神经元的值
        zs = []
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # 求 δ 的值
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        # 乘于前一层的输出值
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for l in xrange(2, self.num_layers):
            # 从倒数第 **l** 层开始更新,**-l** 是 python 中特有的语法表示从倒数第 l 层开始计算
            # 下面这里利用 **l+1** 层的 δ 值来计算 **l** 的 δ 值
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)

接下来则是 evaluate 的实现,调用 feedforward 方法计算训练好的神经网络的输出层神经元值(也即预测值),然后比对正确值和预测值得到精确率。

 

   def evaluate(self, test_data):
        # 获得预测结果
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        # 返回正确识别的个数
        return sum(int(x == y) for (x, y) in test_results)

最后,我们可以利用这个源码来训练一个手写数字识别的神经网络,并输出评估的结果,代码如下:

import mnist_loader
import network

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data = test_data)
# 输出结果
# Epoch 0: 9038 / 10000
# Epoch 1: 9178 / 10000
# Epoch 2: 9231 / 10000
# ...
# Epoch 27: 9483 / 10000
# Epoch 28: 9485 / 10000
# Epoch 29: 9477 / 10000

可以看到,在经过 30 轮的迭代后,识别手写神经网络的精确度在 95% 左右,当然,设置不同的迭代次数,学习率以取样数对精度都会有影响,如何调参也是一门技术活,这个坑就后期再填吧。

  总结

神经网络的优点:

网络实质上实现了一个从输入到输出的映射功能,而数学理论已证明它具有实现任何复杂非线性映射的功能。这使得它特别适合于求解内部机制复杂的问题。

网络能通过学习带正确答案的实例集自动提取“合理的”求解规则,即具有自学习能力。

网络具有一定的推广、概括能力。

神经网络的缺点:

对初始权重非常敏感,极易收敛于局部极小。

容易 Over Fitting 和 Over Training。

如何选择隐藏层数和神经元个数没有一个科学的指导流程,有时候感觉就是靠猜。

应用领域:

常见的有图像分类,自动驾驶,自然语言处理等。

  TODO

但其实想要训练好一个神经网络还面临着很多的坑(譬如下面四条):

1. 如何选择超参数的值,譬如说神经网络的层数和每层的神经元数量以及学习率;

2. 既然对初始化权重敏感,那该如何避免和修正;

3. Sigmoid 激活函数在深度神经网络中会面临梯度消失问题该如何解决;

4. 避免 Overfitting 的 L1 和 L2正则化是什么。

  参考

[1] 周志华 机器学习

[2] 斯坦福大学机器学习在线课程

[3] Parallel Distributed Processing (1986, by David E. Rumelhart, James L. McClelland), Chapter 8 Learning Internal Representations by Error Propagation

[4] How the backpropagation algorithm works

[5] Backpropagation Algorithm

[6] 链式求导法则,台湾中华科技大学数位课程,Youtube 视频,需要翻墙,顺便安利一下他们的数学相关的视频,因为做的都非常浅显易懂

====================================分割线================================

本文作者:AI研习社

本文转自雷锋网禁止二次转载,原文链接

时间: 2025-01-02 17:50:57

一文详解神经网络 BP 算法原理及 Python 实现的相关文章

《末日公寓》攻略图文详解

<末日公寓>攻略图文详解一<末日公寓>是2012年最考验逃生能力的游戏之一.越是情况危急,你越是需要冷静下来,在地球将面临毁灭性灾难的情况下,肯定有什么办法生存下来,末日公寓将是你,也会是你可能带给人类最后的机会.本篇文章将为大家详细讲述解谜探险游戏末日公寓攻略的内容,希望能给各位玩家提供的末日公寓攻略中得到一些帮助.软件名称:<FinalCastle>价格:6元点击下载更新时间:2012年1月15日大小:77.3 MB进入游戏后,首先是一段剧情介绍,之后才真正开始游戏

20分钟搞懂神经网络BP算法

在学习深度学习过程中,无意间发现一篇介绍BP算法的文章,感觉非常直观,容易理解.这篇文章的最大亮点是:不像其他介绍BP算法的文章,用一堆数据符号和公式来推导.文中通过使用一条具体的样本数据,为我们展示了模型训练中的参数迭代计算过程,为我们理解BP算法提供了很直观的理解视角:其次,作者也给出了使用python来实现BP的算法.只要你了解过传统神经网络结构以及大学微积分的知识,都可以毫不费力的在20分钟内完全理解BP算法.这里整理出来,供大家学习参考.要看原文的同学,直接跳到文末点击原文链接. 在开

详解js跨域原理以及2种解决方案_javascript技巧

1.什么是跨域 我们经常会在页面上使用ajax请求访问其他服务器的数据,此时,客户端会出现跨域问题. 跨域问题是由于javascript语言安全限制中的同源策略造成的. 简单来说,同源策略是指一段脚本只能读取来自同一来源的窗口和文档的属性,这里的同一来源指的是主机名.协议和端口号的组合. 例如: 2.实现原理 在HTML DOM中,Script标签是可以跨域访问服务器上的数据的.因此,可以指定script的src属性为跨域的url,从而实现跨域访问. 例如: 这种访问方式是不行的.但是如下方式,

一文详解如何用 python 做中文分词

打算绘制中文词云图?那你得先学会如何做中文文本分词.跟着我们的教程,一步步用 Python 来动手实践吧.   需求 在此前发布的文章<从零开始教你用 Python 做词云>一文中,我们介绍了英文文本的词云制作方法.大家玩儿得可还高兴? 文中提过,选择英文文本作为示例,是因为处理起来最简单.但是很快就有读者尝试用中文文本做词云了.按照前文的方法,你成功了吗? 估计是不成功的.因为这里面缺了一个重要的步骤. 观察你的英文文本.你会发现英文单词之间采用空格作为强制分隔符. 例如: Yes Mini

为什么ResNet和DenseNet可以这么深?一文详解残差块为何有助于解决梯度弥散问题。

传统的"提拉米苏"式卷积神经网络模型,都以层叠卷积层的方式提高网络深度,从而提高识别精度.但层叠过多的卷积层会出现一个问题,就是梯度弥散(Vanishing),backprop无法有效地把梯度更新到前面的网络层,导致前面的层参数无法更新. 而BatchNormalization(BN).ResNet的skip connection就是为了解决这个问题,BN通过规范化输入数据改变数据分布,在前传过程中消除梯度弥散.而skip connection则能在后传过程中更好地把梯度传到更浅的层次

从理论到实践,一文详解 AI 推荐系统的三大算法

  介绍 背景 随着互联网行业的井喷式发展,获取信息的方式越来越多,人们从主动获取信息逐渐变成了被动接受信息,信息量也在以几何倍数式爆发增长.举一个例子,PC时代用google reader,常常有上千条未读博客更新:如今的微信公众号,也有大量的红点未阅读.垃圾信息越来越多,导致用户获取有价值信息的成本大大增加.为了解决这个问题,我个人就采取了比较极端的做法:直接忽略所有推送消息的入口.但在很多时候,有效信息的获取速度极其重要. 由于信息的爆炸式增长,对信息获取的有效性,针对性的需求也就自然出现

全国社会媒体处理大会即将召开,一文详解四天议程精华

全国社会媒体处理大会(SMP)由中国中文信息学会社会媒体处理专委会主办,专注于以社会媒体处理为主题的科学研究与工程开发,为传播社会媒体处理最新的学术研究与技术成果提供广泛的交流平台,旨在构建社会媒体处理领域的产学研生态圈,成为中国乃至世界社会媒体处理的风向标,会议将以社交网络的形式改变传统的学术会议交流体验. 据雷锋网了解,第六届全国社会媒体处理大会(SMP 2017)由中国科学院计算技术研究所承办,将于2017年9月14-17日(也即这周四到下周一)在北京友谊宾馆召开.四天的会议都有哪些内容呢

一文详解英伟达刚发布的 Tesla V100 究竟牛在哪?

众所周知,目前无论是语音识别,还是虚拟个人助理的训练:路线探测,还是自动驾驶系统的研发,在这些人工智能领域,数据科学家们正在面对越来越复杂的 AI 挑战.而为了更好地实现这些颇具未来感的强大功能,就必须在实践中引入一些指数级的更加复杂的深度学习模型. 另一方面,HPC(高性能计算)在现代科学研究中一直起着至关重要的作用.无论是预测天气,新药物的研究,或是探索未来能源,科研人员每天都需要利用大型计算系统对现实世界做各种各样的仿真和预测.而通过引入 AI 技术,HPC 就可以显著提升科研人员进行大数

一文详解 Word2vec 之 Skip-Gram 模型(结构篇)

这次的分享主要是对Word2Vec模型的两篇英文文档的翻译.理解和整合,这两篇英文文档都是介绍Word2Vec中的Skip-Gram模型.下一篇专栏文章将会用TensorFlow实现基础版Word2Vec的skip-gram模型,所以本篇文章先做一个理论铺垫. 原文英文文档请参考链接: - Word2Vec Tutorial - The Skip-Gram Model http://t.cn/Rc5RfJ2 - Word2Vec (Part 1): NLP With Deep Learning