
文章目录
1. 课前导读
1.1 本节课学习目标
- 掌握使用
Conv2D、MaxPooling2D、Flatten、Dense等层构建CNN的标准流程。 - 理解卷积层、池化层、全连接层在CNN中的角色分工及组合顺序。
- 学会设计适用于不同规模图像数据集的CNN架构(浅层/深层)。
- 掌握批归一化层(BatchNormalization)在CNN中的添加位置及其对训练稳定性的提升。
- 能够通过
model.summary()分析网络参数量及每层输出形状,诊断维度不匹配错误。 - 完成CIFAR-10图像分类任务,并通过调整网络深度、卷积核数量等超参数优化性能。
1.2 知识重难点
| 类别 | 内容 |
|---|---|
| 重点 | CNN标准模块:Conv2D + BatchNormalization + Activation + MaxPooling2D;特征图尺寸逐层递减规律;全连接层前的Flatten |
| 难点 | 深层CNN中梯度消失的缓解(BatchNormalization、残差连接铺垫);卷积核数量与特征图通道数的设计权衡 |
| 易混淆点 | padding='same'与'valid'对输出尺寸的影响;批归一化放在激活之前还是之后;Dropout在CNN中的常用位置(全连接层之前) |
1.3 学习前置条件
- 已掌握第21课的CNN核心原理(卷积、池化)。
- 熟悉TensorFlow的
SequentialAPI和函数式API(第12、15课)。 - 能够使用
tf.data加载图像数据集(第16课)。
1.4 学完可掌握能力
- 独立搭建CNN解决图像分类、物体识别等视觉任务。
- 根据输入图像尺寸和分类类别数,设计合理的网络深度与宽度。
- 快速实现模型的原型验证,并通过调整超参数提升准确率。
- 利用可视化手段(特征图、卷积核)分析模型学习情况。
1.5 行业应用场景
- 图像分类:人脸识别、场景识别、产品分类。
- 目标检测基础:CNN作为特征提取器。
- 图像检索:提取CNN特征进行相似度匹配。
- 医学影像分析:X光、CT图像分类。
2. 核心理论精讲
2.1 CNN标准组件及组合顺序
一个典型的CNN由以下组件交替堆叠:
- 卷积层(Conv2D):提取局部特征,增加通道数。
- 激活函数:引入非线性(通常使用ReLU)。
- 批归一化(BatchNormalization):加速收敛,允许更大学习率(可选但推荐)。
- 池化层(MaxPooling2D):降低空间尺寸,增强平移不变性。
- 全连接层(Dense):在特征图展平后进行分类或回归。
标准模块:Conv2D -> BatchNormalization -> Activation -> MaxPooling2D。重复多次后,接Flatten -> Dense -> Dropout -> Dense。
2.2 特征图尺寸变化的规律
假设输入尺寸为 ( H \times W \times C ),卷积层参数:核大小 ( k ),步长 ( s ),填充 ( p )(每边填充数)。输出尺寸:
[
H’ = \left\lfloor \frac{H + 2p - k}{s} \right\rfloor + 1, \quad W’ = \left\lfloor \frac{W + 2p - k}{s} \right\rfloor + 1
]
池化层类似(通常 ( k = s = 2 ),输出尺寸减半)。
全连接层前需要将多维特征图展平为一维,维度 = ( H’ \times W’ \times C’ )。
设计原则:空间尺寸应逐步减小,通道数逐步增加(通常每池化一次通道数翻倍)。最后一层卷积后的特征图尺寸不宜过小(至少4×4),否则丢失空间信息。
2.3 批归一化(BatchNormalization)在CNN中的使用
批归一化对每个通道的激活值进行标准化(均值为0、方差为1),然后学习缩放和平移参数 ( \gamma, \beta )。在CNN中,通常放在卷积层之后、激活函数之前(Conv2D -> BN -> ReLU),也有放在激活之后的实践,但前者更常见。
优点:
- 缓解梯度消失/爆炸。
- 减少对初始化的依赖。
- 允许更大的学习率,加速训练。
- 轻微的正则化效果。
2.4 Dropout在CNN中的位置
由于卷积层参数量相对较少,且池化层已有一定正则化效果,Dropout通常只加在全连接层之前,防止过拟合。全连接层参数量大,易过拟合,Dropout率可设为0.5。现代CNN(如ResNet)常使用全局平均池化替代全连接层,进一步减少参数。
2.5 常见CNN架构模式
- 浅层CNN(MNIST):2个卷积+池化,然后全连接。参数量小,训练快。
- 深层CNN(CIFAR-10):3~4个卷积块,每块2个卷积+1个池化,通道数从32逐渐增加到256。
- 经典架构:LeNet-5、AlexNet、VGG等(后续课程详解)。
3. 环境搭建与工具配置
沿用第21课环境。确保已安装TensorFlow 2.x和Matplotlib。
conda activate tf213
python
导入模块:
import tensorflow as tf
from tensorflow.keras import layers, models, datasets, callbacks
import numpy as np
import matplotlib.pyplot as plt
4. 代码实战教学
4.1 浅层CNN:MNIST手写数字分类
# 加载MNIST
(x_train, y_train), (x_test, y_test) = datasets.mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1).astype(np.float32) / 255.0
x_test = x_test.reshape(-1, 28, 28, 1).astype(np.float32) / 255.0
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
# 构建浅层CNN
model_mnist = 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.Dense(10, activation='softmax')
])
model_mnist.summary()
# 训练
model_mnist.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
history_mnist = model_mnist.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.1, verbose=1)
# 评估
test_loss, test_acc = model_mnist.evaluate(x_test, y_test, verbose=0)
print(f"MNIST Test Accuracy: {test_acc:.4f}")
4.2 深层CNN:CIFAR-10彩色图像分类(带BN和Dropout)
# 加载CIFAR-10
(x_train, y_train), (x_test, y_test) = datasets.cifar10.load_data()
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
# 构建深层CNN
def build_cifar_cnn():
model = models.Sequential()
# Block 1
model.add(layers.Conv2D(32, (3,3), padding='same', input_shape=(32,32,3)))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.Conv2D(32, (3,3), padding='same'))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Dropout(0.2))
# Block 2
model.add(layers.Conv2D(64, (3,3), padding='same'))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.Conv2D(64, (3,3), padding='same'))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Dropout(0.3))
# Block 3
model.add(layers.Conv2D(128, (3,3), padding='same'))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.Conv2D(128, (3,3), padding='same'))
model.add(layers.BatchNormalization())
model.add(layers.Activation('relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Dropout(0.4))
# 分类头
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(10, activation='softmax'))
return model
cifar_model = build_cifar_cnn()
cifar_model.summary()
# 使用学习率调度和早停
lr_schedule = callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6)
early_stop = callbacks.EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
cifar_model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
history_cifar = cifar_model.fit(x_train, y_train, epochs=50, batch_size=64,
validation_split=0.1, callbacks=[lr_schedule, early_stop], verbose=1)
# 评估
test_loss, test_acc = cifar_model.evaluate(x_test, y_test, verbose=0)
print(f"CIFAR-10 Test Accuracy: {test_acc:.4f}")
4.3 函数式API搭建多输入/多输出CNN(拓展)
# 示例:使用函数式API搭建相同结构的CNN(便于后续扩展)
input_layer = layers.Input(shape=(32,32,3))
x = layers.Conv2D(32, 3, padding='same')(input_layer)
x = layers.BatchNormalization()(x)
x = layers.Activation('relu')(x)
x = layers.Conv2D(32, 3, padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.Activation('relu')(x)
x = layers.MaxPooling2D(2)(x)
x = layers.Dropout(0.2)(x)
x = layers.Conv2D(64, 3, padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.Activation('relu')(x)
x = layers.Conv2D(64, 3, padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.Activation('relu')(x)
x = layers.MaxPooling2D(2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Flatten()(x)
x = layers.Dense(256, activation='relu')(x)
x = layers.Dropout(0.5)(x)
output = layers.Dense(10, activation='softmax')(x)
functional_model = tf.keras.Model(inputs=input_layer, outputs=output)
functional_model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
functional_model.summary()
4.4 可视化训练曲线
def plot_history(history):
fig, axes = plt.subplots(1, 2, figsize=(12,4))
axes[0].plot(history.history['accuracy'], label='Train Acc')
axes[0].plot(history.history['val_accuracy'], label='Val Acc')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Accuracy')
axes[0].legend()
axes[0].set_title('Accuracy')
axes[1].plot(history.history['loss'], label='Train Loss')
axes[1].plot(history.history['val_loss'], label='Val Loss')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].legend()
axes[1].set_title('Loss')
plt.show()
plot_history(history_cifar)
4.5 特征图可视化(中间激活)
# 提取中间层的输出
layer_outputs = [layer.output for layer in cifar_model.layers if 'conv2d' in layer.name]
activation_model = tf.keras.Model(inputs=cifar_model.input, outputs=layer_outputs)
sample_img = x_test[0:1] # 取一张测试图
activations = activation_model.predict(sample_img)
# 显示第一个卷积块的特征图(前16通道)
first_conv_acts = activations[0][0] # shape (32,32,32)
plt.figure(figsize=(12,8))
for i in range(16):
plt.subplot(4,4,i+1)
plt.imshow(first_conv_acts[:,:,i], cmap='viridis')
plt.axis('off')
plt.suptitle('Feature maps after first conv layer')
plt.show()
5. 案例实操演练
案例:自定义花卉图像分类器(使用小型花卉数据集,演示从文件夹加载到CNN训练全流程)
5.1 下载与加载花卉数据集(使用tf.keras.utils.get_file)
import pathlib
# 使用TensorFlow内置的花卉数据集(5类)
dataset_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"
data_dir = tf.keras.utils.get_file(origin=dataset_url, fname='flower_photos', extract=True)
data_dir = pathlib.Path(data_dir).parent / 'flower_photos'
print(data_dir)
# 使用image_dataset_from_directory加载
batch_size = 32
img_height = 150
img_width = 150
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size
)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size
)
class_names = train_ds.class_names
print("Classes:", class_names)
5.2 数据增强与标准化
# 数据增强层(仅用于训练)
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1),
layers.RandomZoom(0.1),
])
# 归一化层
normalization_layer = layers.Rescaling(1./255)
# 构建带增强的流水线
def preprocess_train(image, label):
image = tf.cast(image, tf.float32)
image = data_augmentation(image)
image = normalization_layer(image)
return image, label
def preprocess_val(image, label):
image = tf.cast(image, tf.float32)
image = normalization_layer(image)
return image, label
train_ds = train_ds.map(preprocess_train, num_parallel_calls=tf.data.AUTOTUNE).prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.map(preprocess_val, num_parallel_calls=tf.data.AUTOTUNE).prefetch(tf.data.AUTOTUNE)
5.3 构建适配花卉数据的CNN
def build_flower_cnn():
model = models.Sequential([
layers.Conv2D(32, 3, activation='relu', input_shape=(150,150,3)),
layers.MaxPooling2D(),
layers.Conv2D(64, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(128, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(128, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Flatten(),
layers.Dropout(0.5),
layers.Dense(512, activation='relu'),
layers.Dense(len(class_names), activation='softmax')
])
return model
flower_model = build_flower_cnn()
flower_model.summary()
flower_model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
history_flower = flower_model.fit(train_ds, validation_data=val_ds, epochs=20, verbose=1)
5.4 评估与预测
# 在验证集上评估
val_loss, val_acc = flower_model.evaluate(val_ds)
print(f"Validation accuracy: {val_acc:.4f}")
# 展示预测结果
plt.figure(figsize=(10,10))
for images, labels in val_ds.take(1):
predictions = flower_model.predict(images)
for i in range(9):
ax = plt.subplot(3,3,i+1)
plt.imshow(images[i].numpy())
pred_class = class_names[tf.argmax(predictions[i])]
true_class = class_names[labels[i]]
plt.title(f'Pred: {pred_class}\nTrue: {true_class}')
plt.axis('off')
plt.show()
6. 常见坑点与排错总结
6.1 维度不匹配
-
坑1:
Flatten后接Dense时,未正确计算展平后的维度,导致ValueError: Shapes ... incompatible。- 解决:使用
model.summary()检查,确保Flatten前的特征图尺寸合理。
- 解决:使用
-
坑2:
Conv2D的filters(输出通道数)误写为kernel_size的参数顺序。- 注意:
Conv2D(filters, kernel_size, ...)。
- 注意:
6.2 池化层使用错误
-
坑3:池化层
pool_size和strides默认为pool_size,通常设置pool_size=(2,2)即可,无需额外指定步长。 -
坑4:误将
AveragePooling2D用于分类任务,丢失重要信息;通常分类使用最大池化。
6.3 BN和Dropout位置
-
坑5:BatchNormalization放在激活之后,导致标准化后的值经过非线性,效果降低。
- 推荐:
Conv2D -> BN -> Activation。
- 推荐:
-
坑6:Dropout放在卷积层之间,可能损害特征提取能力。现代实践通常只在全连接层前使用Dropout。
6.4 过拟合与欠拟合
- 坑7:训练集准确率远高于验证集,过拟合。增加Dropout率、数据增强、L2正则化。
- 坑8:两者准确率均低,欠拟合。增加网络深度/宽度,减小Dropout,增加训练轮次。
6.5 训练速度慢
- 坑9:未使用
prefetch,导致GPU空闲等待数据。始终在数据流水线末尾添加.prefetch(tf.data.AUTOTUNE)。
7. 知识点总结 + 课后作业
7.1 核心知识点梳理
- CNN组件:
Conv2D提取特征,MaxPooling2D降采样,Flatten展平,Dense分类。 - 标准模块:
Conv2D + BN + ReLU + MaxPool2D。 - 设计原则:空间尺寸逐层减半,通道数逐层翻倍;全连接层前使用Dropout。
- 训练技巧:数据增强、学习率调度、早停。
- 可视化:卷积核权重和特征图可解释模型学习内容。
7.2 基础作业
- 修改CIFAR-10的CNN架构,将第一个卷积块的卷积核数量从32改为64,观察参数量和准确率变化。
- 在MNIST CNN中去掉一个池化层,保持其他不变,比较最终准确率。
- 使用函数式API实现一个CNN,其中包含一个额外的辅助分类器(中间层输出经过Dense分类)。
7.3 进阶实操作业
任务:在CIFAR-100数据集上搭建CNN
- 加载CIFAR-100(100个类别),图像尺寸同CIFAR-10。
- 设计一个至少包含4个卷积块(每块2个卷积+BN+池化)的CNN。
- 使用Adam优化器和余弦退火学习率调度。
- 训练50个epoch,记录验证集准确率。
- 对比CIFAR-10和CIFAR-100的性能差距,分析原因。
7.4 思考拓展题
-
全局平均池化(GlobalAveragePooling2D)与
Flatten+Dense相比有何优势?在什么情况下更适合? -
对于224×224的大尺寸图像(如ImageNet),如果直接使用5×5卷积核,参数量会很大。有哪些常见技巧可以减少参数量?(提示:小卷积核堆叠、深度可分离卷积)
-
在CNN中,为什么通常浅层使用较少的卷积核,深层使用较多的卷积核?这与特征图的尺寸和抽象程度有何关系?
下一课预告:经典卷积网络模型精讲——我们将拆解LeNet、AlexNet等经典结构的创新点,并复现这些模型,理解CNN的发展脉络。
🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航
第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

1293

被折叠的 条评论
为什么被折叠?



