ESRGAN图像超分辨

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

github: https://github.com/xinntao/ESRGAN

ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks发表于ECCV 2018 的 Workshops,在SRGAN的基础上进行了改进,包括改进网络的结构,判决器的判决形式,以及更换了一个用于计算感知域损失的预训练网络

超分辨率生成对抗网络(SRGAN)是一项开创性的工作,能够在单一图像超分辨率中生成逼真的纹理。这项工作发表于CVPR 2017,

文章链接:Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network

但是,放大后的细节通常伴随着令人不快的伪影。为了更进一步地提升视觉质量,作者仔细研究了SRGAN的三个关键部分:1.网络结构 2.对抗性损失 3.感知域损失;并对每一项进行改进,得到ESRGAN。具体而言,文章提出了一种Residual-in-Residual Dense Block (RRDB)的网络单元,在这个单元中,去掉了BN(Batch Norm)层。此外,作者借鉴了relativistic GAN的想法,让判别器预测图像的真实性而不是图像“是否是fake图像”。最后,文章对感知域损失进行改进,使用激活前的特征,这样可以为亮度一致性和纹理恢复提供更强的监督。在这些改进的帮助下,ESRGAN得到了更好的视觉质量以及更逼真和自然的纹理。

在纹理和细节上,ESRGAN都优于SRGAN

SRGAN的思考与贡献

现有的超分辨率网络在不同的网络结构设计以及训练策略下,超分辨的效果得到了很大的提升,特别是PSNR指标。但是,基于PSNR指标的模型会倾向于生成过度平滑的结果,这些结果缺少必要的高频信息。PSNR指标与人类观察者的主观评价从根本上就不统一。

一些基于感知域信息驱动的方法已经提出来用于提升超分辨率结果的视觉质量。例如,感知域的损失函数提出来用于在特征空间(instead of 像素空间)中优化超分辨率模型;生成对抗网络通过鼓励网络生成一些更接近于自然图像的方法来提升超分辨率的质量;语义图像先验信息用于进一步改善恢复的纹理细节。

通过结合上面的方法,SRGAN模型极大地提升了超分辨率结果的视觉质量。但是SRGAN模型得到的图像和GT图像仍有很大的差距。

ESRGAN的改进

文章对这三点做出改进:1.网络的基本单元从基本的残差单元变为Residual-in-Residual Dense Block (RRDB);2.GAN网络改进为Relativistic average GAN (RaGAN);3.改进感知域损失函数,使用激活前的VGG特征,这个改进会提供更尖锐的边缘和更符合视觉的结果。

网络结构及思想

生成器部分

首先,作者参考SRResNet结构作为整体的网络结构,SRResNet的基本结构如下:

SRResNet基本结构

为了提升SRGAN重构的图像的质量,作者主要对生成器G做出如下改变:1.去掉所有的BN层;2.把原始的block变为Residual-in-Residual Dense Block (RRDB),这个block结合了多层的残差网络和密集连接。

如下图所示:

RRDB

思想:

BN层的影响

对于不同的基于PSNR的任务(包括超分辨率和去模糊)来说,去掉BN层已经被证明会提高表现和减小计算复杂度。BN层在训练时,使用一个batch的数据的均值和方差对该batch特征进行归一化,在测试时,使用在整个测试集上的数据预测的均值和方差。当训练集和测试集的统计量有很大不同的时候,BN层就会倾向于生成不好的伪影,并且限制模型的泛化能力。作者发现,BN层在网络比较深,而且在GAN框架下进行训练的时候,更会产生伪影。这些伪影偶尔出现在迭代和不同的设置中,违反了对训练稳定性能的需求。所以为了稳定的训练和一致的性能,作者去掉了BN层。此外,去掉BN层也能提高模型的泛化能力,减少计算复杂度和内存占用。

Trick:

除了上述的改进,作者也使用了一些技巧来训练深层网络:1.对残差信息进行scaling,即将残差信息乘以一个0到1之间的数,用于防止不稳定;2.更小的初始化,作者发现当初始化参数的方差变小时,残差结构更容易进行训练。

判别器部分

除了改进的生成器,作者也基于Relativistic GAN改进了判别器。判别器 D 使用的网络是 VGG 网络,SRGAN中的判别器D用于估计输入到判别器中的图像是真实且自然图像的概率,而Relativistic判别器则尝试估计真实图像相对来说比fake图像更逼真的概率。

具体而言,作者把标准的判别器换成Relativistic average Discriminator(RaD),所以判别器的损失函数定义为:

求均值的操作是通过对mini-batch中的所有数据求平均得到的,xf是原始低分辨图像经过生成器以后的图像。

可以观察到,对抗损失包含了xr和xf,所以这个生成器受益于对抗训练中的生成数据和实际数据的梯度,这种调整会使得网络学习到更尖锐的边缘和更细节的纹理。

感知域损失

文章也提出了一个更有效的感知域损失,使用激活前的特征(VGG16网络)。

感知域的损失当前是定义在一个预训练的深度网络的激活层,这一层中两个激活了的特征的距离会被最小化。与此相反,文章使用的特征是激活前的特征,这样会克服两个缺点。第一,激活后的特征是非常稀疏的,特别是在很深的网络中。这种稀疏的激活提供的监督效果是很弱的,会造成性能低下;第二,使用激活后的特征会导致重建图像与GT的亮度不一致。

使用激活前与激活后的特征的比较,(a)亮度;(b)细节

作者对使用的感知域损失进行了探索。与目前多数使用的用于图像分类的VGG网络构建的感知域损失相反,作者提出一种更适合于超分辨的感知域损失,这个损失基于一个用于材料识别的VGG16网络(MINCNet),这个网络更聚焦于纹理而不是物体。尽管这样带来的增益很小,但作者仍然相信,探索关注纹理的感知域损失对超分辨至关重要。

损失函数

经过上面对网络模块的定义和构建以后,再定义损失函数,就可以进行训练了。

对于生成器G,它的损失函数为:

代码解析:

https://zhuanlan.zhihu.com/p/54473407?utm_id=0

3.提取感知域损失的网络(Perceptual Network)

文章使用了一个用于材料识别的VGG16网络(MINCNet)来提取感知域特征,定义如下:

class MINCNet(nn.Module):
    def __init__(self):
        super(MINCNet, self).__init__()
        self.ReLU = nn.ReLU(True)
        self.conv11 = nn.Conv2d(3, 64, 3, 1, 1)
        self.conv12 = nn.Conv2d(64, 64, 3, 1, 1)
        self.maxpool1 = nn.MaxPool2d(2, stride=2, padding=0, ceil_mode=True)
        self.conv21 = nn.Conv2d(64, 128, 3, 1, 1)
        self.conv22 = nn.Conv2d(128, 128, 3, 1, 1)
        self.maxpool2 = nn.MaxPool2d(2, stride=2, padding=0, ceil_mode=True)
        self.conv31 = nn.Conv2d(128, 256, 3, 1, 1)
        self.conv32 = nn.Conv2d(256, 256, 3, 1, 1)
        self.conv33 = nn.Conv2d(256, 256, 3, 1, 1)
        self.maxpool3 = nn.MaxPool2d(2, stride=2, padding=0, ceil_mode=True)
        self.conv41 = nn.Conv2d(256, 512, 3, 1, 1)
        self.conv42 = nn.Conv2d(512, 512, 3, 1, 1)
        self.conv43 = nn.Conv2d(512, 512, 3, 1, 1)
        self.maxpool4 = nn.MaxPool2d(2, stride=2, padding=0, ceil_mode=True)
        self.conv51 = nn.Conv2d(512, 512, 3, 1, 1)
        self.conv52 = nn.Conv2d(512, 512, 3, 1, 1)
        self.conv53 = nn.Conv2d(512, 512, 3, 1, 1)

    def forward(self, x):
        out = self.ReLU(self.conv11(x))
        out = self.ReLU(self.conv12(out))
        out = self.maxpool1(out)
        out = self.ReLU(self.conv21(out))
        out = self.ReLU(self.conv22(out))
        out = self.maxpool2(out)
        out = self.ReLU(self.conv31(out))
        out = self.ReLU(self.conv32(out))
        out = self.ReLU(self.conv33(out))
        out = self.maxpool3(out)
        out = self.ReLU(self.conv41(out))
        out = self.ReLU(self.conv42(out))
        out = self.ReLU(self.conv43(out))
        out = self.maxpool4(out)
        out = self.ReLU(self.conv51(out))
        out = self.ReLU(self.conv52(out))
        out = self.conv53(out)
        return out

再引入预训练参数,就可以进行特征提取:

class MINCFeatureExtractor(nn.Module):
    def __init__(self, feature_layer=34, use_bn=False, use_input_norm=True, \
                device=torch.device('cpu')):
        super(MINCFeatureExtractor, self).__init__()

        self.features = MINCNet()
        self.features.load_state_dict(
            torch.load('../experiments/pretrained_models/VGG16minc_53.pth'), strict=True)
        self.features.eval()
        # No need to BP to variable
        for k, v in self.features.named_parameters():
            v.requires_grad = False

    def forward(self, x):
        output = self.features(x)
        return output

网络插值思想

为了平衡感知质量和PSNR等评价值,作者提出了一个灵活且有效的方法—网络插值。具体而言,作者首先基于PSNR方法训练的得到的网络G_PSNR,然后再用基于GAN的网络G_GAN进行finetune。

然后,对这两个网络相应的网络参数进行插值得到一个插值后的网络G_INTERP:

这样就可以通过 α 值来调整效果

训练细节

训练细节

放大倍数:4,mini-batch:16

通过Matlab的bicubic函数对HR图像进行降采样得到LR图像。

HR patch大小:128×128(实验发现使用大的patch时,训练一个深层网络效果会更好,因为一个增大的感受域会帮助模型捕捉更具有语义的信息)

训练过程:

1.训练一个基于PSNR指标的模型(L1 Loss)

初始化学习率:2×1e-4

每200000个mini-batch学习率除以2

2.以1中训练的模型作为生成器的初始化

λ=5×10−3,η=0.01,β=0.2 (残差scaling系数)

初始学习率:1e-4,并在50k,100k,200k,300k迭代后减半。

一个基于像素损失函数进行优化的预训练模型会帮助基于GAN的模型生成更符合视觉的结果,原因如下:1.可以避免生成器不希望的局部最优;2.再预训练以后,判别器所得到的输入图像的质量是相对较好的,而不是完全初始化的图像,这样会使判别器更关注到纹理的判别。

优化器:Adam( β1=0.9,β2=0.999 );交替更新生成器和判别器,直到收敛。

生成器的设置:1.16层(基本的残差结构) 2.23层(RDDB)

数据集:DIV2K,Flickr2K,OST;(有丰富纹理信息的数据集会是模型产生更自然的结果)

可以看到,ESRGAN得到的图像PSNR值不高,但是从视觉效果上看会更好,Percpetual Index值更小(越小越好),而且ESRGAN在 PIRM-SR 竞赛上也获得了第一名(在Percpetual Index指标上)。

经过实验以后,作者得出结论:

1.去掉BN:并没有降低网络的性能,而且节省了计算资源和内存占用。而且发现当网络变深变复杂时,带BN层的模型更倾向于产生影响视觉效果的伪影。

2.使用激活前的特征:得到 的图像的亮度更准确,而且可以产生更尖锐的边缘和更丰富的细节。

3.RaGAN:产生更尖锐的边缘和更丰富的细节。

4.RDDB:更加提升恢复得到的纹理(因为深度模型具有强大的表示能力来捕获语义信息),而且可以去除噪声。

网络插值实验

为了平衡视觉效果和PSNR等性能指标,作者对网络插值参数 α 的取值进行了实验,结果如下:

总结

文章提出的ESRGAN在SRGAN的基础上做出了改进,包括去除BN层,基本结构换成RDDB,改进GAN中判别器的判别目标,以及使用激活前的特征构成感知域损失函数,实验证明这些改进对提升输出图像的视觉效果都有作用。此外,作者也使用了一些技巧来提升网络的性能,包括对残差信息的scaling,以及更小的初始化。最后,作者使用了一种网络插值的方法来平衡输出图像的视觉效果和PSNR等指标值。

SRGAN —使用GAN进行图像高分辨的开山之作

论文: https://arxiv.org/abs/1609.04802(CVPR 2017)

这篇文章第一次将将生成对抗网络用在了解决超分辨率问题上。将GAN引入SR领域

之前超分的研究虽然主要聚焦于“恢复细粒度的纹理细节”这个问题上,但将问题一直固定在最大化峰值信噪比(Peak Signal-to-Noise Ratio, PSNR)上,等价于 最小化与GT图像的均方重建误差(mean squared reconstruction error, MSE)。

而这也就导致:

  1. 高频细节(high-frequency details) 的丢失,整体图像过于平滑/模糊;
  2. 与人的视觉感知不一致,超分图像的精确性与人的期望不匹配(人可能更关注前景,而对背景清晰度要求不高)。
中间蓝色框是基于MSE所学到的超分图像所在像素空间,红色框是真实超分图像所在的像素空间流形,基于GAN的方法驱动重构图像往真实图像像素流形区域靠近,从而感知上更真实可信

从而提出3个改进:

  1. 新的backbone:SRResNet;
  2. GAN-based network 及 新的损失函数:
  3. adversarial loss:提升真实感(photo-realistic natural images);
  4. content loss:获取HR image和生成图像的感知相似性(perceptual similarity),而不只是像素级相似性(pixel similarity);或者说特征空间的相似性而不是像素空间的相似性。
  5. 使用主观评估手段:MOS,更加强调人的感知。

SRGAN算法改进细节:

生成网络是新的结构SRResNet(横跨主干网络的skip connection操作很关键),卷积核尺寸k,输出通道数n,步长s。头部后续接了两个输出通道数为256=64*4的卷积块,因为其中PixelShuffle*2会将feature map转化 (64, H*2, W*2)的输出(sub-pixel convolution操作),这样总共upscale *4
  • SRResNet和GAN-based Network

上图就是新的网络结构,G网络是SRResNet,论文使用了16个residual blocks;D网络为8次卷积操作(4次步长为2)+2次全连接层的VGG网络。

损失函数

生成网络的损失函数为:

包含:

