卷积神经网络之VGG模型

卷积神经网络CNN是我们深度学习过程中 的入门网络模型。它在视觉识别任务上的表现很好。一个复杂的CNN网络是带有上百万参数和许多隐含层的。

对于CNN模型,开始训练时我们可以使用一个很大的数据集如ImageNet(CNN模型的两个特点:神经元间的权重共享和卷积层之间的稀疏连接。大部分的CNN模型都需要很大的内存和计算量,特别是在训练过程。要想尽可能的训练精度高,就要在计算量上增加。

AlexNet,VGG,Inception和ResNet是一些流行的CNN网络。接下来我们先初步了解一下VGG。

下面这张图是VGG的网络结构图
在这里插入图片描述

VGG模型的特点是全部采用33的卷积核和22的池化,

通过将卷积层数量加大,使深度更深。这样采用连续的几个3x3的卷积核代替AlexNet中的较大卷积核(11x11,5x5)。如采用2个33的卷积核来代替1个55的较大卷积核,3个33的卷积核来替代1个1111的卷积核。采用堆积的小卷积核是优于采用大的卷积核,因为多层非线性层可以增加网络深度来保证学习更复杂的模式,同时参数还更少,计算代价也小。

从这张图中我们可以看到,VGG卷积层之间使用了一种块结构block来连接,每一个block之间卷积结构相同。都是多次重复使用3*3的卷积核来提取特征,只不过通道数成倍的增加(从64-128-256-512),然后每经过一个下采样或者池化层特征图大小成倍地减小,VGG卷积层之后是3个全连接层。kVGG之后一个明显的趋势是采用这种 模块结构,这是一种很好的设计典范,采用模块化结构可以减少我们网络的设计空间,另外一个点是模块里面使用瓶颈层可以降低计算量。

代码实现

下面是采用VGG16模型实现自定义数据集图像分类(宝可梦图片分类)

  • 项目结构目录如下:
    在这里插入图片描述

  • model.py

import torch.nn as nn

class VGG(nn.Module):
    def __init__(self):
        super(VGG, self).__init__()

        # class torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
        # in_channels(int) – 输入信号的通道。在文本分类中,即为词向量的维度
        # out_channels(int) – 卷积产生的通道。有多少个out_channels,就需要多少个1维卷积
        # kerner_size(int or tuple) - 卷积核的尺寸,卷积核的大小为(k, ),第二个维度是由in_channels来决定的,所以实际上卷积大小为kerner_size * in_channels
        # stride- 卷积步长
        # padding- 输入的每一条边补充0的层数
        # dilation– 卷积核元素之间的间距
        # groups(int, optional) – 从输入通道到输出通道的阻塞连接数
        # bias(bool, optional) - 如果bias = True,添加偏置
		#block1
        self.conv1 = nn.Sequential(  # 利用Sequential 迅速搭建
            nn.Conv2d(3, 64, 3, 1, 1),
            # nn.ReLU(True),
            nn.Conv2d(64, 64, 3, 1, 1), #224*224*64
            # nn.ReLU(True),

        )
        #block2
        self.conv2 = nn.Sequential(
            nn.MaxPool2d(2, 2),
            nn.Conv2d(64, 128, 3, 1, 1),
            nn.Conv2d(128, 128, 3, 1, 1),
            # nn.ReLU(True),
             #128*112*112
        )
        #block3
        self.conv3 = nn.Sequential(
            nn.MaxPool2d(2, 2),
            nn.Conv2d(128, 256, 3, 1, 1),
            nn.Conv2d(256, 256, 3, 1, 1),
            nn.Conv2d(256, 256, 3, 1, 1), #256*56*56
            # nn.ReLU(True),


        )
        #block4
        self.conv4 = nn.Sequential(
            nn.MaxPool2d(2, 2),
            nn.Conv2d(256, 512, 3, 1, 1),
            nn.Conv2d(512, 512, 3, 1, 1),
            nn.Conv2d(512, 512, 3, 1, 1),  #512*28*28
        )
        #block5
        self.conv5 = nn.Sequential(
            nn.MaxPool2d(2, 2),
            nn.Conv2d(512, 512, 3, 1, 1),
            nn.Conv2d(512, 512, 3, 1, 1),
            nn.Conv2d(512, 512, 3, 1, 1), #512*14*14
            # nn.ReLU(True),
            nn.MaxPool2d(2, 2)  #512*7*7
        )

        self.classifier = nn.Sequential(  #3个全连接层
        nn.Linear(512 * 7 * 7, 4096),
        nn.ReLU(True),
        nn.Dropout(0.5),

        nn.Linear(4096, 4096),
        nn.ReLU(True),
        nn.Dropout(0.5),

        nn.Linear(4096, 5)
        )


    def forward(self, x):

        x = self.conv1(x)  # 3*224*224(输入)--->64*224*224 输出图像尺寸=(输入-卷积核大小+2*padding)/步长+1

        x = self.conv2(x)  # 64*224*224(输入)-->64*112*112--->128*112*112

        x = self.conv3(x)  # 128*112*112(输入)--->128*56*56--->256*56*56---> 256 *56*56 如果stride=1,padding=(kernel_size-1)/2,则图像卷积后大小不变

        x = self.conv4(x)  # 256*56*56(输入)--->256*28*28(池化)--->512*28*28--->512*28*28--->512*28*28

        x = self.conv5(x)  # 512*28*28(输入)--池化--->512*14*14--->512*14*14--->512*14*14--->512*7*7 (池化)

        # print(x.shape) 这里可以打印测试一些shape 以面后面的维度出错
        x = x.view(x.size(0), 512*7*7)  # 将多维度的Tensor展平成一维,才放入全连接层
        x = self.classifier(x)
        # x = nn.Softmax(dim=1)
        return x


  • trian.py
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data   # 数据集的抽象类
import torchvision
from torchvision.datasets import ImageFolder
from torch.utils.data import Dataset
import torchvision.transforms as transforms
import warnings
from PIL import Image
import random
from model import VGG     # 从自定义的modle.py文件中导入网络模型
from Visualize import plot_with_labels  # 从自定义的Visualizee.py文件中导入函数

# GPU or CPU

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 利用过滤器来忽略warnings
warnings.filterwarnings('ignore')

# 超参数设置
EPOCH = 10  # 一共训练10次
best_acc = 0.75
BATCH_SIZE = 21  # 21个图片为一个batch 一共1050张图片 共50个batch
LR = 0.01  # learning rate
Data_PATH = 'data/pokemon'  # 图像文件路径
test_num = 150  # 测试集的数量

# 加载数据集
# 数据增强。对训练集进行预处理
transform_train = transforms.Compose([
    transforms.Resize(256),
    # transforms.RandomResizedCrop(224),
    transforms.CenterCrop(224),  # 先四周填充 将图像居中裁剪成224*224
    transforms.RandomHorizontalFlip(),  # 随机水平翻转 默认概率0.5
    transforms.ToTensor(),  # C H W格式   [0,1】数据范围
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # 用给定的均值和标准差对每个通道的数据进行正则化
])                                                                      # Normalized_image=(image-mean)/std
# 对测试集进行数据处理
transform_test = transforms.Compose([
    transforms.Resize(256),  # 数据集图像大小不一,加载测试集时也要进行了Resize and Crop ,否则会报错
    # transforms.RandomResizedCrop(224),
    transforms.CenterCrop(224),  # 先四周填充 将图像居中裁剪成224*224
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
# 用Dataset自带的方法ImageFolder对分好类的文件夹进行读取 作为训练集
train_set = torchvision.datasets.ImageFolder(root=Data_PATH, transform=transform_train)

train_loader = torch.utils.data.DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True, num_workers=2,
                                           pin_memory=True)  # 将数据保存在pin_memory中
# 测试集没有分好类 所以不能用ImageFolder直接读取
# testset = torchvision.datasets.ImageFolder(root='data/test', transform=transform_test)
# test_loader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2,
#                                           pin_memory=True)  # 数据加载器在返回前将张量复制到CUDA固定内存中

# 从pokemon文件夹中提取测试集
dataset = ImageFolder(Data_PATH)
# print(dataset.class_to_idx)
test_data = random.sample(dataset.samples, test_num)  # 随机选取150个作为测试集
# print(test_data)
test_inputs = []  # 测试集的输入 ,一个列表 存放图像的路径
test_labels = []  # 测试集的输出,标签,一个列表 存放图像对应的标签(0-4for x, y in test_data:  # 遍历test_data -->一个元素是元组的列表,每个元组第一个元素是图片的路径,第二个是该图片对应的标签
    test_inputs.append(x)
    test_labels.append(y)


# 自定义数据读取类  要实现__len__ 和__getitem__方法
class MyDataset(Dataset):
    def __init__(self, file_path, labels, transform):
        self.file_path = file_path
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.file_path)

    def __getitem__(self, idx):  # 索引数据集中的某一个数据
        image = Image.open(self.file_path[idx]).convert('RGB')
        image = self.transform(image)
        return image, torch.tensor(self.labels[idx])


test_loader = torch.utils.data.DataLoader(MyDataset(test_inputs, test_labels, transform_test), #先转化成torch能识别的
                                          batch_size=BATCH_SIZE,                                # dataset 再批处理
                                          shuffle=False, num_workers=2,
                                          pin_memory=True)

# 实例化
net = VGG()

# 定义损失函数和优化方式
loss_func = nn.CrossEntropyLoss()  # 损失函数为交叉熵 内置了softmax层
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9,   # net.parameters()可迭代的variable指定因优化哪些参数
                      weight_decay=5e-4)  # 优化方式为mini-batch momentum-SGD小批量梯度下降,weight_decay并采用L2正则化(权值衰减)

