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

简介:

最近这段时间系统性的学习了 BP 算法后写下了这篇学习笔记,因为能力有限,若有明显错误,还请指正。

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

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

函数 J(w)

梯度下降示意图

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

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

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

链式求导法则

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

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

链式求导的例子

神经网络的结构

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

神经网络的结构图

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

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

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

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

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

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

使用激活函数的原因是因为线性模型(无法处理线性不可分的情况)的表达能力不够,所以这里通常需要加入 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)

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

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

均方误差

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

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

权重和偏置的更新公式

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

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

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

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

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

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

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

对函数 z 求 w 的偏导

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

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

Sigmoid 函数求导

所以

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

则权重 w 的更新量为:

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

类似可得 b 的更新量为:

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

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

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

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

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

l 和 l+1 层的神经元

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

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

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

在这里的推导过程只解释了关键的部分。另外也参考了周志华所写的机器学习中的神经网络部分的内容和 neural networks and deep learning的内容。

Python 源码解析

源码来自于 Michael Nielsen大神的深度学习在线教程。

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

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

向前传输(FreedForward)的代码

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

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

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

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

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

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

 
 
  1. def backprop(self, x, y): 
  2.         """ 
  3.         :param x: 
  4.         :param y: 
  5.         :return: 
  6.         """ 
  7.         nabla_b = [np.zeros(b.shape) for b in self.biases] 
  8.         nabla_w = [np.zeros(w.shape) for w in self.weights] 
  9.         # 前向传输 
  10.         activation = x 
  11.         # 储存每层的神经元的值的矩阵,下面循环会 append 每层的神经元的值 
  12.         activations = [x]  
  13.         # 储存每个未经过 sigmoid 计算的神经元的值 
  14.         zs = []  
  15.         for b, w in zip(self.biases, self.weights): 
  16.             z = np.dot(w, activation)+b 
  17.             zs.append(z) 
  18.             activation = sigmoid(z) 
  19.             activations.append(activation) 
  20.         # 求 δ 的值 
  21.         delta = self.cost_derivative(activations[-1], y) * \ 
  22.             sigmoid_prime(zs[-1]) 
  23.         nabla_b[-1] = delta 
  24.         # 乘于前一层的输出值 
  25.         nabla_w[-1] = np.dot(delta, activations[-2].transpose()) 
  26.         for l in xrange(2self.num_layers): 
  27.             # 从倒数第 **l** 层开始更新,**-l** 是 python 中特有的语法表示从倒数第 l 层开始计算 
  28.             # 下面这里利用 **l+1** 层的 δ 值来计算 **l** 的 δ 值 
  29.             z = zs[-l] 
  30.             sp = sigmoid_prime(z) 
  31.             delta = np.dot(self.weights[-l+1].transpose(), delta) * sp 
  32.             nabla_b[-l] = delta 
  33.             nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) 
  34.         return (nabla_b, nabla_w) 

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

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

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

 
  1. import mnist_loader 
  2. import network 
  3.  
  4. training_data, validation_data, test_data = mnist_loader.load_data_wrapper() 
  5. net = network.Network([7843010]) 
  6. net.SGD(training_data, 30103.0, test_data = test_data) 
  7. # 输出结果 
  8. # Epoch 0: 9038 / 10000 
  9. # Epoch 1: 9178 / 10000 
  10. # Epoch 2: 9231 / 10000 
  11. # ... 
  12. # Epoch 27: 9483 / 10000 
  13. # Epoch 28: 9485 / 10000 
  14. # Epoch 29: 9477 / 10000 

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

总结

神经网络的优点:

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

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

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

神经网络的缺点:

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

容易 Over Fitting 和 Over Training。

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

应用领域:

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

TODO

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

  • 如何选择超参数的值,譬如说神经网络的层数和每层的神经元数量以及学习率;
  • 既然对初始化权重敏感,那该如何避免和修正;
  • Sigmoid 激活函数在深度神经网络中会面临梯度消失问题该如何解决;
  • 避免 Overfitting 的 L1 和 L2正则化是什么。

