第二章主要介绍如何使用PyTorch加载预训练模型,包括预训练的残差网络、生成对抗网络和图像理解网络。
  
  使用预训练模型的好处一方面是当我们没有足够的GPU支持时,可以使用别人已经预训练好的模型,如果恰好预训练采用的数据集包含我们自己需要解决的问题的数据,就可以直接使用自己的图片进行预测。个人入门深度学习是需要解决视频行人检测的问题,但由于没有GPU的支持,只能使用Google已经预训练好的行人检测模型(SSD和Faster RCNN),恰好预训练的数据集包含行人这类样本,所以当时根本就不需要再重新训练最后的分类层。
  
  另一个好处当然是在我们训练样本不足的时候,如果采用已经在大规模数据集上训练的模型,可以帮助尽量避免过拟合的发生或者减轻过拟合的程度。



Chapter 2 Pretrained networks

  • 一、 图像分类网络
  • AlexNet
  • ResNet
  • 二、生成对抗网络
  • 三、图像理解网络
  • 四、Torch Hub
  • 五、小结



一、 图像分类网络

  1. ILSVRC竞赛的训练集数据含有120多万图片,对应1000个类别(class o rlabel);ILSVRC竞赛包括一系列的任务:
     
      * 图像分类(图像中含有的物体属于哪一个类别)
      * 目标定位(识别图像中物体的位置)
      * 目标检测(识别并标注图像中的物体)
      * 场景分类(分类图像中的场景)
      * 场景解析(根据语义类别,将图像分割成多个区域)
     
  2. 简要的流程
     
     * 将输入图像封装成torch.Tensor类的实例(对象),并进行预处理,eg:裁剪、归一化等;
     * 将预处理后的数据送入预训练的神经网络,计算每类的得分scores;
     * 网络的输出值封装在torch.Tensor类的实例中,其中分数最高的对应的label为输入图像的所属的类别;
     
  3. torchvision.models包:包含一系列已经在ImageNet数据集上预训练好的网络,eg:AlexNet、ResNet、Inception v3等。
  4. 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]

 

二、生成对抗网络

  1. GAN
      * generative adversarial network
       * generative: 生成假样本;
       * adversarial: 两个网络相互竞争以战胜对方
      
      * 两个子网络
      * generator network: 根据任意的输入产生与真实图片逼真的假样本;目的是欺骗discriminator network, 使其混淆真实和虚假图片。
      * discriminator network: 判别真实的样本和生成网络生成的假样本;目标是发现何时被骗了,但它也帮助generator在生成的图像中识别错误。
      
      * 训练结束后,生成网络可以产生令人信服的假样本,而判别器不再能区分真实的样本和假样本;这两个网络都在基于另外一个网络的输出结果进行训练的,因此可以驱动每个网络的参数优化
  2. 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:

wide deep pytorch实现 deep learning with pytorch_ide


确实out_img2看起来正常一些。


 

三、图像理解网络

  1. 图像理解网络(image captioning model):给定一张图片,网络产生描述图片内容的文字;
  2. two connected halves
     * CNN : 以图片为输入,学习生成描述场景的数值表达
      * RNN : 以CNN产生的数值表达为输入,产生连贯的语句
      * 使用image-caption pairs同时训练

 

四、Torch Hub

  1. TorchVision提供了加载一些预训练模型的接口,这些常用的网络有AlexNet、ResNet等,用户可以使用统一的接口加载这些预训练模型;
  2. Torch Hub: 想要模仿TorchVision的功能,在模型的作者将自己的模型公布在GitHub之后,其他用户可以像加载TorchVision中预训练模型一样使用统一的接口方便地加载第三方模型;
  3. 做法
     * 在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加载预训练模型。