SIIM-ACR肺部图像分割kaggle大赛方案解读

最近开始着手一些医学图像分割的项目和比赛,但是这方面的内容比较稀缺。目前来讲医学图像的处理主要面临以下几个方面的问题:

  1. 图像太大,病理图片有些可以达到10w*10w
  2. 标注不准确,需要很有经验的医生标注,并多个医生反复检查。通常都会面临标注问题

简介

为了快速进入这一领域,我找了SIIM-ACR肺部图像分割kaggle大赛来学习。该比赛由医学影像信息学学会(SIIM)2019年主办,共1475支队伍参加比赛。任务是于从一组胸部X光片中对气胸进行分类(并,将其分割)。气胸可能是由于钝性胸部损伤,潜在的肺部疾病损害或最令人恐惧的原因引起的,根本没有明显的原因。在某些情况下,肺萎陷可能危及生命。评价指标:Dice coefficient。并且获胜者参加C-MIMI 2019。

数据集介绍

数据由DICOM格式的图像以及图像ID和行程编码(RLE)掩码形式的注释组成。一些图像包含气胸(塌陷的肺),数据分为两个阶段发放。总共包括10679份dicom文件(站立位胸片)。有气胸:无气胸 = 2379:8300。 有气胸的胸片皆有mask (run-length-encoded (RLE))格式 。一个胸片如果有多处气胸,会有多个单独的mask。

解决方案介绍

第一名方案
  • 特色:triple rule
    传统分割任务在后处理时是基于doublet rule(top_score_threshold,min_contour_area)进行,即大于threshold的阈值,同时最小区域面积像素大于min_area才能被标记为正样本。Aimoldin Anuar使用(top_score_threshold, min_contour_area, bottom_score_threshold)方案,首先利用top_score_threshold二值化,再把contour_area
classification_mask = predicted > top_score_threshold
mask = predicted.copy()
mask[classification_mask.sum(axis=(1,2,3)) < min_contour_area, :,:,:] = np.zeros_like(predicted[0])
mask = mask > bot_score_threshold
return mask

Best triplet on validation: (0.75, 2000, 0.3)
Best triplet on Public Leaderboard: (0.7, 600, 0.3)


triplet rule
  • 数据增强
albu.Compose([
    albu.HorizontalFlip(),
    albu.OneOf([
        albu.RandomContrast(),
        albu.RandomGamma(),
        albu.RandomBrightness(),
        ], p=0.3),
    albu.OneOf([
        albu.ElasticTransform(alpha=120, sigma=120 * 0.05, alpha_affine=120 * 0.03),
        albu.GridDistortion(),
        albu.OpticalDistortion(distort_limit=2, shift_limit=0.5),
        ], p=0.3),
    albu.ShiftScaleRotate(),
    albu.Resize(img_size,img_size,always_apply=True),
])

没有使用h通道增强

  • 训练
    loss:
    使用 [combo loss] 即结合BCE, dice and focal loss。(3,1,4) for albunet_valid and seunet; (1,1,1) for albunet_public; (2,1,2) for resnet50.
    作者开始只使用1-1-1计划进行训练我获得了最好的public 成绩。但注意到在开始时loss损失比其他损失高大约10倍。为了平衡它们,改用使用3-1-4方案,它为我带来了最佳的验证分数。作为一种折衷,resnet50使用2-1-2方案。
    采样:
    在输入模型的时候在torch dataset上采样。分为了四个阶段,
    第0部分-从具有大学习率(大约1e-3或1e-4),大采样率(0.8)和ReduceLROnPlateau的预训练模型中训练10-12个epoch。可以在imagenet或较低分辨率(512x512)的数据集上对模型进行预训练。这部分的目标是:快速获得足够好的模型,其验证分数约为0.835。
    第1部分-使用正常学习率(〜1e-5),大采样率(0.6)和CosineAnnealingLR或CosineAnnealingWarm重启上一步中的最佳模型,重复直到达到最佳收敛。
    第2部分-使用正常学习率(〜1e-5),小采样率(0.4)和CosineAnnealingLR或CosineAnnealingWarm重新启动调度程序,从上一步中升级最佳模型。重复直到达到最佳收敛。
    所有实验(resnet50除外)在512x512分辨率下使用早期编码器上的冻结编码器进行训练,其尺寸为1024x1024。
  • 每个pipeline 的每折的平均Top3模型融合
  • Horizontal flip TTA
