MixMatch的fastai / Pytorch实现

作者 | Noah Rubinstein

来源 | Medium

编辑 | 代码医生团队

在这篇文章中,将讨论和实施Berthelot,Carlini,Goodfellow,Oliver,Papernot和Raffel [1]的“MixMatch:A Semiistic Approach to Semi-Supervised Learning;”。MixMatch于2019年5月发布,是一种半监督学习算法,其性能明显优于以前的方法。

MixMatch有多大改进?当使用250张标记图像对CIFAR10进行训练时,MixMatch在错误率上的表现优于下一个最佳技术(虚拟对抗训练)近25%(11.08%对36.03%;相比之下,所有50k图像的全监督案例的错误率均为4.13%)。[1]这些远非增量结果,该技术显示了显着改善半监督学习状态的潜力。

半监督学习主要是反对过度拟合的斗争; 当标记集很小时,不需要非常大的神经网络来记忆整个训练集。几乎所有半监督方法背后的一般思想是利用未标记的数据作为标记数据训练的正则化器。不同的技术采用不同形式的正则化,MixMatch论文将它们分为三组:熵最小化,一致性正则化和通用正则化。由于所有三种正则化形式都证明是有效的,因此MixMatch算法包含各自的特征。

MixMatch是近年来出现的几种技术的组合和改进,包括:Mean Teacher [2],Virtual Adversarial Training [3]和Mixup [4]。在较高的层次上,MixMatch的想法是使用模型中的预测标记未标记的数据,然后以多种形式应用重正则化。第一种是多次执行数据增加并取标签预测的平均值。然后,这些预测被“锐化”以减少其熵。最后,在标记和未标记的组上进行混合。

目标是那些熟悉Pytorch的人,但不一定是fastai。对于此帖子的Jupyter笔记本版本,其中包含重现所有结果所需的完整代码如下:

https://github.com/noachr/MixMatch-fastai

fastai

在深入论文之前,将简要介绍一下fastai。Fastai是一个基于Pytorch构建的库,它使编写机器学习应用程序变得更加容易和简单。与纯Pytorch相比,fastai显着减少了生成最先进神经网络所需的样板代码量。在这里将使用fastai的数据管道和训练循环功能。

#Importing fastai will also import numpy, pytorch, etc. 
from fastai.vision import *
from numbers import Integral
import seaborn as sns

组件

首先描述组装MixMatch所需的各个部分,然后将它们组合在一起以形成完整的算法。在论文之后,将使用CIFAR10并将500个随机选择的图像作为标记的训练集。标准10000图像测试装置用于所有精度测量。

数据增强

数据增强是一种广泛使用的一致性正则化技术,其在计算机视觉领域中取得了最大的成功)。这个想法是在保留其语义标签的同时改变输入数据。对于图像,常见的增强包括旋转,裁剪,缩放,增亮等 - 所有变换都不会改变图像的基础内容。MixMatch通过多次执行增强来生成多个新图像,从而更进一步。然后对这些图像上的模型的预测进行平均以产生未标记数据的目标。这使得预测比使用单个图像更稳健。作者发现只有两个增益足以看到这个好处。

Fastai有一个高效的转换系统,将利用它来处理数据。但是因为它设计为每个图像只生成一个增强,将首先修改默认的LabelList以发出多个增强。

#Modified from https://github.com/fastai/fastai/blob/master/fastai/data_block.py#L643
K=2
class MultiTransformLabelList(LabelList):
    def __getitem__(self,idxs:Union[int,np.ndarray])->'LabelList':
        "return a single (x, y) if `idxs` is an integer or a new `LabelList` object if `idxs` is a range."
        idxs = try_int(idxs)
        if isinstance(idxs, Integral):
            if self.item is None: x,y = self.x[idxs],self.y[idxs]
            else:                 x,y = self.item   ,0
            if self.tfms or self.tfmargs:
                #I've changed this line to return a list of augmented images
                x = [x.apply_tfms(self.tfms, **self.tfmargs) for _ in range(K)]
            if hasattr(self, 'tfms_y') and self.tfm_y and self.item is None:
                y = y.apply_tfms(self.tfms_y, **{**self.tfmargs_y, 'do_resolve':False})
            if y is None: y=0
            return x,y
        else: return self.new(self.x[idxs], self.y[idxs])
        
