这是我参与11月更文挑战的第5天
感知器
神经网络可以简单理解成由人工神经元构成的网络,感知器是一种人工神经元,其简单形式如下所示:
例子中感知器有三个输入,输出为一个0或1的数,每个连接会有一个权重,若参数和权重的和大于某个阈值则输入1,否则输出0,形式化表示如下所示:
从公式(1)可知通过改变阈值和权重可以调整得到不同的决策模型。
上面例子仅是简单一个的感知器,更复杂的由多个感知器组成的网络如下所示:
- 第一层感知器:根据输入作出三个简单的判断
- 第二层感知器:通过第一层感知器的判断结果作出下一个判断,第二层感知器比第一层作出的决策更加复杂和抽象。 以此类推,层数越多感知器的决策越抽象且复杂。
修改式(1)用向量点乘表示,即,将用表示,得到:
将用(偏置)替代后,可以将其看为让感知器有多容器输出1的参数(即值越大感知器越容易输出1)。
神经网络学习算法的核心就是根据目标函数来自动调整人工神经元的权重和偏置,不需要人工的直接干预,这是人工神经元根本区别于传统逻辑门的地方。
S型神经元
在学习中通常希望对权重或偏置这类参数微小的调节仅会微小地改变输出结果,然而从公式(2)可以看出感知器这类人工神经元很难满足这个要求,改变参数可能一下将输出结果从0变为1(因为感知器只有两个输出值),因此引入了一种称为S型神经元的人工神经元来改进这一点。
在S型神经元中引入了S型函数,定义为:
S型神经元的输出变为:
S型函数的大致形状如下图所示:
,当其值很大时,函数接近于1,其值很小时接近于0。
若将S型函数转化为阶跃函数如下所示:
可以看出这实际上就是感知器,也就是说S型神经元实际上是感知器的平滑版本,从而实现微调参数可以微调输出结果。
输出的变化量可以表示为:
神经网络架构
神经网络基础架构如下所示:
最左边称为输入层,其中的神经元称为输入神经元,最右边为输出层,其中神经元称为输出神经元,中间的层统称为隐含层。
分类手写数字网络
识别手写数字可以分为两个问题,一是将一串数字分割为单个数字,另一个是将单独数字分类完成识别,这里只解决第二个问题。使用如下所示的三层神经网络来识别一个数字:
- 输入层:设定网络输入的手写数字图像大小为:像素,输入像素是灰度级的,0.0表示白色,1.0表示黑色,中间数值表示渐变的灰色,则输入层应包含 个神经元。
- 隐含层:用表示隐含层神经元的数量,是可调参数,图中 。
- 输出层:包含10个神经元,从 ,哪个神经元被激活则表示分到哪个类。
梯度下降算法学习参数
训练数据集为著名的MNIST数据集。
网络的输入为,维度为784,期望输出为10维向量,即我们希望。例如对于数字为6的输入,网络的期望输出为。
为了量化神经网络的学习目标,定义目标函数:
表示网络中所有权重,表示所有偏置,表示输入数据的总数,表示当输入为时输出的向量,表示对所有输入数据求和,表示取向量的模,这种目标函数是设置方法称为均方误差。直观地理解就是,当输入时网络的输出为,与相差越小则的值越小,因此学习的目的就是找到合适的,令。
这个问题转化为了求解,当:
我们可以采用梯度下降法来求解这个最小化问题,梯度下降法的原理很简单,即每次将两个参数值向函数值变化为负的方向移动直到到达极值点。
使用来表示函数的梯度向量:
由前文可知,可以表示为:
问题转化为了如何选取 使得一定为负。引入学习速率 ,令:
由于是一个很小的正数,因此可以得到:
,因此可以得到恒为负值的,且通过调节的值就能调节梯度下降的速率。下图展示了梯度下降法的几何表达
综上,得到了两个参数的变化选择方法:
反复重复上述步骤直到找到函数的极小值。
随机梯度下降
注意到上述方法的学习过程实际上的很耗时的,先前提到过目标函数,令,得到,在计算中需要对每个输入的计算其梯度值然后求平均值,这使得计算量非常大,学习变得缓慢。
因此引入了随机梯度下降算法来解决加速学习的速度,其核心思想在于随机选取小规模样来计算,在通过小样本计算出来的梯度值估算所有样本的梯度值。
选取一个小批量数据(mini-batch),包含样本数为,在足够大的情况下可以得到:
梯度下降法中每次迭代都重新挑选一批小批量数据训练,直到用完了所有训练集样本,这称为完成了一个训练迭代期(epoch),然后再重新开始新的迭代期。
代码实现
构造神经网络
import numpy as np
import random
# sigmoid函数
def sigmoid(z):
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
# sigmoid函数的导数
return sigmoid(z)*(1-sigmoid(z))
# 定义网络类
# net = Network([2, 3, 1])
# 创建一个第一层2个神经元 第二层 3个神经元 第三层 1个神经元的网络
class Network:
def __init__(self,sizes):
# 获得网络的层数
self.num_layers=len(sizes)
self.sizes = sizes
# 随机初始化偏置和权重
# 第⼀层神经元是⼀个输⼊层不设置偏置,因此从1开始
# np.random.randn randn函数返回一个或一组样本,具有标准正态分布
# np.random.randn(y,1)返回y行1列的数据
self.biases=[np.random.randn(y,1)for y in sizes[1:]]
# zip函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
self.weights = [np.random.randn(y,x)
for x,y in list(zip(sizes[:-1],sizes[1:]))]
# feedforward方法,对于网络给定输入a返回对应输出
def feedforward(self,a):
for b,w in list(zip(self.biases,self.weights)):
#np.dot 矩阵乘法
a = sigmoid(np.dot(w,a)+b)
return a
# 随机梯度下降算法
def SGD(self,training_data,epochs,mini_batch_size,eta,test_data=None):
if test_data:n_test=len(test_data)
# training_data 为一个(x,y)的列表,x表示训练的输入样本的特征,y表示对应期望输出(标签)
# n为训练集样本数
n = len(training_data)
# epochs和mini_batch_size表示迭代期数量,和采样时的⼩批量数据的⼤⼩
for j in range(epochs):
# 随机打乱训练数据
random.shuffle(training_data)
# 从训练集中取k到k+mini_batch_size范围的数据作为mini_batches中的数据
mini_batches=[
training_data[k:k+mini_batch_size]
# range(0,n,mini_batch_size)
# k的取值范围从0开始到n,步长为nimi_batch_size
for k in range(0,n,mini_batch_size)
]
# eta 学习速率
for mini_batch in mini_batches:
# update_mini_batch 对于每⼀个 mini_batch计算⼀次梯度下降
self.update_mini_batch(mini_batch, eta)
# 若设置了test_data则每次迭代测试后评估一次网络,有助于追踪进度但会延缓执行速度
if test_data:
print ("Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test))
else:
print("Epoch {0} complete".format(j))
# 对每个mini_batch使用反向传播梯度下降方法更新网络中的权重和偏置
def update_mini_batch(self, mini_batch, eta):
# np.zeros返回来一个给定形状和类型的用0填充的数组
# .shape可以快速读取矩阵的形状
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:
# self.backprop 反向传播算法,⼀种快速计算代价函数的梯度的⽅法
delta_nabla_b,delta_nabla_w=self.backprop(x,y)
# 计算两个参数的梯度向量
nabla_b = [nb + dnb for nb, dnb in list(zip(nabla_b, delta_nabla_b))]
nabla_w = [nw + dnw for nw, dnw in list(zip(nabla_w, delta_nabla_w))]
# 更新两个参数的值
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)]
# 反向传播算法
# 返回一个元祖,nabla_b, nabla_w表示损失函数C_x的梯度
def backprop(self,x,y):
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] #存储所有激活值的列表,一层层
zs = [] # 存储所有z向量的列表,一层层
for b, w in list(zip(self.biases, self.weights)):
z = np.dot(w, activation) + b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
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
def evaluate(self, test_data):
"""Return the number of test inputs for which the neural
network outputs the correct result. Note that the neural
network's output is assumed to be the index of whichever
neuron in the final layer has the highest activation."""
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)
def cost_derivative(self, output_activations, y):
"""Return the vector of partial derivatives \partial C_x /
\partial a for the output activations."""
return output_activations - y
读取数据集,并转化为合适的格式
import pickle
import gzip
import numpy as np
# load_data返回MNIST数据为一个元祖包括training data、validation data和 test data
# training data为一个包含两个实体的元祖,第一个实体包含实际训练图像。一个numpy ndarray包含50000个实体
# 每个实体依次是一个包含784个值的numpy ndarray,也就是说一个MNIST图片为28X28=784个像素
# 第二个实体是一个包含50000个实体的numpy ndarray,这些实体的取值范围为0-9的实数表示第一个实体所属分类
# validation data和test data是一样的,每个包含10000张图片
def load_data():
# Gzip模块为GNU zip文件提供了一个类文件的接口,它使用zlib来压缩和解压缩数据文件,读写gzip文件
f = gzip.open('mnist.pkl.gz', 'rb')
# pickle.load 从f中读取一个字符串,并将它重构为原来的python对象
training_data, validation_data, test_data = pickle.load(f,encoding='bytes')
f.close()
return training_data, validation_data, test_data
# load_data_wrapper返回一个元祖包含(training_data, validation_data, test_data),将数据格式转化为更易于我们构建网络使用的格式
# training data为一个列表包含50000个二元组:(x,y),x为一个784维的numpy.ndarray,为输入图片
# y为一个10维的numpy.ndarray表示x对应的分类
# validation data和test data 为一个列表包含10000个二元组:(x,y)
def load_data_wrapper():
tr_d, va_d, te_d = load_data()
# np.reshape 在不改变数据内容的情况下,改变一个数组的格式 x为需要处理的数据,改变为(784,1),784行1列
training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
training_results = [vectorized_result(y) for y in tr_d[1]]
training_data = list(zip(training_inputs, training_results))
validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
validation_data = list(zip(validation_inputs, va_d[1]))
test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
test_data = list(zip(test_inputs, te_d[1]))
return training_data, validation_data, test_data
#返回一个10维的单位向量,在第j位设置1.0,其他为0,用来表示网络的分类(0-9)
def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1.0
return e
测试
import handwrite
import mnist_loader
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = handwrite.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
测试结果
Epoch 0: 8917 / 10000
Epoch 1: 9193 / 10000
Epoch 2: 9305 / 10000
Epoch 3: 9301 / 10000
Epoch 4: 9338 / 10000
Epoch 5: 9366 / 10000
Epoch 6: 9356 / 10000
Epoch 7: 9425 / 10000
Epoch 8: 9405 / 10000
Epoch 9: 9415 / 10000
Epoch 10: 9402 / 10000
Epoch 11: 9448 / 10000
Epoch 12: 9437 / 10000
Epoch 13: 9426 / 10000
Epoch 14: 9466 / 10000
Epoch 15: 9414 / 10000
Epoch 16: 9439 / 10000
Epoch 17: 9432 / 10000
Epoch 18: 9473 / 10000
Epoch 19: 9462 / 10000
Epoch 20: 9456 / 10000
Epoch 21: 9471 / 10000
Epoch 22: 9485 / 10000
Epoch 23: 9460 / 10000
Epoch 24: 9472 / 10000
Epoch 25: 9492 / 10000
Epoch 26: 9492 / 10000
Epoch 27: 9475 / 10000
Epoch 28: 9508 / 10000
Epoch 29: 9470 / 10000
可以看到最终测试结果准确率接近95%。