让我顺着思路来做一下这道题~
1. 修改目录名字
首先,我面向AI编程,得到了修改目录的脚本(我命名为my translate program.py
):
1 | import os |
运行之后,我的raw-img里面的文件夹的名字都变成了英文名。
2. 开始写程序
我先:设置 transform(VGG 要求 224x224)、加载数据集、按8:2分成训练与测试数据集,利用random_split、构建Dataloader、定义类别标签。
随后,我开始构建VGG-16模型。我准备着手构建自己的模型,但是注意到Cupa前辈的笔记 (如下):
我有点困惑,所以VGG是nn.Module本来就有的?只需要更改最后一层为多分类即可? (此疑惑在文章后面解决)
好吧!让我先试试自己写一写VGG,看能不能成功,如果不行,我也来用module提供的VGG。
在写的过程中,我一边读《动手》的VGG-16那一篇,一边 面向AI 编程,结果发现实现时遇到了一些问题。我全部询问ChatGPT,得到了如下答案:
非常好,你提出的问题非常重要!这几步对理解深度学习模型的细节至关重要,让我一一为你解释,确保你理解每个步骤!🎯
🔹1.
Sequential
是什么?
nn.Sequential
是 PyTorch 中的一种容器,用来顺序地组织神经网络层。你可以把它理解成 “一层接一层” 的流水线。举个例子:
1
2
3
4
5 model = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)这样就定义了一个卷积层、ReLU激活函数和池化层。
Sequential
会按照你写的顺序,依次将数据从前一层传递到后一层。你在
VGG16
中看到的self.features = nn.Sequential(...)
,就是定义了一个按顺序进行卷积、激活、池化的过程。我们每一个 Block 就是一个Sequential
容器!
🔹2.
x = x.view(x.size(0), -1)
是做什么的?这一步是 展平(Flatten),把一个高维的张量变成一个一维的向量,以便输入到全连接层中。
为啥要展平?
卷积层的输出一般是一个三维张量
[batch_size, channels, height, width]
,但全连接层(nn.Linear
)要求输入是一个二维张量[batch_size, num_features]
,所以要展平。比如,
x.size(0)
是批量大小(batch size),-1
是告诉 PyTorch 自动计算剩下的维度。举个例子:
假设
x
是一个形状为[32, 512, 7, 7]
的张量(32 是批量大小,512 是通道数,7x7 是特征图的高度和宽度)。我们希望把它展平为一个[32, 512*7*7]
的二维张量,也就是把每张图的 512x7x7 的信息展成一个长向量。这个操作可以通过x.view(x.size(0), -1)
来实现。
🔹3. 为什么第一个全连接层是
nn.Linear(512 * 7 * 7, 4096)
?我们需要知道卷积层的输出尺寸。
VGG-16 的输入尺寸是 224x224,每经过一次MaxPool2d(kernel_size=2, stride=2)
操作,图片的尺寸就会减半。假设使用了 5 次最大池化,每次池化都会使图片的尺寸缩小一半。
- 输入尺寸:224x224
- Block1后尺寸:112x112
- Block2后尺寸:56x56
- Block3后尺寸:28x28
- Block4后尺寸:14x14
- Block5后尺寸:7x7
经过卷积层和池化层后,最后的输出是一个 512 x 7 x 7 的张量,意味着 512 个通道,每个通道大小是 7x7。
为了进入全连接层(
nn.Linear
),我们需要将这个三维张量(512x7x7)展平(flatten)成一维向量,展平后是 512 * 7 * 7 = 25088。
所以,第一个全连接层需要接受 25088 个输入特征:nn.Linear(512 * 7 * 7, 4096)
。
🔹4.
nn.Dropout()
是做什么的?
nn.Dropout()
是 正则化方法,目的是防止 过拟合。什么是过拟合?
当模型在训练数据上表现很好,但在测试数据上表现很差,说明模型学到的太多“特定于训练数据”的信息,而没有学到“通用的特征”,这种现象就是 过拟合。
怎么做的?
在训练过程中,
Dropout
会随机“丢弃”一部分神经元(即把它们的输出设为0),使得每次训练时模型的结构都不一样,从而增强模型的泛化能力。例如:
1 nn.Dropout(p=0.5)意味着有 50% 的概率,某个神经元在训练时被“丢弃”。这使得模型不能过度依赖某些特定的神经元,而能学到更强的泛化能力。
🔹5. 完整捋一遍 VGG-16 的过程
现在我们把之前的步骤汇总,捋一遍 VGG-16 的流程。
VGG-16 结构:
- 卷积块(Convolutional Blocks):
每个卷积块包括:
- 2~3 个卷积层(
Conv2d
)- 每个卷积后都加上
ReLU
激活函数(nn.ReLU
)- 每个卷积块最后加一个
MaxPool2d
池化层,池化层的作用是将图像的尺寸缩小一半,降低计算量。- 展平(Flatten):
卷积操作输出的是一个高维的张量(形状类似[batch_size, channels, height, width]
),但全连接层需要的是一个二维张量。所以要用view
方法将它展平,形成一个长向量([batch_size, num_features]
)。- 全连接层(Fully Connected Layers):
- 第一层:接收展平后的输入,尺寸是
512 * 7 * 7 = 25088
,输出 4096 个神经元。- 第二层:输入 4096 个神经元,输出 4096 个神经元。
- 第三层:输出
num_classes
个神经元,代表最终的分类结果(对于你的任务,是 10 类)。- Dropout:
训练时,Dropout 随机丢弃一部分神经元,帮助避免过拟合。
总结一下:
- VGG-16 通过卷积层提取图像特征,然后通过全连接层进行分类。
- 每经过一次池化层,图像的大小减小,但深度(通道数)增加,这样能提取更多高级的特征。
- 最后通过展平(flatten)将特征输入到全连接层中。
- Dropout 用来避免过拟合,提高模型的泛化能力。
✅ 到这里,你对整个 VGG-16 的流程理解了吧?
如果有其他细节不清楚,或者想进一步了解某个部分的工作原理,随时告诉我!接下来我们可以进入训练部分,或者有其他问题都可以继续问我哦!
既然我搞懂了,那我就接着写代码啦。我继续:用上CUDA,实例化model,定义损失函数和优化器,训练模型,评估模型 。现在,主程序基本搭建好了。开始跑吧!
3. 思考
爆显存了!!
于是,我向Cupa请教:
为什么?? 我开始这样推理:
$$
我先回忆我所学的,首先想到:\
卷积层的filter只是用来提取特征的,\
因此,不需要特别去训练它,而是可以用预训练好的卷积层参数。\
而fc层,是对提取的特征进行总结,并与最终的label联系起来,所以要训练。\
这就像:前面不用训练的部分是[卷积出来,发现一个毛绒绒的耳朵]\
后面fc要训练出来,才能是决定[毛绒绒的耳朵是猫的]
$$
我问ChatGPT我推理得对不对,它说:
你的思路基本是正确的,下面我详细说明其中的原因和背后的原理,帮助你更好地理解为什么采用“预训练卷积层 + 训练FC层”的方式会更高效,也能避免你遇到的显存问题。
1. 卷积层的作用:提取通用特征
- 特征提取:
VGG16的卷积层主要任务是从输入图像中提取各种低级到高级的视觉特征(如边缘、纹理、形状等),这些特征在很多视觉任务中都具有普遍性。
使用预训练的卷积层意味着这部分的参数已经在大规模数据集(比如ImageNet)上进行过学习,能够提取出有用的、具有通用性的特征。- 参数数量庞大:
卷积层通常包含大量参数,并且计算量非常高。如果从零开始训练卷积层,不仅需要更多的数据来防止过拟合,也会消耗大量计算资源和显存。
而使用预训练权重可以大大减少训练时的资源需求,使得训练过程更快、更稳定。
2. 全连接层的作用:特征整合与决策
- 任务特定的映射:
全连接层(FC层)的主要作用是将卷积层提取的特征映射到具体的类别标签上。由于不同的任务类别可能完全不同,全连接层需要根据你特定的数据集(比如Animals-10)重新进行训练,以学会如何根据提取的特征做出正确的分类决策。- 参数相对较少:
相较于卷积层,全连接层包含的参数要少得多,这意味着训练FC层所需的内存和计算资源也会更少。这样做既能让你专注于任务相关的部分,同时也能加快训练速度。
3. 训练速度和资源消耗问题
- 显存压力:
当你将卷积层和FC层一起训练时,由于卷积层占用的参数巨大,再加上反向传播时需要存储大量中间结果(激活值、梯度等),这很容易爆显存。
固定住卷积层参数,即“冻结”卷积层,只对全连接层进行梯度计算,这样可以显著降低内存占用。- 训练速度:
卷积层的参数量大且更新频繁,训练时反向传播计算复杂。当冻结卷积层后,只对较少参数的FC层进行反向传播,计算量骤减,自然训练速度提升。
4. 迁移学习(Transfer Learning)的优势
- 数据效率:
预训练模型已经在大数据集上学习到了丰富的视觉表示,而你的Animals-10数据集可能数据量较少。利用预训练卷积层可以在数据相对有限的情况下获得较好的效果,避免从头开始训练带来的过拟合问题。- 快速收敛:
预训练的卷积层权重已经很好地初始化了网络参数,只需要针对你的任务调整FC层,使模型更快收敛,达到较高的准确率。
总结
其实就是在利用迁移学习的思想:
- 预训练的卷积层:利用已有的大规模视觉数据学习到的通用特征,免去了从头训练高成本卷积层的麻烦,同时也节省了显存和时间。
- 训练FC层:专注于任务相关的映射关系,将通用特征重新组合成符合你目标任务的预测结果。
因此,从理论和实践两方面来看,你的思路与推理是正确的。预训练卷积层作为一种通用且高效的特征提取器,再结合任务特定的全连接层,可以大大提高训练效率,并降低资源消耗,同时保持甚至提升模型的性能。
那我就明白啦,我还是把代码改成Cupa前辈的样子吧,嗯!开始运行
4. 运行
但我一运行,它就在往我的C盘 下载权重文件!我的C盘很小的。我的输出框显示:
1 | Downloading: "https://download.pytorch.org/models/vgg16-397923af.pth" to |
所以,我立刻加上
1 | import os |
现在我的C盘不会变得糟糕了,现在是
为了增强熟悉度,在我等待程序下载权重和训练的时候,再复诵一遍整个编程过程吧:
- 设置 transform(VGG 要求 224x224):
transform = ……
- 加载数据集 :
dataset = ……
- 按8:2分成训练与测试数据集:
train_size = int(len(dataset) * 0.8) ……
, 同时利用random_split - 构建Dataloader:
trainloader = DataLoader(trainset, batch_size=128, shuffle=True)……
- 定义类别标签:
class_names = dataset.classes
- 构建模型:
class Net(nn.Module):……
- 用上CUDA:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- 实例化model:
model = XXX(……).to(device)
- 定义损失函数和优化器:
criterion = nn.CrossEntropyLoss()
、optimizer = optim.Adam(model.parameters(), lr=0.001)
- 训练模型:
for epoch in range(num_epochs):……
- 评估模型:
model.eval()
还有with torch.no_grad():
,然后for inputs, labels in testloader:……
最后,我加上了可视化的数据和参考例子。
因为epoch只有两个,所以是直线(不然实在要训练太久)
源码:
1 | import torch |