手写数字识别:从零搭建神经网络

什么是手写数字识别?

我们要解决的问题很简单:让计算机学会识别手写的 0-9 这 10 个数字。就像银行自动识别支票上的数字、快递单自动识别收件人电话那样,只不过我们用的是经典的 MNIST 数据集(包含 6 万张训练图和 1 万张测试图)。

第一步:搭建神经网络(model.py

先来看模型结构,我们要搭建一个简单的卷积神经网络,包含这些层:

第二步:训练模型(train.py

有了模型结构,接下来就是训练了。训练过程就像教小孩认数字:先看例子(训练集),再做测试(测试集),不断纠正错误。

  1. 卷积层:提取图像的边缘、轮廓等特征

  2. 展平层:把二维图像转换成一维数据

  3. 全连接层:对特征进行进一步处理

  4. 输出层:输出 10 个数字的预测概率

    python 复制代码
    # 从 TensorFlow 的 Keras 模块中导入需要的层:
    # Dense:全连接层(用于分类计算),
    # Flatten:展平层(把二维图像转成一维数据),
    # Conv2D:卷积层(提取图像特征)
    from tensorflow.keras.layers import Dense, Flatten, Conv2D
    # 从 Keras 导入 Model 类:用于自定义模型(继承这个类就能自己搭建网络结构)
    from tensorflow.keras import Model
    
    
    # 定义自己的模型类 MyModel,继承自 Keras 的 Model 类(相当于"抄作业",直接用 Model 的基础功能)
    class MyModel(Model):
        # 初始化函数:创建模型时自动执行,用来定义网络的所有层
        def __init__(self):
            # 调用父类(Model)的初始化方法,确保模型基础功能能正常使用
            super(MyModel, self).__init__()
    
            # 定义第1层:卷积层(Conv2D),核心作用是"提取图像的局部特征"(比如线条、轮廓)
            # 参数解释:
            # 32:输出特征图数量(相当于用32种不同的"滤镜"去扫图像,得到32个特征结果)
            # 3:卷积核大小(3x3的正方形,每次扫图像的3x3区域)
            # activation='relu':激活函数(把负数变成0,让模型能学习复杂的非线性特征,避免简单线性计算)
            self.conv1 = Conv2D(32, 3, activation='relu')
    
            # 定义第2层:展平层(Flatten),核心作用是"把二维图像数据转成一维数组"
            # 因为卷积层输出的是二维特征图,全连接层需要一维数据才能计算,这里是"中间转换器"
            self.flatten = Flatten()
    
            # 定义第3层:全连接层(Dense),核心作用是"对提取的特征做进一步计算"
            # 参数解释:
            # 128:该层的神经元数量(128个神经元同时计算,相当于128个不同的计算逻辑)
            # activation='relu':继续用relu激活函数,增加模型的学习能力
            self.d1 = Dense(128, activation='relu')
    
            # 定义第4层:输出层(全连接层),核心作用是"输出最终分类结果"
            # 参数解释:
            # 10:输出神经元数量(对应10个类别,比如MNIST的0-9手写数字)
            # activation='softmax':激活函数(把输出转成"概率",10个值总和为1,最大的那个就是预测的类别)
            self.d2 = Dense(10, activation='softmax')
    
        # 调用函数:模型接收输入数据时执行,定义数据在网络中的"流动路径"(前向传播过程)
        # x:输入的图像数据,shape(格式)是 [batch, 28, 28, 1](后面会详细解释)
        # **kwargs:兼容其他参数(不用管,固定写法)
        def call(self, x, **kwargs):
            # 第一步:输入数据x经过卷积层conv1,提取特征
            # 输入x格式:[batch, 28, 28, 1] → batch:一次训练的图片数量,28x28:图片像素,1:灰度图(只有1个颜色通道)
            # 输出格式:[batch, 26, 26, 32] → 26x26:卷积后图像大小(计算逻辑:(28 - 3 + 1)/1 = 26,因为3x3卷积核不填充边缘),32:32个特征图
            x = self.conv1(x)
    
            # 第二步:卷积后的特征图经过展平层flatten,转成一维数组
            # 输入格式:[batch, 26, 26, 32] → 二维特征图(26x26)+32个特征图
            # 输出格式:[batch, 21632] → 一维数组(计算逻辑:26×26×32 = 21632,把所有特征值排成一列)
            x = self.flatten(x)
    
            # 第三步:一维特征经过全连接层d1,做特征融合计算
            # 输入格式:[batch, 21632] → 展平后的一维特征
            # 输出格式:[batch, 128] → 128个神经元的计算结果(提炼核心特征)
            x = self.d1(x)
    
            # 第四步:最终经过输出层d2,输出10个类别的概率
            # 输入格式:[batch, 128] → d1层的128个核心特征
            # 输出格式:[batch, 10] → 10个值(每个值是对应类别的概率,比如第3个值最大就是预测为类别3)
            return self.d2(x)      
  5. 卷积层:就像人眼先看到线条和轮廓,计算机也需要先提取这些基础特征

  6. 展平层:因为全连接层只能处理一维数据,需要这个 "转换器"

  7. 全连接层:把提取到的特征进行组合,发现更复杂的模式(比如 "圆形 + 竖线" 可能是数字 9)

  8. 输出层:给出每个数字的概率,概率最大的就是模型的预测结果

python 复制代码
# 兼容Python2和Python3的语法(比如print函数、编码格式),现在Python3可省略,但保留不影响
from __future__ import absolute_import, division, print_function, unicode_literals

# 导入TensorFlow库(核心深度学习框架)
import tensorflow as tf
# 从model.py文件中导入我们之前定义的自定义CNN模型MyModel(就是那个含卷积层、全连接层的模型)
from model import MyModel


# 定义主函数:程序入口(运行脚本时会自动执行main())
def main():
    # 加载MNIST数据集:TensorFlow自带的手写数字数据集(6万张训练图,1万张测试图)
    mnist = tf.keras.datasets.mnist

    # 1. 下载并加载数据集
    # mnist.load_data():自动下载(首次运行)并拆分数据为"训练集"和"测试集"
    # x_train:训练集图像(60000张,每张28x28像素),y_train:训练集标签(60000个,0-9的数字)
    # x_test:测试集图像(10000张,每张28x28像素),y_test:测试集标签(10000个,0-9的数字)
    (x_train, y_train), (x_test, y_test) = mnist.load_data()

    # 数据归一化:把图像像素值从0-255(灰度图范围)缩放到0-1之间
    # 原因:神经网络对0-1区间的数值更敏感,训练更快、效果更好
    x_train, x_test = x_train / 255.0, x_test / 255.0

    # 2. 给图像添加"通道维度"
    # 之前自定义的MyModel要求输入格式是 [batch, 28, 28, 1](最后一维是通道数)
    # 而mnist.load_data()加载的图像格式是 [28,28](无通道数),所以用tf.newaxis添加最后一维
    # 最终x_train格式:[60000, 28, 28, 1],x_test格式:[10000, 28, 28, 1]
    x_train = x_train[..., tf.newaxis]
    x_test = x_test[..., tf.newaxis]

    # 3. 创建数据生成器(方便批量训练和打乱数据)
    # tf.data.Dataset.from_tensor_slices:把图像和标签配对,生成数据集
    # shuffle(10000):每次训练前打乱10000个样本(避免模型"死记硬背",提高泛化能力)
    # batch(32):每次训练取32个样本一起计算(批量计算更快,还能稳定训练过程)
    train_ds = tf.data.Dataset.from_tensor_slices(
        (x_train, y_train)).shuffle(10000).batch(32)
    # 测试集不需要打乱,只需要批量处理
    test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)

    # 4. 初始化我们自定义的CNN模型(就是之前写的MyModel类)
    model = MyModel()

    # 5. 定义损失函数(衡量模型预测值和真实值的差距)
    # SparseCategoricalCrossentropy:适用于"标签是整数"的分类任务(比如y_train是0-9的数字)
    # 对比:如果标签是one-hot编码(比如[0,1,0]表示类别1),就用CategoricalCrossentropy
    loss_object = tf.keras.losses.SparseCategoricalCrossentropy()

    # 6. 定义优化器(更新模型参数,减小损失)
    # Adam:目前最常用的优化器,自动调整学习率,训练稳定且快
    optimizer = tf.keras.optimizers.Adam()

    # 7. 定义训练过程的"监控指标"(实时看训练效果)
    # Mean:计算平均损失(比如一批32个样本的平均损失)
    train_loss = tf.keras.metrics.Mean(name='train_loss')
    # SparseCategoricalAccuracy:计算训练准确率(预测对的样本数/总样本数)
    train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

    # 8. 定义测试过程的"监控指标"(看模型在新数据上的效果)
    test_loss = tf.keras.metrics.Mean(name='test_loss')
    test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')

    # 9. 定义训练步骤(单个批次的训练逻辑)
    # @tf.function:把函数编译成TensorFlow图模式,训练速度更快(不用每次都重新构建计算图)
    def train_step(images, labels):
        # tf.GradientTape:"梯度磁带",记录计算过程,方便后续求导(反向传播的核心)
        with tf.GradientTape() as tape:
            # 模型预测:把批次图像输入模型,得到预测结果(每个样本10个类别概率)
            predictions = model(images)
            # 计算损失:用损失函数对比预测结果和真实标签的差距
            loss = loss_object(labels, predictions)

        # 求梯度:根据损失,计算模型所有可训练参数(权重、偏置)的梯度(导数)
        gradients = tape.gradient(loss, model.trainable_variables)
        # 优化器更新参数:用梯度调整参数,减小损失(梯度下降的核心步骤)
        optimizer.apply_gradients(zip(gradients, model.trainable_variables))

        # 更新监控指标:把当前批次的损失和准确率加入统计
        train_loss(loss)
        train_accuracy(labels, predictions)

    # 10. 定义测试步骤(单个批次的测试逻辑)
    @tf.function  # 同样编译成图模式,测试更快
    def test_step(images, labels):
        # 模型预测(测试时不记录梯度,节省资源)
        predictions = model(images)
        # 计算测试损失
        t_loss = loss_object(labels, predictions)

        # 更新测试指标
        test_loss(t_loss)
        test_accuracy(labels, predictions)

    # 11. 定义训练轮数(EPOCHS:把所有训练数据完整过一遍叫1个epoch)
    EPOCHS = 5  # 5轮足够,再多可能过拟合(训练集准确率高,测试集准确率下降)

    # 12. 开始训练循环(每轮训练+测试)
    for epoch in range(EPOCHS):
        # 重置指标:每轮开始前清空上一轮的损失和准确率(避免累计)
        train_loss.reset_state()  # 清空训练损失记录
        train_accuracy.reset_state()  # 清空训练准确率记录
        test_loss.reset_state()  # 清空测试损失记录
        test_accuracy.reset_state()  # 清空测试准确率记录

        # 遍历训练集的所有批次:逐个批次训练
        for images, labels in train_ds:
            train_step(images, labels)  # 执行单个批次的训练

        # 遍历测试集的所有批次:逐个批次测试(不更新模型参数)
        for test_images, test_labels in test_ds:
            test_step(test_images, test_labels)  # 执行单个批次的测试

        # 打印每轮的训练/测试结果
        # template:字符串模板,用format填充数值
        template = 'Epoch {}, 训练损失: {}, 训练准确率: {:.2f}%, 测试损失: {}, 测试准确率: {:.2f}%'
        print(template.format(
            epoch + 1,  # 轮数从1开始(默认从0计数,+1更直观)
            train_loss.result(),  # 本轮平均训练损失
            train_accuracy.result() * 100,  # 训练准确率(×100转成百分比)
            test_loss.result(),  # 本轮平均测试损失
            test_accuracy.result() * 100  # 测试准确率(×100转成百分比)
        ))


# 程序入口:如果直接运行这个脚本,就执行main()函数
if __name__ == '__main__':
    main()# 兼容Python2和Python3的语法(比如print函数、编码格式),现在Python3可省略,但保留不影响
from __future__ import absolute_import, division, print_function, unicode_literals

# 导入TensorFlow库(核心深度学习框架)
import tensorflow as tf
# 从model.py文件中导入我们之前定义的自定义CNN模型MyModel(就是那个含卷积层、全连接层的模型)
from model import MyModel


# 定义主函数:程序入口(运行脚本时会自动执行main())
def main():
    # 加载MNIST数据集:TensorFlow自带的手写数字数据集(6万张训练图,1万张测试图)
    mnist = tf.keras.datasets.mnist

    # 1. 下载并加载数据集
    # mnist.load_data():自动下载(首次运行)并拆分数据为"训练集"和"测试集"
    # x_train:训练集图像(60000张,每张28x28像素),y_train:训练集标签(60000个,0-9的数字)
    # x_test:测试集图像(10000张,每张28x28像素),y_test:测试集标签(10000个,0-9的数字)
    (x_train, y_train), (x_test, y_test) = mnist.load_data()

    # 数据归一化:把图像像素值从0-255(灰度图范围)缩放到0-1之间
    # 原因:神经网络对0-1区间的数值更敏感,训练更快、效果更好
    x_train, x_test = x_train / 255.0, x_test / 255.0

    # 2. 给图像添加"通道维度"
    # 之前自定义的MyModel要求输入格式是 [batch, 28, 28, 1](最后一维是通道数)
    # 而mnist.load_data()加载的图像格式是 [28,28](无通道数),所以用tf.newaxis添加最后一维
    # 最终x_train格式:[60000, 28, 28, 1],x_test格式:[10000, 28, 28, 1]
    x_train = x_train[..., tf.newaxis]
    x_test = x_test[..., tf.newaxis]

    # 3. 创建数据生成器(方便批量训练和打乱数据)
    # tf.data.Dataset.from_tensor_slices:把图像和标签配对,生成数据集
    # shuffle(10000):每次训练前打乱10000个样本(避免模型"死记硬背",提高泛化能力)
    # batch(32):每次训练取32个样本一起计算(批量计算更快,还能稳定训练过程)
    train_ds = tf.data.Dataset.from_tensor_slices(
        (x_train, y_train)).shuffle(10000).batch(32)
    # 测试集不需要打乱,只需要批量处理
    test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)

    # 4. 初始化我们自定义的CNN模型(就是之前写的MyModel类)
    model = MyModel()

    # 5. 定义损失函数(衡量模型预测值和真实值的差距)
    # SparseCategoricalCrossentropy:适用于"标签是整数"的分类任务(比如y_train是0-9的数字)
    # 对比:如果标签是one-hot编码(比如[0,1,0]表示类别1),就用CategoricalCrossentropy
    loss_object = tf.keras.losses.SparseCategoricalCrossentropy()

    # 6. 定义优化器(更新模型参数,减小损失)
    # Adam:目前最常用的优化器,自动调整学习率,训练稳定且快
    optimizer = tf.keras.optimizers.Adam()

    # 7. 定义训练过程的"监控指标"(实时看训练效果)
    # Mean:计算平均损失(比如一批32个样本的平均损失)
    train_loss = tf.keras.metrics.Mean(name='train_loss')
    # SparseCategoricalAccuracy:计算训练准确率(预测对的样本数/总样本数)
    train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

    # 8. 定义测试过程的"监控指标"(看模型在新数据上的效果)
    test_loss = tf.keras.metrics.Mean(name='test_loss')
    test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')

    # 9. 定义训练步骤(单个批次的训练逻辑)
    # @tf.function:把函数编译成TensorFlow图模式,训练速度更快(不用每次都重新构建计算图)
    def train_step(images, labels):
        # tf.GradientTape:"梯度磁带",记录计算过程,方便后续求导(反向传播的核心)
        with tf.GradientTape() as tape:
            # 模型预测:把批次图像输入模型,得到预测结果(每个样本10个类别概率)
            predictions = model(images)
            # 计算损失:用损失函数对比预测结果和真实标签的差距
            loss = loss_object(labels, predictions)

        # 求梯度:根据损失,计算模型所有可训练参数(权重、偏置)的梯度(导数)
        gradients = tape.gradient(loss, model.trainable_variables)
        # 优化器更新参数:用梯度调整参数,减小损失(梯度下降的核心步骤)
        optimizer.apply_gradients(zip(gradients, model.trainable_variables))

        # 更新监控指标:把当前批次的损失和准确率加入统计
        train_loss(loss)
        train_accuracy(labels, predictions)

    # 10. 定义测试步骤(单个批次的测试逻辑)
    @tf.function  # 同样编译成图模式,测试更快
    def test_step(images, labels):
        # 模型预测(测试时不记录梯度,节省资源)
        predictions = model(images)
        # 计算测试损失
        t_loss = loss_object(labels, predictions)

        # 更新测试指标
        test_loss(t_loss)
        test_accuracy(labels, predictions)

    # 11. 定义训练轮数(EPOCHS:把所有训练数据完整过一遍叫1个epoch)
    EPOCHS = 5  # 5轮足够,再多可能过拟合(训练集准确率高,测试集准确率下降)

    # 12. 开始训练循环(每轮训练+测试)
    for epoch in range(EPOCHS):
        # 重置指标:每轮开始前清空上一轮的损失和准确率(避免累计)
        train_loss.reset_state()  # 清空训练损失记录
        train_accuracy.reset_state()  # 清空训练准确率记录
        test_loss.reset_state()  # 清空测试损失记录
        test_accuracy.reset_state()  # 清空测试准确率记录

        # 遍历训练集的所有批次:逐个批次训练
        for images, labels in train_ds:
            train_step(images, labels)  # 执行单个批次的训练

        # 遍历测试集的所有批次:逐个批次测试(不更新模型参数)
        for test_images, test_labels in test_ds:
            test_step(test_images, test_labels)  # 执行单个批次的测试

        # 打印每轮的训练/测试结果
        # template:字符串模板,用format填充数值
        template = 'Epoch {}, 训练损失: {}, 训练准确率: {:.2f}%, 测试损失: {}, 测试准确率: {:.2f}%'
        print(template.format(
            epoch + 1,  # 轮数从1开始(默认从0计数,+1更直观)
            train_loss.result(),  # 本轮平均训练损失
            train_accuracy.result() * 100,  # 训练准确率(×100转成百分比)
            test_loss.result(),  # 本轮平均测试损失
            test_accuracy.result() * 100  # 测试准确率(×100转成百分比)
        ))