#I'll also need to change the default collate function to accomodate multiple augments
def MixmatchCollate(batch):
    batch = to_data(batch)
    if isinstance(batch[0][0],list):
        batch = [[torch.stack(s[0]),s[1]] for s in batch]
    return torch.utils.data.dataloader.default_collate(batch)

Fastai的数据块api允许灵活地加载,标记和整理几乎任何形式的数据。但是它没有一种方法来获取一个文件夹的子集和整个另一个文件夹,这是必需的。因此将继承ImageList类并添加自定义方法。将使用fastai的get_transforms方法,没有参数来使用默认的图像变换; 它们围绕中心y轴旋转,旋转高达10度,变焦,照明变化和翘曲。Fastai的变换系统在应用时自动随机化每个变换的精确参数。

#Grab file path to cifar dataset. Will download data if not present
path = untar_data(URLs.CIFAR)
 
#Custom ImageList with filter function
class MixMatchImageList(ImageList):
    def filter_train(self,num_items,seed=2343):
        train_idxs = np.array([i for i,o in enumerate(self.items) if Path(o).parts[-3] != "test"])
        valid_idxs = np.array([i for i,o in enumerate(self.items) if Path(o).parts[-3] == "test"])
        np.random.seed(seed)
        keep_idxs = np.random.choice(train_idxs,num_items,replace=False)
        self.items = np.array([o for i,o in enumerate(self.items) if i in np.concatenate([keep_idxs,valid_idxs])])
        return self
    
#Create two databunch objects for the labeled and unlabled images. A fastai databunch is a container for train, validation, and
#test dataloaders which automatically processes transforms and puts the data on the gpu.
data_labeled = (MixMatchImageList.from_folder(path)
                .filter_train(500) #Use 500 labeled images for traning
                .split_by_folder(valid="test") #test on all 10000 images in test set
                .label_from_folder()
                .transform(get_transforms(),size=32)
                #On windows, must set num_workers=0. Otherwise, remove the argument for a potential performance improvement
                .databunch(bs=64,num_workers=0)
                .normalize(cifar_stats))
 
train_set = set(data_labeled.train_ds.x.items)
src = (ImageList.from_folder(path)
        .filter_by_func(lambda x: x not in train_set)
        .split_by_folder(valid="test"))
src.train._label_list = MultiTransformLabelList
data_unlabeled = (src.label_from_folder()
         .transform(get_transforms(),size=32)
         .databunch(bs=128,collate_fn=MixmatchCollate,num_workers=0)
         .normalize(cifar_stats))
 
#Databunch with all 50k images labeled, for baseline
data_full = (ImageList.from_folder(path)
        .split_by_folder(valid="test")
        .label_from_folder()
        .transform(get_transforms(),size=32)
        .databunch(bs=128,num_workers=0)
        .normalize(cifar_stats))

Mixup

Mixing最初是由Zhang,Cisse,Dauphin和Lopez-Paz [4]在2018年引入的,属于一般或传统正规化的范畴。Mixup不是将单个图像传递给模型,而是在两个单独的训练图像之间执行线性插值,并将其传递给模型。使用与图像相同的λ系数,也对图像的一个热编码标签进行插值。该系数是从β分布中随机抽取的,由alpha参数化。通常,α需要调整到数据集。在小的α值时,β分布的尾部大部分重量接近0或1.随着α的增加,分布变得均匀,然后在0.5左右加标。因此α可以被视为控制混合的强度; 小值只会产生少量混淆,而较大的值偏向最大混合(50/50)。在极端情况下,α= 0导致根本没有混淆,并且当α→∞时,β接近以0.5为中心的狄拉克δ分布。作者建议从.75的值开始,如下所示仍然具有尾部的大部分重量。本文对原方法进行了一次修改,即将λ设置为max(λ,1-λ); 这使混合偏向原始图像。

Beta分布

def mixup(a_x,a_y,b_x,b_y,alpha=0.75):
    l = np.random.beta(alpha,alpha)
    l = max(l,1-l)
    x = l * a_x + (1-l) * b_x
    y = l* a_y + (1-l) * b_y
    return x,y

Mixup

锐化

