M N I S T MNIST MNIST 手写数字识别的 F C N N FCNN FCNN 推理实现
前面的文章中,我们分别学习了激活函数的选择、前向传播的计算流程,并用 N u m P y NumPy NumPy 手动实现了隐藏层和输出层的 A f f i n e Affine Affine 变换与激活函数。但这些知识点始终是"零件"状态------我们还没有把它们组装成一个真正可用的神经网络。
本文将把这些零件拼装起来,从零构建一个能识别手写数字的神经网络。数据集选用深度学习领域的 H e l l o Hello Hello W o r l d World World ------ M N I S T MNIST MNIST 手写数字集,包含 60000 60000 60000 张训练图像和 10000 10000 10000 张测试图像,每张都是 28 × 28 28×28 28×28 像素的灰度手写数字( 0 0 0~ 9 9 9)。
本文的目标是实现一个简单 3 3 3 层神经网络,通过加载预训练的权重数据,实现简单的推理过程,自动识别手写数字。通过这个实战案例,读者可以直观感受神经网络的推理流程:数据如何从输入层流入,经过层层变换,最终输出预测结果。
所谓 F C N N FCNN FCNN( F u l l y Fully Fully- C o n n e c t e d Connected Connected N e u r a l Neural Neural N e t w o r k Network Network,全连接神经网络),是指相邻两层之间的神经元两两全部相连,每个神经元都接收上一层所有神经元的输出作为输入
这正是我们之前一直提及的结构,后面我们还将学习到卷积神经网络 C N N CNN CNN、循环神经网络 R N N RNN RNN,对抗神经网络 G A N GAN GAN 等
本文的相关mnist数据集以及预训练的sample_weight.pkl详见附录,本文代码位于项目目录predict下
数据集
-
介绍 ------ 本文采用
MNIST作为数据集。MNIST是深度学习领域最经典的入门数据集之一,由美国国家标准与技术研究所收集整理。它包含70000张手写数字灰度图像:
-
结构 ------
MNIST的图像数据是28×28像素的手写数字灰度图像(1通道),各个像素取值在0到255之间,每个图像数据都相应地标有7、2、1等标签。训练图像有6万张, 测试图像有1万张。属性 说明 属性 说明 图像内容 手写数字 0~9--- --- 图像尺寸 28 × 28像素(灰度图)每张图像 784个像素值(28×28展平)训练集 60000张测试集 10000张 -
任务 ------ 输入一张
28×28的手写数字图像,神经网络输出它属于0~9中哪个数字。
网络设计
-
目标 ------ 设计并实现神经网络,能够读取预设定的权重参数
sample_weight.pkl,输入一张28×28的MNIST手写数字图像,网络输出它属于0~9中哪个数字。 -
网络结构 ------ 设计的神经网络的包括:
-
输入层有 784 784 784 个神经元,来源于图像大小的 28 28 28× 28 28 28= 784 784 784
-
输出层有 10 10 10 个神经元,来源于 10 10 10类别分类,数字 0 0 0 到 9 9 9 共 10 10 10 类别
-
2 2 2 个隐藏层,第 1 1 1 个隐藏层有 50 50 50 个神经元,第 2 2 2 个隐藏层有 100 100 100 个神经元(个数不绝对,可以设置为任何值)
-
隐藏层采用 s i g m o i d sigmoid sigmoid 函数,输出层采用 s o f t m a x softmax softmax 函数
-
网络结构如下:
I n p u t → A f f i n e → S i g m o i d → A f f i n e → S i g m o i d → A f f i n e → S o f t m a x → O u t p u t Input → Affine → Sigmoid → Affine → Sigmoid → Affine → Softmax → Output Input→Affine→Sigmoid→Affine→Sigmoid→Affine→Softmax→Output