论文对VGG高层特征和低层特征分别做了实验,最终选择可能关注更多图像内容的高层特征作为论文实验的损失特征图。

判别网络的损失函数为二分类交叉熵损失函数:

SRGAN实验设置

使用数据集Set5,Set14,BSD100,BSD300测试集对训练模型进行实验评估: 4×分辨率超分,然后对图像的每个边界移除4个像素点,最后center-cropped计算PSNR和SSIM,进行有效性统计分析。

随机采样ImageNet数据集中350张图像进行训练(参考源码):

SRGAN实验结果及分析

消融实验说明:

  1. skip-connection结构的有效性;
  2. PSNR体现不出人的感知(MOS);
  3. GAN-based Network能更好捕捉一些人的感知细节(高频信息?),MOS更高;
  4. VGG特征重建也有助于捕捉图像的部分感知细节。

联邦图机器学习

近年来,图已被广泛应用于表示和处理很多领域的复杂数据,如医疗、交通运输、生物信息学和推荐系统等。图机器学习技术是获取隐匿在复杂数据中丰富信息的有力工具,并且在像节点分类和链接预测等任务中,展现出很强的性能。
尽管图机器学习技术取得了重大进展,但大多数都需要把图数据集中存储在单机上。然而,随着对数据安全和用户隐私的重视,集中存储数据变的不安全和不可行。图数据通常分布在多个数据源(数据孤岛),由于隐私和安全的原因,从不同的地方收集所需的图数据变的不可行。
例如一家第三方公司想为一些金融机构训练图机器学习模型,以帮助他们检测潜在的金融犯罪和欺诈客户。每个金融机构都拥有私有客户数据,如人口统计数据以及交易记录等。每个金融机构的客户形成一个客户图,其中边代表交易记录。由于严格的隐私政策和商业竞争,各个机构的私有客户数据无法直接与第三方公司或其它他机构共享。同时,机构之间也可能有关联,这可以看作是机构之间的结构信息。因此面临的主要挑战是:在不直接访问每个机构的私有客户数据的情况下,基于私有客户图和机构间结构信息,来训练用于金融犯罪检测的图机器学习模型。
联邦学习(FL)是一种分布式机器学习方案,通过协作训练解决数据孤岛问题。它使参与者(即客户)能够在不共享其私有数据的情况下联合训练机器学习模型。因此,将 FL 与图机器学习相结合成为解决上述问题的有希望的解决方案。
本文中,来自弗吉尼亚大学的研究者提出联邦图机器学习(FGML,Federated Graph Machine Learning)。一般来说,FGML 可以根据结构信息的级别分为两种设置:
第一种是具有结构化数据的 FL,在具有结构化数据的 FL 中,客户基于其图数据协作训练图机器学习模型,同时将图数据保留在本地。
第二种是结构化 FL,在结构化 FL 中,客户端之间存在结构信息,形成客户端图。可以利用客户端图设计更有效的联合优化方法。

论文地址:https://arxiv.org/pdf/2207.11812.pdf
虽然 FGML 提供了一个有前景的蓝图,但仍存在一些挑战:
1、跨客户端的信息缺失。在具有结构化数据的 FL 中,常见的场景是每个客户端机器都拥有全局图的子图,并且一些节点可能具有属于其他客户端的近邻。出于隐私考虑,节点只能在客户端内聚合其近邻的特征,但无法访问位于其它客户端上的特征,这导致节点表示不足。
2、图结构的隐私泄漏。在传统 FL 中,不允许客户端公开其数据样本的特征和标签。在具有结构化数据的 FL 中,还应考虑结构信息的隐私。结构信息可以通过共享邻接矩阵直接公开,也可以通过传输节点嵌入间接公开。
3、跨客户端的数据异构性。与传统 FL 中数据异构性来自 non-IID 数据样本不同,FGML 中的图数据包含丰富的结构信息。同时,不同客户的图结构也会影响图机器学习模型的性能。 4、参数使用的策略。在结构化 FL 中,客户端图使客户端能够从其相邻客户端获取信息。在结构化 FL 中,需要设计有效的策略,以充分利用由中心服务器协调或完全分散的近邻信息。
为了应对上述挑战,研究人员开发了大量算法。目前各种算法主要关注标准 FL 中的挑战和方法,只有少数人尝试解决 FGML 中的具体问题和技术。有人发表对 FGML 进行分类的综述性论文,但没有总结 FGML 中的主要技术。而有的综述文章仅涵盖了 FL 中数量有限的相关论文,并非常简要地介绍了目前现有的技术。

而在今天介绍的这篇论文中,作者首先介绍 FGML 中两种问题设计的概念。然后,回顾了每种 shezhi 下的最新的技术进展,还介绍了 FGML 的实际应用。并对可用于 FGML 应用的可访问图数据集和平台进行总结。最后,作者给出了几个有前途的研究方向。文章的主要贡献包括:
FGML 技术分类:文章给出了基于不同问题的 FGML 分类法,并总结了每个设置中的关键挑战。
全面的技术回顾:文章全面概述了 FGML 中的现有技术。与现有其它综述性论文相比,作者不仅研究了更广泛的相关工作,而且提供了更详细的技术分析,而不是简单地列出每种方法的步骤。
实际应用:文章首次总结 FGML 的实际应用。作者根据应用领域对其进行分类,并介绍每个领域中的相关工作。
数据集和平台:文章介绍了 FGML 中现有的数据集和平台,对于想在 FGML 中开发算法和部署应用程序的工程师和研究人员非常有帮助。
未来方向:文章不仅指出了现有方法的局限性,而且给出了 FGML 未来的发展方向。

这里对文章的主要结构做下简介。第 2 节简要介绍了图机器学习中的定义以及 FGML 中两种设置的概念和挑战。第 3 节和第 4 节回顾了这两种设置中的主流技术。第 5 节进一步探讨了 FGML 在现实世界中的应用。第 6 节介绍了相关 FGML 论文中使用的开放图数据集和 FGML 的两个平台。在第 7 节中提供了未来可能的发展方向。最后第 8 节对全文进行了总结。
更多详细信息请参考原论文。

用语言模型学习表示蛋白质的功能特性

以数据为中心的方法已被用于开发用于阐明蛋白质未表征特性的预测方法;然而,研究表明,这些方法应进一步改进,以有效解决生物医学和生物技术中的关键问题,这可以通过更好地代表手头的数据来实现。新的数据表示方法主要从在自然语言处理方面取得突破性改进的语言模型中汲取灵感。最近,这些方法已应用于蛋白质科学领域,并在提取复杂的序列-结构-功能关系方面显示出非常有希望的结果。在这项研究中,土耳其中东科技大学(Middle East Technical University)的研究人员,首先对每种方法进行分类/解释,然后对它们的预测性能进行基准测试,对蛋白质表示学习进行了详细调查:(1)蛋白质之间的语义相似性,(2)基于本体的蛋白质功能,(3)药物靶蛋白家族和(4)突变后蛋白质-蛋白质结合亲和力的变化。这项研究的结论将有助于研究人员将基于机器/深度学习的表示技术应用于蛋白质数据以进行各种预测任务,并激发新方法的发展。该研究以「Learning functional properties of proteins with language models」为题,于 2022 年 3 月 21 日发布在《Nature Machine Intelligence》。

蛋白质科学是一门广泛的学科,它通过实验室实验(即蛋白质组学)和计算方法(例如分子建模、机器学习、数据科学)分析单个蛋白质以及生物体的整个蛋白质组,最终创建准确且可重复使用的方法用于生物医学和生物技术。蛋白质信息学可以定义为蛋白质科学的计算和以数据为中心的分支,通过它对蛋白质的定量方面进行建模。蛋白质的功能表征对于开发新的有效的生物医学策略和生物技术产品至关重要。截至 2021 年 5 月,UniProt 蛋白质序列和注释知识库中约有 2.15 亿条蛋白质条目;然而,其中只有 56 万份(约 0.26%)由专家手动审查和注释,这表明当前的排序(数据生产)和注释(标签)能力之间存在很大差距。这种差距主要是由于从湿实验室实验及其手动管理中获得结果的成本较高,同时具有时间密集性。为了补充基于实验和管理的注释,使用计算机方法势在必行。在这种情况下,许多研究小组一直致力于开发新的计算方法来预测蛋白质的酶活性、生物物理特性、蛋白质和配体相互作用、三维结构以及最终的功能。蛋白质功能预测(PFP)可以定义为自动或半自动地将功能定义分配给蛋白质。生物分子功能的主要术语被编入基因本体论(GO)系统;这是一个概念的分层网络,用于注释基因和蛋白质的分子功能,以及它们的亚细胞定位和它们所涉及的生物过程。PFP 最全面的基准项目是功能注释的关键评估(CAFA)挑战;在该项目中,参与者预测一组目标蛋白的基于 GO 的功能关联,这些目标蛋白的功能后来通过手动调节确定,用于评估参与预测因子的性能;迄今为止的 CAFA 挑战表明,PFP 仍然是一个开放的问题。以前的研究已经表明,复杂的计算问题,其中特征是高维的并且具有复杂/非线性关系,适合基于深度学习的技术。这些技术可以有效地从嘈杂的高维输入数据中学习与任务相关的表示。因此,深度学习已成功应用于计算机视觉、自然语言处理和生命科学等各个领域。生物分子的特征(例如,基因、蛋白质、RNA 等)应被提取并编码为定量/数值向量(即表示),以用于基于机器/深度学习的预测建模。给定生物分子的原始和高维输入特征,表示模型将该特征向量计算为该生物分子的简洁和正交表示。经过优化训练的监督预测系统可以有效地学习数据集中样本的特征,并使用这些表示作为输入来执行预测任务(例如,序列上的 DNA 结合区域、生化特性、亚细胞定位等)。蛋白质表示方法可以分为两大类;(1)经典表示(即模型驱动的方法),使用预定义的属性规则生成,例如基因/蛋白质之间的进化关系或氨基酸的物理化学性质,以及(2)数据驱动的表示,使用统计和机器学习算法(例如人工神经网络)构建,这些算法针对预定义任务进行训练,例如预测序列上的下一个氨基酸。之后,训练模型的输出——即表示特征向量——可以用于其他与蛋白质信息学相关的任务,例如功能预测。从这个意义上说,表示学习模型利用了知识从一个任务到另一个任务的转移。这个过程的广义形式被称为迁移学习,据报道它在时间和成本方面是一种高效的数据分析方法。因此,蛋白质表示学习模型最大限度地减少了对数据标记的需求。蛋白质表示学习是一个年轻但高度活跃的研究领域,主要受到自然语言处理 (NLP) 方法的启发。因此,蛋白质表示学习方法在文献中经常被称为蛋白质语言模型。之前的研究表明,各种蛋白质表示学习方法,尤其是那些结合了深度学习的方法,已经成功地提取了蛋白质的相关固有特征。参见:https://www.nature.com/articles/s42256-022-00457-9/tables/1尽管有研究评估学习的蛋白质表示模型,但需要进行全面的调查和基准测试,以便在学习蛋白质的多个方面(包括基于本体的功能定义、语义关系、家族和相互作用)的背景下系统地评估这些方法。在新的研究中,中东科技大学的研究人员对自 2015 年以来提出的可用蛋白质表示学习方法进行了全面调查,并通过详细的基准分析测量了这些方法捕获蛋白质功能特性的潜力。涵盖了经典和基于人工学习的方法,并深入了解了它们各自代表蛋白质的方法。研究人员根据它们的技术特征和应用对这些方法进行分类。为了评估每个表示模型在多大程度上捕获了功能信息的不同方面,该团队构建并应用了基于以下的基准:(1) 蛋白质之间的语义相似性推断,(2) 基于本体的 PFP,(3) 药物靶蛋白家族分类,(4) 蛋白质-蛋白质结合亲和力估计。

图示:研究的示意图。(来源:论文)此外,该团队还提供了相关的基准测试软件(Protein Representation Benchmark, PROBE),它允许人们轻松评估任何表示方法在该团队定义的四个基准测试任务中的性能。研究人员希望该工作能够,为希望将基于机器/深度学习的表示技术,应用于生物分子数据进行预测建模的研究人员提供信息。也希望这项研究能够激发新的想法,以开发新颖、复杂和强大的以数据为中心的方法来解决蛋白质科学中的开放问题。基于表示学习的方法在蛋白质功能分析中的表现通常优于经典方法在该团队所有的基准测试中,观察到学习表示(尤其是大型模型)在预测性能方面优于经典模型,证实了基于人工学习的数据驱动方法在表示生物分子的功能特性方面的优势。另一方面,在 PFP 预测基准的分子功能类别中,HMMER 是一种基于隐马尔可夫模型(HMM)的生物分子相似性检测和功能注释的经典方法,可以与基于深度学习的蛋白质表示方法竞争。该结果与先前的研究一致,即序列相似性与蛋白质的生化特性高度相关,以至于使用此特征的简单矢量表示几乎可以执行复杂的序列建模方法。鉴于这些结果,研究人员表示,将同源信息明确纳入表征学习模型的训练可能会导致考虑到预测性能的改进。这从基于深度学习的高性能蛋白质结构预测器(例如 RoseTTAFold 和 AlphaFold2)中也很明显,它们使用多个序列比对来显着丰富基于序列的输入。他们认为,在当前状态下,学习到的蛋白质表示对于其他原因也是必不可少的。模型设计和训练数据类型/来源是表征学习的关键因素蛋白质表征学习中最关键的因素之一是表征模型的设计。例如,在这里的基准测试中,包含了两种类型的 BERT 模型。TAPE-BERT-PFAM 接受了 3200 万个蛋白质结构域序列的训练。ProtBERT-BFD 训练有 21 亿个宏基因组序列片段;然而,这两者之间的性能差异是微不足道的。另一方面,使用相同 2.1B 数据集(例如 ProtT5-XL)训练的更复杂的模型在大多数基准测试中表现出更好的性能。因此,研究人员认为模型设计/架构是最重要的(与这些方法的设计/架构相关的信息在方法中给出,并在结果部分就预测性能进行了讨论)。关于训练数据源的另一个发现是,合并多种数据类型可能会在与功能相关的预测任务中带来更好的性能。例如,AAC 和 APAAC 都使用氨基酸组成;然而,APAAC 还在其表示模型中添加了物理化学特性,并且在语义相似性推断和 PFP 基准测试中表现得更好。同样,Mut2Vec 结合了突变配置文件、PPI 和文本数据,并取得了最佳性能;尤其是在语义相似性推理基准测试中。在蛋白质表征学习方法的构建和评估过程中应考虑潜在的数据泄漏数据泄漏可以定义为机器学习方法的训练和验证阶段之间的知识意外泄漏,导致性能测量过于乐观,是性能测试期间应考虑的关键问题。研究人员分析发现,某些表示模型在与这些模型预训练的任务生物学相关的任务中表现良好;尽管数据和实际任务彼此不同。蛋白质表征学习的现状和挑战蛋白质表示学习领域存在一些挑战。尽管大多数蛋白质表示学习模型(迄今为止提出的)都是源自 NLP 模型(基于 LSTM/transformer 的深度学习模型),但建模语言和蛋白质的问题之间存在结构差异。据估计,一个以美国为母语的成年英语使用者,平均使用 46,200 个词条和多词表达;然而,蛋白质中只有 20 种不同的氨基酸,它们被表示模型以类似于语言的引理的方式处理。这些 NLP 模型为每个单词计算一个表示向量。类似地,当这种方法应用于蛋白质序列数据时,会为每个氨基酸计算一个表示向量。这些向量被汇集起来,为每个句子/文档和蛋白质创建固定大小的向量,分别用于 NLP 和蛋白质信息学任务。因此,与 NLP 相比,蛋白质表示中的少量构建块(即 20 个氨基酸)可能为较小的模型在与蛋白质表示学习领域中的较大模型竞争时带来优势。因此,鼓励对蛋白质序列特异性学习模型进行更多研究。

