写在前面
(今天我们来介绍两篇论文,以CE-Net为主,因为CE-Net用到了PsP-Net中的block,所以我们顺带一起讲一下。semantic seg是逐像素点的分类,所以某种意义上讲semantic seg也可以称为 dense seg)
《CE-Net: Context Encoder Network for 2D Medical Image Segmentation》是一篇将空洞卷积和金字塔池化结合,专门用在2D医学图像分割的paper,文章发表在IEEE transcations on medical imaging(TMI2019).
论文地址:https://arxiv.org/abs/1903.02740
代码实现(Pytroch):https://github.com/Guzaiwang/CE-Net(官方实现)
数据集(开源):Kaggle上肺部、视神经、视网膜(https://drive.grand-challenge.org/DRIVE/)、细胞分割。
分析问题
Unet连续的卷积和池化操作会丢掉很多空间信息(注意哦 这里分别提到了卷积和池化操作都会丢spatial info 那么下面的解决方法就是针对这两个操作分别进行改进的)
解决方法
作者提出CE-Net,主要的contribution有:
1.对Unet进行结构上的改进
- 提出DAC(Dense Atrous Convolution)block。 利用空洞卷积,通过增大感受野获取更多high-level的info,作者说high-level的信息有助于分割精确度的提升。
- 提出RMP(Residual Multi-kernel pooling)block。 与其说是“提出”,不如说直接拿PsP-Net中的block来用了,就是金字塔池化,利用不同kernel-size的池化操作,preserve不同scale的空间信息。
2.对Unet内部细节进行改进
encoder所有的特征提取器均采用预训练的ResNet-34(即所有的卷积操作全部变成ResNet部分结构了)替换了U-Net中直接两次卷积操作;decoder采用了bottleneck的设计(即先1*1conv先减少参数 再3*3deconv转置卷积操作 最后1*1conv还原channel )
3.泛化性
作者实验部分将CE-Net在非常多的数据集(肺结节分割、视网膜血管分割、细胞分割)上进行训练,各项指标性结果均有有效提升。
整体网络结构
CE-Net主要由feature encoder + context extractor + feature decoder三个模块组成,比起原始的Unet,本文创新点主要在context extactor这里;
context extractor(上图中红色虚线框部分)主要由DAC + RMP组成。
那下面咱们详细看一下这两个主要的block吧~
DAC block(to capture high-level semantic feature map)
还记得我们开始提出的问题吗,这个block就是针对连续的卷积操作丢失空间信息问题的。
下面先来看空洞卷积示意图,其作用就是在参数量不变的情况下增大卷积核的感受野。以下图为例,kernel同样有9个参数,从左到右第一个卷积核的感受野是3,第二个是7,第三个是11。我的理解是感受野越大那么获取的high-level信息越多,越有利于提高分割结果的准确性;而感受野越小的话丢失的细节信息越少,对小目标识别来说更有利。
受到Inception-ResNet-V2和以上空洞卷积的启发,本文中作者提出了以下DAC结构:
简单解释一下Inception,它加宽(widen)了卷积操作,是一个并行结构,用不同size的卷积核分别对同一个feature map进行卷积操作,得到的结果进行concat操作,也可以叫做Multi-scale的操作。
提到了Inception加宽了卷积操作,同样的也可以加深(deepen)卷积操作,这就是何恺明大神著名的ResNet思想啦,可以使网络结构很深同时保证梯度不爆炸不消失。
那现在作者将Inception和ResNet思想结合起来,理论上讲可以同时拥有两种结构的优点。
RMP block(to detect objects at different sizes)
这个block结构上跟PsP-net是一样的。
CE-Net的RMP block如下:
PsP-Net的PPM如下:
结构上都是对同一个feature map并行不同尺寸的pooling操作,最后把结果concat起来。这样可以得到不同size的feature map,为了减少训练参数每次pooling后进行一个1*1卷积。
实验结果
作者认为传统的交叉熵损失不适合小目标(即前景/背景很小)的医学图像,所以损失函数选用Diceloss+reg(正则化项用来防止网络过拟合)
作者分别对视神经、视网膜血管、肺部、细胞进行了分割实验。
先来看指标性结果:
再看一下视觉效果:
无论是从指标性结果还是视觉效果上看,本文对CE-Net确实有不错的提升,而且是对各个分割任务都有提升。
关键代码解释(看注释哦~)
class CE_Net_backbone_DAC_with_inception(nn.Module):
def __init__(self, num_classes=1, num_channels=3):
super(CE_Net_backbone_DAC_with_inception, self).__init__()
filters = [64, 128, 256, 512]
resnet = models.resnet34(pretrained=True)
self.firstconv = resnet.conv1 # Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
self.firstbn = resnet.bn1
self.firstrelu = resnet.relu
self.firstmaxpool = resnet.maxpool
self.encoder1 = resnet.layer1
'''
Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(2): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
'''
self.encoder2 = resnet.layer2
self.encoder3 = resnet.layer3
self.encoder4 = resnet.layer4
self.dblock = DACblock_with_inception(512)
self.decoder4 = DecoderBlock(512, filters[2])
self.decoder3 = DecoderBlock(filters[2], filters[1])
self.decoder2 = DecoderBlock(filters[1], filters[0])
self.decoder1 = DecoderBlock(filters[0], filters[0])
self.finaldeconv1 = nn.ConvTranspose2d(filters[0], 32, 4, 2, 1)
self.finalrelu1 = nonlinearity
self.finalconv2 = nn.Conv2d(32, 32, 3, padding=1)
self.finalrelu2 = nonlinearity
self.finalconv3 = nn.Conv2d(32, num_classes, 3, padding=1)
def forward(self, x):
# Encoder
x = self.firstconv(x)
x = self.firstbn(x)
x = self.firstrelu(x)
x = self.firstmaxpool(x)
e1 = self.encoder1(x)
e2 = self.encoder2(e1)
e3 = self.encoder3(e2)
e4 = self.encoder4(e3)
# Center
e4 = self.dblock(e4)
# e4 = self.spp(e4)
# Decoder
d4 = self.decoder4(e4) + e3
d3 = self.decoder3(d4) + e2
d2 = self.decoder2(d3) + e1
d1 = self.decoder1(d2)
out = self.finaldeconv1(d1)
out = self.finalrelu1(out)
out = self.finalconv2(out)
out = self.finalrelu2(out)
out = self.finalconv3(out)
return F.sigmoid(out)
class CE_Net_backbone_inception_blocks(nn.Module):
def __init__(self, num_classes=1, num_channels=3):
super(CE_Net_backbone_inception_blocks, self).__init__()
filters = [64, 128, 256, 512]
resnet = models.resnet34(pretrained=True)
self.firstconv = resnet.conv1
self.firstbn = resnet.bn1
self.firstrelu = resnet.relu
self.firstmaxpool = resnet.maxpool
self.encoder1 = resnet.layer1
self.encoder2 = resnet.layer2
self.encoder3 = resnet.layer3
self.encoder4 = resnet.layer4
self.dblock = DACblock_with_inception_blocks(512)
self.decoder4 = DecoderBlock(512, filters[2])
self.decoder3 = DecoderBlock(filters[2], filters[1])
self.decoder2 = DecoderBlock(filters[1], filters[0])
self.decoder1 = DecoderBlock(filters[0], filters[0])
self.finaldeconv1 = nn.ConvTranspose2d(filters[0], 32, 4, 2, 1)
self.finalrelu1 = nonlinearity
self.finalconv2 = nn.Conv2d(32, 32, 3, padding=1)
self.finalrelu2 = nonlinearity
self.finalconv3 = nn.Conv2d(32, num_classes, 3, padding=1)
def forward(self, x):
# Encoder
x = self.firstconv(x)
x = self.firstbn(x)
x = self.firstrelu(x)
x = self.firstmaxpool(x)
e1 = self.encoder1(x)
e2 = self.encoder2(e1)
e3 = self.encoder3(e2)
e4 = self.encoder4(e3)
# Center
e4 = self.dblock(e4)
# e4 = self.spp(e4)
# Decoder
d4 = self.decoder4(e4) + e3
d3 = self.decoder3(d4) + e2
d2 = self.decoder2(d3) + e1
d1 = self.decoder1(d2)
out = self.finaldeconv1(d1)
out = self.finalrelu1(out)
out = self.finalconv2(out)
out = self.finalrelu2(out)
out = self.finalconv3(out)
return F.sigmoid(out)
思考
1.CE-Net的有效性还有待在自己的数据集上考证(毕竟实验里对这么多医学图像分割任务都有提升未免也太厉害了)
2.这两block的位置是不是可以放在encode的其他地方。比如在Unet之后有一篇Unet++发表在MICCAI,它就引入了更多的skip connection和上采样,如下图所示。按照这个思路DAC完全可以放在U-net每个doubleconv之后。
后记
未来一周我会在自己的数据集上进行训练测试CE-net看一下其有效性,待补充...
杂
在家充电完成啦,回来继续好好学习,保持积极平稳的心态,稳步前进!今天突然想到诗经里的“投我以木桃,报之以琼瑶.匪报也,永以为好也!”写的好美啊。古人可以借诗抒情,今人也可以借知乎文章明志嘛~因为写这个文章会有一些花时间,但今后我会保持一周至少一次论文输出,加油!(这次的配图来自泰山/玉皇山顶~
以上,如果有理解不当的地方,欢迎大家批评指正呀!(乐交诤友, 鞠躬感谢!