


物体检测回归框的坐标点,分割是逐个像素做分类

目标检测太糙了,如果想知道精细的,每个像素属于哪一类



实例分割会将每个实例识别出来,可以理解为真正的目标检测的进化版本

在之前的alexnet和googlenet中我们通过缩放图像来将输入形状相同,但是对于语义分割工作而言并不合适,因为这样会破坏我们的标签映射。为了解决这个问题,我们通常将图片裁减为固定尺寸的小图。

一种是做模拟数据正广,另外就是采集这种图片
用于图像语义分割任务的最常用损失函数是像素级别的交叉熵损失,这种损失会逐个检查每个像素,将对每个像素类别的预测结果(概率分布向量)与我们的独热编码标签向量进行比较。
假设我们需要对每个像素的预测类别有5个,则预测的概率分布向量长度为5:


补充:
二分类与多分类的交叉熵损失函数

由于交叉熵损失会分别评估 每个像素的类别预测,然后对所有像素的损失进行平均,因此我们实质上是在对图像中的每个像素进行平等地学习。如果多个类在图像中的分布不均衡,那么这可能导致训练过程由像素数量多的类所主导,即模型会主要学习数量多的类别样本的特征,并且学习出来的模型会更偏向将像素预测为该类别。
FCN论文和U-Net论文中针对这个问题,对输出概率分布向量中的每个值进行加权,即希望模型更加关注数量较少的样本,以缓解图像中存在的类别不均衡问题。
比如对于二分类,正负样本比例为1: 99,此时模型将所有样本都预测为负样本,那么准确率仍有99%这么高,但其实该模型没有任何使用价值。
为了平衡这个差距,就对正样本和负样本的损失赋予不同的权重,带权重的二分类损失函数公式如下:

上面针对不同类别的像素数量不均衡提出了改进方法,但有时还需要将像素分为难学习和容易学习这两种样本。
容易学习的样本模型可以很轻松地将其预测正确,模型只要将大量容易学习的样本分类正确,loss就可以减小很多,从而导致模型不怎么顾及难学习的样本,所以我们要想办法让模型更加关注难学习的样本。
对于较难学习的样本,将 bce loss 修改为:
语义分割任务中常用的还有一个基于 Dice 系数的损失函数,该系数实质上是两个样本之间重叠的度量。此度量范围为 0~1,其中 Dice 系数为1表示完全重叠。Dice 系数最初是用于二进制数据的,可以计算为:


Dice loss是针对前景比例太小的问题提出的,dice系数源于二分类,本质上是衡量两个样本的重叠部分。
对于神经网络的输出,分子与我们的预测和标签之间的共同激活有关,而分母分别与每个掩码中的激活数量有关,这具有根据标签掩码的尺寸对损失进行归一化的效果。

对于每个类别的mask,都计算一个 Dice 损失:

将每个类的 Dice 损失求和取平均,得到最后的 Dice soft loss。
需要注意的是Dice Loss存在两个问题:
(1) 训练误差曲线非常混乱,很难看出关于收敛的信息。尽管可以检查在验证集上的误差来避开此问题。
(2) Dice Loss比较适用于样本极度不均的情况,一般的情况下,使用 Dice Loss 会对反向传播造成不利的影响,容易使训练变得不稳定。
所以在一般情况下,还是使用交叉熵损失函数。
总结:
交叉熵损失把每个像素都当作一个独立样本进行预测,而 dice loss 和 iou loss 则以一种更“整体”的方式来看待最终的预测输出。
这两类损失是针对不同情况,各有优点和缺点,在实际应用中,可以同时使用这两类损失来进行互补。
网络特点
3. 全卷积(Convolutional)
4. 上采样(Upsample):转置卷积/反卷积
5. 跳跃结构(Skip Layer)
全卷积:
FCN将传统CNN中的全连接层转化成一个个的卷积层。如下图所示,在传统的CNN结构中,前5层是卷积层,第6层和第7层分别是一个长度为4096的一维向量,第8层是长度为1000的一维向量,分别对应1000个类别的概率。FCN将这3层表示为卷积层,卷积核的大小(通道数,宽,高)分别为(4096,1,1)、(4096,1,1)、(1000,1,1)。所有的层都是卷积层,故称为全卷积网络。

