RefineNet

论文地址(2016):RefineNet: Multi-Path Refinement Networks with Identity Mappings for High-Resolution Semantic Segmentation

对于高分辨率的图像分割问题,基于编解码结构的分割网络虽然有效,但因为卷积和池化下采样的存在,特征图在变小的过程会逐渐损失一些细粒度的信息,非常不利于高分辨率图像的像素稠密预测。针对这个问题,此前的各项研究归纳而言提出了如下三点处理方法:

(1)类似于FCN和UNet,直接使用转置卷积上采样来恢复图像像素,但转置卷积对于下采样过程中丢失的低层信息的恢复能力有限。

(2)使用空洞卷积,通过给常规卷积中插入空洞的方式来增大卷积感受野,并且没有缩小图像尺寸,但这种方式计算开销增大,模型运行效率降低,并且空洞卷积作为一种较为粗糙的子采样(sub-sampling),也会存在图像重要信息损失的问题。

(3)使用跳跃连接。类似于UNet中编解码器间的跳跃连接,直接将编码器每一层的特征图连接到解码器上采样结果上,能够对解码图像进行信息补充。

相关研究认为,对于编解码结构而言,所有层次的特征对语义分割都是有帮助的。高层次的特征用于识别图像中的语义信息,低层次的特征则有助于恢复高分辨率图像的边界细节。但如何有效利用中间层次的信息值得进一步探索,前述充分使用跳跃连接的方法或许会更加有效。基于此,研究人员提出了一种针对高分辨率图像语义分割的多层次特征精细化网络:RefineNet。提出RefineNet的论文为RefineNet: Multi-Path Refinement Networks for High-Resolution Semantic Segmentation,该网络基于ResNet结构和跳跃连接,使用多路径的精细化网络结构来获取最佳的分割结果,是高分辨率图像分割的经典网络。

RefineNet简要结构如图5-9所示。RefineNet总体上仍然是编解码结构,编码器部分根据预训练的ResNet网络划分了4个卷积块,与之对应的是级联了4个RefineNet单元到解码器部分。每个编码器ResNet卷积块的输出特征图都会被连接到对应的RefineNet单元,如图中的ResNet-4连接到RefineNet-4单元,到了RefineNet-3单元,除了接收来自ResNet-3的输出外,还需要接收RefineNet-4单元的输出,对于RefineNet-3单元而言就构成了两路径的输入。这样层层向上级联,就构成了多路径的RefineNet。其中编码器中每个特征图到解码器RefineNet单元的连接也叫长程残差连接(long-range residual connections)。

上图仅给出了包含了编码器在内的RefineNet简要结构,而RefineNet单元的具体结构如下图所示。一个RefineNet单元由残差卷积单元(Residual convolution unit,RCU)、多分辨率融合(Multi-resolution Fusion)和链式残差池化(Chained Residual Pooling,CRP)组成。RCU较为简单,就是常规的ResNet结构,每一个输入路径都会经过两次RCU操作后再输出到下一个单元。RCU的跳跃连接在RefineNet中也被称为短程残差连接(short-range residual connections)。紧接着是一个多分辨率特征图融合层,将上一层RCU输出的多路径特征图经过一个33的卷积和上采样操作后进行加总,得到合并后的特征图。最后是一个CRP单元,这也是RefineNet的特色结构,通过3个链式的池化和卷积残差组合来捕捉大图像区域的背景上下文信息。将CRP之后得到特征图再经过一次RCU即可到最终的分割输出。

作为一种针对高分辨率图像的精细化分割网络,RefineNet的结构设计无疑是成功的,当时在多个公开数据集上均取得了SOTA性能表现。这种多路径的精细化网络能够通过迭代精炼的方式将粗糙的语义特征精炼为细粒度的语义特征。其次,基于长短程的残差连接能够使得模型进行端到端的训练,推理时也非常高效。最后,链式残差池化也使得网络能够更好的捕捉大图像的上下文信息。

RefineNet代码完整实现可参考:

https://github.com/DrSleep/refinenet-pytorch

其中关于RCU和CRP模块的实现如下代码所示。

class RCUBlock(nn.Module):
    
    def __init__(self, in_planes, out_planes, n_blocks, n_stages):
        super(RCUBlock, self).__init__()
        for i in range(n_blocks):
            for j in range(n_stages):
                setattr(self, '{}{}'.format(i + 1, stages_suffixes[j]),
                        conv3x3(in_planes if (i == 0) and (j == 0) else out_planes,
                                out_planes, stride=1,
                                bias=(j == 0)))
        self.stride = 1
        self.n_blocks = n_blocks
        self.n_stages = n_stages
    
    def forward(self, x):
        for i in range(self.n_blocks):
            residual = x
            for j in range(self.n_stages):
                x = F.relu(x)
                x = getattr(self, '{}{}'.format(i + 1, stages_suffixes[j]))(x)
            x += residual
        return 
        
class CRPBlock(nn.Module):

    def __init__(self, in_planes, out_planes, n_stages):
        super(CRPBlock, self).__init__()
        for i in range(n_stages):
            setattr(self, '{}_{}'.format(i + 1, 'outvar_dimred'),
                    conv3x3(in_planes if (i == 0) else out_planes,
                            out_planes, stride=1,
                            bias=False))
        self.stride = 1
        self.n_stages = n_stages
        self.maxpool = nn.MaxPool2d(kernel_size=5, stride=1, padding=2)

    def forward(self, x):
        top = x
        for i in range(self.n_stages):
            top = self.maxpool(top)
            top = getattr(self, '{}_{}'.format(i + 1, 'outvar_dimred'))(top)
            x = top + x
        return x

预测效果:

关于NLP多标签文本分类的一些思路–(待更新)

作为刚入门的小白,有必要去记录一些NLP分类任务的小trick,感觉对于涨点提分十分有用。这个文章后面有新的想法会持续更新

华为有一个NLP关于医学电子病历的疾病多标签分类比赛,因为之前比较少去做NLP方向的东西,仅仅是学习过相关rnn、transformer、bert论文呢,所以,参赛纯粹是为了了解了解NLP方向,好在nlp做文本分类算是比较简单的下游任务,但在参赛过程中,会发现,其实对于文本分类来说,基本的bert-base的效果不是很好,但其实感觉不是出在模型架构方面,对于简单的分类任务,一个12层的bert应该适足以胜任了,因此将注意力不要过多的放在模型结构上。

任务说明

本赛题是利用病人电子病历文本信息推断出其可能患有疾病的疾病诊断任务。电子病历文本信息主要包括病人的性别、年龄、主诉、现病史、既往史、体格检查和辅助检查。标签信息为病人的出院诊断疾病。本赛题任务需要根据病人的电子病历文本信息推断出病人所患有的全部疾病。(注:病人的出院诊断疾病并不是单一的) 

模型输出格式:

{ “ZY000001”: [“高血压”, “肺气肿”, “先天性心脏病”]}

评分标准

本赛题采用macro F1作为评价指标。评价指标计算公式如下:

对于每一个预测的疾病有真阳性(True Positive,TP),假阳性(False Positive,FP),假阴性(False Negative),真阴性(True Negative),n表示n种疾病。

这个得分最高在 0.83左右。我也试了几次,但到0.57就没再动过了…..,后面准备去尝试下下面的方法,看看有啥效果吗。

思路:

在github中找到一个分类会议任务的比赛ppt模型讲解:

https://github.com/TJBioMedNLP/chip2019task3

废话说完,来点或许能提分的干货:

数据方面:

1、数据清洗(很多脏数据)、数据增强

说实话,感觉这个比赛的要点就是数据处理,想提分就看你的数据的好坏,现在是真的意识到数据处理对于一个模型的影响之大了,后面要着重关注下这方面了。

本次提供的训练集中出现了一些不需要诊断的疾病:睾丸鞘膜积液、宫颈炎性疾病、口腔粘膜溃疡、头部外伤、急性阴道炎、女性盆腔炎、急性气管炎,需要自己去将该类数据清洗,另外,通过数据统计分析,可以获得训练集中各个label的数量严重不平衡,如何处理也是一个问题,是否可以通过数据集增强,提高某些类别的测试数据。

另外 性别、年龄、主诉、现病史、既往史、体格检查和辅助检查 等长度会超出模型的最大长度,如何解决、最大化利用上述信息也是一个问题。我做过对这些数据做过分析,对于 性别、年龄、主诉、现病史、既往史、体格检查和辅助检查 等 统计过平均长度、最大最小长度,从几十到几百不等。另外,去看下数据集就可以看到,有大量的标点符号和短语。另外,据说emr_id这个信息也是一个重要的信息???我一脸震惊。此外年龄和性别也会影响。

其他:

1、如果训练时使用文本长度为n,测试使用比n长一些的长度,可以涨点分

2、模型预训练会提分(或者找相关领域预训练模型)

这里我找了两个预训练模型:

https://huggingface.co/trueto/medbert-base-chinese

https://huggingface.co/nghuyong/ernie-health-zh

3、增大训练时输入编码长度(文本序列长度),当然,需要显卡的性能支持

all_tokens = self.tokenizer.encode_plus(content, max_length=pad_size, padding=”max_length”, truncation=True)

可以提高max_length的大小,但是比较吃显卡。

4、交叉验证

交叉验证经常用于给定的数据集训练、评估和最终选择机器学习模型,因为它有助于评估模型的结果在实践中如何推广到独立的数据集,最重要的是,交叉验证已经被证明产生比其他方法更低的偏差的模型。重复的k折交叉验证,主要是会重复进行n次的k折交叉验证,这样会产生n次结果,一般通过平均方法或者(投票规则)得到最后的结果

 第一种是简单交叉验证,所谓的简单,是和其他交叉验证方法相对而言的。首先,我们随机的将样本数据分为两部分(比如: 70%的训练集,30%的测试集),然后用训练集来训练模型,在测试集上验证模型及参数。接着,我们再1把样本打乱,重新选择训练集和测试集,继续训练数据和检验模型。最后我们选择损失函数评估最优的模型和参数。

选择分层k-折交叉验证:

分层采样就是在每一份子集中都保持原始数据集的类别比例,保证采样数据跟原始数据的类别分布保持一致,该方法在有效的平衡方差和偏差。当针对不平衡数据时,使用随机的K-fold交叉验证,可能出现在子集中叫少的类别的分布与原始类别分布不一致。因此,针对不平衡数据往往使用stratified k-fold交叉验证。

