GuoXin Li's Blog

DogsVSCats

字数统计: 3.4k阅读时长: 16 min
2020/11/18 Share

Dog VS Cat

VGG网络

VGG 是一个卷积神经网络模型,是哈弗 Robotics Institute Visual Geometry Group 组的 Karen Simonyan 和 Andrew Zisserman 在2014年提出的。2014年在最大的图像识别竞赛 ILSVRC 中,这个模型获得第二名,第一名是 GoogleNet,VGG 模型在多个迁移学习任务中好于 GoogleNet。

VGG 是卷积神经网络的重大突破,在这些之后:

  • LeNet-5 (1998)
  • AlexNet (2012)
  • ZFNet (2013)
  • GoogleNet/Inception (2014)

注:AlexNet 的提出,使得大型卷积网络变得流行了起来。ZFNet 是基于 AlexNet 的一种模型改进。

VGG的特点

img

上图中,上面是AlexNet和下面VGG-16两种模型结构比较

AlexNet 包含有5个卷积层和3个全连接层,在第1、2和5卷积层使用了3个最大池化层

VGG-16 包含有13个卷积层,卷积核为3X3,3个全连接层

VGG-19包含了16个卷积层和3个全连接层

VGG 网络结构非常一致,从头到尾全部使用了 3X3 的卷积核和2X2的 max-pooling

VGG-16网络结构一镜到底:https://dgschwend.github.io/netscope/#/preset/vgg-16

VGG Dogs VS Cats

image-20201118152910384

Description

本次作业实现使用预训练好的 VGG 模型,下载Imagenet 1000 个类的 JSON 文件。

VGG 模型网络组成元素:

  • 卷积层(CONV)是发现图像中局部的 pattern
  • 全连接层(FC)是在全局上建立特征的关联
  • 池化(Pool)是给图像将维以提高特征的invariance
1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
import matplotlib.pyplot as plt
import os
import torch
import torch.nn as nn
import torchvision
from torchvision import models,transforms,datasets
import time
import json

# 判断是否存在GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using gpu:{}'.format(torch.cuda.is_available()) )

1
Using: gpu:True
1
2
3
# 下载数据并解压
! wget http://fenggao-image.stor.sinaapp.com/dogscats.zip
! unzip dogscats.zip

数据处理

  • 图片裁剪

  • 转换为 tensor 格式

    因为图片读入后为 numpy 格式,需要在 pytorch中进行处理,就需要转化为 tensor 的格式。

    至于 tensor 的含义:

    tensor:张量

    是一个可用来表示在一些矢量标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积外积、线性映射以及笛卡儿积

    —wikipedia

    概念:能够用指标表示法表示的物理量,并且该物理量满足坐标变换关系。

    • 0阶张量:无自由指标的量,与坐标系选取无关,如温度、质量、能量等标量
    • 1阶张量(向量):有一个自由指标的量,如坐标 ,位移等矢量
    • 2阶张量:有2个自由指标的量,如应力,应变等
    • 3阶张量:如数据立方

    在深度学习里,张量即为一个多维数组,其可以创建更高维度的矩阵

    Tensor对象的3个属性:

    • Rank: number of dimensions
    • Shape: number of rows and columns
    • Type: data type of tensor’s elements
  • 归一化

    • 不同的图片映射到同一坐标系,拥有相同的尺度
    • 像素值大小不同的问题转化为具有相似特征分布的问题
    • 一定程度上消除了因为过度曝光,质量不佳或者噪声等各种原因对模型权值更新的影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #归一化每个通道的平均值和标准差

# 格式整合
vgg_format = transforms.Compose([
# 将每个图片进行裁剪,CenterCrop参数有两个,只有一个时为正方形
transforms.CenterCrop(224),
# 将numpy数据格式转换为tensor格式
transforms.ToTensor(),
#归一化
normalize,
])

data_dir = './dogscats'

dsets = {x: datasets.ImageFolder(os.path.join(data_dir, x), vgg_format)
for x in ['train', 'valid']}

