您的位置:首页 > 理论基础 > 计算机网络

神经网络理论介绍及实现

2018-01-17 19:02 197 查看

神经网络架构

假设我们的神经网络结构如下图所示,每一个圆都代表一个神经元,第一层被称为输入层,最后一层被称为输出层,位于中间的被称为隐藏层。输入层和输出层的设计往往是非常直接的。以我们需要学习的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最小。因此我们需要计算∂C∂w和∂C∂b,从而结合梯度下降算法求得C的最小值。接下来将以计算∂C∂w为例进行说明。

计算输出层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=∂C∂aL∂aL∂zL.(3)

同理,隐藏层之间也有类似的共有部分,例如我们可以用δL−1来计算隐藏层最后一层的前一层的偏导,表达式如公式(4)所示。

δL−1=∂C∂aL∂aL∂zL∂zL∂aL−1∂aL−1∂zL−1=δL∂zL∂aL−1∂aL−1∂zL−1.(4)

通过公式(3),(4),公式(1),(2)可以改写为以下形式:

∂C∂wL∂C∂wL−1==δL∂zL∂wL,δL−1∂zL−1∂<
2f3d8
span style="position: absolute; clip: rect(1.952em 1000em 2.702em -0.447em); top: -2.548em; left: 0.003em;">wL−1.(5)(6)

假设激活函数为σ(z),对公式(3)∼(6)进行详细的计算。

δLδL−1∂C∂wL∂C∂wL−1===========∂C∂aL∂aL∂zL∇aC⋅σ′(zL),δL∂zL∂aL−1∂aL−1∂zL−1δL∂(wLaL−1+bL)∂aL−1σ′(zL−1)δLwLσ′(zL−1),δL∂zL∂wLδL∂(wLaL−1+bL)∂wLδLaL−1,δL−1∂zL−1∂wL−1δL−1∂(wL−1aL−2+bL−1)∂wL−1δL−1aL−2.(7)(8)(9)(10)

按照相同的原理,我们可以推得:

∂C∂bL=δL∂zL∂bL=δL∂(wLaL−1+bL)∂bL=δL.(11)

将公式(9),(10)合并,并用l,l+1分别替换L−1,L,则公式(7)∼(11)可总结为以下三个式子:

δl={∇aC⋅σ′(zL),δl+1wl+1σ′(zl),if l=L,if l∈{L−1,L−2,⋯,2},∂C∂wl=δlal−1,l∈{L,L−1,⋯,2},∂C∂bl=δl,l∈{L,L−1,⋯,2}.(12)(13)(14)

观察公式(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进行预测

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号
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息