2. TensorFlow 模型训练基础(上):MNIST 手写数字识别
各位同学,咱们直接进入正题。今天要聊的是 TensorFlow 模型训练的基础部分,我选了 MNIST 手写数字识别这个经典案例。为什么选它?因为简单、直观,而且能完整走通从数据到模型的整个流程。你想想看,一个能识别手写数字的模型,其实已经包含了深度学习最核心的几个环节。
2.1 MNIST 数据集:深度学习的 "Hello World"
MNIST 数据集,说白了就是一堆 28x28 像素的手写数字图片,从 0 到 9 一共十类。训练集有 6 万张,测试集 1 万张。我刚开始接触深度学习时,第一个跑通的模型就是这个。嗯,这里要注意,虽然 MNIST 现在被很多人说 "太简单了",但它的价值在于帮你理解数据管道、模型结构、训练流程这些基本功。
数据集关键信息:
- 图片尺寸:28 x 28 像素(灰度图,单通道)
- 训练样本:60,000 张
- 测试样本:10,000 张
- 类别数:10(数字 0-9)
- 像素值范围:0-255(需要归一化到 0-1)
在 Keras 中加载 MNIST 非常简单,一行代码就能搞定。我个人习惯用 tf.keras.datasets.mnist.load_data(),它会自动下载并返回训练集和测试集的元组。
import tensorflow as tf
from tensorflow import keras
# 加载 MNIST 数据集
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# 查看数据形状
print(f"训练集图片形状: {x_train.shape}") # (60000, 28, 28)
print(f"训练集标签形状: {y_train.shape}") # (60000,)
print(f"测试集图片形状: {x_test.shape}") # (10000, 28, 28)
2.2 数据预处理:别让原始数据坑了你
数据加载完,千万别直接扔进模型。我在项目中遇到过不少新手,数据预处理没做好,模型怎么调都收敛不了。这里有两个关键步骤:
- 归一化:把像素值从 0-255 缩放到 0-1 之间。神经网络对输入数据的尺度很敏感,大数值会让梯度爆炸。
- 标签处理:分类任务中,标签需要转换成 one-hot 编码,或者直接用 sparse_categorical_crossentropy 损失函数。
# 归一化:像素值除以 255.0
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
# 标签处理:使用 sparse_categorical_crossentropy 时不需要 one-hot
# 如果要用 categorical_crossentropy,需要做 one-hot 编码
# y_train = keras.utils.to_categorical(y_train, 10)
我的经验:归一化时一定要用 255.0 而不是 255,否则整数除法会直接截断成 0。曾经有个学员因为这个 bug 调了一整天,模型 loss 死活不降。
2.3 Keras Sequential API:搭积木一样建模型
Keras 提供了两种构建模型的方式:Sequential API 和 Functional API。对于 MNIST 这种简单的前馈网络,Sequential API 最合适。它就像搭积木,一层一层往上堆。
我常用的网络结构是这样的:
- 输入层:把 28x28 的二维图片展平成一维向量(784 个神经元)
- 隐藏层:全连接层,128 个神经元,激活函数用 ReLU
- Dropout 层:随机丢弃 20% 的神经元,防止过拟合
- 输出层:全连接层,10 个神经元,激活函数用 Softmax
model = keras.Sequential([
# 输入层:将 28x28 图片展平为 784 维向量
keras.layers.Flatten(input_shape=(28, 28)),
# 隐藏层:128 个神经元,ReLU 激活
keras.layers.Dense(128, activation='relu'),
# Dropout 层:随机丢弃 20% 的神经元
keras.layers.Dropout(0.2),
# 输出层:10 个神经元,Softmax 输出概率
keras.layers.Dense(10, activation='softmax')
])
# 查看模型结构
model.summary()
你想想看,这个模型其实就做了三件事:展平、特征提取、分类。Flatten 层把二维数据变成一维,Dense 层做矩阵乘法加激活函数,Dropout 层随机屏蔽一些神经元来增强泛化能力。
避坑指南:我曾经在项目中把 input_shape 写成了 (28, 28, 1),但 MNIST 默认加载的数据形状是 (28, 28),没有通道维度。如果你用 Flatten 层,它会自动处理,但如果你用 Conv2D 层,就必须显式添加通道维度。
2.4 模型编译:配置训练参数
模型搭好了,接下来要告诉它怎么学。编译阶段需要指定三个东西:优化器、损失函数、评估指标。
| 参数 | 常用选项 | 说明 |
|---|---|---|
| 优化器 | adam, sgd, rmsprop | 控制梯度下降的更新策略 |
| 损失函数 | sparse_categorical_crossentropy, categorical_crossentropy | 衡量预测值与真实值的差距 |
| 评估指标 | accuracy, precision, recall | 监控训练过程中的性能 |
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
这里我解释一下为什么用 sparse_categorical_crossentropy。如果你的标签是整数形式(比如 0, 1, 2...),就用这个;如果你的标签做了 one-hot 编码(比如 [0,0,1,0...]),就用 categorical_crossentropy。我个人习惯用前者,省去 one-hot 编码的步骤。
2.5 模型训练:见证 loss 下降的过程
编译完成后,调用 fit() 方法开始训练。这里有几个关键参数:
- epochs:训练轮数,每轮遍历一次全部训练数据
- batch_size:每次梯度更新使用的样本数
- validation_data:验证集,用于监控过拟合
history = model.fit(
x_train, y_train,
epochs=10,
batch_size=32,
validation_data=(x_test, y_test)
)
训练过程中,你会看到 loss 逐渐下降,accuracy 逐渐上升。我记得第一次跑这个模型时,看到 loss 从 2.3 降到 0.1 左右,那种成就感真的很棒。但要注意,如果 validation loss 开始上升而 training loss 还在下降,那就是过拟合的信号。
训练输出示例:
Epoch 1/10
1875/1875 [==============================] - 3s 1ms/step - loss: 0.2956 - accuracy: 0.9145 - val_loss: 0.1387 - val_accuracy: 0.9587
Epoch 2/10
1875/1875 [==============================] - 2s 1ms/step - loss: 0.1417 - accuracy: 0.9575 - val_loss: 0.1039 - val_accuracy: 0.9682
...
Epoch 10/10
1875/1875 [==============================] - 2s 1ms/step - loss: 0.0738 - accuracy: 0.9772 - val_loss: 0.0758 - val_accuracy: 0.9765
2.6 保存模型为 .h5 格式
训练完成后,一定要保存模型。为什么?因为后续的量化部署、版本控制都需要这个文件。Keras 提供了两种保存方式:
- 保存整个模型:包括网络结构、权重、优化器状态
- 仅保存权重:只保存参数,加载时需要重新定义网络结构
对于部署场景,我建议保存整个模型,这样加载时一步到位。
# 保存整个模型为 .h5 格式
model.save('mnist_model.h5')
# 加载模型
loaded_model = keras.models.load_model('mnist_model.h5')
# 验证加载的模型
test_loss, test_acc = loaded_model.evaluate(x_test, y_test, verbose=0)
print(f'测试集准确率: {test_acc:.4f}')
我的习惯:保存模型时,文件名最好包含版本号和关键信息,比如 mnist_v1_97.6acc.h5。这样后续做版本控制时,一眼就能看出哪个模型性能更好。我在项目中吃过亏,一堆 unnamed_model.h5 文件,根本分不清哪个是哪个。
2.7 本章知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张流程图。它展示了从数据加载到模型保存的完整链路,每个环节都有对应的操作和注意事项。
这张图把整个流程串起来了。你跟着这个顺序走,从数据加载到模型保存,每一步都有对应的代码实现。下一章我们会在这个基础上,深入讨论模型版本控制和量化部署的细节。
本章核心要点:
- MNIST 是 28x28 的灰度手写数字图片,共 10 类
- 数据预处理必须做归一化,否则模型难以收敛
- Sequential API 适合简单的前馈网络,代码简洁
- 编译时指定优化器、损失函数、评估指标
- 训练时监控 validation loss 防止过拟合
- 保存为 .h5 格式,文件名包含版本信息便于管理