精华内容
下载资源
问答
  • 真心感叹自己接触太晚,但是又愁于感天动地的英文阅读能力,再加上原论文有些语句的描述实在是晦涩难懂(尤其是XLNet),在翻阅大量博客和视屏之后,总算花了一整天时间,把过去几年最火的几个模型原理...

    前言

    过去两年可谓是NLP领域的高光年,自从18年BERT提出,狂刷了11项下游任务的记录之后,全网络预训练模型开启了NLP嵌入表示的新时代,真正意义上实现了自然语言处理的迁移学习。
    作为一个刚入坑没多久的白中白,真心感叹自己接触太晚,但是又愁于感天动地的英文阅读能力,再加上原论文有些语句的描述实在是晦涩难懂,在翻阅大量博客和视屏之后,总算花了一整天时间,把过去几年最火的几个模型原理大致梳理明白了。借此写一篇博客,虽然许多内容参考了其他blog,但也是经过自己消化理解,按照自己的思路罗列的。目的只是为了把一些记下来的零散的东西梳理清楚,put all these things together。
    文章的篇幅比较长,提及多个模型的要点,并将其按照顺序梳理,想要一次性读完有点困难。像我一样的初学者可以把这篇文章当做map,有些不懂的地方可以穿越到文末罗列的参考文章去,至于大佬…饶弟弟一命。

    1. ELMO–上下文语境的提出

    1.1 传统词嵌入的问题

    不管是基于local训练的Word2vec,还是基于全局的Glove,传统词嵌入存在的一个很大的问题:一旦预训练词向量,就只能将其按照训练结果嵌入模型中,这样会导致词向量无法根据具体语境作动态的调整。比如说下面这个句子:

    There is a traffic 'jam',so I began to eat my apple 'jam'.
    

    第一个jam是堵塞,第二个jam是果酱。明显,两个单词的含义完全不同。如果仅仅只是将jam用同一个词向量表示,无疑给下游任务造成了巨大的噪声。因此传统词嵌入只能依据具体语境去训练Embedding(或者在有更多语料的相似语境训练),无法像CV一样做迁移学习(VGGNet)。

    1.2 ELMo的提出

    18年8月,AllenNLP提出了一个上下文相关模型–ELMo(Embeddings from Language Models),强调了上下文语境的重要性。
    双向LSTM
    ELMo结构其实就是一个Bi-LSTM,他的输入是词向量列表(可以是Word2vec、Glove),把这些词向量先从左向右encoding得到一个token间的联系(图中红色),再从右到左(图中紫色),最后将这两部分拼接得到句子的context信息。
    在这里插入图片描述
    而这个拼接的过程异常简单,只是做了一个头尾连接,再加权求和得到一个融合了上下文语境的词向量(图中蓝色)。
    在这里插入图片描述

    1.3 ELMo的缺点

    1. 不完全双向:虽然明面上叫做Bi-LSTM,但其实只是两个单向的LSTM的隐藏层强行拼接之后做了一个线性组合,而这两个LSTM是分开计算的,并没有做到同时考虑上下文注意,这里讲的是同时双向
    2. 照镜子:又叫做“套娃”,也就是在执行预测的时候,某个位置的token可能已经提前察觉到了自己的信息,就如同照镜子一般。这个是考虑双向的模型都要面对的一个棘手问题,因为在ELMo训练的过程中,模型的task是预测下一个单词的概率分布,随着层数的增加,某个位置的单词在某个方向预测的时候可能会被底层间接地透露要预测的答案
      在这里插入图片描述
      如上图,假如说现在要预测第二列第二行,那么这个cell获悉的信息将是下层B输出的内容,它左边是A,右边是CD,因此这个cell在预测的时候获知了A|CD
      接下来如果要继续预测第二列第三行,还是依据下层的输出来预测,显然,它获悉了从左边过来的BCD,和从右边过来的AB|D,也就是将ABCD的信息全部透露给了这个cell,这对于模型的训练是不利的(考试作弊一般)
    3. 需要有针对下游任务的接口:这是ELMo本身决定的,因为它是通过语言模型任务得到句子中单词的 embedding 表示,以此作为补充的新特征给下游任务使用,是一种基于特征的预训练(Feature-based Pre-Training <下文会讲到 BERT是一种 Fine-tuning 的模式 >)。也可以这样理解:ELMo是将每一层的隐藏向量加权求和得到最终的向量,每一层可以当作是一个独立的功能模块。例如,原论文中设定了两个隐层,第一隐层可以学到对词性、句法等信息,对此有明显需求的任务可以对第一隐层参数学到比较大的值;第二隐层更适合对词义消歧有需求的任务,从而对特定任务可以分配更高权重。
    4. LSTM的长期依赖:这个是LSTM,或者说是RNN一直以来存在的一个问题,虽然说LSTM利用偷窥孔连接的机制很大程度上缓解了这个问题,但是并没有从根本上解决反向传播过程中的梯度消失。
    5. 基于LSTM的模型特征抽取能力偏弱:这点是通过和Transformer比较得出的,下文将会提到(没有比较就没有伤害)

    2. BERT–站在巨人的肩膀之上

    2.1 诞生背景

    BERT在ELMo提出的同年9月正式发表,本来ELMo就因为训练慢、效率不高被一些人摒弃,BERT一出算是彻底凉了。BERT的全称是Deep Bidirectional Transformers for Language Understanding,硬取了名字缩写BERT,目的是刚好和ELMo在一部国外很火的动画片里面撞名,也算是对ELMo的一种致(tong)敬(qing)了吧。

    下图,红色小不点是ELMo,黄色长脸冲顶一字眉的是BERT
    在这里插入图片描述
    BERT的灵感来源是OpenAI在17年发布过的一篇名为“Attention is all your need”论文中提到的Transformer模型,刚好ELMo又率先提出了上下文语境这一关键的概念,给BERT的诞生创造了非常好的条件。所以可以这样说–BERT是站在巨人的肩膀之上的
    BERT利用了Transformer中的encoder代替了传统的LSTM,所以它的结构可以被看做是一种堆叠的Transformer。
    在这里插入图片描述
    由于Transformer是BERT的一个至关重要的概念,因此下面重点介绍一下Transformer模型。

    2.2 Transformer–Attention is all your need

    2.2.1 背景

    Transformer由17年一篇著名论文“Attention is All Your Need”提出(敢这么取title的也真的不多),现在是谷歌云TPU推荐的参考模型.。在论文中,作者提出了一种全新的注意力机制-- self-Attention,做到了仅仅利用Attention代替了传统的RNN(作者还大力诟病RNN,从title也可以看出其不屑),实现了快速并行计算,挖掘了DNN的特性。其模型结构特别适用于MNT,能够大程度地提升翻译的准确率。

    2.2.2 直观理解Transformer

    Transformer主要分为两个部分:编码组件+解码组件,这点和传统的RNN编码、解码的结构很像,但是内部结构完全不同。
    在这里插入图片描述
    编码组件由多个encoder(编码器)堆叠而成(原论文中是6个,也可以换成其他数量)在这里插入图片描述
    每个encoder又有两个子层,分别是 FFN(前馈神经网络) + self-Attention(自注意力层)
    在这里插入图片描述
    FFN就是再熟悉不过的MLP(多层感知器架构),重点和创新点就在于self-Attention层。它的输入是一个单词经过词嵌入处理的句子,也就是一个词向量列表(下图中的 x,论文中是512维),输出是结合了句子本身上下文注意力之后的融合向量(下图中 z)。
    在这里插入图片描述
    什么叫做融合了自身上下文注意力?比方说现在有一个句子:

    The animal didn’t cross the street because 'it' was too tired
    

    对于我们学过英语的人来说,我们心里很清楚句子里的那个 ‘it’ 指代的是前文的 ‘animal’,不是‘apple’,也不是 ‘Tom’;而它现在的状态是‘tired’,不是‘excited’,也不是‘sexy’.因为我们知道在处理‘it’这个单词的时候,应该把注意力投入到哪里。把注意力这个抽象的概念可视化就可以得到下面这幅图:
    在这里插入图片描述
    如图,颜色越深说明这个单词和‘it’的关系越大,投入的注意力自然就越多。从图中可以看出,“The”、“animal”是和“it”关系最大的,这符合常理。

    而从xz的过程,就要借助self-Attention层的三个隐藏的权重矩阵—WQ (查询矩阵)、WK(键矩阵) 、WV(值矩阵)

    2.2.3 self-Attenton编码过程

    1. 首先对于输入的词向量列表x1 x2…xn,我们将其和self-Attention层的三个权重矩阵WQWKWV 相乘,得到三种中间向量Q (查询向量) K键向量V值向量)。例如我们现在有两个单词:“Thinking”和“Machines”的初始词向量x1和x2,我们做的操作如下:
      在这里插入图片描述
      可能到这里你会觉得很懵逼,为什么要有这三个中间向量?这里先卖个关子,下文会解释其中的原因。
    2. 接下来我们将用Q和K这两个中间向量来计算self-Attention的分数。比方说我们想要知道“Thinking”这个单词应该对句子上下文的其他单词投入多少注意力,我们要做的就是计算Q1*K1、Q1*K2,然后把这两个分数除以一个常数(目的是为了让梯度更稳定),再经过softmax归一化,如下图
      在这里插入图片描述
      经过计算,“Thinking”自己和自己的关系最近,self-Attention分数达到0.88,这个值就代表了之前我们可视化过的 注意力程度(8是论文中使用的键向量的维数64的平方根,这里也可以使用其它值,8只是默认值)在这里插入图片描述
    3. 最后,我们将上面得到的归一化之后的分数和第一步获得的中间向量V1 V2相乘求和,随后就得到了“Thinking”这个单词在该层的自注意力向量z1,如下图。
      在这里插入图片描述

    2.2.4 Q、K、V的灵感来源

    很多blog在谈及这三个向量的时候总是顺理成章地带过,很少有解释的。其实这三个中间向量对应三个概念:Queries、Key、Value,其灵感来自于早期的文件检索。
    比如说我们现在要检索的内容是“iron man”,而我们的文件库中有下面的信息:

       Key    :   Value
    ‘iron man’:   史塔克
    ‘thunder’ :   索尔
    ‘spider’  :   帕克
    

    这个时候Q就是我们要查询的内容‘iron man’,利用Q分别和文件库中的K1 K2 K3相乘得到一个相似程度的分数,比方说结果是0.73 0.07 0.20,根据这个分数查找到我们需要检索的内容,即“iron man --史塔克” 。
    self-Attention就是运用了这个灵感,来计算每个单词和其他单词之间的关联程度,即自注意力大小。

    2.2.5 Multi-headed attention

    到现在为止,你应该已经对Transformer的核心概念有了直观的理解。事实上,为了能够从不同角度捕捉不同的关联程度,Transformer利用了一种"多头"的self-Attention机制。即一个self-Attention层拥有多组WQWKWV ,每组分别用于不同的特征提取。
    在这里插入图片描述
    下面这幅图是之前已经见过的注意力可视化:
    在这里插入图片描述
    如果把橘色当作是“it”指代名词的投入注意力程度,那么也可以用其他颜色,从其他角度,来展示其他单词和"it"关系的远近。比方说下面的绿色就可以表示“it”现在所处状态:
    在这里插入图片描述
    可以看到和“it”关系最大的是‘tired’,同样我们可以把其他head都可视化:
    在这里插入图片描述
    也就是说,每个“头”都将生成一组独立的z
    在这里插入图片描述
    而Multi-headed attention最后做的就是把所有“头”通过一个高纬度的矩阵W0 put together
    在这里插入图片描述
    把上述整个过程放到一起:
    在这里插入图片描述
    到此,我们对self-Attention层也算是彻底讲清楚了

    2.2.6 其他细节

    2.2.6.1 Position Encoding

    由于self-Attention的机制,使其不能像RNN那样捕捉到token之间的空间、序列关系,即很有可能会丢失位置信息。比方说:

    我吃坏了'东西',得去拉'肚子' vs 我吃坏了'肚子',得去拉'东西'
    

    如果仅仅利用self-Attention,很有可能模型会对上述两个句子产生混淆(显然后者并不那么文雅…),因为“肚子”、“东西”这两个名词和“吃”、"拉"两个动词之间貌似并没有很强的关联性(别问我为什么没有关联性,问就是奥里给!)
    解决的办法就是引入一个新的Embedding:Position Encoding(位置编码),而Transformer采用的是正弦波,目的是为了模拟信号的周期变化。这种周期性的循环一定程度上会加强模型的泛化能力。
    在这里插入图片描述
    通过将融入Position Encoding之后的向量可视化,可以观察到两个token的空间距离越近,他们位置编码的点积结果就越大,这也符合逻辑。
    在这里插入图片描述
    当然,也可以选择其他的Position Encoding策略,甚至可以一开始把Position Encoding随机初始化,通过训练来更新(BERT的做法)

    2.2.6.2 残差连接&Layer Normalization

    除了到目前为止介绍的"多头机制"和position encoding,作者还建议再self-Attetion上使用Residul(残差)和Layer Normalization(层正则化),这些也都不是必须的,但是作者建议使用。

    1. Residual:残差最初是出现在CV中的ResNet,用于解决网络层数过深导致训练无法收敛的问题。具体细节可以参考其他blog.这里不作过多赘述。
      在这里插入图片描述

    2. Layer Normalization:层正则是Toronto Univercity于16年提出的,它和我们最熟悉的Batch Normalization使用的方法一模一样。只不过这里是对每一层的结果 at 求出均值和方差,而不是对Batch.
      在这里插入图片描述
      至于这两者之间的区别可以用下面这张图体现(红色是Batch,黄色是Layer):
      在这里插入图片描述
      也就是说在进行Layer Normalization的时候,我们并不去关心Batch中其他的向量,只关心当前一层进行正则化,从而避免了和Batch的强相关(Batch normalization则是以Batch为单位求均值、方差,进行正则划)

      原论文:Layer Normalization

    最终,一个完整的encoder将会是这样:
    在这里插入图片描述

    2.2.7 self-Attention解码过程

    解码器和编码器的工作原理几乎相同。编码器的顶端最后会输出一个包含K、V的注意力向量集合,这个向量集将被为给解码器的“编码解码注意力层”,这些层可以帮助解码器关注输入序列哪些位置需要被添加注意力。
    解码器最终的输出将会经过一个softmax的转换,得到下一个要吐出来的单词的概率分布(其实就是个Language Model了)。具体细节这里不再过多赘述,若觉得有地方显得模糊可以参考blog:
    图解Transformer(完整版)

    Transformer的最终结构如下图:
    在这里插入图片描述

    2.3 回到BERT

    上文花了大量笔墨介绍Transformer,因为它对于BERT来说是一个关键点,到这里相信你已经对BERT的核心-Transformer有了比较直观的理解了,接下来对于BERT的理解也将变得简单。
    PS:如果你对 Transformer还有不清楚的,建议跳转到文末,查询对应的 blog,或者自行查阅资料。

    基于ELMo和Transformer,BERT想要做的就是 既保留ELMo的序列结构,又拥有Transformer的真正双向的特点,和它的并行计算的能力,从而进一步增加词向量模型泛化能力,充分描述字符级、词级、句子级甚至句间关系特征。因此,BERT采用了一种“Bi-Transformer”的整体结构.
    在这里插入图片描述
    为了达到这个目的,BERT就必须解决ELMo遗留的问题,尤其是如何解决照镜子的难题,这对于任何一个想要同时考虑双向的模型来说都是绕不过去的。
    因此,BERT将其训练的任务分为两个部分:MLM(Masked language Model)+ NSP(Next Sentence Prediction)

    2.3.1 MLM–困难版完形填空

    2.3.1.1 任务描述

    Masked language Model(随机掩盖原词)这个任务是这样的:给定一句话,随机将这句话中15%的token给Mask掉,然后利用剩下的token去预测被Mask掉的原词。
    由于Mask这个操作是与训练过程中引入的,对于下游的NLP具体任务来说,Mask并不存在,因此BERT在Mask的位置按照一定比例进行了如下操作来更改token:

    1. 80%的概率用“[Mask]”特殊标记掩盖 -------------------------------This sexy gril is my [Mask]
    2. 10%的概率用语料中的随机采样来的token替换原词-----------------This sexy boy is my girlfriend
    3. 10%的概率不做任何替换----------------------------------------------This sexy gril is my girlfriend

    2.3.1.2 带来的效果

    这个任务带来的好处是会迫使模型去理解上下文,并且赋予了模型一定程度的纠错能力。这很好理解,就和我们自己高中的时候刷完型一样,为了填空,我们必须仔细地斟酌上下文地语境,根据语境去预测这个Mask是动词、名词或者形容词?动词的话时态如何,是否第三人称单数?名词的话是否是单复数+s?更何况这还不是一般的完形填空,因为MLM的机制,不仅可能把原词Mask了,更可怕的是会出现随机替换的现象(比方说上面的 This sexy boy is my girlfriend),这无疑是一种困难版地完形填空,给人的感觉就仿佛是在有意刁难。但是正是这种刁难,让模型的训练变得十分有效果,它必须尽全力去观测上下文,时态、指代…
    而BERT的每一个cell采用的是Transformer,这很大地促动了模型对于任务的达成。因为我们已经知道,Transformer擅长的就是通过Multi-head来抽取语料中不同的上下文特征。
    在这里插入图片描述

    2.3.2 NSP–简化版段落重排

    2.3.2.1 任务描述

    Next Sentence Prediction(下一个句子预测)这个任务是这样的:给定一个句对,通过对这两句话的分析,判断第二句话是否真的是第一句话在原文中的下一句。
    训练时,句对的第二句将会以50%的概率从全部文本(注意这里的“全部文本”,下文介绍的ALBERT抓住了这一点进行了优化)中随机抽取,剩下50%的概率选取第一个句子在原文中的真正的下一句。
    熟悉Word2vec的同学可能会想到负采样,其实这里也是一样,即随机抽选一个负样本,在挑出正样本,通过二分类进行预测,只不过NSP是建立在sentence-level之上的。

    2.3.2.2 带来的效果

    正是由于sentence-level的加入,BERT在一些需要依赖句子级信息的NLP下游任务中迸发了很强的潜力,(QA、SLI…)而不是仅仅学到一堆token级别的词向量,这点也很好地解决了ELMo中,需要根据下游任务调整各层隐藏层权重的问题。

    2.3.2.3 实现的细节

    1. [SEP]&[CLS] :为了实现sentence-level,BERT在每个input前面加了特殊标记 [CLS] ,在每个句子的结尾部分加了另一个特殊标记 [SEP]
      [SEP] 很好理解,作为一个句子结尾的记号,关键在于对 [CLS] 的理解。 [CLS] 的作用其实就是将整个句对/句子的上层抽象信息作为最终的最高隐层输入softmax中。

      就拿一个句对来说,通过BERT的上下文信息提取,让Transformer对 [CLS] 进行深度encoding,由于Transformer是可以无视空间和距离地把全局信息encoding进每个位置的,因此 [CLS] 最终将会提取到有关于两个句对之间的高层抽象关系
      如下图,提取的有关于句对的信息将会被输入到一个分类器,输出预测结果。
      在这里插入图片描述
    2. Position embedding:和Transformer一样,BERT也会丢失一些token之间的空间距离关系。在Transformer中,Posisuijiction encoding是采用了正弦波,而BERT显得简单粗暴,直接将Position encoding随机初始化,通过反向传播更新。
    3. Segment embedding:由于是sentence-level,所以输入中还必须加入一个flag来区分两个句子。最简单的做法是加0/1区分。

    所以总结一下,BERT做的事情就是把Token Embedding、Position embedding、Segment embedding粗暴地concat(没错,就是直接concat),并且在句子/句对中加上了 [SEP]&[CLS] 两个标记。把上述put together就是类似与下面这幅图:
    在这里插入图片描述
    PS.对于BERT直接将三个Embedding暴力concat的做法,其实是有很多争论的,有兴趣可以点击下面这个链接,参与讨(zui)论(pao):
    神仙打架传送门

    2.4 BERT的优点

    相较于ELMo,BERT的成功总结一下有下面几点:

    1. 同时双向:这点得益于Transformer的self-Attention.
    2. 遮住了镜子:禁止套娃。在介绍ELMo的时候提到过,双向条件会让每个词在多层次语境中间接地看到自己。而BERT解决这个问题的一个杀招就是MLM,即掩码。MLM这个任务被加入到BERT的训练,产生的效果就是直接把镜子遮住了,或者说直接把自己给遮住了。
      在预测15%被Mask掉的token时,模型根本没办法,或者说根本不敢去相信自己察觉到的信息,因为很有可能这个信息是个错误的引导(典型的DAE LM,引入噪音,增加鲁棒,下文会提到)
    3. 模型更加复杂化:采用两种任务联合训练的模式,使得模型输出的每个字、词都尽可能地、全面地、准确地刻画出文本的整体信息,为后续的fine-tuning做了更多有效的工作。另外不得不提的一点是,BERT相较于ELMo,拥有更多的训练数据和模型参数。(参数过多其实也是一个潜在的缺点,而ALBERT的优化也抓住了这一点,下文会介绍)

    至于BERT的缺点,将会在下文介绍XLNet之前罗列。

    3. ALBERT–BERT的“减肥计划”

    3.1 诞生背景

    ALBERT是19年由Google蓝振忠等人发表的一种轻量级BERT,是BERT众多变体中的一种。可谓是将原先的BRET进行了一次“大瘦身”。其最初的设计灵感来源于卷积视觉中的AlexNet(因为蓝振忠博士在研究生阶段就是做CV出身的),旨在通过将BERT中的模型参数大幅度减少,解决参数过多超出内存导致无法将网络加深、加宽的问题。
    下图展示了AlexNet在网络加深情况下,模型效果的变化,可以看出随着模型深度增加,其精度也在大幅上升。
    在这里插入图片描述

    3.2 ALBERT的参数压缩

    蓝振忠博士和其组内成员分析了BERT的参数组成,将模型参数分为两部分:

    • Token Embedding-----------约占20%
    • Attention Feed-forward-----约占80%

    因此,蓝博决定从上面两个方向去对BERT参数进行改动。

    3.2.1 Token Embedding

    在Token Embedding层,BERT的做法是将每个token的one-hot编码连入一个高维的平坦层。如下图:
    在这里插入图片描述
    这种做法带来的副作用是很明显的:

    • 反向传播更新参数,只能更新hot的那一个点,过于稀疏(比方说词表长度为3W,每次却只能更新一个)
    • Token Embedding 时,每个token还是上下文无关的(context independent)而没有融合上下文信息的token,其含义其实是非常简单的(你可以理解成词与词之间是正交的,one-hot之间毫无关联),因此没有必要将其展成那么高的维度。

    因此可以采用如下方法将Token Embedding层参数减少:
    在这里插入图片描述
    在Token Embedding的过程中,先将向量压缩成一个低维的表示,在真正需要进行Attention操作之前,再把Token Embedding的维度提升。这样的做法会带来两个好处:

    • size减小:Token Embedding size维度减少.上图中最左边的那一层需要通过和一个矩阵相乘得到第二层,一旦第二层维度减少这个矩阵参数也自然变少。
    • 参数转移:很明显,对于BERT来说,其参数有用的部分是集中在context dependent的层上,而不涉及context的Token EMbedding 拥有的大量参数是浪费的。一旦将这部分参数减少,就可以将后面的Hidden-layer embeddings 加深(上图红色层),提高参数的利用率。

    所以这一步通过将原本一个高纬度网络分解为两个较低维度的中间网络,实现了从O(V x H)到O(V x E + E x H)的转变(E是远小于H的)
    在这里插入图片描述
    通过实验验证得到结果(采用多个下游任务取Average进行评估):
    在这里插入图片描述
    可以看到实验结果表示,在模型性能基本不变的情况下(0.6%),Token Embedding层的参数减少了80%以上。

    3.2.2 Attention&Feed-Forward

    蓝博参考了论文“Efficient training of bert by progressively stacking”对 Attention&Feed-Forward 层可视化的结果,发现对于encoder的各层,其参数在结构上呈现一种相似性(下图中的第一列其实就是[CLS]标记,每一个encoder层除了参数大小不一样,分布结构上呈现很强的一致性):
    在这里插入图片描述
    原论文:Efficient training of bert by progressively stacking
    如果能够把所有 Attention&Feed-Forward 层的参数进行共享,那么BERT模型的参数又将是下降一个数量级的
    在这里插入图片描述
    实验得出的结果如下:
    在这里插入图片描述
    在模型深度保持不变的前提下,将 Attention&Feed-Forward 层的所有参数共享,使得参数从原先的108M降低到了31M,而精度并没有太大的降低(2.5%)
    在这里插入图片描述
    如果再观察地仔细一点,会发现只共享FFN参数的精度和将attention&FFN所有参数共享的精度几乎一致(0.3%),而FFN参数一旦共享,精度就会下降比较明显。说明精度的降低主要是因为FFN参数的共享。从这个角度来看,Attetion层的参数完全可以放心大胆共享
    但是考虑到Attention层的参数占整个encoder的不到3/8,因此蓝博决定将所有参数进行共享

    3.2.3 瘦身之后的结果

    在上述两种方法实行基础上,得到的实验结果:
    在这里插入图片描述
    最终,ALBERT将Token Embedding 层参数从原先E=768降低到E=128,并将Attention&Feed-Forward层的所有参数进行共享。将BERT最开始108M的参数压缩到12M,同时精度仅掉了2.2%!

    随后,兰博团队将压缩之后的网络进行了大规模的加宽、加深,并且进行了长时间的训练。最终的结果如下:

    在这里插入图片描述最终30%参数减少的情况下,还提升了ALBERT 3.5%的精度,这个结果可以说是非常不错的了。但是这样做的代价同样很昂贵,ALBERT花了BERT3倍的时间才将模型训练至收敛。(因为毕竟网络被加宽加深,模型变得比BERT还要复杂,而这些新增的参数还是需要被更新,更何况这些参数还都是被refine共享过的,所以在计算上没有地方可以偷懒。一种典型的以时间换空间的做法)

    3.3 ALBERT在其他细节上的优化

    除了将BERT的体积大幅减少,ALBERT还针对BERT隐藏的一些缺点进行了修正。

    3.3.1 SOP代替NSP

    我们知道BERT的sentence-level实现的很重要的一点就是进行了NSP(Next Sentence Prediction)。其意图是为了训练模型对句子连续性的把握,但是对于负样本的采样,BERT的策略却是从整个语料库中随机抽取样本,这将会带来一个潜在的问题:网络很有可能学到句对间topic的判别,而不是句对连续性的把握。显然,前者的难度远低于后者,但是这并不是BERT的本意。这也就是为何在许多下游任务中,将NSP这个任务去掉,BERT产生的性能反而更好的原因。
    ALBERT做出的一个很重要的改进就是强制让模型去辨别句对连续性,即SOP(Sentence Oder Prediction),而它采用的策略极其简单—负样本的获得,是通过正样本两个句对顺序的颠倒。看似简单的操作,却保障了负样本的两个句子是从属于同一个topic,而句序是明显错误的效果。
    下图是SOP代替了NSP之后产生的效果:
    在这里插入图片描述
    可以看到SOP不管是在真正的SOP任务上,精度达到86.5%,同时对于真正的下游NSP任务,效果也不是很差,达到了78.9%(蓝色框)。但是NSP明显只能在NSP任务上取得high score(一方面也是因为这样的任务更简单)

    3.3.2 去除MLM中的drop out

    这个操作其实不难理解。可以从两个角度去解释:

    • BERT的训练本就是基于大量的网络文本、书籍文献。从这个角度讲,BERT其实完全没有必要去担心over fitting.由于MLM本身就是一个非常有难度的任务(困难版完形),没必要再去添加噪声刁难它,只要拥有足够的算力,BERT理论上只会越学越好。
    • MLM这个操作其实已经引入了很强的噪声(DAE LM),从这个角度看,drop out 也只是多此一举。

    在去掉了drop out 之后,下游任务貌似只有轻微的提升:
    在这里插入图片描述
    但是它带来的内存上的优化却是十分明显的:
    在这里插入图片描述
    红色框圈起来的地方是去除drop out之前,可以明显看出其内存占用更高(每一个条纹宽度是去除drop out之后的三倍左右)。这也是可以解释的,因为drop out 会产生许多临时变量,而这些变量对于ALBERT来说是没有用处的。

    4. Autoregressive vs Auto-encoding

    前文已经将BERT、ALBERT的大致内容介绍了,为了介绍最后一个XLNet,必须先知道两个很重要的概念:AutoregressiveAuto-encoding。从XLNet的关注点,去分析这两大范式的优缺点。

    4.1 定义

    • Autoregressive(自回归模型):自回归是时间序列分析或者信号处理领域喜欢用的一个术语。可以直接理解成语言模型,即一种基于上文预测下文,或者基于下文预测上文的语言模型。典型的代表是GPT、ELMo.
    • Auto-encoding(自编码模型):什么叫做自编码?说的通俗一点,其实就是一个自个儿和自个儿玩的模型(自己和自己下棋,自己和自己对唱…玩着玩着棋艺就变高了,玩着玩着唱功就变好了)。最开始自编码的提出是想要通过一个深度网络,对数据进行压缩成低纬,之后解压缩还原原始数据,目的为了获得输入数据的更加有效的表示。但是这样的一种模式非常适用于无监督的pre-training.
      和CV不同,在NLP领域,我们不得不面对的一个问题就是我们有大量的数据,可是我们没有标注。正是由于这个原因,完全无监督预训练网络对于许多label稀少的下游任务来说十分重要,而Auto-encoding这种LM则正是NLP迁移学习的一个理想模型,就如本文开篇说的那样。而BERT就是Auto-encoding的一个典型代表。不仅如此,BERT采用了MLM,在输入层加入一定的噪音,是一种典型的DAE LM(Denoising Autoencoder)。

    4.2 优缺点比较

    • Autoregressive:自回归模型的一般形式可以用下面这幅图表示
      在这里插入图片描述
      自回归模型旨在利用序列的单向迭代,通过已知的上文信息Pθ(Xt | X<t)的累积,来预测某个位置单词的概率Pθ(X),模型的目标是最大化这个预测概率。这是一种再常见不过的LM。

    优点

    1. training和fine-tuning具有一致性,天然匹配某些下游任务。(文本生成类,一个个往外面吐字的,如机器翻译、摘要…)
    2. 模型没有基于条件独立假设(目标函数中用的是"="符号,从概率的角度讲这个性质很好,这点需要和Auto-encoding进行对比)。

    缺点

      • 无法实现同时双向(这点已经强调很多次了)

    • Auto-encoding:自编码模型表示如下图
      在这里插入图片描述
      由于是DAE LM,因此在目标函数中加了一个mt,取值为[0,1],作用相当于对被Mask掉的单词的开关。
      有人可能会注意到,这里的目标函数和Autoregressive不一样,很重要的一点是Auto-encoding的目标函数中间用的是约等于符号。这是因为等号右边式子的值并不服从product rule(条件概率的乘法法则),所以Auto-encoding模型具有独立性假设
      比方说上图,把句字New York is a city的New York给mask了,目标是预测被遮住的New York,即求得 P(New York|is a city) 并将其最大化(先不考虑log)。这里先把“is a city”当作是一个整体,把它用C代替,“New"和“York“分别用A、B表示。所以我们的目标就是max P(AB|C)
      为了计算这个联合概率,假设事件A和事件B是条件独立的,即P(AB|C)≈P(A|C)*P(B|C)
      放到上面这个句子,也就是P(New York|is a city)≈P(New|is a city)*P(York|is a city)。但是很明显,对于"New”、"York"来说,离开了其中任何一个,另一个剩下的也失去了原本含义,因为本身这两个token是属于一个单词的。在遇到这种情况时,Auto-encoding的条件独立假设就会存在比较严重的问题,因为它强行将token之间原本可能存在的关联性给打破了。接下来列一下Auto-encoding的优缺点。

    优点

      • 同时双向

    缺点:

    1. 模型基于条件独立假设
    2. training和fine-tuning不一致(因为在训练过程中引入了特殊的掩码Mask,这是下游任务不存在的)

    从这里可以发现,Autoregressive 和 Auto-encoding之间的优缺点刚好反一下,那么有没有一种模型,可以兼顾Autoregressive 和 Auto-encoding的优点呢?这也就是XLNet的诞生初衷。

    5. XLNet–似乱非乱两相随

    5.1 诞生初衷

    在XLNet诞生之前,预训练方面存在着两大阵营,分别是以ELMo、GPT一众为代表的AR(Auto regressive) 和以BERT为首的 AE(Auto-encoding)。这两大阵营都存在着各自的优点和缺点,但是其优缺点大致上呈现一种互补关系。为了能够同时兼顾这两大范式的优点,19年Google brain结合了当时最先进的自回归模型Transformer-XL,提出了一种采用泛化自回归,克服BERT等自编码模型缺点的新模型—XLNet.
    XLNet一出,BERT之前连战11项的巅峰记录被无情刷了下去。XLNet在20 个任务上超过了BERT的表现,并在18个任务上取得了当前最佳效果,其中包括了包括机器问答、自然语言推断、情感分析和文档排序。
    那么XLNet到底是采用了什么方法进行AR与AE的融合的呢?
    归根到底一句话:似乱非乱两相随

    5.2 Permutation Language model–似乱非乱

    XLNet采用的是自回归模型,同时运用了BERT引以为傲的MLM训练模式,即给定一个句子,将其中的一部分单词进行Mask,然后利用剩下的句子信息去还原被mask的单词。由于是采用自回归,BERT这种自编码模型的两个缺点:训练与下游不协调、独立性假设损失关联性, XLNet就可以很自然地避开。那么XLNet要解决的首要问题就是如何实现同时双向融合
    为了解决这个难题,XLNet想出了一个聪明的点子----Permutation Language model(乱序语言模型)。

    5.2.1 直观理解

    所谓乱序,道理很简单,就是把输入的句子进行全排列,每一种排列方式进行一次Mask预测,最后取所有全排列下的期望值。比方说现在输入的句子是:boy next door,把’next‘用Mask替换掉,如果不采用全排列,直接将输入喂给AR,也就是"boy [Mask] door",那么模型只能单向地提取到"boy"的信息。
    而Permutation Language model要做的就是将上述的句子产生3!种排列情况:

    boy [Mask] door
    boy door [Mask] 
    [Mask] boy door
    [Mask] door boy
    door [Mask] boy
    door boy [Mask]
    

    对于每一种排列,单向地计算[Mask]位置的原词概率,计算的公式就是之前讲过的AR的product rule.
    在这里插入图片描述
    最后将6种排列的结果取一个期望,由于所有的排列情况都被考虑进去,在预测过程中,Mask的位置就已经获得了上下文的全部信息
    XLNet就是通过这种将输入顺序打乱重排的方法,来实现基于AR的同时双向。
    当然,在实际训练的过程中,由于输入的长度会很大,因此计算所有排列情况实际上是不可行的,这会产生巨大的时间消耗。因此XLNet每次只在全排列中取一部分。

    5.2.2 实现细节

    在实现这个乱序采样的过程中,XLNet做的并不是真的将输入打乱顺序,而是采用了打乱order的方法。具体过程如下图:
    在这里插入图片描述
    ZT 表示所有order组成的集合。拿之前那个例子来说就是:

    boy     [Mask]   door ---------【1,2,3】
    boy      door   [Mask] --------【1,3,2】
    [Mask]   boy     door----------【2,1,3】
    [Mask]   door    boy-----------【2,3,1】
    door    [Mask]   boy-----------【3,2,1】
    door     boy    [Mask]---------【3,1,2】
    

    ZT ={【1,2,3】、【1,3,2】、【2,1,3】、【2,3,1】、【3,2,1】、【3,1,2】}
    而每次要做的,就是从ZT 中抽取一个order Z ,比方说 Z =【2,3,1】,然后用这个order去计算Mask位置的概率:P(boy door next)=P(boy)*P(door|boy)*P(next|boy door)
    然后重复上述抽样过程。


    再详细一点,假如说现在输入有四个单词,被Mask的是3号位置的单词,我们可以看一下不同order下的不同处理。
    如果现在随机抽到的order是【3,2,4,1】,那么这个过程就会像下图。由于现在的order里,3是第一个,所以它获取不到任何信息,因此计算 h3(1) 只会用到一个默认的cell–mem。
    在这里插入图片描述
    假设order是【2,4,3,1】,那么计算 h3(1) 将会需要2、4的信息:
    在这里插入图片描述
    【1,4,2,3】:
    在这里插入图片描述
    【4,3,1,2】:
    在这里插入图片描述
    所以可以看到,所谓的打乱并不会真的改变输入序列的顺序,只是将order随机打乱,再根据order去获得预测Mask需要考虑位置。因此可以说Permutation Language Model是 似乱非乱.


    另外,XLNet在实现不同的order上采用的是 Attention Mask 矩阵,例如【3,2,4,1】,他的 Attention Mask 矩阵就是下面这幅图:
    在这里插入图片描述
    比方说现在要预测4,那么需要考虑的上文是3和2, Attention Mask 矩阵的第4行就只有2,3两列是1,其他地方是0;再比如要预测3,这个order下3前面没有任何信息,所以 Attention Mask 矩阵的第三行全为0.

    5.3 Two-Stream Self-Attention–两相随

    5.3.1 位置信息的丢失

    到目前位置,我们可以看到,利用全排序的XLNet既解决了自编码模型的缺点,又能同时捕捉上下文信息。那么它是否已经是没有问题存在的了呢?答案是否定的。

    5.3.1.1 standard parameterization

    我们知道,标准的AR目标函数是长这幅样子的:
    在这里插入图片描述
    比方说:P(ABC)=P(A)*P(B|A)*P(C|AB)即product rule。这个式子其实可以简化成下面的这种形式:P(ABC)=hθ(A:B) * e(C)
    hθ函数用于将A与B包含的信息经过线性转换融合为张量,e函数则将C也转换成张量的形式,即AB融合的张量与C张量做点积。因为 P(A)*P(B|A)*P(C|AB)的含义不就是在考虑A与B的条件下,计算C吗。所以可以通过这种转化为张量的方式,以hθ(A:B) 和 e( C )点积,得到两个张量的相近程度。这从逻辑上是可以解释的。
    所以,上述AR的标准目标函数还可以转化成下面这种形式:
    在这里插入图片描述
    X1:t-1表示的是t位置之前的所有时间步,经过hθ转化为张量和e( C)点积。由于等式左边是概率,因此我们将点积结果经过softmax的归一化,变成概率的形式。

    这样的标准形式存在的一个问题就是会导致位置信息的丢失
    比方说现在的输入是这样一个句子:

    哈利 波特 是 一个 优秀的 魔法师
    

    假设我们运气不好,Mask的时候"哈利"、"波特"两个字刚好都被遮住了。

    [Mask] [Mask] 是 一个 优秀的 魔法师
    

    随后我们按照XLNet的做法对order集合 ZT 进行抽样,我们运气又很差,抽到下面这个排列:

    是 一个 优秀的 魔法师 [Mask] [Mask] 
    

    由于被预测的两个Mask都被排在最后两个字,因此在从左到右计算任何一个位置的Mask时,计算出来的"哈利"和"波特"概率结果是一模一样的(如果现在问你这两个Mask哪个是"哈利",你也不知道了,这就是乱序带来的问题)。

    5.3.1.2 new parameterization

    为了解决位置信息丢失的问题,XLNet在标准目标函数的基础上加了一个位置信息 Zt (区别于之前的order集合 ZT ),得到了新的目标函数:
    在这里插入图片描述
    所以这个位置信息 Zt 应该怎样结合到我们的输入里面去呢?
    回忆一下BERT,BERT也会遇到有关于位置信息丢失的情况(我吃坏了'东西',得去拉'肚子' vs 我吃坏了'肚子',得去拉'东西'),而它采用的做法十分暴力,直接将position Embedding 和输入的Token Embedding直接拼接。那么是否在XLNet里也可以把位置信息(position Embedding)和内容信息(Token Embedding)直接结合呢?


    假如说现在的order是【3,2,4,1】,如果我们想要去预测位置2(这里偷不到图,只能手画了…怕乱,所以只画了一部分)
    在这里插入图片描述
    由于2前面只有一个3,那么我们只需要获得3的信息,同时我们也需要获得2的位置信息,因此X2需要向高层仅传递位置信息,而不能透露有关于2的内容。也就是说现在面临的情况是:我们需要将位置信息和内容信息分开

    5.3.2 双流自注意力

    正是由于需要将位置信息和内容信息分开,XLNet使用了一种叫做Two-Stream Self-Attention(双流自注意力)的方法。它将每个Transformer cell分为两个部分:

    • content representation:包含了所有上下文信息(内容+位置)
    • query representation:仅包含了当前的位置信息

    所以还是之前的那个例子,现在的模型将会变成如下(画的丑了点,将就一下…):
    在这里插入图片描述
    红色的路径表示位置信息的传播,黑色的是全部信息,可以看到实现双流自注意力之后,XLNet可以做到不向模型透露内容的前提下,加入有关于预测位置的位置信息。

    5.3.2.1 K、Q、V的分流

    上面为了简单,将每一个Transformer cell看作是两部分,实际上XLNet实现双流用到了之前讲到过的,self-Attention层的三个中间矩阵:WK、WQ、WV
    为了实现双流自注意力,XLNet的做法是将Mask仅与 WQ 做点积,而将其与WK WV 完全隔绝。
    这样的做法不难理解,因为之前讲过,Transformer的每一个输出 Z ,其内容来源是 V1:t ,通过将 V 与一个分数加权求和得到 Z , 而这个分数是来自Q K 的点积再经过归一得到的概率,以此分配每个 V 的权重。因此对于上层来说,V内容信息的组成,而 K分配注意力权重的依据。所以这两个信息自然不能混入有关Mask的内容。
    在这里插入图片描述
    比如上面这幅图,黑线代表全部信息,后面三个词的全部信息将会传给K与V;红线、蓝线分别代表位置和内容,第一个词仅内容信息传递给K、V,而其全部信息是作为查询向量传入Q的。
    这种做法还有另一个好处,由于K、V无法获得有关于Mask的信息,使得BERT之前存在的与下游任务fine-tuning不一致的问题也得到了解决。因此这种做法也可以应用到BERT中。

    5.3.2.2 数学上的表达

    最后给出h、g函数的数学表达:
    对于content representation而言,它和传统的Transformer一样,是包含了所有上下文信息的表示,计算公式如下:
    在这里插入图片描述
    对应使用的Attention mask矩阵:
    在这里插入图片描述
    而对于query representation,他的数学表达式:
    在这里插入图片描述
    对应的Attention mask矩阵:
    在这里插入图片描述
    可以看到,它的对角线是空的,因为每个位置只能包含当前位置信息。


    到这里为止,有关于XLNet的主要内容也算是介绍好了,其实还有一部分关于Transformer-XL的细节这里没有提及,碍于篇幅暂且省略(其实是懒 )。具体细节如果还有不明白的可以参考下面这篇blog:
    XLNet原理解读
    有关于XLNet具体细节可以尝试阅读原论文:
    XLNet: Generalized Autoregressive Pretraining for Language Understanding

    6. 总结

    我夜观天象,掐指一算,看到这里之人必定是骨骼精奇,若是给打通了任督二脉,还不得飞上天去
    首先,能看到这里的说明朋友你耐心不错,还是想感谢一下的。这篇文章将近两万字,也花了自己一天一夜,中间没怎么休息。刚开始也想过把文章分成5篇,但是考虑到发文初衷就是为了能够把有关BERT的前因后果,往来始终串起来,若是打散,就失去了梳理、整合的初心。还是想通过这篇文章,一方面让自己做个总结,另一方面帮助到一些像我一样的小白。很多内容都是自己花了心思的,渴求能够把一些核心的思想直观通俗地表达出去。
    有关于这些前沿的模型,光看blog是不够的,只有摸过论文、代码才能正真get好多细节。
    另外,自己是初学,可能有很多地方理解或者描述地不到位的,还望指正。


    2020-7-10凌晨更新

    凌晨突然看到一条推送,chris(cs224n那个快乐的老爷爷)报告指出,BERT能够学到句法结构信息。他的团队通过将BERT吐出来的 contextual embedding投射到低维空间(为了避免特定语境造成的干扰),随后在这些低纬embedding上生成一棵最小生成树,发现这棵树的结构居然和人工句法分析标注的树形很相近(如下图)
    在这里插入图片描述
    同时,通过对multi self-attention某些头的attention分数进行分析,他们也发现BERT某些头还能够学到依存句法共指特征,而这些本就是我们人类期望LM能够学到的hand-crafted的语法特征,BERT成功学到了;这些很可能得益于他的自监督训练。(还是只能说明BERT那两个自监督训练太巧妙了,因为模型架构再复杂、巧妙只是提升了模型能够达到的上限,而预训练的任务则驱动模型达到他的最佳性能,帮助触碰到他的上限


    参考资料

    展开全文
  • 多个分类器集成可以获得超过单个分类器的效果,但集成分类器就要求有多个分类器,在训练速度和测试速度方面不占优势。本文提出的方法可以提高集成学习的训练速度,通过一次训练,获得多个分类器,解决了集成学习训练...

    参考论文

    SNAPSHOT ENSEMBLES: TRAIN 1, GET M FOR FREE

    解决的问题

    多个分类器集成可以获得超过单个分类器的效果,但集成分类器就要求有多个分类器,在训练速度和测试速度方面不占优势。本文提出的方法可以提高集成学习的训练速度,通过一次训练,获得多个分类器,解决了集成学习训练速度慢的问题。
    overview

    解决方法

    深度学习训练过程中,只有经历足够长的epoch后,test loss才会随着lr的降低而降低,这说明loss空间中存在的局部最小值点是稳定的,这些局部最小值点的模型从不同方面描述了特征空间,可以用于集成学习。本文提出的方法主要有以下两点

    • 周期性学习率策略
      和常规学习率策略不同,本文把整个训练过程平均划分成M个阶段,每个阶段内学习率lr都从一个固定初始值开始,逐步降低。每个阶段结束后都保留一个snapshot,训练结束后一共获得M个snapshot,从后面抽取m个组成集成分类器。如此在一次训练中,获得M个模型,训练速度和常规训练方法一样。
      cycle-lr

    • 利用前一阶段的结果初始化当前阶段模型
      对于简单的模型而言,这一个步骤没那么重要,没有这一步也可以得到不错的结果。对于复杂的模型,这一步十分重要,因为复杂的模型需要较多的epoch才可以收敛到稳定的局部极小值点,除非增加每个阶段的epoch(同时也增大了整个训练时间),否则很难得到较好的结果

    实验分析

    对比实验

    exp-main-result

    • single model: 采用常规lr更新策略,比如每经过1/4训练epoch,学习速率降低一个数量级
    • NoCycle Snapshot Ensemble:不使用周期性学习率更新策略,但依然每个相同epoch抽取一个模型,最后做集成
    • SingleCycle Ensembles:采用周期性学习率更新策略,但是每个阶段模型采用随机初始化,而不是采用前一个阶段的训练结果,这种方式对于简单的模型也可以产生不错的结果,毕竟其遍历了更大的参数空间。但如果模型复杂,则该方法效果变差。

    集成需要的模型数量

    exp-densenet100-cifar
    采用本文方法获得的最后一个阶段的模型的性能是低于常规训练方法得到模型的,但是两个snapshot集成后test loss就降低到baseline之下
    exp-densenet40-ciar
    一般随着集成模型数量的提升,test loss会逐步降低(不一定成立,只是一个大的趋势),但集成多个模型会降低预测速度

    和标准集成学习的比较

    exp-true-ensembling
    和标准集成学习(采用不同初始化参数,使用常规学习率更新策略,完整训练出多个模型做集成)对比,本文的方法效果是没有标准集成学习好的,当然本文中的方法的训练速度要快很多

    展开全文
  • 总之就是相应的网络结构参数越来越,结构越来越复杂。这样无论对于工作还是学习上,想要训练一网络变得越来越困难。迁移学习的概念很好的解决了这问题。在之前的视频场景分类中介绍过,将一张图片输入到一...

    由于计算机计算能力的增强和相关AI芯片的产出,深度学习中的神经网络结构也是朝着更深、更宽等方向发展。总之就是相应的网络结构参数越来越多,结构越来越复杂。这样无论对于工作还是学习上,想要训练一个网络变得越来越困难。迁移学习的概念很好的解决了这个问题。在之前的视频场景分类中介绍过,将一张图片输入到一个网络,在具体的分类层的前一层输出的数据可以看成是这张图片的全局特征。我们只需对相应的特征进行处理。因此,我们可以利用前人训练好的模型的输出数据当做自己设计模型的输入,这样只需训练自己的网络模型结构中的参数既可。由此引来了,如何把自己的和别人的网络模型合并成一个网络模型。

    结构图graph

            TensorFlow的特点就是图和数据项分离,这样增加了网络的灵活性。我们在编写整个网络结构,就类似于画结构图,最后将数据喂给整个网络。但在读别人的程序中,经常会看到Graph,GraphDef,MetaGraph这三个关键词。同样都是图,这三个却又很大的区别。这篇文章对于三种‘图’介绍的不错。在这里做一下总结,Graph是op和tensor的集合。因此在训练阶段画出的图形都是以这种形式保存的,很好的解释了图和数据的分离。GraphDef是序列化后的图,以这种方式存在,相当于数据烧进了图里面,不再有变量的存在。因此,我们在加载或生成pb文件时经常看到GraphDef的定义。MetaGraph相当于是由计算图和相关的元数据构成,这相比于Graph多了些在训练过程中产生的一些数据(比如说训练迭代次数)。以上是我对着三种图的理解,可能存在理解偏差,如有错误请指出。

    由此三种图对应三种读取和存储方法:

    tf.train.Saver()/saver.restore()
    
    tf.train.export_meta_graph/import_meta_graph
    
    tf.train.write_graph()/tf.import_graph_def()

           本文主要是对两个pb文件进行合并,因此,可能看到GraphDef存在的形式比较多。首先这里给出一段遍历pb文件中所有节点的程序,因为在生成还是加载pb文件时,需要知道输入和输出的节点名称。

    graph_def = tf.GraphDef()
        with open(model_path, "rb") as f:
            graph_def.ParseFromString(f.read())
            for i,n in enumerate(graph_def.node):
    			print("node name: %s" % n.name)

    模型合并

            该篇文章写的算是不错的。但是,如果对于‘图’的理解不是那么深,则很容易让思维陷入局限。总之,该篇文章很好的介绍了,把第一个模型的输出作为第二个模型输入,最后保存成一个模型。核心代码就直接拿来用了:

    with tf.Graph().as_default() as g_combined:
      with tf.Session(graph=g_combined) as sess:
    
        x = tf.placeholder(tf.string, name="base64_input")
    
        y, = tf.import_graph_def(g1def, input_map={"input_string:0": x}, return_elements=["DecodeJPGOutput:0"])
    
        z, = tf.import_graph_def(g2def, input_map={"myInput:0": y}, return_elements=["myOutput:0"])
        tf.identity(z, "myOutput")
    
        tf.saved_model.simple_save(sess,
                  "./modelbase64",
                  inputs={"base64_input": x},
                  outputs={"myOutput": z})

    但是如果是这样的场景该怎么办?比如:第一个模型的输出按照条件的不同有可能是第二个模型的输入,也有可能不是(第二个模型的输入和第一个模型的输入相同);第一个模型多个输出的集合作为第二个模型的输入,等等。另外:tf.saved_model.simple_save对于该代码保存的模型,在用常规pb文件加载方式进行加载时出现错误,后面我会介绍另外一种保存方式。

          其实,陷入的思维局限就是:模型的合并是两个模型的串联,第一个模型的输出必须紧跟着第二个模型。可能有人会说,当然是这样啊,不然我合并这两个模型干嘛。但是,如果在工程化上,如果你的图是两个以上甚至十几个,这样维护起来就太麻烦了。

         真正的模型合并其实并不是几张模型图串联起来,想象成并联的说法也不是那么准确。确切的说法感觉应该是,众多子图构成一张大图。如何控制子图之间的联系,不是在生成大图时决定的,而是在加载大图后。以两个子图合成一个大图为例:

        with com_graph.as_default() as g_combined:
            with tf.Session(graph=g_combined) as sess:
                graph_def_one = load_def1(first_pb_path)
                graph_def_two = load_def2(second_pb_path)
                rgbs = tf.placeholder(dtype=tf.uint8,name="video_images")
                features = tf.placeholder(dtype=tf.float32,name="image_feature")
                #first graph load
                Frame_Features = tf.import_graph_def(graph_def_one, input_map={'DecodeJpeg:0': rgbs[:, :, ::-1]},
                                                      return_elements=['pool_3/_reshape:0'])
    
                tf.identity(Frame_Features, "pool_3/_reshape:0")
                # second graph load
                z, = tf.import_graph_def(graph_def_two, input_map={"Placeholder:0": features}, return_elements=["model/Mul_2:0"])
                tf.identity(z, "model/Mul_2")
                # freeze combined graph
                g_combined_def = graph_util.convert_variables_to_constants(sess, sess.graph_def, ["model/Mul_2","pca_final_feature"])
                tf.train.write_graph(g_combined_def, out_path, 'my_model.pb', as_text=False)
    
    def load_def2(model_path):
        graph_def1 = tf.GraphDef()
        with open(model_path, "rb") as f:
            graph_def1.ParseFromString(f.read())
            return graph_def1

    上面就是将两个模型合成一个模型的代码,可以看到,其实并没有将第一张图的输出当成第二张图的输入。他们彼此没有多大关系,只是每张子图在进行加载的时候,该函数:

    def import_graph_def(graph_def,
                         input_map=None,
                         return_elements=None,
                         name=None,
                         op_dict=None,
                         producer_op_list=None):

    需要指名input(input_map)和输出return_elements(其实也不是必须有,可以输入为None)。因为是输入的pb文件模型,所以加载的图都是graph_def形式。

    通过以上代码就可以将两个模型合并,生成一个总的pb文件。加载和单个pb文件类似。主要是通过

    sess.graph.get_tensor_by_name()

    获取节点名字。

    global graph
        graph = tf.get_default_graph()
        with graph.as_default():
    
            graph_def = tf.GraphDef.FromString(open(model_path, 'rb').read())
            tf.import_graph_def(graph_def, name="")
            with tf.Session() as sess:
                sess.run(tf.global_variables_initializer())
                input_image_tensor = sess.graph.get_tensor_by_name("video_images:0")
                feature = sess.graph.get_tensor_by_name("pool_3/_reshape:0")
                image_feature = sess.graph.get_tensor_by_name("image_feature:0")
                predict_result = sess.graph.get_tensor_by_name("model/Mul_2:0")
                features = []
                coord = tf.train.Coordinator()
                threads = tf.train.start_queue_runners(sess=sess, coord=coord)
                for rgb in frame_iterator(video_file):
                    feature_value, = sess.run(
                        [pca_feature],
                        feed_dict={input_image_tensor: rgb[:, :, ::-1]})
                    features.append(feature_value)
                features = tf.concat(features, axis=0)
                features = tf.reshape(features,[-1,1024])
                features = tf.expand_dims(features,0)
    
                predict_result_value, = sess.run(
                    [predict_result],
                    feed_dict={image_feature: sess.run(features)})
                print('the result is')
                print(predict_result_value)
    
                coord.request_stop()
                coord.join(threads)

    model_path是新pb文件路径。一定要注意的是,新的pb文件的输入,是要按照在生成pb文件的placehold名字,而不是子图的。比如video_images是合成后pb文件的输入节点。其等价于子图节点DecodeJpeg。本文的例子就是,子图一通过frame_iterator()循环函数决定调用次数(假设300次)。然后将这三百次的结果进行append,随后reshape新的矩阵形式的Tensor送入子图二。

    总之,在之前合并模型时都是将子图串起来合成,而这次并不是单纯的串联。所以对之前合并模型的认识有一定的局限。在合成模型的程序时,循环子图三百次得出结果再把结果输送到子图二,结果造成生成的pb文件大小超过上限而导致失败。我想这是因为:在合并的时候,由于这个循环而导致是合成的是301个子模型,并不是两个。那么这张图确实是太大。

    展开全文
  • 但是我看了看整合的代码,和之前没有用angularjs的基本没有什么变化,一些极小的变动也只是基于angularjs的语法,因此完全可以参考之前说些的表单列表展示相关的内容,这里也就直接进入到下一步骤,创建流程模型了...

     

    本来在创建了表单之后应该是表单列表和预览功能,但是我看了看整合的代码,和之前没有用angularjs的基本没有什么变化,一些极小的变动也只是基于angularjs的语法,因此完全可以参考之前说些的表单列表展示相关的内容,这里也就直接进入到下一个步骤,创建流程模型了。

     

    在之前的创建流程模型一节里,我讲代码比较多,实际上在这里还有很重要的一个环节没有细说,那就是自定义流程图,画流程图的过程也是有不少需要注意的事项的,在这一节我会适当的以截图加解释进行说明。

     

    而在创建流程模型的过程中,因为之前也是用java、spring、angularjs等,所以在代码上实际上并没有什么变化,不同之处就在流程图制作上,还有就是activiti内部自己实现,我们不用管他,那么这里着重要讲的就是调用activiti-modeler来画流程图。

     

    我们的创建模型首先是在自己制作的页面填好模型的name、key、description等,然后提交到后台,进而跳转到activiti-modeler流程图制作界面。自定义的页面如下图:

     

     

    填写数据的这个页面代码如下:

     

    <div id="create"style="margin-top:100px;margin-left:290px;background-color:#9cc;height:350px;width:40%;font-size:26px;position:relative;float:left;"> 
        <p style="font-size:30px">创建模型</p>
        Name   :<input type="text" name="name"ng-model="activiti.name"/>
        </br> 
        </br> 
        Key    :<input type="text" name="key"ng-model="activiti.key"/> 
        </br> 
        </br> 
        Description:<input type="text" name="description"ng-model="activiti.description"/> 
        </br> 
        </br> 
        <input style="font-size:28px;cursor:pointerborder:1px solid;border-radius:0.5em;" type="button"value="创建模型" ng-click="createTo(activiti);"/> 
               
        <input style="font-size:28px;cursor:pointer;cursor:pointerborder:1px solid;border-radius:0.5em;" type="button"value="返回"/> 
    </div> 

     

     

     

    在我们填好相关数据以后,点击提交,便会触动createTo方法进入到js代码中,对应的整个js代码如下:

     

    angular.module('activitiApp') 
    .controller('createCtr', ['$rootScope','$scope','$http','$location','$state', function($rootScope,$scope,$http,$location,$state){ 
        //创建模型
        $http.post("createFlush.do").success(function(result){
           if(result.isLogin==="yes"){
               $rootScope.userName=result.userName;
           }else{
               $location.path("/login");
           }
        });
        $scope.createTo=function(activiti){
            //向后台提交数据
          $http.post("./create.do",activiti,{headers:'Content-Type:application/json'}).success(function(createResult){
          console.log(createResult);
          $location.path("/modelList");
        window.open("http://localhost:8080/activitiTest1"+createResult.path+createResult.modelId);
          });
     
        } 
     
    }]) 

     

     

     

    在上述的代码中可以看到createTo方法是直接调用$http向后台发送post请求,然后把页面中的数据对象直接传到后台,并没有多做处理,这里涉及到angularjs的双向数据绑定,如果有不明白的,可以自己去了解以下。

    需要注意的是,发送请求并接收后台返回数据后,从返回数据中拿到path和modelid然后跳转到activiti-model流程图设计页面,这里可以先看一下下边的后台代码再返回来看。

     

    经过http请求,操作便运行到后台,相应的后台代码如下:

     

    /**
         * 创建模型
         *
         * @author:tuzongxun
         * @Title: create
         * @param@param activiti
         * @param@param request
         * @param@param response
         * @param@return
         * @return Object
         * @date Mar 17, 201612:30:29 PM
         * @throws
         */
        @RequestMapping(value = "/create.do", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
        @ResponseBody
        public Object create(@RequestBody ActivitiModel activiti,
               HttpServletRequest request, HttpServletResponse response) {
           Map<String, String> map = new HashMap<String, String>();
           Boolean isLogin = this.isLogin(request);
           if (isLogin) {
               Model newModel = repositoryService.newModel();
               try {
     
                  ObjectMapper objectMapper = new ObjectMapper();
                  ObjectNode modelObjectNode = objectMapper.createObjectNode();
                  modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME,
                         activiti.getName());
                  modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);
                  modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION,
                         StringUtils.defaultString(activiti.getDescription()));
                  newModel.setMetaInfo(modelObjectNode.toString());
                  newModel.setName(activiti.getName());
                  newModel.setKey(StringUtils.defaultString(activiti.getKey()));
                  repositoryService.saveModel(newModel);
                  ObjectNode editorNode = objectMapper.createObjectNode();
                  editorNode.put("id", "canvas");
                  editorNode.put("resourceId", "canvas");
                  ObjectNode stencilSetNode = objectMapper.createObjectNode();
                  stencilSetNode.put("namespace",
                         "http://b3mn.org/stencilset/bpmn2.0#");
                  editorNode.put("stencilset", stencilSetNode);
                  repositoryService.addModelEditorSource(newModel.getId(),
                         editorNode.toString().getBytes("utf-8"));
               } catch (Exception e) {
                  e.getStackTrace();
               }
               //response.sendRedirect(request.getContextPath() +
               //"/service/editor?id="
               // +newModel.getId());
               map.put("isLogin", "yes");
               map.put("userName",
                      (String) request.getSession().getAttribute("userName"));
               map.put("path", "/service/editor?id=");
               map.put("modelId", newModel.getId());
           } else {
               map.put("isLogin", "no");
           }
           return map;
        }

     

     

     

     

     

    这一段代码里涉及到的东西还比较多,有一些我也不是很理解,只是这段代码基本上就是这样不怎么会变了,我也暂时没有深入理解,可以看到这段代码跟之前说列的也是一样,那么这里我要说的是最后走完这段代码返回数据给前台。

     

    可以看到其中有一个path属性和modelId,正是这两个属性返回给前台后,前台angularjs再控制路由跳转到activiti-model流程图设计页面(这个页面完全整合而来,非自己创造)。根据path和modelId跳转后的界面如下:

     

     

    这个界面是整合而来,在跳转的过程中会自动把之前我们填写的数据带入过来,当然了,还有modelId,这个时候数据库实际上已经有了数据。

     

    进入到这个界面,那么下边就要正式开始设计流程图了,首先如下图所示的创建开始节点,创建方法是找到StartEvent按钮,按住鼠标左键拖拽到右方空白处即可。

     

     

    然后在这个开始节点后边有许多按钮可以点击,我们选择下图中箭头所指的个人任务,然后便新建了一个任务节点,当然了,如果能学到自定义流程图这一步,我想大家应该已经对流程图的基础有一定了解了,起码在eclipse中画流程图应该已经没有问题,所以这里大家应该不难理解,否则的话,不如先用eclipse中的插件了解一下。

     

     

    这样的话,依照上边的步骤我们就可以创建出如下图的流程图雏形,包含开始节点、两个任务节点,然后是结束节点:

     

     

    那么接下来就是比较重要的了,开始进入自定义表单整合的环节。

    在上图中可以看到右方我用箭头指了一个地方,attributes,顾名思义,就是属性,这里是可以点开的,假如大家能走到这一步来,就可以看到在不同情况下这里显示的不一样。

    首先我们不选中任何节点,可以看到属性如下:

     

     

    而我们选中开始节点和任务节点是属性又分别如下图1和图2:

     

     

    图2:

     

    但是不管是开始节点还是任务节点,都有Formkey这个属性,在这里我们就可以指定每个节点需要使用的自定义表单文件的名称,通常是xxx.form的形式。指定好formkey以后就可以点击保存,也可以设置其他的属性,如任务中的assignment和name、key等等。

    而这里的整个页面都不是自己写的,保存相关的方法也不是自己写的,只要数据库配置正确,activiti-modeler就会自动把这些数据保存到数据库中,模型创建完毕。

    展开全文
  • 4、上面的效果说明整个发送消息以及监听消费消息的逻辑大局上是木有问题了,另外,值得一提的是,这个event可以由多个listener绑定,相当于RabbitMQ中一个message可以由多个listener去监听。所以学习RabbitMQ整块...
  • 图|一文看懂25神经网络模型

    万次阅读 多人点赞 2017-07-03 08:20:46
    光是知道各式各样的神经网络模型缩写(如:DCIGN、BiLSTM、DCGAN……还有哪些?),就已经让人招架不住了。 因此,这里整理出一份清单来梳理所有这些架构。其中大部分是人工神经网络,也有一些完全不同的怪物。...
  • keras将两独立的模型融合起来(输入单输出)

    万次阅读 多人点赞 2019-07-24 21:56:51
    参考:keras实现多个模型融合 **优点:**可以给两个模型单独赋予权重 **想法:**用在迁移学习中时,可以先预训练好其中一个模型,保存权重,再载入到总的模型中 import keras from keras.models import Model from keras....
  • 事件驱动模型事件监听机制观察者模式案例1.Spring事件机制(事件监听机制)案例2.基于Springboot事件监听机制整合EventBus应用案例3. 事件监听机制 熟悉Spring的同学,spring提供了一套完整的事件监听机制。要了解...
  • 一文看懂25神经网络模型

    万次阅读 多人点赞 2017-06-17 10:26:08
    光是知道各式各样的神经网络模型缩写(如:DCIGN、BiLSTM、DCGAN……还有哪些?),就已经让人招架不住了。因此,这里整理出一份清单来梳理所有这些架构。其中大部分是人工神经网络,也有一些完全不同的怪物。尽管...
  • 因为被“批处理”的2物体的网格模型需要使用相同材质的目的,在于其纹理是相同的,这样才可以实现同时渲染的目的。因而保证材质相同,是为了保证被渲染的纹理相同。 因此,为了将2纹理不同的材质合二为一,我.....
  • 论文提出了一新的机器学习模型特征相关性(MFR),通过将表达相似度和基于先验知识的相似度纳入评估标准,来准确地测量一对基因之间的条件相关性。 2. 介绍     基因之间的相互作用通常被建模为一对基因之间...
  • keras多模型加载

    千次阅读 2018-03-22 13:53:22
    最近在作两个网络的整合工作,想着能不能用keras中的load_weights()函数来实现两次加载不同的预训练好...则可以通过两次的load_weights(model_weights_path, by_name = True)调用,从而加载好了预训练好的两个模型...
  • 在一个视图中使用多个模型 ”中,介绍了下面六个方式,“ViewModel,Partial View, Tuple, ViewData, ViewBag 和TempData”. 我们在特定的情境中进行选择哪一种方法时,可能会有疑惑。在这篇文章中,我会分享一下我...
  • SpringBoot整合RabbitMQ 实现五种消息模型 详细教程

    千次阅读 多人点赞 2019-06-18 16:43:32
    中间件的应用场景,然后带着大家一起实现RabbitMQ的五种消息模型。 消息队列中间件 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题实现高性能,高可用,可伸缩和终一致性...
  • 原文:A Topic Modeling Approach and ...在本文中,我们提出一统一的将主题模型方法(Topic Model)整合进随机游走(Random Walk)框架的方法,并将其应用到学术搜索里。具体地,我们提出了一种能同时将论文,作者和出版
  • 【数据库设计】逻辑设计-ER模型转换为关系模型

    万次阅读 多人点赞 2017-09-26 16:06:05
    如何把ER模型转换为关系模型这是数据库工程设计进行到逻辑设计的一重大环节,简单的说,如果概念设计是用ER...1:N1:N 遇到 1:N 关系的话在N端添加另一端的主键,假如有学生和班级两个实体,一个班级可以容纳多个学生,
  • 因子模型水平测试题

    千次阅读 2018-05-02 10:26:50
    1 试卷说明测试目标:因子模型是量化股票组合投资领域的基本工具,介绍性的资料很。但学习这些资料之后,甚至一些老手也很难判断自己掌握到什么程度,或是在哪些方面有所缺失。因此,我们几位从业者合力整理了...
  • tensorflow lite模型生成以及bazel的安装使用以及出现的问题和解决方案整合
  • 如何合并两TensorFlow模型

    千次阅读 热门讨论 2018-10-15 21:10:31
    这是Tensorflow SavedModel模型系列文章的第三篇,也是终章。在《Tensorflow SavedModel模型的保存与加载》中,我们谈到了Tensorflow模型如何保存为SavedModel格式,以及...在本文中,我们将探讨如何合并两个模型,...
  • 推荐系统 - 目标模型融合部分

    千次阅读 2019-12-11 09:36:55
    既是给不同的模型加权,让不同模型融合在一起 有两种加权方式: 1)、权值参数固定,给不同的场景设定不同的权重参数,给不同的特征设定不同的参数 适用于特征数量少,预测结果可观察的情况; 修改权重参数的...
  • 原文:One Model At A Time: Integrating And Running ...译者注:如果你对如何在公司产品中引入和运用深度学习模型有浓厚的兴趣,下文也许会给你带来一些帮助。三年来,我们一直在EyeEm公司开发计算机视觉产品-这些产
  • 宣传官网 http://xb.exrick.cn 在线Demo http://xboot.exrick.cn 开源版Github地址 ...由于modelId=1随意输入,后台并无id=1的数据,所以报错,具体新建一个模型的接口自行百度即可,当然也可付费获取完整版。
  • 2.1 维度模型 2.1.1 星型模型 2.1.2 雪花模型 2.1.3星座模型 2.2 范式模型 2.3 Data Vault模型 2.4 Anchor模型
  • 深度学习--多模型集成

    千次阅读 2019-04-02 18:15:11
    转自:https://blog.csdn.net/qq_21997625/article/details/80388639 ... 模型集成方法 集成学习(ensemble learning)是机器学习中一类学习算法,值训练多个学习器并将它们组合起来使用的方法。这类算法通常在实...
  • 如何把ER模型转换为关系模型

    万次阅读 多人点赞 2017-11-26 21:26:24
    如何把ER模型转换为关系模型这是数据库工程设计进行到逻辑设计的一重大环节,简单的说,如果概念设计是用ER...1:N1:N 遇到 1:N 关系的话在N端添加另一端的主键,假如有学生和班级两个实体,一个班级可以容纳多个学生,
  • 机器学习模型优化之模型融合

    万次阅读 多人点赞 2018-01-07 15:37:24
    前言:在机器学习训练完模型之后我们要考虑模型的效率问题,常用的模型效率分析手段有: 研究模型学习曲线,判断模型是否过拟合或者...模型融合:模型融合就是训练多个模型,然后按照一定的方法集成过个模型,应为...
  • 提高机器学习模型性能的五关键方法

    万次阅读 多人点赞 2018-09-08 11:52:10
    如何提高机器学习模型性能, 可从五关键方面入手。 1. 数据预处理 2. 特征工程 3. 机器学习算法 4. 模型集成与融合 5. 数据增强 以下是各个方面的具体分析和方法: [ 说明:1、这里主要是各个关键方法的...
  • 在构建一有指导的数据挖掘模型,首先要理解和定义一些模型试图估计的目标变量。 首先要定义模型的结构和目标。 二、增加响应建模。 三、考虑模型的稳定性。 四、通过预测模型、剖析模型来讨论模型的稳定性。 下面...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 177,374
精华内容 70,949
关键字:

多个模型结果如何整合