dset_sizes = {x: len(dsets[x]) for x in ['train', 'valid']}
dset_classes = dsets['train'].classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 训练时 shuffle是true,当检验时则为False
loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size=64, shuffle=True, num_workers=6)
loader_valid = torch.utils.data.DataLoader(dsets['valid'], batch_size=5, shuffle=False, num_workers=6)


'''
valid 数据一共有2000张图,每个batch是5张,因此,下面进行遍历一共会输出到 400
同时,把第一个 batch 保存到 inputs_try, labels_try,分别查看
'''
count = 1
for data in loader_valid:
print(count, end='\n')
if count == 1:
inputs_try,labels_try = data
count +=1

print(labels_try)
print(inputs_try.shape)

1
2
3
4
5
6
7
8
1
2
...
...
...
400
tensor([0, 0, 0, 0, 0])
torch.Size([5, 3, 224, 224])

模型下载 ,创建 VGG Model

1
!wget https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json

1
2
3
4
5
6
7
8
9
10
--2020-11-17 06:42:58--  https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json
Resolving s3.amazonaws.com (s3.amazonaws.com)... 52.216.169.189
Connecting to s3.amazonaws.com (s3.amazonaws.com)|52.216.169.189|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 35363 (35K) [application/octet-stream]
Saving to: ‘imagenet_class_index.json’

imagenet_class_inde 100%[===================>] 34.53K --.-KB/s in 0.09s

2020-11-17 06:42:58 (405 KB/s) - ‘imagenet_class_index.json’ saved [35363/35363]

创建的是 VGG16 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
model_vgg = models.vgg16(pretrained=True)

with open('./imagenet_class_index.json') as f:
class_dict = json.load(f)
dic_imagenet = [class_dict[str(i)][1] for i in range(len(class_dict))]

inputs_try , labels_try = inputs_try.to(device), labels_try.to(device)
model_vgg = model_vgg.to(device)

outputs_try = model_vgg(inputs_try)

print(outputs_try)
print(outputs_try.shape)

'''
可以看到结果为5行,1000列的数据,每一列代表对每一种目标识别的结果。
但是我也可以观察到,结果非常奇葩,有负数,有正数,
为了将VGG网络输出的结果转化为对每一类的预测概率,我们把结果输入到 Softmax 函数
'''
m_softm = nn.Softmax(dim=1)
probs = m_softm(outputs_try)
vals_try,pred_try = torch.max(probs,dim=1)

print( 'prob sum: ', torch.sum(probs,1))
print( 'vals_try: ', vals_try)
print( 'pred_try: ', pred_try)

print([dic_imagenet[i] for i in pred_try.data])
imshow(torchvision.utils.make_grid(inputs_try.data.cpu()),
title=[dset_classes[x] for x in labels_try.data.cpu()])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Downloading: "https://download.pytorch.org/models/vgg16-397923af.pth" to /root/.cache/torch/hub/checkpoints/vgg16-397923af.pth
100%
528M/528M [00:38<00:00, 14.3MB/s]

tensor([[ 0.7979, 1.5667, 0.8546, ..., 3.1719, 3.3310, 1.1075],
[-1.1246, 2.7357, -1.9035, ..., -5.2773, 5.4166, 6.0531],
[-2.8606, -2.5214, -1.5476, ..., -3.1250, 0.7820, 3.6555],
[-0.8027, -1.4977, 2.9460, ..., -3.7837, -0.4200, 5.5966],
[-2.6016, -4.2326, -2.6736, ..., -4.2833, 0.4802, 2.5251]],
device='cuda:0', grad_fn=<AddmmBackward>)
torch.Size([5, 1000])
prob sum: tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000], device='cuda:0',
grad_fn=<SumBackward1>)
vals_try: tensor([0.2639, 0.9191, 0.5210, 0.8608, 0.3465], device='cuda:0',
grad_fn=<MaxBackward0>)
pred_try: tensor([333, 281, 281, 281, 359], device='cuda:0')
['hamster', 'tabby', 'tabby', 'tabby', 'black-footed_ferret']

修改模型

修改最后一层网络,冻结前面层的参数,使用预训练好的模型,需要把最后的 nn.Linear 层由 1000类替换为 2类。

为了在训练中冻结前面层的参数,需要设置 required_grad=False。这样,反向传播训练梯度时,前面层的权重就不会自动更新了,训练中,只会更新最后一层的参数。