第二名方案
  • 特色之处:分为两个任务
  1. 分类:这部分用于分类图像是否与气胸有关。模型是一个基于unet的多任务模型,带有一个用于分类的分支。
    数据:所有数据
    Cls损失:BCE +Focal损失
    Seg损失:BCE
    增强:hflip, scale, rotate, bright, blur
    主干:seresnext 50,seresnext101,efficiencynet-b3
    集成:stacking
  2. 分割:
    有两种用于分割的模型: unet和deeplabv3。
    数据:带有气胸的数据
    loss:dice loss
    增强:与分类相同
    主干:seresnext50,seresnext101,efficiencynet-b3,efficiencynet-b5
    集成:平均

分类结果:seresnext50>serenext101>efficientnet-b3.
分割结果:seresnext101>efficientnet-b5>seresnext50>efficientnet-b3.

  • 增强
RESIZE_SIZE = 1024 # or 768
train_transform = albumentations.Compose([
        albumentations.Resize(RESIZE_SIZE, RESIZE_SIZE),
        albumentations.OneOf([
            albumentations.RandomGamma(gamma_limit=(60, 120), p=0.9),
            albumentations.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.9),
            albumentations.CLAHE(clip_limit=4.0, tile_grid_size=(4, 4), p=0.9),
        ]),
        albumentations.OneOf([
            albumentations.Blur(blur_limit=4, p=1),
            albumentations.MotionBlur(blur_limit=4, p=1),
            albumentations.MedianBlur(blur_limit=4, p=1)
        ], p=0.5),
        albumentations.HorizontalFlip(p=0.5),
        albumentations.ShiftScaleRotate(shift_limit=0.2, scale_limit=0.2, rotate_limit=20,
                                        interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_CONSTANT, p=1),
        albumentations.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), max_pixel_value=255.0, p=1.0)
    ])
第三名方案
  • 特色:分为两个阶段,先分割肺部,在crop之后分割气胸部位
    由于图像尺寸很小,为了节省内存和计算量,首先训练了一个UNET模型,以从1024x1024图像中预测出肺部,所有模型均基于裁剪后的肺部。而576x576裁剪后的图像对于模型来说已经足够好了。


    整个分割过程图

    肺部分割图

    气胸分割利用外部数据集

    气胸分割图

    无阈值搜索,仅使用0.5
    Optimizer:Adam具有0.0001的学习率,训练期间无学习率变化。
    时期:6个时期后模型参数的EMA为15

第四名方案

型号:UNet。
backbone:具有冻结批归一化功能的ResNet34骨干。
预处理:随机裁剪(512, 512)大小,到(768, 768)大小
增强:ShiftScaleRotate,RandomBrightnessContrast,ElasticTransform,Horizo​​ntalFlip
优化器:Adam,batch_size = 8
Scheduler:CosineAnnealingLR
附加功能:非空样本的比例线性地从0.8减少到0.22(如在数据集中一样),取决于时期。它有助于更​​快地收敛。
损失:2.7 * BCE(pred_mask, gt_mask) + 0.9 * DICE(pred_mask, gt_mask) + 0.1 * BCE(pred_empty, gt_empty)。这pred_mask是UNet的预测,pred_empty是空掩码分类的分支的预测。
后处理:if pred_empty > 0.4 or area(pred_mask) < 800: pred_mask = empty。在验证集上选择参数。
融合:将8折的4个最佳检查点平均,水平翻转TTA。
硬件:4 x RTX 2080

第五名方案

与第三名类似,利用外部数据集,先分类再分割
Network : Unet with Aspp
Backbone : se50 & se101
Image size: (1024, 1024)
Optimizer : Adam
Loss : 1024 * BCE(results, masks) + BCE(cls, cls_target)
Semi-supervision: mean-teacher[1-2] with NIH Dataset (0.874 ---> 0.880)

Scores:
stage1 0.8821
stage2 0.8643

第六名方案

基于EncodingNet(ResNets,512和1024大小)和UNet(EfficientNet4,se-resnext50,具有512、640和1024大小的SENet154)的最终解决方案。
最佳增强与crops and rotations有关。没有使用对比度和亮度转换。
损失:BCE + Dice(尝试过Focal,没有用)
较低的图像尺寸会降低很多分数,因此全尺寸模型应该更好。
技巧:
带有大量TTA的EncondingNet上的分类(11种方法)
删除小区域Segmentation
也注意到水平翻转增强降低了局部CV,但是没有在LB上检查它。
验证使用了4折交叉。对于每个样本,都进行了4混合模型。

  • 增强
