自己动手写神经网络

0 阅读3分钟

《自己动手写神经网络》旨在通过带领读者实现一个迷你神经网络,使读者对权重、阈值、特征提取、梯度下降、误差反向传播法等有个感性的认识,为学习深度学习打下坚实的基础。该神经网络用于识别图像中的数字是 0 还是 1,后文称作 0/1 网络,源码仓库如下:

https://github.com/pandengyang/polly.git

本教程也是笔者学习《深度学习中的数学》的学习笔记。

目录

  • 0 0/1 网络
  • 1 从一个物理实验谈起
    • 1.1 误差
    • 1.2 最小二乘法
    • 1.3 导数
    • 1.4 数据拟合
  • 2 神经网络
    • 2.1 神经元
    • 2.2 网络结构
    • 2.3 输入层
    • 2.4 隐藏层
    • 2.5 输出层
    • 2.6 激活函数
  • 3 矩阵
    • 3.1 矩阵运算
      • 3.1.1 加法
      • 3.1.2 减法
      • 3.1.3 数乘
      • 3.1.4 矩阵乘法
      • 3.1.5 转置
    • 3.2 NumPy
  • 4 训练
    • 4.1 加载数据集
    • 4.2 参数初始化
    • 4.3 激活函数
    • 4.4 正向传播
    • 4.5 损失函数
    • 4.6 梯度下降法
    • 4.7 向量
    • 4.8 链式法则
      • 4.8.1 标量对矩阵的链式法则
      • 4.8.2 逐元素运算的链式法则
      • 4.8.3 矩阵乘法的链式法则
      • 4.8.4 广播的链式法则
    • 4.9 误差反向传播法
    • 4.10 参数存储
  • 5 推理
    • 5.1 加载模型参数
    • 5.2 加载测试数据集
    • 5.3 设置激活函数
    • 5.4 正向传播
    • 5.5 预测结果

0 0/1 网络

0/1 网络 的数据集为 64 张图像,图像内容为数字 01。图像分辨率为 3×43 \times 4 像素,每个像素为黑色或白色。图像内容示例如下:

数字图像零.png

数字图像壹.png

图像文件名为 {sn}-{y}.bmp。文件名示例如下:

  • 1-0.bmp
  • 23-0.bmp
  • 64-1.bmp

sn 为图像编号,取值范围为 1~64y 为图像内容,取值范围为 01

0/1 网络 为图像的每个像素分配了一个坐标。坐标示例如下:

数字图像像素坐标.png

本教程利用 64 张图像训练 0/1 网络,使其能识别图像中的数字 01

本项目在 Python 3.12 上测试,依赖 numpypillow 库。环境搭建命令如下:

python -m pip install numpy pillow

1 从一个物理实验谈起

温度传感器利用电阻随温度升高而稳定增加的特性来精确测温。科学家需要准确地知道电阻随温度变化的公式,这通常根据实验数据得出。假设科学家拿到了一组实验数据,记录了不同温度下某导体的电阻值。实验数据如下:

温度电阻
1122.1
1223.0
1324.2
1425.1
1526.0
1627.1
1728.0
1829.2
1930.1
2031.0

以温度为横坐标,电阻为纵坐标,在坐标纸上绘制出这些点。温度与电阻关系散点图如下:

温度与电阻关系散点图.png

上图为温度与电阻关系的散点图,可以直观地看到数据点几乎排成一条直线,说明电阻随温度呈线性变化趋势。

在坐标纸上绘制一条贴近上述坐标点的直线,量出其与 x 轴、y 轴的交点坐标。拟合直线如下:

拟合直线.png

依据交点坐标,即可求出电阻随温度变化的公式。

理论上,依据上述温度与电阻关系散点可绘制出无数条直线(只需稍微旋转或平移即可),每一条都能得到一个公式。那么,哪一条才是最准确的呢?

1.1 误差

要判断公式准不准,需要一个衡量标准。最直观的就是看每个点的真实电阻值和公式预测的电阻值相差多少,这个差值叫做误差。