当训练数据集不能代表整个数据集分布是,这时候使用stratified k折交叉验证可能不是好的方法,而可能比较适合使用简单的重复随机k折交叉验证。

  • 1.把整个数据集随机划分成k份
  • 2.用其中k-1份训练模型,然后用第k份验证模型
  • 3.记录每个预测结果获得的误差
  • 4.重复这个过程,知道每份数据都做过验证集
  • 5.记录下的k个误差的平均值,被称为交叉验证误差。可以被用做衡量模型性能的标准
>>> from sklearn.model_selection import StratifiedKFold
>>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]])
>>> y = np.array([0, 0, 1, 1])
>>> skf = StratifiedKFold(n_splits=2)
>>> skf.get_n_splits(X, y)
2
>>> print(skf)  
StratifiedKFold(n_splits=2, random_state=None, shuffle=False)
>>> for train_index, test_index in skf.split(X, y):
...    print("TRAIN:", train_index, "TEST:", test_index)
...    X_train, X_test = X[train_index], X[test_index]
...    y_train, y_test = y[train_index], y[test_index]
TRAIN: [1 3] TEST: [0 2]
TRAIN: [0 2] TEST: [1 3]

具体来说:

以k-fold CV为例:仍然是把原始数据集分成训练集和测试集,但是训练模型的时候不使用测试集。最常见的一个叫做k_fold CV

  • 具体来说就是把训练集平分为k个fold,其中每个fold依次作为测试集、余下的作为训练集,进行k次训练,得到共计k组参数。取k组参数的均值作为模型的最终参数
  1. 优点:充分压榨了数据集的价值。在样本集不够大的情况下尤其珍贵。
  2. 缺点:运算起来花时间。

K折交叉验证训练单个模型:

通过对 k 个不同分组训练的结果进行平均来减少方差,因此模型的性能对数据的划分就不那么敏感,经过多次划分数据集,大大降低了结果的偶然性,从而提高了模型的准确性。具体做法如下:

  • step1:不重复抽样将原始数据随机分为 k 份。
  • step2:每一次挑选其中 1 份作为验证集,剩余 k-1 份作为训练集用于模型训练。一共训练k个模型。
  • step3:在每个训练集上训练后得到一个模型,用这个模型在测试集上测试,计算并保存模型的评估指标,
  • step4:计算 k 组测试结果的平均值作为模型最终在测试集上的预测值,求k 个模型评估指标的平均值,并作为当前 k 折交叉验证下模型的性能指标。

6、模型融合

模型融合:通过融合多个不同的模型,可能提升机器学习的性能。这一方法在各种机器学习比赛中广泛应用, 也是在比赛的攻坚时刻冲刺Top的关键。而融合模型往往又可以从模型结果,模型自身,样本集等不同的角度进行融合。即多个模型的组合可以改善整体的表现。集成模型是一种能在各种的机器学习任务上提高准确率的强有力技术。

模型融合是比赛后期一个重要的环节,大体来说有如下的类型方式:

1. 简单加权融合:

  • 回归(分类概率):算术平均融合(Arithmetic mean),几何平均融合(Geometric mean);
  • 分类:投票(Voting);
  • 综合:排序融合(Rank averaging),log融合。

2. stacking/blending:

  • 构建多层模型,并利用预测结果再拟合预测。

3. boosting/bagging:

  • 多树的提升方法,在xgboost,Adaboost,GBDT中已经用到。

平均法(Averaging)

基本思想:对于回归问题,一个简单直接的思路是取平均。稍稍改进的方法是进行加权平均。权值可以用排序的方法确定,举个例子,比如A、B、C三种基本模型,模型效果进行排名,假设排名分别是1,2,3,那么给这三个模型赋予的权值分别是3/6、2/6、1/6。

平均法或加权平均法看似简单,其实后面的高级算法也可以说是基于此而产生的,Bagging或者Boosting都是一种把许多弱分类器这样融合成强分类器的思想。

简单算术平均法:Averaging方法就多个模型预测的结果进行平均。这种方法既可以用于回归问题,也可以用于对分类问题的概率进行平均。

加权算术平均法:这种方法是平均法的扩展。考虑不同模型的能力不同,对最终结果的贡献也有差异,需要用权重来表征不同模型的重要性importance。

投票法(voting)

基本思想:假设对于一个二分类问题,有3个基础模型,现在我们可以在这些基学习器的基础上得到一个投票的分类器,把票数最多的类作为我们要预测的类别。

绝对多数投票法:最终结果必须在投票中占一半以上。

相对多数投票法:最终结果在投票中票数最多。

加权投票法:每个弱学习器的分类票数乘以权重,并将各个类别的加权票数求和,最大值对应的类别即最终类别。

硬投票:对多个模型直接进行投票,不区分模型结果的相对重要度,最终投票数最多的类为最终被预测的类。

软投票:增加了设置权重的功能,可以为不同模型设置不同权重,进而区别模型不同的重要度。

堆叠法(Stacking)

基本思想

stacking 就是当用初始训练数据学习出若干个基学习器后,将这几个学习器的预测结果作为新的训练集,来学习一个新的学习器。对不同模型预测的结果再进行建模。

将个体学习器结合在一起的时候使用的方法叫做结合策略。对于分类问题,我们可以使用投票法来选择输出最多的类。对于回归问题,我们可以将分类器输出的结果求平均值。

上面说的投票法和平均法都是很有效的结合策略,还有一种结合策略是使用另外一个机器学习算法来将个体机器学习器的结果结合在一起,这个方法就是Stacking。在stacking方法中,我们把个体学习器叫做初级学习器,用于结合的学习器叫做次级学习器或元学习器(metalearner),次级学习器用于训练的数据叫做次级训练集。次级训练集是在训练集上用初级学习器得到的。

  • step1:训练T个初级学习器,要使用交叉验证的方法在Train Set上面训练(因为第二阶段建立元学习器的数据是初级学习器输出的,如果初级学习器的泛化能力低下,元学习器也会过拟合)
  • step2:T个初级学习器在Train Set上输出的预测值,作为元学习器的训练数据D,有T个初级学习器,D中就有T个特征。D的label和训练初级学习器时的label一致。
  • step3:T个初级学习器在Test Set上输出的预测值,作为训练元学习器时的测试集,同样也是有T个模型就有T个特征。
  • step4:训练元学习器,元学习器训练集D的label和训练初级学习器时的label一致。

混合法(Blending)

基本思想:Blending采用了和stacking同样的方法,不过只从训练集中选择一个fold的结果,再和原始特征进行concat作为元学习器meta learner的特征,测试集上进行同样的操作。

把原始的训练集先分成两部分,比如70%的数据作为新的训练集,剩下30%的数据作为测试集。

  • 第一层,我们在这70%的数据上训练多个模型,然后去预测那30%数据的label,同时也预测test集的label。
  • 在第二层,我们就直接用这30%数据在第一层预测的结果做为新特征继续训练,然后用test集第一层预测的label做特征,用第二层训练的模型做进一步预测。

Blending训练过程:

  1. 整个训练集划分成训练集training sets和验证集validation sets两个部分;
  2. 在training sets上训练模型;
  3. 在validation sets和test sets上得到预测结果;
  4. 将validation sets的原始特征和不同基模型base model预测得到的结果作为新的元学习器meta learner的输入,进行训练 ;
  5. 使用训练好的模型meta learner在test sets以及在base model上的预测结果上进行预测,得到最终结果。

Stacking与Blending的对比:

优点在于:

  • blending比stacking简单,因为不用进行k次的交叉验证来获得stacker feature
  • blending避开了一个信息泄露问题:generlizers和stacker使用了不一样的数据集

缺点在于:

  • blending使用了很少的数据(第二阶段的blender只使用training set10%的量)
  • blender可能会过拟合
  • stacking使用多次的交叉验证会比较稳健

Bagging

基本思想:Bagging基于bootstrap(自采样),也就是有放回的采样。训练子集的大小和原始数据集的大小相同。Bagging的技术使用子集来了解整个样本集的分布,通过bagging采样的子集的大小要小于原始集合。

  • 采用bootstrap的方法基于原始数据集产生大量的子集
  • 基于这些子集训练弱模型base model
  • 模型是并行训练并且相互独立的
  • 最终的预测结果取决于多个模型的预测结果

Bagging是一种并行式的集成学习方法,即基学习器的训练之间没有前后顺序可以同时进行,Bagging使用“有放回”采样的方式选取训练集,对于包含m个样本的训练集,进行m次有放回的随机采样操作,从而得到m个样本的采样集,这样训练集中有接近36.8%的样本没有被采到。按照相同的方式重复进行,我们就可以采集到T个包含m个样本的数据集,从而训练出T个基学习器,最终对这T个基学习器的输出进行结合。

Boosting

基础思想:Boosting是一种串行的工作机制,即个体学习器的训练存在依赖关系,必须一步一步序列化进行。Boosting是一个序列化的过程,后续模型会矫正之前模型的预测结果。也就是说,之后的模型依赖于之前的模型。

其基本思想是:增加前一个基学习器在训练训练过程中预测错误样本的权重,使得后续基学习器更加关注这些打标错误的训练样本,尽可能纠正这些错误,一直向下串行直至产生需要的T个基学习器,Boosting最终对这T个学习器进行加权结合,产生学习器委员会。

Boosting训练过程:

  • 基于原始数据集构造子集
  • 初始的时候,所有的数据点都给相同的权重
  • 基于这个子集创建一个基模型
  • 使用这个模型在整个数据集上进行预测
  • 基于真实值和预测值计算误差
  • 被预测错的观测值会赋予更大的权重
  • 再构造一个模型基于之前预测的误差进行预测,这个模型会尝试矫正之前的模型
  • 类似地,构造多个模型,每一个都会矫正之前的误差
  • 最终的模型(strong learner)是所有弱学习器的加权融合

7、损失函数,注意不同类别的权重(使用F1_loss、Hamming Loss、数据类别分布不均,如何解决长尾分布(加权损失、先验权重))

相关论文: sigmoidF1: A Smooth F1 Score Surrogate Loss for Multilabel Classification

平时我们在做多标签分类,或者是多分类的时候,经常使用的loss函数一般是binary_crossentropy(也就是log_loss)或者是categorical_crossentropy,不过交叉熵其实还是有点问题的,在多标签分类的问题里,交叉熵并非是最合理的损失函数。在多标签分类的问题中,我们最终评价往往会选择F1分数作为评价指标,那么是否能直接将F1-score制作成为一个loss函数呢?当然是可以的。

在多分类/多标签分类中,F1-score有两种衍生格式,分别是micro-F1和macro-F1。是两种不同的计算方式。

