CNN 深度学习指南 - 代码驱动学习

本教程采用代码驱动的学习方式,每个模块按照以下结构组织:

  1. 概念介绍:解释涉及的核心概念
  2. 代码实现:可直接运行的代码片段
  3. 原理解释:从"是什么"到"为什么"的深入讲解
  4. 常见问题:针对初学者的疑问解答

目录


模块1:数据加载

1.1 核心概念

MNIST数据集

MNIST是一个手写数字识别数据集,包含70,000张28×28像素的灰度图像,分为:

  • 训练集:60,000张图片
  • 测试集:10,000张图片
  • 标签:每张图对应一个0-9的数字

它是机器学习领域的标准入门数据集,类似于编程中的"Hello World"。

数据结构

X_train: 图像数据,形状(60000, 28, 28),类型uint8,值域[0, 255]
y_train: 标签数据,形状(60000,),类型uint8,值域[0, 9]

1.2 代码实现

import numpy as np
import matplotlib.pyplot as plt
from tensorflow import keras
import matplotlib

# 设置中文字体(Windows)
matplotlib.rc('font', family='Microsoft YaHei')
matplotlib.rc('axes', unicode_minus=False)

# 加载 MNIST 数据集
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()

# 查看数据基本信息
print(f"训练集: {X_train.shape}")   # (60000, 28, 28)
print(f"测试集: {X_test.shape}")    # (10000, 28, 28)
print(f"像素值范围: [{X_train.min()}, {X_train.max()}]")  # [0, 255]

# 可视化前10张图片
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flat):
    ax.imshow(X_train[i], cmap='gray')
    ax.set_title(f"标签: {y_train[i]}")
    ax.axis('off')
plt.show()

1.3 原理解释

为什么要加载MNIST?

MNIST作为标准数据集有以下优势:

  1. 数据量适中:60,000张训练图足够训练模型,又不会太大导致计算困难
  2. 任务明确:10分类问题,便于理解和评估
  3. 基准对比:有大量公开结果可以对比模型性能

数据形状的含义

X_train.shape = (60000, 28, 28)

这表示:

  • 第一个维度60000:样本数量(60,000张图片)
  • 第二个维度28:图像高度(28像素)
  • 第三个维度28:图像宽度(28像素)

这种3维数组结构是NumPy的标准表示方式,便于批量处理。

可视化的作用

可视化前10张图片的目的是:

  1. 验证数据正确性:确认图片确实是手写数字
  2. 了解数据特征:观察数字的书写风格、清晰度等
  3. 发现潜在问题:检查是否有异常样本或标签错误

1.4 常见问题

问:为什么选择28×28这个尺寸?

答:这是MNIST数据集的原始规格。28×28对于手写数字识别来说是一个合理的尺寸:

  • 足够大:能保留数字的关键特征
  • 足够小:计算成本低,适合快速实验

如果使用自己的图片,需要调整到这个尺寸。

问:数据类型uint8是什么意思?

答:uint8是无符号8位整数,取值范围0-255。这正是灰度图像的标准表示:

  • 0表示纯黑色
  • 255表示纯白色
  • 中间值表示不同程度的灰色

在预处理时我们会将其转换为浮点数并归一化到[0, 1]范围。


模块2:数据预处理

2.1 核心概念

归一化(Normalization)

归一化是将数据缩放到特定范围(通常是0-1)的过程。对于图像数据,就是将像素值从[0, 255]转换为[0.0, 1.0]。

通道维度(Channel Dimension)

在深度学习中,图像通常表示为4维张量:(batch_size, height, width, channels)。

  • batch_size: 批次大小,一次处理的样本数
  • height: 图像高度
  • width: 图像宽度
  • channels: 通道数,灰度图为1,彩色图(RGB)为3

MNIST原始数据是3维的(batch, height, width),需要添加通道维度变成4维才能输入CNN。

2.2 代码实现

# 步骤1:归一化(像素值从 0-255 变为 0-1)
X_train = X_train / 255.0
X_test = X_test / 255.0

# 步骤2:增加通道维度(CNN 要求)
X_train = X_train.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)