图示:蛋白质语义相似性推理基准结果。(来源:论文)模型的可解释性对于理解模型为何如此行事至关重要。在可解释表示中,所有特征都以隔离形式编码,这意味着向量上每个位置对应的特征是已知的;然而,该研究中研究的大多数学习蛋白质表示是不可解释/可解释的。例如,蛋白质中 TIM 桶结构的存在可能在其表示向量的第五位编码,而分子量信息可能在第三和第四位之间共享。一般来说,在数据科学领域,解缠结研究试图将样本的真实属性与输出向量的各个位置联系起来。蛋白质表征的解开是一个新课题,迄今为止只有少数表征模型开发人员探讨了这个问题。因此,尚不存在系统方法,并且需要新的框架来标准化评估蛋白质表示模型的可解释性。迄今为止提出的大多数蛋白质表示模型仅使用一种类型的数据(例如蛋白质序列)进行训练。然而,蛋白质知识与多种类型的生物信息相关,例如 PPI、翻译后修饰、基因/蛋白质(共)表达等;只有少数可用的蛋白质表示模型使用了多种类型的数据。在该团队的基准研究时所涉及的方法中,Mut2Vec 就是一个这样的例子,它结合了 PPI、突变和生物医学文本,并且比 GO BP 和基于 CC 的 PFP 中的许多仅基于序列的表示产生了更准确的结果。研究人员建议整合其他类型的蛋白质相关数据,尤其是进化关系,可能会进一步提高预测任务的准确性。MSA-Transformer 和无向图模型(例如 DeepSequence)通过深度学习利用同源信息。DeepSequence 使用 MSA 的后验分布计算潜在因子,而 MSA-Transformer 使用基于行和列的注意力来结合 MSA 和蛋白质语言模型。尽管 MSA-Transformer 在基准测试中表现出平均性能,但在之前的文献中,发现它在二级结构和接触预测任务上是成功的,这表明 MSA-Transformer 具有捕捉进化关系的能力。与此相关的是,文献中明确要求能够从广义的角度有效地表示蛋白质的整体蛋白质载体,用于各种不同的蛋白质信息学相关目的。研究人员认为,可以通过连接多个先前使用不同类型的生物数据独立构建的表示向量来创建这些整体表示,并使用这些向量的集成版本训练新模型以用于高级监督任务,例如预测生物过程和/或复杂的结构特征。构建这些整体表示的另一种方法是通过图表示学习直接学习整合多种蛋白质关系(例如,其他蛋白质、配体、疾病、表型、功能、途径等)的异构图。

图示:基于本体的蛋白质功能预测基准结果。(来源:论文)蛋白质表示学习方法可用于设计新蛋白质蛋白质设计是生物技术的主要挑战之一。合理的蛋白质设计涉及评估许多不同替代序列/结构的活性和功能,以为实验验证提供最有希望的候选者,这可以看作是一个优化问题。为此目的要探索的序列空间是巨大的。例如,人类蛋白质的平均长度约为 350 个氨基酸,其中存在 20^350 种不同的组合,尽管其中大多数是非功能性序列。在过去的几十年中,计算方法已被用于蛋白质设计,并且这些方法已经产生了有希望的结果,特别是在酶设计、蛋白质折叠和组装以及蛋白质表面设计方面。因此已经开发出高效的抗体和生物传感器。其中一些方法使用量子力学计算、分子动力学和统计力学,每种方法的计算成本都非常高,并且需要专业知识。类似的缺点也表现在主流的蛋白质设计软件,如 Rosetta。最近的研究表明,基于人工学习的生成建模可用于从头蛋白质设计。在机器学习领域,生成建模与判别建模相反,是一种生成合成样本的方法,这些样本服从从真实样本中学习到的概率分布。这是通过有效地学习训练数据集中样本的表示来实现的。

图示:药物靶蛋白家族分类基准结果。(来源:论文)深度学习最近已成为生成模型架构的关键方法,并已应用于包括蛋白质/肽设计在内的各个领域。例如,Madani 团队使用蛋白质语言模型从头开始设计属于不同蛋白质家族的新功能蛋白质,并通过湿实验室实验验证了他们的设计。这些研究表明,表示学习对于蛋白质和配体(药物)设计中的新应用至关重要。

图示:蛋白质-蛋白质结合亲和力估计基准结果。(来源:论文)研究人员相信蛋白质表示学习方法将在不久的将来对蛋白质科学的各个领域产生影响,并在现实世界中应用,这要归功于它们在输入级别集成异构蛋白质数据(即理化性质/属性、功能注释等)的灵活性,以及它们有效提取复杂潜在特征的能力。论文链接:https://www.nature.com/articles/s42256-022-00457-9

CE Loss 与 BCE Loss (分类问题损失函数)

有两个问题曾困扰着我:

  1. 为何MSE loss是一种回归问题的loss,不可以用在分类问题?而非要用CE或BCE呢?
  2. 为何CE与softmax激活函数搭配,而BCE与sigmoid搭配?有什么理由?

在学习过后,我发现这个问题在数学上有多种理解的角度,而结论却是一致的。在这里我梳理出一种角度,在未来如果有新的理解再进行补充。

该思路学习自李宏毅老师的机器学习课程

这说明,如果用MSE loss来训练分类问题,不论预测接近真实值或是接近错误值,梯度都很小。这也就解释了为何我们需要CE或BCE损失来处理分类问题。

2. BCE 损失函数

既然在分类问题中,MSE损失函数的梯度不能满足需要,现在我们来推导BCE损失函数的梯度。

3. CE 损失函数

江湖上流传一句话,交叉熵损失可以采用“sigmoid+BCE”或是“softmax+CE”。对于前者,上一部分已经对其回传的梯度进行了推导,证实了其合理性。

在这一部分我们对“softmax+CE”的梯度进行推导。

4. 应用

在Pytorch中,“sigmoid+BCE”对应的是torch.nn.BCEWithLogitsLoss,而“softmax+CE”对应的是torch.nn.CrossEntropyLoss

具体参数和用法可以参考 BCEWithLogitsLoss 和 CrossEntropyLoss

在分类问题中,如果遇到类别间不互斥的情况,只能采用“sigmoid+BCE”;

如果遇到类别间互斥的情况(只能有一类胜出),“sigmoid+BCE”化为多个二分类问题与“softmax+CE”直接进行分类都是有被用到的方法。

Softmax函数和Sigmoid函数的区别与联系

1. 前言

对于Softmax函数和Sigmoid函数,我们分为两部分讲解,第一部分:对于分类任务,第二部分:对于二分类任务(详细讲解)。

优点:1. Sigmoid函数的输出在(0,1)之间,输出范围有限,优化稳定,可以用作输出层。2. 连续函数,便于求导。

缺点:1. 最明显的就是饱和性,从上图也不难看出其两侧导数逐渐趋近于0,容易造成梯度消失。2.激活函数的偏移现象。Sigmoid函数的输出值均大于0,使得输出不是0的均值,这会导致后一层的神经元将得到上一层非0均值的信号作为输入,这会对梯度产生影响。 3. 计算复杂度高,因为Sigmoid函数是指数形式。

2.2 Softmax函数

Softmax =多类别分类问题=只有一个正确答案=互斥输出(例如手写数字,鸢尾花)。构建分类器,解决只有唯一正确答案的问题时,用Softmax函数处理各个原始输出值。Softmax函数的分母综合了原始输出值的所有因素,这意味着,Softmax函数得到的不同概率之间相互关联。

Softmax函数是二分类函数Sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。如图2所示,Softmax直白来说就是将原来输出是3,1,-3通过Softmax函数一作用,就映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们就可以将它理解成概率,在最后选取输出结点的时候,我们就可以选取概率最大(也就是值对应最大的)结点,作为我们的预测目标。

由于Softmax函数先拉大了输入向量元素之间的差异(通过指数函数),然后才归一化为一个概率分布,在应用到分类问题时,它使得各个类别的概率差异比较显着,最大值产生的概率更接近1,这样输出分布的形式更接近真实分布。

Softmax可以由三个不同的角度来解释。从不同角度来看softmax函数,可以对其应用场景有更深刻的理解:

  1. softmax可以当作arg max的一种平滑近似,与arg max操作中暴力地选出一个最大值(产生一个one-hot向量)不同,softmax将这种输出作了一定的平滑,即将one-hot输出中最大值对应的1按输入元素值的大小分配给其他位置。
  2. softmax将输入向量归一化映射到一个类别概率分布,即 n 个类别上的概率分布(前文也有提到)。这也是为什么在深度学习中常常将softmax作为MLP的最后一层,并配合以交叉熵损失函数(对分布间差异的一种度量)。
  3. 从概率图模型的角度来看,softmax的这种形式可以理解为一个概率无向图上的联合概率。因此你会发现,条件最大熵模型与softmax回归模型实际上是一致的,诸如这样的例子还有很多。由于概率图模型很大程度上借用了一些热力学系统的理论,因此也可以从物理系统的角度赋予softmax一定的内涵。

2.3 总结

  1. 如果模型输出为非互斥类别,且可以同时选择多个类别,则采用Sigmoid函数计算该网络的原始输出值。
  2. 如果模型输出为互斥类别,且只能选择一个类别,则采用Softmax函数计算该网络的原始输出值。
  3. Sigmoid函数可以用来解决多标签问题,Softmax函数用来解决单标签问题。[1]
  4. 对于某个分类场景,当Softmax函数能用时,Sigmoid函数一定可以用。

3. 二分类任务

对于二分类问题来说,理论上,两者是没有任何区别的。由于我们现在用的Pytorch、TensorFlow等框架计算矩阵方式的问题,导致两者在反向传播的过程中还是有区别的。实验结果表明,两者还是存在差异的,对于不同的分类模型,可能Sigmoid函数效果好,也可能是Softmax函数效果。

然后我们再分析为什么两者之间还存着差异(以Pytorch为例):

首先我们要明白,当你用Sigmoid函数的时候,你的最后一层全连接层的神经元个数为1,而当你用Softmax函数的时候,你的最后一层全连接层的神经元个数是2。这个很好理解,因为Sigmoid函数只有是目标和不是目标之分,实际上只存在一类目标类,另外一个是背景类。而Softmax函数将目标分类为了二类,所以有两个神经元。这也是导致两者存在差异的主要原因。

Sigmoid函数针对两点分布提出。神经网络的输出经过它的转换,可以将数值压缩到(0,1)之间,得到的结果可以理解成分类成目标类别的概率P,而不分类到该类别的概率是(1 – P),这也是典型的两点分布的形式。

Softmax函数本身针对多项分布提出,当类别数是2时,它退化为二项分布。而它和Sigmoid函数真正的区别就在——二项分布包含两个分类类别(姑且分别称为A和B),而两点分布其实是针对一个类别的概率分布,其对应的那个类别的分布直接由1-P得出。

简单点理解就是,Sigmoid函数,我们可以当作成它是对一个类别的“建模”,将该类别建模完成,另一个相对的类别就直接通过1减去得到。而softmax函数,是对两个类别建模,同样的,得到两个类别的概率之和是1。

神经网络在做二分类时,使用Softmax还是Sigmoid,做法其实有明显差别。由于Softmax是对两个类别(正反两类,通常定义为0/1的label)建模,所以对于NLP模型而言(比如泛BERT模型),Bert输出层需要通过一个nn.Linear()全连接层压缩至2维,然后接Softmax(Pytorch的做法,就是直接接上torch.nn.CrossEntropyLoss);而Sigmoid只对一个类别建模(通常就是正确的那个类别),所以Bert输出层需要通过一个nn.Linear()全连接层压缩至1维,然后接Sigmoid(torch就是接torch.nn.BCEWithLogitsLoss)。

总而言之,Sotfmax和Sigmoid确实在二分类的情况下可以化为相同的数学表达形式,但并不意味着二者有一样的含义,而且二者的输入输出都是不同的。Sigmoid得到的结果是“分到正确类别的概率和未分到正确类别的概率”,Softmax得到的是“分到正确类别的概率和分到错误类别的概率”。

一种常见的错法(NLP中):即错误地将Softmax和Sigmoid混为一谈,再把BERT输出层压缩至2维的情况下,却用Sigmoid对结果进行计算。这样我们得到的结果其意义是什么呢?

假设我们现在BERT输出层经nn.Linear()压缩后,得到一个二维的向量:

[-0.9419267177581787, 1.944047451019287]

对应类别分别是(0,1)。我们经过Sigmoid运算得到:

tensor([0.2805, 0.8748])

