2.1 一个识别图像主体的预训练网络

作为我们对深度学习的第1次尝试,我们将运行一个非常先进的深度神经网络,它是在对象识别任务中预先训练过的。有许多预先训练过的网络可以通过源代码库访问。研究人员通常会在发表论文的同时发布他们的源代码,而且代码通常带有通过在参考数据集上训练模型而获得的权重。例如,使用其中一个模型可以使我们轻松地为下一个Web服务配备图像识别功能。

我们即将在这里探讨的预训练网络是已经在ImageNet数据集的子集上训练过的。ImageNet是一个由斯坦福大学维护的包含1400多万幅图像的非常大的数据集。所有图像都用来自WordNet数据集的名词层次结构标记,而WordNet数据集又是一个大型的英语词汇数据库。

ImageNet数据集和其他公共数据集一样,源于学术竞赛。竞赛是机构和公司的研究人员经常互相挑战的传统竞争场所。其中,ImageNet大规模视觉识别挑战赛(ImageNet Large Scale Visual Recognition Challenge,ILSVRC)自2010年成立以来广受欢迎。这个特殊的竞赛基于几个任务,每年可以有所不同,如图像分类(识别图像类别)、目标定位(在图像中识别物体的位置)、目标检测(识别和标记图像中的对象)、场景分类(对图像中的情形进行分类)和场景分析(将图像分割成与语义类别相关的区域,如牛、房子、奶酪和帽子等)等。

具体来说,图像分类任务包括获取一个输入图像,并从1000个类别中生成5个标签的列表,列表按置信度排序,描述图像的内容。

ILSVRC的训练集由120万幅图像组成,每幅图像用1000个名词中的一个来标记,如“dog”,这些名词被称为图像的类(class)。从这个意义上讲,我们将交替使用术语标签(lable)和类。在图2.1中,我们可以看到一些ImageNet图像。

图2.1 ImageNet图像的一个小样本

我们最终能够拍摄自己的图像并将其输入预训练模型中,如图2.2所示。模型将为该图像生成一个预测的标签列表,我们可以检查该列表以查看模型认为我们的图像是什么。模型对有些图像的预测是准确的,而有些则不是。

图2.2 推理的过程

输入的图像将首先被预处理成一个多维数组类torch.Tensor的实例。它是一个具有高度和宽度的RGB图像,因此这个张量将有3个维度:RGB通道和2个特定大小的空间图像维度。我们将在第3章详细讨论张量是什么,但现在,可以把它想象成一个浮点数的向量或矩阵。我们的模型将把处理过的输入图像传入预先训练过的网络中,以获得每个类的分数。根据权重,最高的分数对应最可能的类。然后将每个类一对一地映射到标签上。该输出被包含在一个含有1000个元素的torch.Tensor张量中,每个元素表示与该类相关的分数。在做这些之前,我们需要先了解网络本身,看看它的底层结构,并了解如何在模型使用数据之前准备数据。

2.1.1 获取一个预先训练好的网络用于图像识别

如前所述,现在我们将使用在ImageNet上训练过的网络。首先,让我们看看TorchVision项目,该项目包含一些表现优异的、关于计算机视觉的神经网络架构,如AlexNet、ResNet和Inception-v3等。它还可以方便地访问像ImageNet这样的数据集和其他工具,以加快PyTorch的计算机视觉应用程序运行的速度。我们将在本书做进一步探讨。现在,让我们加载并运行这2个网络:首先是AlexNet,它是在图像识别方面早期具有突破性的网络之一;然后是残差网络,简称ResNet,它在2015年的ILSVRC中获胜。如果在第1章你还没有启动和运行PyTorch,现在是时候做这些事情了。

在torchvision.models中可以找到预定义的模型(参见源码code/p1ch2/2_pre_trained_networks.ipynb):

# In[1]
from torchvision import models

我们可以看看实际的模型:

# In[2]
dir(models)

