从零开始!用 FNN 实现手写数字识别
隐藏层:2 层,第一层 128 个神经元,第二层 64 个神经元,激活函数用 ReLU(常用的非线性激活函数,能解决梯度消失问题)。运行代码后,我们会看到一张手写数字 “5” 的图片(因为 x_train [0] 的标签是 5),这就是我们要让模型学习识别的数据。print("训练集图片形状:", x_train.shape) # (60000, 28, 28):60000张28×28的图片。#
一、项目背景:为什么选择手写数字识别?
在深度学习领域,手写数字识别就像 “Hello World” 一样,是入门的必经之路。它的任务很简单:让计算机识别出手写的 0-9 这 10 个数字。
为什么说它适合入门呢?主要有三个原因:
- 数据集成熟:MNIST 数据集是深度学习领域的 “标杆数据集”,包含 60000 张训练图片和 10000 张测试图片,每张图片是 28×28 像素的灰度图,标签明确,无需额外处理。
- 任务难度适中:数字识别的分类任务相对简单,不需要复杂的模型结构,用基础的 FNN 就能达到不错的效果,容易获得成就感。
- 覆盖核心流程:从数据加载、预处理,到模型搭建、训练、评估,再到结果可视化,整个过程完整覆盖了深度学习项目的核心环节,能帮我们快速熟悉开发流程。
二、准备工作:环境与工具
在开始之前,我们需要准备好开发环境。这里我选择 Python 作为开发语言,搭配以下常用库:
- TensorFlow/PyTorch:深度学习框架,用于搭建和训练模型(本文以 TensorFlow 为例,PyTorch 版本会在文末补充)。
- NumPy:用于数值计算,处理数据矩阵。
- Matplotlib:用于数据可视化,展示图片和训练曲线。
- Scikit-learn:用于简单的数据预处理(如归一化)。
-
安装命令(以 TensorFlow 为例):pip install tensorflow numpy matplotlib scikit-learn
三、步骤拆解:从数据到模型
1. 加载并探索 MNIST 数据集
首先,我们用 TensorFlow 自带的接口加载 MNIST 数据集。加载后的数据会自动分成训练集和测试集,并且像素值范围在 0-255 之间。
import tensorflow as tf
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt
import numpy as np
# 加载数据集
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 探索数据形状
print("训练集图片形状:", x_train.shape) # (60000, 28, 28):60000张28×28的图片
print("训练集标签形状:", y_train.shape) # (60000,):60000个标签
print("测试集图片形状:", x_test.shape) # (10000, 28, 28)
print("测试集标签形状:", y_test.shape) # (10000,)
# 可视化一张图片
plt.imshow(x_train[0], cmap='gray')
plt.title(f"标签:{y_train[0]}")
plt.axis('off')
plt.show()
运行代码后,我们会看到一张手写数字 “5” 的图片(因为 x_train [0] 的标签是 5),这就是我们要让模型学习识别的数据。
2. 数据预处理:让模型 “更好学”
原始数据无法直接输入 FNN,我们需要做两步预处理:
- 扁平化(Flatten):FNN 的输入是一维向量,而 MNIST 图片是 28×28 的二维矩阵,所以需要把每张图片转换成 784(28×28)个元素的一维向量。
- 归一化(Normalization):将像素值从 0-255 缩放到 0-1 之间。这样做可以加快模型的训练速度,避免因数值过大导致的梯度爆炸问题。
# 1. 扁平化:将28×28的二维矩阵转为784维的一维向量
x_train_flatten = x_train.reshape(x_train.shape[0], -1) # (60000, 784)
x_test_flatten = x_test.reshape(x_test.shape[0], -1) # (10000, 784)
# 2. 归一化:像素值从0-255转为0-1
x_train_normal = x_train_flatten / 255.0
x_test_normal = x_test_flatten / 255.0
# 3. 标签独热编码(One-Hot Encoding)
# 因为是10分类任务,需要将标签转为10维向量(如标签5转为[0,0,0,0,0,1,0,0,0,0])
y_train_onehot = tf.keras.utils.to_categorical(y_train, 10)
y_test_onehot = tf.keras.utils.to_categorical(y_test, 10)
print("预处理后训练集输入形状:", x_train_normal.shape) # (60000, 784)
print("预处理后训练集标签形状:", y_train_onehot.shape) # (60000, 10)
3. 搭建 FNN 模型:核心结构解析
FNN 的核心是 “输入层 - 隐藏层 - 输出层” 的全连接结构,每一层的神经元都与上一层的所有神经元相连。
-
输入层:784 个神经元(对应扁平化后的图片向量)。隐藏层:2 层,第一层 128 个神经元,第二层 64 个神经元,激活函数用 ReLU(常用的非线性激活函数,能解决梯度消失问题)。
-
输出层:10 个神经元(对应 0-9 这 10 个数字),激活函数用 Softmax(将输出转为概率分布,总和为 1,方便判断类别)。
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
# 搭建模型
model = Sequential([
# 输入层(可省略,模型会自动根据输入数据推断)
# Dense(128, input_shape=(784,), activation='relu'),
# 隐藏层1:128个神经元,ReLU激活
Dense(128, activation='relu'),
# 隐藏层2:64个神经元,ReLU激活
Dense(64, activation='relu'),
# 输出层:10个神经元,Softmax激活
Dense(10, activation='softmax')
])
# 查看模型结构
model.summary()
运行model.summary()后,我们可以看到模型的各层参数:
- 隐藏层 1:784(输入)×128(神经元) + 128(偏置)= 100480 个参数。
- 隐藏层 2:128×64 + 64 = 8256 个参数。
- 输出层:64×10 + 10 = 650 个参数。
-
总参数约 10.9 万,对于 FNN 来说,这个规模非常适合入门。
4. 编译与训练模型:让模型 “学会” 识别
模型搭建好后,需要先 “编译”,指定训练时的优化器、损失函数和评估指标;然后用训练集 “训练” 模型,让它从数据中学习规律。
# 编译模型
model.compile(
optimizer='adam', # 优化器:Adam(常用且高效)
loss='categorical_crossentropy', # 损失函数:适用于多分类的交叉熵
metrics=['accuracy'] # 评估指标:准确率
)
# 训练模型
history = model.fit(
x_train_normal, y_train_onehot, # 训练数据和标签
epochs=10, # 训练轮次:整个训练集遍历10次
batch_size=32, # 批次大小:每次训练32张图片
validation_split=0.1 # 验证集比例:从训练集中划分10%作为验证集
)
- epochs:训练轮次。轮次太少会导致模型 “学不会”(欠拟合),轮次太多会导致 “学过头”(过拟合),这里先设为 10。
- batch_size:批次大小。每次用 32 张图片更新一次模型参数,平衡训练速度和稳定性。
- validation_split:验证集用于在训练过程中监控模型性能,避免过拟合。
-
5. 模型评估:看看模型 “学得怎么样”
训练完成后,我们需要用测试集评估模型的泛化能力(即对新数据的识别能力),同时可视化训练过程中的准确率和损失变化。
# 用测试集评估模型
test_loss, test_acc = model.evaluate(x_test_normal, y_test_onehot)
print(f"测试集准确率:{test_acc:.4f}") # 通常能达到97%以上
# 可视化训练过程
plt.figure(figsize=(12, 4))
# 1. 准确率曲线
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='训练准确率')
plt.plot(history.history['val_accuracy'], label='验证准确率')
plt.title('准确率变化')
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.legend()
# 2. 损失曲线
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.title('损失变化')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend()
plt.show()
从结果中我们可以看到:
- 训练准确率和验证准确率都在不断上升,说明模型在持续学习。
- 如果验证准确率在后期开始下降,而训练准确率还在上升,说明出现了过拟合,需要调整模型(如增加正则化)。
- 测试集准确率通常能达到 97% 以上,对于一个简单的 FNN 来说,这个效果已经很不错了。
-
6. 模型预测:用新数据测试
最后,我们用测试集中的图片来测试模型的预测效果,看看它能不能正确识别手写数字。
# 随机选择5张测试图片进行预测
np.random.seed(42) # 固定随机种子,保证结果可复现
random_idx = np.random.choice(x_test_normal.shape[0], 5)
test_imgs = x_test_normal[random_idx]
test_labels = y_test[random_idx]
# 预测
predictions = model.predict(test_imgs)
# 取概率最大的索引作为预测结果
predict_labels = np.argmax(predictions, axis=1)
# 可视化预测结果
plt.figure(figsize=(10, 4))
for i in range(5):
plt.subplot(1, 5, i+1)
# 还原为28×28的图片
plt.imshow(test_imgs[i].reshape(28, 28), cmap='gray')
# 标题:真实标签 vs 预测标签
plt.title(f"真实:{test_labels[i]}\n预测:{predict_labels[i]}")
plt.axis('off')
plt.show()
四、模型优化:如何让准确率更高?
如果想进一步提升模型性能,可以尝试以下优化方法:
- 增加隐藏层或神经元数量:但要注意避免过拟合。
- 添加正则化:在 Dense 层中加入kernel_regularizer=tf.keras.regularizers.l2(0.001),抑制参数过大。
- 使用 dropout:在隐藏层后加入Dropout(0.2),随机 “关闭” 部分神经元,防止过拟合。
- 调整超参数:比如将 epochs 改为 15,batch_size 改为 64,或更换优化器(如 SGD)。
- 数据增强:虽然 MNIST 数据简单,但也可以通过旋转、平移等方式生成更多训练数据。
-
优化后的模型测试准确率通常能达到 98% 以上,甚至接近 99%。
更多推荐

所有评论(0)