1 、简介
在临床实践中,医学图像解释通常涉及多标签分类,因为患者的犯病的地方往往会出现多种症状或并发症。最近,在医学图像解释上基于深度学习的框架已达到专家级的性能,这其中部分功劳可以归功于大量准确的注释或者说标注。但是,手动注释大量医学图像是不切实际的,而自动注释快速但不精确(可能引入损坏的标签)。
在这项工作中,作者提出了一种新的正则化方法,称为Flow-Mixup,用于带有损坏标签的多标签医学图像分类。Flow-Mixup指导模型捕获每种异常的鲁棒特征,从而帮助有效处理损坏的标签,并可以应用自动注释。
具体地说,Flow-Mixup通过向模型的隐藏状态添加约束来解耦提取的特征。而且,与其他已知的正则化方法相比,Flow-Mixup更稳定,更有效,理论和经验分析所示。对两个心电图数据集和包含损坏标签的胸部X射线数据集进行的实验验证了Flow-Mixup是有效的,并且对损坏的标签不敏感。
这项工作的3个主要贡献:
- 1、提出了一种用于多标签医学图像分类的正则化方法Flow-Mixup,并证明了Flow-Mixup对已损坏标签的鲁棒性;
- 2、对Mixup、Manifold-Mixup以及Flow-Mixup进行了比较,表明使用Mixup、Manifold-Mixup会产生“correlation conflicts”现象和“distribution shift”现象。
- 3、在几个带有损坏标签的多标签医学图像分类数据集上进行了实验,验证了Flow-Mixup优于已知的正则化方法。
2 相关正则化方法
2.1 Mixup
Mixup介绍
Mixup是一种运用在计算机视觉中的对图像进行混类增强的算法,它可以将不同类之间的图像进行混合,从而扩充训练数据集。
Mixup原理
假设是一个样本,是该样本对应的标签;是另一个样本,是该样本对应的标签,是由参数为,的贝塔分布计算出来的混合系数,由此可以得到Mixup原理公式为:
其中指的是贝塔分布,是混合后的样本,是混合后的样本对应的标签。
需要说明几点:
1.在论文作者多组实验中,无论如何设置,的值,期望始终近似为0.5。这可能是由于权重在每个batch样本都会随机产生,在整个训练过程中会有N个batch,权重在N个batch中期望近似为0.5。所以作者认为产生与样本混合权重的Beta分布参数时,算法效果相对较好。Beta分布如下图:
2.与没有太多的限制,当=1时,就是两张图片样本混合;当>1时,便是2个图片样本两两对应混合。此外,与可以是同一批样本,也可以是不同批样本。一般在代码实现过程中,两个图片是同一批样本,唯一不同的是,是原始图片样本,而是对在维度进行shuffle后得到的。
Mixup代码实现
mixup代码实现部分如下:
import numpy as np import torch import torch.nn as nn from loss.focal import FocalLoss LOSS=FocalLoss() def criterion(batch_x, batch_y, alpha=1.0, use_cuda=True): ''' batch_x:批样本数,shape=[batch_size,channels,width,height] batch_y:批样本标签,shape=[batch_size] alpha:生成lam的beta分布参数,一般取0.5效果较好 use_cuda:是否使用cuda returns: mixed inputs, pairs of targets, and lam ''' if alpha > 0: #alpha=0.5使得lam有较大概率取0或1附近 lam = np.random.beta(alpha, alpha) else: lam = 1 batch_size = batch_x.size()[0] if use_cuda: index = torch.randperm(batch_size).cuda() else: index = torch.randperm(batch_size) #生成打乱的batch_size索引 #获得混合的mixed_batchx数据,可以是同类(同张图片)混合,也可以是异类(不同图片)混合 mixed_batchx = lam * batch_x + (1 - lam) * batch_x[index, :] """ Example: 假设batch_x.shape=[2,3,112,112],batch_size=2时, 如果index=[0,1]的话,则可看成mixed_batchx=lam*[[0,1],3,112,112]+(1-lam)*[[0,1],3,112,112]=[[0,1],3,112,112],即为同类混合 如果index=[1,0]的话,则可看成mixed_batchx=lam*[[0,1],3,112,112]+(1-lam)*[[1,0],3,112,112]=[batch_size,3,112,112],即为异类混合 """ batch_ya, batch_yb = batch_y, batch_y[index] return mixed_batchx, batch_ya, batch_yb, lam def mixup_criterion(criterion, inputs, batch_ya, batch_yb, lam): return lam * criterion(inputs, batch_ya) + (1 - lam) * criterion(inputs, batch_yb) ########################################################################## #####################修改位置3:train.py文件修改代码如下###################### if torch.cuda.is_available() and DEVICE.type=="cuda": #add inputs, targets = inputs.cuda(), targets.cuda() else: inputs = inputs.to(DEVICE) targets = targets.to(DEVICE).long() if cfg['USE_MIXUP']: inputs, targets_a, targets_b, lam = mixup.mixup_data( inputs,targets,cfg["MIXUP_ALPHA"], torch.cuda.is_available()) #映射为Variable inputs, targets_a, targets_b = map(Variable, (inputs,targets_a,targets_b)) #抽取特征,BACKBONE为粗特征抽取网络 features = BACKBONE(inputs) #抽取特征,HEAD为精细的特征抽取网络 outputs = mixup.mixup_criterion(HEAD, features, targets_a, targets_b, lam) loss = mixup.mixup_criterion(LOSS, outputs, targets_a, targets_b, lam) else: features = BACKBONE(inputs) outputs = HEAD(features, targets) loss = FocalLoss(outputs, labels)
2.2 Manifold Mixup
基于Mixup,后续涌现出一些很棒的工作。Manifold mixup通过对Hidden States进行插值,取得了优于Mixup的效果。
Maniflod Mixup的具体流程如下:
- 1、随机选网络的某一层第k层(包括输入层);
- 2、传2个Batch的数据给网络,前向传播到第k层,得到隐藏表征hidden reprense 和;
- 3、使用Mixup:
- 4、继续前向传播直至得到输出;
- 5、计算损失和梯度:
def mixup_process(out, target_reweighted, lam): indices = np.random.permutation(out.size(0)) out = out*lam + out[indices]*(1-lam) target_shuffled_onehot = target_reweighted[indices] target_reweighted = target_reweighted * lam + target_shuffled_onehot * (1 - lam) #t1 = target.data.cpu().numpy() #t2 = target[indices].data.cpu().numpy() #print (np.sum(t1==t2)) return out, target_reweighted class PreActResNet(nn.Module): def __init__(self, block, num_blocks, initial_channels, num_classes, per_img_std= False, stride=1): super(PreActResNet, self).__init__() self.in_planes = initial_channels self.num_classes = num_classes self.per_img_std = per_img_std #import pdb; pdb.set_trace() self.conv1 = nn.Conv2d(3, initial_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.layer1 = self._make_layer(block, initial_channels, num_blocks[0], stride=1) self.layer2 = self._make_layer(block, initial_channels*2, num_blocks[1], stride=2) self.layer3 = self._make_layer(block, initial_channels*4, num_blocks[2], stride=2) self.layer4 = self._make_layer(block, initial_channels*8, num_blocks[3], stride=2) self.linear = nn.Linear(initial_channels*8*block.expansion, num_classes) def _make_layer(self, block, planes, num_blocks, stride): strides = [stride] + [1]*(num_blocks-1) layers = [] for stride in strides: layers.append(block(self.in_planes, planes, stride)) self.in_planes = planes * block.expansion return nn.Sequential(*layers) def compute_h1(self,x): out = x out = self.conv1(out) out = self.layer1(out) return out def compute_h2(self,x): out = x out = self.conv1(out) out = self.layer1(out) out = self.layer2(out) return out def forward(self, x, target= None, mixup=False, mixup_hidden=False, mixup_alpha=None): #import pdb; pdb.set_trace() if self.per_img_std: x = per_image_standardization(x) if mixup_hidden: layer_mix = random.randint(0,2) elif mixup: layer_mix = 0 else: layer_mix = None out = x if mixup_alpha is not None: lam = get_lambda(mixup_alpha) lam = torch.from_numpy(np.array([lam]).astype('float32')).cuda() lam = Variable(lam) if target is not None : target_reweighted = to_one_hot(target,self.num_classes) if layer_mix == 0: out, target_reweighted = mixup_process(out, target_reweighted, lam=lam) out = self.conv1(out) out = self.layer1(out) if layer_mix == 1: out, target_reweighted = mixup_process(out, target_reweighted, lam=lam) out = self.layer2(out) if layer_mix == 2: out, target_reweighted = mixup_process(out, target_reweighted, lam=lam) out = self.layer3(out) if layer_mix == 3: out, target_reweighted = mixup_process(out, target_reweighted, lam=lam) out = self.layer4(out) out = F.avg_pool2d(out, 4) out = out.view(out.size(0), -1) out = self.linear(out) if target is not None: return out, target_reweighted else: return out
然而,由于“correlation conflicts”现象和“distribution shift”现象,这两种方法由于混淆忽略了异常之间的特征相关性,因此都不太适合多标签分类,而Manifold Mixup在训练中往往是不稳定的。因此本文提出了一种用于多标签医学图像分类的Flow-Mixup方法,避免了Mixup和Manifold Mixup的缺点。
3 Flow-Mixup
3.1 Flow-Mixup概述
在本文中提出了一种新的正则化方法Flow-Mixup,用于多标签医学图像的分类。考虑到深度学习分类器,其中是一个非线性函数,是一个线性函数。采用Flow-Mixup的正向训练过程有以下几个步骤:
首先,在训练前选择隐藏状态s将模型分为非线性部分和线性部分、,为模型输出。
其次,将数据(例如图像)转发到选定的隐藏状态,并将一个新的混合模块应用到隐藏状态的特征(混合模块如下图所示)。
最后,经过Mixing module的处理后,该特征继续进行前向传播,直到输出。在Mixing module中,Flow-Mixup将模型的前端部分限制为学习非线性函数,其余部分为线性函数。
在处理多标签医学图像时,非线性函数提取异常特征时,模型的线性函数会将异常特征投影到标签空间。当非线性部分的输出被输入到线性部分时,非线性部分的约束被保持,而线性部分要求其输入位于线性可分空间。不同于Mixup,特殊的Mixing module引入了额外的Flow维度,从而允许在一个模型中同时使用几个Mixing module。
3.2 Mixing Module
在深度学习模型中,图像的张量一般有4个维度:batch维度、channel维度、width和height维度。本文提出的Flow-Mixup引入了一个新的维数,称为Flow维数。如图1左图所示,假设原特征z在经过Mixing Module处理前的Flow维数为1,则Mixing Module的输出的Flow维数为2。Flow大小通过特征连接操作增加。在特征馈送到Mixing Module后,第1步是复制这些特征。然后,对特征拷贝进行Mixing处理,然后沿着Flow维串接成原始特征。Mixing Module的正向过程定义为:
其中,Mixing操作将一个特征副本变换为2个小副本,,并对其采用标准Mixing,如图1右侧所示:
和是通过对进行随机索引Shuffle得到的。p从beta分布pB(α,α)中随机抽样,是控制Mixing度的超参数。表示流向级联,导致Flow size增大。根据式(2),将特征z转换为具有双流量大小的。由于在前向传播过程中Flow大小增加了1倍,Mixing Module在后向传播过程中应将梯度减半,以保持梯度的大小。混合模的反向传播定义为:
其中表示原始特征的梯度,表示混合特征的梯度(如图1所示)。这样,Mixing Module可以同时应用于几种隐藏状态,同时保留原始特征,如图2(b)所示。
注意,正则化方法不能完全限制后续层为线性函数,因此应用几个Mixing Module有助于加强线性约束。在实现中,如果隐藏状态是最后一种状态(图2(b))或只有一种状态(图2(a))应用Mixing Module,则可选择计算原始特征向输出层的前向传播。如果原始特性没有前向传播,Mixing Module则退化为普通的Mixup操作:
在正向传播计算和在反向传播。