作者利用上述方程作为熵最小化的一种形式,强化模型对未标记数据的预测。如果温度T <1,则效果是使预测更加确定,并且随着T降至零,预测接近单热分布(参见下图)。这个相对简单的步骤,不涉及学习参数,对算法来说非常重要。在消融研究中,本文报告在去除锐化步骤(将T设置为1)时精度降低超过16%。

锐化随机分布

半监督学习中熵最小化背后的思想是分类器的决策边界不应该通过数据空间的高密度区域。如果是这种情况,边界将分割非常接近的数据。此外小扰动会导致预测发生很大变化。由于决策边界附近的预测更不确定,熵最小化试图使模型对其预测更有信心,从而使边界远离数据。虽然其他方法[3]为损失添加了熵项,但MixMatch通过上面的等式直接降低了未标记目标的熵。

def sharpen(p,T=0.5):
    u = p ** (1/T)
    return u / u.sum(dim=1,keepdim=True)

作为这种技术的一个例子,尝试一个比CIFAR - MNIST更简单,更容易可视化的分类问题。仍然会将500个随机示例作为标记的训练集,并将其余部分保留为未标记的集合。完整的图像用于训练,还将使用tSNE将每个图像缩小为两个维度以进行可视化。按照与MixMatch相同的方法对未标记数据进行半监督训练,将使用模型本身生成伪标签。该模型仅由两个卷积层和一个线性头组成。没有使用混淆或数据增强,因此可以隔离熵最小化的影响。损失函数也与MixMatch大致相同,使用交叉熵标记数据和未标记数据的均方误差(参见下面的损失部分,了解其背后的基本原理)。在不使用锐化的情况下训练上部图像,在下部图像中使用锐化的伪标签T = 0.5。对于十个时期的每个训练,未锐化的模型具有80.1%的测试精度,并且锐化的模型具有90.7%的准确度。在下面的图像中,颜色对应于预测的类别,并且标记大小与预测置信度成反比(较小的标记更有信心)。如标记尺寸所示,未锐化模型具有很多不确定性,尤其是在簇的边缘周围,而锐化模型在其预测中更加自信。

锐化对MNIST半监督训练的影响。使用tSNE将MNIST中的图像缩小为二维。颜色对应于预测的类别,并且标记大小与预测置信度成反比(较小的标记更有信心)。上图描绘了T = 1的训练,下图描绘了T = 0.5。

MixMatch算法

现在有了所有部分,就可以实现完整的算法。以下是单次训练迭代的步骤:

  1. 提供一批带标签的标签数据和一批未标记的数据。
  2. 增加标记批次以生成新的训练批次。
  3. 在未标记的批次中增加每个图像K次,以产生总共批量大小* K个新的未标记示例。
  4. 对于未标记批次中的每个原始图像,将K个扩充版本传递给模型。平均模型对增量的预测,以便为增强图像生成单个伪标签。
  5. 锐化伪标签。
  6. 增强的标记数据集及其标签形成集合X.增强的未标记数据及其(预测的)标签形成集合U.
  7. 连接将U和X设置为集合W. Shuffle W.
  8. 通过将mixup应用于集合X和| X |来形成集合X' W.的例子
  9. 表格设置U'通过应用mixup来设置U和W中未在步骤8中使用的示例。

然后将X'(标记为混合)和U'(未标记的混合)传递给模型,并使用相应的混合标签计算损失。

模型

将使用具有28层和生长因子2的宽resnet模型来匹配纸张。将使用fastai包含的WRN实现并匹配本文中使用的架构。

model = models.WideResNet(num_groups=3,N=4,num_classes=10,k=2,start_nf=32)

损失

有了数据和模型,现在将实现训练所需的最后一块 - 损失函数。损失函数是两个项的总和,标记和未标记的损失。标记的损失使用标准交叉熵; 然而,未标记的损失函数是l2损失。这是因为l2损失对非常不正确的预测不太敏感。交叉熵损失是无界的,并且随着模型的正确类别的预测概率变为零,交叉熵变为无穷大。然而由于l2损失,因为正在处理概率,最坏的情况是模型在目标为1时预测为0,反之亦然; 这导致丢失1.由于未标记的目标来自模型本身,该算法不希望过于严厉地惩罚不正确的预测。参数λ(l 在lambda被保留的代码中)控制两个术语之间的平衡。