上采样——转置卷积:
可以发现,经过多次卷积(还有pooling)以后,得到的图像越来越小,分辨率越来越低(粗略的图像),那么FCN是如何得到图像中每一个像素的类别的呢?为了从这个分辨率低的粗略图像恢复到原图的分辨率,FCN使用了上采样。例如经过5次卷积(和pooling)以后,图像的分辨率依次缩小了2,4,8,16,32倍。对于最后一层的输出图像,需要进行32倍的上采样,以得到原图一样的大小。这个上采样是通过反卷积(deconvolution)实现的。
另外补充一句,上采样(upsampling)一般包括2种方式:

一张更为形象的说明如下:

跳跃结构:
对第5层的输出(32倍放大)反卷积到原图大小,得到的结果还是不够精确,一些细节无法恢复。于是Jonathan将第4层的输出和第3层的输出也依次反卷积,分别需要16倍和8倍上采样,结果就精细一些了。
其卷积过程类似:
相对应的:


1.为深度学习解决语义分割提供了基本思路,激发了很多优秀的工作
2.输入图像大小没有限制,结构灵活
3.更加高效,节省时间和空间
不足
1.结果不够精细,边界不清晰
2.没有充分考虑到语义间的上下文关系
3.padding操作可能会引入噪声
网络结构:

Fully Convolutional Networks
他对每个像素的类别预测存储在通道k里面

3.1 在 DCGAN[1],生成器将随机值转变为一个全尺寸图片,此时需用到转置卷积。
3.2 在语义分割中,会在编码器中用卷积层提取特征,然后在解码器中恢复原先尺寸,从而对原图中的每个像素分类。该过程同样需用转置卷积。经典方法有 FCN[2] 和 U-net[3]。
3.3 CNN 可视化[4]:通过转置卷积将 CNN 的特征图还原到像素空间,以观察特定特征图对哪些模式的图像敏感。
转置卷积和卷积的区别:
转置卷积的具体实现:

如图所示,input里的每个元素和kernel相乘,最后把对应位置相加,相当于卷积的逆变换

X和y的形状被交换了
其实就是padding在输出后把输出的矩阵的前padding行和列与后padding行和列给删除了而已。
import torch
from torch import nn
from d2l import torch as d2l
def trans_conv(X, K):
h, w = K.shape
Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Y[i:i + h, j:j + w] += X[i, j] * K
return Y
## 验证转置卷积
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
Y = trans_conv(X, K)
X,K,Y
(tensor([[0., 1.],
[2., 3.]]),
tensor([[0., 1.],
[2., 3.]]),
tensor([[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]))
## 当输入x和卷积核k都是四维张量的时候,可以使用高级AIP获取到相同的结果
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2) #(批量大小,通道数,高,宽)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
tconv(X)
tensor([[[[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]]], grad_fn=<SlowConvTranspose2DBackward>)
填充
在转置卷积之中,填充是对输出进行填充,将输出外部n层褪去,所以加上填充是会缩小输出大小的
## 填充为1
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
tconv(X)
tensor([[[[4.]]]], grad_fn=<SlowConvTranspose2DBackward>)





等价于:



步幅在卷积里面使得高款成倍的减少,这里使得高宽成倍的增加




%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
import os
模型构建
# 使用在ImageNet数据集上预训练的ResNet-18进行图像特征的提取,并将网络实例记为pretrained_net
# 注意ResNet-18的最后几层是全局平均池化层和全连接层,在FCN中不需要
pretrained_net = torchvision.models.resnet18(pretrained=True)
list(pretrained_net.children())[-3:]
[Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
),
AdaptiveAvgPool2d(output_size=(1, 1)),
Linear(in_features=512, out_features=1000, bias=True)]
# 根据pretrained net创建一个新的网络实例,去除FCN不需要的部分
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])
# 使用1*1的卷积层将输出通道数转换为Pascal VO2012数据集的类别数(21类).
# 这里的输出通道数选择21的原因是为了减少后面transpose层的计算量(减少到最小)
num_classes =21
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))
初始化转置卷积层

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
# 这里使用转置层实现双线性差值,构建一个输入高宽放大两倍的转置卷积层,并将卷积核使用`bilinear_kernel`函数构建
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))
# 读取图像X,将上采样的结果记为Y,为了输出打印图片需要调整维度
img = torchvision.transforms.ToTensor()(d2l.Image.open('./course_file/pytorch/img/catdog.jpg'))
X = img.unsqueeze(0)
Y = conv_trans(X)
out_img = Y[0].permute(1, 2, 0).detach()
d2l.set_figsize()
print('input image shape:', img.permute(1, 2, 0).shape)
d2l.plt.imshow(img.permute(1, 2, 0))
input image shape: torch.Size([561, 728, 3])

