神经网络架构
假设我们的神经网络结构如下图所示,每一个圆都代表一个神经元,第一层被称为输入层,最后一层被称为输出层,位于中间的被称为隐藏层。输入层和输出层的设计往往是非常直接的。以我们需要学习的MNIST数据集为例,想要验证一张手写的数字图片是否为9,假设图片大小为 64∗64 ,那么就有 64∗64 个输入神经元,输出层则只有一个神经元,当输出值大于 0.5 时表明输入的图片是9,输出值小于 0.5 时,表明输入的图片不是9。
反向传播算法
与回归问题一样,我们也需要通过最小化代价函数来优化预测精度,但是由于神经网络包含了多个隐藏层,每个隐藏层都会输出预测,因此无法通过传统的梯度下降方法来最小化代价函数,而需要逐层考虑误差,并逐层优化。因此,在多层神经网络里面,我们需要通过反向传播算法优化预测精度。
算法流程
在实际应用中,我们一般将反向传播算法与学习算法一起使用,例如Stochatic Gradiant Decent。结合之后的算法流程总结如下:
- 输入 n 个训练样本
- 对于每一个训练样本
xi,i∈{1,2,⋯,n} :设置输入层的对应激活值为 a1i ,然后执行以下步骤:
- 前向传播: 对于 l∈{2,3,⋯,L} ,分别计算 zli=wlal−1i+bl , ali=σ(zli) 。
- 输出层误差 δL :计算 δLi=∇aC.∗σ′(zLi)
- 反向传播误差:对于 l∈{L−1,L−2,⋯,2} ,分别计算 δli=((wl+1)Tδl+1i).∗σ′(zli) 。
-
- 梯度下降:对于 l∈{L,L−1,⋯,2} ,更新 wl→wl−αn∑iδli(al−1i)T , bl→bl−αn∑iδli 。
理论推导
我们的最终目标是计算
minw,bC(w,b)
,即找到一组参数
(w,b)
使得代价函数
C
最小。因此我们需要计算
- 计算输出层
L
的偏导
∂C∂wL 。根据链式法则,我们可以得到下式:
∂C∂wL=∂C∂aL∂aL∂zL∂zL∂wL.(1) - 计算隐藏层
L−1
的偏导
∂C∂wL−1
。根据链式法则,我们可以得到下式:
∂C∂wL−1=∂C∂aL∂aL∂zL∂zL∂aL−1∂aL−1∂zL−1∂zL−1∂wL−1.(2)
观察公式
(1),(2)
,很明显用红框圈出来的是两个式子共有的一部分,通常我们称之为
δL
,表达式如公式
(3)
所示。我们可以用
δL
来计算输出层前一层的偏导。
同理,隐藏层之间也有类似的共有部分,例如我们可以用
δL−1
来计算隐藏层最后一层的前一层的偏导,表达式如公式
(4)
所示。
通过公式
(3),(4)
,公式
(1),(2)
可以改写为以下形式:
假设激活函数为
σ(z)
,对公式
(3)∼(6)
进行详细的计算。
按照相同的原理,我们可以推得:
将公式
(9),(10)
合并,并用
l,l+1
分别替换
L−1,L
,则公式
(7)∼(11)
可总结为以下三个式子:
观察公式 (12)∼(14) 可以看出,当前层的代价函数偏导,需要依赖于后一层的计算结果。这也是为什么这个算法的名称叫做反向传播算法。
应用实践
接下来我们将用反向传播算法对MNIST手写数字数据集进行识别。这个问题比较简单,数字共有10种可能,分别为 {0,1,⋯,9} ,因此是一个10分类问题。
完整代码请参考GitHub: machine-learning-notes(python3.6)
载入数据
首先我们从MNIST手写数字数据集官网下载训练集和测试集,并解压到data
文件夹中,data
文件夹中应该包含t10k-images.idx3-ubyte, t10k-labels.idx1-ubyte, train-images.idx3-ubyte, train-labels.idx1-ubyte这四个文件。接下来通过python-mnist包对数据集进行导入。如果尚未安装该包,可通过以下命令进行安装:
pip install python-mnist
使用python-mnist包载入数据,代码如下所示:
import numpy as np
from mnist import MNIST
from sklearn.preprocessing import MinMaxScaler
def vectorized_result(j):
"""
将数字(0...9)变为one hot向量
输入:
j: int,数字(0...9)
输出:
e: np.ndarray, 10维的向量,其中第j位为1,其他位都为0。
"""
e = np.zeros((10, 1));
e[j] = 1.0
return e
def load_data_wrapper(dirpath):
"""
载入mnist数字识别数据集,并对其进行归一化处理
输入:
dirpath: str, 数据所在文件夹路径
输出:
training_data: list, 包含了60000个训练数据集,其中每一个数据由一个tuple '(x, y)'组成,
x是训练的数字图像,类型是np.ndarray, 维度是(784,1)
y表示训练的图像所属的标签,是一个10维的one hot向量
test_data: list, 包含了10000个测试数据集,其中每一个数据由一个tuple '(x, y)'组成,
x是测试的数字图像,类型是np.ndarray, 维度是(784,1)
y表示测试的图像所属标签,int类型,是一个(0...9)的数字
"""
mndata = MNIST(dirpath)
tr_i, tr_o = mndata.load_training()
te_i, te_o = mndata.load_testing()
min_max_scaler = MinMaxScaler()
tr_i = min_max_scaler.fit_transform(tr_i)
te_i = min_max_scaler.transform(te_i)
training_inputs = [np.reshape(x, (784, 1)) for x in tr_i]
training_outputs = [vectorized_result(y) for y in tr_o]
training_data = list(zip(training_inputs, training_outputs))
test_inputs = [np.reshape(x, (784, 1)) for x in te_i]
test_data = list(zip(test_inputs, te_o))
return training_data, test_data
training_data, test_data = load_data_wrapper("../data/")
执行时,你可能会遇到下面的错误:
FileNotFoundError: [Errno 2] No such file or directory: '../data/t10k-images-idx3-ubyte'
这是因为python-mnist包中批量载入数据集时默认的文件名为t10k-images-idx3-ubyte,而从官网下载的数据集文件名为t10k-images.idx3-ubyte,因此只需要修改data
文件夹中的文件名即可成功运行。
构建神经网络
网络初始化
搭建网络的基本框架,包括神经网络各个层的数目,以及初始化参数。class Network(object): def __init__(self, sizes): """初始化神经网络 1. 根据输入,得到神经网络的结构 2. 根据神经网络的结构使用均值为0,方差为1的高斯分布初始化参数权值w和偏差b。 输入: sizes: list, 表示神经网络各个layer的数目,例如[784, 30, 10]表示3层的神经网络。 输入层784个神经元,隐藏层只有1层,有30个神经元,输出层有10个神经元。 """ self.num_layers = len(sizes) self.sizes = sizes self.biases = [np.random.randn(y, 1) for y in sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
随机梯度下降
def SGD(self, training_data, epochs, mini_batch_size, alpha, test_data=None): """随机梯度下降 输入: training_data:是由tuples ``(x, y)``组成的list,x表示输入,y表示预计输出 epoches:int, 表示训练整个数据集的次数 mini_batch_size: int, 在SGD过程中每次迭代使用训练集的数目 alpha: float, 学习速率 test_data: 是由tuples ``(x, y)``组成的list,x表示输入,y表示预计输出。 如果提供了``test_data``,则每经过一次epoch,都计算并输出当前网络训练结果在测试集上的准确率。 虽然可以检测网络训练效果,但是会降低网络训练的速度。 """ if test_data: n_test = len(test_data) m = len(training_data) for j in range(epochs): np.random.shuffle(training_data) mini_batches = [training_data[k:k+mini_batch_size] for k in range(0, m, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch(mini_batch, alpha) if test_data: print("Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test)) else: print("Epoch {0} complete".format(j))
更新权值 w 和偏差
b def update_mini_batch(self, mini_batch, alpha): """每迭代一次mini_batch,根据梯度下降方法,使用反向传播得到的结果更新权值``w``和偏差``b`` 输入: mini_batch: 由tuples ``(x, y)``组成的list alpha: int,学习速率 """ 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: delta_nabla_b, delta_nable_w = self.back_prop(x, y) 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_nable_w)] self.weights = [w-(alpha/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(alpha/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]
反向传播
def back_prop(self, x, y): """反向传播 1. 前向传播,获得每一层的激活值 2. 根据输出值计算得到输出层的误差``delta`` 3. 根据``delta``计算输出层C_x对参数``w``, ``b``的偏导 4. 反向传播得到每一层的误差,并根据误差计算当前层C_x对参数``w``, ``b``的偏导 输入: x: np.ndarray, 单个训练数据 y: np.ndarray, 训练数据对应的预计输出值 输出: nabla_b: list, C_x对``b``的偏导 nabla_w: list, C_x对``w``的偏导 """ nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # forward prop activation = x activations = [x] 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) # backward prop 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 range(2, self.num_layers): 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)
Cx 对 aL 的偏导
def cost_derivative(self, output_activations, y): """代价函数对a的偏导 输入: output_activations: np.ndarray, 输出层的激活值,即a^L y: np.ndarray, 预计输出值 输出: output_activations-y: list, 偏导值 """ return (output_activations-y)
准确率计算
def evaluate(self, test_data): """计算准确率,将测试集中的x带入训练后的网络计算得到输出值, 并得到最终的分类结果,与预期的结果进行比对,最终得到测试集中被正确分类的数目 输入: test_data: 由tuples ``(x, y)``组成的list 输出: int, 测试集中正确分类的数据个数 """ test_results = [(np.argmax(self.feed_forward(x)), y) for x, y in test_data] return sum(int(x==y) for (x, y) in test_results)
- 前馈
根据当前网络训练的结果,对数据 x <script type="math/tex" id="MathJax-Element-57">x</script>进行预测
def feed_forward(self, a): """前馈 输入: a:np.ndarray 输出: a:np.ndarray,预测输出 """ for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a
- 前馈
激活函数及其导数
def sigmoid(z): """The sigmoid function""" return 1.0/(1.0+np.exp(-z)) def sigmoid_prime(z): """Derivative of the sigmoid function""" return sigmoid(z)*(1-sigmoid(z))
训练
训练全部的数据需要一定的时间(在我实验室的老机器上用时3m32s,仅供参考),如果想要快速的查看训练结果,可以取部分训练集和测试集进行训练和测试。net = Network([784, 30, 10]) net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
输出: Epoch 0: 9121 / 10000 Epoch 1: 9271 / 10000 Epoch 2: 9317 / 10000 Epoch 3: 9371 / 10000 Epoch 4: 9362 / 10000 Epoch 5: 9395 / 10000 Epoch 6: 9393 / 10000 Epoch 7: 9475 / 10000 Epoch 8: 9473 / 10000 Epoch 9: 9473 / 10000 Epoch 10: 9450 / 10000 Epoch 11: 9466 / 10000 Epoch 12: 9477 / 10000 Epoch 13: 9497 / 10000 Epoch 14: 9475 / 10000 Epoch 15: 9477 / 10000 Epoch 16: 9481 / 10000 Epoch 17: 9483 / 10000 Epoch 18: 9498 / 10000 Epoch 19: 9471 / 10000 Epoch 20: 9488 / 10000 Epoch 21: 9486 / 10000 Epoch 22: 9465 / 10000 Epoch 23: 9461 / 10000 Epoch 24: 9499 / 10000 Epoch 25: 9496 / 10000 Epoch 26: 9501 / 10000 Epoch 27: 9498 / 10000 Epoch 28: 9499 / 10000 Epoch 29: 9506 / 10000
可以看到经过30轮的训练,准确率已经达到了95.06%(epoch 29)。作为第一次尝试,这个准确率已经非常令人满意了。
接下来我们增大隐藏层的层数,例如50,来重新训练,看看效果如何。隐藏层增加后,训练速度会变得更加缓慢(用时4m46s),在等待训练完成的过程中,可以去倒杯茶,放松一下身体。net = Network([784, 50, 10]) net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
输出: Epoch 0: 9176 / 10000 Epoch 1: 9307 / 10000 Epoch 2: 9406 / 10000 Epoch 3: 9433 / 10000 Epoch 4: 9476 / 10000 Epoch 5: 9508 / 10000 Epoch 6: 9499 / 10000 Epoch 7: 9502 / 10000 Epoch 8: 9528 / 10000 Epoch 9: 9533 / 10000 Epoch 10: 9569 / 10000 Epoch 11: 9573 / 10000 Epoch 12: 9559 / 10000 Epoch 13: 9592 / 10000 Epoch 14: 9566 / 10000 Epoch 15: 9588 / 10000 Epoch 16: 9575 / 10000 Epoch 17: 9588 / 10000 Epoch 18: 9584 / 10000 Epoch 19: 9587 / 10000 Epoch 20: 9583 / 10000 Epoch 21: 9607 / 10000 Epoch 22: 9589 / 10000 Epoch 23: 9595 / 10000 Epoch 24: 9605 / 10000 Epoch 25: 9600 / 10000 Epoch 26: 9600 / 10000 Epoch 27: 9595 / 10000 Epoch 28: 9592 / 10000 Epoch 29: 9599 / 10000
观察结果可以发现准确率上升到了96.07%(epoch 21)。在这个实例下,增加隐藏层提高了训练的准确率。但是并非一直如此,在后续的文章中,我将继续介绍如何提高网络的训练速度和训练效果。
参考文献
Michael A. Nielsen, “Neural network and deep learning”, Determination Press, 2015
作者:mrpanc
博客:http://blog.csdn.net/peter_cpan
Github:https://github.com/mrpanc
2018年1月17号