将通过在前3000次迭代(大约10个时期)中线性地增加未标记损失的权重来略微偏离本文。在应用这种增加之前,模型训练存在困难; 发现早期时期的准确性会非常缓慢地增加。由于训练开始时的预测标签基本上是随机的,因此延迟应用未标记的损失是有意义的。当未标记的损失的权重变得显着时,该模型应该做出相当好的预测。

class MixupLoss(nn.Module):
    def forward(self, preds, target, unsort=None, ramp=None, bs=None):
        if unsort is None:
            return F.cross_entropy(preds,target)
        preds = preds[unsort]
        preds_l = preds[:bs]
        preds_ul = preds[bs:]
        preds_l = torch.log_softmax(preds_l,dim=1)
        preds_ul = torch.softmax(preds_ul,dim=1)
        loss_x = -(preds_l * target[:bs]).sum(dim=1).mean()
        loss_u = F.mse_loss(preds_ul,target[bs:])
        self.loss_x = loss_x.item()
        self.loss_u = loss_u.item()
        return loss_x + 75.0 * ramp * loss_u

训练

在训练之前,让回顾一下已经介绍的超参数。

超参数

该论文的作者声称T和K在大多数数据集中应相对恒定,而α和λ需要在每组中进行调整。将使用与论文官方实施相同的超参数值。

一个实现细节:该论文提到,它不是学习速率退火,而是用训练模型参数的指数移动平均值更新第二个模型。这是正则化的另一种形式,但对算法不是必不可少的。对于那些感兴趣的人,可以在存储库中使用EMA模型进行训练。然而与学习速率调度没有明显的好处,并且为了简单起见,将放弃EMA并使用fastai实施的单周期策略来安排学习和动量率。

将使用fastai的回调系统编写一个处理大多数MixMatch步骤的方法。此方法从已标记和未标记的集合中获取批次,获取预测的标签,然后执行混合。

class MixMatchTrainer(LearnerCallback):
    _order=-20
    def on_train_begin(self, **kwargs):
        self.l_dl = iter(data_labeled.train_dl)
        self.smoothL, self.smoothUL = SmoothenValue(0.98), SmoothenValue(0.98)
        self.recorder.add_metric_names(["l_loss","ul_loss"])
        self.it = 0
        
    def on_batch_begin(self, train, last_input, last_target, **kwargs):
        if not train: return
        try:
            x_l,y_l = next(self.l_dl)
        except:
            self.l_dl = iter(data_labeled.train_dl)
            x_l,y_l = next(self.l_dl)
            
        x_ul = last_input
        
        with torch.no_grad():
            ul_labels = sharpen(torch.softmax(torch.stack([self.learn.model(x_ul[:,i]) for i in range(x_ul.shape[1])],dim=1),dim=2).mean(dim=1))
            
        x_ul = torch.cat([x for x in x_ul])
        ul_labels = torch.cat([y.unsqueeze(0).expand(K,-1) for y in ul_labels])
        
        l_labels = torch.eye(data_labeled.c).cuda()[y_l]
        
        w_x = torch.cat([x_l,x_ul])
        w_y = torch.cat([l_labels,ul_labels])
        idxs = torch.randperm(w_x.shape[0])
        
        mixed_input, mixed_target = mixup(w_x,w_y,w_x[idxs],w_y[idxs])
        bn_idxs = torch.randperm(mixed_input.shape[0])
        unsort = [0] * len(bn_idxs)
        for i,j in enumerate(bn_idxs): unsort[j] = i
        mixed_input = mixed_input[bn_idxs]
    
 
        ramp = self.it / 3000.0 if self.it < 3000 else 1.0
        return {"last_input": mixed_input, "last_target": (mixed_target,unsort,ramp,x_l.shape[0])}
    
    def on_batch_end(self, train, **kwargs):
        if not train: return
        self.smoothL.add_value(self.learn.loss_func.loss_x)
        self.smoothUL.add_value(self.learn.loss_func.loss_u)
        self.it += 1
 
    def on_epoch_end(self, last_metrics, **kwargs):
        return add_metrics(last_metrics,[self.smoothL.smooth,self.smoothUL.smooth])   

