《自己动手写神经网络》旨在通过带领读者实现一个迷你神经网络,使读者对权重、阈值、特征提取、梯度下降、误差反向传播法等有个感性的认识,为学习深度学习打下坚实的基础。该神经网络用于识别图像中的数字是 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
- 3.1 矩阵运算
- 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 张图像,图像内容为数字 0 或 1。图像分辨率为 像素,每个像素为黑色或白色。图像内容示例如下:
图像文件名为 {sn}-{y}.bmp。文件名示例如下:
- 1-0.bmp
- 23-0.bmp
- 64-1.bmp
sn 为图像编号,取值范围为 1~64,y 为图像内容,取值范围为 0 或 1。
0/1 网络 为图像的每个像素分配了一个坐标。坐标示例如下:
本教程利用 64 张图像训练 0/1 网络,使其能识别图像中的数字 0 或 1。
本项目在 Python 3.12 上测试,依赖 numpy、pillow 库。环境搭建命令如下:
python -m pip install numpy pillow
1 从一个物理实验谈起
温度传感器利用电阻随温度升高而稳定增加的特性来精确测温。科学家需要准确地知道电阻随温度变化的公式,这通常根据实验数据得出。假设科学家拿到了一组实验数据,记录了不同温度下某导体的电阻值。实验数据如下:
| 温度 | 电阻 |
|---|---|
| 11 | 22.1 |
| 12 | 23.0 |
| 13 | 24.2 |
| 14 | 25.1 |
| 15 | 26.0 |
| 16 | 27.1 |
| 17 | 28.0 |
| 18 | 29.2 |
| 19 | 30.1 |
| 20 | 31.0 |
以温度为横坐标,电阻为纵坐标,在坐标纸上绘制出这些点。温度与电阻关系散点图如下:
上图为温度与电阻关系的散点图,可以直观地看到数据点几乎排成一条直线,说明电阻随温度呈线性变化趋势。
在坐标纸上绘制一条贴近上述坐标点的直线,量出其与 x 轴、y 轴的交点坐标。拟合直线如下:
依据交点坐标,即可求出电阻随温度变化的公式。
理论上,依据上述温度与电阻关系散点可绘制出无数条直线(只需稍微旋转或平移即可),每一条都能得到一个公式。那么,哪一条才是最准确的呢?
1.1 误差
要判断公式准不准,需要一个衡量标准。最直观的就是看每个点的真实电阻值和公式预测的电阻值相差多少,这个差值叫做误差。
如果直接用 真实值 – 预测值,那么有的点预测值偏大,误差为负;有的点预测值偏小,误差为正。正负相抵后,总误差可能很小,无法衡量公式准不准,所以需要让误差总是正的。这通常有两种方法:
- 绝对值:
- 平方:
平方不仅能消除正负,还能放大误差(比如误差 2 变成 4 ,误差 3 变成 9),让大误差点更显眼,优化时会优先照顾它们。所以通常选用平方误差,把所有点的平方误差加起来,就是总误差。总误差公式如下:
其中 是真实电阻, 是公式预测的电阻。通常还会将总误差除以数据量,以消除数据量对误差的影响,这就是均方误差。均方误差公式如下:
1.2 最小二乘法
既然误差越小,直线就越准,那问题就变成了:找一条直线 ,使得均方误差最小。这就是最小二乘法的核心思想。将真实电阻、预测电阻代入均方误差公式,数据如下:
| 温度 | 真实电阻 | 预测电阻 |
|---|---|---|
| 11 | 22.1 | |
| 12 | 23.0 | |
| 13 | 24.2 | |
| 14 | 25.1 | |
| 15 | 26.0 | |
| 16 | 27.1 | |
| 17 | 28.0 | |
| 18 | 29.2 | |
| 19 | 30.1 | |
| 20 | 31.0 |
代入均方误差公式,展开后得到:
上式为二元二次方程,其曲面为凸的椭圆抛物面,像一口碗,有唯一的最小值点。数学上,通过对 和 求偏导寻找最小值点。
1.3 导数
对于一元函数 ,导数 描述了函数在某一点的变化率。从几何上看,导数就是函数曲线在该点切线的斜率。例如,对于函数 ,其导数为 。在 处,导数值为 6,说明函数在这一点以斜率 6 变化。
当处理多元函数时,比如上述的均方误差 依赖于两个变量 和 ,就需要用到偏导数。偏导数衡量的是函数沿着某一个坐标轴方向的变化率,而将其他变量视为常数。例如,对于函数 ,对 的偏导数(记作 )是 (把 当作常数),对 的偏导数是 (把 当作常数)。
回到上述问题, 是二元函数,它的最小值点应该满足两个偏导数同时为零: 且 。这就像在一元函数中,极值点处导数为零一样。
接下来,具体计算 对 和 的偏导数。
求偏导公式如下:
令导数为 0,即可求出令均方误差最小的 、 值。方程组如下:
解方程组得:
由此得到描述电阻与温度关系的最准确公式:
1.4 数据拟合
上述过程就是数据拟合:根据已知的数据点,找出一条曲线(这里是一条直线)来反映数据的变化趋势。现在,把这个思路推广一下:如果数据点不是直线,而是弯弯曲曲的复杂曲线,怎么办呢?那就需要更复杂的公式,比如多项式、指数函数等。而神经网络,本质上就是一个超级复杂的、包含成千上万个参数的公式。神经网络的学习过程,就是根据数据自动调整这些参数,让公式的输出尽可能地贴近真实值。
2 神经网络
2.1 神经元
神经元接收多个输入信号,但每个信号的重要性不同。权重(weight)就是用来衡量这种重要性的系数。每个输入都有一个对应的权重,权重越大,这个输入对最终决策的影响就越大。
生物神经元有一个阈值:只有当所有输入信号的加权和超过某个值时,它才会兴奋(产生输出)。否则,它就不兴奋,忽略微小信号。这个特性很重要,如果没有阈值,神经系统就会变得情绪不稳定。在人工神经元中,用偏置(bias)来模拟这个阈值。
神经元的输入可以表示为:
其中, 是权重, 是输入, 是偏置。
如果只是加权求和再加偏置,那整个神经元还是线性的。线性模型叠加再多,也只能表达直线,无法拟合复杂曲线。为了让神经元有非线性能力,引入激活函数(activation function)。常见的激活函数有:
- Sigmoid:把输出压缩到
0~1之间,像平滑的开关 - ReLU:
max(0, x),简单且有效,是目前最常用的激活函数之一
0/1 网络使用 Sigmoid 激活函数。Sigmoid 公式如下:
Sigmoid 函数曲线如下:
激活函数给神经元注入了灵魂,让它能表达是与否、强与弱这类非线性关系。
2.2 网络结构
单个神经元能力有限,但把很多神经元连接起来,就组成了神经网络。就像大脑有无数个神经元相互连接,才产生了智能。一个典型的神经网络分为三层:
- 输入层:接收原始数据。比如一张图像的像素值
- 隐藏层:中间层,可以有一层或多层。负责提取特征、进行非线性变换
- 输出层:给出最终结果。比如是数字
1还是数字0
深度学习使用的神经网络是有多个隐藏层的神经网络。0/1 网络只有一个隐藏层,各层连接如下:
图中,每一层的神经元与下一层所有神经元相连,这称为全连接神经网络。神经网络还有很多其他经典结构:如卷积神经网络、循环神经网络、图神经网络等。
一个完整的神经网络包含:
- 神经网络结构
- 神经网络参数:各神经元的权重和偏置
- 激活函数
2.3 输入层
输入层负责读取神经网络的输入,并将信息原样输出。0/1 网络中,数据加载器从左上角到右下角逐个读取样本图像的各像素,若像素为黑色,则输入为 1;若像素为白色,则输入为 0。输入层读取到的就是代表样本图像 12 个像素的 0/1 序列。
2.4 隐藏层
隐藏层是神经网络的核心,它负责从原始像素中提取出有意义的特征。什么是特征?特征就是数据中能帮助我们做出判断的关键模式。比如,要判断一张图里是数字 0 还是数字 1,需要看它有没有圆圈(数字 0 的特征)或竖线(数字 1 的特征)。
下面以训练好的 0/1 网络 的参数来讲解什么是特征,隐藏层如何提取特征。
隐藏层神经元 的权重如下:
| 权重 | 输入层 | 隐藏层 | 值 |
|---|---|---|---|
| -0.6302202 | |||
| 1.60545878 | |||
| -1.09651057 | |||
| -1.330712 | |||
| 1.32532169 | |||
| -1.31416219 | |||
| -1.31317393 | |||
| 1.03433711 | |||
| -1.28556487 | |||
| -1.22585296 | |||
| 1.67217289 | |||
| -1.0077602 |
隐藏层神经元 的偏置如下:
| 偏置 | 值 |
|---|---|
| 0.99517127 |
隐藏层神经元 的权重如下:
| 权重 | 输入层 | 隐藏层 | 值 |
|---|---|---|---|
| -0.17489004 | |||
| 0.39992454 | |||
| -0.35969918 | |||
| -0.56722885 | |||
| 0.35618619 | |||
| -0.55750559 | |||
| -0.62140746 | |||
| 0.50465389 | |||
| -0.45594783 | |||
| -0.4811531 | |||
| 0.6613442 | |||
| -0.26214341 |
隐藏层神经元 的偏置如下:
| 偏置 | 值 |
|---|---|
| 0.09878039 |
隐藏层神经元 的权重如下:
| 权重 | 输入层 | 隐藏层 | 值 |
|---|---|---|---|
| 0.79412472 | |||
| -1.76962622 | |||
| 1.31201553 | |||
| 1.44717745 | |||
| -1.40210612 | |||
| 1.48501276 | |||
| 1.47822735 | |||
| -1.23524117 | |||
| 1.39521465 | |||
| 1.42453392 | |||
| -1.90108568 | |||
| 1.05302035 |
隐藏层神经元 的偏置如下:
| 偏置 | 值 |
|---|---|
| -1.16758538 |
输出层神经元 的权重如下:
| 权重 | 隐藏层 | 输出层 | 值 |
|---|---|---|---|
| -4.3035679 | |||
| -1.20890508 | |||
| 5.30706513 |
输出层神经元 的偏置如下:
| 偏置 | 值 |
|---|---|
| -0.0521834 |
输出层神经元 的权重如下:
| 权重 | 隐藏层 | 输出层 | 值 |
|---|---|---|---|
| 4.31689612 | |||
| 1.22928151 | |||
| -5.28673997 |
输出层神经元 的偏置如下:
| 偏置 | 值 |
|---|---|
| 0.02728745 |
对于神经元 ,其主要正权重对应输入如下:
主要负权重对应输入如下:
在图像中标记出正负权重对应的像素,如下图所示:
正权重恰好对应图像中央一列,这正是数字 1 的垂直主干;负权重分布在 0 的方形轮廓上,起到抑制干扰的作用。