# Out[2]:
['AlexNet',
 'DenseNet',
 'Inception3',
 'ResNet',
 'SqueezeNet',
 'VGG',
...
 'alexnet',
 'densenet',
 'densenet121',
...
 'resnet',
 'resnet101',
 'resnet152',
...
 ]

首字母大写的名称指的是实现了许多流行模型的Python类,它们的体系结构不同,即输入和输出之间操作的编排不同。首字母小写的名称指的是一些便捷函数,它们返回这些类实例化的模型,有时使用不同的参数集。例如,resnet101表示返回一个有101层网络的ResNet实例,resnet152表示返回一个有152层网络的ResNet实例,以此类推。下面我们将注意力转向AlexNet。

2.1.2 AlexNet

AlexNet架构在2012年的ILSVRC中以较大的优势胜出,前5名的测试错误率(也就是说,正确的标签必须在前5名中)为15.4%。相比之下,没有深度网络的次好作品仅占26.2%。这是计算机视觉历史上的一个关键时刻:此刻,社区开始意识到深度学习在视觉任务中的潜力。随之而来的是不断的改进,更现代的架构和训练方法使得前5名的错误率低至3%。

按照现在的标准,与先进的模型相比,AlexNet是一个相当小的网络。但它非常适合让我们着眼于神经网络的例子,通过它我们可以学习在一个新的图像上运行一个训练好的模型。

AlexNet架构如图2.3所示,并不是说我们已理解了AlexNet架构的所有组成部分,但是我们可以预见该架构的几个方面。首先每个块包含一系列乘法和加法运算函数,以及我们将在第5章中介绍的一些其他函数。我们可以将每个块看作一个过滤器,一个接收一幅或多幅图像作为输入并生成其他图像作为输出的函数。这种做法是在训练期间,基于训练时所看到的例子和所期望的输出决定的。

图2.3 AlexNet架构

在图2.3中,输入图像从左侧进入并依次经过5个过滤器,每个过滤器生成一些输出图像。经过每个过滤器后,图像会被缩小。在过滤器堆栈中,最后一个过滤器产生的图像被排列成一个拥有4096个元素的一维向量,并被分类以产生1000个输出,每个输出对应一个类。

为了在输入图像上运行AlexNet架构,我们可以创建一个AlexNet类的实例,如下列代码所示。

# In[3]:
alexnet = models.AlexNet()

此时,alexnet是一个可以运行AlexNet架构的对象。现在,我们还不需要了解这个架构的细节。alexNet仅是一个不透明的对象,可以像函数一样调用它。通过向alexnet提供一些精确的输入数据(我们很快会看到这些输入数据应该是什么样的),我们将在网络中运行一个正向传播(forward pass)。也就是说,输入将经过一组神经元,其输出将被传递给下一组神经元,直到得到最后的输出。实际上,如果我们有一个真实类型的input对象,我们可以使用output=alexnet(input)运行正向传播。

但如果我们这样做,我们将通过整个网络提供的数据来产生输出……糟糕!这是因为网络没有初始化:它的权重,即输入的相加和相乘所依据的数字没有经过任何训练。网络本身就是一块白板,或者说是随机的白板,我们现在要做的就是从头训练它或加载之前训练好的网络。

至此,我们回到models模块。我们已经知道首字母大写的名称对应实现了许多流行模型的Python类,而首字母小写的名称是指用预定义的层和单元数实例化模型的函数,可以选择性地下载和加载预先训练好的权重。请注意,使用这些函数没有什么必要:通过它们只是为了方便地使用与预训练好的网络的构建方式相匹配的层和单元来实例化模型。

2.1.3 ResNet

现在我们将使用resnet101来实例化一个具有101层的卷积神经网络。客观地说,在2015年ResNet出现之前,在如此深的网络中达到稳定的训练是极其困难的。ResNet提出了一个技巧使之变为可能,并在这一年一举击败了好几个深度学习测试基准。

现在让我们创建一个网络实例。我们将传递一个参数,指示函数下载resnet101在ImageNet数据集上训练好的权重。

# In[4]:
resnet = models.resnet101(pretrained=True)

