我们以经典的 MNIST手写数字识别 为例,用 TensorFlow/Keras 框架的代码逐步解释CNN的运行过程。我会在代码中插入详细注释,并用比喻辅助理解。
完整代码及逐行解释
import tensorflow as tf
from tensorflow.keras import layers, models
# 1. 加载数据:MNIST手写数字数据集(28x28像素的灰度图)
(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
# 2. 数据预处理:归一化到0~1,并调整形状以适应CNN输入
train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255
# 3. 构建CNN模型
model = models.Sequential([
# --- 第一层:卷积 + 激活 + 池化 ---
# 卷积层:用32个3x3的“放大镜”扫描图像
layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
# 池化层:2x2窗口取最大值,相当于生成缩略图
layers.MaxPooling2D((2,2)),
# --- 第二层:卷积 + 激活 + 池化 ---
layers.Conv2D(64, (3,3), activation='relu'),
layers.MaxPooling2D((2,2)),
# --- 展开特征图,连接全连接层 ---
layers.Flatten(), # 把多维数据压扁成一维
layers.Dense(64, activation='relu'), # 全连接层做高级判断
layers.Dense(10, activation='softmax') # 输出10个数字的概率
])
# 4. 编译模型:定义如何训练
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 5. 训练模型
model.fit(train_images, train_labels, epochs=5, batch_size=64)
# 6. 评估模型
test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"测试准确率:{test_acc}")
# 7. 预测单张图片(比如第一张测试图)
predictions = model.predict(test_images[0:1])
print("预测结果概率:", predictions)
print("预测数字:", tf.argmax(predictions[0]).numpy())
关键代码段解释
1. 数据预处理
train_images = train_images.reshape((60000, 28, 28, 1)) # 添加通道维度(灰度图通道数为1)
train_images = train_images.astype('float32') / 255 # 归一化到0~1
比喻:把图片从“一叠纸”整理成“标准卡片”,每张卡片尺寸28x28,单色,亮度值压缩到0~1。
2. 卷积层
layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1))
参数解释:
32
:使用32种不同的“放大镜”(滤波器),每种负责找一种基础特征(如横线、竖线、弧线)。(3,3)
:每个放大镜是3x3像素的小窗口。activation='relu'
:只保留明显的特征(负数归零,正数保留)。
输出形状:输入28x28 → 经过3x3卷积后变为26x26(边缘被裁剪),32个通道(每个滤波器生成一个通道)。
3. 池化层
layers.MaxPooling2D((2,2))
作用:把2x2区域内的最大值保留,其他丢弃。
效果:26x26 → 13x13(缩小一半),保留关键特征的位置,忽略细微变化。
4. 深层卷积
layers.Conv2D(64, (3,3), activation='relu')
第二层卷积:用64种更复杂的“放大镜”扫描上一层的结果。
特征升级:第一层找的是“边缘”,第二层可能找“拐角”、“圆圈”等组合特征。
5. 全连接层
layers.Flatten()
layers.Dense(64, activation='relu')
layers.Dense(10, activation='softmax')
Flatten:把最后的高维特征图(如3x3x64)压成一维向量(如576个数字)。
Dense层:像传统神经网络一样,综合所有特征判断数字是0~9中的哪一个。
Softmax:输出10个概率值,总和为1(例如“有80%概率是数字5”)。
运行过程可视化
假设输入是一个手写数字“5”:
第一层卷积:识别出“5”的顶部横线、右下弯曲线条。
特征图可能显示这些线条的位置(激活区域高亮)。
池化后:缩小图像,保留这些线条的大致区域。
第二层卷积:识别出“横线+弯曲线”组合成的“上半圆”和“下半竖线”。
全连接层:综合这些高级特征,判断这是数字“5”。
为什么代码要这样写?
卷积层堆叠:逐步提取从简单到复杂的特征。
池化层穿插:减少计算量,增强鲁棒性。
通道数递增:浅层用较少滤波器(如32),深层用更多(如64),因为高层特征更复杂。
全连接收尾:将空间特征转换为分类决策。
试试改变参数!
修改滤波器数量:
Conv2D(16, ...)
→ 观察准确率变化。移除池化层:模型可能过拟合(记住训练集细节,但测试集表现差)。
增加卷积层:模型可以识别更复杂的图案,但需要更多数据和计算资源。
通过这段代码,你可以直观地看到CNN如何从像素到边缘,再到形状,最后识别出数字的整个过程