1
2
3
4
5
6
7
8
9
10
11
12
print(model_vgg)

model_vgg_new = model_vgg;

for param in model_vgg_new.parameters():
param.requires_grad = False
model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 2)
model_vgg_new.classifier._modules['7'] = torch.nn.LogSoftmax(dim = 1)

model_vgg_new = model_vgg_new.to(device)

print(model_vgg_new.classifier)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=2, bias=True)
(7): LogSoftmax(dim=1)
)

训练并测试全连接层

三个步骤

  • 创建损失函数和优化器
  • 训练模型
  • 测试模型

在这里,改用 Adam 替换 SGD 优化器

  • SGD (Stochastic Gradient Descent)朴素梯度下降,最为简单,没有动量的概念。

    缺点在于:收敛速度慢,可能在鞍点(沿着某一个方向是稳定的另一个方向是不稳定的,物理含义即一个方向上是极大值,另一个方向上是极小值)处震荡。

    为了跳出鞍点处,提出了动量的概念因此有了几种改进的优化器

  • Adam 简单说是一种添加了动量的梯度下降优化器,大大提高了收敛的速度。

    缺点在于:可能越过全局最优解;某些情况可能不收敛

在这里设置一个最大精度值max_acc,每一个 epoch 都与之进行比较,当新的 epoch 精度更好时替换之,保存训练出的最好的模型和最后一个训练的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
'''
第一步:创建损失函数和优化器

损失函数 NLLLoss() 的 输入 是一个对数概率向量和一个目标标签.
它不会为我们计算对数概率,适合最后一层是log_softmax()的网络.
'''
from tqdm import trange,tqdm
criterion = nn.NLLLoss()

# 学习率
lr = (1e-3)/2/2

# 随机梯度下降
optimizer_vgg = torch.optim.Adam(model_vgg_new.classifier[6].parameters(),lr = lr)

'''
第二步:训练模型
'''

def train_model(model,dataloader,size,epochs=1,optimizer=None):
model.train()

max_acc = -1
for epoch in range(epochs):
running_loss = 0.0
running_corrects = 0
count = 0
for inputs,classes in dataloader:
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
loss = criterion(outputs,classes)
optimizer = optimizer
optimizer.zero_grad()
loss.backward()
optimizer.step()
_,preds = torch.max(outputs.data,1)
# statistics
running_loss += loss.data.item()
running_corrects += torch.sum(preds == classes.data)
count += len(inputs)
print('Training: No. ', count, ' process ... total: ', size)
epoch_loss = running_loss / size
epoch_acc = running_corrects.data.item() / size
print('Loss: {:.4f} Acc: {:.4f}'.format(
epoch_loss, epoch_acc))
#/content/imagenet_class_index.json

if epoch_acc>max_acc:
max_acc = epoch_acc
torch.save(model, '/content/model_best_new.pth')
# tqdm.write("\n Got A Nice Model Acc:{:.8f}".format(max_acc))
tqdm.write('\nepoch: {} \tLoss: {:.8f} Acc: {:.8f}'.format(epoch,epoch_loss, epoch_acc))
tqdm.write('\n Best acc is: {} \n'.format(max_acc))
time.sleep(0.1)

torch.save(model, '/content/model_last_new.pth')



# 模型训练
train_model(model_vgg_new,loader_train,size=dset_sizes['train'], epochs=10,
optimizer=optimizer_vgg)

image-20201119132604195

(epochs 起初改为100,后来发现改为10,效果也已经不错了)

valid 数据模型测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def test_model(model,dataloader,size):
model.eval()
predictions = np.zeros(size)
all_classes = np.zeros(size)
all_proba = np.zeros((size,2))
i = 0
running_loss = 0.0
running_corrects = 0
for inputs,classes in dataloader:
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
loss = criterion(outputs,classes)
_,preds = torch.max(outputs.data,1)
# statistics
running_loss += loss.data.item()
running_corrects += torch.sum(preds == classes.data)
predictions[i:i+len(classes)] = preds.to('cpu').numpy()
all_classes[i:i+len(classes)] = classes.to('cpu').numpy()
all_proba[i:i+len(classes),:] = outputs.data.to('cpu').numpy()
i += len(classes)
print('Testing: No. ', i, ' process ... total: ', size)
epoch_loss = running_loss / size
epoch_acc = running_corrects.data.item() / size
print('Loss: {:.4f} Acc: {:.4f}'.format(
epoch_loss, epoch_acc))
return predictions, all_proba, all_classes

