深度学习入门(3):vgg16

引言

        相比于alexnet,vgg16进一步优化了这个黑盒模型,用实验的方式证明了哪些模块有效,哪些模块对检测效果提升有限。奠基了卷积神经网络一些基础的模块。本文参考pytorch实战7:手把手教你基于pytorch实现VGG16_vgg16 pytorch-CSDN博客,此处只做记录供本人复习记录。

正文

 VGG16创新点:      

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网络结构:

VGG16 包含 16 层带有参数的层,其中包含 13 个卷积层和 3 个全连接层。具体结构如下:

1. 卷积层(Convolutional Layers)
  • VGG16 的卷积层使用了小的 3×3 卷积核,每个卷积层的步长为 1,采用零填充(padding)保证输入输出的空间大小不变。
  • 每一层卷积之后,通常会接着一个 最大池化层(Max Pooling),池化使用 2×2 的窗口,步长为 2。
2. 网络结构详细层级
层次 类型 输出尺寸 卷积核 / 池化 输出通道数
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
3. 全连接层(Fully Connected Layers)
  • 在卷积层和池化层后,VGG16 网络展平卷积特征图,并将其传入全连接层(即全连接层逐步整合卷积后的特征)。
层次 类型 输出维度
18 全连接层 (FC) 4096
19 全连接层 (FC) 4096
20 全连接层 (FC) 1000 (分类数)
4. 激活函数
  • 所有卷积层和全连接层之后都应用了 ReLU 激活函数,使网络能够学习到非线性特征。
5. 输出层
  • 最后一层全连接层输出分类结果(对于 ImageNet 数据集来说,最终输出 1000 类的概率分布)。通常使用 Softmax 激活函数 将网络输出转化为概率。

VGG16实现:

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轮准确率

深度学习入门(3):vgg16_第1张图片

深度学习入门(3):vgg16_第2张图片

加载预训练模型50轮效果

你可能感兴趣的:(深度学习,人工智能)