# 验证预处理结果
print(f"预处理后训练集形状: {X_train.shape}")  # (60000, 28, 28, 1)
print(f"像素值范围: [{X_train.min()}, {X_train.max()}]")  # [0.0, 1.0]

2.3 原理解释

为什么要归一化?

归一化有两个主要作用:

  1. 加速模型收敛

    当输入特征在不同尺度时,损失函数的等高线会呈现椭圆形,梯度下降会呈锯齿状前进,收敛缓慢。归一化后,等高线接近圆形,梯度可以直接指向最优解。

  2. 避免数值问题

    • 防止激活值过大导致梯度爆炸
    • 使权重更新更加稳定和均衡
    • 便于设置学习率等超参数

为什么要增加通道维度?

Conv2D层要求输入必须是4维张量,这是因为卷积操作需要在通道维度上进行。即使灰度图只有1个通道,也需要显式指定。

# 原始形状: (60000, 28, 28) - 3维数组
# CNN要求: (60000, 28, 28, 1) - 4维张量
#                              ↑
#                         必须指定通道数

# reshape(-1, 28, 28, 1)中的-1表示自动推断该维度大小
# 等价于reshape(60000, 28, 28, 1)

彩色图的处理

如果是RGB彩色图,已经有3个通道,不需要reshape,只需归一化:

# RGB图像形状: (height, width, 3)
image_rgb = image_rgb / 255.0  # 直接归一化即可

2.4 常见问题

问:如果不归一化会发生什么?

答:模型仍然可以训练,但会出现以下问题:

  • 收敛速度显著变慢,可能需要更多epochs
  • 梯度更新不稳定,可能在最优解附近震荡
  • 需要更小的学习率,进一步延长训练时间

问:为什么用255.0而不是255?

答:使用255.0(浮点数)确保除法结果是浮点数。如果用255(整数),在某些编程语言中可能会进行整数除法导致精度丢失。Python中两者效果相同,但使用255.0更明确表达意图。

问:可以用其他归一化方法吗?

答:可以。常见的归一化方法包括:

  • Min-Max归一化:(x - min) / (max - min),将数据映射到[0, 1]
  • Z-Score标准化:(x - mean) / std,使数据均值为0,标准差为1
  • 对于图像数据,Min-Max归一化(除以255)是最常用的方法

模块3:模型构建

3.1 核心概念

卷积神经网络(CNN)

CNN是一种专门用于处理网格状数据(如图像)的神经网络,其核心思想是通过卷积核提取局部特征,并通过多层堆叠学习层次化的特征表示。

主要组件:

  1. 卷积层(Conv2D):使用滤波器扫描图像,提取局部特征(边缘、纹理等)
  2. 池化层(MaxPooling):对特征图进行下采样,减少计算量并增强鲁棒性
  3. 全连接层(Dense):整合所有特征,做出最终分类决策
  4. Dropout:随机丢弃部分神经元,防止过拟合

3.2 代码实现

from tensorflow.keras import layers, models

model = models.Sequential([
    # 第一层卷积 + 池化
    layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
    layers.MaxPooling2D(2, 2),
    
    # 第二层卷积 + 池化
    layers.Conv2D(64, (3,3), activation='relu'),
    layers.MaxPooling2D(2, 2),
    
    # 展平 + 全连接层
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.3),
    layers.Dense(10, activation='softmax')
])

# 查看模型结构
model.summary()

输出示例:

Model: "sequential"
_________________________________________________________________
Layer (type)                Output Shape              Param #   
=================================================================
conv2d (Conv2D)             (None, 26, 26, 32)        320       
max_pooling2d               (None, 13, 13, 32)        0         
conv2d_1 (Conv2D)           (None, 11, 11, 64)        18496     
max_pooling2d_1             (None, 5, 5, 64)          0         
flatten (Flatten)           (None, 1600)              0         
dense (Dense)               (None, 128)               204928    
dropout (Dropout)           (None, 128)               0         
dense_1 (Dense)             (None, 10)                1290      
=================================================================
Total params: 225,034

3.3 原理解释

整体架构

输入(28×28×1) 
  ↓