predictions, all_proba, all_classes = test_model(model_vgg_new,loader_valid,size=dset_sizes['valid'])

1
2
3
4
5
6
7
8
9
Testing: No.  5  process ... total:  2000
Testing: No. 10 process ... total: 2000
Testing: No. 15 process ... total: 2000
...
...
...
Testing: No. 1995 process ... total: 2000
Testing: No. 2000 process ... total: 2000
Loss: 0.0105 Acc: 0.9795

研习社 test 数据测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 加载测试数据
dsets_mine = datasets.ImageFolder("/content/yanxishe/", vgg_format)

loader_test = torch.utils.data.DataLoader(dsets_mine, batch_size=1, shuffle=False, num_workers=0)
# 加载 best model
model_vgg_new = torch.load(r'/content/model_best_new.pth')
model_vgg_new = model_vgg_new.to(device)

dic = {}
def test(model,dataloader,size):
model.eval()
predictions = np.zeros(size)
cnt = 0
for inputs,_ in tqdm(dataloader):
inputs = inputs.to(device)
outputs = model(inputs)
_,preds = torch.max(outputs.data,1)
key = dsets_mine.imgs[cnt][0].split("\\")[-1].split('.')[0]
dic[key] = preds[0]
cnt = cnt +1
test(model_vgg_new,loader_test,size=2000)

image-20201119132146392

将测试结果 dic 按顺序写入 csv 文件

1
2
3
with open("result1.csv",'a+') as f:
for key in range(2000):
f.write("{},{}\n".format(key, dic["/content/yanxishe/test/"+str(key)]))

提交结果

image-20201119141718413

最早的结果是训练 epoch 修改为100,SGD优化器时,训练 Acc 为0.99时获得的。

第二个是修改优化器为 Adam 时的结果, Acc 同样为0.99时获得的结果。

总结

  • 增加 epoch 数量会提高训练的准确度

  • 优化器改为 Adam 会提高训练时收敛的速度,但对于结果好像没有太大的提升

  • 当修改了学习率时,发现对 Acc 有不错的影响,选择 epoch = 10, 测试了 0.001 学习率,和0.005学习率,发现0.005可以在一开始时就获得比较高的 Acc。

    对于学习率的设置有:

学习率
学习速度
使用时间点 刚开始训练时 经过一定轮数后
副作用 1. 易损失值爆炸;2. 易震荡 1. 易过拟合;2. 收敛速度慢
  • 设置一个最大精度值max_acc,每一个 epoch 的 Acc 都与之进行比较,当新的 epoch 精度更好时替换之,保存训练出的最好的模型。然后用最好的训练模型进行 valid 数据集测试和 test 数据集测试。
CATALOG
  1. 1. Dog VS Cat
    1. 1.1. VGG网络
    2. 1.2. VGG Dogs VS Cats
    3. 1.3. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 训练时 shuffle是true,当检验时则为False loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size=64, shuffle=True, num_workers=6) loader_valid = torch.utils.data.DataLoader(dsets['valid'], batch_size=5, shuffle=False, num_workers=6) ''' valid 数据一共有2000张图,每个batch是5张,因此,下面进行遍历一共会输出到 400 同时,把第一个 batch 保存到 inputs_try, labels_try,分别查看 ''' count = 1 for data in loader_valid: print(count, end='\n') if count == 1: inputs_try,labels_try = data count +=1 print(labels_try) print(inputs_try.shape)
      1. 1.3.1. 模型下载 ,创建 VGG Model
    4. 1.4. 1 !wget https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json
      1. 1.4.1. 修改模型
      2. 1.4.2. 训练并测试全连接层
      3. 1.4.3. valid 数据模型测试
      4. 1.4.4. 研习社 test 数据测试
      5. 1.4.5. 提交结果
      6. 1.4.6. 总结