文章目录
前言
笔者从人工智能小白的角度,力求能够从原文中解析出最高效率的知识。
之前看了很多博客去学习AI,但发现虽然有时候会感觉很省时间,但到了复现的时候就会傻眼,因为太多实现的细节没有提及。而且博客具有很强的主观性,因此我建议还是搭配原文来看。
请下载原文《Very Deep Convolutional Networks for Large-Scale Image Recognition》搭配阅读本文,会更高效哦!
一、论文精读
0.标题,摘要和结论
《Very Deep Convolutional Networks for Large-Scale Image Recognition》首先,看完标题,摘要和结论,我了解到了以下信息:
(1)创新点:多层33 小卷积核取代了大卷积核,并采用22池化层。
(2)网络深度至19层,宽度逐层加宽。VGG-16,VGG-19迁移学习基模型。
(3)模型简洁,常规经典CNN结构的极致,但参数量比较大,集中在FCL略显臃肿。
1.Introduction
(1)深度学习发展的三推力:数据、算力、算法。
(2)增加深度,采用3*3的小卷积核来降低参数量。
(3)具有很好的迁移泛化能力,仅仅用VGG做特征提取(4096维向量)(本身不需要微调),再用SVM,就可以用ImageNet训练好的模型,解决VOC数据集上的问题。
2.Convnet Configurations
(1)唯一预处理:图像每一个像素减去所在训练集上所有对应像素的RGB均值。
(2)卷积核为33,为什么?因为只有33才能够捕获左右上下和中心的最小尺寸。
(3)为了卷积后W和H不变:W ‘= (W +2P-K)/S + 1,设置Padding = 1,池化层2*2,步长为2。
(4)最后层用了soft-max归一化,激活层为ReLU,不再使用Local Response Normalization(AlexNet曾使用过),第四章证明无用,不会提高性能,反而会导致内存和计算的消耗。
(5)结构:D:VGG16,E:VGG19。
分为五个block,每个block进行了一次下采样,但每次下采样后卷积核都变大一倍,为了把长宽的像素信息变小,把深度通道的语义信息逐渐加宽。然后是三个全连接层FC,最后是一个soft-max分类输出层。
(6)相比于其他卷积核为77的模型,33卷积核就可以充分地提取到图像的特征信息。从感受野来理解,3个33的卷积核才相当于一个77的卷积核,但是参数量会少一些,可以加深层数,有更好的非线性的表达能力,相当于添加正则化,可以防止过拟合。(这个思路被GoogleNet采用。)
3.Classification Framework
(1)采用SGD+momentu(0.9),权重衰减(L2正则化),前两个连接层使用了Dropout(0.5)防止过拟合。初始学习率10 ^-2,性能停止提升,就除以10(AlexNet,ResNet)。先大跨步,接近min值后小碎步。训练74epoches后停止。
思考:为什么Dropout可以减少过拟合?
回答:
① 记忆随机抹去,不再死记硬背。
② Dropout减少了神经元之间的联合依赖性。
③ 添加了随机性,dropout导致两个神经元不一定每次都在一个dropout网络中出现,提高了robust。
(2)训练策略:更深更小的卷积核,某些层使用了权重初始化策略,先训练浅模型,然后把模型参数用到深层的模型之中。
(3)为了解决梯度消失等训练不动的问题,先跑了A模型,然后把参数拿出来做初始值,在预训练好的层中并没有减少学习率,所以放在网络中一起训练。
(4)确定Training image size:
方案一:固定S。
方案二:多尺度S。
每张图片S都可能变化,从[Smin,Smax]中随机选取,通过尺度抖动也就是缩放,也相当于数据增强,不同尺度训练就可以识别不同尺度的物体。
(5)Testing(看了很多资料,存疑状态,目前理解):确定测试尺度Q,把测试尺度最短边缩放为Q尺度,Q不一定等于S,训练时S可以变动。整个网络在图像上密集地进行预测,把全连接层转成了77的卷积层,最后全连接层转成了11的卷积层,这个网络结构就可以接受不同尺寸的数据,如果丢进去数据尺寸为Q,比S 大,那全连接层输出就是特征图。然后再把每个通道进行全局平均池化,这样就算最后输出是441000也可以转化为111000。然后把图像翻转,再输出一次,把两次结果进行soft-max然后取平均。
(6)采用数据计算并行而非模型并行的策略。
4.Classification Experiments
4.1 single scale evaluation(Q为固定值)
(1)如果S固定,Q=S。如果S在范围内变动,Q = 0.5(Smin+Smax)。存在模型退化的问题,即深度高于19层,性能上反而会下降(后被ResNet解决)。
(2)深层小卷积核比浅层大卷积核效果好。
(3)训练时,变动S的尺度非常重要,相对于固定S的效果都要好,这样训练可以帮助模型识别不同尺度的数据。
4.2 Multi-Scale Evaluation(Q为范围值)
(1)训练集尺度与测试集尺度不能相差太大,否则会显著降低性能。最好是来自同一分布,尺寸差异最好在32以内。
(2)S为范围值时表现更好,同上。
4.3 Multi-Corp Evaluation
(1)比对了dense及crop的方法,发现结合使用后,误差更低。这是因为假设两种方法的边界条件不一样。
二、代码阅读
import torch
import torch.nn as nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
from tqdm import tqdm
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.ToTensor()
])
1.定义超参数
BATCH_SIZE = 128 # 批的大小
2. CIFAR-10
train_dataset = datasets.CIFAR10('./CIFAR10', train=True, transform=transform, download=False)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, pin_memory=True)
test_dataset = datasets.CIFAR10('./CIFAR10', train=False, transform=transform, download=False)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True)
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
3.定义网络模型
class VGG16(nn.Module):
def __init__(self, num_classes=1000):
super(VGG16, self).__init__()
self.features = nn.Sequential(
# 1
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
# 2
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 3
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
# 4
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 5
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 6
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 7
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 8
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 9
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 10
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 11
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 12
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 13
nn.Conv2d(512, 512, kernel_size=3, padding=1),
# nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.classifier = nn.Sequential(
# 14
nn.Linear(512, 4096),
nn.ReLU(True),
nn.Dropout(),
# 15
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
# 16
nn.Linear(4096, num_classes),
)
# self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
4.创建模型
net = VGG16().to('cuda')
5.定义优化器和损失函数
criterion = nn.CrossEntropyLoss() # 交叉式损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # 优化器
6.定义轮数
EPOCHS = 50
for epoch in range(EPOCHS):
train_loss = 0.0
for i, (datas, labels) in tqdm(enumerate(train_loader)):
datas, labels = datas.to('cuda'), labels.to('cuda')
# 梯度置零
optimizer.zero_grad()
# 训练
outputs = net(datas)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 累计损失
train_loss += loss.item()
print("Epoch : {} , Batch :{} , Loss : {:.3f}".format(epoch+1, i+1, train_loss/len(train_loader.dataset)))
7.保存模型
PATH = 'cifar_net.pth'
torch.save(net.state_dict(), PATH)
8.加载模型
model = net.to('cuda')
model.load_state_dict(torch.load(PATH)) # .load_state_dict() 加载模型
9.测试
correct = 0
total = 0
with torch.no_grad():
for i, (datas, labels) in enumerate(test_loader):
datas, labels = datas.to('cuda'), labels.to('cuda')
# 输出
outputs = model(datas) # outputs.data.shape --> torch.Size([128, 10])
_, predicted = torch.max(outputs.data, dim=1) # 第一个是值的张量,第二个是序号的张量
# 累计数据量
total += labels.size(0) # labels.size() --> torch.Size([128]), labels.size(0) --> 128
# 比较有多少个预测正确
correct += (predicted == labels).sum() # 相同为1,不同为0,利用sum()求总和
print('在10000张测试集图片上的准确率:{:.3f}'.format(correct / total * 100))
10.显示每一类预测的概率
class_correct = list(0. for i in range(10))
total = list(0. for i in range(10))
with torch.no_grad():
for (images, labels) in test_loader:
# 输出
outputs = model(images)
# 获取到每一行最大值的索引
_, predicted = torch.max(outputs, dim=1)
c = (predicted == labels).squeeze() # squeeze() 去掉0维[默认], unsqueeze() 增加一维
if labels.shape[0] == 128:
for i in range(BATCH_SIZE):
label = labels[i] # 获取每一个label
class_correct[label] += c[i].item() # 累计True的个数,注意 1+True=2, 1+False=1
total[label] += 1 # 该类总的个数
11.输出正确率
for i in range(10):
print('正确率 : %5s : %2d %%' % (classes[i], 100 * class_correct[i] / total[i]))