利用 Python 搭建起了一个简单的神经网络模型,并完成识别手写数字。
这里使用scikit-learn库内建的手写数字字符集作为本文的数据集。scikit-learn库是一个经典的机器学习库,在使用前需要安装其库和其他依赖库。
主要包括:numpy、scipy、matplotlib、jupyter、pandas、seaborn。
例如:pip install numpy
这里有一点需要注意,在国内使用原始源下载第三方库,下载速度特别慢,甚至有可能会出现下载失败的情况。所以在下载第三方库时,一般会选择换国内源。换国内源一般有两种方式:临时方式和永久方式。
阿里云:http://mirrors.aliyun.com/pypi/simple/
豆瓣:http://pypi.douban.com/simple/
USTC:https://pypi.mirrors.ustc.edu.cn/simple/
THU:https://pypi.tuna.tsinghua.edu.cn/simple/
换国内清华源
pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple/
我这里使用的是Mac,所以本文主要的环境展示基本是基于Mac系统的。
1.打开终端 cd ~
2.查看是否存存在.pip文件夹。ls -a
3.如果不存在就创建。mkdir .pip。然后在.pip文件夹下创建pip.conf配置文。touch pip.conf
永久替换为阿里云源
[global]
index-url=http://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host=mirrors.aliyun.com
以阿里源为例,进入到 C:\Users
打开 pip.ini,输入下面的内容:
[global]
index-url=http://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host=mirrors.aliyun.com
学习机器学习时,较为常见的方式是使用jupyter,所以本文也会使用该工具,该工具使用较为简单,这里不多介绍了。
# 打开jupyter
jupyter notebook
我们先来查看一下需要使用到的数据集。该数据集包含由 1797 张数字 0 到 9 的手写字符影像转换后的数字矩阵,目标值是 0-9。
# 导入数据集
from sklearn import datasets
digits = datasets.load_digits()
digits
属性 | 描述 |
---|---|
images | 8x8 矩阵,记录每张手写字符图像对应的像素灰度值 |
data | 将 images 对应的 8x8 矩阵转换为行向量 |
target | 记录 1797 张影像各自代表的数字 |
根据灰度值矩阵,使用 Matplotlib 把字符对应的灰度图像和标签显示出来看看。在jupyter中需要添加%matplotlib inline,pycharm里面则不需要。
# 根据灰度值矩阵,使用 Matplotlib 把字符对应的灰度图像和标签
from matplotlib import pyplot as plt
%matplotlib inline
image1 = digits.images[0]
print("标签为:", digits.target[0])
plt.imshow(image1, cmap=plt.cm.gray_r)
从图中可以看到,我们需要识别的图片是 8×8 的灰度图,它们的标签和图片内容一一对应。
神经元间的连接线上有权重w 。神经网络工作时,将前一层神经元的输出与权重w相乘再加上一个偏移量bias得到的结果,传递给下一层神经元。即有:
w11∗al+w12∗a2+w13∗a3+bias1=b1
w21∗al+w22∗a2+w23∗a3+bias2=b2
本质上讲,神经网络就是随便给定一组w和bias,再判断在该w,bias条件下模型的好坏,再通过一定的算法对w和bias进行更新。如此循环,直到求出最佳的w矩阵和 bias矩阵的值。求取这些参数的过程其实就是模型的训练(学习)过程。
我们把数据在网络层中从左到右计算的过程称之为正向传播。
import numpy as np
class FullyConnect:
# 传入参数 len_x 为输入数据的特征长度(也就是第一层的神经元个数)
# len_y 为输出数据的个数(也就是下一层的神经元个数)
def __init__(self, len_x, len_y):
# m 个神经元的网络层到n个神经元的网络层之间的 w 矩阵的大小为( n*m )
self.weights = np.random.randn(len_y, len_x) / np.sqrt(len_x)
self.bias = np.random.randn(len_y, 1) # 使用随机数初始化参数,bias 的个数之后输出层的个数有关
self.lr = 0 # 先将学习速率初始化为 0 ,最后统一设置学习速率
# 全连接的正向传播过程,输入的便是训练数据
def forward(self, x):
self.x = x # 把中间结果保存下来,以备反向传播时使用
# 计算全连接层的输出,也就是上面矩阵乘法公式的代码表示
self.y = np.array([np.dot(self.weights, xx) + self.bias for xx in x])
return self.y # 将这一层计算的结果向前传递
对于神经网络来说,一条样本只能占一行,因此这里我们需要把大小 8×8 的图片转换成一个行向量传入神经网络中。DIGITS 数据集中的 data 属性已经为我们做好了这一点。
# 前2张图片的行向量
digits.data[0:2]
接下来,我们把前两个行向量传入全连接中层,并且输出全连接层的预测结果。
fully_connet = FullyConnect(64, 1) # 传入网络层1,网络层2的长度
full_result = fully_connet.forward(digits.data[0:2])
full_result # 这里只传入两条数据用于测试。得到一次正向传播后,两张图片的预测值
实际运用当中,有多种激活函数可以选择,你甚至可以自己定义一个属于自己的激活函数。这里我们使用最经典的一种激活函数:Sigmoid 激活函数。将全连接输出的数据z,放入激活函数中,最终得到该神经元的输出。
class Sigmoid:
def __init__(self): # 无参数,不需初始化
pass
# 这里输入的变量的x
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
# 完成正向传播,将输入的z ,放入 Sigmoid 函数中,最终得到结果 h,并返回
def forward(self, x):
self.x = x
self.y = self.sigmoid(x)
return self.y
很多神经网络其实就是让数据不断的通过全连接层和激活函数层,最终得到预测结果。那么问题来了,得到预测结果后,如何说明当前状态下的模型是优还是劣呢?神经网络是否还需要继续训练下去呢?为此,我们引入了损失函数的概念。
损失函数,就是模型预测出来的标签与真实标签的差异。而定义这种差异的函数,就被称为损失函数。深度学习的训练过程其实就是求解损失函数最小值的过程。比如计算真实值和预测值之间的绝对误差,当得到的值比较大时,就说明该神经网络的输出与预期的正确输出偏差较大。反之,如果得到的值很小甚至等于 0 ,就说明我们的模型工作的不错,能够正确的预测输出值。
实际上,现在已经有很多种损失函数供我们选择,这里使用一种最经典的损失函数:二次损失函数(Quadratic Loss Function)。
生活中标签 的形式各种各样,有可能是预测天气的阴天,晴天,雨天等标签,也可能是预测字母的 a,b,c等。而如何将这些标签转换成计算机能够识别的标签呢?有很多种方式,比如十进制。但是如果使用十进制来表示这些离散标签的话,会有一个缺点。假设我把 0 当做晴天,1 当做雨天,2 当做阴天。那么在计算损失时,(晴天,阴天)的损失和(晴天,雨天)的损失会不同。可他们都是把标签预测错了,没有理由让他们的损失不同。因此便提出了独热编码的概念。
独热编码:数字的每一位只有 0 和 1 的取值,且每一个都代表一个标签,如果这位取1,其他位则必须为0。如下图所示:
当第 0 位为 1 ,其他位为 0 的时候,则表示晴天。当第 1 位为 1,其他位为 0 的时候,则表示雨天,其他的标签同理。这里把他们看做向量坐标,则晴天与阴天的距离和晴天与雪天的距离都为 1。这样计算出来的损失也就相等了。
# 利用 Python 实现二次损失函数层
class QuadraticLoss:
def __init__(self):
pass
# 传入的参数,第一个参数为预测出来的标签值,第二个参数为实际标签值
def forward(self, x, label):
# 将真实 label 转换成独热编码
self.x = x
# 由于我们的label本身只包含一个数字,我们需要将其转换成和模型输出值尺寸相匹配的向量形式
self.label = np.zeros_like(x)
for a, b in zip(self.label, label):
a[b] = 1.0 # 只有正确标签所代表的位置概率为1,其他为 0
# 计算损失
self.loss = np.sum(np.square(x - self.label)) / \
self.x.shape[0] / 2 # 求平均后再除以 2 是为了表示方便
return self.loss
接下来,我们初始化上面所说的晴天,雨天,阴天,雪天等四种天气。然后,利用所写损失函数,观察(阴天,雪天)的损失与(雨天,雪天)的损失是否相同。
# 测试
loss = QuadraticLoss()
# 假设神经网络算出样本的预测值为0,即为雪天
pred = np.zeros((1, 4))
pred[0][0] = 1
print("实际为阴天和预测值为雪天的平均损失是:", loss.forward(pred, [1]))
print("实际为雨天和预测值为雪天的平均损失是:", loss.forward(pred, [2]))
从结果可以看出,通过独热编码后的(阴天,雪天)的损失与(雨天,雪天)的损失相同。
class Accuracy:
def __init__(self):
pass
def forward(self, x, label): # 只需forward
self.accuracy = np.sum(
[np.argmax(xx) == ll for xx, ll in zip(x, label)]) # 对预测正确的实例数求和
self.accuracy = 1.0 * self.accuracy / x.shape[0] # 也就是计算正确率
return self.accuracy
使用这些网络层构建出一个完整的神经网络的正向传播。并传入需要预测的数据集,进行一次正向传播,查看输出结果。
# 图片大小为 8*8
# 则此时一张图片就是一条数据,每张图片对应一个 label(0-9范围内)
x = digits.data
print(x[0])
labels = digits.target
print(labels[0])
# 开始搭建神经网络
inner_layers = []
inner_layers.append(FullyConnect(8 * 8, 10))
inner_layers.append(Sigmoid())
# 神经网络搭建完成
losslayer = QuadraticLoss() # 计算损失
accuracy = Accuracy() # 计算准确率
# 开始将数据送入神经网络进行正向传播
for layer in inner_layers: # 前向计算
x = layer.forward(x)
loss = losslayer.forward(x, labels) # 调用损失层forward函数计算损失函数值
accu = accuracy.forward(x, labels)
print('loss:', loss, 'accuracy:', accu)
一次的正向传播之后,模型的损失很大,正确率接近为 0 。那么有没有什么办法可以减少损失进而提高正确率呢?这里我们使用一种求解损失最小值的方法:梯度下降算法。基本做法就是反向传播。
class QuadraticLoss:
def __init__(self):
pass
# 正向传播和上文一样
def forward(self, x, label):
self.x = x
self.label = np.zeros_like(x)
for a, b in zip(self.label, label):
a[b] = 1.0
self.loss = np.sum(np.square(x - self.label)) / \
self.x.shape[0] / 2 # 求平均后再除以2是为了表示方便
return self.loss
# 定义反向传播
def backward(self):
# 这里的dx,就是我们求得函数关于x偏导数,也就是梯度,将它保存起来,后面更新的时候会用到
self.dx = (self.x - self.label) / self.x.shape[0] # 2被抵消掉了
return self.dx
# 激活函数的反向传播
class Sigmoid:
def __init__(self): # 无参数,不需初始化
pass
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
self.x = x
self.y = self.sigmoid(x)
return self.y
def backward(self, d):
sig = self.sigmoid(self.x)
self.dx = d * sig * (1 - sig)
return self.dx # 反向传递梯度
这个过程也是最重要的过程,他将接收激活函数层传递过来的,处理后的损失误差。而这一层也将通过损失误差,计算相应的参数 , 的梯度 , 。
# 我们开始改写全连接层,并且最后利用梯度下降对参数进行更新。
class FullyConnect:
def __init__(self, l_x, l_y): # 两个参数分别为输入层的长度和输出层的长度
# 使用随机数初始化参数,请暂时忽略这里为什么多了np.sqrt(l_x)
self.weights = np.random.randn(l_y, l_x) / np.sqrt(l_x)
self.bias = np.random.randn(l_y, 1) # 使用随机数初始化参数
self.lr = 0 # 先将学习速率初始化为0,最后统一设置学习速率
def forward(self, x):
self.x = x # 把中间结果保存下来,以备反向传播时使用
self.y = np.array([np.dot(self.weights, xx) +
self.bias for xx in x]) # 计算全连接层的输出
return self.y # 将这一层计算的结果向前传递
def backward(self, d):
# 根据链式法则,将反向传递回来的导数值乘以x,得到对参数的梯度
ddw = [np.dot(dd, xx.T) for dd, xx in zip(d, self.x)]
# 每一条数据都能求出一个ddw,然后对他们取一个平均,得到平均的梯度变化
self.dw = np.sum(ddw, axis=0) / self.x.shape[0]
self.db = np.sum(d, axis=0) / self.x.shape[0]
self.dx = np.array([np.dot(self.weights.T, dd) for dd in d])
# 利用梯度下降的思想,更新参数。这里的lr就是步长的意思
self.weights -= self.lr * self.dw
self.bias -= self.lr * self.db
return self.dx # 反向传播梯度
这里我们将数据的前 1500 条作为训练数据,后面的作为测试数据。得到如下数据集:
# 划分数据集
train_data,train_target = digits.data[:1500],digits.target[:1500]
test_data,test_target = digits.data[1500:-1],digits.target[1500:-1]
train_data.shape,train_target.shape,test_data.shape,test_target.shape
接下来,我们利用上面所写的网络层,搭建一个用于数字识别的网络结构。该网络结构由(全连接层,激活函数层,全连接层,激活函数)组成。具体代码如下:
inner_layers = []
inner_layers.append(FullyConnect(64, 60)) # 因为每条数据的长度为 8*8=64,因此这里第一个全连接层,接收长度为64
inner_layers.append(Sigmoid())
inner_layers.append(FullyConnect(60, 10))
inner_layers.append(Sigmoid())
inner_layers
接下来,初始化损失函数,准确率函数,学习率以及迭代次数。
# 接下来,初始化损失函数,准确率函数,学习率以及迭代次数。
losslayer = QuadraticLoss()
accuracy = Accuracy()
for layer in inner_layers:
layer.lr = 1000 #所有中间层设置学习速率
epochs = 150 # 对训练数据遍历的次数,也就是学习时间。
#在开始的时候,准确率会随之学习时间的增加而提高。
#当模型学习完训练数据中的所有信息后,准确率就会趋于稳定
losslayer,accuracy,epochs
最后,对模型进行训练。且每训练10次,则输出一次测试结果。
for i in range(epochs):
losssum = 0
iters = 0
x = train_data
label = train_target
x = x.reshape(-1,64,1)
for layer in inner_layers: # 前向计算
x = layer.forward(x)
loss = losslayer.forward(x, label) # 调用损失层forward函数计算损失函数值
losssum += loss
iters += 1
d = losslayer.backward() # 调用损失层backward函数层计算将要反向传播的梯度
for layer in inner_layers[::-1]: # 反向传播
d = layer.backward(d)
if i%10==0:
x = test_data
label = test_target
x = x.reshape(-1,64,1)
for layer in inner_layers:
x = layer.forward(x)
accu = accuracy.forward(x, label) # 调用准确率层forward()函数求出准确率
print('epochs:{},loss:{},test_accuracy:{}'.format(i,losssum / iters,accu))
完整demo代码下载地址:完整代码下载