[Conv2D(32) → MaxPool]  # 提取基础特征,降维
  ↓ (26×26×32 → 13×13×32)
[Conv2D(64) → MaxPool]  # 提取高级特征,再降维
  ↓ (11×11×64 → 5×5×64)
Flatten                  # 展平为一维向量
  ↓ (1600)
Dense(128)               # 整合特征
  ↓ (128)
Dropout(0.3)             # 防止过拟合
  ↓ (128)
Dense(10) + Softmax      # 输出10个类别的概率
  ↓ (10)
输出: [P(0), P(1), ..., P(9)]

逐层详解

1. Conv2D(32, (3,3), activation=‘relu’)

这是什么?

Conv2D是二维卷积层,使用多个滤波器(filter)在图像上滑动,通过卷积运算提取局部特征。

参数含义:

  • 32: 滤波器数量,即输出特征图的通道数
  • (3,3): 滤波器尺寸,3×3的感受野
  • activation=‘relu’: 激活函数,引入非线性
  • input_shape=(28,28,1): 输入数据的形状(仅在第一层需要指定)

工作原理:

卷积运算是将滤波器与图像局部区域进行对应位置相乘再求和:

图像局部:          滤波器:
[0.0, 0.5, 0.2]   [w1, w2, w3]
[0.8, 1.0, 0.7] × [w4, w5, w6]
[0.3, 0.9, 0.4]   [w7, w8, w9]

结果 = 0.0*w1 + 0.5*w2 + ... + 0.4*w9

滤波器在图像上从左到右、从上到下依次滑动,生成一张特征图。32个滤波器生成32张特征图。

输出尺寸计算:

输出尺寸 = (输入尺寸 - 滤波器尺寸) / 步长 + 1
         = (28 - 3) / 1 + 1
         = 26

所以输出形状为: (26, 26, 32)

为什么用ReLU?

ReLU(Rectified Linear Unit)函数定义为 f(x) = max(0, x):

  • 引入非线性,让网络能学习复杂模式
  • 解决梯度消失问题(相比sigmoid/tanh)
  • 计算简单,训练速度快
2. MaxPooling2D(2, 2)

这是什么?

最大池化层,在每个2×2区域内取最大值,将特征图尺寸减半。

工作原理:

2×2 区域:
[0.3, 0.8]
[0.5, 0.2]

取最大值: max(0.3, 0.8, 0.5, 0.2) = 0.8

作用:

  1. 降维:减少计算量(像素减少75%)
  2. 保留主要特征:最显著的激活值
  3. 增强鲁棒性:对小位移不敏感

为什么用2×2?

2×2是平衡信息保留和降维的最佳选择:

  • 1×1没有降维效果
  • 3×3或更大丢失太多空间信息
3. Conv2D(64, (3,3), activation=‘relu’)

这和第一层有什么区别?

第一层处理的是原始像素,检测基础特征(边缘、线条);第二层处理的是第一层的特征图,检测组合特征(角点、形状)。

关键差异:

第一层:
  输入: 原始像素 (28×28×1)
  滤波器: 3×3×1(32)
  学习: 基础特征(边缘、线条)

第二层:
  输入: 第一层的特征图 (13×13×32)
  滤波器: 3×3×32(64) ← 注意深度是32!
  学习: 组合特征(角点、形状)

第二层的滤波器深度是32,因为它需要同时考虑第一层输出的32个通道。

为什么要增加到64个滤波器?

  • 深层需要检测更多组合特征
  • 补偿空间维度的损失(13→11)
  • 随着网络加深,通常会增加滤波器数量
4. Flatten()

这是什么?

展平层,将3D特征图拉成1D向量,作为全连接层的输入。

数据变化:

输入: (5, 5, 64)
输出: (1600,)

计算: 5 × 5 × 64 = 1600

为什么要展平?

  • 卷积层输出:3D张量(保留空间信息)
  • 全连接层输入:1D向量(整合全局信息)
  • 展平是从"特征提取"到"分类决策"的桥梁
5. Dense(128, activation=‘relu’)

这是什么?

全连接层,每个神经元与输入的所有值相连,学习特征的组合模式。