-
单数据处理
加载数据集
-
加载
mnist数据集,使用load_mnist函数以 ( 训练图像 , 训练标签 ) , ( 测试图像,测试标签 ) (训练图像,训练标签),(测试图像,测试标签) (训练图像,训练标签),(测试图像,测试标签) 的形式返回读入的数据,其中函数参数如下:normalize=True------ 将像素值从0~255归一化到 `0.0~1.0``flatten=True------ 将28×28的图像展平为 784 维向量one_hot_label=False------ 标签保持整数形式(如 3),而非one-hot编码
ps :
load_mnist函数来源于dataset/mnist.py,主要功能是从网络下载并读取数据集,具体详见附录pythondef get_data(): (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False) return x_test, t_test
初始化参数
-
初始化权重、偏置参数
-
这里主要通过加载预训练好的网络参数
sample_weight.pkl -
在学习了反向传播等内容后,我们可以自行训练神经网络进行学习,获得自己的参数。
-
-
函数返回的
params是一个字典,主要包含:参数名 形状 连接方向 说明 W1(784, 50)输入层 → 隐藏层 1 输入层 784 个神经元 → 隐藏层 1 共 50 个神经元的权重矩阵 b1(50,)隐藏层 1 隐藏层 1 共 50 个神经元的偏置向量 W2(50, 100)隐藏层 1 → 隐藏层 2 隐藏层 1 共 50 个神经元 → 隐藏层 2 共 100 个神经元的权重矩阵 b2(100,)隐藏层 2 隐藏层 2 共 100 个神经元的偏置向量 W3(100, 10)隐藏层 2 → 输出层 隐藏层 2 共 100 个神经元 → 输出层 10 个神经元的权重矩阵 b3(10,)输出层 输出层 10 个神经元的偏置向量 pythondef init_network(): with open("sample_weight.pkl", 'rb') as f: params = pickle.load(f) return params
激活函数
-
定义激活函数,包括 s i g m o i d sigmoid sigmoid 和 s o f t m a x softmax softmax 函数
pythondef sigmoid(x): return 1 / (1 + np.exp(-x))pythondef softmax(x): if x.ndim == 2: x = x.T x = x - np.max(x, axis=0) y = np.exp(x) / np.sum(np.exp(x), axis=0) return y.T x = x - np.max(x) return np.exp(x) / np.sum(np.exp(x))
前向传播
-
定义神经网络结构,用于前向传播。这里的网络共三层,具体如下:
pythondef predict(params, x): """ 对输入图像进行前向传播,输出每个类别的概率 网络结构:Input(784) → [Affine → Sigmoid] → [Affine → Sigmoid] → [Affine → Softmax] → Output(10) """ # 取出各层参数 W1, W2, W3 = params['W1'], params['W2'], params['W3'] b1, b2, b3 = params['b1'], params['b2'], params['b3'] # 第 1 层:输入层(784) → 隐藏层1(50) a1 = np.dot(x, W1) + b1 # Affine 变换:(N,784)·(784,50) = (N,50) z1 = sigmoid(a1) # Sigmoid 激活:输出 (N,50),每个值在 (0,1) 之间 # 第 2 层:隐藏层1(50) → 隐藏层2(100) a2 = np.dot(z1, W2) + b2 # Affine 变换:(N,50)·(50,100) = (N,100) z2 = sigmoid(a2) # Sigmoid 激活:输出 (N,100) # 第 3 层:隐藏层2(100) → 输出层(10) a3 = np.dot(z2, W3) + b3 # Affine 变换:(N,100)·(100,10) = (N,10) y = softmax(a3) # Softmax 激活:输出 (N,10),每行是一个概率分布 return y
识别准确率
-
计算神经网络的识别准确率,如输出 0.9352 0.9352 0.9352 表示有 93.52 % 93.52\% 93.52% 的数据被正确分类
-
predict()函数以NumPy数组的形式输出各个标签对应的概率如输出
[0.1, 0.3, 0.2, ..., 0.04]的数组,表示0的概率为0.1,1的概率为0.3等 -
使用
np.argmax()取出概率列表中的最大值的索引(即这张图数字几的概率最高),作为预测结果 -
for语句逐一与真实标签进行对比,统计并给出被正确分类数据的占比
pythondef accuracy(params, x, t): """ 单数据评价识别精度 :param params: 训练好的网络预训练权重 :param x: 输入数据 :param t: 监督标签 :return: 精度值 """ accuracy_cnt = 0 for i in range(len(x)): y = predict(params, x[i]) p = np.argmax(y) if p == t[i]: accuracy_cnt += 1 return float(accuracy_cnt) / len(x) -
可视化
-
从数据集随机取
5张进行识别,把识别结果展示在界面pythondef random_show(params, x, t): """ 随机取 MNIST 数据集中 5 张图像进行识别并展示到界面 :param params: 训练好的网络(包含预训练权重) :param x: 测试图像数据,形状 (N, 784) :param t: 测试图像的真实标签,形状 (N,) :return: None """ indices = random.sample(range(len(x)), 5) fig, axes = plt.subplots(1, 5, figsize=(15, 5)) for i, idx in enumerate(indices): img = x[idx].reshape(28, 28) # 预测结果 y = predict(params, x[idx]) prob = np.max(y) # 概率值 pred_digit = np.argmax(y) # 预测数字标签 true_digit = t[idx] # 真实数字标签 axes[i].imshow(img, cmap='gray') axes[i].set_title(f'True: {true_digit} \n' f'Pred: {pred_digit} ({prob:.3%})', fontsize=10, color='black' if pred_digit == true_digit else 'red') axes[i].axis('off') plt.tight_layout() plt.show()
主程序
-
首先读取数据集、预训练参数,然后计算识别精度,最后可视化展示图片
pythonif __name__ == '__main__': x, t = get_data() params = init_network() # 识别精度 accuracy = accuracy(params, x, t) print(f"Accuracy: {accuracy}") # 展示图片 random_show(params, x, t)python输出:Accuracy:0.9352,表示有 93.52% 的数据被正确分类
批数据处理
-
上面的代码,在主程序中是逐一对每张图片进行推理,通过
for循环一并统计并计算识别精度Accuracy。输入的图片形状是784,通过神经网络推导,输出包含10个概率的一维数组,代表10个数字的概率
-
现在考虑打包输入多张图像的情形。比如想用
predict()函数一次性打包处理100张图像。为此,可以把x的形状改为100×784,将100张图像打包作为输入数据,输出100张图片的概率数组
-
这种打包式的输入数据称为批 ( b a t c h batch batch),批有"捆"的意思,图像就如同纸一样扎成一捆
批处理能大幅缩短处理时间,主要因为两点:
- 大多数数值计算库针对大型数组运算做了高度优化;
- 当数据读写成为瓶颈时,一次性加载一批数据可以减少数据总线的负荷,让更多时间用在计算上
-
改进代码
pythondef accuracy_batch(params, x, t, batch_size): """ 批处理评价识别精度 :param params: 训练好的网络(包含预训练权重) :param x: 测试图像数据,形状 (N, 784) :param t: 测试图像的真实标签,形状 (N,) :param batch_size: 一次批处理数 :return: 精度值 """ accuracy_cnt = 0 for i in range(0, len(x), batch_size): x_batch = x[i:i + batch_size] y_batch = predict(params, x_batch) p = np.argmax(y_batch, axis=1) accuracy_cnt += np.sum(p == t[i:i + batch_size]) return float(accuracy_cnt) / len(x)其中:
-
使用
range(start, end, step)指定步数batch_size,每次取100个图片pythonfor i in range(0, len(x), batch_size): -
通过
x[i:i+batch_size]取出从第i个到第i+batch_n个之间的数据pythonx_batch = x[i:i+batch_size] y_batch = predict(network, x_batch) -
使用
np.argmax()取出概率列表中的最大值的索引,一次批量统计pythonp = np.argmax(y_batch, axis=1) accuracy_cnt += np.sum(p == t[i:i+batch_size])
-
-
主程序
pythonif __name__ == '__main__': x, t = get_data() params = init_network() # 评价识别精度 accuracy = accuracy_batch(params, x, t, batch_size=100) print(f"Accuracy: {accuracy}")
封装为类
将上面的代码整理为一个名为
ThreeLayerNet的类,统一封装predict、accuracy等核心方法,方便后续训练和测试时调用
类的实现
python
import pickle
import random
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.functions import *
class ThreeLayerNet:
"""
简单三层全连接神经网络 Fully-Connected Neural Network, FCNN
网络结构:Input(784) → [Affine → Sigmoid] → [Affine → Sigmoid] → [Affine → Softmax] → Output(10)
"""
def __init__(self, weight_file="sample_weight.pkl"):
"""
初始化两层全连接神经网络,从预训练文件中读取权重、配置等参数
:param weight_file: 预训练文件路径
"""
with open(weight_file, 'rb') as f:
self.params = pickle.load(f)
def predict(self, x):
"""
前向传播
:param x: 输入图像数据,形状 (784,) 或 (N, 784)
:return: y: 预测结果,形状 (10,) 或 (N, 10),每个元素表示对应类别的概率
"""
W1, W2, W3 = self.params['W1'], self.params['W2'], self.params['W3']
b1, b2, b3 = self.params['b1'], self.params['b2'], self.params['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = softmax(a3)
return y
def accuracy(self, x, t):
"""
单数据评价识别精度
:param x: 测试图像数据,形状 (N, 784)
:param t: 测试图像的真实标签,形状 (N,)
:return:
"""
accuracy_cnt = 0
for i in range(len(x)):
y = self.predict(x[i])
p = np.argmax(y)
if p == t[i]:
accuracy_cnt += 1
return float(accuracy_cnt) / len(x)
def accuracy_batch(self, x, t, batch_size):
"""
批处理评价识别精度
:param x: 测试图像数据,形状 (N, 784)
:param t: 测试图像的真实标签,形状 (N,)
:param batch_size: 一次批处理数
:return:
"""
accuracy_cnt = 0
for i in range(0, len(x), batch_size):
x_batch = x[i:i + batch_size]
y_batch = self.predict(x_batch)
p = np.argmax(y_batch, axis=1)
accuracy_cnt += np.sum(p == t[i:i + batch_size])
return float(accuracy_cnt) / len(x)
def random_show(self, x, t):
"""
随机取 MNIST 数据集中 5 张图像进行识别并展示到界面
:param x: 测试图像数据,形状 (N, 784)
:param t: 测试图像的真实标签,形状 (N,)
:return: None
"""
indices = random.sample(range(len(x)), 5)
fig, axes = plt.subplots(1, 5, figsize=(15, 5))
for i, idx in enumerate(indices):
img = x[idx].reshape(28, 28)
# 预测结果
y = self.predict(x[idx])
prob = np.max(y) # 概率值
pred_digit = np.argmax(y) # 预测数字标签
true_digit = t[idx] # 真实数字标签
axes[i].imshow(img, cmap='gray')
axes[i].set_title(f'True: {true_digit} \n'
f'Pred: {pred_digit} ({prob:.3%})',
fontsize=10,
color='black' if pred_digit == true_digit else 'red')
axes[i].axis('off')
plt.tight_layout()
plt.show()
类的调用
python
if __name__ == '__main__':
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
# 初始化全连接神经网络FCNN
network = ThreeLayerNet(weight_file="sample_weight.pkl")
# 评价识别精度
accuracy = network.accuracy_batch(x_train, t_train, batch_size=100)
# accuracy = network.accuracy(x_test, t_test)
print(f"Accuracy: {accuracy}")
# 展示图片
network.random_show(x_train, t_train)
附录 :手写数字识别 Demo 项目地址:MnistRecognition: A simple handwritten digit recognition system using the MNIST dataset and a neural network.
参考文献:1 斋藤康毅. 深度学习入门:基于Python的理论与实现M. 陆宇杰, 译. 北京: 人民邮电出版社, 2018.