作者:曾梓华
来源:51CTO
相关文章
|
5天前
|
机器学习/深度学习 自然语言处理 算法
机器学习算法原理与应用:深入探索与实战
【5月更文挑战第2天】本文深入探讨机器学习算法原理,包括监督学习(如线性回归、SVM、神经网络)、非监督学习(聚类、PCA)和强化学习。通过案例展示了机器学习在图像识别(CNN)、自然语言处理(RNN/LSTM)和推荐系统(协同过滤)的应用。随着技术发展,机器学习正广泛影响各领域,但也带来隐私和算法偏见问题,需关注解决。
|
6天前
|
机器学习/深度学习 PyTorch TensorFlow
【Python机器学习专栏】循环神经网络(RNN)与LSTM详解
【4月更文挑战第30天】本文探讨了处理序列数据的关键模型——循环神经网络(RNN)及其优化版长短期记忆网络(LSTM)。RNN利用循环结构处理序列依赖,但遭遇梯度消失/爆炸问题。LSTM通过门控机制解决了这一问题,有效捕捉长距离依赖。在Python中,可使用深度学习框架如PyTorch实现LSTM。示例代码展示了如何定义和初始化一个简单的LSTM网络结构,强调了RNN和LSTM在序列任务中的应用价值。
|
6天前
|
机器学习/深度学习 PyTorch TensorFlow
【Python机器学习专栏】卷积神经网络(CNN)的原理与应用
【4月更文挑战第30天】本文介绍了卷积神经网络(CNN)的基本原理和结构组成,包括卷积层、激活函数、池化层和全连接层。CNN在图像识别等领域表现出色,其层次结构能逐步提取特征。在Python中,可利用TensorFlow或PyTorch构建CNN模型,示例代码展示了使用TensorFlow Keras API创建简单CNN的过程。CNN作为强大深度学习模型,未来仍有广阔发展空间。
|
6天前
|
机器学习/深度学习 自然语言处理 语音技术
【Python 机器学习专栏】Python 深度学习入门:神经网络基础
【4月更文挑战第30天】本文介绍了Python在深度学习中应用于神经网络的基础知识,包括神经网络概念、基本结构、训练过程,以及Python中的深度学习库TensorFlow和PyTorch。通过示例展示了如何使用Python实现神经网络,并提及优化技巧如正则化和Dropout。最后,概述了神经网络在图像识别、语音识别和自然语言处理等领域的应用,并强调掌握这些知识对深度学习的重要性。随着技术进步,神经网络的应用将持续扩展,期待更多创新。
|
6天前
|
机器学习/深度学习 运维 算法
【Python机器学习专栏】异常检测算法在Python中的实践
【4月更文挑战第30天】本文介绍了异常检测的重要性和在不同领域的应用,如欺诈检测和网络安全。文章概述了四种常见异常检测算法:基于统计、距离、密度和模型的方法。在Python实践中,使用scikit-learn库展示了如何实现这些算法,包括正态分布拟合、K-means聚类、局部异常因子(LOF)和孤立森林(Isolation Forest)。通过计算概率密度、距离、LOF值和数据点的平均路径长度来识别异常值。
|
6天前
|
机器学习/深度学习 数据可视化 算法
【Python机器学习专栏】t-SNE算法在数据可视化中的应用
【4月更文挑战第30天】t-SNE算法是用于高维数据可视化的非线性降维技术,通过最小化Kullback-Leibler散度在低维空间保持数据点间关系。其特点包括:高维到二维/三维映射、保留局部结构、无需预定义簇数量,但计算成本高。Python中可使用`scikit-learn`的`TSNE`类实现,结合`matplotlib`进行可视化。尽管计算昂贵,t-SNE在揭示复杂数据集结构上极具价值。
|
6天前
|
机器学习/深度学习 算法 数据挖掘
【Python机器学习专栏】关联规则学习:Apriori算法详解
【4月更文挑战第30天】Apriori算法是一种用于关联规则学习的经典算法,尤其适用于购物篮分析,以发现商品间的购买关联。该算法基于支持度和置信度指标,通过迭代生成频繁项集并提取满足阈值的规则。Python中可借助mlxtend库实现Apriori,例如处理购物篮数据,设置支持度和置信度阈值,找出相关规则。
|
6天前
|
机器学习/深度学习 算法 数据挖掘
【Python机器学习专栏】层次聚类算法的原理与应用
【4月更文挑战第30天】层次聚类是数据挖掘中的聚类技术,无需预设簇数量,能生成数据的层次结构。分为凝聚(自下而上)和分裂(自上而下)两类,常用凝聚层次聚类有最短/最长距离、群集平均和Ward方法。优点是自动确定簇数、提供层次结构,适合小到中型数据集;缺点是计算成本高、过程不可逆且对异常值敏感。在Python中可使用`scipy.cluster.hierarchy`进行实现。尽管有局限,层次聚类仍是各领域强大的分析工具。
|
6天前
|
机器学习/深度学习 算法 数据挖掘
【Python 机器学习专栏】K-means 聚类算法在 Python 中的实现
【4月更文挑战第30天】K-means 是一种常见的聚类算法,用于将数据集划分为 K 个簇。其基本流程包括初始化簇中心、分配数据点、更新簇中心并重复此过程直到收敛。在 Python 中实现 K-means 包括数据准备、定义距离函数、初始化、迭代和输出结果。虽然算法简单高效,但它需要预先设定 K 值,且对初始点选择敏感,可能陷入局部最优。广泛应用在市场分析、图像分割等场景。理解原理与实现对应用聚类分析至关重要。
|
11天前
|
存储 人工智能 数据处理
Python:编程的艺术与科学的完美交融
Python:编程的艺术与科学的完美交融
15 1

热门文章

最新文章