# 程序入口:如果直接运行这个脚本,就执行main()函数
if __name__ == '__main__':
    main()

训练过程讲解

  1. 数据准备

    • 加载数据后进行归一化(0-255→0-1),这是神经网络的常见操作
    • 添加通道维度,让数据格式符合模型要求
  2. 核心训练逻辑

    • 每轮训练:用训练集更新模型参数,然后用测试集评估效果
    • 梯度下降:通过计算损失的梯度,不断调整模型参数,让预测越来越准
    • 批量训练:每次用 32 张图片一起训练,既高效又稳定
  3. 训练结果 :运行后会看到类似这样的输出:

简单来说,卷积层就像一系列 "特征探测器",会逐步学习到数字的关键特征:

  • 第一层可能学到识别边缘和线条
  • 后续层会组合这些边缘,学习到更复杂的形状(比如圆圈、拐角)
  • 最后通过全连接层判断这些形状组合起来是哪个数字
相关推荐
前进的李工2 小时前
LeetCode hot100:094 二叉树的中序遍历:从递归到迭代的完整指南
python·算法·leetcode·链表·二叉树
z***y8622 小时前
机器学习重点
人工智能·机器学习
AI人工智能+2 小时前
文档抽取技术:通过OCR、NLP和机器学习技术,将非结构化的合同、发票等文档转化为结构化数据
人工智能·计算机视觉·nlp·ocr·文档抽取
johnny2333 小时前
AI IDE/插件(三):Task Master、DeepCode
ide·人工智能
ConardLi3 小时前
前端程序员原地失业?全面实测 Gemini 3.0,附三个免费使用方法!
前端·人工智能·后端
w***Q3503 小时前
深度学习博客
人工智能·深度学习
爱编程的喵喵3 小时前
《华为数据之道》发行五周年暨《数据空间探索与实践》新书发布会召开,共探AI时代数据治理新路径
人工智能·华为
ins_lizhiming3 小时前
在华为910B GPU服务器上运行DeepSeek-R1-0528模型
人工智能·pytorch·python·华为
ModestCoder_3 小时前
【学习笔记】Diffusion Policy for Robotics
论文阅读·人工智能·笔记·学习·机器人·强化学习·具身智能