AI学习-CNN 深度学习指南 - 代码驱动学习
CNN 深度学习指南 - 代码驱动学习
本教程采用代码驱动的学习方式,每个模块按照以下结构组织:
- 概念介绍:解释涉及的核心概念
- 代码实现:可直接运行的代码片段
- 原理解释:从"是什么"到"为什么"的深入讲解
- 常见问题:针对初学者的疑问解答
目录
模块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作为标准数据集有以下优势:
- 数据量适中:60,000张训练图足够训练模型,又不会太大导致计算困难
- 任务明确:10分类问题,便于理解和评估
- 基准对比:有大量公开结果可以对比模型性能
数据形状的含义
X_train.shape = (60000, 28, 28)
这表示:
- 第一个维度60000:样本数量(60,000张图片)
- 第二个维度28:图像高度(28像素)
- 第三个维度28:图像宽度(28像素)
这种3维数组结构是NumPy的标准表示方式,便于批量处理。
可视化的作用
可视化前10张图片的目的是:
- 验证数据正确性:确认图片确实是手写数字
- 了解数据特征:观察数字的书写风格、清晰度等
- 发现潜在问题:检查是否有异常样本或标签错误
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 原理解释
为什么要归一化?
归一化有两个主要作用:
-
加速模型收敛
当输入特征在不同尺度时,损失函数的等高线会呈现椭圆形,梯度下降会呈锯齿状前进,收敛缓慢。归一化后,等高线接近圆形,梯度可以直接指向最优解。
-
避免数值问题
- 防止激活值过大导致梯度爆炸
- 使权重更新更加稳定和均衡
- 便于设置学习率等超参数
为什么要增加通道维度?
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是一种专门用于处理网格状数据(如图像)的神经网络,其核心思想是通过卷积核提取局部特征,并通过多层堆叠学习层次化的特征表示。
主要组件:
- 卷积层(Conv2D):使用滤波器扫描图像,提取局部特征(边缘、纹理等)
- 池化层(MaxPooling):对特征图进行下采样,减少计算量并增强鲁棒性
- 全连接层(Dense):整合所有特征,做出最终分类决策
- 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
作用:
- 降维:减少计算量(像素减少75%)
- 保留主要特征:最显著的激活值
- 增强鲁棒性:对小位移不敏感
为什么用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(符合概率分布)
- 放大差异(便于决策)
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 常见问题
问:为什么测试准确率比验证准确率低?
答:测试集是完全未见过的数据,更能反映真实性能。验证集来自训练集,可能与训练集有相似性。
问:如何提高准确率?
答:
- 增加训练轮数
- 数据增强(旋转、缩放、平移)
- 增加模型复杂度(更多卷积层)
- 调整超参数(学习率、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: 训练时损失不下降?
解决方法:
- 减小学习率: Adam(learning_rate=0.0001)
- 检查数据是否归一化
- 简化模型测试(先去掉一些层)
问题3: 出现过拟合?
解决方法:
- 增加 Dropout: 从 0.3 到 0.5
- 数据增强: 旋转、缩放、平移
- 减少模型复杂度
- 早停: 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()
参考资料
总结
学习路径
- 运行代码,看到结果
- 理解每个模块的作用
- 深入原理,回答"为什么"
- 实验调参,培养直觉
核心思想
- 卷积:局部检测,参数共享
- 池化:降维,保留主要信息
- 多层:层次化特征学习
- 全连接:整合特征,做出决策
- 梯度下降:沿着梯度反方向优化
下一步
- CIFAR-10:彩色图像分类
- 数据增强:提升小数据集性能
- 迁移学习:利用预训练模型
- 目标检测:YOLO、Faster R-CNN
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)