AUG = Compose(
    [
       HorizontalFlip(p=0.5),
       OneOf(
           [
              ElasticTransform(
                  alpha=300,
                  sigma=300 * 0.05,
                  alpha_affine=300 * 0.03
              ),
              GridDistortion(),
              OpticalDistortion(distort_limit=2, shift_limit=0.5),
           ],
           p=0.3
       ),
       RandomSizedCrop(min_max_height=(900, 1024), height=1024, width=1024, p=0.5),
       ShiftScaleRotate(rotate_limit=20, p=0.5)
    ],
    p=1
)

Model1:在Pascal数据集上预训练了Resnet50的EncNet。 首先使用AUG对512 x 512分辨率的4折合模型进行了训练。 其次使用AUG训练了1024 x 1024分辨率的4折合模型。 4折。
损失:DICE
增强:AUG
TTA:翻转,Clahe和np.arange(-20, 21, 5)角度旋转(总计10 TTA)
模型2-3:以 1024 x 1024分辨率训练了两个具有SEResnext50和SEResnet152主干的类似Unet的模型。每个模型也为4折。
损失:骰子
增强:AUG
TTA:翻转
Model4:具有EfficientNetB4主干的类似Unet的模型,经过640 x 640分辨率训练。 也做4折混合。
损失:Dice+ BCE
增强:AUG
TTA:无TTA
最终模型:
模型1,2,3和4平均
分类阈值:〜0.32
细分阈值:〜0.375

第七名方案

解决方案非常简单,它是不同模型(每个都有几折)的整体简单平均值:

  • FPNetResNet50(5折)
  • FPNetResNet101(5折)
  • FPNetResNet101(7倍,种子不同)
  • PANetDilatedResNet34(4折)
  • PANetResNet50(4折)
  • EMANetResNet101(2折)
    使用AdamW优化器以768x768(或更接近)训练的模型 。对于FPN模型,使用Flip TTA,其余使用比例尺(1024)TTA。我们使用了两个阈值,一个用于细分,另一个(较高值)用于分类。
    对于EMANets,使用PANet的GAU模块将8x的陡峭双线性上采样更改为2x逐渐上采样。它大大提高了性能。
    为了进行汇总,使用了模型的软预测的简单平均值(S型之后,阈值之前)。最后使用两个值进行阈值化。较小的值(0.2)用于分割,另一个(0.72)用于非气胸图像的分类。
    没用:HRNet
    FishNet as encoder (not bad but no improvements either)
    A second-level classifier based on gradient boosting using segnets' outputs and metadata as features
第七名方案
  • 使用单一模型deeplabV3+
    数据分割:整体保持率10%,剩余90%保持率10倍,按气胸大小分层
    架构:DeepLabV3 +
    骨干网:具有组标准化功能的ResNet50 / 101和ResNeXt50 / 101
    损失:加权BCE(在所有图像上训练)或Dice(仅在正片上训练)
    优化程序:Vanilla SGD,动量0.9
    培训:批量大小为4,1024 x 1024; 批量大小为1,1280 x 1280仅分割(未在阶段2重新训练)
    方法:余弦退火,100个epoch,5个snapshots,初始LR 0.01至0.0001
    合计:
    -总共12个模型( 每个模型x3个snapshots)
    -仅在正样本上使用softloss,训练了4个模型
    -在加权BCE的所有图像上训练了8个模型
    -在所有图像上训练了4个模型作为“分类器”
    -最大像素值为用作分类得分,在4个模型中平均
    -将仅对正值训练的4个模型的像素级得分乘以该分类得分,然后取平均值-最终合奏:将上述平均得分乘以基于其他4/8模型的像素级得分在所有图像上进行训练
    -Hflip TTA
    后期处理:
    -删除区域总大小小于2048、4096像素的图像
  • 总结:
    与Felipe的整合模型:他使用Unet和EfficientNetB4内核,使用512 x 512的softDice训练的所有图像达到0.8750。无法正确地整合我们的模型,最终放弃了它,转而支持稍微好一点的整合。
    亲自尝试了较低的分辨率和Unet,LinkNet,PSPNet,EncNet,HRNet等。所有这些都比DeepLab差,分辨率为1024 x 1024
    全图像分类器:效果不如分割
    SGD比Adabound优化器Adam更好
    Hflip TTA +去除小区域起到效果
    加权BCE损失最稳定,Lovasz,softloss不稳定或根本没有收敛
    validation:0.8737
    阶段1public:0.8780(删除4096像素)
    阶段2私有:0.8627(删除2048像素)
第八名方案

