- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
语义分割的目标是对图像中的每个像素进行分类,而全卷积网络(Fully Convolutional Network,FCN)正是实现这一目标的经典模型。本文将以《动手学深度学习》(d2l-zh)仓库中 chapter_computer-vision/fcn_origin.md 为主线,完整讲解 FCN 的模型设计、转置卷积上采样、双线性插值初始化、VOC 数据集读取、训练与预测可视化全流程,并深入 d2l/torch.py 与 d2l/mxnet.py 源码,从数据管道到多 GPU 训练验证每一步的底层实现。读完本文,你将能够独立搭建并训练一个可对任意图像逐像素输出类别掩码的 FCN 模型。
从语义分割到全卷积网络
在目标检测任务中,模型输出的是矩形边界框,标注与预测粒度较粗;而语义分割要求像素级的识别与理解——不仅要判断图像里有什么物体,还要为每一个像素标出其所属的语义类别。FCN 正是为了解决这一任务提出的:它使用卷积神经网络将图像像素直接变换为像素类别 :cite:Long.Shelhamer.Darrell.2015。
与之前图像分类、目标检测中用到的 CNN 不同,FCN 会把中间层特征图的高和宽变换回输入图像的尺寸,这一步由 chapter_computer-vision/transposed-conv.md 中介绍的转置卷积(transposed convolution)实现。由此带来的关键性质是:
- 输出与输入在像素级别上一一对应;
- 输出任意空间位置的通道维,保存着输入图像同一空间位置像素的类别预测。
也就是说,FCN 不再依赖全连接层输出固定长度的类别向量,而是通过空间维度的还原,让每个像素都拥有一份完整的类别分布。
模型设计:特征提取 → 1×1 卷积 → 转置卷积上采样
FCN 的基本设计分三步:
- CNN 抽取图像特征:使用一个在 ImageNet 上预训练的分类骨干网络;
- 1×1 卷积变换通道数:将特征图的通道数变为类别个数;
- 转置卷积恢复空间尺寸:将特征图的高和宽变换回输入图像的尺寸。
这样一来,模型输出与输入图像具有相同的高和宽,输出通道则包含该空间位置像素的类别预测。
使用预训练 ResNet-18 抽取特征
仓库中首先加载 ImageNet 预训练的 ResNet-18 作为特征提取器,记为pretrained_net。ResNet-18 最后几层包含全局平均汇聚层(global average pooling)和全连接层,这两层在 FCN 中并不需要,会被剔除:
# mxnet pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) pretrained_net.features[-3:], pretrained_net.output# pytorch pretrained_net = torchvision.models.resnet18(pretrained=True) list(pretrained_net.children())[-3:]随后创建 FCN 实例net:复制 ResNet-18 中除全局平均汇聚层和全连接层以外的全部预训练层。以 PyTorch 为例,net = nn.Sequential(*list(pretrained_net.children())[:-2])恰好截掉了最后两个子模块。
验证下采样倍率
给定高 320、宽 480 的输入,net的前向传播会将高和宽缩减为原来的 1/32,即 10 和 15:
X = torch.rand(size=(1, 3, 320, 480)) net(X).shape # torch.Size([1, 512, 10, 15])这也是 FCN 中“32 倍上采样”的由来——骨干网络把空间分辨率压缩了 32 倍,后面的转置卷积需要恰好放大 32 倍才能还原输入尺寸。
1×1 卷积与 32 倍转置卷积上采样
接下来用1×1 卷积层把输出通道数变换为 Pascal VOC2012 数据集的类别数(21 类),再用步幅为 32 的转置卷积层把特征图放大 32 倍:
num_classes = 21 # mxnet net.add(nn.Conv2D(num_classes, kernel_size=1), nn.Conv2DTranspose(num_classes, kernel_size=64, padding=16, strides=32)) # pytorch net.add_module('final_conv', nn.Conv2d(512, num_classes, kernel_size=1)) net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes, kernel_size=64, padding=16, stride=32))为什么选择核 64、填充 16、步幅 32?根据 chapter_preliminaries/padding-and-strides.md 中卷积层输出形状的计算方法:
$$(320-64+16\times2+32)/32 = 10,\quad (480-64+16\times2+32)/32 = 15$$
正好把 10×15 的特征图还原为 320×480。推广到一般情形:若步幅为 $s$、填充为 $s/2$(假设 $s/2$ 为整数)、卷积核高宽为 $2s$,则转置卷积恰好把输入高和宽各放大 $s$ 倍。本例中 $s=32$、填充 $16$、核 $64$,严格满足这一规律。
用双线性插值初始化转置卷积层
在图像处理中,将图像放大即上采样(upsampling)。双线性插值(bilinear interpolation)是最常用的上采样方法之一,也常被用来初始化转置卷积层。
双线性插值的计算过程
假设给定输入图像,要计算上采样输出图像上的每个像素:
- 将输出图像的坐标 $(x,y)$ 按输入与输出的尺寸之比映射到输入图像的坐标 $(x',y')$,注意映射后 $x'$ 和 $y'$ 是实数;
- 在输入图像上找到离 $(x',y')$ 最近的 4 个像素;
- 输出图像在 $(x,y)$ 处的像素值,依据这 4 个像素及其与 $(x',y')$ 的相对距离加权计算得出。
bilinear_kernel 的实现
双线性插值的上采样可以用转置卷积层实现,其卷积核由下面的bilinear_kernel函数构造(限于篇幅,本文不展开其算法推导):
# pytorch def bilinear_kernel(in_channels, out_channels, kernel_size): factor = (kernel_size + 1) // 2 if kernel_size % 2 == 1: center = factor - 1 else: center = factor - 0.5 og = (torch.arange(kernel_size).reshape(-1, 1), torch.arange(kernel_size).reshape(1, -1)) filt = (1 - torch.abs(og[0] - center) / factor) * \ (1 - torch.abs(og[1] - center) / factor) weight = torch.zeros((in_channels, out_channels, kernel_size, kernel_size)) weight[range(in_channels), range(out_channels), :, :] = filt return weight注意该函数将filt写入每个(in_channel, out_channel)对应的核切片,即每个输入通道与输出通道之间共享同一套双线性滤波核,这正是把转置卷积当作可学习上采样器的初始化基础。
实验验证:图像放大 2 倍
构造一个将输入高和宽各放大 2 倍的转置卷积层(核 4、填充 1、步幅 2),并用bilinear_kernel初始化:
# mxnet conv_trans = nn.Conv2DTranspose(3, kernel_size=4, padding=1, strides=2) conv_trans.initialize(init.Constant(bilinear_kernel(3, 3, 4))) # pytorch conv_trans = nn.ConvTranspose2d(3, 3, kernel_size=4, padding=1, stride=2, bias=False) conv_trans.weight.data.copy_(bilinear_kernel(3, 3, 4))读取仓库中的 img/catdog.jpg 作为输入,将上采样结果记为Y。为打印图像需要调整通道维的位置(PyTorch 中使用permute(1, 2, 0)把 CHW 转回 HWC):
img = torchvision.transforms.ToTensor()(d2l.Image.open('img/catdog.jpg')) X = img.unsqueeze(0) Y = conv_trans(X) out_img = Y[0].permute(1, 2, 0).detach()运行结果表明,转置卷积层把图像高和宽各放大了 2 倍;除坐标刻度不同外,双线性插值放大的图像与原图肉眼几乎一致。
FCN 的初始化方案
在 FCN 中,转置卷积层用双线性插值上采样初始化,而 1×1 卷积层使用 Xavier 初始化:
# mxnet W = bilinear_kernel(num_classes, num_classes, 64) net[-1].initialize(init.Constant(W)) net[-2].initialize(init=init.Xavier()) # pytorch W = bilinear_kernel(num_classes, num_classes, 64) net.transpose_conv.weight.data.copy_(W)这里的W尺寸为 (21, 21, 64, 64):把 64×64 的双线性核复制到每一组输入/输出通道对上,作为 32 倍上采样的起点。双线性初始化保证了训练初期上采样行为平滑合理,之后梯度会在此基础上继续学习精细的像素映射。
读取 VOC 语义分割数据集
FCN 使用 chapter_computer-vision/semantic-segmentation-and-dataset.md 中介绍的语义分割数据集加载方式。随机裁剪的输出图像形状指定为 320×480——高和宽都必须能被 32 整除,与转置卷积的 32 倍上采样对齐:
batch_size, crop_size = 32, (320, 480) train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size)在 d2l/torch.py 中可以看到这一数据管道的完整实现:
d2l.DATA_HUB['voc2012']注册了 VOCtrainval_11-May-2012.tar 的下载地址与 SHA-1 校验值,download_extract负责下载解压到VOCdevkit/VOC2012;read_voc_images依据ImageSets/Segmentation/train.txt或val.txt中的文件名列表,从JPEGImages读取输入图、从SegmentationClass读取同尺寸的标签图;VOCSegDataset在__init__中完成通道标准化:(img / 255 - mean) / std,其中均值[0.485, 0.456, 0.406]、标准差[0.229, 0.224, 0.225],即 ImageNet 统计量;filter会剔除尺寸小于裁剪窗口的样本;voc_colormap2label将 RGB 颜色映射到类别索引,voc_label_indices在__getitem__中把标签图的每个像素从颜色映射为类别编号,同时voc_rand_crop对特征图与标签图做同步随机裁剪。
VOC 的 21 个类别定义在VOC_CLASSES与VOC_COLORMAP(见 d2l/torch.py):背景加 20 类物体(飞机、自行车、鸟、船、瓶子、公交车、汽车、猫、椅子、牛、餐桌、狗、马、摩托车、人、盆栽、羊、沙发、火车、电视/显示器)。同色像素属于同一语义类,这就是标签可视化的依据。
训练:像素级交叉熵损失与多 GPU 加速
FCN 的损失函数与准确率计算在本质上与图像分类并无不同,唯一差别在于:类别维度位于转置卷积层的输出通道上,因此损失计算必须沿通道维(即第 1 维)做 softmax 与交叉熵:
# pytorch 自定义损失:对每个像素求交叉熵,再对高、宽求平均 def loss(inputs, targets): return F.cross_entropy(inputs, targets, reduction='none').mean(1).mean(1) num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus() trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)对应的 mxnet 训练配置为num_epochs=5, lr=0.1, wd=1e-3,并使用gluon.loss.SoftmaxCrossEntropyLoss(axis=1)显式指定类别轴为第 1 维(通道维)。准确率则基于所有像素的预测类别是否正确来统计。
d2l.train_ch13(实现于 d2l/torch.py)支持多 GPU 训练:它将网络包装进nn.DataParallel并迁移到devices[0],逐 epoch 用train_batch_ch13计算每个 batch 的损失与准确率,累加训练指标,最后调用evaluate_accuracy_gpu在测试集上评估并实时绘制训练曲线。注意 mxnet 与 PyTorch 学习率相差 100 倍(0.1 vs 0.001),这是不同框架优化器默认行为差异导致的,沿用原文档取值即可。
预测与结果可视化
预测时,输入图像需要先在每个通道做标准化,再转成 CNN 所需的四维输入格式(NCHW),取argmax得到每个像素的类别索引:
def predict(img): X = test_iter.dataset.normalize_image(img).unsqueeze(0) pred = net(X.to(devices[0])).argmax(dim=1) return pred.reshape(pred.shape[1], pred.shape[2])为了把预测类别可视化,需要用label2image将类别索引映射回数据集中的标注颜色。其核心是 d2l/torch.py 中定义的VOC_COLORMAP颜色表:以类别索引查表colormap[X, :],即可把 (H, W) 的类别掩码还原为 (H, W, 3) 的伪彩色分割图:
def label2image(pred): colormap = torch.tensor(d2l.VOC_COLORMAP, device=devices[0]) X = pred.long() return colormap[X, :]尺寸不整除 32 的应对策略
测试集中图像大小形状各异,而模型使用了步幅为 32 的转置卷积,因此当输入图像的高或宽无法被 32 整除时,转置卷积输出的高或宽会与输入尺寸产生偏差。原文档给出的解决方案是:
- 在图像中截取多块高、宽均为 32 整数倍的矩形区域,分别对这些区域中的像素做前向传播;
- 这些区域的并集需完整覆盖输入图像;
- 当一个像素被多个区域覆盖时,取它在不同区域中转置卷积输出(预 softmax 的 logits)的平均值作为 softmax 运算的输入,再预测类别。
为简单起见,演示代码只读取 4 张较大的测试图像,并从左上角截取 320×480 区域做预测,然后逐行打印裁剪区域、预测结果、标注真值三行图像:
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012') test_images, test_labels = d2l.read_voc_images(voc_dir, False) n, imgs = 4, [] for i in range(n): crop_rect = (0, 0, 320, 480) X = torchvision.transforms.functional.crop(test_images[i], *crop_rect) pred = label2image(predict(X)) imgs += [X.permute(1, 2, 0), pred.cpu(), torchvision.transforms.functional.crop( test_labels[i], *crop_rect).permute(1, 2, 0)] d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2)可见 FCN 输出的伪彩色分割图与 VOC 标注在空间结构上高度一致,验证了“输出与输入逐像素对应”的设计目标。
小结
- 全卷积网络先使用 CNN 抽取图像特征,再通过 1×1 卷积层把通道数变换为类别个数,最后通过转置卷积层把特征图的高和宽还原为输入图像的尺寸;
- 在 FCN 中,可以将转置卷积层初始化为双线性插值的上采样,以取得平滑合理的训练起点;
- 训练时只需把交叉熵损失的类别轴指定为转置卷积输出通道维,即可完成像素级监督,配合多 GPU 的
train_ch13工具即可端到端训练。
练习
- 如果将转置卷积层改用 Xavier 随机初始化,结果会有什么变化?试着对比两类初始化的训练曲线与最终分割精度;
- 调节超参数(学习率、批大小、训练轮数),能进一步提升模型精度吗?
- 预测测试图像中所有像素的类别,并用
label2image完整可视化; - 最初的 FCN 论文 :cite:
Long.Shelhamer.Darrell.2015还使用了某些 CNN 中间层的输出(即多尺度特征融合),尝试在本仓库实现这一想法。
进阶阅读:本文配套的中文原文档为 chapter_computer-vision/fcn.md,其中还包含 PaddlePaddle 实现;转置卷积的数学原理与矩阵转置的联系见 chapter_computer-vision/transposed-conv.md;数据集的构建细节见 chapter_computer-vision/semantic-segmentation-and-dataset.md。
- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
相关推荐
动手学深度学习(D2L)全卷积网络 FCN 实战:从图像分类到逐像素语义分割
动手学深度学习(D2L)全卷积网络 FCN 实战:从图像分类到逐像素语义分割 全卷积网络(Fully Convolutional Network,FCN)是语义
人工智能深度学习机器学习教程《动手学深度学习》图像卷积实战:互相关运算、卷积层与卷积核学习
《动手学深度学习》图像卷积实战:互相关运算、卷积层与卷积核学习 卷积神经网络(CNN)之所以能以远少于全连接网络的参数高效处理图像,根源就在于卷积层所执行的核心
人工智能深度学习机器学习教程Jetson Inference 语义分割实战:使用 segNet 对图像进行逐像素分类(FCN-ResNet18 全卷积网络)
Jetson Inference 语义分割实战:使用 segNet 对图像进行逐像素分类(FCN ResNet18 全卷积网络) 导读 本篇指南聚焦于 jets
人工智能计算机视觉深度学习微调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考