语义分割快速入门
0 前言
在CV领域,图像识别、目标检测用的比语义分割多很多,我在过去的几年里,几乎没有用过语义分割做过项目。但语义分割作为CV中极其重要的一环,有必要掌握。
深度学习的某一个领域要想快速入门,要做的不是去看有哪些模型,而是要先把模型当成一个黑盒,然后搞明白这个黑盒的输入输出是什么,评价指标有哪些,损失函数有哪些,知道这些后,再去看看有哪些模型。比如,你不知道目标检测,那你要做的并不是直接去看RCNN、YOLO这些模型,而是知道目标检测的任务是什么,这个任务的输入是一张图片,输出是若干个边框信息,这些边框信息包括边框的位置、大小、类别、置信度得分等,接下来是要搞清楚它的评估指标有哪些,目标检测的最常用损失函数是mAP,损失函数是什么样的,搞清楚这些之后,再去看有哪些常用模型,这些模型的结构是什么。
关于模型,在多模态出来之前,一般都是用CNN的模型做语义分割,最常用的是 Unet 系列和 DeepLab 系列。如今,基于多模态的SAM系列,已经称为该领域的SOTA,基于transformer的分割模型是未来的发展方向。但当前而言,由于transformer在模型部署方面还存在一些问题,基于CNN的语义分割模型是当前工业界的应用主流,所以我们还是有必要学习一下。
1 语义分割任务的输入输出
语义分割任务的输入是一张图片,输出是一个矩阵,该矩阵与输入图片有相同的高宽,矩阵中每个元素的值,代表了图片中相同位置像素所属的类别,简而言之,语义分割任务就是对图片中每个像素进行分类。
下面是一个10×10的图片的target,target也是一个矩阵,该矩阵也是10×10,矩阵中0、1、2是不同的类别,语义分割任务就是输入这张图片后,输出下面的矩阵:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 2, 2, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 2, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 2, 2, 2, 1, 1]]
当然,target转成标签文件保存的时候,一般不会直接保存矩阵,而是先将上述矩阵映射成三通道的数据(让每个类别都有不同的颜色,方便查看),然后保存成PNG格式的图像。
2 语义分割的评估指标
评估指标主要用的是mIOU,这里的IOU是真实target与预测值的交并比,每个类别算一下IOU,然后取平均,就是mIOU。
假如下面左图中人物区域是真实的target,右图是预测值,那么IOU就是将真实值与预测值放到同一幅图像上,然后计算交并比。
mIOU能到80%就算预测得很好了。
3 损失函数
语义分割问题的本质是分类问题,那么就可以用交叉熵来作为损失函数,考虑到正负样本的比例问题,所以需要在交叉熵的基础上进行改进。
3.1 带权交叉熵
计算某个像素的损失:
l
o
s
s
=
−
∑
c
=
1
M
w
c
y
c
l
o
g
(
y
c
^
)
loss = -\sum_{c=1}^{M} w_cy_clog(\hat{y_c} )
loss=−c=1∑Mwcyclog(yc^)
M表示类别说,c表示第c个类别, y c ^ \hat{y_c} yc^表示预测为第c个类别的概率, y c y_c yc为第c个类别的真实概率(通常都是0或1,也有可能做了标签平滑), w c w_c wc表示第c个类别在计算损失时所占的权重。
这里的权值计算方式为:
w
c
=
n
c
n
e
g
n
c
p
o
s
w_c=\frac{n^{neg}_c}{n^{pos}_c}
wc=ncposncneg
第c个类别而言,它是负样本与正样本的比例。
假设训练集中正负样本比例为1: 99,这样模型对这个类别,只要无脑预测成负样本,都有99%的准确率,这样就容易出现模型懒惰。在损失函数中,加了权重之后, w c w_c wc就是99,此时如果预测错误,就会面临非常严格地处罚(因为权重很大)。
当然,权值的计算方式有很多,这里举得是最简单的。
计算单张图片的损失函数时,需要把图像中的所有像素的损失进行相加,然后取平均值。计算多张图像的损失函数时,则是对多张图片的损失取平均。
3.2 Focal Loss
上面针对不同类别的像素数量不均衡提出了改进方法,但有时还需要将像素分为难学习和容易学习这两种样本。
l
o
s
s
=
−
∑
c
=
1
M
α
(
1
−
y
c
^
)
r
y
c
l
o
g
(
y
c
^
)
loss = -\sum_{c=1}^{M} \alpha (1-\hat{y_c})^r y_c log(\hat{y_c})
loss=−c=1∑Mα(1−yc^)ryclog(yc^)
α \alpha α 是训练集中,像素为第c个类别所占比例,即它是样本权值。
( 1 − y c ^ ) r (1-\hat{y_c})^r (1−yc^)r 表示该像素点对模型的当前阶段而言,预测成c类的难度,之所以是当前阶段,那是因为可能下一轮参数更新后,这个像素点的难度就下降了, y c ^ \hat{y_c} yc^越大,表示预测成第c个类别越容易,那么说明难度小,即 ( 1 − y c ^ ) r (1-\hat{y_c})^r (1−yc^)r 越小,r 通常取2。
3.3 Soft Dice Loss
dice系数可以用来衡量两个区域的重合度:
d
i
c
e
=
2
∣
A
∩
B
∣
∣
A
∣
+
∣
B
∣
dice=\frac{2\left | A\cap B \right |}{\left | A \right |+\left | B \right |}
dice=∣A∣+∣B∣2∣A∩B∣
公式中,
∣
A
∣
|A|
∣A∣表示区域A的像素数量,
∣
A
∩
B
∣
\left | A\cap B \right |
∣A∩B∣ 表示A和B两个区域的重合像素数量。
dice取0-1,为1时表示完全重合。
如果语义分割问题为二分类,则dice的计算方式为:
d
i
c
e
=
2
⋅
∑
i
y
^
i
y
i
+
ϵ
∑
i
y
^
i
2
+
∑
i
y
i
2
+
ϵ
dice=\frac{2 \cdot \sum_{i} \hat{y}_{i} y_{i}+\epsilon}{\sum_{i} \hat{y}_{i}^{2}+\sum_{i} y_{i}^{2}+\epsilon}
dice=∑iy^i2+∑iyi2+ϵ2⋅∑iy^iyi+ϵ
这里
i
i
i 表示图像中的第 i 个像素。
基于dice,有一种常见的损失函数叫 soft dice loss,计算方式如下:
soft dice loss
=
1
−
2
⋅
∑
i
y
^
i
y
i
+
ϵ
∑
i
y
^
i
2
+
∑
i
y
i
2
+
ϵ
\text { soft dice loss }=1-\frac{2 \cdot \sum_{i} \hat{y}_{i} y_{i}+\epsilon}{\sum_{i} \hat{y}_{i}^{2}+\sum_{i} y_{i}^{2}+\epsilon}
soft dice loss =1−∑iy^i2+∑iyi2+ϵ2⋅∑iy^iyi+ϵ
ϵ
\epsilon
ϵ 是一个很小的平滑项,用于避免分母为零。这种函数无法对每个像素单独求损失,只能对整幅图像。
前面的基于交叉熵的损失函数是先对每个像素算个损失,然后把整幅图像的损失相加求平均,损失函数通过减少错误分类的像素数量,进而更新模型参数。这里的计算单位则是整幅图像,由于Dice Loss 本质上是基于交集和并集的比值,更关注的是预测值与目标值的重叠区域,因此损失函数会试图最大化预测区域与目标区域的重叠程度,即便某个类别的像素点远远少于其他类别也没关系,所以对类别不平衡问题具有一定的鲁棒性。
这种方法能够更好地平衡不同类别间的权重分配,特别是在处理含有大量背景像素或其他占比较大的类别时,仍能有效地引导模型学习到少量但至关重要的目标类别信息。
当然,dice损失也是有缺点的。由于损失函数存在分母,且包含 y i ^ \hat{y_i} yi^的平方,在计算梯度的时候,分母将变成 y i ^ \hat{y_i} yi^的四阶矩,假如预测的 ∑ y ^ i 2 \sum \hat{y}_{i}^{2} ∑y^i2很小(即在大多数像素上都没预测到物体),此时如果 ∑ y i 2 \sum y_i^2 ∑yi2 也很小,则会使得计算得到的梯度很大,容易导致训练不稳定。所以在实际应用中,Soft Dice Loss 常与交叉熵损失(Cross-Entropy Loss)结合使用,如:
loss = alpha * soft_dice_loss + beta * cross_entropy_loss
对于多分类问题,soft dice loss计算方式则为:
soft dice loss
=
∑
c
(
1
−
2
⋅
∑
i
y
^
i
,
c
y
i
,
c
+
ϵ
∑
i
y
^
i
,
c
2
+
∑
i
y
i
,
c
2
+
ϵ
)
\text { soft dice loss }=\sum_{c}(1-\frac{2 \cdot \sum_{i} \hat{y}_{i,c} y_{i,c}+\epsilon}{\sum_{i} \hat{y}_{i,c}^{2}+\sum_{i} y_{i,c}^{2}+\epsilon})
soft dice loss =c∑(1−∑iy^i,c2+∑iyi,c2+ϵ2⋅∑iy^i,cyi,c+ϵ)
3.4 IOU Loss
对于二分类,公式为:
iou loss
=
1
−
∑
i
y
^
i
y
i
+
ϵ
∑
i
y
^
i
+
∑
i
y
i
+
ϵ
\text{iou loss}=1-\frac{\sum_{i} \hat{y}_{i} y_{i}+\epsilon}{\sum_{i} \hat{y}_{i}+\sum_{i} y_{i}+\epsilon}
iou loss=1−∑iy^i+∑iyi+ϵ∑iy^iyi+ϵ
它实际上就是 1-IOU。
和Dice Loss一样,它也存在训练过程不稳定的问题。
4 数据增强与alumentation
语义分割数据增强,相对于图像识别、目标检测,有几个方法是不能使用的:
- 一是resize,因为缩放涉及到了图像插值,而标签插值的话会出现小数(最近邻插值虽然不会出现小数,但会影响物体的边界,我也看到一些人用过,但放大倍数很小,对边界没有造成太大影响)。
- 二是Padding填充,图像填充0,标签该填什么,填充0的话,谁说padding拓展的区域一定是背景?
- 三是旋转不能使用,除非旋转90°或者90°的倍数,否则旋转会导致大量的空白区域出现,而标签中的空白区域无法填充。(在卷积的时候,对于中间的特征图可以padding,这里谈论的是图像预处理)
语义分割可以使用的数据增强有:HSV颜色变换、随机裁剪(裁剪到网络的规定尺寸)、随机镜像、90°旋转倍数等。
对于尺寸小于网络输入的图片,一般直接舍弃,因为没法处理。
5 常用模型
5.1 原始 UNet
Unet网络是医学图像分割领域常用的分割网络,因为网络的结构很像个U,所以称为Unet,其结构如下图所示(图片来源:听风吹等浪起:
从图中可以看出,下采样用的是最大池化层,左边的过程是下采样,右边的过程是上采样和特征融合。
5.2 UNet++
UNet++是对UNet网络架构进行改进的一个版本,他在UNet的基础上做了更强的特征融合,其结构如下:
图中黑色圆圈和原始UNet是相同的,绿色圆圈是特征融合的结果,我大致说一下过程:
- 1 X 1 , 0 X^{1,0} X1,0上采样后和 X 0 , 0 X^{0,0} X0,0融合得到 X 1 , 0 X^{1,0} X1,0;
- 2 X 2 , 0 X^{2,0} X2,0上采样后和 X 1 , 0 X^{1,0} X1,0融合得到 X 1 , 1 X^{1,1} X1,1;
- 3 X 1 , 1 X^{1,1} X1,1上采样后和 X 0 , 1 X^{0,1} X0,1、 X 0 , 0 X^{0,0} X0,0融合得到 X 1 , 1 X^{1,1} X1,1,这一步是三个特征层融合。
- 4 依次类推,每次特征图上采样后,要进行融合时,都需要把前面已经有的且尺寸一样的特征都融合进来。
- 5 最后得到的 X 0 , 4 X^{0,4} X0,4,是先对 X 1 , 3 X^{1,3} X1,3上采样,然后和 X 0 , 0 X^{0,0} X0,0、 X 0 , 1 X^{0,1} X0,1、 X 0 , 2 X^{0,2} X0,2、 X 0 , 3 X^{0,3} X0,3进行融合的结果。
最后计算损失函数时,并不是单单拿
X
0
,
4
X^{0,4}
X0,4的结果和target进行对比,而是同时拿
X
0
,
1
X^{0,1}
X0,1、
X
0
,
2
X^{0,2}
X0,2、
X
0
,
3
X^{0,3}
X0,3、
X
0
,
4
X^{0,4}
X0,4和target对比,也就是说,损失函数是这四层特征与目标值之间的损失之和。这样做的好处是,方便模型剪枝:
因为 X 0 , 1 X^{0,1} X0,1、 X 0 , 2 X^{0,2} X0,2、 X 0 , 3 X^{0,3} X0,3、 X 0 , 4 X^{0,4} X0,4和target的尺寸一致,并且也计算了它们和target的损失,那么它们中的任何一个都有一定的预测能力,所以推理的时候,如果要求速度快,不一定非得算到 X 0 , 4 X^{0,4} X0,4,可以只算到 X 0 , 2 X^{0,2} X0,2、 X 0 , 3 X^{0,3} X0,3,或者干脆只算到 X 0 , 1 X^{0,1} X0,1,当然,越到后面的结果,精度越高。
关于UNet++的代码实现可以看这篇文章。我个人认为没必要看源码,能理解模型结构就行,用的时候直接掉包。
5.3 DeepLabv3+
DeepLabv3+的结构如下图所示:
DeepLabv3+主体是Encoder-Decoder结构,其中Encoder有一个被称为 ASPP 的模块,它仿照 SPP 结构,在骨干网络获取特征图之后,使用几个不同膨胀率的空洞卷积层(padding等于膨胀率,可以保证卷积后的尺寸和卷积之前一致),以及一个全局平均池化层,并行地从特征图中提取信息,最后将这些信息进行特征融合。由于空洞卷积可以扩大感受野,不同的膨胀率对应的感受野不一样,所以多个空洞层能提取到不同尺度的空间信息,随后的融合实际上是多尺度特征的融合。另外,Encoder还会把backbone第一个块的特征图输出,因为这个特征图非常的底层,具有丰富的细节信息。
在Encoder处理完之后,Decoder 接收来自 ASPP 和骨干网络的低级特征(即ASPP的输出和backbone第一个块的特征图),通过一系列上采样和卷积操作逐渐恢复到输入图像的分辨率,这个过程有助于提高预测结果中对象边界的准确性。
关于DeepLabv3+的代码,可以看这篇文章。
5.4 模型选择
U-Net/U-Net++ 和 DeepLab 都是用于语义分割任务的深度学习模型,但它们在设计、应用场景及性能等方面各有优劣。以下是对这两种模型的优缺点分析:
5.4.1 U-Net 的优缺点
优点
-
对小目标友好:
- U-Net/U-Net++ 设计了跳跃连接(skip connections),可以有效地将低层次特征传递到高层次,这对于处理医学图像等小目标尤其有利,因为它有助于保留细节信息。
-
精确的边界定位:
- 由于其独特的编码器-解码器结构和跳跃连接,U-Net/U-Net++ 在边缘和细节的分割上表现出色,非常适合需要高精度的任务,如细胞或肿瘤的分割。
-
相对简单的架构:
- 相比于一些复杂的网络结构,U-Net/U-Net++ 的架构较为简单,易于理解和实现,同时训练成本也相对较低。
缺点
-
感受野有限:
- U-Net/U-Net++ 主要依赖于下采样和上采样的操作来获得不同尺度的信息,这限制了它的感受野,可能无法很好地捕捉到大范围的上下文信息。
-
多尺度信息处理能力有限:
- 虽然跳跃连接帮助它结合了不同层的信息,但在处理非常复杂或多尺度的对象时,U-Net/U-Net++ 可能不如其他专为此设计的模型效果好。
5.4.2 DeepLab 的优缺点
优点
-
强大的多尺度信息处理能力:
- DeepLab 系列通过使用空洞卷积(Atrous Convolution)和 Atrous Spatial Pyramid Pooling (ASPP) 来增强模型捕捉不同尺度对象的能力,这对自然场景中的语义分割特别有用。
-
更大的感受野:
- 使用空洞卷积可以在不增加参数数量或降低分辨率的情况下扩大感受野,使模型能够更好地理解图像中较大范围的上下文信息。
-
高效利用计算资源:
- DeepLabV3+ 引入了可分离卷积(Separable Convolution),这不仅减少了参数量,还降低了计算复杂度,提高了效率。
缺点
-
计算复杂度较高:
- 尽管有优化,DeepLab 的某些版本(尤其是早期版本)仍然具有较高的计算需求,对于硬件资源有限的环境来说可能不太适用。
-
相对于U-Net/U-Net++,在小目标上的表现可能不如后者:
- 对于特定领域的小目标,特别是那些要求极高精度边界的情况,DeepLab 可能没有U-Net 那么有效,因为它的设计更倾向于解决大规模、多样化场景的问题。
5.4.3 选择
-
选择 U-Net/U-Net++ 的情况:如果你的工作集中在医学图像分析、卫星图像处理或其他需要高度精确边缘检测的小规模或特定领域的任务上,U-Net/U-Net++ 是一个很好的选择。
-
选择 DeepLab 的情况:如果你面对的是更大规模的数据集,或者你的任务涉及自然场景中的语义分割,比如自动驾驶汽车的街景解析、人物分割等,那么 DeepLab 及其变体可能更适合你。它们提供了更强的多尺度信息处理能力和更大的感受野。
根据具体的应用需求、数据集特性以及可用的计算资源来决定使用哪种模型是最为关键的。