fastai Learner对象包含数据加载器和模型,负责执行训练循环。它还具有许多实用功能,例如学习速率发现和预测解释。此实现中的一个时期是一次遍历整个未标记的数据集。

learn = Learner(data_unlabeled,model,loss_func=MixupLoss(),callback_fns=[MixMatchTrainer],metrics=accuracy)

结果

作为参考,这些测试是在具有16个CPU和单个P100 GPU的Google Compute Engine虚拟机上运行的。第一步是建立一些基线,以便比较MixMatch的性能。首先将尝试使用所有50k训练图像的完全监督的情况。接下来将仅训练500张带标签的图像,没有无监督的组件。最后将使用上一节中定义的学习者使用MixMatch进行训练。

结果

结论

MixMatch显然拥有令人印象深刻的性能,但缺点是训练的额外时间成本。与完全监督的情况相比,训练MixMatch的时间大约长2.5倍。其中一些可能是由于实现效率低下但产生多个增强然后获得标签的模型预测具有显着的成本,尤其是在一个GPU情况下。使用官方Tensorflow实现进行了比较,并验证了MixMatch需要很长时间才能完全收敛; 超过12小时的训练导致错误率比文件中报告的错误率高几个百分点。P100设置需要近36小时的训练才能完全匹配其结果。但是几个小时的训练将实现绝大多数的准确性改进,

虽然增强和锐化是非常有益的,但该论文的消融研究表明,单一最重要的组件,错误的是MixUp。就它为什么如此有效地运作而言,这也是最神秘的组成部分 - 为什么在图像之间的预测中强制执行线性有助于模型?当然它减少了训练数据的记忆,但数据增加也是如此,在这种情况下几乎没有相同的效果。即使是最初的MixUp论文也只提供关于其功效的非正式论据:

“这种线性行为可以在训练样例之外进行预测时减少不良振荡的数量。此外从奥卡姆剃刀的角度来看,线性是一种很好的归纳偏差,因为它是最简单的行为之一“[4]

其他研究已经扩展了这个想法,例如通过混合中间状态而不是输入[6],或者使用神经网络代替β函数来生成混合系数[5]。但是无法找到可靠的理论依据; 这是另一种落入“它只是工作”类别的技术。绘制生物类比很难 - 人类很难通过将其与不相关的概念相结合来学习概念。

MixMatch是一种非常有前景的方法,适用于计算机视觉以外的领域。看到它进一步理解和应用将是有趣的。

参考

[1]:Berthelot,David,Nicholas Carlini,Ian Goodfellow,Nicolas Papernot,Avital Oliver和Colin Raffel。“MixMatch:A综合方法半监督学习”的arXiv:1905.02249 [CS,统计],2019年5月6日

http://arxiv.org/abs/1905.02249

[2]:Tarvainen,Antti和Harri Valpola。“意思是说教师是更好的榜样:加权平均一致性目标提高半监督深层的学习效果。”的arXiv:1703.01780 [CS,统计],3月6日,2017年

http://arxiv.org/abs/1703.01780

[3]:Miyato,Takeru,Shin-ichi Maeda,Masanori Koyama和Shin Ishii。“虚拟对抗性训练:一个正则化方法监督,半监督学习。”的arXiv:1704.03976 [CS,统计],4月12日,2017年

http://arxiv.org/abs/1704.03976

[4]:Zhang,Hongyi,Moustapha Cisse,Yann N. Dauphin和David Lopez-Paz。“的mixup:超越经验风险最小化。”的arXiv:1710.09412 [CS,统计] 10月25日,2017年

http://arxiv.org/abs/1710.09412

[5]:郭宏宇,毛永义,张瑞东。“MixUp as Locally Linear Out-Of-Manifold Regularization,”nd,9。

[6]:Verma,Vikas,Alex Lamb,Christopher Beckham,Amir Najafi,Ioannis Mitliagkas,Aaron Courville,David Lopez-Paz和Yoshua Bengio。“流形混合:通过插入隐藏状态来表现更好”,2018年6月13日。

https://appxiv.org/abs/1806.05236v7

原文发布于微信公众号 - 相约机器人(xiangyuejiqiren)

原文发表时间:2019-06-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券