工作原理:

每个输出神经元:
h_i = relu(w_i1*x1 + w_i2*x2 + ... + w_i1600*x1600 + b_i)

128个神经元学习128种"特征组合模式":
- 模式1: "上面有圆弧 + 下面有直线" → 可能是5
- 模式2: "两个封闭圆圈" → 可能是8
- ...

参数量计算:

权重矩阵: (1600, 128) = 204,800 个参数
偏置向量: (128,) = 128 个参数
总计: 204,928 个参数

重要澄清:权重矩阵的真相

每个权重参数都是独立的,通过反向传播独立更新:

# 正确理解:每个值都是独立的
W = [[w_1_1, w_1_2, ..., w_1_128],   # 第1行
     [w_2_1, w_2_2, ..., w_2_128],   # 第2行(完全不同)
     ...
     [w_1600_1, ..., w_1600_128]]    # 第1600行

总参数: 1600 × 128 = 204,800 个独立权重
6. Dropout(0.3)

这是什么?

Dropout是一种正则化技术,在训练时随机"关闭"30%的神经元。

工作原理:

训练时: [0.5, 0.8, 0.0, 0.9, ...] → [0.5, 0.0, 0.0, 0.9, ...]
测试时: 所有神经元工作,但输出 × 0.7

为什么要这样做?

想象团队决策:

  • 没有Dropout:依赖某几个"明星员工",这些人不在就瘫痪了 → 过拟合
  • 有Dropout:每次随机让人休息,迫使每个人独立工作 → 提高泛化能力
7. Dense(10, activation=‘softmax’)

这是什么?

输出层,产生10个概率值,对应数字0-9的分类结果。

Softmax函数:

Softmax将任意实数转换为概率分布:

softmax(xi) = e^xi / Σ(e^xj)

步骤1: 取指数
exp_z = [e^2.0, e^1.0, e^0.1, ...] = [7.39, 2.72, 1.11, ...]

步骤2: 求和
sum_exp = 7.39 + 2.72 + 1.11 + ... = 39.95

步骤3: 归一化
probabilities = [7.39/39.95, 2.72/39.95, ...]
              = [0.185, 0.068, ...]

为什么用Softmax?

  1. 转换为概率(可解释为置信度)
  2. 总和为1(符合概率分布)
  3. 放大差异(便于决策)

3.4 常见问题

问:为什么第二层用64个滤波器而不是63?

答:63也可以!64只是对GPU更友好(warp大小=32)。实际效果几乎一样。滤波器数量可以根据任务复杂度调整,常见选择是32、64、128、256等。

问:滤波器是预先设定的吗?

答:不是!初始化时是随机值,通过训练学习得到。不同的滤波器会学习到不同的特征。

问:为什么不用单层卷积?

答:多层可以层次化学习特征(边缘→形状→部件),效果更好且参数更少。深层网络可以捕获更抽象的特征。

问:可以用其他尺寸的卷积核吗?

答:可以!如(5,5)、(1,5)、(5,1)。3×3是最常用的,因为参数量少且效果好。更大的卷积核可以捕获更大范围的特征,但计算成本更高。


模块4:模型训练

4.1 核心概念

编译(Compile)

编译是配置模型训练过程的关键步骤,需要指定:

  • 优化器(optimizer):如何更新权重
  • 损失函数(loss):如何衡量预测误差
  • 评估指标(metrics):如何评价模型性能

训练(Fit)

训练是让模型从数据中学习的过程,通过多次迭代(epoch)逐步优化权重。

关键术语:

  • epoch: 整个训练集遍历一次
  • batch_size: 每次更新权重使用的样本数
  • validation_split: 用于监控过拟合的验证集比例

4.2 代码实现

# 编译模型
model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# 训练模型
history = model.fit(
    X_train, y_train,
    epochs=5,           # 训练5轮
    batch_size=128,     # 每批128个样本
    validation_split=0.1,  # 10%数据作为验证集
    verbose=1           # 显示进度
)