下载期间,我们可以花点儿时间来“欣赏”一下resnet101的4450万个参数,竟有如此多的参数需要自动优化。

2.1.4 准备运行

通过前面的步骤我们获得了什么呢?出于好奇,我们来看看resnet101是什么样子的。我们可以通过输出返回模型的值的方式来实现这一点。它为我们提供了在图2.3中看到的相同信息的文本表示,并提供了关于网络结构的详细信息。目前可能信息量有些过大,但随着本书的推进,我们将逐渐理解这段代码告诉我们的信息。

# In[5]:
resnet

# Out[5]:
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)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1,
                       ceil_mode=False)
  (layer1): Sequential(
    (0): Bottleneck(
...
    )
  )
  (avgpool): AvgPool2d(kernel_size=7, stride=1, padding=0)
  (fc): Linear(in_features=2048, out_features=1000, bias=True)
)

我们在这里看到的是许多模块(modules),每行一个。请注意,它们与Python模块没有任何关系:它们是独立的操作,是神经网络的构建模块。它们在其他深度学习框架中也被称为层(layers)。

如果我们向下滚动,会看到许多Bottleneck模块一个接一个地重复出现,总共有101个,包括卷积和其他模块。这是典型的计算机视觉深度神经网络的结构:过滤器和非线性函数或多或少地顺序级联,以层(fc)结束,为1000个输出类(out_features)中的每个类生成分数。

可以像调用函数一样调用resnet变量,将一幅或多幅图像作为输入,并为1000个ImageNet类生成对等数量的分数。然而,在此之前我们必须对输入的图像进行预处理,使其大小正确,使其值(颜色)大致处于相同的数值范围。为此,TorchVision模块提供了转换操作,允许我们快速定义基本预处理函数的管道。

# In[6]:
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个像素,并将其转换为一个张量,对其RGB分量(红色、绿色和蓝色)进行归一化处理,使其具有定义的均值和标准差。张量是一种PyTorch多维数组,在本例中,是一个包含颜色、高度和宽度的三维数组。如果我们想让网络产生有意义的答案,那么这些转换就需要与训练期间向网络提供的内容相匹配。在7.1.3小节中,当开始制作自己的图像识别模型时,我们再更深入地讨论转换。

现在我们抓取我们非常喜欢的一幅狗的图像,如从GitHub代码库中下载一张名为bobby.jpg的图片,对其进行预处理,然后查看ResNet对它识别的结果。我们可以使用一个Python的图像操作模块Pillow从本地文件系统加载一幅图像。

# In[7]:
from PIL import Image
img = Image.open("../data/p1ch2/bobby.jpg")

如果我们使用的是Jupyter Notebook,需要做以下操作来查看图像,图像将在下面的<PIL.JpegImagePlugin…>代码之后显示。

# In[8]:
img
# Out[8]:
<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1280x720 at
 0x1B1601360B8>

否则,我们可以调用show()方法,它将弹出一个带有查看器的窗口,显示图2.4所示的图像:

>>> img.show()

图2.4 我们非常喜欢的一幅狗的图像

接下来,我们可以通过预处理管道传递图像:

# In[9]:
img_t = preprocess(img)

然后我们可以按照网络期望的方式对输入的张量进行重塑、裁剪和归一化处理。

# In[10]:
import torch
batch_t = torch.unsqueeze(img_t, 0)

现在可以运行我们的模型了。

2.1.5 运行模型

在深度学习中,在新数据上运行训练过的模型的过程被称为推理(inference)。为了进行推理,我们需要将网络置于eval模式。

# In[11]:
resnet.eval()

# Out[11]:
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)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1,
                       ceil_mode=False)
  (layer1): Sequential(
    (0): Bottleneck(
...
    )
  )
  (avgpool): AvgPool2d(kernel_size=7, stride=1, padding=0)
  (fc): Linear(in_features=2048, out_features=1000, bias=True)
)

如果我们忘记这样做,那么一些预先训练过的模型,如批归一化(Batch Normalization)和Dropout将不会产生有意义的答案,这仅仅是因为它们内部工作的方式。现在eval设置好了,我们准备进行推理。