前者0.2805指的是分类类别为0的概率,0.8748指的是分类类别为1的概率。二者相互独立,可看作两次独立的实验(显然在这里不适用,因为0-1类别之间显然不是相互独立的两次伯努利事件)。所以显而易见的,二者加和并不等于1。

若用softmax进行计算,可得:

tensor([0.0529, 0.9471])

这里两者加和是1,才是正确的选择。

经验:

对于NLP而言,这两者之间确实有差别,Softmax的处理方式有时候会比Sigmoid的处理方式好一点。

对于CV而言,这两者之间也是有差别的,Sigmoid的处理方式有时候会比Softmax的处理方式好一点。

逼真度超越「AI设计师」DALL·E 2!谷歌大脑推出新的文本生成图像模型——Imagen

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

demo地址:https://imagen.research.google/

文本生成图像模型界又出新手笔!

这次的主角是Google Brain推出的 Imagen,再一次突破人类想象力,将文本生成图像的逼真度和语言理解提高到了前所未有的新高度!比前段时间OpeAI家的DALL·E 2更强!

话不多说,我们来欣赏这位AI画师的杰作~A brain riding a rocketship heading towards the moon.(一颗大脑乘着火箭飞向月球。)


Imagen的工作原理

本方案的主要内容包括三部分,如下图所示,首先是文本编码器部分,本文直接使用的是T5,然后是Diffusion生成模型,这部分与Glide类似,都是使用classifier-free引导的方式。最后就是对生成的小图进行超分,变为大图。下面分模块详细介绍:

2.1. text编码部分

文本编码器部分对比了BERT(base模型参数量:1.1亿)、CLIP(0.63亿)以及T5(模型参数量:110亿),后来发现T5效果最好。并且还舍弃了之前常规的基于<text, image>数据对,对Text Encoder进行finetune的流程。理由个很直接,因为参数量大好几个量级,不需要finetune。

2.2. Diffusion生成部分

这部分跟Glide中的基本相近,可以直接与Glide文章中对eps建模公式进行对比。只是在uncondition的时候没有使用空的文本表示。

text condition diffusion model using classifier-free guidance

classifier-free应该是diffusion必备的优化方式了。融合text特征到生成模型中的部分也可以直接看Glide。这部分的模型还是典型的64*64的U-Net结构,如下图2所示。之所以选择小模型主要还是diffusion的迭代过程太长,导致生成过程慢,所以生成小图是提速最方便的,但是也注定了无法生成比较复杂内容和空间关系的大图。UNet网络由左编码部分,右解码部分和下两个卷积+激活层组成。

编码部分:左边红框架构中是由4个重复结构组成:2个3×3卷积层,非线形ReLU层和一个stride为2的2×2 max pooling层。每一次下采样特征通道的数量加倍。

解码部分:右边蓝框,反卷积也有4个重复结构组成。每个重复结构前先使用反卷积,每次反卷积后特征通道数量减半,特征图大小加倍反卷积之后,反卷积的结果和编码部分对应步骤的特征图拼接起来。拼接后的特征图再进行2次3×3的卷积,最后一层的卷积核为1×1 的卷积核,将64通道的特征图转化为特定类别数量的结果

2.3. Diffusion超分部分

超分的好处是可以直接带来效率的提高,但是可能会影响最终生成的细节失真,本文在本文提到通过噪声的增强,可以提升模型在控制失真上鲁棒性,具体原理还是要详细看论文了。这部分模型使用的是U-Net的变体Efficient U-Net模型,有点就是提升记忆感知、推理效率以及训练收敛速度。

大型预训练语言模型×级联扩散模型

Imagen使用在纯文本语料中进行预训练的通用大型语言模型(例如T5),它能够非常有效地将文本合成图像:在Imagen中增加语言模型的大小,而不是增加图像扩散模型的大小,可以大大地提高样本保真度和图像-文本对齐。

Imagen的研究突出体现在:

  • 大型预训练冻结文本编码器对于文本到图像的任务来说非常有效;
  • 缩放预训练的文本编码器大小比缩放扩散模型大小更重要;
  • 引入一种新的阈值扩散采样器,这种采样器可以使用非常大的无分类器指导权重;
  • 引入一种新的高效U-Net架构,这种架构具有更高的计算效率、更高的内存效率和更快的收敛速度;
  • Imagen在COCO数据集上获得了最先进的FID分数7.27,而没有对COCO进行任何训练,人类评分者发现,Imagen样本在图像-文本对齐方面与COCO数据本身不相上下。

2
引入新基准DrawBench

为了更深入地评估文本到图像模型,Google Brain 引入了DrawBench,这是一个全面的、具有挑战性的文本到图像模型基准。通过DrawBench,他们比较了Imagen与VQ-GAN+CLIP、Latent Diffusion Models和DALL-E 2等其他方法,发现人类评分者在比较中更喜欢Imagen而不是其他模型,无论是在样本质量上还是在图像-文本对齐方面。

  • 并排人类评估;
  • 对语意合成性、基数性、空间关系、长文本、生词和具有挑战性的提示几方面提出了系统化的考验;
  • 由于图像-文本对齐和图像保真度的优势,相对于其他方法,用户强烈倾向于使用Imagen。

3 打开了潘多拉魔盒?

像Imagen这样从文本生成图像的研究面临着一系列伦理挑战。

首先,文本-图像模型的下游应用多种多样,可能会从多方面对社会造成影响。Imagen以及一切从文本生成图像的系统都有可能被误用的潜在风险,因此社会要求开发方提供负责任的开源代码和演示。基于以上原因,Google决定暂时不发布代码或进行公开演示。而在未来的工作中,Google将探索一个负责任的外部化框架,从而将各类潜在风险最小化。

其次,文本到图像模型对数据的要求导致研究人员严重依赖于大型的、大部分未经整理的、网络抓取的数据集。虽然近年来这种方法使算法快速进步,但这种性质的数据集往往会夹带社会刻板印象、压迫性观点、对边缘群体有所贬损等“有毒”信息。

为了去除噪音和不良内容(如色情图像和“有毒”言论),Google对训练数据的子集进行了过滤,同时Google还使用了众所周知的LAION-400M数据集进行过滤对比,该数据集包含网络上常见的不当内容,包括色情图像、种族主义攻击言论和负面社会刻板印象。Imagen依赖于在未经策划的网络规模数据上训练的文本编码器,因此继承了大型语言模型的社会偏见和局限性。这说明Imagen可能存在负面刻板印象和其他局限性,因此Google决定,在没有进一步安全措施的情况下,不会将Imagen发布给公众使用。

OpenAI 发布文字生成图像工具 DALL·E 2

paper: https://arxiv.org/abs/2204.06125

官方地址: https://openai.com/dall-e-2/

论文讲解:DALL·E 2【论文精读】

Dall-mini:一个小型的Dall开源库

提供一个api: https://www.craiyon.com/

DALL·E 2 能做什么:

官网

DALL-E 2不仅能按用户指令生成明明魔幻,却又看着十分合理不明觉厉的图片。作为一款强大的模型,目前我们已知DALL-E 2还可以:

  • 生成特定艺术风格的图像,仿佛出自该种艺术风格的画家之手,十分原汁原味!
  • 保持一张图片显着特征的情况下,生成该图片的多种变体,每一种看起来都十分自然;
  • 修改现有图像而不露一点痕迹,天衣无缝。

1、根据文字生成图片(概念 属性 风格):

An astronaut riding a horse in a photorealistic style
A bowl of soup that is a portal to another dimension as digital art

2、 DALL·E 2 can take an image and create different
variations of it inspired by the original.(输入图片,生成跟原始图片相似的图片)

DALL-E 2目前曝光的功能令人瞠目结舌,不禁激起了众多AI爱好者的讨论,这样一个强大模型,它的工作原理到底是什么?!

3、DALL·E 2 can make realistic edits to existing images from a natural language caption. It can add and remove elements while taking shadows, reflections, and textures into account.

·

1、工作原理:简单粗暴

图源:https://arxiv.org/abs/2204.0612

针对图片生成这一功能来说,DALL-E 2的工作原理剖析出来看似并不复杂:

  1. 首先,将文本提示输入文本编码器,该训练过的编码器便将文本提示映射到表示空间。
  2. 接下来,称为先验的模型将文本编码映射到相应的图像编码,图像编码捕获文本编码中包含的提示的语义信息。
  3. 最后,图像解码模型随机生成一幅从视觉上表现该语义信息的图像。

2、工作细节:处处皆奥妙

可是以上步骤说起来简单,分开看来却是每一步都有很大难度,让我们来模拟DALL-E 2的工作流程,看看究竟每一步都是怎么走通的。

我们的第一步是先看看DALL-E 2是怎么学习把文本和视觉图像联系起来的。

第一步 – 把文本和视觉图像联系起来

输入“泰迪熊在时代广场滑滑板”的文字提示后,DALL-E 2生成了下图:

DALL-E 2是怎么知道“泰迪熊”这个文本概念在视觉空间里是什么样子的?

其实DALL-E 2中的文本语义和与其相对的视觉图片之间的联系,是由另一个OpenAI模型CLIP(Contrastive Language-Image Pre-training)学习的。

CLIP接受过数亿张图片及其相关文字的训练,学习到了给定文本片段与图像的关联。

也就是说,CLIP并不是试图预测给定图像的对应文字说明,而是只学习任何给定文本与图像之间的关联。CLIP做的是对比性而非预测性的工作。

整个DALL-E 2模型依赖于CLIP从自然语言学习语义的能力,所以让我们看看如何训练CLIP来理解其内部工作。

CLIP训练

训练CLIP的基本原则非常简单:

  1. 首先,所有图像及其相关文字说明都通过各自的编码器,将所有对象映射到m维空间。
  2. 然后,计算每个(图像,文本)对的cos值相似度。
  3. 训练目标是使N对正确编码的图像/标题对之间的cos值相似度最大化,同时使N2 – N对错误编码的图像/标题对之间的cos值相似度最小化。
CLIP训练流程

CLIP对DALL-E 2的意义

CLIP几乎就是DALL-E 2的心脏,因为CLIP才是那个把自然语言片段与视觉概念在语义上进行关联的存在,这对于生成与文本对应的图像来说至关重要。

第二步 – 从视觉语义生成图像

训练结束后,CLIP模型被冻结,DALL-E 2进入下一个任务——学习怎么把CLIP刚刚学习到的图像编码映射反转。CLIP学习了一个表示空间,在这个表示空间当中很容易确定文本编码和视觉编码的相关性, 我们需要学会利用表示空间来完成反转图像编码映射这个任务。

而OpenAI使用了它之前的另一个模型GLIDE的修改版本来执行图像生成。GLIDE模型学习反转图像编码过程,以便随机解码CLIP图像嵌入。

“一只吹喷火喇叭的柯基”一图经过CLIP的图片编码器,GLIDE利用这种编码生成保持原图像显着特征的新图像。 图源:https://arxiv.org/abs/2204.06125

如上图所示,需要注意的是,我们的目标不是构建一个自编码器并在给定的嵌入条件下精确地重建图像,而是在给定的嵌入条件下生成一个保持原始图像显着特征的图像。为了进行图像生成,GLIDE使用了扩散模型(Diffusion Model)。

何为扩散模型?

扩散模型是一项受热力学启发的发明,近年来越来越受到学界欢迎。扩散模型学习通过逆转一个逐渐噪声过程来生成数据。如下图所示,噪声处理过程被视为一个参数化的马尔可夫链,它逐渐向图像添加噪声使其被破坏,最终(渐近地)导致纯高斯噪声。扩散模型学习沿着这条链向后走去,在一系列步骤中逐渐去除噪声,以逆转这一过程。

扩散模型示意图 图源:https://arxiv.org/pdf/2006.11239.pdf

如果训练后将扩散模型“切成两半”,则可以通过随机采样高斯噪声来生成图像,然后对其去噪,生成逼真的图像。大家可能会意识到这种技术很容易令人联想到用自编码器生成数据,实际上扩散模型和自编码器确实是相关的。

GLIDE的训练

虽然GLIDE不是第一个扩散模型,但其重要贡献在于对模型进行了修改,使其能够生成有文本条件的图像。

GLIDE扩展了扩散模型的核心概念,通过增加额外的文本信息来增强训练过程,最终生成文本条件图像。让我们来看看GLIDE的训练流程:

动图封面

下面是一些使用GLIDE生成的图像示例。作者指出,就照片真实感和文本相似度两方面而言,GLIDE的表现优于DALL-E(1)。

DALL-E 2使用了一种改进的GLIDE模型,这种模型以两种方式使用投影的CLIP文本嵌入。第一种方法是将它们添加到GLIDE现有的时间步嵌入中,第二种方法是创建四个额外的上下文标记,这些标记连接到GLIDE文本编码器的输出序列。

GLIDE对于DALL-E 2的意义

GLIDE对于DALL-E 2亦很重要,因为GLIDE能够将自己按照文本生成逼真图像的功能移植到DALL-E 2上去,而无需在表示空间中设置图像编码。因此,DALL-E 2使用的修改版本GLIDE学习的是根据CLIP图像编码生成语义一致的图像。

第三步 – 从文本语义到相应的视觉语义的映射

到了这步,我们如何将文字提示中的文本条件信息注入到图像生成过程中?

回想一下,除了图像编码器,CLIP还学习了文本编码器。DALL-E 2使用了另一种模型,作者称之为先验模型,以便从图像标题的文本编码映射到对应图像的图像编码。DALL-E 2的作者用自回归模型和扩散模型进行了实验,但最终发现它们的性能相差无几。考虑到扩散模型的计算效率更高,因此选择扩散模型作为 DALL-E 2的先验。

从文本编码到相应图像编码的先验映射 修改自图源:https://arxiv.org/abs/2204.06125

先验训练

DALL-E 2中扩散先验的运行顺序是:

  1. 标记化的文本;
  2. 这些标记的CLIP文本编码;
  3. 扩散时间步的编码;
  4. 噪声图像通过CLIP图像编码器;
  5. Transformer输出的最终编码用于预测无噪声CLIP图像编码。

第四步 – 万事俱备

