神经网络 | ⑤ MNIST 手写数字识别的 FCNN 推理实现

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通道),各个像素取值在 0255 之间,每个图像数据都相应地标有 7 21 等标签。训练图像有 6 万张, 测试图像有 1 万张。

    属性 说明 属性 说明
    图像内容 手写数字 0~9 --- ---
    图像尺寸 28 × 28 像素(灰度图) 每张图像 784 个像素值(28×28 展平)
    训练集 60000 测试集 10000
  • 任务 ------ 输入一张 28×28 的手写数字图像,神经网络输出它属于 0~9 中哪个数字。


网络设计

  • 目标 ------ 设计并实现神经网络,能够读取预设定的权重参数 sample_weight.pkl ,输入一张 28×28MNIST 手写数字图像,网络输出它属于 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 编码

    psload_mnist 函数来源于 dataset/mnist.py,主要功能是从网络下载并读取数据集,具体详见附录

    python 复制代码
    def 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 个神经元的偏置向量
    python 复制代码
    def 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​ 函数

    python 复制代码
    def sigmoid(x):
        return 1 / (1 + np.exp(-x))
    python 复制代码
    def 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))
前向传播
  • 定义神经网络结构,用于前向传播。这里的网络共三层,具体如下:

    python 复制代码
    def 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.11 的概率为 0.3

    • 使用 np.argmax() 取出概率列表中的最大值的索引(即这张图数字几的概率最高),作为预测结果

    • for 语句逐一与真实标签进行对比,统计并给出被正确分类数据的占比

    python 复制代码
    def 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 张进行识别,把识别结果展示在界面

    python 复制代码
    def 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()
主程序
  • 首先读取数据集、预训练参数,然后计算识别精度,最后可视化展示图片

    python 复制代码
    if __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),批有"捆"的意思,图像就如同纸一样扎成一捆

    批处理能大幅缩短处理时间,主要因为两点:

    1. 大多数数值计算库针对大型数组运算做了高度优化;
    2. 当数据读写成为瓶颈时,一次性加载一批数据可以减少数据总线的负荷,让更多时间用在计算上
  • 改进代码

    python 复制代码
    def 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 个图片

      python 复制代码
      for i in range(0, len(x), batch_size):
    • 通过 x[i:i+batch_size] 取出从第 i 个到第 i+batch_n 个之间的数据

      python 复制代码
        x_batch = x[i:i+batch_size]
          y_batch = predict(network, x_batch)
    • 使用 np.argmax() 取出概率列表中的最大值的索引,一次批量统计

      python 复制代码
        p = np.argmax(y_batch, axis=1)
          accuracy_cnt += np.sum(p == t[i:i+batch_size])
  • 主程序

    python 复制代码
    if __name__ == '__main__':
        x, t = get_data()
        params = init_network()
    
        # 评价识别精度
        accuracy = accuracy_batch(params, x, t, batch_size=100)
        print(f"Accuracy: {accuracy}")


封装为类

将上面的代码整理为一个名为 ThreeLayerNet 的类,统一封装 predictaccuracy 等核心方法,方便后续训练和测试时调用

类的实现
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.