第22课:TensorFlow|TF搭建基础CNN网络【卷积层、池化层、全连接层组合实战】

在这里插入图片描述


1. 课前导读

1.1 本节课学习目标

  • 掌握使用Conv2DMaxPooling2DFlattenDense等层构建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的Sequential API和函数式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 维度不匹配

  • 坑1Flatten后接Dense时,未正确计算展平后的维度,导致ValueError: Shapes ... incompatible

    • 解决:使用model.summary()检查,确保Flatten前的特征图尺寸合理。
  • 坑2Conv2Dfilters(输出通道数)误写为kernel_size的参数顺序。

    • 注意Conv2D(filters, kernel_size, ...)

6.2 池化层使用错误

  • 坑3:池化层pool_sizestrides默认为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 基础作业

  1. 修改CIFAR-10的CNN架构,将第一个卷积块的卷积核数量从32改为64,观察参数量和准确率变化。
  2. 在MNIST CNN中去掉一个池化层,保持其他不变,比较最终准确率。
  3. 使用函数式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 思考拓展题

  1. 全局平均池化(GlobalAveragePooling2D)与Flatten + Dense相比有何优势?在什么情况下更适合?

  2. 对于224×224的大尺寸图像(如ImageNet),如果直接使用5×5卷积核,参数量会很大。有哪些常见技巧可以减少参数量?(提示:小卷积核堆叠、深度可分离卷积)

  3. 在CNN中,为什么通常浅层使用较少的卷积核,深层使用较多的卷积核?这与特征图的尺寸和抽象程度有何关系?


下一课预告:经典卷积网络模型精讲——我们将拆解LeNet、AlexNet等经典结构的创新点,并复现这些模型,理解CNN的发展脉络。


🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航

去订阅

第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值