如果直接用 真实值 – 预测值,那么有的点预测值偏大,误差为负;有的点预测值偏小,误差为正。正负相抵后,总误差可能很小,无法衡量公式准不准,所以需要让误差总是正的。这通常有两种方法:

  • 绝对值:真实值预测值|{\text{真实值}} - {\text{预测值}}|
  • 平方:(真实值预测值)2({\text{真实值}} - {\text{预测值}})^2

平方不仅能消除正负,还能放大误差(比如误差 2 变成 4 ,误差 3 变成 9),让大误差点更显眼,优化时会优先照顾它们。所以通常选用平方误差,把所有点的平方误差加起来,就是总误差。总误差公式如下:

E=i=1N(yiy^i)2E = \sum_{i=1}^N (y_i - \hat{y}_i)^2

其中 yiy_i 是真实电阻,y^i\hat{y}_i 是公式预测的电阻。通常还会将总误差除以数据量,以消除数据量对误差的影响,这就是均方误差。均方误差公式如下:

MSE=1Ni=1N(yiy^i)2MSE = \frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)^2

1.2 最小二乘法

既然误差越小,直线就越准,那问题就变成了:找一条直线 y=ax+by = ax + b,使得均方误差最小。这就是最小二乘法的核心思想。将真实电阻、预测电阻代入均方误差公式,数据如下:

温度真实电阻 yiy_i预测电阻 y^i\hat{y}_i
1122.1a×11+ba \times 11 + b
1223.0a×12+ba \times 12 + b
1324.2a×13+ba \times 13 + b
1425.1a×14+ba \times 14 + b
1526.0a×15+ba \times 15 + b
1627.1a×16+ba \times 16 + b
1728.0a×17+ba \times 17 + b
1829.2a×18+ba \times 18 + b
1930.1a×19+ba \times 19 + b
2031.0a×20+ba \times 20 + b

代入均方误差公式,展开后得到:

MSE(a,b)=248.5a2+31ab+b2840.44a53.16b+714.712\text{MSE}(a, b) = 248.5a^2 + 31ab + b^2 - 840.44a - 53.16b + 714.712

上式为二元二次方程,其曲面为凸的椭圆抛物面,像一口碗,有唯一的最小值点。数学上,通过对 aabb 求偏导寻找最小值点。

MSE.png

1.3 导数

对于一元函数 f(x)f(x),导数 f(x)f'(x) 描述了函数在某一点的变化率。从几何上看,导数就是函数曲线在该点切线的斜率。例如,对于函数 f(x)=x2f(x) = x^2,其导数为 f(x)=2xf'(x) = 2x。在 x=3x=3 处,导数值为 6,说明函数在这一点以斜率 6 变化。

当处理多元函数时,比如上述的均方误差 MSE(a,b)\text{MSE}(a, b) 依赖于两个变量 aabb,就需要用到偏导数。偏导数衡量的是函数沿着某一个坐标轴方向的变化率,而将其他变量视为常数。例如,对于函数 f(x,y)=x2+xyf(x, y) = x^2 + xy,对 xx 的偏导数(记作 fx\frac{\partial f}{\partial x})是 2x+y2x + y(把 yy 当作常数),对 yy 的偏导数是 xx(把 xx 当作常数)。

回到上述问题,MSE(a,b)\text{MSE}(a, b) 是二元函数,它的最小值点应该满足两个偏导数同时为零:MSEa=0\frac{\partial \text{MSE}}{\partial a} = 0MSEb=0\frac{\partial \text{MSE}}{\partial b} = 0。这就像在一元函数中,极值点处导数为零一样。

接下来,具体计算 MSE(a,b)\text{MSE}(a, b)aabb 的偏导数。

求偏导公式如下:

MSEa=497a+31b840.44\frac{\partial \text{MSE}}{\partial a} = 497a + 31b - 840.44
MSEb=31a+2b53.16\frac{\partial \text{MSE}}{\partial b} = 31a + 2b - 53.16

令导数为 0,即可求出令均方误差最小的 aabb 值。方程组如下:

0=497a+31b840.440 = 497a + 31b - 840.44
0=31a+2b53.160 = 31a + 2b - 53.16

解方程组得:

a=8238250.9976a = \frac{823}{825} \approx 0.9976
b=917282511.1176b = \frac{9172}{825} \approx 11.1176

由此得到描述电阻与温度关系的最准确公式:

y=0.9976x+11.1176y = 0.9976x + 11.1176

1.4 数据拟合

上述过程就是数据拟合:根据已知的数据点,找出一条曲线(这里是一条直线)来反映数据的变化趋势。现在,把这个思路推广一下:如果数据点不是直线,而是弯弯曲曲的复杂曲线,怎么办呢?那就需要更复杂的公式,比如多项式、指数函数等。而神经网络,本质上就是一个超级复杂的、包含成千上万个参数的公式。神经网络的学习过程,就是根据数据自动调整这些参数,让公式的输出尽可能地贴近真实值。

2 神经网络

2.1 神经元

神经元接收多个输入信号,但每个信号的重要性不同。权重(weight)就是用来衡量这种重要性的系数。每个输入都有一个对应的权重,权重越大,这个输入对最终决策的影响就越大。

生物神经元有一个阈值:只有当所有输入信号的加权和超过某个值时,它才会兴奋(产生输出)。否则,它就不兴奋,忽略微小信号。这个特性很重要,如果没有阈值,神经系统就会变得情绪不稳定。在人工神经元中,用偏置(bias)来模拟这个阈值。

神经元.png

神经元的输入可以表示为:

z=w1x1+w2x2++wnxn+bz = w_1 x_1 + w_2 x_2 + \ldots + w_n x_n + b

其中,wiw_i 是权重,xix_i 是输入,bb 是偏置。

如果只是加权求和再加偏置,那整个神经元还是线性的。线性模型叠加再多,也只能表达直线,无法拟合复杂曲线。为了让神经元有非线性能力,引入激活函数(activation function)。常见的激活函数有:

  • Sigmoid:把输出压缩到 0~1 之间,像平滑的开关
  • ReLU:max(0, x),简单且有效,是目前最常用的激活函数之一

0/1 网络使用 Sigmoid 激活函数。Sigmoid 公式如下:

Sigmoid(x)=11+ex\text{Sigmoid}(x) = \frac{1}{1 + e^{-x}}

Sigmoid 函数曲线如下:

Sigmoid函数曲线.png

激活函数给神经元注入了灵魂,让它能表达是与否、强与弱这类非线性关系。

神经元贰.png

2.2 网络结构

单个神经元能力有限,但把很多神经元连接起来,就组成了神经网络。就像大脑有无数个神经元相互连接,才产生了智能。一个典型的神经网络分为三层:

  • 输入层:接收原始数据。比如一张图像的像素值
  • 隐藏层:中间层,可以有一层或多层。负责提取特征、进行非线性变换
  • 输出层:给出最终结果。比如是数字 1 还是数字 0

深度学习使用的神经网络是有多个隐藏层的神经网络。0/1 网络只有一个隐藏层,各层连接如下:

神经网络.png

图中,每一层的神经元与下一层所有神经元相连,这称为全连接神经网络。神经网络还有很多其他经典结构:如卷积神经网络、循环神经网络、图神经网络等。

一个完整的神经网络包含:

  • 神经网络结构
  • 神经网络参数:各神经元的权重和偏置
  • 激活函数

2.3 输入层

输入层负责读取神经网络的输入,并将信息原样输出。0/1 网络中,数据加载器从左上角到右下角逐个读取样本图像的各像素,若像素为黑色,则输入为 1;若像素为白色,则输入为 0。输入层读取到的就是代表样本图像 12 个像素的 0/1 序列。

输入层.png

2.4 隐藏层

隐藏层是神经网络的核心,它负责从原始像素中提取出有意义的特征。什么是特征?特征就是数据中能帮助我们做出判断的关键模式。比如,要判断一张图里是数字 0 还是数字 1,需要看它有没有圆圈(数字 0 的特征)或竖线(数字 1 的特征)。

数字图像零.png

数字图像壹.png

下面以训练好的 0/1 网络 的参数来讲解什么是特征,隐藏层如何提取特征。

隐藏层神经元 h1h1 的权重如下:

权重输入层隐藏层
w111w^1_{1-1}x1x_1h1h1-0.6302202
w211w^1_{2-1}x2x_2h1h11.60545878
w311w^1_{3-1}x3x_3h1h1-1.09651057
w411w^1_{4-1}x4x_4h1h1-1.330712
w511w^1_{5-1}x5x_5h1h11.32532169
w611w^1_{6-1}x6x_6h1h1-1.31416219
w711w^1_{7-1}x7x_7h1h1-1.31317393
w811w^1_{8-1}x8x_8h1h11.03433711
w911w^1_{9-1}x9x_9h1h1-1.28556487
w1011w^1_{10-1}x10x_{10}h1h1-1.22585296
w1111w^1_{11-1}x11x_{11}h1h11.67217289
w1211w^1_{12-1}x12x_{12}h1h1-1.0077602

隐藏层神经元 h1h1 的偏置如下:

偏置
b11b^1_10.99517127

隐藏层神经元 h2h2 的权重如下:

权重输入层隐藏层
w122w^2_{1-2}x1x_1h2h2-0.17489004
w222w^2_{2-2}x2x_2h2h20.39992454
w322w^2_{3-2}x3x_3h2h2-0.35969918
w422w^2_{4-2}x4x_4h2h2-0.56722885
w522w^2_{5-2}x5x_5h2h20.35618619
w622w^2_{6-2}x6x_6h2h2-0.55750559
w722w^2_{7-2}x7x_7h2h2-0.62140746
w822w^2_{8-2}x8x_8h2h20.50465389
w922w^2_{9-2}x9x_9h2h2-0.45594783
w1022w^2_{10-2}x10x_{10}h2h2-0.4811531
w1122w^2_{11-2}x11x_{11}h2h20.6613442
w1222w^2_{12-2}x12x_{12}h2h2-0.26214341

隐藏层神经元 h2h2 的偏置如下:

偏置
b21b^1_20.09878039

隐藏层神经元 h3h3 的权重如下:

权重输入层隐藏层
w133w^3_{1-3}x1x_1h3h30.79412472
w233w^3_{2-3}x2x_2h3h3-1.76962622
w333w^3_{3-3}x3x_3h3h31.31201553
w433w^3_{4-3}x4x_4h3h31.44717745
w533w^3_{5-3}x5x_5h3h3-1.40210612
w633w^3_{6-3}x6x_6h3h31.48501276
w733w^3_{7-3}x7x_7h3h31.47822735
w833w^3_{8-3}x8x_8h3h3-1.23524117
w933w^3_{9-3}x9x_9h3h31.39521465
w1033w^3_{10-3}x10x_{10}h3h31.42453392
w1133w^3_{11-3}x11x_{11}h3h3-1.90108568
w1233w^3_{12-3}x12x_{12}h3h31.05302035

隐藏层神经元 h3h3 的偏置如下:

偏置
b31b^1_3-1.16758538

输出层神经元 y1y1 的权重如下:

权重隐藏层输出层
w112w^2_{1-1}h1h1y1y1-4.3035679
w212w^2_{2-1}h2h2y1y1-1.20890508
w312w^2_{3-1}h3h3y1y15.30706513

输出层神经元 y1y1 的偏置如下:

偏置
b12b^2_1-0.0521834

输出层神经元 y2y2 的权重如下:

权重隐藏层输出层
w122w^2_{1-2}h1h1y2y24.31689612
w222w^2_{2-2}h2h2y2y21.22928151
w322w^2_{3-2}h3h3y2y2-5.28673997

输出层神经元 y2y2 的偏置如下:

偏置
b22b^2_20.02728745

对于神经元 h1h1,其主要正权重对应输入如下:

  • x2x2
  • x5x5
  • x8x8
  • x11x11

主要负权重对应输入如下:

  • x4x4
  • x6x6
  • x7x7
  • x9x9
  • x10x10
  • x12x12

在图像中标记出正负权重对应的像素,如下图所示:

h1提取的特征.png

正权重恰好对应图像中央一列,这正是数字 1 的垂直主干;负权重分布在 0 的方形轮廓上,起到抑制干扰的作用。