micro-F1是先计算先拿总体样本来计算出TP、TN、FP、FN的值,再使用这些值计算出percision和recall,再来计算出F1值。

macro-F1则是先对每一种分类,视作二分类,计算其F1值,最后再对每一个分类进行简单平均。

简单的记的话其实是这样的,micro(微观)与macro(宏观)的含义其实是,micro-F1是在样本的等级上做平均,是最小颗粒度上的平均了,所以是微观。macro-F1是在每一个分类的层面上做平均,每一个分类都包含很多样本,所以相对是宏观。

作为loss函数的F1

F1-score改造成loss函数相对较为简单,F1是范围在0~1之间的指标,越大代表性能越好,在作为loss时只需要取(1-F1)即可。

一下是keras中的实现:

(这里的K就是keras的后端,一般来说就是tensorflow)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
K = keras.backend
def f1_loss(y_true, y_pred):
#计算tp、tn、fp、fn
tp = K.sum(K.cast(y_true*y_pred, ‘float’), axis=0)
tn = K.sum(K.cast((1-y_true)*(1-y_pred), ‘float’), axis=0)
fp = K.sum(K.cast((1-y_true)*y_pred, ‘float’), axis=0)
fn = K.sum(K.cast(y_true*(1-y_pred), ‘float’), axis=0)

#percision与recall,这里的K.epsilon代表一个小正数,用来避免分母为零
p = tp / (tp + fp + K.epsilon())
r = tp / (tp + fn + K.epsilon())

#计算f1
f1 = 2*p*r / (p+r+K.epsilon())
f1 = tf.where(tf.is_nan(f1), tf.zeros_like(f1), f1)#其实就是把nan换成0
return 1 – K.mean(f1)

这个函数可以直接在keras模型编译时使用,如下:

1
2
3
4
# 类似这样
model.compile(optimizer=tf.train.AdamOptimizer(0.003),
loss=f1_loss,
metrics=[‘acc’,’mae’])
 
def f1_loss(predict, target):
    predict = torch.sigmoid(predict)
    predict = torch.clamp(predict * (1-target), min=0.01) + predict * target
    tp = predict * target
    tp = tp.sum(dim=0)
    precision = tp / (predict.sum(dim=0) + 1e-8)
    recall = tp / (target.sum(dim=0) + 1e-8)
    f1 = 2 * (precision * recall / (precision + recall + 1e-8))
    return 1 - f1.mean()

8、考虑将多分类 变成多个二分类任务

9、除了bert模型,还可以尝试Performer、ernie-health

Performer 是ICLR 2021的新paper,在处理长序列预测方面有非常不错的结果,速度快,内存小,在LRA(long range arena 一个统一的benchmark)上综合得分不错。

论文:https://arxiv.org/pdf/2009.14794.pdf

ernie-health :Building Chinese Biomedical Language Models via Multi-Level Text Discrimination
中文题目:基于多层次文本辨析构建中文生物医学语言模型
论文地址:https://arxiv.org/pdf/2110.07244.pdf
领域:自然语言处理,生物医学
发表时间:2021
作者:Quan Wang等,百度
模型下载:https://huggingface.co/nghuyong/ernie-health-zh
模型介绍:https://github.com/PaddlePaddle/Research/tree/master/KG/eHealth
模型代码:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/ernie-health

10、NLP中的对抗训练 (添加的扰动是微小)

  1. 提高模型应对恶意对抗样本时的鲁棒性;
  2. 作为一种regularization,减少overfitting,提高泛化能力。

对抗训练其实是“对抗”家族中防御的一种方式,其基本的原理呢,就是通过添加扰动构造一些对抗样本,放给模型去训练,以攻为守,提高模型在遇到对抗样本时的鲁棒性,同时一定程度也能提高模型的表现和泛化能力。

那么,什么样的样本才是好的对抗样本呢?对抗样本一般需要具有两个特点:

  1. 相对于原始输入,所添加的扰动是微小的;
  2. 能使模型犯错。

NLP中的两种对抗训练 + PyTorch实现

a. Fast Gradient Method(FGM)

上面我们提到,Goodfellow在15年的ICLR [7] 中提出了Fast Gradient Sign Method(FGSM),随后,在17年的ICLR [9]中,Goodfellow对FGSM中计算扰动的部分做了一点简单的修改。假设输入的文本序列的embedding vectors [v1,v2,…,vT] 为 x ,embedding的扰动为:

实际上就是取消了符号函数,用二范式做了一个scale,需要注意的是:这里的norm计算的是,每个样本的输入序列中出现过的词组成的矩阵的梯度norm。原作者提供了一个TensorFlow的实现 [10],在他的实现中,公式里的 x 是embedding后的中间结果(batch_size, timesteps, hidden_dim),对其梯度 g 的后面两维计算norm,得到的是一个(batch_size, 1, 1)的向量 ||g||2 。为了实现插件式的调用,笔者将一个batch抽象成一个样本,一个batch统一用一个norm,由于本来norm也只是一个scale的作用,影响不大。实现如下:

import torch
class FGM():
    def __init__(self, model):
        self.model = model
        self.backup = {}

    def attack(self, epsilon=1., emb_name='emb.'):
        # emb_name这个参数要换成你模型中embedding的参数名
        for name, param in self.model.named_parameters():
            if param.requires_grad and emb_name in name:
                self.backup[name] = param.data.clone()
                norm = torch.norm(param.grad)
                if norm != 0 and not torch.isnan(norm):
                    r_at = epsilon * param.grad / norm
                    param.data.add_(r_at)

    def restore(self, emb_name='emb.'):
        # emb_name这个参数要换成你模型中embedding的参数名
        for name, param in self.model.named_parameters():
            if param.requires_grad and emb_name in name: 
                assert name in self.backup
                param.data = self.backup[name]
        self.backup = {}

需要使用对抗训练的时候,只需要添加五行代码:

# 初始化
fgm = FGM(model)
for batch_input, batch_label in data:
    # 正常训练
    loss = model(batch_input, batch_label)
    loss.backward() # 反向传播,得到正常的grad
    # 对抗训练
    fgm.attack() # 在embedding上添加对抗扰动
    loss_adv = model(batch_input, batch_label)
    loss_adv.backward() # 反向传播,并在正常的grad基础上,累加对抗训练的梯度
    fgm.restore() # 恢复embedding参数
    # 梯度下降,更新参数
    optimizer.step()
    model.zero_grad()

PyTorch为了节约内存,在backward的时候并不保存中间变量的梯度。因此,如果需要完全照搬原作的实现,需要用register_hook接口[11]将embedding后的中间变量的梯度保存成全局变量,norm后面两维,计算出扰动后,在对抗训练forward时传入扰动,累加到embedding后的中间变量上,得到新的loss,再进行梯度下降。

b. Projected Gradient Descent(PGD)

内部max的过程,本质上是一个非凹的约束优化问题,FGM解决的思路其实就是梯度上升,那么FGM简单粗暴的“一步到位”,是不是有可能并不能走到约束内的最优点呢?当然是有可能的。于是,一个很intuitive的改进诞生了:Madry在18年的ICLR中[8],提出了用Projected Gradient Descent(PGD)的方法,简单的说,就是“小步走,多走几步”,如果走出了扰动半径为 ϵ 的空间,就映射回“球面”上,以保证扰动不要过大:

import torch
class PGD():
    def __init__(self, model):
        self.model = model
        self.emb_backup = {}
        self.grad_backup = {}

    def attack(self, epsilon=1., alpha=0.3, emb_name='emb.', is_first_attack=False):
        # emb_name这个参数要换成你模型中embedding的参数名
        for name, param in self.model.named_parameters():
            if param.requires_grad and emb_name in name:
                if is_first_attack:
                    self.emb_backup[name] = param.data.clone()
                norm = torch.norm(param.grad)
                if norm != 0 and not torch.isnan(norm):
                    r_at = alpha * param.grad / norm
                    param.data.add_(r_at)
                    param.data = self.project(name, param.data, epsilon)

    def restore(self, emb_name='emb.'):
        # emb_name这个参数要换成你模型中embedding的参数名
        for name, param in self.model.named_parameters():
            if param.requires_grad and emb_name in name: 
                assert name in self.emb_backup
                param.data = self.emb_backup[name]
        self.emb_backup = {}

    def project(self, param_name, param_data, epsilon):
        r = param_data - self.emb_backup[param_name]
        if torch.norm(r) > epsilon:
            r = epsilon * r / torch.norm(r)
        return self.emb_backup[param_name] + r

    def backup_grad(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                self.grad_backup[name] = param.grad.clone()

    def restore_grad(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                param.grad = self.grad_backup[name]

使用的时候,要麻烦一点:

pgd = PGD(model)
K = 3
for batch_input, batch_label in data:
    # 正常训练
    loss = model(batch_input, batch_label)
    loss.backward() # 反向传播,得到正常的grad
    pgd.backup_grad()
    # 对抗训练
    for t in range(K):
        pgd.attack(is_first_attack=(t==0)) # 在embedding上添加对抗扰动, first attack时备份param.data
        if t != K-1:
            model.zero_grad()
        else:
            pgd.restore_grad()
        loss_adv = model(batch_input, batch_label)
        loss_adv.backward() # 反向传播,并在正常的grad基础上,累加对抗训练的梯度
    pgd.restore() # 恢复embedding参数
    # 梯度下降,更新参数
    optimizer.step()
    model.zero_grad()

实验对照

为了说明对抗训练的作用,笔者选了四个GLUE中的任务进行了对照试验。实验代码是用的Huggingface的transfomers/examples/run_glue.py [12],超参都是默认的,对抗训练用的也是相同的超参。

除了监督训练,对抗训练还可以用在半监督任务中,尤其对于NLP任务来说,很多时候输入的无监督文本多的很,但是很难大规模地进行标注,Distributional Smoothing with Virtual Adversarial Training. https://arxiv.org/abs/1507.00677 提到的 Virtual Adversarial Training进行半监督训练。

11、Pseudo Labeling(伪标签)提高模型的分类效果

简而言之,Pseudo Labeling将测试集中判断结果正确的置信度高的样本加入到训练集中,从而模拟一部分人类对新对象进行判断推演的过程。效果比不上人脑那么好,但是在监督学习问题中,Pseudo Labeling几乎是万金油,几乎能够让你模型各个方面的表现都得到提升。

  1. 使用原始训练集训练并建立模型
  2. 使用训练好的模型对测试集进行分类
  3. 将预测正确置信度高的样本加入到训练集中
  4. 使用结合了部分测试集样本的新训练集再次训练模型
  5. 使用新模型再次进行预测

总之:提分点很多,但能否有效以及能否实现又是另一个事情了,毕竟有时候是否有效也取决于数据集,毕竟缘分,妙不可言~,后续我会抽时间将上面的tricks都尝试尝试。

区域卷积神经⽹络(R-CNN)系列

来自:动手学深度学习

视频讲解:Faster R-CNN原理 、源码解析

1、R-CNN

R-CNN ⾸先从输⼊图像中选取若⼲(例如2000个)提议区域(如锚框也是⼀种选取方法),并标注它们的类别和边界框(如偏移量)。[Girshick et al., 2014] 然后,⽤卷积神经⽹络对每个提议区域进⾏前向计算以抽取其特征。接下来,我们⽤每个提议区域的特征来预测类别和边界框。

具体来说,R-CNN包括以下四个步骤:

  1. 对输⼊图像使⽤ 选择性搜索来选取多个⾼质量的提议区域 [Uijlings et al., 2013] 。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和⼤小。每个提议区域都将被标注类别和真实边界框。
  2. 选择⼀个预训练的卷积神经⽹络,并将其在输出层之前截断。将每个提议区域变形为⽹络需要的输⼊尺⼨,并通过前向计算输出抽取的提议区域特征。
  3. 将每个提议区域的特征连同其标注的类别作为⼀个样本。训练多个⽀持向量机对⽬标分类,其中每个⽀持向量机⽤来判断样本是否属于某⼀个类别。
  4. 将每个提议区域的特征连同其标注的边界框作为⼀个样本,训练线性回归模型来预测真实边界框。
  • 尽管 R-CNN 模型通过预训练的卷积神经⽹络有效地抽取了图像特征,但它的速度很慢。想象⼀下,我们可能从⼀张图像中选出上千个提议区域,这需要上千次的卷积神经⽹络的前向计算来执⾏⽬标检测。这种庞⼤的计算量使得 R-CNN 在现实世界中难以被⼴泛应⽤。

2、Fast R-CNN

R-CNN 的主要性能瓶颈在于,对每个提议区域,卷积神经⽹络的前向计算是独⽴的,而没有共享计算。由于这些区域通常有重叠,独⽴的特征抽取会导致重复的计算。Fast R-CNN [Girshick, 2015] 对 R-CNN 的主要改进之⼀,是仅在整张图象上执⾏卷积神经⽹络的前向计算。

它的主要计算如下:

  1. 与 R-CNN 相⽐,Fast R-CNN ⽤来提取特征的卷积神经⽹络的输⼊是整个图像,而不是各个提议区域。此外,这个⽹络通常会参与训练。设输⼊为⼀张图像,将卷积神经⽹络的输出的形状记为 1×c×h1×w1。
  2. 假设选择性搜索⽣成了n个提议区域。这些形状各异的提议区域在卷积神经⽹络的输出上分别标出了形状各异的兴趣区域。然后,这些感兴趣的区域需要进⼀步抽取出形状相同的特征(⽐如指定⾼度h2和宽度w2),以便于连结后输出。为了实现这⼀⽬标,Fast R-CNN 引⼊了 兴趣区域 (RoI) 池化层:将卷积神经⽹络的输出和提议区域作为输⼊,输出连结后的各个提议区域抽取的特征,形状为n × c × h2 × w2。
  3. 通过全连接层将输出形状变换为n × d,其中超参数d取决于模型设计。
  4. 预测n个提议区域中每个区域的类别和边界框。更具体地说,在预测类别和边界框时,将全连接层的输出分别转换为形状为 n × q(q 是类别的数量)的输出和形状为 n × 4 的输出。其中预测类别时使⽤softmax 回归。

在Fast R-CNN 中提出的兴趣区域汇聚层与 6.5节 中介绍的汇聚层有所不同。在汇聚层中,我们通过设置池化窗口、填充和步幅的⼤小来间接控制输出形状。而兴趣区域汇聚层对每个区域的输出形状是可以直接指定的。例如,指定每个区域输出的⾼和宽分别为 h2 和 w2。对于任何形状为 h × w 的兴趣区域窗口,该窗口将被划分为 h2 × w2 ⼦窗口⽹格,其中每个⼦窗口的⼤小约为(h/h2) × (w/w2)。在实践中,任何⼦窗口的⾼度和宽度都应向上取整,其中的最⼤元素作为该⼦窗口的输出。因此,兴趣区域汇聚层可从形状各异的兴趣区域中均抽取出形状相同的特征。

3、Faster R-CNN

为了较精确地检测⽬标结果,Fast R-CNN 模型通常需要在选择性搜索中⽣成⼤量的提议区域。Faster R-CNN [Ren et al., 2015] 提出将选择性搜索替换为 区域提议⽹络(region proposal network),从而减少提议区域的⽣成数量,并保证⽬标检测的精度。

与Fast R-CNN 相⽐,Faster R-CNN 只将⽣成提议区域的⽅法从选择性
搜索改为了区域提议⽹络,模型的其余部分保持不变。具体来说,区域提议⽹络的计算步骤如下:

  1. 使⽤填充为1的 3 × 3 的卷积层变换卷积神经⽹络的输出,并将输出通道数记为 c。这样,卷积神经⽹络为图像抽取的特征图中的每个单元均得到⼀个⻓度为 c 的新特征。
  2. 以特征图的每个像素为中⼼,⽣成多个不同⼤小和宽⾼⽐的锚框并标注它们。
  3. 使⽤锚框中⼼单元⻓度为 c 的特征,分别预测该锚框的⼆元类别(含⽬标还是背景)和边界框。
  4. 使⽤⾮极⼤值抑制,从预测类别为⽬标的预测边界框中移除相似的结果。最终输出的预测边界框即是兴趣区域汇聚层所需的提议区域。

值得⼀提的是,区域提议⽹络作为 Faster R-CNN 模型的⼀部分,是和整个模型⼀起训练得到的。换句话说,Faster R-CNN 的⽬标函数不仅包括⽬标检测中的类别和边界框预测,还包括区域提议⽹络中锚框的⼆元类别和边界框预测。作为端到端训练的结果,区域提议⽹络能够学习到如何⽣成⾼质量的提议区域,从而在减少了从数据中学习的提议区域的数量的情况下,仍保持⽬标检测的精度

4、Mask R-CNN

如果在训练集中还标注了每个⽬标在图像上的像素级位置,那么 Mask R-CNN [He et al., 2017] 能够有效地利⽤这些详尽的标注信息进⼀步提升⽬标检测的精度。

如 图13.8.5 所⽰,Mask R-CNN 是基于 Faster R-CNN 修改而来的。具体来说,Mask R-CNN 将兴趣区域汇聚层替换为了 兴趣区域 (RoI) 对⻬层使⽤ 双线性插值(bilinear interpolation)来保留特征图上的空间信息,从而更适于像素级预测。兴趣区域对⻬层的输出包含了所有与兴趣区域的形状相同的特征图。它们不仅被⽤于预测每个兴趣区域的类别和边界框,还通过额外的全卷积⽹络预测⽬标的像素级位置。

补充:ROI Align 和 ROI Pooling

这两个都是用在rpn之后的。具体来说,从feature map上经过RPN得到一系列的proposals,大概2k个,这些bbox大小不等,如何将这些bbox的特征进行统一表示就变成了一个问题。即需要找一个办法从大小不等的框中提取特征使输出结果是等长的。最开始目标检测模型Faster RCNN中用了一个简单粗暴的办法,叫ROI Pooling。该方式在语义分割这种精细程度高的任务中,不够精准,由此发展来了ROI Align。

ROI Pooling:

假如现在有一个8×8的feature map,现在希望得到2×2的输出,有一个bbox坐标为[0,3,7,8]。

这个bbox的w=7,h=5,如果要等分成四块是做不到的,因此在ROI Pooling中会进行取整。就有了上图看到的h被分割为2,3,w被分割成3,4。这样之后在每一块(称为bin)中做max pooling,可以得到下图的结果。

这样就可以将任意大小bbox转成2×2表示的feature。

ROI Pooling需要取整,这样的取整操作进行了两次,一次是得到bbox在feature map上的坐标时。

例如:原图上的bbox大小为665×665,经backbone后,spatial scale=1/32。因此bbox也相应应该缩小为665/32=20.78,但是这并不是一个真实的pixel所在的位置,因此这一步会取为20。0.78的差距反馈到原图就是0.78×32=25个像素的差距。如果是大目标这25的差距可能看不出来,但对于小目标而言差距就比较巨大了。

ROI Align

因此有人提出不需要进行取整操作,如果计算得到小数,也就是没有落到真实的pixel上,那么就用最近的pixel对这一点虚拟pixel进行双线性插值,得到这个“pixel”的值。

  1. 将bbox区域按输出要求的size进行等分,很可能等分后各顶点落不到真实的像素点上
  2. 没关系,在每个bin中再取固定的4个点(作者实验后发现取4效果较好),也就是图二右侧的蓝色点
  3. 针对每一个蓝点,距离它最近的4个真实像素点的值加权(双线性插值),求得这个蓝点的值
  4. 一个bin内会算出4个新值,在这些新值中取max,作为这个bin的输出值
  5. 最后就能得到2×2的输出

ROI Pooling和ROI Align

这两个都是用在rpn之后的。具体来说,从feature map上经过RPN得到一系列的proposals,大概2k个,这些bbox大小不等,如何将这些bbox的特征进行统一表示就变成了一个问题。即需要找一个办法从大小不等的框中提取特征使输出结果是等长的。

最开始目标检测模型Faster RCNN中用了一个简单粗暴的办法,叫ROI Pooling。

该方式在语义分割这种精细程度高的任务中,不够精准,由此发展来了ROI Align。

今天就总结下两者的思想。

ROI Pooling

假如现在有一个8×8的feature map,现在希望得到2×2的输出,有一个bbox坐标为[0,3,7,8]。

这个bbox的w=7,h=5,如果要等分成四块是做不到的,因此在ROI Pooling中会进行取整。就有了上图看到的h被分割为2,3,w被分割成3,4。这样之后在每一块(称为bin)中做max pooling,可以得到下图的结果。

这样就可以将任意大小bbox转成2×2表示的feature。

ROI Pooling需要取整,这样的取整操作进行了两次,一次是得到bbox在feature map上的坐标时。

例如:原图上的bbox大小为665×665,经backbone后,spatial scale=1/32。因此bbox也相应应该缩小为665/32=20.78,但是这并不是一个真实的pixel所在的位置,因此这一步会取为20。0.78的差距反馈到原图就是0.78×32=25个像素的差距。如果是大目标这25的差距可能看不出来,但对于小目标而言差距就比较巨大了。

图1

ROI Align

因此有人提出不需要进行取整操作,如果计算得到小数,也就是没有落到真实的pixel上,那么就用最近的pixel对这一点虚拟pixel进行双线性插值,得到这个“pixel”的值。

具体做法如下图所示:

  1. 将bbox区域按输出要求的size进行等分,很可能等分后各顶点落不到真实的像素点上
  2. 没关系,在每个bin中再取固定的4个点(作者实验后发现取4效果较好),也就是图二右侧的蓝色点
  3. 针对每一个蓝点,距离它最近的4个真实像素点的值加权(双线性插值),求得这个蓝点的值
  4. 一个bin内会算出4个新值,在这些新值中取max,作为这个bin的输出值
  5. 最后就能得到2×2的输出

船长

赵雷

请告诉我寂寞的时长
那里是否铺满花儿的香
在海上漂了很久会觉得岸上很晃
雨打湿了衣裳就适应了海水的凉

海北的路上没有信号
海北的灯光比繁星亮
海北的机车妹匆匆忙
海北的空调像是冬天一样冷得让我
缩成一只小鸟 缩成一只小鸟
可是没有人知道
我是刚刚经过暴风雨回到岸上的船长
会到岸的船长
而我却迷失方向
像是气球一样四处撞

槟榔一定配香烟才够爽
海北的男孩讲话有些娘娘腔
尽管看上去他们更强壮
但是我有胆量去征服海洋

上天没有给我华丽的皮囊
至今学不会打领结穿西装
海北没有适合我的衣裳
我喜欢光着膀子露出胸膛

吃饱的乌鸦在叫
迟到的学生赛跑
妩媚的夜让我想念我的船和那些被雨淋湿的海鸟
哭泣的乌云在飘
沉默的大地在摇
我开始习惯热流里涌来的爆米花味道

我的心总有一些问号
我的心总有一些孤傲
我的心总有一些潦倒
但从来没有任何事能把我困扰

我的心总有一些荒谬
我的心总有一些寂寥
我的心总有一些征兆
所以我停靠海北
来到海北 在海北
这里的人也会因为爱情喝得烂醉
一样拥挤的海北
璀璨的海北
哦 baby
我无法入睡

UNET 3+

论文: https://arxiv.org/abs/2004.08790

github: https://github.com/ZJUGiveLab/UNet-Version

UNet是医学影像分割领域应用最广泛的的网络,其性能和网络中多尺度特征的融合密切相关。此后的UNet++通过嵌套结构和密集的跳过连接原始网络进行了改进。本文提出的UNet3+通过全尺度的连接和深度监督来融合深层和浅层特征的同时对各个尺度的特征进行监督。提出的UNet3+网络可以在减少网络参数的同时提高计算效率,在两个数据集上验证了方法有效性。相关代码已经开源。

现有的分割网络如UNET、PSPNET和DeepLab等网络都通常会通过多尺度的方式提取图像的信息。低层次的细节特征图中具有更丰富的例如边界这样空间信息,高层特征图中包含更多的例如物体位置这样的高级语义特征。然而,随着网络的下采样和上采样,这些高低层的信息并没有被充分地利用。因此,文章提出的UNet3+对网络的编码器与解码器连接以及解码器内部之间的连接进行了改进。此外,文章通过提出的混合损失函数对各层进行深度监督和分类分支指导分割的方式,进一步提高了分割的精度。总结来说,文章主要有以下四点贡献:

  1. 设计了一种新的网络结构UNet3+,通过引入全尺度的跳过连接,在全尺度特征映射中融合了低层细节和高层语义,充分利用了多尺度特征的同时具有更少的参数;
  2. 通过深度监督让网络从全尺度特征中学习分割表示,提出了更优的混合损失函数以增强器官的边界;
  3. 提出分类指导模块,通过与图像分类分支联合训练的方式,减少了网络在非器官图像的过度分割(over-segmentation);
  4. 在肝脏和脾脏数据集上进行了广泛的实验,证明了UNet 3+的有效性。
  5. 从图中可以看到,UNet3+与UNet主体上非常相似,不同之处在于从编码器到解码器的跳过连接以及不同层级的编码器之间的连接。以图中的节点 XDe3 为例,它的信息来自于两方面,一是比其更浅(包括同一层级)的编码器,二十比其更深的解码器。不同层级的特征通过maxpooling和双线性上采样的方式进行尺寸统一。解码层的卷积分两步,第一步是对来及各个节点的信息进行各自的卷积,第二步是对堆叠的特征通过卷积来进行信息的融合和提取。值得注意的一个细节是,进行第一个卷积层时,来自各层的数据被卷积到相同的特征图数(在这里是n/5,n为所在层的特征图数)。

从图中可以看到,UNet3+与UNet主体上非常相似,不同之处在于从编码器到解码器的跳过连接以及不同层级的编码器之间的连接。以图中的节点 XDe3 为例,它的信息来自于两方面,一是比其更浅(包括同一层级)的编码器,二是比其更深的解码器。不同层级的特征通过maxpooling和双线性上采样的方式进行尺寸统一。解码层的卷积分两步,第一步是对来及各个节点的信息进行各自的卷积,第二步是对堆叠的特征通过卷积来进行信息的融合和提取。值得注意的一个细节是,进行第一个卷积层时,来自各层的数据被卷积到相同的特征图数(在这里是n/5,n为所在层的特征图数)。

2.全尺度的深度监督

为了进一步优化网络对图像边界的分割,文章借鉴了图像质量评估中常用的多尺度SSIM(MS-SSIM)提出了MS-SSIM loss。

本文最终采用了混合损失函数(focal loss,ms-ssim loss和iou loss)来对各层进行监督。

ℓseg=ℓfl+ℓms−ssim+ℓiou

3. 分类指导模块(CGM)

在大多数医学图像分割中,非器官图像中出现假阳性不可避免。这通常是保留在较浅层中背景噪声信息导致的过分割现象。为了实现更精确的分割,文章尝试通过添加一个额外的分类任务来解决这个问题,该分类任务被设计用于预测输入图像是否有器官。简单来说,当预测到图像包含待分割器官的概率较小时,对输出图像乘以0使得输出全黑。

文章采用了LITS的肝脏数据集和自己采集的脾脏数据集通过两组实验来进行验证。

第一组对UNet、UNet++、UNet3+(带深度监督和不带)以Vgg和ResNet101作为backbone进行了对比。可以在以Vgg为backbone时,UNet3+比其UNet在两个数据集上分别有2.8%和4.1%的提升。网络相比于UNet++也有较大的提升。另外,可以看到UNet3+使用了更少的参数得到了更好的结果。可视化的结果表明即使在器官较小的情况下网络也能得到更加精细连贯的分割。

文章进一步以ResNet作为backbone,将网络与当前比较先进的分割网络进行对比。在这里,在验证网络有效性的同时,文章对提出的损失函数和分类分支进行了消融实验。

文章对之前在UNet解码器只接收的来自同一层编码器和深一层解码器的连接方式进行了改进,使得解码器都能获得来自每一个更浅的编码器和更深的解码器的信息,使得网络能够更好地提取和融合多尺度的信息。网络的结构设计简洁优雅,是一篇非常不错的UNet改进文章。另外文章提出的MS-SSIM损失和分类指导模块也挺有意思。当然我对文章也有一些思考。第一,网络结构设计中,对于来自不同层级的特征,进行融合时可以考虑通过PSP或者Deeplab的方式(JPU是一种很好选择),也可以考虑通过SE的方式来进行通道的选择。第二,文章提出的MS-SSIM能够更好地分割图像的边界,那么选取豪斯多夫距离这样的指标可以更好地证明方法的有效性。第三,文章通过CGM来对输出进行限制,但是对于器官的顶端和底端这样本身有比较多歧义性图像,容易造成无法分割的情况,可以考虑进行soft的指导。

关于NLP数据清洗和数据增强

最近参加一个NLP关于医学电子病历的疾病多标签分类比赛,因为之前比较少去做NLP方向,所以,参赛纯粹是为了了解了解NLP方向,好在nlp做文本分类算是比较简单的下游任务,但在参赛过程中,会发现,其实对于文本分类来说,基本的bert-base的效果不是很好,但其实感觉不是出在模型架构方面,对于简单的分类任务,一个12层的bert应该适足以胜任了,因此需要将注意力看向数据预处理、数据清洗、数据增强。以及数据类别分布不均匀,也可以尝试使用不同的损失函数。另外,不可否认,输入的文本长度越长,效果应该会更好一些,但奈何没有“钞”能力。此外,还可以尝试模型集成、交叉验证(五折交叉验证,即:将训练集分为五部分,一部分做验证集,剩下四部分做训练集,相当于得到五个模型。验证集组合起来就是训练集。五个模型对测试集的预测取均值得到最终的预测结果。)这块的思路还挺多的,算是做个记录,方便后面在处理类似文本分类任务时候的一个参考。

言归正传:今天来总结下NLP中的数据处理。

1、数据清洗

什么是数据清洗:

数据清洗是指发现并纠正数据文件中可识别的错误的最后一道程序,包括检查数据一致性,处理无效值和缺失值等。与问卷审核不同,录入后的数据清理一般是由计算机而不是人工完成。数据清洗从名字上也看的出就是把“脏”的“洗掉”,指发现并纠正数据文件中可识别的错误的最后一道程序,包括检查数据一致性,处理无效值和缺失值等。

为什么要进行数据清洗

因为数据仓库中的数据是面向某一主题的数据的集合,这些数据从多个业务系统中抽取而来而且包含历史数据,这样就避免不了有的数据是错误数据、有的数据相互之间有冲突,这些错误的或有冲突的数据显然是我们不想要的,称为“脏数据”。我们要按照一定的规则把“脏数据”“洗掉”,这就是数据清洗。

清洗后,一个数据集应该与系统中其他类似的数据集保持一致。 检测到或删除的不一致可能最初是由用户输入错误、传输或存储中的损坏或不同存储中类似实体的不同数据字典定义引起的。 数据清理与数据确认(data validation)的不同之处在于,数据确认几乎总是意味着数据在输入时被系统拒绝,并在输入时执行,而不是执行于批量数据。

数据清洗不仅仅更正错误,同样加强来自各个单独信息系统不同数据间的一致性。专门的数据清洗软件能够自动检测数据文件,更正错误数据,并用全企业一致的格式整合数据。

数据清洗流程:

(1)中文首先需要分词,可以采用结巴分词、HanNLP、刨丁解牛等分词工具;

(2)数据规范化处理(Normalization):比如通常会把文本中的大写转成小写,清除文本中的句号、问号、感叹号等特殊字符,并且仅保留字母表中的字母和数字。小写转换和标点移除是两个最常见的文本 Normalization 步骤。是否需要以及在哪个阶段使用这两个步骤取决于你的最终目标。

去除一些停用词。而停用词是文本中一些高频的代词、连词、介词等对文本分类无意义的词,通常维护一个停用词表,特征提取过程中删除停用表中出现的词,本质上属于特征选择的一部分。具体可参考Hanlp的停用词表https://github.com/hankcs/HanLP

(3)Tokenization,Token 是“符号”的高级表达。一般指具有某种意义,无法再分拆的符号。在英文自然语言处理中,Tokens 通常是单独的词。因此,Tokenization 就是将每个句子分拆成一系列词。可以使用NLTK工具箱来完成相关操作。

(4)Stop Word 是无含义的词,例如 ‘is’/‘our’/‘the’/‘in’/‘at’ 等。它们不会给句子增加太多含义,单停止词是频率非常多的词。 为了减少我们要处理的词汇量,从而降低后续程序的复杂度,需要清除停止词。

(5)Part-of-Speech Tagging:还记得在学校学过的词性吗?名词、代词、动词、副词等等。识别词在句子中的用途有助于我们更好理解句子内容。并且,标注词性还可以明确词之间的关系,并识别出交叉引用。同样地,NLTK 给我们带来了很多便利。你可以将词传入 PoS tag 函数。然后对每个词返回一个标签,并注明不同的词性。

(6)Named Entity 一般是名词短语,又来指代某些特定对象、人、或地点 可以使用 ne_chunk()方法标注文本中的命名实体。在进行这一步前,必须先进行 Tokenization 并进行 PoS Tagging。

(7)Stemming and Lemmatization:为了进一步简化文本数据,我们可以将词的不同变化和变形标准化。Stemming 提取是将词还原成词干或词根的过程。

(8)一些词在句首句尾句中出现的概率不一样,统计N-GRAM特征的时候要在句首加上BOS,句尾加上EOS作标记。

(9)把长文本分成句子和单词这些fine granularity会比较有用。

(10) 一般会有一个dictionary,不在dictionary以内的单词就用UNK取代。

(11)单词会被转成数字(它对应的index,从0开始,一般0就是UNK)。

(12)做机器翻译的时候会把单词转成subword units。

这块的代码还是比较多

1、A Python toolkit for file processing, text cleaning and data splitting. 文件处理,文本清洗和数据划分的python工具包。

2、基本的文本清洗,主要解决文本数据处理的问题

数据增强

与计算机视觉中使用图像进行数据增强不同,NLP中文本数据增强是非常罕见的。这是因为图像的一些简单操作,如将图像旋转或将其转换为灰度,并不会改变其语义。语义不变变换的存在使增强成为计算机视觉研究中的一个重要工具。

方法

1. 词汇替换

这一类的工作,简单来说,就是去替换原始文本中的某一部分,而不改变句子本身的意思。

1.1 基于同义词典的替换

在这种方法中,我们从句子中随机取出一个单词,将其替换为对应的同义词。例如,我们可以使用英语的 WordNet 数据库来查找同义词,然后进行替换。WordNet 是一个人工维护的数据库,其中包含单词之间的关系。

Zhang 等人在2015年的论文 “Character-level Convolutional Networks for Text Classification” 中使用了这种方法。Mueller 等人也使用类似的方法为他们的句子相似度模型生成额外的 10K 条训练数据。这一方法也被 Wei 等人在他们的 “Easy Data Augmentation” 论文中使用。对于如何使用,NLTK 提供了对 WordNet 的接口;我们还可以使用 TextBlob API。此外,还有一个名为 PPDB 的数据库,其中包含数百万条同义词典,可以通过编程方式下载和使用。

1.2 基于 Word-Embeddings 的替换

在这种方法中,我们采用预先训练好的词向量,如 Word2Vec、GloVe、FastText,用向量空间中距离最近的单词替换原始句子中的单词。Jiao 等人在他们的论文 “TinyBert” 中使用了这种方法,以改进语言模型在下游任务上的泛化性;Wang 等人使用它来对 tweet 语料进行数据增强来学习主题模型。

例如,可以用三个向量空间中距离最近的单词替换原始句子中的单词,可以得到原始句子的三个变体。我们可以使用像 Gensim 包来完成这样的操作。在下面这个例子中,我们通过在 Tweet 语料上训练的词向量找到了单词 “awesome” 的同义词。

1.3 基于 Masked Language Model 的替换

像 BERT、ROBERTA 和 ALBERT 这样基于 Transformer 的模型已经使用 “Masked Language Modeling” 的方式,即模型要根据上下文来预测被 Mask 的词语,通过这种方式在大规模的文本上进行预训练。

Masked Language Modeling 同样可以用来做文本的数据增强。例如,我们可以使用一个预先训练好的 BERT 模型,然后对文本的某些部分进行 Mask,让 BERT 模型预测被 Mask 的词语。我们称这种方法叫 Mask Predictions。和之前的方法相比,这种方法生成的文本在语法上更加通顺,因为模型在进行预测的时候考虑了上下文信息。我们可以很方便的使用 HuggingFace 的 transfomers 库,通过设置要替换的词语并生成预测来做文本的数据增强。

1.4 基于 TF-IDF 的替换

这种数据增强方法是 Xie 等人在 “Unsupervised Data Augmentation” 论文中提出来的。其基本思想是,TF-IDF 分数较低的单词不能提供信息,因此可以在不影响句子的基本真值标签的情况下替换它们。

具体如何计算整个文档中单词的 TF-IDF 分数并选择最低的单词来进行替换,可以参考作者公开的代码

2. Back Translation(回译)

在这种方法中,我们使用机器翻译的方法来复述生成一段新的文本。Xie 等人使用这种方法来扩充未标注的样本,在 IMDB 数据集上他们只使用了 20 条标注数据,就可以训练得到一个半监督模型,并且他们的模型优于之前在 25000 条标注数据上训练得到的 SOTA 模型。

使用机器翻译来回译的具体流程如下:

  • 找一些句子(如英语),翻译成另一种语言,如法语
  • 把法语句子翻译成英语句子
  • 检查新句子是否与原来的句子不同。如果是,那么我们使用这个新句子作为原始文本的补充版本。

我们还可以同时使用多种不同的语言来进行回译以生成更多的文本变体。如下图所示,我们将一个英语句子翻译成目标语言,然后再将其翻译成三种目标语言:法语、汉语和意大利语。

这种方法也在 Kaggle 上的 “Toxic Comment Classification Challenge” 的第一名解决方案中使用。获胜者将其用于训练数据扩充和测试,在应用于测试的时候,对英语句子的预测概率以及使用三种语言(法语、德语、西班牙语)的反向翻译进行平均,以得到最终的预测。

对于如何实现回译,可以使用 TextBlob 或者谷歌翻译。

3. Text Surface Transformation

这些是使用正则表达式应用的简单模式匹配变换,Claude Coulombe 在他的论文中介绍了这些变换的方法。

在论文中,他给出了一个将动词由缩写形式转换为非缩写形式的例子,我们可以通过这个简单的方法来做文本的数据增强。

需要注意的是,虽然这样的转换在大部分情况下不会改变句子原本的含义,但有时在扩展模棱两可的动词形式时可能会失败,比如下面这个例子:

为了解决这一问题,论文中也提出允许模糊收缩 (非缩写形式转缩写形式),但跳过模糊展开的方法 (缩写形式转非缩写形式)。

我们可以在这里找到英语缩写的列表。对于展开,可以使用 Python 中的 contractions 库

4. Random Noise Injection

这些方法的思想是在文本中注入噪声,来生成新的文本,最后使得训练的模型对扰动具有鲁棒性。

4.1 Spelling error injection

在这种方法中,我们在句子中添加一些随机单词的拼写错误。可以通过编程方式或使用常见拼写错误的映射来添加这些拼写错误,具体可以参考这个链接

4.2 QWERTY Keyboard Error Injection

这种方法试图模拟在 QWERTY 键盘布局上打字时由于键之间非常接近而发生的常见错误。这种错误通常是在通过键盘输入文本时发生的。

4.3 Unigram Noising

这种方法已经被 Xie 等人和 UDA 的论文所使用,其思想是使用从 unigram 频率分布中采样的单词进行替换。这个频率基本上就是每个单词在训练语料库中出现的次数。

4.4 Blank Noising

该方法由 Xie 等人在他们的论文中提出,其思想是用占位符标记替换一些随机单词。本文使用 “_” 作为占位符标记。在论文中,他们使用它作为一种避免在特定上下文上过度拟合的方法以及语言模型平滑的机制,这项方法可以有效提高生成文本的 Perplexity 和 BLEU 值。

4.5 Sentence Shuffling

这是一种很初级的方法,我们将训练样本中的句子打乱,来创建一个对应的数据增强样本。

Sentence Shuffling 的过程

4.6 Random Insertion

这个方法是由 Wei 等人在其论文 “Easy Data Augmentation” 中提出的。在该方法中,我们首先从句子中随机选择一个不是停止词的词。然后,我们找到它对应的同义词,并将其插入到句子中的一个随机位置。(也比较 Naive)

4.7 Random Swap

这个方法也由 Wei 等人在其论文 “Easy Data Augmentation” 中提出的。该方法是在句子中随机交换任意两个单词。

4.8 Random Deletion

该方法也由 Wei 等人在其论文 “Easy Data Augmentation” 中提出。在这个方法中,我们以概率 p 随机删除句子中的每个单词。

5. Instance Crossover Augmentation

这种方法由 Luque 在他 TASS 2019 的论文中介绍,灵感来自于遗传学中的染色体交叉操作。

在该方法中,一条 tweet 被分成两半,然后两个相同情绪类别(正/负)的 tweets 各自交换一半的内容。这么做的假设是,即使结果在语法和语义上不健全,新的文本仍将保留原来的情绪类别。

Instance Crossover 的过程

这中方法对准确性没有影响,并且在 F1-score 上还有所提升,这表明它帮助了模型提升了在罕见类别上的判断能力,比如 tweet 中较少的中立类别。

6. Syntax-tree Manipulation

这种方法最先是由 Coulombe 提出的,其思想是解析并生成原始句子的依赖树,使用规则对其进行转换来对原句子做复述生成。

例如,一个不会改变句子意思的转换是句子的主动语态和被动语态的转换。

7. MixUp for Text

Mixup 是 Zhang 等人在 2017 年提出的一种简单有效的图像增强方法。其思想是将两个随机图像按一定比例组合成,以生成用于训练的合成数据。对于图像,这意味着合并两个不同类的图像像素。它在模型训练的时候可以作为的一种正则化的方式。

为了把这个想法带到 NLP 中,Guo 等人修改了 Mixup 来处理文本。他们提出了两种将 Mixup 应用于文本的方法:

7.1 wordMixup

在这种方法中,在一个小批中取两个随机的句子,它们被填充成相同的长度;然后,他们的 word embeddings 按一定比例组合,产生新的 word embeddings 然后传递下游的文本分类流程,交叉熵损失是根据原始文本的两个标签按一定比例计算得到的。

7.2 sentMixup

在这种方法中,两个句子首先也是被填充到相同的长度;然后,通过 LSTM/CNN 编码器传递他们的 word embeddings,我们把最后的隐藏状态作为 sentence embedding。这些 embeddings 按一定的比例组合,然后传递到最终的分类层。交叉熵损失是根据原始文本的两个标签按一定比例计算得到的。

8. 生成式的方法

这一类的工作尝试在生成额外的训练数据的同时保留原始类别的标签。

Conditional Pre-trained Language Models

这种方法最早是由 Anaby-Tavor 等人在他们的论文 “Not Enough Data? Deep Learning to the Rescue!” Kumar 等人最近的一篇论文在多个基于 Transformer 的预训练模型中验证了这一想法。

问题的表述如下:

  1. 在训练数据中预先加入类别标签,如下图所示。
训练数据中加入类别标签

2. 在这个修改过的训练数据上 finetune 一个大型的预训练语言模型 (BERT/GPT2/BART) 。对于 GPT2,目标是去做生成任务;而对于 BERT,目标是要去预测被 Mask 的词语。

3. 使用经过 finetune 的语言模型,可以使用类标签和几个初始单词作为模型的提示词来生成新的数据。本文使用每条训练数据的前 3 个初始词来为训练数据做数据增强。

9. 实现过程

nlpaug 和 textattack 等第三方 Python 库提供了简单易用的 API,可以轻松使用上面介绍的 NLP 数据增强方法。

1、 NLP Chinese Data Augmentation 一键中文数据增强工具: https://github.com/425776024/nlpcda

使用:pip install nlpcda

介绍

一键中文数据增强工具,支持:

2、 TextAttack 是一个可以实行自然语言处理的Python 框架,用于方便快捷地进行对抗攻击,增强数据,以及训练模型。https://github.com/QData/TextAttack/blob/master/README_ZH.md

文档:https://textattack.readthedocs.io/en/latest/0_get_started/basic-Intro.html

3、中文语料的EDA数据增强工具

4、中文谐音词/字库

10. 结论

通过阅读许多 NLP 数据增强方面的论文,我发现大多数方法都是具有很强的任务属性的,并且针对这些方法的实验也只在某些特定的场景进行了验证。可以见得,系统地比较这些方法并且分析它们在其他任务上的表现在未来将是一项有趣的研究。

Rotamer-Free Protein Sequence Design Based on Deep Learning and Self-Consistency

论文地址: https://www.nature.com/articles/s43588-022-00273-6

中国科大用深度学习实现高实验成功率的蛋白质序列从头设计

中国科学技术大学生命科学与医学部刘海燕教授、陈泉副教授团队与信息科学技术学院李厚强教授团队合作,开发了一种基于深度学习为给定主链结构从头设计氨基酸序列的算法ABACUS-R,在实验验证中,ABACUS-R的设计成功率和设计精度超过了原有统计能量模型ABACUS。相关成果以“Rotamer-Free Protein Sequence Design Based on Deep Learning and Self-Consistency”为题于北京时间2022年7月21日发表于Nature Computational Science。

刘海燕教授、陈泉副教授团队致力于发展数据驱动的蛋白质设计方法,建立并实验验证了利用神经网络能量函数从头设计主链结构的SCUBA模型,以及对给定主链结构设计氨基酸序列的统计能量函数ABACUS。然而,通过优化能量函数来进行序列设计的方法在成功率、计算效率等方面仍有不足。近期有多项研究表明,用深度学习进行氨基酸序列设计能够在天然氨基酸残基类型恢复率等计算指标上超过能量函数方法;但截至目前已正式发表的工作中,对相关方法的实验验证结果远未达到能量函数方法的成功率。该论文报道的ABACUS-R模型,则不仅在计算指标上超过ABACUS,在实验验证中成功率和结构精度也有大幅提高。

用ABACUS-R进行序列设计的方法由两部分组成(图1)。第一部分为预训练的编码器-解码器网络:该网络用Transformer把中心氨基酸残基的化学和空间结构环境映射为隐空间表示向量,再用多层感知机网络将该向量解码为包括中心残基氨基酸类型在内的多种真实特征(图1a)。在方法的第二部分,经用非冗余天然蛋白序列结构数据训练后,ABACUS-R编码器-解码器被用于给定主链结构的全部或部分氨基酸序列从头设计。具体为:从任意初始序列出发,对各个类型待定残基分别应用ABACUS-R编码器-解码器,得到环境依赖的最适宜残基类型,并反复迭代至不同位点的残基类型最大程度自洽(图1b)。

图1. 用ABACUS-R模型进行蛋白质序列设计的原理。(a) 预训练的编码器-解码器网络;(b)采用自洽迭代策略进行全序列从头设计。

ABACUS-R方法包含两部分:(1)一个encoder-decoder网络被预训练用以推断给定骨架的局部环境时中心残基的侧链类型 (2)用该encoder-decoder网络连续更新每个残基的类型,最终收敛获得自洽(self-consistent)。网络的输入是中心残基与空间上最邻近(Cα间距离)k个残基组成的局部结构。邻近残基的特征包含空间层面的相对位置与取向信息(XSPA)、序列层面的相对位置信息(XRSP)以及邻近残基的残基类型(XAA)。第i个中心残基的特征包含全零的XSPA、被mask的XAA以及骨架上的15个ϕi−2ψi−2ωi−2 ⋯ ϕi+2ψi+2ωi+2,这些特征组合起来会被映射到与邻近残基特征相同的维度。以上模型输入的信息都是旋转平移不变的。局部结构中的所有残基的特征经过可学习的映射后融合后,得到每个残基总特征En。{En; n = 0, 1, 2, … , k}经过基于 transformer架构的encoder-decoder,预测每个中心残基的类型以及其他辅助任务。

自洽迭代设计的方法是:对序列随机初始化,第一轮随机选择80%的残基通过encoder-decoder并行预测其残基类型,以后每轮随机选择的残基数目逐渐下降。最终的设计结果会逐渐收敛。

在理论验证的基础上,中国科大团队尝试了实验表征用ABACUS-R对3个天然主链结构重新设计的57条序列;其中86%的序列(49条)可溶表达并能折叠为稳定单体;实验解析的5个高分辨晶体结构与目标结构高度一致(主链原子位置均方根位移在1Å以下)(图2)。此外,与以前报道的从头设计蛋白相似,ABACUS-R从头设计的蛋白表现出超高热稳定性,去折叠温度大多可达100℃以上。

作者将PDB中的非冗余结构按照两种不同的方式划分了95%作为训练集、5%作为测试集,第一种划分方式确保测试集的结构不会存在训练集中出现过的CATH拓扑,训练得到的模型为Model­­eval;第二种划分方式时随机划分Modelfinal。Model­­eval可以用来评估模型能力的无偏向性的表现,而Modelfinal使用了更丰富的数据训练表现应当更好。

表现评估

Encoder-decoder的架构可以进行多任务学习,除了训练序列的恢复的任务以外,还可以预测二级结构、SASA、B-factor与侧链扭转角χ1、χ2。多个任务可以增强模型设计序列的能力(图2a),Model­­eval与Model­­final都可以在测试集上最好取得50%左右准确度。在测试集上的结果显示,虽然有些残基类型没有恢复正确,但是模型也学习到了替换为性质相似的残基(图2b)。

相较于ABACUS模型,ABACUS-R序列设计更高的成功率和结构精度进一步增强了数据驱动蛋白质从头设计方法的实用性。ABACUS-R还提供了一种对蛋白质局部结构信息的预训练表示方式,可用于序列设计以外的其他任务。

Decoder网络输出的是每个位置上残基类型的-logP,类似于选择不同残基对应的能量,所以作者将ProTherm数据集中蛋白突变的ΔΔG与模型计算出相应的−ΔΔlogits进行了比较,发现二者有一定的相关性(图2d),说明模型一定程度上学习到了能量。

接着,作者验证了模型的自洽性,测试集中100个蛋白属于CATH的三个大类,对其中的每个蛋白从随机序列出发设计10条序列,随着迭代的次数变多,平均-logP会趋于收敛(图3a),同时未收敛的残基比例也会收敛(图3b)。不同CATH类别的骨架上取得的序列恢复率差距不大(图3c)。同一蛋白骨架设计出的序列会有很高的相似性(0.76-0.89)。设计出的序列与天然序列相比,序列的成分高度相似(图3d),Pearson相关系数达到了0.93,但GLU、ALA与LYS出现得更频繁,而Gln、His、Met出现得更少。此外,ABACUS-R设计出的序列与ABACUS设计出的序列相比,平均每个残基的Rosetta打分更低(图3e),而平均的-logP打分却更高(图3f),这意味着ABACUS-R学习到的能量与Rosetta打分函数存在正交的部分。

图3. ABACUS-R的自洽能力、设计能力以及学习到的能量与Rosetta打分的比较

相较于其他深度学习方法在单个残基恢复任务上的表现,ABACUS-R超过了除DenseCPD外的所有方法(表1),在整条序列重设计任务上ABACUS-R在两个测试集上都取得了最好的表现

实验验证

最后,作者在3种天然骨架(PDB ID: 1r26, 1cy5 and 1ubq)上通过实验验证了ABACUS-R的设计能力。设计的方法有两种:第一种采用迭代自洽的设计方法(生成序列的多样性低),第二种采用迭代时对decoder输出结果进行采样(生成序列的多样性高,但-logP能量也略高)。

第一种方法设计的27条序列有26条成功表达,体积排阻色谱与1H NMR实验结果显示所有的蛋白都以单体形式存在,示差扫描量热实验显示5条序列有很好的热稳定性( 97~117 C )。最终,1r26的3个设计与1cy5的1个设计成功解出了晶体结构,Cα RMSD位于0.51~0.88 Å,而1ubq的1个设计虽然没有解出结构,但已有的实验结果显示它折叠成了明确的三维结构。

第二种方法对同一骨架设计的序列相似度在58%左右。30条设计的序列中,25条被成功表达,23条能被可溶地纯化。所有设计同样都是单体存在并且折叠成了明确的三维结构,5个设计有很好的热稳定性(85~118 C)。最终,1r26的1个设计被成功解出了晶体结构,Cα RMSD为0.67 Å。相较方法一的自洽设计,方法二设计成功率下降,成功设计的蛋白热稳定性也略微下降,但作者认为可以接受。

最后,作者展示了所有1r26设计晶体结构核心的侧链pack(图4a,b),以及 1cy5设计晶体结构的侧链的极性作用(图4c),说明了ABACUS-R学会了设计侧链的组合以pack好的结构。

深度学习语义分割理论与实战指南

看到github中一个写的很棒的图像分割的介绍和代码实现:

https://github.com/luwill/Semantic-Segmentation-Guide

对于刚入门的同学来说,十分友好。

引言

图像分类、目标检测和图像分割是基于深度学习的计算机视觉三大核心任务。三大任务之间明显存在着一种递进的层级关系,图像分类聚焦于整张图像,目标检测定位于图像具体区域,而图像分割则是细化到每一个像素。基于深度学习的图像分割具体包括语义分割、实例分割和全景分割。语义分割的目的是要给每个像素赋予一个语义标签。语义分割在自动驾驶、场景解析、卫星遥感图像和医学影像等领域都有着广泛的应用前景。本文作为基于PyTorch的语义分割技术手册,对语义分割的基本技术框架、主要网络模型和技术方法提供一个实战性指导和参考。

1. 语义分割概述

图像分割主要包括语义分割(Semantic Segmentation)和实例分割(Instance Segmentation)。那语义分割和实例分割具体都是什么含义?二者又有什么区别和联系?语义分割是对图像中的每个像素都划分出对应的类别,即实现像素级别的分类;而类的具体对象,即为实例,那么实例分割不但要进行像素级别的分类,还需在具体的类别基础上区别开不同的个体。例如,图像有多个人甲、乙、丙,那边他们的语义分割结果都是人,而实例分割结果却是不同的对象。另外,为了同时实现实例分割与不可数类别的语义分割,相关研究又提出了全景分割(Panoptic Segmentation)的概念。语义分割、实例分割和全景分割具体如图1(b)、(c)和(d)图所示。

Fig1. Image Segmentation
在开始图像分割的学习和尝试之前,我们必须明确语义分割的任务描述,即搞清楚语义分割的输入输出都是什么。输入是一张原始的RGB图像或者单通道图像,但是输出不再是简单的分类类别或者目标定位,而是带有各个像素类别标签的与输入同分辨率的分割图像。简单来说,我们的输入输出都是图像,而且是同样大小的图像。如图2所示。 

Fig2. Pixel Representation
类似于处理分类标签数据,对预测分类目标采用像素上的one-hot编码,即为每个分类类别创建一个输出的通道。如图3所示。 

Fig3. Pixel One-hot
图4是将分割图添加到原始图像上的叠加效果。这里需要明确一下mask的概念,在图像处理中我们将其译为掩码,如Mask R-CNN中的Mask。Mask可以理解为我们将预测结果叠加到单个通道时得到的该分类所在区域。 

Fig4. Pixel labeling
所以,语义分割的任务就是输入图像经过深度学习算法处理得到带有语义标签的同样尺寸的输出图像。

。。。。。。。(剩余部分在github可查看)

Attention UNet

论文: https://arxiv.org/abs/1804.03999

以CNN为基础的编解码结构在图像分割上展现出了卓越的效果,尤其是医学图像的自动分割上。但一些研究认为以往的FCN和UNet等分割网络存在计算资源和模型参数的过度和重复使用,例如相似的低层次特征被级联内的所有网络重复提取。针对这类普遍性的问题,相关研究提出了给UNet添加注意力门控(Attention Gates, AGs)的方法,形成一个新的图像分割网络结构:Attention UNet。提出Attention UNet的论文为Attention U-Net: Learning Where to Look for the Pancreas,发表在2018年CVPR上。注意力机制原先是在自然语言处理领域被提出并逐渐得到广泛应用的一种新型结构,旨在模仿人的注意力机制,有针对性的聚焦数据中的突出特征,能够使得模型更加高效。

Attention UNet的网络结构如下图所示,需要注意的是,论文中给出的3D版本的卷积网络。其中编码器部分跟UNet编码器基本一致,主要的变化在于解码器部分。其结构简要描述如下:编码器部分,输入图像经过两组3*3*3的3D卷积和ReLU激活,然后再进行最大池化下采样,经过3组这样的卷积-池化块之后,网络进入到解码器部分。编码器最后一层的特征图除了直接进行上采样外,还与来自编码器的特征图进行注意力门控计算,然后再与上采样的特征图进行合并,经过三次这样的上采样块之后即可得到最终的分割输出图。相比于普通UNet的解码器,Attention UNet会将解码器中的特征与编码器连接过来的特征进行注意力门控处理,然后再与上采样进行拼接。经过注意力门控处理后得到的特征图会包含不同空间位置的重要性信息,使得模型能够重点关注某些目标区域。

我们将Attention UNet的注意力门控单独拿出来进行分析,看AGs是如何让模型能够聚焦到目标区域的。如图中上图所示,将Attention UNet网络中的一个上采样块单独拿出来,其中x_l为来自同层编码器的输出特征图,g表示由解码器部分用于上采样的特征图,这里同时也作为注意力门控的门控信号参数与x_l的注意力计算,而x^hat_l即为经过注意力门控计算后的特征图,此时x^hat_l是包含了空间位置重要性信息的特征图,再将其与下一层上采样后的特征图进行合并才得到该上采样块最终的输出。

将x_l和g_i计算得到的注意力系数再次与x_l相乘即可得到x^hat_l,这种经过与注意力系数相乘后的特征图会让图像中不相关的区域值变小,目标区域的值相对会变大,提升网络预测速度同时,也会提高图像的分割精度。论文中的各项实验结果也表明,经过注意力门控加成后后UNet,效果均要优于原始的UNet。下述代码给出了Attention UNet的一个2D参考实现,并且下采样次数由论文中的3次改为了4次。


### 定义Attention UNet类
class Att_UNet(nn.Module):
    def __init__(self,img_ch=3,output_ch=1):
        super(Att_UNet, self).__init__()
        self.Maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.Conv1 = conv_block(ch_in=img_ch, ch_out=64)
        self.Conv2 = conv_block(ch_in=64, ch_out=128)
        self.Conv3 = conv_block(ch_in=128, ch_out=256)
        self.Conv4 = conv_block(ch_in=256, ch_out=512)
        self.Conv5 = conv_block(ch_in=512, ch_out=1024)

        self.Up5 = up_conv(ch_in=1024, ch_out=512)
        self.Att5 = Attention_block(F_g=512, F_l=512, F_int=256)
        self.Up_conv5 = conv_block(ch_in=1024, ch_out=512)

        self.Up4 = up_conv(ch_in=512, ch_out=256)
        self.Att4 = Attention_block(F_g=256, F_l=256, F_int=128)
        self.Up_conv4 = conv_block(ch_in=512, ch_out=256)
        
        self.Up3 = up_conv(ch_in=256, ch_out=128)
        self.Att3 = Attention_block(F_g=128, F_l=128, F_int=64)
        self.Up_conv3 = conv_block(ch_in=256, ch_out=128)
        
        self.Up2 = up_conv(ch_in=128, ch_out=64)
        self.Att2 = Attention_block(F_g=64, F_l=64, F_int=32)
        self.Up_conv2 = conv_block(ch_in=128, ch_out=64)

        self.Conv_1x1 =
       nn.Conv2d(64, output_ch, kernel_size=1, stride=1, padding=0)
    
  ### 定义前向传播流程
    def forward(self,x):
        # 编码器部分
        x1 = self.Conv1(x)
        x2 = self.Maxpool(x1)
        x2 = self.Conv2(x2)
        x3 = self.Maxpool(x2)
        x3 = self.Conv3(x3)
        x4 = self.Maxpool(x3)
        x4 = self.Conv4(x4)
        x5 = self.Maxpool(x4)
        x5 = self.Conv5(x5)

        # 解码器+连接部分
        d5 = self.Up5(x5)
        x4 = self.Att5(g=d5,x=x4)
        d5 = torch.cat((x4,d5),dim=1)        
        d5 = self.Up_conv5(d5)        
        d4 = self.Up4(d5)
        x3 = self.Att4(g=d4,x=x3)
        d4 = torch.cat((x3,d4),dim=1)
        d4 = self.Up_conv4(d4)

        d3 = self.Up3(d4)
        x2 = self.Att3(g=d3,x=x2)
        d3 = torch.cat((x2,d3),dim=1)
        d3 = self.Up_conv3(d3)
        d2 = self.Up2(d3)
        x1 = self.Att2(g=d2,x=x1)
        d2 = torch.cat((x1,d2),dim=1)
        d2 = self.Up_conv2(d2)
        d1 = self.Conv_1x1(d2)
        return d1
  
  ### 定义Attention门控块
class Attention_block(nn.Module):
    def __init__(self, F_g, F_l, F_int):
        super(Attention_block, self).__init__()
    # 注意力门控向量
        self.W_g = nn.Sequential(
            nn.Conv2d(F_g, F_int,
            kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(F_int)
            )
        # 同层编码器特征图向量
        self.W_x = nn.Sequential(
            nn.Conv2d(F_l, F_int,
            kernel_size=1,stride=1,padding=0,bias=True),
            nn.BatchNorm2d(F_int)
        )
    # ReLU激活函数
    self.relu = nn.ReLU(inplace=True)
    # 卷积+BN+sigmoid激活函数
        self.psi = nn.Sequential(
            nn.Conv2d(F_int, 1,
            kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(1),
            nn.Sigmoid()
        )
        
    ###  Attention门控的前向计算流程 
    def forward(self,g,x):
        g1 = self.W_g(g)
        x1 = self.W_x(x)
        psi = self.relu(g1+x1)
        psi = self.psi(psi)
        return x*psi

总结来说,Attention UNet提出了在原始UNet基础添加注意力门控单元,注意力得分能够使得图像分割时聚焦到目标区域,该结构作为一个通用结构可以添加到任何任务类型的神经网络结构中,在语义分割网络中对前景目标区域的像素更具有敏感度。Attention UNet壮大了UNet家族网络,此后基于其的改进版本也层出不穷。