利用 Python 搭建起了一个简单的神经网络模型,并完成识别手写数字。

1.前置工作

1.1 环境配置

这里使用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
Windows下替换永久国内源

以阿里源为例,进入到 C:\Users<Your_Username>\AppData\Roaming 目录下,创建一个 pip 文件夹,并在该文件夹中新建一个文件 pip.ini。
打开 pip.ini,输入下面的内容:

[global]
    index-url=http://mirrors.aliyun.com/pypi/simple/
[install]
    trusted-host=mirrors.aliyun.com

1.2 准备数据集

学习机器学习时,较为常见的方式是使用jupyter,所以本文也会使用该工具,该工具使用较为简单,这里不多介绍了。

# 打开jupyter 
jupyter notebook

我们先来查看一下需要使用到的数据集。该数据集包含由 1797 张数字 0 到 9 的手写字符影像转换后的数字矩阵,目标值是 0-9。

# 导入数据集
from sklearn import datasets

digits = datasets.load_digits()
digits

在这里插入图片描述
加载完成的 DIGITS 数据集中包含 3 个属性:

属性描述
images8x8 矩阵,记录每张手写字符图像对应的像素灰度值
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 的灰度图,它们的标签和图片内容一一对应。

2.人工神经网络

2.1 神经网络全连接层

在这里插入图片描述
神经元间的连接线上有权重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  # 这里只传入两条数据用于测试。得到一次正向传播后,两张图片的预测值

在这里插入图片描述
上面的结果是一次前向传播计算后的预测结果。

2.2 激活函数

在这里插入图片描述
实际运用当中,有多种激活函数可以选择,你甚至可以自己定义一个属于自己的激活函数。这里我们使用最经典的一种激活函数: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

使用matplotlib画出该激活函数的图像。
在这里插入图片描述

2.3 损失函数

很多神经网络其实就是让数据不断的通过全连接层和激活函数层,最终得到预测结果。那么问题来了,得到预测结果后,如何说明当前状态下的模型是优还是劣呢?神经网络是否还需要继续训练下去呢?为此,我们引入了损失函数的概念。

损失函数,就是模型预测出来的标签与真实标签的差异。而定义这种差异的函数,就被称为损失函数。深度学习的训练过程其实就是求解损失函数最小值的过程。比如计算真实值和预测值之间的绝对误差,当得到的值比较大时,就说明该神经网络的输出与预期的正确输出偏差较大。反之,如果得到的值很小甚至等于 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]))

在这里插入图片描述
从结果可以看出,通过独热编码后的(阴天,雪天)的损失与(雨天,雪天)的损失相同。

2.4 准确率函数

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  # 反向传递梯度

2.5 全连接层的反向传播

这个过程也是最重要的过程,他将接收激活函数层传递过来的,处理后的损失误差。而这一层也将通过损失误差,计算相应的参数 𝑤 , 𝑏 的梯度 𝑑𝑤 , 𝑑𝑏 。

# 我们开始改写全连接层,并且最后利用梯度下降对参数进行更新。
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  # 反向传播梯度

3.训练神经网络

这里我们将数据的前 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代码下载地址:完整代码下载

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