3、卷积神经网络(CNN)进阶:卷积层与池化层原理、经典CNN架构(ResNet/VGG)、图像分类实战、迁移学习技巧
各位同学,欢迎来到CNN进阶篇。说实话,很多初学者学完基础CNN后,会觉得「不就是卷积+池化+全连接嘛」。但真到了项目里,你会发现事情没那么简单。我当年第一次用CNN做工业质检,模型死活不收敛,折腾了两天才发现是卷积核尺寸选错了。嗯,今天咱们就把这些坑一个个填上。
3.1 卷积层:不只是「滑动窗口」
卷积层的核心操作,说白了就是用一个可学习的「小窗口」在图像上滑动,提取局部特征。但这里有几个关键参数,我建议你刻在脑子里:
- 卷积核大小(Kernel Size):常用3×3或5×5。3×3是黄金尺寸,参数量少,堆叠起来感受野还大。
- 步长(Stride):每次滑动的像素数。步长=2时,特征图尺寸直接减半。
- 填充(Padding):保持特征图尺寸不变,常用「same padding」。
- 输入输出通道数:输入是RGB三通道,输出通道数由卷积核数量决定。
重要公式:输出特征图尺寸 = (输入尺寸 - 卷积核尺寸 + 2×填充) / 步长 + 1
举个例子:输入224×224,卷积核3×3,步长1,填充1,输出还是224×224。
我在项目中遇到过一个问题:用5×5卷积核代替两个3×3卷积核,参数量反而更大。你想想看,两个3×3的参数量是2×9=18,一个5×5是25。所以现在主流架构都爱用堆叠小卷积核,而不是一个大卷积核。
3.2 池化层:降维的艺术
池化层的作用很纯粹:降低特征图尺寸,减少计算量,同时保留主要特征。常用的有两种:
| 池化类型 | 操作 | 特点 |
|---|---|---|
| 最大池化(Max Pooling) | 取窗口内最大值 | 保留纹理、边缘等强特征 |
| 平均池化(Average Pooling) | 取窗口内平均值 | 保留背景信息,更平滑 |
我的习惯:图像分类任务,我一般用最大池化。因为分类更关注「有没有这个特征」,而不是「特征有多强」。
曾经有个项目,我用平均池化替换了最大池化,准确率掉了3个点。后来发现是因为目标物体太小,平均池化把特征「平均没了」。所以池化层的选择,真的要看具体任务。
3.3 经典CNN架构:VGG与ResNet
说到经典架构,VGG和ResNet是绕不开的两座大山。我建议你先把这两个吃透,再去碰那些花里胡哨的新模型。
3.3.1 VGG:简单粗暴的堆叠
VGG的核心思想就一句话:全部用3×3卷积 + 2×2最大池化。VGG16有16层可学习参数,VGG19有19层。结构非常规整:
- 卷积层:3×3,步长1,填充1
- 池化层:2×2,步长2
- 全连接层:4096 → 4096 → 1000
VGG的优点是结构清晰,容易理解。缺点也很明显——参数量巨大,VGG16有1.38亿个参数。我记得第一次在单卡上训练VGG16,跑了三天三夜。现在想想,真是年轻不懂事。
3.3.2 ResNet:残差学习的革命
ResNet解决了深度网络退化问题。什么叫退化?就是网络越深,准确率反而下降。这不是过拟合,是梯度消失/爆炸导致的优化困难。
ResNet的核心创新是残差块(Residual Block):
# 残差块伪代码
def residual_block(x):
identity = x
out = Conv2D(64, 3, padding='same')(x)
out = BatchNormalization()(out)
out = ReLU()(out)
out = Conv2D(64, 3, padding='same')(out)
out = BatchNormalization()(out)
out = Add()([out, identity]) # 残差连接
out = ReLU()(out)
return out
说白了,就是让网络学习「残差」F(x) = H(x) - x,而不是直接学习H(x)。这样即使网络很深,梯度也能通过捷径连接直接回传。
ResNet的版本:ResNet18、ResNet34、ResNet50、ResNet101、ResNet152。数字越大,层数越多。实际项目中,ResNet50是性价比最高的选择。
3.4 图像分类实战:从零搭建一个分类器
光说不练假把式。咱们用CIFAR-10数据集,手撸一个简单的CNN分类器。CIFAR-10有10个类别,每张图32×32×3。
import tensorflow as tf
from tensorflow.keras import layers, models
model = models.Sequential([
# 第一组卷积
layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32, 32, 3)),
layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
layers.MaxPooling2D((2, 2)),
layers.Dropout(0.25),
# 第二组卷积
layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
layers.MaxPooling2D((2, 2)),
layers.Dropout(0.25),
# 全连接层
layers.Flatten(),
layers.Dense(512, activation='relu'),
layers.Dropout(0.5),
layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 训练
history = model.fit(x_train, y_train,
batch_size=64,
epochs=50,
validation_data=(x_test, y_test))
避坑指南:我曾经在训练时忘记加Dropout,结果模型在训练集上准确率99%,测试集只有70%。过拟合是图像分类的头号敌人,Dropout和早停法一定要用上。
3.5 迁移学习:站在巨人的肩膀上
迁移学习,说白了就是拿别人训练好的模型,改一改用到自己的任务上。我90%的项目都用迁移学习,因为从头训练太费时费力了。
3.5.1 为什么迁移学习有效?
预训练模型(比如在ImageNet上训练的ResNet)已经学会了通用的视觉特征——边缘、纹理、形状。这些特征对大多数图像任务都适用。你只需要微调最后几层,让它适应你的特定任务。
3.5.2 迁移学习的两种方式
| 方式 | 操作 | 适用场景 |
|---|---|---|
| 特征提取 | 冻结预训练模型的所有层,只训练新加的分类层 | 数据量小(几百张),与预训练数据集相似 |
| 微调 | 解冻部分高层,与分类层一起训练 | 数据量中等(几千张),任务差异较大 |
# 迁移学习示例:用ResNet50做猫狗分类
base_model = tf.keras.applications.ResNet50(
weights='imagenet',
include_top=False, # 去掉原分类层
input_shape=(224, 224, 3)
)
# 冻结所有层
base_model.trainable = False
# 添加新分类层
model = models.Sequential([
base_model,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation='relu'),
layers.Dropout(0.5),
layers.Dense(1, activation='sigmoid') # 二分类
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
我的经验:微调时,学习率要调小,一般是原学习率的1/10。我习惯用1e-5起步,观察loss下降情况再调整。另外,解冻的层数不要太多,从最后几层开始试。
3.6 本章知识体系
下面这张图,是我梳理的CNN进阶知识脉络。建议你保存下来,复习时对照着看。
好了,CNN进阶的内容就到这里。记住,理论是基础,但真正让你成长的,是动手实践和踩坑。下次遇到图像分类任务,别急着从头搭模型,先想想能不能用迁移学习。嗯,相信我,你会感谢这个习惯的。