第二章主要介绍如何使用PyTorch加载预训练模型,包括预训练的残差网络、生成对抗网络和图像理解网络。
使用预训练模型的好处一方面是当我们没有足够的GPU支持时,可以使用别人已经预训练好的模型,如果恰好预训练采用的数据集包含我们自己需要解决的问题的数据,就可以直接使用自己的图片进行预测。个人入门深度学习是需要解决视频行人检测的问题,但由于没有GPU的支持,只能使用Google已经预训练好的行人检测模型(SSD和Faster RCNN),恰好预训练的数据集包含行人这类样本,所以当时根本就不需要再重新训练最后的分类层。
另一个好处当然是在我们训练样本不足的时候,如果采用已经在大规模数据集上训练的模型,可以帮助尽量避免过拟合的发生或者减轻过拟合的程度。
Chapter 2 Pretrained networks
- 一、 图像分类网络
- AlexNet
- ResNet
- 二、生成对抗网络
- 三、图像理解网络
- 四、Torch Hub
- 五、小结
一、 图像分类网络
- ILSVRC竞赛的训练集数据含有120多万图片,对应1000个类别(class o rlabel);ILSVRC竞赛包括一系列的任务:
* 图像分类(图像中含有的物体属于哪一个类别)
* 目标定位(识别图像中物体的位置)
* 目标检测(识别并标注图像中的物体)
* 场景分类(分类图像中的场景)
* 场景解析(根据语义类别,将图像分割成多个区域)
- 简要的流程
* 将输入图像封装成torch.Tensor类的实例(对象),并进行预处理,eg:裁剪、归一化等;
* 将预处理后的数据送入预训练的神经网络,计算每类的得分scores;
* 网络的输出值封装在torch.Tensor类的实例中,其中分数最高的对应的label为输入图像的所属的类别;
- torchvision.models包:包含一系列已经在ImageNet数据集上预训练好的网络,eg:AlexNet、ResNet、Inception v3等。
- top-5 test error rate:正确的标签位于预测的top-5的比例
AlexNet
# 加载预训练的模型
from torchvision import models
dir(models)
输出:
['AlexNet',
'DenseNet',
'GoogLeNet',
'GoogLeNetOutputs',
'Inception3',
'InceptionOutputs',
'MNASNet',
'MobileNetV2',
'ResNet',
'ShuffleNetV2',
'SqueezeNet',
'VGG',
'_GoogLeNetOutputs',
'_InceptionOutputs',
'__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__path__',
'__spec__',
'_utils',
'alexnet',
'densenet',
'densenet121',
'densenet161',
'densenet169',
'densenet201',
'detection',
'googlenet',
'inception',
'inception_v3',
'mnasnet',
'mnasnet0_5',
'mnasnet0_75',
'mnasnet1_0',
'mnasnet1_3',
'mobilenet',
'mobilenet_v2',
'quantization',
'resnet',
'resnet101',
'resnet152',
'resnet18',
'resnet34',
'resnet50',
'resnext101_32x8d',
'resnext50_32x4d',
'segmentation',
'shufflenet_v2_x0_5',
'shufflenet_v2_x1_0',
'shufflenet_v2_x1_5',
'shufflenet_v2_x2_0',
'shufflenetv2',
'squeezenet',
'squeezenet1_0',
'squeezenet1_1',
'utils',
'vgg',
'vgg11',
'vgg11_bn',
'vgg13',
'vgg13_bn',
'vgg16',
'vgg16_bn',
'vgg19',
'vgg19_bn',
'video',
'wide_resnet101_2',
'wide_resnet50_2']
在上述models中
- 大写字母对应的是实现不同网络模型的类,定义其对应的层
- 小写字母对应的是方法,返回具体类的实例
alexnet = models.AlexNet() # 创建AlexNet类的实例,定义网络结构
alexnet
输出:
AlexNet(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
(1): ReLU(inplace=True)
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU(inplace=True)
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU(inplace=True)
(8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU(inplace=True)
(10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
(classifier): Sequential(
(0): Dropout(p=0.5, inplace=False)
(1): Linear(in_features=9216, out_features=4096, bias=True)
(2): ReLU(inplace=True)
(3): Dropout(p=0.5, inplace=False)
(4): Linear(in_features=4096, out_features=4096, bias=True)
(5): ReLU(inplace=True)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
- alexnet返回了AlexNet网络的结构,输入数据之后,就可以进行"forward pass";
- forward pass:输入数据送入第一个神经元–>输出值作为下一个神经元的输入,继续计算–>直到最后的输出
- forward pass实现代码:output = alexnet(input)
ResNet
# 1 加载ImageNet预训练的模型resnet101
resnet = models.resnet101(pretrained=True) # 定义残差网络
resnet
输出:
# 上述代码会自行下载resnet101模型,并输出其结构,这里只展示一下下载的过程,由于结构比较长,因此只显示一部分
Downloading: "https://download.pytorch.org/models/resnet101-5d3b4d8f.pth" to /root/.cache/torch/hub/checkpoints/resnet101-5d3b4d8f.pth
100%
170M/170M [02:14<00:00, 1.33MB/s]
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
...
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=1000, bias=True)
)
关于上述resnet的结构
- 每一行表示一个module(与Python中的module不同),这里可以理解为神经网络中的layers;
- 一系列的重复Bottleneck modules:卷积核和非线性函数的顺序级联
- 最后的输出:以一个全连接层fc结束,产生输入特征
# 2. 定义数据预处理函数
from torchvision import transforms
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
'''
预处理过程
* 输入图像缩放到256 * 256
* 裁剪到224 * 224
* 转换成张量
* 根据给定的均值和标准差进行normalize
'''
&bnsp;
# 3. 加载输入图片,并进行预处理
from PIL import Image
img = Image.open('images/1.jpg')
img.show()
img_t = preprocess(img)
print(img_t.shape)
输出:
torch.Size([3, 224, 224])
第一个维度‘3’表示通道
# 4. 运行并返回神经网络的预测结果
import torch
batch_t = torch.unsqueeze(img_t, 0) # 在img_t的指定位置“0"添加维数为1的维度
print(batch_t.shape)
resnet.eval() # 设置model的模式为eval
out = resnet(batch_t) # 输入网络,进行计算,返回的out是网络预测的对应1000个类别的分数
# 输出最高分数对应的下标和分数
score, index = torch.max(out, 1) # 使用max()返回最高分数score及其对应的下标index(tensor类型)
percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100 # 对网络的预测值进行softmax
print(index[0].item(), percentage[index[0]].item())
# 输出预测top-5
_, indices = torch.sort(out, descending=True) # 使用sort()对预测结果进行排序
for idx in indices[0][:5]:
print(idx.item(), percentage[idx].item())
输出:
torch.Size([1, 3, 224, 224])
549 34.246551513671875
549 34.246551513671875
729 13.628211975097656
443 10.604706764221191
767 5.58426570892334
721 3.554825782775879
- 在新数据上运行已经训练好的神经网络的过程称–>inference
- 在进行inference之前,需要将网络设置成“eval”模式,使用.eval()
- 使用torch.nn.functional.softmax函数将输出值的范围调整到[0, 1]
二、生成对抗网络
- GAN
* generative adversarial network
* generative: 生成假样本;
* adversarial: 两个网络相互竞争以战胜对方
* 两个子网络
* generator network: 根据任意的输入产生与真实图片逼真的假样本;目的是欺骗discriminator network, 使其混淆真实和虚假图片。
* discriminator network: 判别真实的样本和生成网络生成的假样本;目标是发现何时被骗了,但它也帮助generator在生成的图像中识别错误。
* 训练结束后,生成网络可以产生令人信服的假样本,而判别器不再能区分真实的样本和假样本;这两个网络都在基于另外一个网络的输出结果进行训练的,因此可以驱动每个网络的参数优化 - CycleGAN
* 可以将一类图像转换成另一类图像,例如可以将马转换成斑马,而且训练时并不需要马和斑马的图片以输入对的形式作为ground truth;意思就是针对输入的马的图片,我们并不需要提供给网络斑马的图片作为target。
* 有两个独立的生成网络和判别网络:第一个生成网络从一张图片开始学习生成目标分布,例如学习根据马产生斑马,第一个判别网络分辨是真实的斑马图片还是生成的斑马图片;与此同时将生成的斑马图片输入第二个生成网络,第二个生成网络根据斑马图片学习产生马的图片,即向第一个生成网络相反的方向产生图片,然后第二个判别网络辨别是真实的马还是生成的马的图片。
# 1. 定义一个残差结构的网络(这部分代码可以先忽略,后续会学到如何定义网络)
import torch
import torch.nn as nn
class ResNetBlock(nn.Module): # <1>
def __init__(self, dim):
super(ResNetBlock, self).__init__()
self.conv_block = self.build_conv_block(dim)
def build_conv_block(self, dim):
conv_block = []
conv_block += [nn.ReflectionPad2d(1)]
conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=0, bias=True),
nn.InstanceNorm2d(dim),
nn.ReLU(True)]
conv_block += [nn.ReflectionPad2d(1)]
conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=0, bias=True),
nn.InstanceNorm2d(dim)]
return nn.Sequential(*conv_block)
def forward(self, x):
out = x + self.conv_block(x) # <2>
return out
class ResNetGenerator(nn.Module):
def __init__(self, input_nc=3, output_nc=3, ngf=64, n_blocks=9): # <3>
assert(n_blocks >= 0)
super(ResNetGenerator, self).__init__()
self.input_nc = input_nc
self.output_nc = output_nc
self.ngf = ngf
model = [nn.ReflectionPad2d(3),
nn.Conv2d(input_nc, ngf, kernel_size=7, padding=0, bias=True),
nn.InstanceNorm2d(ngf),
nn.ReLU(True)]
n_downsampling = 2
for i in range(n_downsampling):
mult = 2**i
model += [nn.Conv2d(ngf * mult, ngf * mult * 2, kernel_size=3,
stride=2, padding=1, bias=True),
nn.InstanceNorm2d(ngf * mult * 2),
nn.ReLU(True)]
mult = 2**n_downsampling
for i in range(n_blocks):
model += [ResNetBlock(ngf * mult)]
for i in range(n_downsampling):
mult = 2**(n_downsampling - i)
model += [nn.ConvTranspose2d(ngf * mult, int(ngf * mult / 2),
kernel_size=3, stride=2,
padding=1, output_padding=1,
bias=True),
nn.InstanceNorm2d(int(ngf * mult / 2)),
nn.ReLU(True)]
model += [nn.ReflectionPad2d(3)]
model += [nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
model += [nn.Tanh()]
self.model = nn.Sequential(*model)
def forward(self, input): # <3>
return self.model(input)
# 2. 创建网络实例
netG = ResNetGenerator()
- netG网络作为一个生成模型,已经使用horse2zebra数据集(1068张马的图片、1335张斑马的图片)进行了预训练,模型的参数保存在.pth文件中。
- 网络的训练过程:输入一张马的图片,识别图片中的马,并逐渐修改像素值使其看起来像斑马。
- 预训练相当于有人已经训练好了模型,如果作者还提供了训练好的模型的参数,那么后人可以直接使用预训练的模型做测试。
# 3. 加载预训练模型的参数
model_path = 'model/horse2zebra_0.4.0.pth'
model_data = torch.load(model_path)
netG.load_state_dict(model_data)
netG.eval() # 设置成eval模式
# 4. 定义数据预处理函数,预处理的目的是使得图片满足网络的输入要求
from torchvision import transforms
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.ToTensor()
])
# 5. 读入图片并送入网络
from PIL import Image
img = Image.open('images/horse.jpg')
img_t = preprocess(img) # 预处理
batch_t = torch.unsqueeze(img_t, 0) # 添加batch维度,此时batch_size=1
batch_out = netG(batch_t)
batch_out.shape
输出:
torch.Size([1, 3, 256, 316])
# 6. 将网络的输出转换成图片
out_t = batch_out.squeeze() # 去掉batch的维度
out_img = transforms.ToPILImage()(out_t)
out_img.save('images/zebra.jpg')
out_t2 = (batch_out.squeeze()+ 1.0) / 2.0
out_img2 = transforms.ToPILImage()(out_t2)
out_img2.save('images/zebra2.jpg')
- 这里注意一下out_t和out_t2的区别,书上使用的是out_2,但是我暂时没有明白为什么要加上1.0再除以2.0,不过我做了一下对比。
out_img:
out_img2:
确实out_img2看起来正常一些。
三、图像理解网络
- 图像理解网络(image captioning model):给定一张图片,网络产生描述图片内容的文字;
- two connected halves
* CNN : 以图片为输入,学习生成描述场景的数值表达
* RNN : 以CNN产生的数值表达为输入,产生连贯的语句
* 使用image-caption pairs同时训练
四、Torch Hub
- TorchVision提供了加载一些预训练模型的接口,这些常用的网络有AlexNet、ResNet等,用户可以使用统一的接口加载这些预训练模型;
- Torch Hub: 想要模仿TorchVision的功能,在模型的作者将自己的模型公布在GitHub之后,其他用户可以像加载TorchVision中预训练模型一样使用统一的接口方便地加载第三方模型;
- 做法
* 在GitHub上公布网络模型时,在根目录下添加"hubconf.py"文件,该文件定义提供给其他用户的入口函数,这些入口函数一般根据参数实例化模型;
* 其他用户在加载模型时,使用
torch.hub.load(
name and branch of the Github repo,
name of the entry function,
pretained=True/False # 是否加载预训练时的参数
)
会自行下载对应的分支到本地目录,默认为.torch/hub,并运行入口函数返回实例化模型。
五、小结
这章的内容比较简单,基本就是介绍了如何加载预训练的模型。虽然涉及到了GAN和图像理解网络,但是并没有详细介绍其原理。加载预训练模型的方法有:
- 加载TorchVison中预训练的模型,可以设置是否使用预训练参数;
- 定义网络结构之后,加载保存已训练网络参数的.pth文件,用预训练的参数初始化模型;
- 如果作者在GitHub上公布代码时遵循了Torch Hub的规则,并上传了预训练参数,可以使用torch.hub.load加载预训练模型。