# 开始训练
if __name__ == '__main__':
    with open('model_params.txt', 'w') as f4:   # 将模型参数写入model_params.txt文件
        for parameters in net.parameters():     # 模块参数的迭代器
            f4.write(str(parameters))
            f4.write('\n')
        for name, parameters in net.named_parameters():
            f4.write(name + ':' + str(parameters.size()))
            f4.write('\n')
        f4.flush()
        f4.close()
    with open("acc.txt", "w") as f1:        # 将测试准确率写入acc.txt文件
        with open("log.txt", "w")as f2:     # 训练日志
            save_train_Acc = []
            save_test_Acc = []
            save_train_Loss = []
            save_test_Loss = []
            for epoch in range(EPOCH):
                net.train()  # 开始训练
                train_corr = 0.
                total_loss = 0.
                for i, data in enumerate(train_loader):
                    train_input, target = data
                    # train_input, target = Variable(train_input), Variable(target)
                    train_input, target = train_input.to(device), target.to(device)  # GPU
                    output = net(train_input)
                    loss = loss_func(output, target)
                    total_loss += loss.item()  # 总的损失 用item()累加成数字
                    result = torch.max(output, 1)[1]  # 返回概率最大值位置的索引  one-hot 编码---》标签
                    train_corr += (result == target).sum()  # 训练的结果与原来的标签相等的个数
                    # SGD
                    optimizer.zero_grad()
                    loss.backward()   # 计算梯度
                    optimizer.step()  # 更新参数

                    # 每五个batch打印一下
                    # print(len(train_loader))  # 共50个batch
                    if i % 5 == 0:
                        print('Epoch: %d | step: %d | train_loss: %.4f | train_acc: %.2f '
                              % (epoch, i, loss.item(), float(train_corr) / ((i + 1) * BATCH_SIZE)))
                        f2.write('Epoch: %d | step: %d | train_loss: %.4f | train_acc: %.2f'
                                 % (epoch, i, loss.item(), float(train_corr) / ((i + 1) * BATCH_SIZE)))
                        f2.write('\n')
                        f2.flush()
                # 训练完一个epoch 就打印总的损失值 准确率
                print('Epoch: %d | iteration: %d | total_loss: %.4f | train_acc: %.2f'
                      % (epoch, i, total_loss, float(train_corr) / len(train_set)))
                f2.write('Epoch: %d | step: %d | total_loss: %.4f | train_acc: %.2f'
                         % (epoch, i, total_loss, float(train_corr) / len(train_set)))
                f2.write('\n')
                f2.flush()
                save_train_Acc.append(round(float(train_corr) / len(train_set), 2))
                save_train_Loss.append(round(float(total_loss) / len(train_set), 2))
                with torch.no_grad():  # 显示地取消模型变量的梯度 测试时不要在梯度更新
                    net.eval()  # 开始测试
                    print("Starting testing!")
                    test_loss = 0.
                    correct = 0.
                    accuracy = 0.
                    for i, data in enumerate(test_loader):
                        test_x, target = data
                        # test_x, target = Variable(test_x), Variable(target)
                        test_x, target = test_x.to(device), target.to(device)

                        output = net(test_x)
                        _, pred = torch.max(output.data, 1)  # 或者pred =output.argmax(dim=1)
                        # 计算准确率
                        loss = loss_func(output, target)
                        test_loss += loss.item()
                        correct += pred.eq(target.data).sum()
                    test_loss /= len(test_inputs)
                    accuracy = float(correct) / float(len(test_inputs))  # 注意如果不加float  accuracy 为0.00 !!!
                    save_test_Acc.append(round(accuracy, 2))
                    save_test_Loss.append(round(test_loss, 2))
                    torch.save(net.state_dict(), 'params.pkl')  # 仅保存和加载模型参数
                    # 打印 总的损失值 正确的个数 和测试准确率 并写入到acc.txt文件中去
                    print('Epoch: ', epoch, '| test_loss: %.4f' % test_loss, '| correct: %d' % correct,
                          '| test accuracy: %.3f' % accuracy)
                    f1.write('Epoch: %d | test_loss: %.4f | correct: %d | test accuracy: %.3f'
                             % (epoch, test_loss, correct, accuracy))
                    f1.write('\n')
                    f1.flush()  # 将缓冲区写入
                    # plot_with_labels(save_train_Loss, save_train_Acc, save_test_Loss, save_test_Acc)
                    if accuracy > best_acc:
                        f3 = open("best_acc.txt", "w")
                        f3.write("EPOCH=%d,best_acc= %.3f" % (epoch + 1, accuracy))
                        f3.close()
                        best_acc = accuracy
            print("Congratulating! Training Finished, TotalEPOCH=%d" % EPOCH)
            plot_with_labels(save_train_Loss, save_train_Acc, save_test_Loss, save_test_Acc)

  • visualize.py 画一个简单的折线图来看一下训练和测试的精度