# 可视化训练过程
plt.figure(figsize=(12, 4))
plt.subplot(1,2,1)
plt.plot(history.history['accuracy'], label='训练准确率')
plt.plot(history.history['val_accuracy'], label='验证准确率')
plt.legend(); plt.title('准确率变化')

plt.subplot(1,2,2)
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.legend(); plt.title('损失变化')
plt.show()

训练输出示例:

Epoch 1/5
422/422 [==============================] - 10s 23ms/step - loss: 0.3245 - accuracy: 0.9012 - val_loss: 0.1456 - val_accuracy: 0.9567

Epoch 2/5
422/422 [==============================] - 9s 22ms/step - loss: 0.1123 - accuracy: 0.9678 - val_loss: 0.0876 - val_accuracy: 0.9734

...

Epoch 5/5
422/422 [==============================] - 9s 22ms/step - loss: 0.0456 - accuracy: 0.9867 - val_loss: 0.0567 - val_accuracy: 0.9823

4.3 原理解释

编译模型的三个关键参数

optimizer=‘adam’

Adam优化器结合了Momentum和RMSprop的优点:

  • 自适应调整学习率
  • 对超参数不敏感,默认设置通常就很好
  • 适合大多数深度学习任务
loss=‘sparse_categorical_crossentropy’

稀疏交叉熵损失,用于多分类任务:

  • "sparse"表示标签是整数形式(0-9),而非one-hot编码
  • 衡量预测概率分布与真实分布的差异
  • 值越低表示预测越准确
# sparse_categorical_crossentropy:标签为整数
y_train = [3, 5, 2, ...]  

# categorical_crossentropy:标签为one-hot
y_train = [[0,0,0,1,0,...], [0,0,0,0,0,1,...], ...]
metrics=[‘accuracy’]

准确率:预测正确的样本比例,直观易懂的业务指标。

训练参数的含义

epochs=5

整个训练集遍历5次。MNIST简单任务,5轮通常足够。

batch_size=128

每次更新权重使用128个样本。小批量梯度下降:平衡内存效率和收敛稳定性。

validation_split=0.1

从训练集中划分10%作为验证集,用于监控过拟合。

梯度下降原理

目标:找到让损失最小的权重值。

# 梯度下降公式
W_new = W_old - learning_rate * gradient

# 例如:
W_old = 0.5
learning_rate = 0.001  # Adam 默认值
gradient = 2.0

W_new = 0.5 - 0.001 * 2.0 = 0.498

反向传播过程:

前向:输入 → Conv → Pool → ... → 输出 → 计算Loss
反向:Loss ← 梯度 ← ... ← Pool ← Conv ← 输入
更新:W = W - lr * gradient

TensorFlow自动完成这个过程,无需手动实现。

如何解读训练曲线?

理想情况:

  • 训练准确率和验证准确率都上升,且差距小
  • 训练损失和验证损失都下降,且接近

过拟合迹象:

  • 训练准确率持续上升,但验证准确率停滞或下降
  • 训练损失持续下降,但验证损失开始上升

欠拟合迹象:

  • 训练准确率和验证准确率都很低

4.4 常见问题

问:为什么需要验证集?

答:监控过拟合。如果训练性能好但验证性能差,说明模型记住了训练数据而非学习规律。

问:batch_size选多大合适?

答:32-256是常见范围。128是平衡速度和性能的不错选择。较小的batch_size可能带来更好的泛化,但训练较慢。

问:训练多少轮合适?

答:观察验证集准确率。不再提升时可以停止(早停)。可以使用EarlyStopping回调自动实现。


模块5:模型评估

5.1 核心概念

评估(Evaluate)

在未见过的测试集上评估模型性能,反映模型的泛化能力。

预测(Predict)

使用训练好的模型对新数据进行预测,输出每个类别的概率。

错误分析

分析预测错误的样本,理解模型局限性,发现改进方向。

5.2 代码实现

# 测试集评估
test_loss, test_acc = model.evaluate(X_test, y_test)
print(f"\n测试集准确率: {test_acc:.4f}")  # 应该能到 99%+

# 找出预测错误的样本
predictions = model.predict(X_test)
predicted_labels = np.argmax(predictions, axis=1)