常见:
数据拆分:CV5
优化器:Adam
Scheduler:Reduce lr on plateau
增强:relatively aggressive: ShiftScaleRotate, Grid- and Elastic-transformation, GaussianNoise,GaussianNoise
分类:
型号:se_resnext101(2个快照),senet154
分辨率:768x768
Loss:BCE
附加功能:TTAx2(水平),伪标记,梯度累加(bs = 100-200)
这里的关键是模型的选择。
分割:
型号:Unets:dpn98,se_resnet101,se_densenet121(每个快照2张)
训练过程包括四个阶段:

  1. 损失:BCE + dice;尺寸:512x512
  2. 损失:BCE + dice;尺寸:1024x1024
  3. 损失:BCE + dice;尺寸:1024x1024
  4. 损失:symmetric lovasz;尺寸:1024x1024
    附加:TTAx6(水平+重新缩放),梯度累加(bs = 50-100)
    什么没有奏效:
  • 与其他编码器一起使用的Unets的效果更差(resnet34,resnet50,dpn107,dpn131,se_resnext50_32x4d,se_resnext101_32x4d,senet154)。senet154在尺寸为512x512的情况下比其他型号要好得多,但在扩展到1024x1024时完全失败。
  • Lookahead optimizer优化器未改善优化过程
  • 分类的其他损失:FocalLoss,SoftF1Loss。FocalLoss的准确性优于BCE,但f0.5度量则较差。
第九名方案
  • 两个步骤:
    分类:使用Unet模型。并通过像素数阈值确定图片中是否存在气胸。根据验证,最佳阈值为2000。
    骨干:seresnext50
    数据:在此步骤中,使用了所有图像和平衡的批次(气胸/非气胸),这大大加快了收敛速度。
    数据拆分:5倍和10倍按气胸区域分层。
    输入大小:768x768
    损失:BCE
    增强: hflips, rotations(up to 10 degree), random brightness, contrast and gamma, blur
    Lr调度:reduce lr on plateau with patience=3 epochs.
    这种方法给出了很好的结果,但是仍然存在大量的假否定示例。因此决定使用一开始就训练的分类模型,如果所有模型都将其归因于肺气胸,则将其标记为包含气胸。
    分割:
    这个阶段的核心要素也是Unet模型,但仅在分类阶段被确定为包含气胸的图片中进行模型预测。
    骨干:seresnext50
    数据:仅包含气胸的图像
    数据分割:按气胸区域分层的5折和10折
    输入大小:928x928、768x768
    损失:BCE +DICE
    增强:与分类阶段相同
    Lr调度: reduce lr on plateau with patience=5 epochs.
第十名方案UNet with mask scoring head

预测分割:img大小896
预测分类:img尺寸768
整合预测:删除低概率mask
在此术语中,MS-EUNet的意思是“用EfifcientNet B4 UNet”。
TTA采用水平翻转

class BCEDiceLoss(nn.Module):
    """
    Loss defined as alpha * BCELoss - (1 - alpha) * DiceLoss
    """
    def __init__(self, alpha=0.5):
        super(BCEDiceLoss, self).__init__()
        self.bce_loss = nn.BCEWithLogitsLoss()
        self.dice_loss = DiceLoss()
        self.alpha = alpha

    def forward(self, logits, targets):
        bce_loss = self.bce_loss(logits, targets)
        dice_loss = self.dice_loss(logits, targets)
        loss = self.alpha * bce_loss + (1. - self.alpha) * dice_loss
        return loss


class DiceScoreL2Loss(nn.Module):
    """
    Loss for mask scoring UNet

    Caution: This loss for 1 class !!!!!
    """
    def __init__(self):
        super(DiceScoreL2Loss, self).__init__()
        self.smooth = 1e-8
        self.sum_dim = (2, 3)

    def forward(self, dice_logits, logits, masks_gt):
        dice_preds = torch.sigmoid(dice_logits)

        # BATCH x 1 x H x W
        masks_preds = torch.sigmoid(logits)

        # BATCH x 1
        intersect = (masks_preds * masks_gt).sum(self.sum_dim).float()
        union = (masks_preds.sum(self.sum_dim) + masks_gt.sum(self.sum_dim))

        # Calc dice(BATCH x 1)
        dice_gt = (2. * intersect + self.smooth) / (union + self.smooth)

        # Calc L2 Loss
        cond = torch.abs(dice_gt - dice_preds)
        loss = 0.5 * cond ** 2
        return loss.mean()

loss_func1 = BCEDiceLoss(alpha=0.7)
loss_func2 = DiceScoreL2Loss()
loss = loss_func1(logits, masks) + 0.2 * loss_func2(logits_score, logits, masks)
方案汇总

你可能感兴趣的:(SIIM-ACR肺部图像分割kaggle大赛方案解读)