现在,我们已经拥有了DALL-E 2的所有“零件”,万事俱备,只需要将它们组合在一起就可以获得我们想要的结果——生成与文本指示相对应的图像:

  1. 首先,CLIP文本编码器将图像描述映射到表示空间;
  2. 然后扩散先验从CLIP文本编码映射到相应的CLIP图像编码;
  3. 最后,修改版的GLIDE生成模型通过反向扩散从表示空间映射到图像空间,生成众多可能图像中的一个。
DALL-E 2图像生成流程的高级概述 修改自图源:https://arxiv.org/abs/2204.06125

以上就是DALL-E 2的工作原理啦~

希望大家能注意到DALL-E 2开发的3个关键要点

  • DALL-E 2体现了扩散模型在深度学习中的能力,DALL-E 2中的先验子模型和图像生成子模型都是基于扩散模型的。虽然扩散模型只是在过去几年才流行起来,但其已经证明了自己的价值,我们可以期待在未来的各种研究中看到更多的扩散模型~
  • 第二点是我们应看到使用自然语言作为一种手段来训练最先进的深度学习模型的必要性与强大力量。DALL-E 2的强劲功能究其根本还是来自于互联网上提供的绝对海量的自然语言&图像数据对。使用这些数据不仅消除了人工标记数据集这一费力的过程所带来的发展瓶颈;这些数据的嘈杂、未经整理的性质也更加反映出深度学习模型必须对真实世界的数据具有鲁棒性。
  • 最后,DALL-E 2重申了Transformer作为基于网络规模数据集训练的模型中的最高地位,因为Transformer的并行性令人印象十分深刻。

ELECTRA: 超越BERT, 19年最佳NLP预训练模型

ELECTRA的全称是Efficiently Learning an Encoder that Classifies Token Replacements Accurately

Github: https://github.com/ymcui/Chinese-ELECTRA

ELECTRA : https://arxiv.org/abs/2003.10555

右边的图是左边的放大版,纵轴是GLUE分数,横轴是FLOPs (floating point operations),Tensorflow中提供的浮点数计算量统计。从上图可以看到,同等量级的ELECTRA是一直碾压BERT的,而且在训练更长的步数之后,达到了当时的SOTA模型——RoBERTa的效果。从左图曲线上也可以看到,ELECTRA效果还有继续上升的空间

2. 模型结构

NLP式的Generator-Discriminator

ELECTRA最主要的贡献是提出了新的预训练任务和框架,把生成式的Masked language model(MLM)预训练任务改成了判别式的Replaced token detection(RTD)任务,判断当前token是否被语言模型替换过。那么问题来了,我随机替换一些输入中的字词,再让BERT去预测是否替换过可以吗?可以的,因为我就这么做过,但效果并不好,因为随机替换太简单了

那怎样使任务复杂化呢?。。。咦,咱们不是有预训练一个MLM模型吗?

于是作者就干脆使用一个MLM的G-BERT来对输入句子进行更改,然后丢给D-BERT去判断哪个字被改过,如下:

于是,我们NLPer终于成功地把CV的GAN拿过来了!

Replaced Token Detection

但上述结构有个问题,输入句子经过生成器,输出改写过的句子,因为句子的字词是离散的,所以梯度在这里就断了,判别器的梯度无法传给生成器,于是生成器的训练目标还是MLM(作者在后文也验证了这种方法更好),判别器的目标是序列标注(判断每个token是真是假),两者同时训练,但判别器的梯度不会传给生成器,目标函数如下:

因为判别器的任务相对来说容易些,RTD loss相对MLM loss会很小,因此加上一个系数,作者训练时使用了50。

另外要注意的一点是,在优化判别器时计算了所有token上的loss,而以往计算BERT的MLM loss时会忽略没被mask的token。作者在后来的实验中也验证了在所有token上进行loss计算会提升效率和效果。

事实上,ELECTRA使用的Generator-Discriminator架构与GAN还是有不少差别,作者列出了如下几点:

3. 实验及结论

创新总是不易的,有了上述思想之后,可以看到作者进行了大量的实验,来验证模型结构、参数、训练方式的效果。

Weight Sharing

生成器和判别器的权重共享是否可以提升效果呢?作者设置了相同大小的生成器和判别器,在不共享权重下的效果是83.6,只共享token embedding层的效果是84.3,共享所有权重的效果是84.4。作者认为生成器对embedding有更好的学习能力,因为在计算MLM时,softmax是建立在所有vocab上的,之后反向传播时会更新所有embedding,而判别器只会更新输入的token embedding。最后作者只使用了embedding sharing。

Smaller Generators

从权重共享的实验中看到,生成器和判别器只需要共享embedding的权重就足矣了,那这样的话是否可以缩小生成器的尺寸进行训练效率提升呢?作者在保持原有hidden size的设置下减少了层数,得到了下图所示的关系图:

可以看到,生成器的大小在判别器的1/4到1/2之间效果是最好的。作者认为原因是过强的生成器会增大判别器的难度(判别器:小一点吧,我太难了)。

Training Algorithms

实际上除了MLM loss,作者也尝试了另外两种训练策略:

  1. Adversarial Contrastive Estimation:ELECTRA因为上述一些问题无法使用GAN,但也可以以一种对抗学习的思想来训练。作者将生成器的目标函数由最小化MLM loss换成了最大化判别器在被替换token上的RTD loss。但还有一个问题,就是新的生成器loss无法用梯度上升更新生成器,于是作者用强化学习Policy Gradient的思想,最终优化下来生成器在MLM任务上可以达到54%的准确率,而之前MLE优化下可以达到65%。(感谢 @阿雪我要 勘误)
  2. Two-stage training:即先训练生成器,然后freeze掉,用生成器的权重初始化判别器,再接着训练相同步数的判别器。

对比三种训练策略,得到下图:

可见“隔离式”的训练策略效果还是最好的,而两段式的训练虽然弱一些,作者猜测是生成器太强了导致判别任务难度增大,但最终效果也比BERT本身要强,进一步证明了判别式预训练的效果。

Small model? Big model?

这两节真是吊打之前的模型,作者重申了他的主要目的是提升预训练效率,于是做了GPU单卡就可以愉快训练的ELECTRA-Small和BERT-Small,接着和尺寸不变的ELMo、GPT等进行对比,结果如下:

数据简直优秀,仅用14M参数量,以前13%的体积,在提升了训练速度的同时还提升了效果,这里我疯狂点赞。

小ELECTRA的本事我们见过了,那大ELECTRA行吗?直接上图:

上面是各个模型在GLUE dev/text上的表现,可以看到ELECTRA仅用了1/4的计算量就达到了RoBERTa的效果。而且作者使用的是XLNet的语料,大约是126G,但RoBERTa用了160G。由于时间和精力问题,作者们没有把ELECTRA训练更久(应该会有提升),也没有使用各种榜单Trick,所以真正的GLUE test上表现一般(现在的T5是89.7,RoBERTa是88.5,没看到ELECTRA)。

Efficiency Analysis

前文中提到了,BERT的loss只计算被替换的15%个token,而ELECTRA是全部都计算的,所以作者又做了几个实验,探究哪种方式更好一些:

  1. ELECTRA 15%:让判别器只计算15% token上的损失
  2. Replace MLM:训练BERT MLM,输入不用[MASK]进行替换,而是其他生成器。这样可以消除这种pretrain-finetune直接的diff。
  3. All-Tokens MLM:接着用Replace MLM,只不过BERT的目标函数变为预测所有的token,比较接近ELECTRA。

三种实验结果如下:

可以看到:

  1. 对比ELECTRA和ELECTRA 15%:在所有token上计算loss确实能提升效果
  2. 对比Replace MLM和BERT:[MASK]标志确实会对BERT产生影响,而且BERT目前还有一个trick,就是被替换的10%情况下使用原token或其他token,如果没有这个trick估计效果会差一些。
  3. 对比All-Tokens MLM和BERT:如果BERT预测所有token 的话,效果会接近ELECTRA

另外,作者还发现,ELECTRA体积越小,相比于BERT就提升的越明显,说明fully trained的ELECTRA效果会更好。另外作者推断,由于ELECTRA是判别式任务,不用对整个数据分布建模,所以更parameter-efficient

4. 总结

无意中发现了这篇还在ICLR盲审的ELECTRA,读完摘要就觉得发现了新大陆,主要是自己也试过Replaced Token Detection这个任务,因为平时任务效果的分析和不久前看的一篇文章,让我深刻感受到了BERT虽然对上下文有很强的编码能力,却缺乏细粒度语义的表示,我用一张图表示大家就明白了:

这是把token编码降维后的效果,可以看到sky和sea明明是天与海的区别,却因为上下文一样而得到了极为相似的编码。细粒度表示能力的缺失会对真实任务造成很大影响,如果被针对性攻击的话更是无力,所以当时就想办法加上更细粒度的任务让BERT去区分每个token,不过同句内随机替换的效果并不好。相信这个任务很多人都想到过,不过都没有探索这么深入,这也告诫我们,idea遍地都是,往下挖才能有SOTA。

ELECTRA是BERT推出这一年来我见过最赞的idea,它不仅提出了能打败MLM的预训练任务,更推出了一种十分适用于NLP的类GAN框架。毕竟GAN太牛逼了,看到deepfake的时候我就想,什么时候我们也能deepcheat,但听说GAN在NLP上的效果一直不太好,这次ELECTRA虽然只用了判别器,但个人认为也在一定程度上打开了潘多拉魔盒。

算法工程师面试知识点整理:

https://mp.weixin.qq.com/s/nPVbgOBOPs5VjW6_U-Om3w

HuggingFace Transformers —-BERT 源码

摘自: https://zhuanlan.zhihu.com/p/360988428

众所周知,BERT模型自2018年问世起就各种屠榜,开启了NLP领域预训练+微调的范式。到现在,BERT的相关衍生模型层出不穷(XL-Net、RoBERTa、ALBERT、ELECTRA、ERNIE等),要理解它们可以先从BERT这个始祖入手。

HuggingFace是一家总部位于纽约的聊天机器人初创服务商,很早就捕捉到BERT大潮流的信号并着手实现基于pytorch的BERT模型。这一项目最初名为pytorch-pretrained-bert,在复现了原始效果的同时,提供了易用的方法以方便在这一强大模型的基础上进行各种玩耍和研究。

随着使用人数的增加,这一项目也发展成为一个较大的开源社区,合并了各种预训练语言模型以及增加了Tensorflow的实现,并且在2019年下半年改名为Transformers。截止写文章时(2021年3月30日)这一项目已经拥有43k+的star,可以说Transformers已经成为事实上的NLP基本工具。https://github.com/huggingface/transformers​github.com/huggingface/transformers

本文基于Transformers版本4.4.2(2021年3月19日发布)项目中,pytorch版的BERT相关代码,从代码结构、具体实现与原理,以及使用的角度进行分析,包含以下内容:

  1. BERT Tokenization分词模型(BertTokenizer)
  2. BERT Model本体模型(BertModel)
    1. BertEmbeddings
    2. BertEncoder
      1. BertLayer
        1. BertAttention
          1. BertSelfAttention
          2. BertSelfOutput
        2. BertIntermediate
        3. BertOutput
      2. BertPooler
  3. BERT-based Models应用模型(请看下篇)
    1. BertForPreTraining
    2. BertForSequenceClassification
    3. BertForMultiChoice
    4. BertForTokenClassification
    5. BertForQuestionAnswering
  4. BERT训练与优化(请看下篇)
    1. Pre-Training
    2. Fine-Tuning
      1. AdamW
      2. Warmup

1 Tokenization(BertTokenizer)

和BERT有关的Tokenizer主要写在/models/bert/tokenization_bert.py/models/bert/tokenization_bert_fast.py 中。

这两份代码分别对应基本的BertTokenizer,以及不进行token到index映射的BertTokenizerFast,这里主要讲解第一个。

class BertTokenizer(PreTrainedTokenizer):
    """
    Construct a BERT tokenizer. Based on WordPiece.

    This tokenizer inherits from :class:`~transformers.PreTrainedTokenizer` which contains most of the main methods.
    Users should refer to this superclass for more information regarding those methods.
    ...
    """

BertTokenizer 是基于BasicTokenizerWordPieceTokenizer 的分词器:

  • BasicTokenizer负责处理的第一步——按标点、空格等分割句子,并处理是否统一小写,以及清理非法字符。
    • 对于中文字符,通过预处理(加空格)来按字分割;
    • 同时可以通过never_split指定对某些词不进行分割;
    • 这一步是可选的(默认执行)。
  • WordPieceTokenizer在词的基础上,进一步将词分解为子词(subword) 。
    • subword介于char和word之间,既在一定程度保留了词的含义,又能够照顾到英文中单复数、时态导致的词表爆炸和未登录词的OOV(Out-Of-Vocabulary)问题,将词根与时态词缀等分割出来,从而减小词表,也降低了训练难度;
    • 例如,tokenizer这个词就可以拆解为“token”和“##izer”两部分,注意后面一个词的“##”表示接在前一个词后面。

BertTokenizer 有以下常用方法:

  • from_pretrained:从包含词表文件(vocab.txt)的目录中初始化一个分词器;
  • tokenize:将文本(词或者句子)分解为子词列表;
  • convert_tokens_to_ids:将子词列表转化为子词对应下标的列表;
  • convert_ids_to_tokens :与上一个相反;
  • convert_tokens_to_string:将subword列表按“##”拼接回词或者句子;
  • encode:对于单个句子输入,分解词并加入特殊词形成“[CLS], x, [SEP]”的结构并转换为词表对应下标的列表;对于两个句子输入(多个句子只取前两个),分解词并加入特殊词形成“[CLS], x1, [SEP], x2, [SEP]”的结构并转换为下标列表;
  • decode:可以将encode方法的输出变为完整句子。

以及,类自身的方法:

>>> from transformers import BertTokenizer
>>> bt = BertTokenizer.from_pretrained('./bert-base-uncased/')
>>> bt('I like natural language progressing!')
{'input_ids': [101, 1045, 2066, 3019, 2653, 27673, 999, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1]}

2 Model(BertModel)

和BERT模型有关的代码主要写在/models/bert/modeling_bert.py中,这一份代码有一千多行,包含BERT模型的基本结构和基于它的微调模型等。

下面从BERT模型本体入手分析:

class BertModel(BertPreTrainedModel):
    """

    The model can behave as an encoder (with only self-attention) as well as a decoder, in which case a layer of
    cross-attention is added between the self-attention layers, following the architecture described in `Attention is
    all you need <https://arxiv.org/abs/1706.03762>`__ by Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit,
    Llion Jones, Aidan N. Gomez, Lukasz Kaiser and Illia Polosukhin.

    To behave as an decoder the model needs to be initialized with the :obj:`is_decoder` argument of the configuration
    set to :obj:`True`. To be used in a Seq2Seq model, the model needs to initialized with both :obj:`is_decoder`
    argument and :obj:`add_cross_attention` set to :obj:`True`; an :obj:`encoder_hidden_states` is then expected as an
    input to the forward pass.
    """ 

BertModel主要为transformer encoder结构,包含三个部分:

  1. embeddings,即BertEmbeddings类的实体,对应词嵌入;
  2. encoder,即BertEncoder类的实体;
  3. pooler, 即BertPooler类的实体,这一部分是可选的。

补充:注意BertModel也可以配置为Decoder,不过下文中不包含对这一部分的讨论。

下面将介绍BertModel的前向传播过程中各个参数的含义以及返回值:

    def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        past_key_values=None,
        use_cache=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ): ...
  • input_ids:经过tokenizer分词后的subword对应的下标列表;
  • attention_mask:在self-attention过程中,这一块mask用于标记subword所处句子和padding的区别,将padding部分填充为0;
  • token_type_ids: 标记subword当前所处句子(第一句/第二句/padding);
  • position_ids: 标记当前词所在句子的位置下标;
  • head_mask: 用于将某些层的某些注意力计算无效化;
  • inputs_embeds: 如果提供了,那就不需要input_ids,跨过embedding lookup过程直接作为Embedding进入Encoder计算;
  • encoder_hidden_states: 这一部分在BertModel配置为decoder时起作用,将执行cross-attention而不是self-attention;
  • encoder_attention_mask: 同上,在cross-attention中用于标记encoder端输入的padding;
  • past_key_values:这个参数貌似是把预先计算好的K-V乘积传入,以降低cross-attention的开销(因为原本这部分是重复计算);
  • use_cache: 将保存上一个参数并传回,加速decoding;
  • output_attentions:是否返回中间每层的attention输出;
  • output_hidden_states:是否返回中间每层的输出;
  • return_dict:是否按键值对的形式(ModelOutput类,也可以当作tuple用)返回输出,默认为真。

补充:注意,这里的head_mask对注意力计算的无效化,和下文提到的注意力头剪枝不同,而仅仅把某些注意力的计算结果给乘以这一系数。

返回部分如下:

        # BertModel的前向传播返回部分
        if not return_dict:
            return (sequence_output, pooled_output) + encoder_outputs[1:]

        return BaseModelOutputWithPoolingAndCrossAttentions(
            last_hidden_state=sequence_output,
            pooler_output=pooled_output,
            past_key_values=encoder_outputs.past_key_values,
            hidden_states=encoder_outputs.hidden_states,
            attentions=encoder_outputs.attentions,
            cross_attentions=encoder_outputs.cross_attentions,
        )

可以看出,返回值不但包含了encoder和pooler的输出,也包含了其他指定输出的部分(hidden_states和attention等,这一部分在encoder_outputs[1:])方便取用:

        # BertEncoder的前向传播返回部分,即上面的encoder_outputs
        if not return_dict:
            return tuple(
                v
                for v in [
                    hidden_states,
                    next_decoder_cache,
                    all_hidden_states,
                    all_self_attentions,
                    all_cross_attentions,
                ]
                if v is not None
            )
        return BaseModelOutputWithPastAndCrossAttentions(
            last_hidden_state=hidden_states,
            past_key_values=next_decoder_cache,
            hidden_states=all_hidden_states,
            attentions=all_self_attentions,
            cross_attentions=all_cross_attentions,
        )

此外,BertModel还有以下的方法,方便BERT玩家进行各种骚操作:

  1. get_input_embeddings:提取embedding中的word_embeddings即词向量部分;
  2. set_input_embeddings:为embedding中的word_embeddings赋值;
  3. _prune_heads:提供了将注意力头剪枝的函数,输入为{layer_num: list of heads to prune in this layer}的字典,可以将指定层的某些注意力头剪枝。

补充:剪枝是一个复杂的操作,需要将保留的注意力头部分的Wq、Kq、Vq和拼接后全连接部分的权重拷贝到一个新的较小的权重矩阵(注意先禁止grad再拷贝),并实时记录被剪掉的头以防下标出错。具体参考BertAttention部分的prune_heads方法。

2.1 BertEmbeddings

包含三个部分求和得到:

  1. word_embeddings,上文中subword对应的嵌入。
  2. token_type_embeddings,用于表示当前词所在的句子,辅助区别句子与padding、句子对间的差异。
  3. position_embeddings,句子中每个词的位置嵌入,用于区别词的顺序。和transformer论文中的设计不同,这一块是训练出来的,而不是通过Sinusoidal函数计算得到的固定嵌入。一般认为这种实现不利于拓展性(难以直接迁移到更长的句子中)。

三个embedding不带权重相加,并通过一层LayerNorm+dropout后输出,其大小为(batch_size, sequence_length, hidden_size)

补充:这里为什么要用LayerNorm+Dropout呢?为什么要用LayerNorm而不是BatchNorm?可以参考一个不错的回答:

transformer 为什么使用 layer normalization,而不是其他的归一化方法?369 赞同 · 15 评论回答

2.2 BertEncoder

包含多层BertLayer,这一块本身没有特别需要说明的地方,不过有一个细节值得参考:

利用gradient checkpointing技术以降低训练时的显存占用。

补充:gradient checkpointing即梯度检查点,通过减少保存的计算图节点压缩模型占用空间,但是在计算梯度的时候需要重新计算没有存储的值,参考论文《Training Deep Nets with Sublinear Memory Cost》,过程如下示意图:

动图封面

在BertEncoder中,gradient checkpoint是通过torch.utils.checkpoint.checkpoint实现的,使用起来比较方便,可以参考文档:torch.utils.checkpoint – PyTorch 1.8.1 documentation​pytorch.org/docs/stable/checkpoint.html

这一机制的具体实现比较复杂(没看懂),在此不作展开。

再往深一层走,就进入了Encoder的某一层:

2.2.1 BertLayer

这一层包装了BertAttention和BertIntermediate+BertOutput(即Attention后的FFN部分),以及这里直接忽略的cross-attention部分(将BERT作为Decoder时涉及的部分)。

理论上,这里顺序调用三个子模块就可以,没有什么值得说明的地方。

然而这里又出现了一个细节

        # 这是forward的一部分
        self_attention_outputs = self.attention(
            hidden_states,
            attention_mask,
            head_mask,
            output_attentions=output_attentions,
            past_key_value=self_attn_past_key_value,
        )
        outputs = self_attention_outputs[1:]  # add self attentions if we output attention weights

        # 中间省略一部分……

        layer_output = apply_chunking_to_forward(
            self.feed_forward_chunk, self.chunk_size_feed_forward, self.seq_len_dim, attention_output
        )
        outputs = (layer_output,) + outputs

        # 省略一部分……

        return outputs

    # 这是feed_forward_chunk的部分
    def feed_forward_chunk(self, attention_output):
        intermediate_output = self.intermediate(attention_output)
        layer_output = self.output(intermediate_output, attention_output)
        return layer_output

看到上面那个apply_chunking_to_forwardfeed_forward_chunk了吗(为什么要整这么复杂,直接调用它不香吗)?

那么这个apply_chunking_to_forward到底是啥?深入看看……

def apply_chunking_to_forward(
    forward_fn: Callable[..., torch.Tensor], chunk_size: int, chunk_dim: int, *input_tensors
) -> torch.Tensor:
    """
    This function chunks the :obj:`input_tensors` into smaller input tensor parts of size :obj:`chunk_size` over the
    dimension :obj:`chunk_dim`. It then applies a layer :obj:`forward_fn` to each chunk independently to save memory.

    If the :obj:`forward_fn` is independent across the :obj:`chunk_dim` this function will yield the same result as
    directly applying :obj:`forward_fn` to :obj:`input_tensors`.
    ...
    """

哦,又是一个节约显存的技术——包装了一个切分小batch或者低维数操作的功能:这里参数chunk_size其实就是切分的batch大小,而chunk_dim就是一次计算维数的大小,最后拼接起来返回。

不过,在默认操作中不会特意设置这两个值(在源代码中默认为0和1),所以会直接等效于正常的forward过程。

继续往下深入,就是Transformer的核心:BertAttention部分,以及紧随其后的FFN部分。

2.2.1.1 BertAttention

本以为attention的实现就在这里,没想到还要再下一层……其中,self成员就是多头注意力的实现,而output成员实现attention后的全连接+dropout+residual+LayerNorm一系列操作。

class BertAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.self = BertSelfAttention(config)
        self.output = BertSelfOutput(config)
        self.pruned_heads = set()

首先还是回到这一层。这里出现了上文提到的剪枝操作,即prune_heads方法:

    def prune_heads(self, heads):
        if len(heads) == 0:
            return
        heads, index = find_pruneable_heads_and_indices(
            heads, self.self.num_attention_heads, self.self.attention_head_size, self.pruned_heads
        )

        # Prune linear layers
        self.self.query = prune_linear_layer(self.self.query, index)
        self.self.key = prune_linear_layer(self.self.key, index)
        self.self.value = prune_linear_layer(self.self.value, index)
        self.output.dense = prune_linear_layer(self.output.dense, index, dim=1)

        # Update hyper params and store pruned heads
        self.self.num_attention_heads = self.self.num_attention_heads - len(heads)
        self.self.all_head_size = self.self.attention_head_size * self.self.num_attention_heads
        self.pruned_heads = self.pruned_heads.union(heads) 

这里的具体实现概括如下:

  • find_pruneable_heads_and_indices是定位需要剪掉的head,以及需要保留的维度下标index;
  • prune_linear_layer则负责将Wk/Wq/Wv权重矩阵(连同bias)中按照index保留没有被剪枝的维度后转移到新的矩阵。

接下来就到重头戏——Self-Attention的具体实现。

2.2.1.1.1 BertSelfAttention

预警:这一块可以说是模型的核心区域,也是唯一涉及到公式的地方,所以将贴出大量代码。

初始化部分:

class BertSelfAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"):
            raise ValueError(
                "The hidden size (%d) is not a multiple of the number of attention "
                "heads (%d)" % (config.hidden_size, config.num_attention_heads)
            )

        self.num_attention_heads = config.num_attention_heads
        self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
        self.all_head_size = self.num_attention_heads * self.attention_head_size

        self.query = nn.Linear(config.hidden_size, self.all_head_size)
        self.key = nn.Linear(config.hidden_size, self.all_head_size)
        self.value = nn.Linear(config.hidden_size, self.all_head_size)

        self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
        self.position_embedding_type = getattr(config, "position_embedding_type", "absolute")
        if self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query":
            self.max_position_embeddings = config.max_position_embeddings
            self.distance_embedding = nn.Embedding(2 * config.max_position_embeddings - 1, self.attention_head_size)

        self.is_decoder = config.is_decoder
  • 除掉熟悉的query、key、value三个权重和一个dropout,这里还有一个谜一样的position_embedding_type,以及decoder标记(当然,我不打算介绍cross-attenton部分);
  • 注意,hidden_size和all_head_size在一开始是一样的。至于为什么要看起来多此一举地设置这一个变量——显然是因为上面那个剪枝函数,剪掉几个attention head以后all_head_size自然就小了;
  • hidden_size必须是num_attention_heads的整数倍,以bert-base为例,每个attention包含12个head,hidden_size是768,所以每个head大小即attention_head_size=768/12=64;
  • position_embedding_type是什么?继续往下看就知道了……

然后是重点,也就是前向传播过程。

首先回顾一下multi-head self-attention的基本公式:

其中 |h| 表示注意力头的个数, [⋅] 表示向量拼接, Wo∈R|h|dv×dx 。

而这些注意力头,众所周知是并行计算的,所以上面的query、key、value三个权重是唯一的——这并不是所有heads共享了权重,而是“拼接”起来了。

补充:原论文中多头的理由为Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this.而另一个比较靠谱的分析有:

为什么Transformer 需要进行 Multi-head Attention?1036 赞同 · 46 评论回答

看看forward方法:

    def transpose_for_scores(self, x):
        new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
        x = x.view(*new_x_shape)
        return x.permute(0, 2, 1, 3)

    def forward(
        self,
        hidden_states,
        attention_mask=None,
        head_mask=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        past_key_value=None,
        output_attentions=False,
    ):
        mixed_query_layer = self.query(hidden_states)

        # 省略一部分cross-attention的计算
        key_layer = self.transpose_for_scores(self.key(hidden_states))
        value_layer = self.transpose_for_scores(self.value(hidden_states))
        query_layer = self.transpose_for_scores(mixed_query_layer)

        # Take the dot product between "query" and "key" to get the raw attention scores.
        attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
        # ...
  • 这里的transpose_for_scores用来把hidden_size拆成多个头输出的形状,并且将中间两维转置以进行矩阵相乘;
  • 这里key_layer/value_layer/query_layer的形状为:(batch_size, num_attention_heads, sequence_length, attention_head_size)
  • 这里attention_scores的形状为:(batch_size, num_attention_heads, sequence_length, sequence_length),符合多个头单独计算获得的attention map形状。

到这里实现了K与Q相乘,获得raw attention scores的部分,按公式接下来应该是按dk进行scaling并做softmax的操作。然而——