import matplotlib.pyplot as plt
import numpy as np


def plot_with_labels(save_train_loss, save_train_acc, save_test_loss, save_test_acc):
    X1 = np.arange(len(save_train_loss))
    Y1 = np.array(save_train_loss)
    Y2 = np.array(save_train_acc)
    Y3 = np.array(save_test_loss)
    Y4 = np.array(save_test_acc)
    # 生成图形
    plt.figure('Visualize loss and acc')
    # 第一张子图 训练效果图
    plt.subplot(1, 2, 1)
    plt.plot(X1, Y1, 'b^-', label='train_loss_rate', linewidth=2)  # 颜色蓝色,点形三角形,线性实线,设置图例显示内容,线条宽度为2
    plt.plot(X1, Y2, 'ro-', label='train_acc_rate', linewidth=2)  # 颜色红色,点形圆形,线性虚线,设置图例显示内容左上角,线条宽度为2
    plt.xlabel('Epochs')
    plt.ylabel('Train Loss / Acc')
    plt.legend(loc='upper left')  # 左上角显示图例
    plt.xticks(np.arange(0, len(save_train_loss), 1))  # 设置横坐标轴的刻度为 010 的数组
    plt.ylim([0, 1])  # 设置纵坐标轴范围为 01
    plt.grid()  # 显示网格
    plt.title('train')  # 图形的标题

    # 第二张子图 测试图
    plt.subplot(1, 2, 2)
    plt.title('test')
    plt.xlabel('Epochs')
    plt.ylabel('Test Loss / Acc')
    plt.xticks(np.arange(0, len(save_test_loss), 1))  # 设置横坐标轴的刻度为 010 的数组
    plt.ylim([0, 1])  # 设置纵坐标轴范围为 01
    plt.plot(X1, Y3, 'b^-', label='test_loss_rate', linewidth=2)  # 颜色蓝色,点形三角形,线性实线,设置图例显示内容,线条宽度为2
    plt.plot(X1, Y4, 'ro-', label='test_acc_rate', linewidth=2)
    plt.legend(loc='upper left')
    plt.grid()  # 显示网格
    plt.savefig("temp.png", dpi=500, bbox_inches='tight')
    plt.show()
  • 训练结果:
    (因为时间有限 这里只运行了一会儿)
    在这里插入图片描述

当然这里还有关于VGG模型的很多地方我还没有搞明白,为什么使用了Multi-Scale的方法做数据增强 而且有的结构中还采用了1*1的卷积核,还有初始化参数,训练过程中使用的mini-batch的梯度下降法等等,还需要深入其中。

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值