wrong_idx = np.where(predicted_labels != y_test)[0]
print(f"共预测错误: {len(wrong_idx)} 张")

# 展示几张错误的
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat[:len(wrong_idx[:10])]):
    ax.imshow(X_test[wrong_idx[i]].reshape(28,28), cmap='gray')
    ax.set_title(f"真实:{y_test[wrong_idx[i]]} 预测:{predicted_labels[wrong_idx[i]]}", color='red')
    ax.axis('off')
plt.show()

输出示例:

313/313 [==============================] - 2s 6ms/step - loss: 0.0523 - accuracy: 0.9834

测试集准确率: 0.9834
共预测错误: 166 张

5.3 原理解释

评估指标

准确率(Accuracy)
准确率 = 正确预测数 / 总样本数
       = 9834 / 10000
       = 98.34%
损失(Loss)

交叉熵损失:衡量预测概率分布与真实分布的差异,越低越好。

错误分析的价值

为什么要看错误样本?

  • 理解模型局限性
  • 发现改进方向
  • 业务洞察比单纯看准确率更重要

常见错误类型:

  • 书写不规范(潦草的7被识别为1)
  • 图像模糊或残缺
  • 相似数字混淆(4↔9,3↔5,7↔1)

5.4 常见问题

问:为什么测试准确率比验证准确率低?

答:测试集是完全未见过的数据,更能反映真实性能。验证集来自训练集,可能与训练集有相似性。

问:如何提高准确率?

答:

  1. 增加训练轮数
  2. 数据增强(旋转、缩放、平移)
  3. 增加模型复杂度(更多卷积层)
  4. 调整超参数(学习率、batch_size等)

问:99%准确率够好吗?

答:取决于应用场景。医疗、金融可能需要99.9%+,普通应用98%就够了。关键是理解那1%的错误发生在什么情况下。


核心原理速查

数据流动总览

输入(28×28×1) 
  ↓ Conv2D(32, 3×3)
(26×26×32)  # 32张特征图,检测基础特征
  ↓ MaxPool(2×2)
(13×13×32)  # 降维
  ↓ Conv2D(64, 3×3)
(11×11×64)  # 64张特征图,检测组合特征
  ↓ MaxPool(2×2)
(5×5×64)    # 再降维
  ↓ Flatten
(1600,)     # 展平
  ↓ Dense(128)
(128,)      # 抽象特征
  ↓ Dropout(0.3)
(128,)      # 防过拟合
  ↓ Dense(10) + Softmax
(10,)       # 10个概率

关键概念对照表

概念 作用 类比 常见问题
卷积 检测局部特征 模具压印 滤波器数量、尺寸怎么选?
池化 降维,保留主要信息 照片压缩 为什么用2×2?
ReLU 引入非线性 开关 会丢失信息吗?
展平 3D→1D 拆书排成一排 为什么要展平?
全连接 整合特征做决策 综合证据 Dense(128)的意义?
Dropout 防止过拟合 随机让人休息 为什么有效?
Softmax 转换为概率 分数变百分比 为什么用这个?
梯度 指引优化方向 下山导航仪 怎么计算的?

超参数调优指南

参数 推荐值 调整策略
滤波器数量 32→64→128 简单任务少一些,复杂任务多一些
卷积核大小 3×3 可用 5×5、1×5、5×1
池化尺寸 2×2 大图像可用 4×4
学习率 0.001 (Adam默认) 震荡则减小,收敛慢则增大
batch_size 128 32-256 之间调整
epochs 5-20 观察验证集,早停
Dropout 0.3-0.5 过拟合则增大

常见问题排查

问题1: 准确率只有 90%?

检查清单:

  • 归一化: X_train.min(), X_train.max() 应该是 [0.0, 1.0]
  • 形状: X_train.shape 应该是 (60000, 28, 28, 1)
  • 增加 epochs: 从 5 增加到 10
  • 检查标签: y_train 应该是 0-9 的整数

问题2: 训练时损失不下降?

解决方法:

  1. 减小学习率: Adam(learning_rate=0.0001)
  2. 检查数据是否归一化
  3. 简化模型测试(先去掉一些层)

问题3: 出现过拟合?

