李沐动手学深度学习(PyTorch)课程学习笔记第七章:计算机视觉。
1. 图像增广
图像增广在对训练图像进行一系列的随机变化之后,生成相似但不同的训练样本,从而扩大了训练集的规模。此外,应用图像增广的原因是,随机改变训练样本可以减少模型对某些属性的依赖,从而提高模型的泛化能力。例如,我们可以以不同的方式裁剪图像,使感兴趣的对象出现在不同的位置,减少模型对于对象出现位置的依赖。我们还可以调整亮度、颜色等因素来降低模型对颜色的敏感度。
下面的代码有50%的几率使图像向左或向右翻转:
1 | trans = torchvision.transforms.RandomHorizontalFlip() |
有50%的几率向上或向下翻转,注意,上下翻转图像不如左右图像翻转那样常用,需要根据数据集的特征考虑是否可以将图像上下翻转:
1 | trans = torchvision.transforms.RandomVerticalFlip() |
随机裁剪一个面积为原始面积10%到100%的区域,该区域的宽高比从0.5~2之间随机取值。然后,区域的宽度和高度都被缩放到200像素:
1 | trans = torchvision.transforms.RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2)) |
我们可以改变图像颜色的四个方面:亮度、对比度、饱和度和色调。在下面的示例中,我们随机更改图像的亮度,随机值为原始图像的50%(1 - 0.5)到150%(1 + 0.5)之间:
1 | trans = torchvision.transforms.ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0) |
在实践中,我们将结合多种图像增广方法。我们可以通过使用一个 Compose
实例来综合上面定义的不同的图像增广方法,并将它们应用到每个图像:
1 | trans = transforms.Compose([ |
图像增广可以直接作用在图像数据上,也可以在使用 torchvision.datasets
导入数据集的时候通过 transform
参数指定:
1 | X = trans(X) |
2. 微调
微调(fine-tuning)是迁移学习(transfer learning)中的常见技巧,微调包括以下四个步骤:
- 在源数据集(例如 ImageNet 数据集)上预训练神经网络模型,即源模型。
- 创建一个新的神经网络模型,即目标模型。这将复制源模型上的所有模型设计及其参数(输出层除外)。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出层与源数据集的标签密切相关;因此不在目标模型中使用该层。
- 向目标模型添加输出层,其输出数是目标数据集中的类别数。然后随机初始化该层的模型参数。
- 在目标数据集(如椅子数据集)上训练目标模型。输出层将从头开始进行训练,而所有其他层的参数将根据源模型的参数进行微调。
当目标数据集比源数据集小得多时,微调有助于提高模型的泛化能力。
我们将在一个 CIFAR10 数据集上微调 ResNet-18 模型。该模型已在 ImageNet 数据集上进行了预训练:
1 | import torch |
3. 目标检测和边界框
在图像分类任务中,我们假设图像中只有一个主要物体对象,我们只关注如何识别其类别。然而,很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。在计算机视觉里,我们将这类任务称为目标检测(object detection)或目标识别(object recognition)。
下面加载本节将使用的示例图像。图像左边是一只狗,右边是一只猫。它们是这张图像里的两个主要目标:
1 | import torch |
在目标检测中,我们通常使用边界框(bounding box)来描述对象的空间位置。边界框是矩形的,由矩形左上角的以及右下角的 x
和 y
坐标决定。另一种常用的边界框表示方法是边界框中心的 (x, y)
轴坐标以及框的宽度和高度。
在这里,我们定义在这两种表示法之间进行转换的函数:box_corner_to_center
从两角表示法转换为中心宽度表示法,而 box_center_to_corner
反之亦然。输入参数 boxes
可以是长度为4的张量,也可以是形状为 (N, 4)
的二维张量,其中 N 是边界框的数量。
1 | def box_corner_to_center(boxes): |
我们将根据坐标信息定义图像中狗和猫的边界框。图像中坐标的原点是图像的左上角,向右的方向为 x
轴的正方向,向下的方向为 y
轴的正方向:
1 | # bbox是边界框的英文缩写 |
我们可以将边界框在图中画出,以检查其是否准确。画之前,我们定义一个辅助函数 bbox_to_rect
。它将边界框表示成 matplotlib
的边界框格式,在图像上添加边界框之后,我们可以看到两个物体的主要轮廓基本上在两个框内:
1 | def bbox_to_rect(bbox, color): |
4. 目标检测数据集
目标检测领域没有像 MNIST 和 Fashion-MNIST 那样的小数据集。为了快速测试目标检测模型,我们收集并标记了一个小型数据集。首先,我们拍摄了一组香蕉的照片,并生成了1000张不同角度和大小的香蕉图像。然后,我们在一些背景图片的随机位置上放一张香蕉的图像。最后,我们在图片上为这些香蕉标记了边界框。
包含所有图像和 CSV 标签文件的香蕉检测数据集可以直接从互联网下载,通过 read_data_bananas
函数,我们读取香蕉检测数据集的图像和标签。该数据集的 CSV 文件内含目标类别标签和位于左上角和右下角的真实边界框坐标:
1 | import torch |
以下 BananasDataset
类别将允许我们创建一个自定义 Dataset
实例来加载香蕉检测数据集:
1 | class BananasDataset(Dataset): |
最后,我们定义 load_data_bananas
函数,来为训练集和测试集返回两个数据加载器实例。对于测试集,无须按随机顺序读取它:
1 | def load_data_bananas(batch_size): |
让我们读取一个小批量,并打印其中的图像和标签的形状。图像的小批量的形状为:(批量大小, 通道数, 高度, 宽度)
,它与我们之前图像分类任务中的相同。标签的小批量的形状为:(批量大小, M, 5)
,其中 M 是数据集的任何图像中边界框可能出现的最大数量。
小批量计算虽然高效,但它要求每张图像含有相同数量的边界框,以便放在同一个批量中。通常来说,图像可能拥有不同数量个边界框;因此,在达到 M 之前,边界框少于 M 的图像将被非法边界框填充。这样,每个边界框的标签将被长度为5的数组表示。数组中的第一个元素是边界框中对象的类别,其中-1表示用于填充的非法边界框。数组的其余四个元素是边界框左上角和右下角的 (x, y)
坐标值(值域在0~1之间)。对于香蕉数据集而言,由于每张图像上只有一个边界框,因此 M = 1
。
1 | batch_size, edge_size = 32, 256 |
接下来让我们展示10幅带有真实边界框的图像。我们可以看到在所有这些图像中香蕉的旋转角度、大小和位置都有所不同。当然,这只是一个简单的人工数据集,实践中真实世界的数据集通常要复杂得多:
1 | # d2l.show_images()函数的实现 |
5. 锚框
由于本节难度较大,因此详细分析见:D2L-计算机视觉-锚框。
1 | import torch |
6. 多尺度目标检测
在上一节中,我们以输入图像的每个像素为中心,生成了多个锚框。基本而言,这些锚框代表了图像不同区域的样本。然而,如果为每个像素都生成的锚框,我们最终可能会得到太多需要计算的锚框。想象一个561*728的输入图像,如果以每个像素为中心生成五个形状不同的锚框,就需要在图像上标记和预测超过200万个锚框(561*728*5)。
6.1 多尺度锚框
减少图像上的锚框数量并不困难。比如,我们可以在输入图像中均匀采样一小部分像素,并以它们为中心生成锚框。此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。直观地说,比起较大的目标,较小的目标在图像上出现的可能性更多样。例如,1*1、1*2和2*2的目标可以分别以4、2和1种可能的方式出现在2*2的图像上。因此,当使用较小的锚框检测较小的物体时,我们可以采样更多的区域,而对于较大的物体,我们可以采样较少的区域。
为了演示如何在多个尺度下生成锚框,让我们先读取一张图像。它的高度和宽度分别为561和728像素:
1 | import torch |
display_anchors
函数定义如下。我们在特征图(fmap)上生成锚框(anchors),每个单位(像素)作为锚框的中心。由于锚框中的 (x, y)
轴坐标值(anchors)已经被除以特征图(fmap)的宽度和高度,因此这些值介于0和1之间,表示特征图中锚框的相对位置。
由于锚框(anchors)的中心分布于特征图(fmap)上的所有单位,因此这些中心必须根据其相对空间位置在任何输入图像上均匀分布。更具体地说,给定特征图的宽度和高度 fmap_w
和 fmap_h
,以下函数将均匀地对任何输入图像中 fmap_h
行和 fmap_w
列中的像素进行采样。以这些均匀采样的像素为中心,将会生成大小为 s
(假设列表 s
的长度为1)且宽高比(ratios)不同的锚框:
1 | def display_anchors(fmap_w, fmap_h, s): |
首先,让我们考虑探测小目标。为了在显示时更容易分辨,在这里具有不同中心的锚框不会重叠:锚框的尺度设置为0.15,特征图的高度和宽度设置为4。我们可以看到,图像上4行和4列的锚框的中心是均匀分布的:
1 | display_anchors(fmap_w=4, fmap_h=4, s=[0.15]) |
然后,我们将特征图的高度和宽度减小一半,然后使用较大的锚框来检测较大的目标。当尺度设置为0.4时,一些锚框将彼此重叠:
1 | display_anchors(fmap_w=2, fmap_h=2, s=[0.4]) |
最后,我们进一步将特征图的高度和宽度减小一半,然后将锚框的尺度增加到0.8。此时,锚框的中心即是图像的中心:
1 | display_anchors(fmap_w=1, fmap_h=1, s=[0.8]) |
6.2 多尺度检测
既然我们已经生成了多尺度的锚框,我们就将使用它们来检测不同尺度下各种大小的目标。下面,我们介绍一种基于 CNN 的多尺度目标检测方法,将在第8节(SSD)中实现。
在某种规模上,假设我们有 c
张形状为 h * w
的特征图。使用上一小节中的方法,我们生成了 hw
组锚框,其中每组都有 a
个中心相同的锚框。例如,在上一小节实验的第一个尺度上,给定10个(通道数量)4 * 4
的特征图,我们生成了16组锚框,每组包含3个中心相同的锚框。接下来,每个锚框都根据真实值边界框来标记了类和偏移量。在当前尺度下,目标检测模型需要预测输入图像上 hw
组锚框类别和偏移量,其中不同组锚框具有不同的中心。
假设此处的 c
张特征图是 CNN 基于输入图像的正向传播算法获得的中间输出。既然每张特征图上都有 hw
个不同的空间位置,那么相同空间位置可以看作含有 c
个单元。根据感受野的定义,特征图在相同空间位置的 c
个单元在输入图像上的感受野相同:它们表征了同一感受野内的输入图像信息。因此,我们可以将特征图在同一空间位置的 c
个单元变换为使用此空间位置生成的 a
个锚框类别和偏移量。本质上,我们用输入图像在某个感受野区域内的信息,来预测输入图像上与该区域位置相近的锚框类别和偏移量。
当不同层的特征图在输入图像上分别拥有不同大小的感受野时,它们可以用于检测不同大小的目标。例如,我们可以设计一个神经网络,其中靠近输出层的特征图单元具有更宽的感受野,这样它们就可以从输入图像中检测到较大的目标。
简言之,我们可以利用深层神经网络在多个层次上对图像进行分层表示,从而实现多尺度目标检测。在第8节我们将通过一个具体的例子来说明它是如何工作的。
7. 区域卷积神经网络(R-CNN)系列
7.1 R-CNN
R-CNN 首先从输入图像中选取若干(例如2000个)提议区域(如锚框也是一种选取方法),并标注它们的类别和边界框(如偏移量)。然后,用卷积神经网络对每个提议区域进行前向传播以抽取其特征。接下来,我们用每个提议区域的特征来预测类别和边界框。具体来说,R-CNN 包括以下四个步骤:
- 对输入图像使用选择性搜索来选取多个高质量的提议区域。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框;
- 选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征;
- 将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别;
- 将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框。
尽管 R-CNN 模型通过预训练的卷积神经网络有效地抽取了图像特征,但它的速度很慢。想象一下,我们可能从一张图像中选出上千个提议区域,这需要上千次的卷积神经网络的前向传播来执行目标检测。这种庞大的计算量使得 R-CNN 在现实世界中难以被广泛应用。
7.2 Fast R-CNN
R-CNN 的主要性能瓶颈在于,对每个提议区域,卷积神经网络的前向传播是独立的,而没有共享计算。由于这些区域通常有重叠,独立的特征抽取会导致重复的计算。Fast R-CNN 对 R-CNN 的主要改进之一,是仅在整张图像上执行卷积神经网络的前向传播。Fast R-CNN 的主要计算如下:
- 与 R-CNN 相比,Fast R-CNN 用来提取特征的入卷积神经网络的输入是整个图像,而不是各个提议区域。此外,这个网络通常会参与训练。设输入为一张图像,将卷积神经网络的输出的形状记为
1 * c * h1 * w1
; - 假设选择性搜索生成了
n
个提议区域。这些形状各异的提议区域在卷积神经网络的输出上分别标出了形状各异的兴趣区域。然后,这些感兴趣的区域需要进一步抽取出形状相同的特征(比如指定高度h2
和宽度w2
),以便于连结后输出。为了实现这一目标,Fast R-CNN 引入了兴趣区域汇聚层(RoI pooling):将卷积神经网络的输出和提议区域作为输入,输出连结后的各个提议区域抽取的特征,形状为n * c * h2 * w2
; - 通过全连接层将输出形状变换为
n * d
,其中超参数d
取决于模型设计; - 预测
n
个提议区域中每个区域的类别和边界框。更具体地说,在预测类别和边界框时,将全连接层的输出分别转换为形状为n * q
(q
是类别的数量)的输出和形状为n * 4
的输出。其中预测类别时使用 Softmax 回归。
下面,我们演示了兴趣区域汇聚层的计算方法。假设卷积神经网络抽取的特征 X
的高度和宽度都是4,且只有单通道:
1 | import torch |
让我们进一步假设输入图像的高度和宽度都是40像素,且选择性搜索在此图像上生成了两个提议区域。每个区域由5个元素表示:区域目标类别、左上角和右下角的 (x, y)
坐标:
1 | rois = torch.Tensor([[0, 0, 0, 20, 20], [0, 0, 10, 30, 30]]) |
由于 X
的高和宽是输入图像高和宽的1/10,因此,两个提议区域的坐标先按 spatial_scale
乘以0.1。然后,在 X
上分别标出这两个兴趣区域 X[:, :, 0:3, 0:3]
和 X[:, :, 1:4, 0:4]
。最后,在 2 * 2
的兴趣区域汇聚层中,每个兴趣区域被划分为子窗口网格,并进一步抽取相同形状 2 * 2
的特征:
1 | print(torchvision.ops.roi_pool(X, rois, output_size=(2, 2), spatial_scale=0.1)) |
7.3 Faster R-CNN
为了较精确地检测目标结果,Fast R-CNN 模型通常需要在选择性搜索中生成大量的提议区域。Faster R-CNN 提出将选择性搜索替换为区域提议网络(region proposal network),从而减少提议区域的生成数量,并保证目标检测的精度。具体来说,区域提议网络的计算步骤如下:
- 使用填充为1的
3 * 3
的卷积层变换卷积神经网络的输出,并将输出通道数记为c
。这样,卷积神经网络为图像抽取的特征图中的每个单元均得到一个长度为c
的新特征; - 以特征图的每个像素为中心,生成多个不同大小和宽高比的锚框并标注它们;
- 使用锚框中心单元长度为
c
的特征,分别预测该锚框的二元类别(含目标还是背景)和边界框; - 使用非极大值抑制,从预测类别为目标的预测边界框中移除相似的结果。最终输出的预测边界框即是兴趣区域汇聚层所需的提议区域。
值得一提的是,区域提议网络作为 Faster R-CNN 模型的一部分,是和整个模型一起训练得到的。换句话说,Faster R-CNN 的目标函数不仅包括目标检测中的类别和边界框预测,还包括区域提议网络中锚框的二元类别和边界框预测。作为端到端训练的结果,区域提议网络能够学习到如何生成高质量的提议区域,从而在减少了从数据中学习的提议区域的数量的情况下,仍保持目标检测的精度。
7.4 Mask R-CNN
如果在训练集中还标注了每个目标在图像上的像素级位置,那么 Mask R-CNN 能够有效地利用这些详尽的标注信息进一步提升目标检测的精度。
Mask R-CNN 是基于 Faster R-CNN 修改而来的。具体来说,Mask R-CNN 将兴趣区域汇聚层替换为了兴趣区域对齐层(RoI Align),使用双线性插值(bilinear interpolation)来保留特征图上的空间信息,从而更适于像素级预测。兴趣区域对齐层的输出包含了所有与兴趣区域的形状相同的特征图。它们不仅被用于预测每个兴趣区域的类别和边界框,还通过额外的全卷积网络预测目标的像素级位置。本章的后续章节将更详细地介绍如何使用全卷积网络预测图像中像素级的语义。
8. 单发多框检测(SSD)
SSD 模型主要由基础网络组成,其后是几个多尺度特征块。基本网络用于从输入图像中提取特征,因此它可以使用深度卷积神经网络。单发多框检测论文中选用了在分类层之前截断的 VGG,现在也常用 ResNet 替代。我们可以设计基础网络,使它输出的高和宽较大。这样一来,基于该特征图生成的锚框数量较多,可以用来检测尺寸较小的目标。接下来的每个多尺度特征块将上一层提供的特征图的高和宽缩小(如减半),并使特征图中每个单元在输入图像上的感受野变得更广阔。
回想一下在第6节中,通过深度神经网络分层表示图像的多尺度目标检测的设计。由于接近顶部的多尺度特征图较小,但具有较大的感受野,它们适合检测较少但较大的物体。简而言之,通过多尺度特征块,单发多框检测生成不同大小的锚框,并通过预测边界框的类别和偏移量来检测大小不同的目标,因此这是一个多尺度目标检测模型。
8.1 类别预测层与边界框预测层
设目标类别的数量为 q
。这样一来,锚框有 q + 1
个类别,其中第0类是背景。在某个尺度下,设特征图的高和宽分别为 h
和 w
。如果以其中每个单元为中心生成 a
个锚框,那么我们需要对 hwa
个锚框进行分类。如果使用全连接层作为输出,很容易导致模型参数过多。回忆 NiN 一节介绍的使用卷积层的通道来输出类别预测的方法,单发多框检测采用同样的方法来降低模型复杂度。
具体来说,类别预测层使用一个保持输入高和宽的卷积层。这样一来,输出和输入在特征图宽和高上的空间坐标一一对应。考虑输出和输入同一空间坐标 (x, y)
:输出特征图上 (x, y)
坐标的通道里包含了以输入特征图 (x, y)
坐标为中心生成的所有锚框的类别预测。因此输出通道数为 a * (q + 1)
。
类别预测层的定义如下:
1 | import torch |
边界框预测层的设计与类别预测层的设计类似。唯一不同的是,这里需要为每个锚框预测4个偏移量,而不是 q + 1
个类别:
1 | # 预测锚框和真实边界框的offset,对每一个锚框有4个预测值 |
8.2 连结多尺度的预测
单发多框检测使用多尺度特征图来生成锚框并预测其类别和偏移量。在不同的尺度下,特征图的形状或以同一单元为中心的锚框的数量可能会有所不同。因此,不同尺度下预测输出的形状可能会有所不同。
在以下示例中,我们为同一个小批量构建两个不同比例(Y1
和 Y2
)的特征图,其中 Y2
的高度和宽度是 Y1
的一半。以类别预测为例,假设 Y1
和 Y2
的每个单元分别生成了5个和3个锚框。进一步假设目标类别的数量为10,对于特征图 Y1
和 Y2
,类别预测输出中的通道数分别为 5 * (10 + 1) = 55
和 3 * (10 + 1) = 33
,其中任一输出的形状是 (批量大小, 通道数, 高度, 宽度)
:
1 | def forward(x, block): |
除了批量大小这一维度外,其他三个维度都具有不同的尺寸。为了将这两个预测输出链接起来以提高计算效率,我们将把这些张量转换为更一致的格式。
通道维包含中心相同的锚框的预测结果。我们首先将通道维移到最后一维。因为不同尺度下批量大小仍保持不变,我们可以将预测结果转成二维的 (批量大小, 高 * 宽 * 通道数)
的格式,以方便之后在维度1上的连结。这样一来,尽管 Y1
和 Y2
在通道数、高度和宽度方面具有不同的大小,我们仍然可以在同一个小批量的两个不同尺度上连接这两个预测输出:
1 | # start_dim=1表示将后面三个维度展平成一维 |
8.3 高和宽减半块
高和宽减半块将输入特征图的高度和宽度减半,会扩大每个单元在其输出特征图中的感受野,该模块此前已在 VGG 中使用过:
1 | # 高宽减半块,该模块将输入特征图的高度和宽度减半 |
8.4 基本网络块
基本网络块用于从输入图像中抽取特征。为了计算简洁,我们构造了一个小的基础网络,该网络串联3个高和宽减半块,并逐步将通道数翻倍:
1 | def base_net(): |
8.5 完整的模型
完整的单发多框检测模型由五个模块组成,每个块生成的特征图既用于生成锚框,又用于预测这些锚框的类别和偏移量。在这五个模块中,第一个是基本网络块,第二个到第四个是高和宽减半块,最后一个模块使用全局最大池化层将高度和宽度都降到1。从技术上讲,第二到第五个区块都是 SSD 中的多尺度特征块:
1 | def get_blk(i): |
现在我们为每个块定义前向传播。与图像分类任务不同,此处的输出包括:CNN 特征图 Y
、在当前尺度下根据 Y
生成的锚框、预测的这些锚框的类别和偏移量(基于 Y
):
1 | # 为每个块定义前向传播,此处的cls_predictor和bbox_predictor为已经构造好的卷积层 |
超参数的设置过程可以看:单发多框检测(SSD)。
1 | sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79], [0.88, 0.961]] |
现在,我们就可以按如下方式定义完整的模型 TinySSD
了:
1 | class TinySSD(nn.Module): |
8.6 训练模型
首先读取数据集和设置超参数:
1 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
然后定义损失函数和评价函数,目标检测有两种类型的损失。第一种有关锚框类别的损失:我们可以简单地复用之前图像分类问题里一直使用的交叉熵损失函数来计算;第二种有关正类锚框偏移量的损失:预测偏移量是一个回归问题。但是,对于这个回归问题,我们在这里不使用平方损失,而是使用 L1 范数损失,即预测值和真实值之差的绝对值。掩码变量 bbox_masks
令负类锚框和填充锚框不参与损失的计算。最后,我们将锚框类别和偏移量的损失相加,以获得模型的最终损失函数:
1 | cls_loss = nn.CrossEntropyLoss(reduction='none') |
我们可以沿用准确率评价分类结果。由于偏移量使用了 L1 范数损失,我们使用平均绝对误差来(MAE)评价边界框的预测结果。这些预测结果是从生成的锚框及其预测偏移量中获得的:
1 | def cls_eval(cls_preds, cls_labels): |
最后是训练模型,在训练模型时,我们需要在模型的前向传播过程中生成多尺度锚框 anchors
,并预测其类别 cls_preds
和偏移量 bbox_preds
。然后,我们根据标签信息 label
为生成的锚框标记类别 cls_labels
和偏移量 bbox_labels
。最后,我们根据类别和偏移量的预测和标注值计算损失函数:
1 | def train(net, train_iter, valid_iter, num_epochs, lr, device): |
8.7 预测目标
在预测阶段,我们希望能把图像里面所有我们感兴趣的目标检测出来。在下面,我们读取并调整测试图像的大小,然后将其转成卷积层需要的四维格式。使用 multibox_detection
函数,我们可以根据锚框及其预测偏移量得到预测边界框,然后通过非极大值抑制来移除相似的预测边界框。最后,我们筛选所有置信度不低于0.9的边界框,做为最终输出:
1 | X = torchvision.io.read_image('../images/banana.jpg').unsqueeze(0).float() # 将其转成卷积层需要的四维格式 |
9. 语义分割和数据集
在前几节中讨论的目标检测问题中,我们一直使用方形边界框来标注和预测图像中的目标。本节将探讨语义分割(semantic segmentation)问题,它重点关注于如何将图像分割成属于不同语义类别的区域。与目标检测不同,语义分割可以识别并理解图像中每一个像素的内容:其语义区域的标注和预测是像素级的。与目标检测相比,语义分割标注的像素级的边框显然更加精细。
9.1 图像分割和实例分割
计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation)。我们在这里将它们同语义分割简单区分一下:
- 图像分割将图像划分为若干组成区域,这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以图像
catdog.jpg
作为输入,图像分割可能会将狗分为两个区域:一个覆盖以黑色为主的嘴和眼睛,另一个覆盖以黄色为主的其余部分身体。 - 实例分割也叫同时检测并分割(simultaneous detection and segmentation),它研究如何识别图像中各个目标实例的像素级区域。与语义分割不同,实例分割不仅需要区分语义,还要区分不同的目标实例。例如,如果图像中有两条狗,则实例分割需要区分像素属于的两条狗中的哪一条。
9.2 Pascal VOC2012 语义分割数据集
最重要的语义分割数据集之一是 Pascal VOC2012,下面我们深入了解一下这个数据集:
1 | import os |
进入路径 ../data/VOCdevkit/VOC2012
之后,我们可以看到数据集的不同组件。ImageSets/Segmentation
路径包含用于训练和测试样本的文本文件,而 JPEGImages
和 SegmentationClass
路径分别存储着每个示例的输入图像和标签。此处的标签也采用图像格式,其尺寸和它所标注的输入图像的尺寸相同。此外,标签中颜色相同的像素属于同一个语义类别。下面将 read_voc_images
函数定义为将所有输入的图像和标签读入内存:
1 | def read_voc_images(voc_dir, is_train=True): |
下面我们绘制前5个输入图像及其标签。在标签图像中,白色和黑色分别表示边框和背景,而其他颜色则对应不同的类别:
1 | imgs = train_features[0:5] + train_labels[0:5] |
接下来,我们列举 RGB 颜色值和类名:
1 | VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], [0, 0, 128], [128, 0, 128], |
通过上面定义的两个常量,我们可以方便地查找标签中每个像素的类索引。我们定义了 voc_colormap2label
函数来构建从上述 RGB 颜色值到类别索引的映射,而 voc_label_indices
函数将 RGB 值映射到在 Pascal VOC2012 数据集中的类别索引:
1 | def voc_colormap2label(): |
例如,在第一张样本图像中,飞机头部区域的类别索引为1,而背景索引为0:
1 | colormap2label = voc_colormap2label() |
之前的实验我们通过再缩放图像使其符合模型的输入形状。然而在语义分割中,这样做需要将预测的像素类别重新映射回原始尺寸的输入图像。这样的映射可能不够精确,尤其在不同语义的分割区域。为了避免这个问题,我们将图像裁剪为固定尺寸,而不是再缩放。具体来说,我们使用图像增广中的随机裁剪,裁剪输入图像和标签的相同区域:
1 | def voc_rand_crop(feature, label, height, width): |
我们通过继承高级 API 提供的 Dataset
类,自定义了一个语义分割数据集类 VOCSegDataset
。通过实现 __getitem__
函数,我们可以任意访问数据集中索引为 idx
的输入图像及其每个像素的类别索引。由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本可以通过自定义的 filter
函数移除掉。此外,我们还定义了 normalize_image
函数,从而对输入图像的 RGB 三个通道的值分别做标准化:
1 | class VOCSegDataset(Dataset): |
最后,我们定义以下 load_data_voc
函数来下载并读取 Pascal VOC2012 语义分割数据集。它返回训练集和测试集的数据迭代器:
1 | def load_data_voc(batch_size): |