先出现在眼前的是一个奇怪的positional_embedding,以及一堆爱因斯坦求和:

        # ...
        if self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query":
            seq_length = hidden_states.size()[1]
            position_ids_l = torch.arange(seq_length, dtype=torch.long, device=hidden_states.device).view(-1, 1)
            position_ids_r = torch.arange(seq_length, dtype=torch.long, device=hidden_states.device).view(1, -1)
            distance = position_ids_l - position_ids_r
            positional_embedding = self.distance_embedding(distance + self.max_position_embeddings - 1)
            positional_embedding = positional_embedding.to(dtype=query_layer.dtype)  # fp16 compatibility

            if self.position_embedding_type == "relative_key":
                relative_position_scores = torch.einsum("bhld,lrd->bhlr", query_layer, positional_embedding)
                attention_scores = attention_scores + relative_position_scores
            elif self.position_embedding_type == "relative_key_query":
                relative_position_scores_query = torch.einsum("bhld,lrd->bhlr", query_layer, positional_embedding)
                relative_position_scores_key = torch.einsum("bhrd,lrd->bhlr", key_layer, positional_embedding)
                attention_scores = attention_scores + relative_position_scores_query + relative_position_scores_key
        # ...

补充:关于爱因斯坦求和约定,参考以下文档

torch.einsum – PyTorch 1.8.1 documentation​pytorch.org/docs/stable/generated/torch.einsum.html

补充:这里的positional_embedding引入了attention map中的位置嵌入——为什么要这么做呢?我目前还没搞明白……

对于不同的positional_embedding_type,有三种操作:

  • absolute:默认值,这部分就不用处理;
  • relative_key:对key_layer作处理,将其与这里的positional_embedding和key矩阵相乘作为key相关的位置编码;
  • relative_key_query:对key和value都进行相乘以作为位置编码。

暂时跳过这一迷惑的部分,回到正常attention的流程:

        # ...
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)
        if attention_mask is not None:
            # Apply the attention mask is (precomputed for all layers in BertModel forward() function)
            attention_scores = attention_scores + attention_mask  # 这里为什么是+而不是*?

        # Normalize the attention scores to probabilities.
        attention_probs = nn.Softmax(dim=-1)(attention_scores)

        # This is actually dropping out entire tokens to attend to, which might
        # seem a bit unusual, but is taken from the original Transformer paper.
        attention_probs = self.dropout(attention_probs)

        # Mask heads if we want to
        if head_mask is not None:
            attention_probs = attention_probs * head_mask

        context_layer = torch.matmul(attention_probs, value_layer)

        context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
        new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
        context_layer = context_layer.view(*new_context_layer_shape)

        outputs = (context_layer, attention_probs) if output_attentions else (context_layer,)

        # 省略decoder返回值部分……
        return outputs

重大疑问:这里的attention_scores = attention_scores + attention_mask是在做什么?难道不应该是乘mask吗?

因为这里的attention_mask已经【被动过手脚】,将原本为1的部分变为0,而原本为0的部分(即padding)变为一个较大的负数,这样相加就得到了一个较大的负值:

  • 至于为什么要用【一个较大的负数】?因为这样一来经过softmax操作以后这一项就会变成接近0的小数。
(Pdb) attention_mask
tensor([[[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]],
        [[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]],
        [[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]],
        ...,
        [[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]],
        [[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]],
        [[[    -0.,     -0.,     -0.,  ..., -10000., -10000., -10000.]]]],
       device='cuda:0')

那么,这一步是在哪里执行的呢?

我在modeling_bert.py中没有找到答案,但是在modeling_utils.py中找到了一个特别的类:class ModuleUtilsMixin,在它的get_extended_attention_mask方法中发现了端倪:

    def get_extended_attention_mask(self, attention_mask: Tensor, input_shape: Tuple[int], device: device) -> Tensor:
        """
        Makes broadcastable attention and causal masks so that future and masked tokens are ignored.

        Arguments:
            attention_mask (:obj:`torch.Tensor`):
                Mask with ones indicating tokens to attend to, zeros for tokens to ignore.
            input_shape (:obj:`Tuple[int]`):
                The shape of the input to the model.
            device: (:obj:`torch.device`):
                The device of the input to the model.

        Returns:
            :obj:`torch.Tensor` The extended attention mask, with a the same dtype as :obj:`attention_mask.dtype`.
        """
        # 省略一部分……

        # Since attention_mask is 1.0 for positions we want to attend and 0.0 for
        # masked positions, this operation will create a tensor which is 0.0 for
        # positions we want to attend and -10000.0 for masked positions.
        # Since we are adding it to the raw scores before the softmax, this is
        # effectively the same as removing these entirely.
        extended_attention_mask = extended_attention_mask.to(dtype=self.dtype)  # fp16 compatibility
        extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0
        return extended_attention_mask

那么,这个函数是在什么时候被调用的呢?和BertModel有什么关系呢?

OK,这里涉及到BertModel的继承细节了:BertModel继承自BertPreTrainedModel ,后者继承自PreTrainedModel,而PreTrainedModel继承自[nn.Module, ModuleUtilsMixin, GenerationMixin] 三个基类。——好复杂的封装!

这也就是说, BertModel必然在中间的某个步骤对原始的attention_mask调用了get_extended_attention_mask ,导致attention_mask从原始的[1, 0]变为[0, -1e4]的取值。

真相只有一个!

最终在BertModel的前向传播过程中找到了这一调用(第944行):

        # We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
        # ourselves in which case we just need to make it broadcastable to all heads.
        extended_attention_mask: torch.Tensor = self.get_extended_attention_mask(attention_mask, input_shape, device)

问题解决了:这一方法不但实现了改变mask的值,还将其广播(broadcast)为可以直接与attention map相加的形状。

不愧是你,HuggingFace。

抱脸虫可不是乱叫的!

除此之外,值得注意的细节有:

  • 按照每个头的维度进行缩放,对于bert-base就是64的平方根即8;
  • attention_probs不但做了softmax,还用了一次dropout,这是担心attention矩阵太稠密吗……这里也提到很不寻常,但是原始Transformer论文就是这么做的;
  • head_mask就是之前提到的对多头计算的mask,如果不设置默认是全1,在这里就不会起作用;
  • context_layer即attention矩阵与value矩阵的乘积,原始的大小为:(batch_size, num_attention_heads, sequence_length, attention_head_size) ;
  • context_layer进行转置和view操作以后,形状就恢复了(batch_size, sequence_length, hidden_size)

OK, that’s all for attention.

2.2.1.1.2 BertSelfOutput

这一块操作略多但不复杂,一目了然:

class BertSelfOutput(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, hidden_states, input_tensor):
        hidden_states = self.dense(hidden_states)
        hidden_states = self.dropout(hidden_states)
        hidden_states = self.LayerNorm(hidden_states + input_tensor)
        return hidden_states

补充:这里又出现了LayerNorm和Dropout的组合,只不过这里是先Dropout,进行残差连接后再进行LayerNorm。至于为什么要做残差连接,最直接的目的就是降低网络层数过深带来的训练难度,对原始输入更加敏感~

2.2.1.2 BertIntermediate

看完了BertAttention,在Attention后面还有一个全连接+激活的操作:

class BertIntermediate(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
        if isinstance(config.hidden_act, str):
            self.intermediate_act_fn = ACT2FN[config.hidden_act]
        else:
            self.intermediate_act_fn = config.hidden_act

    def forward(self, hidden_states):
        hidden_states = self.dense(hidden_states)
        hidden_states = self.intermediate_act_fn(hidden_states)
        return hidden_states
  • 这里的全连接做了一个扩展,以bert-base为例,扩展维度为3072,是原始维度768的4倍之多;

补充:为什么要过一个FFN?不知道……谷歌最近的论文貌似说明只有attention的模型什么用都没有:

Attention is Not All You Need: Pure Attention Loses Rank Doubly Exponentially with Depth​arxiv.org/abs/2103.03404

  • 这里的激活函数默认实现为gelu(Gaussian Error Linerar Units(GELUS): GELU(x)=xP(X<=x)=xΦ(x) ;当然,它是无法直接计算的,可以用一个包含tanh的表达式进行近似(略)。

作为参考(图源网络):

至于为什么在transformer中要用这个激活函数……

补充:看了一些研究,应该是说GeLU比ReLU这些表现都好,以至于后续的语言模型都沿用了这一激活函数。

2.2.1.3 BertOutput

在这里又是一个全连接+dropout+LayerNorm,还有一个残差连接residual connect:

class BertOutput(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.intermediate_size, config.hidden_size)
        self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, hidden_states, input_tensor):
        hidden_states = self.dense(hidden_states)
        hidden_states = self.dropout(hidden_states)
        hidden_states = self.LayerNorm(hidden_states + input_tensor)
        return hidden_states

这里的操作和BertSelfOutput不能说没有关系,只能说一模一样……非常容易混淆的两个组件。

以下内容还包含基于BERT的应用模型,以及BERT相关的优化器和用法,将在下一篇文章作详细介绍。

2.2.3 BertPooler

这一层只是简单地取出了句子的第一个token,即[CLS]对应的向量,然后过一个全连接层和一个激活函数后输出:

(这一部分是可选的,因为pooling有很多不同的操作)

class BertPooler(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.activation = nn.Tanh()

    def forward(self, hidden_states):
        # We "pool" the model by simply taking the hidden state corresponding
        # to the first token.
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output

Takeaways·小结

  • 在HuggingFace实现的Bert模型中,使用了多种节约显存的技术:
    • gradient checkpoint,不保留前向传播节点,只在用时计算;
    • apply_chunking_to_forward,按多个小批量和低维度计算FFN部分;
  • BertModel包含复杂的封装和较多的组件。以bert-base为例,主要组件如下:
    • 总计Dropout出现了1+(1+1+1)x12=37次;
    • 总计LayerNorm出现了1+(1+1)x12=25次;
    • 总计dense全连接层出现了(1+1+1)x12+1=37次,并不是每个dense都配了激活函数……
BertModel(
  (embeddings): BertEmbeddings(
    (word_embeddings): Embedding(30522, 768, padding_idx=0)
    (position_embeddings): Embedding(512, 768)
    (token_type_embeddings): Embedding(2, 768)
    (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
    (dropout): Dropout(p=0.1, inplace=False)
  )
  (encoder): BertEncoder(
    (layer): ModuleList(
      (0): BertLayer(
        (attention): BertAttention(
          (self): BertSelfAttention(
            (query): Linear(in_features=768, out_features=768, bias=True)
            (key): Linear(in_features=768, out_features=768, bias=True)
            (value): Linear(in_features=768, out_features=768, bias=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (output): BertSelfOutput(
            (dense): Linear(in_features=768, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
        (intermediate): BertIntermediate(
          (dense): Linear(in_features=768, out_features=3072, bias=True)
        )
        (output): BertOutput(
          (dense): Linear(in_features=3072, out_features=768, bias=True)
          (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )
      # 如此重复11层,直到大厦崩塌:)
    )
  )
  (pooler): BertPooler(
    (dense): Linear(in_features=768, out_features=768, bias=True)
    (activation): Tanh()

3 BERT-based Models

基于BERT的模型都写在/models/bert/modeling_bert.py里面,包括BERT预训练模型和BERT分类模型,UML图如下:

BERT模型一图流(建议保存后放大查看):

画图工具:Pyreverse

首先,以下所有的模型都是基于BertPreTrainedModel这一抽象基类的,而后者则基于一个更大的基类PreTrainedModel。这里我们关注BertPreTrainedModel的功能:

  • 用于初始化模型权重,同时维护继承自PreTrainedModel的一些标记身份或者加载模型时的类变量。

下面,首先从预训练模型开始分析。

3.1 BertForPreTraining

众所周知,BERT预训练任务包括两个:

  • Masked Language Model(MLM):在句子中随机用[MASK]替换一部分单词,然后将句子传入 BERT 中编码每一个单词的信息,最终用[MASK]的编码信息预测该位置的正确单词,这一任务旨在训练模型根据上下文理解单词的意思;
  • Next Sentence Prediction(NSP):将句子对A和B输入BERT,使用[CLS]的编码信息进行预测B是否A的下一句,这一任务旨在训练模型理解预测句子间的关系。
图源网络

而对应到代码中,这一融合两个任务的模型就是BertForPreTraining,其中包含两个组件:

class BertForPreTraining(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)

        self.bert = BertModel(config)
        self.cls = BertPreTrainingHeads(config)

        self.init_weights()
    # ...

这里的BertModel在上一篇文章中已经详细介绍了(注意,这里设置的是默认add_pooling_layer=True,即会提取[CLS]对应的输出用于NSP任务),而BertPreTrainingHeads则是负责两个任务的预测模块:

class BertPreTrainingHeads(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.predictions = BertLMPredictionHead(config)
        self.seq_relationship = nn.Linear(config.hidden_size, 2)

    def forward(self, sequence_output, pooled_output):
        prediction_scores = self.predictions(sequence_output)
        seq_relationship_score = self.seq_relationship(pooled_output)
        return prediction_scores, seq_relationship_score 

又是一层封装:BertPreTrainingHeads包裹了BertLMPredictionHead 和一个代表NSP任务的线性层。这里不把NSP对应的任务也封装一个BertXXXPredictionHead,估计是因为它太简单了,没有必要……

补充:其实是有封装这个类的,不过它叫做BertOnlyNSPHead,在这里用不上……

继续下探BertPreTrainingHeads :

class BertLMPredictionHead(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.transform = BertPredictionHeadTransform(config)

        # The output weights are the same as the input embeddings, but there is
        # an output-only bias for each token.
        self.decoder = nn.Linear(config.hidden_size, config.vocab_size, bias=False)

        self.bias = nn.Parameter(torch.zeros(config.vocab_size))

        # Need a link between the two variables so that the bias is correctly resized with `resize_token_embeddings`
        self.decoder.bias = self.bias

    def forward(self, hidden_states):
        hidden_states = self.transform(hidden_states)
        hidden_states = self.decoder(hidden_states)
        return hidden_states

这个类用于预测[MASK]位置的输出在每个词作为类别的分类输出,注意到:

  • 该类重新初始化了一个全0向量作为预测权重的bias;
  • 该类的输出形状为[batch_size, seq_length, vocab_size],即预测每个句子每个词是什么类别的概率值(注意这里没有做softmax);
  • 又一个封装的类:BertPredictionHeadTransform,用来完成一些线性变换:
class BertPredictionHeadTransform(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        if isinstance(config.hidden_act, str):
            self.transform_act_fn = ACT2FN[config.hidden_act]
        else:
            self.transform_act_fn = config.hidden_act
        self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)

    def forward(self, hidden_states):
        hidden_states = self.dense(hidden_states)
        hidden_states = self.transform_act_fn(hidden_states)
        hidden_states = self.LayerNorm(hidden_states)
        return hidden_states

补充:感觉这一层去掉也行?输出的形状也没有发生变化。我个人的理解是和Pooling那里做一个对称的操作,同样过一层dense再接分类器……

回到BertForPreTraining,继续看两块loss是怎么处理的。它的前向传播和BertModel的有所不同,多了labelsnext_sentence_label 两个输入:

  • labels:形状为[batch_size, seq_length] ,代表MLM任务的标签,注意这里对于原本未被遮盖的词设置为-100,被遮盖词才会有它们对应的id,和任务设置是反过来的
    • 例如,原始句子是I want to [MASK] an apple,这里我把单词eat给遮住了输入模型,对应的label设置为[-100, -100, -100, 【eat对应的id】, -100, -100]
    • 为什么要设置为-100而不是其他数? 因为torch.nn.CrossEntropyLoss默认的ignore_index=-100,也就是说对于标签为100的类别输入不会计算loss。
  • next_sentence_label: 这一个输入很简单,就是0和1的二分类标签。
    # ...
    def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        labels=None,
        next_sentence_label=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ): ...

OK,接下来两部分loss的组合:

        # ...
        total_loss = None
        if labels is not None and next_sentence_label is not None:
            loss_fct = CrossEntropyLoss()
            masked_lm_loss = loss_fct(prediction_scores.view(-1, self.config.vocab_size), labels.view(-1))
            next_sentence_loss = loss_fct(seq_relationship_score.view(-1, 2), next_sentence_label.view(-1))
            total_loss = masked_lm_loss + next_sentence_loss
        # ...

直接相加,就是这么单纯的策略。

当然,这份代码里面也包含了对于只想对单个目标进行预训练的BERT模型(具体细节不作展开):

  • BertForMaskedLM:只进行MLM任务的预训练;
    • 基于BertOnlyMLMHead,而后者也是对BertLMPredictionHead的另一层封装;
  • BertLMHeadModel:这个和上一个的区别在于,这一模型是作为decoder运行的版本;
    • 同样基于BertOnlyMLMHead
  • BertForNextSentencePrediction:只进行NSP任务的预训练。
    • 基于BertOnlyNSPHead,内容就是一个线性层……

接下来介绍的是各种Fine-tune模型,基本都是分类任务:

图源:原始BERT论文附录

3.2 BertForSequenceClassification

这一模型用于句子分类(也可以是回归)任务,比如GLUE benchmark的各个任务。

  • 句子分类的输入为句子(对),输出为单个分类标签。

结构上很简单,就是BertModel(有pooling)过一个dropout后接一个线性层输出分类:

class BertForSequenceClassification(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.num_labels = config.num_labels

        self.bert = BertModel(config)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)
        self.classifier = nn.Linear(config.hidden_size, config.num_labels)

        self.init_weights()
        # ...

在前向传播时,和上面预训练模型一样需要传入labels输入。

  • 如果初始化的num_labels=1,那么就默认为回归任务,使用MSELoss;
  • 否则认为是分类任务。

3.3 BertForMultipleChoice

这一模型用于多项选择,如RocStories/SWAG任务。

  • 多项选择任务的输入为一组分次输入的句子,输出为选择某一句子的单个标签。

结构上与句子分类相似,只不过线性层输出维度为1,即每次需要将每个样本的多个句子的输出拼接起来作为每个样本的预测分数。

  • 实际上,具体操作时是把每个batch的多个句子一同放入的,所以一次处理的输入为[batch_size, num_choices]数量的句子,因此相同batch大小时,比句子分类等任务需要更多的显存,在训练时需要小心。

3.4 BertForTokenClassification

这一模型用于序列标注(词分类),如NER任务。

  • 序列标注任务的输入为单个句子文本,输出为每个token对应的类别标签。

由于需要用到每个token对应的输出而不只是某几个,所以这里的BertModel不用加入pooling层;

  • 同时,这里将_keys_to_ignore_on_load_unexpected这一个类参数设置为[r"pooler"],也就是在加载模型时对于出现不需要的权重不发生报错。

3.5 BertForQuestionAnswering

这一模型用于解决问答任务,例如SQuAD任务。

  • 问答任务的输入为问题+(对于BERT只能是一个)回答组成的句子对,输出为起始位置和结束位置用于标出回答中的具体文本。

这里需要两个输出,即对起始位置的预测和对结束位置的预测,两个输出的长度都和句子长度一样,从其中挑出最大的预测值对应的下标作为预测的位置。

  • 对超出句子长度的非法label,会将其压缩(torch.clamp_)到合理范围。

作为一个迟到的补充,这里稍微介绍一下ModelOutput这个类。它作为上述各个模型输出包装的基类,同时支持字典式的存取和下标顺序的访问,继承自python原生的OrderedDict 类。


以上就是关于BERT源码的介绍,下面介绍一些关于BERT模型实用的训练细节。

4 BERT训练和优化

4.1 Pre-Training

预训练阶段,除了众所周知的15%、80%mask比例,有一个值得注意的地方就是参数共享。

不止BERT,所有huggingface实现的PLM的word embedding和masked language model的预测权重在初始化过程中都是共享的:

class PreTrainedModel(nn.Module, ModuleUtilsMixin, GenerationMixin):
    # ...
    def tie_weights(self):
        """
        Tie the weights between the input embeddings and the output embeddings.

        If the :obj:`torchscript` flag is set in the configuration, can't handle parameter sharing so we are cloning
        the weights instead.
        """
        output_embeddings = self.get_output_embeddings()
        if output_embeddings is not None and self.config.tie_word_embeddings:
            self._tie_or_clone_weights(output_embeddings, self.get_input_embeddings())

        if self.config.is_encoder_decoder and self.config.tie_encoder_decoder:
            if hasattr(self, self.base_model_prefix):
                self = getattr(self, self.base_model_prefix)
            self._tie_encoder_decoder_weights(self.encoder, self.decoder, self.base_model_prefix)
    # ...

至于为什么,应该是因为word_embedding和prediction权重太大了,以bert-base为例,其尺寸为(30522, 768),降低训练难度。

4.2 Fine-Tuning

微调也就是下游任务阶段,也有两个值得注意的地方。

4.2.1 AdamW

首先介绍一下BERT的优化器:AdamW(AdamWeightDecayOptimizer)。

这一优化器来自ICLR 2017的Best Paper:《Fixing Weight Decay Regularization in Adam》中提出的一种用于修复Adam的权重衰减错误的新方法。论文指出,L2正则化和权重衰减在大部分情况下并不等价,只在SGD优化的情况下是等价的;而大多数框架中对于Adam+L2正则使用的是权重衰减的方式,两者不能混为一谈。

AdamW是在Adam+L2正则化的基础上进行改进的算法,与一般的Adam+L2的区别如下:

关于AdamW的分析可以参考:AdamW and Super-convergence is now the fastest way to train neural nets​www.fast.ai/2018/07/02/adam-weight-decay/paperplanet:都9102年了,别再用Adam + L2 regularization了1183 赞同 · 34 评论文章ICLR 2018 有什么值得关注的亮点?610 赞同 · 21 评论回答

话说,《STABLE WEIGHT DECAY REGULARIZATION》这篇好像吐槽AdamW的Weight Decay实现还是有问题……有空整整优化器相关的内容。

通常,我们会选择模型的weight部分参与decay过程,而另一部分(包括LayerNorm的weight)不参与(代码最初来源应该是Huggingface的示例):

补充:关于这么做的理由,我暂时没有找到合理的解答,但是找到了一些相关的讨论

https://forums.fast.ai/t/is-weight-decay-applied-to-the-bias-term/73212/4​forums.fast.ai/t/is-weight-decay-applied-to-the-bias-term/73212/4
    # model: a Bert-based-model object
    # learning_rate: default 2e-5 for text classification
    param_optimizer = list(model.named_parameters())
    no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
    optimizer_grouped_parameters = [
        {'params': [p for n, p in param_optimizer if not any(
            nd in n for nd in no_decay)], 'weight_decay': 0.01},
        {'params': [p for n, p in param_optimizer if any(
            nd in n for nd in no_decay)], 'weight_decay': 0.0}
    ]
    optimizer = AdamW(optimizer_grouped_parameters,
                      lr=learning_rate)
    # ...

4.2.2 Warmup

BERT的训练中另一个特点在于Warmup,其含义为:

  • 在训练初期使用较小的学习率(从0开始),在一定步数(比如1000步)内逐渐提高到正常大小(比如上面的2e-5),避免模型过早进入局部最优而过拟合;
  • 在训练后期再慢慢将学习率降低到0,避免后期训练还出现较大的参数变化。

在Huggingface的实现中,可以使用多种warmup策略:

TYPE_TO_SCHEDULER_FUNCTION = {
    SchedulerType.LINEAR: get_linear_schedule_with_warmup,
    SchedulerType.COSINE: get_cosine_schedule_with_warmup,
    SchedulerType.COSINE_WITH_RESTARTS: get_cosine_with_hard_restarts_schedule_with_warmup,
    SchedulerType.POLYNOMIAL: get_polynomial_decay_schedule_with_warmup,
    SchedulerType.CONSTANT: get_constant_schedule,
    SchedulerType.CONSTANT_WITH_WARMUP: get_constant_schedule_with_warmup,
}

具体而言:

  • CONSTANT:保持固定学习率不变;
  • CONSTANT_WITH_WARMUP:在每一个step中线性调整学习率;
  • LINEAR:上文提到的两段式调整;
  • COSINE:和两段式调整类似,只不过采用的是三角函数式的曲线调整;
  • COSINE_WITH_RESTARTS:训练中将上面COSINE的调整重复n次;
  • POLYNOMIAL:按指数曲线进行两段式调整。

具体使用参考transformers/optimization.py

  • 最常用的还是get_linear_scheduler_with_warmup即线性两段式调整学习率的方案……
def get_scheduler(
    name: Union[str, SchedulerType],
    optimizer: Optimizer,
    num_warmup_steps: Optional[int] = None,
    num_training_steps: Optional[int] = None,
): ...

Transformer综述—(Natural Language Processing, NLP)

  • github学习地址:https://github.com/datawhalechina/learn-nlp-with-transformers

自然语言处理(Natural Language Processing, NLP)是一种重要的人工智能(Artificial Intelligence, AI)技术。我们随处可以见到NLP技术的应用,比如网络搜索,广告,电子邮件,智能客服,机器翻译,智能新闻播报等等。最近几年,基于深度学习(Deep Learning, DL)的NLP技术在各项任务中取得了很好的效果,这些基于深度学习模型的NLP任务解决方案通常不使用传统的、特定任务的特征工程而是仅仅使用一个端到端(end-to-end)的神经网络模型就可以获得很好的效果。本教程将会基于最前沿的深度学习模型结构(transformers)来解决NLP里的几个经典任务。通过本教程的学习,我们将能够了解transformer相关原理、熟练使用transformer相关的深度学习模型来解决NLP里的实际问题以及在各类任务上取得很好的效果。

自然语言与深度学习的课程推荐:CS224n: Natural Language Processing with Deep Learning 自然语言处理的书籍推荐:Speech and Language Processing

常见的NLP任务

本教程将NLP任务划分为4个大类:1、文本分类, 2、序列标注,3、问答任务——抽取式问答和多选问答,4、生成任务——语言模型、机器翻译和摘要生成。

  • 文本分类:对单个、两个或者多段文本进行分类。举例:“这个教程真棒!”这段文本的情感倾向是正向的,“我在学习transformer”和“如何学习transformer”这两段文本是相似的。
  • 序列标注:对文本序列中的token、字或者词进行分类。举例:“我在国家图书馆学transformer。”这段文本中的国家图书馆是一个地点,可以被标注出来方便机器对文本的理解。
  • 问答任务——抽取式问答和多选问答:1、抽取式问答根据问题从一段给定的文本中找到答案,答案必须是给定文本的一小段文字。举例:问题“小学要读多久?”和一段文本“小学教育一般是六年制。”,则答案是“六年”。2、多选式问答,从多个选项中选出一个正确答案。举例:“以下哪个模型结构在问答中效果最好?“和4个选项”A、MLP,B、cnn,C、lstm,D、transformer“,则答案选项是D。
  • 生成任务——语言模型、机器翻译和摘要生成:根据已有的一段文字生成(generate)一个字通常叫做语言模型,根据一大段文字生成一小段总结性文字通常叫做摘要生成,将源语言比如中文句子翻译成目标语言比如英语通常叫做机器翻译。

虽然各种基于transformer的深度学习模型已经在多个人工构建的NLP任务中表现出色,但由于人类语言博大精深,深度学习模型依然有很长的路要走。

Transformer的兴起

2017年,Attention Is All You Need论文首次提出了Transformer模型结构并在机器翻译任务上取得了The State of the Art(SOTA, 最好)的效果。2018年,BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding使用Transformer模型结构进行大规模语言模型(language model)预训练(Pre-train),再在多个NLP下游(downstream)任务中进行微调(Finetune),一举刷新了各大NLP任务的榜单最高分,轰动一时。2019年-2021年,研究人员将Transformer这种模型结构和预训练+微调这种训练方式相结合,提出了一系列Transformer模型结构、训练方式的改进(比如transformer-xl,XLnet,Roberta等等)。如下图所示,各类Transformer的改进不断涌现。

图:各类Transformer改进,来源:A Survey of Transformers

另外,由于Transformer优异的模型结构,使得其参数量可以非常庞大从而容纳更多的信息,因此Transformer模型的能力随着预训练不断提升,随着近几年计算能力的提升,越来越大的预训练模型以及效果越来越好的Transformers不断涌现,简单的统计可以从下图看出:

图:预训练模型参数不断变大,来源Huggingface

尽管各类Transformer的研究非常多,总体上经典和流行的Transformer模型都可以通过HuggingFace/Transformers, 48.9k Star获得和免费使用,为初学者、研究人员提供了巨大的帮助。

本教程也将基于HuggingFace/Transformers, 48.9k Star进行具体的编程和任务解决方案实现。

NLP中的预训练+微调的训练方式推荐阅读: 2021年如何科学的“微调”预训练模型? 从Word Embedding到Bert模型—自然语言处理中的预训练技术发展史