解决方法:

  1. 增加 Dropout: 从 0.3 到 0.5
  2. 数据增强: 旋转、缩放、平移
  3. 减少模型复杂度
  4. 早停: EarlyStopping(patience=3)

完整代码

以下是完整的MNIST手写数字识别代码:

import numpy as np
import matplotlib.pyplot as plt
from tensorflow import keras
import matplotlib

# 设置中文字体(Windows)
matplotlib.rc('font', family='Microsoft YaHei')
matplotlib.rc('axes', unicode_minus=False)

# MNIST 数据集,第一次会自动下载(~11MB)
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()

# 打印原始数据的形状和范围
print(f"训练集图像形状: {X_train.shape}, 数据类型: {X_train.dtype}")
print(f"训练集标签形状: {y_train.shape}")
print(f"像素值范围: [{X_train.min()}, {X_train.max()}]")
print(f"前5个标签: {y_train[:5]}")

print(f"训练集: {X_train.shape}")   # (60000, 28, 28)
print(f"测试集: {X_test.shape}")    # (10000, 28, 28)

# 看几张图
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flat):
    ax.imshow(X_train[i], cmap='gray')
    ax.set_title(f"标签: {y_train[i]}")
    ax.axis('off')
plt.show()

# 像素值归一化到 0-1(原来是 0-255)
X_train = X_train / 255.0
X_test = X_test / 255.0

# 增加通道维度(CNN 需要)
X_train = X_train.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)


from tensorflow.keras import layers, models

model = models.Sequential([
    # 第一层卷积:提取边缘、线条等特征
    layers.Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)),
    layers.MaxPooling2D(2, 2),

    # 第二层卷积:提取更复杂的特征
    layers.Conv2D(64, (3,3), activation='relu'),
    layers.MaxPooling2D(2, 2),

    # 展平后接全连接层
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.3),     # 防过拟合
    layers.Dense(10, activation='softmax')   # 10个数字
])

model.summary()   # 打印模型结构

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

history = model.fit(
    X_train, y_train,
    epochs=5,           # 训练5轮,大概5分钟
    batch_size=128,
    validation_split=0.1,
    verbose=1
)

# 画训练曲线
plt.figure(figsize=(12, 4))
plt.subplot(1,2,1)
plt.plot(history.history['accuracy'], label='训练准确率')
plt.plot(history.history['val_accuracy'], label='验证准确率')
plt.legend(); plt.title('准确率变化')

plt.subplot(1,2,2)
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.legend(); plt.title('损失变化')
plt.show()

# 测试集评估
test_loss, test_acc = model.evaluate(X_test, y_test)
print(f"\n测试集准确率: {test_acc:.4f}")  # 应该能到 99%+

# 找出预测错误的样本,很有意思
predictions = model.predict(X_test)
predicted_labels = np.argmax(predictions, axis=1)

wrong_idx = np.where(predicted_labels != y_test)[0]
print(f"共预测错误: {len(wrong_idx)} 张")

# 展示几张错误的
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat[:len(wrong_idx[:10])]):
    ax.imshow(X_test[wrong_idx[i]].reshape(28,28), cmap='gray')
    ax.set_title(f"真实:{y_test[wrong_idx[i]]} 预测:{predicted_labels[wrong_idx[i]]}", color='red')
    ax.axis('off')
plt.show()

参考资料

  1. MNIST 数据集官网
  2. Keras 官方文档
  3. TensorFlow 教程
  4. 深度学习花书
  5. CS231n: CNN for Visual Recognition

总结

学习路径

  1. 运行代码,看到结果
  2. 理解每个模块的作用
  3. 深入原理,回答"为什么"
  4. 实验调参,培养直觉

核心思想

  • 卷积:局部检测,参数共享
  • 池化:降维,保留主要信息
  • 多层:层次化特征学习
  • 全连接:整合特征,做出决策
  • 梯度下降:沿着梯度反方向优化

下一步

  • CIFAR-10:彩色图像分类
  • 数据增强:提升小数据集性能
  • 迁移学习:利用预训练模型
  • 目标检测:YOLO、Faster R-CNN
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