# In[12]:
out = resnet(batch_t)
out

# Out[12]:
tensor([[ -3.4803, -1.6618, -2.4515, -3.2662, -3.2466, -1.3611,
          -2.0465, -2.5112, -1.3043, -2.8900, -1.6862, -1.3055,
...
           2.8674, -3.7442,  1.5085, -3.2500, -2.4894, -0.3354,
           0.1286, -1.1355,  3.3969,  4.4584]])

一组涉及4450万个参数的惊人操作才刚刚开始。最终产生了一个拥有1000个分数的向量,每个ImageNet类对应一个分数,而且这个过程并没有花费多久时间。

我们现在需要找出得分高的类的标签,这将告诉我们模型从图像中得到了什么。如果标签符合人类对图像的描述,就太棒了!这就意味着一切正常。如果不是,那么要么是在训练过程中出现了什么问题,要么是图像与模型期望的完全不同,以至于模型无法正确处理它,或是存在其他类似的问题。

要查看预测标签的列表,我们需要加载一个文本文件,按照训练中呈现给网络的顺序列出标签,然后我们从网络中产生最高得分的索引处挑选出标签。几乎所有用于图像识别的模型的输出形式都与我们即将使用的输出形式类似。

让我们为ImageNet数据集类加载一个包含1000个标签的文件。

# In[13]:
with open('../data/p1ch2/imagenet_classes.txt') as f:
    labels = [line.strip() for line in f.readlines()]

此时,我们需要确定与我们之前获得的out张量中最高分对应的索引。我们可以使用PyTorch的max()函数来做到这一点,它可以输出一个张量中的最大值以及最大值所在的索引。

# In[14]:
_, index = torch.max(out, 1)

现在我们可以使用索引来访问标签。在这里,索引不是一个普通的Python数字,而是一个拥有单元素的一维张量,如tensor([207])。因此我们需要使用index[0]获得实际的数字作为标签列表的索引。我们还可以使用torch.nn.functional.softmax()将输出归一化到[0,1],然后除以总和。这就给了我们一些大致类似于模型在其预测中的置信度,在本例中,模型有约96%的把握认为它看到的是一只金毛猎犬。

# In[15]:
percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100
labels[index[0]], percentage[index[0]].item()

# Out[15]:
('golden retriever', 96.29334259033203)

哦,哪一个好呢?

由于该模型产生了分数,我们还可以找出第2好、第3好等是什么。为此,我们可以使用sort()函数,它将值按升序或降序排列,并提供排序值在原始数组中的索引。

# In[16]:
_, indices = torch.sort(out, descending=True)
[(labels[idx], percentage[idx].item()) for idx in indices[0][:5]]

# Out[16]:
[('golden retriever', 96.29334259033203),
 ('Labrador retriever', 2.80812406539917),
 ('cocker spaniel, English cocker spaniel, cocker', 0.28267428278923035),
 ('redbone', 0.2086310237646103),
 ('tennis ball', 0.11621569097042084)]

我们看到前4个答案是狗,之后的结果就变得有趣起来。第5个答案是网球,这是因为在图片中狗的附近有很多的网球,因此模型将狗错误地识别成网球,此时,对于模型而言,它认为在这种场景中只有0.1%的概率将网球识别为其他的东西。这是一个很好的例子,说明了人类和神经网络看待世界在方式上的根本差异,也说明了我们的数据当中很容易混入一些奇怪的、微妙的偏置。

放松一下,我们可以继续用一些随机图像来检测网络,看看它会产生什么结果。该网络成功与否在很大程度上取决于受试者是否在训练集中表现良好。如果我们提交一幅包含训练集之外的对象的图像,网络很可能会以相当高的可信度得出错误的答案。通过实验了解模型对未知数据的反应是很有用的。

我们刚刚运行的网络从狗的例子中学会了从大量现实世界的对象中识别狗。现在我们来看看不同的体系结构如何完成其他类型的任务,从图像生成开始。