相比于alexnet,vgg16进一步优化了这个黑盒模型,用实验的方式证明了哪些模块有效,哪些模块对检测效果提升有限。奠基了卷积神经网络一些基础的模块。本文参考pytorch实战7:手把手教你基于pytorch实现VGG16_vgg16 pytorch-CSDN博客,此处只做记录供本人复习记录。
1. 使用小卷积核堆叠代替大卷积核
VGG16 采用多个连续的 3×3 小卷积核堆叠,而不是使用 5×5 或 7×7 的大卷积核。
例如,两个 3×3 卷积核的感受野等价于一个 5×5 卷积核,但参数更少:
两个 3×3 卷积核的参数量为:2 × (3×3×C×C) = 18C²(假设输入输出通道数均为 C)
一个 5×5 卷积核为:1 × (5×5×C×C) = 25C²
优点:更深的网络、更多非线性、参数更少、性能更好。
2. 网络结构统一,模块化明显
整个网络结构非常规整,几乎只使用了 3×3 卷积核和 2×2 最大池化层。
结构非常适合后续的网络扩展与迁移学习,也便于工程实现和优化。
3. 深度加深(16~19 层)
相较于 AlexNet(8 层)和 ZFNet,VGG 显著加深了网络结构,证明了更深的网络可以带来更强的特征表达能力。
VGG16 有 13 个卷积层 + 3 个全连接层,总共 16 层有参数的网络。
4. 全连接层对特征的整合
使用三个全连接层,其中两个为 4096 维,最后一个为类别输出。
有效整合深层的空间特征,用于分类任务。
5. 可迁移性强
VGG 的结构简单、通用性强,后来被广泛用于图像分类、目标检测(如 Faster R-CNN)、风格迁移等任务中作为 backbone。
VGG16 包含 16 层带有参数的层,其中包含 13 个卷积层和 3 个全连接层。具体结构如下:
层次 | 类型 | 输出尺寸 | 卷积核 / 池化 | 输出通道数 |
---|---|---|---|---|
1 | 卷积层 (Conv) | 224×224×64 | 3×3 | 64 |
2 | 卷积层 (Conv) | 224×224×64 | 3×3 | 64 |
3 | 最大池化层 (MaxPool) | 112×112×64 | 2×2 | 64 |
4 | 卷积层 (Conv) | 112×112×128 | 3×3 | 128 |
5 | 卷积层 (Conv) | 112×112×128 | 3×3 | 128 |
6 | 最大池化层 (MaxPool) | 56×56×128 | 2×2 | 128 |
7 | 卷积层 (Conv) | 56×56×256 | 3×3 | 256 |
8 | 卷积层 (Conv) | 56×56×256 | 3×3 | 256 |
9 | 卷积层 (Conv) | 56×56×256 | 3×3 | 256 |
10 | 最大池化层 (MaxPool) | 28×28×256 | 2×2 | 256 |
11 | 卷积层 (Conv) | 28×28×512 | 3×3 | 512 |
12 | 卷积层 (Conv) | 28×28×512 | 3×3 | 512 |
13 | 卷积层 (Conv) | 28×28×512 | 3×3 | 512 |
14 | 最大池化层 (MaxPool) | 14×14×512 | 2×2 | 512 |
15 | 卷积层 (Conv) | 14×14×512 | 3×3 | 512 |
16 | 卷积层 (Conv) | 14×14×512 | 3×3 | 512 |
17 | 最大池化层 (MaxPool) | 7×7×512 | 2×2 | 512 |
层次 | 类型 | 输出维度 |
---|---|---|
18 | 全连接层 (FC) | 4096 |
19 | 全连接层 (FC) | 4096 |
20 | 全连接层 (FC) | 1000 (分类数) |
1.网络模型
class My_VGG16(nn.Module):
def __init__(self,num_classes=5,init_weight=True):
super(My_VGG16, self).__init__()
# 特征提取层
self.features = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,stride=1,padding=1),
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 分类层
self.classifier = nn.Sequential(
nn.Linear(in_features=7*7*512,out_features=4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(in_features=4096,out_features=4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(in_features=4096,out_features=num_classes)
)
# 参数初始化
if init_weight: # 如果进行参数初始化
for m in self.modules(): # 对于模型的每一层
if isinstance(m, nn.Conv2d): # 如果是卷积层
# 使用kaiming初始化
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
# 如果bias不为空,固定为0
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):# 如果是线性层
# 正态初始化
nn.init.normal_(m.weight, 0, 0.01)
# bias则固定为0
nn.init.constant_(m.bias, 0)
def forward(self,x):
x = self.features(x)
x = torch.flatten(x,1)
result = self.classifier(x)
return result
2.加载数据集
class My_Dataset(Dataset):
def __init__(self,filename,transform=None):
self.filename = filename # 文件路径
self.transform = transform # 是否对图片进行变化
self.image_name,self.label_image = self.operate_file()
def __len__(self):
return len(self.image_name)
def __getitem__(self,idx):
# 由路径打开图片
image = Image.open(self.image_name[idx])
# 下采样: 因为图片大小不同,需要下采样为224*224
trans = transforms.RandomResizedCrop(224)
image = trans(image)
# 获取标签值
label = self.label_image[idx]
# 是否需要处理
if self.transform:
image = self.transform(image)
# image = image.reshape(1,image.size(0),image.size(1),image.size(2))
# print('变换前',image.size())
# image = interpolate(image, size=(227, 227))
# image = image.reshape(image.size(1),image.size(2),image.size(3))
# print('变换后', image.size())
# 转为tensor对象
label = torch.from_numpy(np.array(label))
return image,label
def operate_file(self):
# 获取所有的文件夹路径 '../data/net_train_images'的文件夹
dir_list = os.listdir(self.filename)
# 拼凑出图片完整路径 '../data/net_train_images' + '/' + 'xxx.jpg'
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 由于一个文件夹的所有标签都是同一个值,而字符值必须转为数字值,因此我们使用数字0-4代替标签值
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
# 将标签每个复制200个
for j in range(5):
for i in range(200):
label_list.append(temp_list[j])
return name_list,label_list
# 测试集数据加载器
class My_Dataset_test(My_Dataset):
def operate_file(self):
# 获取所有的文件夹路径
dir_list = os.listdir(self.filename)
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 将标签每个复制一百个
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
for j in range(5):
for i in range(100): # 只修改了这里
label_list.append(temp_list[j])
return name_list,label_list
3.训练模型
# 训练过程
def train():
batch_size = 16 # 批量训练大小
model = My_VGG16() # 创建模型
# 加载预训练vgg
# model = load_pretrained()
# 定义优化器
optimizer = optim.SGD(params=model.parameters(), lr=lr)
# 将模型放入GPU中
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
# 定义损失函数
loss_func = nn.CrossEntropyLoss()
# 加载数据
train_set = My_Dataset('data/net_train_images',transform=transforms.ToTensor())
train_loader = DataLoader(train_set, batch_size, shuffle=True)
# 训练20次
for i in range(200):
loss_temp = 0 # 临时变量
for j,(batch_data,batch_label) in enumerate(train_loader):
# 数据放入GPU中
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 梯度清零
optimizer.zero_grad()
# 模型训练
prediction = model(batch_data)
# 损失值
loss = loss_func(prediction,batch_label)
loss_temp += loss.item()
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
# 每25个批次打印一次损失值
print('[%d] loss: %.4f' % (i+1,loss_temp/len(train_loader)))
# 是否调整学习率,如果调整的话,需要把优化器也移动到循环内部
# adjust_lr(loss_temp/len(train_loader))
# torch.save(model,'VGG16.pkl')
test(model)
3.测试模型
def test(model):
# 批量数目
batch_size = 10
# 预测正确个数
correct = 0
# 加载数据
test_set = My_Dataset_test('data/net_test_images', transform=transforms.ToTensor())
test_loader = DataLoader(test_set, batch_size, shuffle=False)
# 开始
for batch_data,batch_label in test_loader:
# 放入GPU中
batch_data, batch_label = batch_data.cuda(), batch_label.cuda()
# 预测
prediction = model(batch_data)
# 将预测值中最大的索引取出,其对应了不同类别值
predicted = torch.max(prediction.data, 1)[1]
# 获取准确个数
correct += (predicted == batch_label).sum()
print('准确率: %.2f %%' % (100 * correct / 500)) # 因为总共500个测试数据
4.加载预训练模型(可选)
# 加载预训练模型
def load_pretrained():
path = 'F:/官方_预训练模型/vgg16-397923af.pth' # 需要改为自己的路径
model = vgg16()
model.load_state_dict(torch.load(path))
return model
#-------修改网络架构匹配预加载模型-------------------
# VGG16:自己的模型
class My_VGG16(nn.Module):
def __init__(self, num_classes=5, init_weight=True, use_pretrained=False):
super(My_VGG16, self).__init__()
# 加载 torchvision 的预训练模型
pretrained_model = load_pretrained()
# 使用预训练模型的 features 部分
self.features = pretrained_model.features
# 自定义 classifier 部分
self.classifier = nn.Sequential(
nn.Linear(7*7*512, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, num_classes)
)
# 如果不使用预训练,就自己初始化参数
if init_weight and not use_pretrained:
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def forward(self,x):
x = self.features(x)
x = torch.flatten(x,1)
result = self.classifier(x)
return result
预训练模型可大大加快训练迭代速度,减少对大规模数据依赖
5.调整训练参数(可选)
# 调整学习率
loss_save = []
flag = 0
lr = 0.0002
def adjust_lr(loss):
global flag,lr
loss_save.append(loss)
if len(loss_save) >= 2:
# 如果已经训练了2次,可以判断是否收敛或波动
if abs(loss_save[-1] - loss_save[-2]) <= 0.0005:
# 如果变化范围小于0.0005,说明可能收敛了
flag += 1
if loss_save[-1] - loss_save[-2] >= 0:
# 如果损失值增加,也记一次
flag += 1
if flag >= 3:
# 如果出现3次这样的情况,需要调整学习率
lr /= 10
print('学习率已改变,变为了%s' % (lr))
# 并将flag清为0
flag = 0
调参是不得不品的一环,八仙过海,各显神通
6.测试准确率
200轮准确率
加载预训练模型50轮效果