print('output image shape:', out_img.shape)
d2l.plt.imshow(out_img);

可以看到,转置卷积层将图像的高和宽分别放大了2倍。除了坐标刻度不同,双线性插值放大的图像和原图看上去没什么两样。所以我们在全卷积网络中,[用双线性插值的上采样初始化转置卷积层。对于1 × 1 卷积层,我们使用Xavier初始化参数。]
W = bilinear_kernel(num_classes, num_classes, 64)
net.transpose_conv.weight.data.copy_(W);
损失函数
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)
预测
预测时需要将输入图像在各个通道做标准化,并转换成卷积神经网络的四维输入格式
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])
U-Net与FCN都是很小的分割网络,既没有使用空洞卷积,也没有后接CRF,结构简单。
Unet的本质目标是解决小目标的分割问题,越深的网络感受野越大越更加适合大目标。所以Unet天生的优势是网络结构简单,适合做小目标

解码:线性插值
特征融合:相加或者拼接后过1*1卷积下采样

这个结构就是先对图片进行卷积和池化,在Unet论文中是池化4次,比方说一开始的图片是224x224的,那么就会变成112x112,56x56,28x28,14x14四个不同尺寸的特征。然后我们对14x14的特征图做上采样或者反卷积,得到28x28的特征图,这个28x28的特征图与之前的28x28的特征图进行通道伤的拼接concat,然后再对拼接之后的特征图做卷积和上采样,得到56x56的特征图,再与之前的56x56的特征拼接,卷积,再上采样,经过四次上采样可以得到一个与输入图像尺寸相同的224x224的预测结果。
Unet的好处我感觉是:网络层越深得到的特征图,有着更大的视野域,浅层卷积关注纹理特征,深层网络关注本质的那种特征,所以深层浅层特征都是有格子的意义的;另外一点是通过反卷积得到的更大的尺寸的特征图的边缘,是缺少信息的,毕竟每一次下采样提炼特征的同时,也必然会损失一些边缘特征,而失去的特征并不能从上采样中找回,因此通过特征的拼接,来实现边缘特征的一个找回。
(1)因为医学图像边界模糊、梯度复杂,需要较多的高分辨率信息。高分辨率用于精准分割。
(2)人体内部结构相对固定,分割目标在人体图像中的分布很具有规律,语义简单明确,低分辨率信息能够提供这一信息,用于目标物体的识别。UNet结合了低分辨率信息(提供物体类别识别依据)和高分辨率信息(提供精准分割定位依据),完美适用于医学图像分割。
(3)**可解释性重要。**由于医疗影像最终是辅助医生的临床诊断,所以网络告诉医生一个3D的CT有没有病是远远不够的,医生还要进一步的想知道,病灶在哪一层,在哪一层的哪个位置,分割出来了吗,能求体积嘛?同时对于网络给出的分类和分割等结果,医生还想知道为什么,所以一些神经网络可解释性的trick就有用处了,比较常用的就是画activation map。看网络的哪些区域被激活了,如下图。
(4)数据量少。医学影像的数据获取相对难一些,很多比赛只提供不到100例数据。所以我们设计的模型不宜多大,参数过多,很容易导致过拟合。 原始UNet的参数量在28M左右(上采样带转置卷积的UNet参数量在31M左右),而如果把channel数成倍缩小,模型可以更小。缩小两倍后,UNet参数量在7.75M。缩小四倍,可以把模型参数量缩小至2M以内,非常轻量。个人尝试过使用Deeplab v3+和DRN等自然图像语义分割的SOTA网络在自己的项目上,发现效果和UNet差不多,但是参数量会大很多。






1.目标大,2. 识别难


感受野:33->55

假设:1. 相邻像素相似2.空洞卷积可以防止过拟合







