Navigation

    Gpushare.com

    • Register
    • Login
    • Search
    • Popular
    • Categories
    • Recent
    • Tags
    1. Home
    2. 155****7220
    • Profile
    • Following 0
    • Followers 2
    • Topics 92
    • Posts 105
    • Best 90
    • Groups 0

    155****7220

    @155****7220

    176
    Reputation
    47
    Profile views
    105
    Posts
    2
    Followers
    0
    Following
    Joined Last Online
    Website wmathor.com

    155****7220 Unfollow Follow

    Best posts made by 155****7220

    • RE: 【有奖话题NO.7】传说中的万能Transformer,你有用过吗?

      @188-7853 这题我会,因为我本就是做NLP研究的,所以我对Transformer的理解相较于CV的同学来说要更深一些

      Transformer出来以后,大量的研究都是针对其中Self-Attention的改进

      例如有人从低秩瓶颈的角度分析了Self-Attention中attention矩阵存在大量0的问题,于是通过增大key size的方法解决了低秩的问题。参考论文《Low-Rank Bottleneck in Multi-head Attention Models》、《Talking-Heads Attention》

      还有人从self-attention的时空复杂度进行改进,例如去掉q,k乘积的部分,直接采用随机生成的一个矩阵当作attention,或者是用输入过一个linear生成的矩阵当作attention。参考论文《Synthesizer: Rethinking Self-Attention in Transformer Models》;以及一些线性时空复杂度的attention魔改方法。参考论文《Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention》

      看了这么多魔改Transformer的论文之后,我个人也有一些感悟。虽然平方级别的复杂度是Transformer存在的一个问题,但这也是他的优点,例如可以很方便的处理多模态的数据,比方说利用文本数据生成q,利用图像数据生成k,利用音频数据生成v,这样就可以做到多模态数据之间的交互,而这一点反而是上面提到的线性化attention无法做到的。所以成也self-attention,败也self-attention吧

      因为有了Transformer,才会有人不断向参数量更大,训练时间更长的预训练模型以及预训练效果发起冲击;而对一些研究者来说,他们没有那么大的算力去做预训练,甚至下游任务微调可能都做不到,于是又有一些人开始研究如何缩小Transformer。可以说有了Transformer,大家才有了新的"水论文"的方向,于是我想起一句话:人人都笑Transformer,人人都是Transformer

      posted in 有奖话题
      155****7220
      155****7220
    • 未闻Prompt名

      个人觉得2021年NLP最火的两个idea,一个是对比学习(Contrastive Learning),另一个就是Prompt

      浅谈我对Prompt的理解

      Prompt说简单也简单,看了几篇论文以及博客后发现其实就是构建一个语言模版,从而实现无监督训练。但是细想起来又觉得复杂,因为总感觉里面还有很多细节,因此本文就来从头梳理一下Prompt(Prompt很多地方会翻译成「范式」,但是「范式」这个词本身也不好理解,因此读者把他看作是「模板」即可)

      今天我还与室友讨论预训练模型(例如BERT)到底做了什么,我给出的回答是

      预训练模型提供了一个非常好的初始化参数,这组参数在预训练任务上的表现非常好(预训练损失非常低),但是由于下游任务千奇百怪,我们需要在这组参数的基础上进行Fine-tune以适应我们的下游任务(使得下游任务的损失值非常低)

      上面这段话其实隐含了目前做NLP任务的大致流程,即"Pre-train, Fine-tune",而对我们来说实际上大部分时候都是直接拿别人预训练好的模型做Fine-tune,并没有Pre-train这一步

      融入了Prompt的模式大致可以归纳成"Pre-train, Prompt, and Predict",在该模式中,下游任务被重新调整成类似预训练任务的形式。例如,通常的预训练任务有MLM(Masked Language Model),在文本情感分类任务中,对于"I love this movie"这句输入,可以在后面加上Prompt:“the movie is ___”,组成如下这样一句话:

      I love this movie, the movie is ___
      

      然后让预训练模型用表示情感的答案(例如"great"、"terrible"等)做完形填空,最后再将该答案转换为情感分类的标签。这样一来,我们就可以通过构造合适的「模板」,控制模型的输出空间,从而训练一个完全无监督的预训练模型来解决各种各样的下游任务

      注意,Prompt设计的这种完形填空和MLM任务是有区别的,二者虽然都是都是词分类,但是候选集不同,MLM的候选词是整个词库,不过如果是生成任务,那么Prompt和MLM的候选集就是一样的,都是整个词库

      如何构建Prompt

      对于输入文本xxx,存在一个函数fPrompt(x)f_{\text{Prompt}}(x)fPrompt​(x),将xxx转化成x‘x^{‘}x‘的形式,即
      $$
      x^{’}=f_{\text{Prompt}}(x)
      $$
      该函数通常会进行两步操作:

      1. 使用一个模板,模板通常为一段自然语言句子,并且该句子包含两个空位置:用于填输入xxx的位置[X][X][X]、用于生成答案文本zzz的位置[Z][Z][Z]
      2. 把输入xxx填到[X][X][X]的位置

      以前文提到的例子为例,在文本情感分类任务中,假设输入是

      x = "I love this movie"
      

      使用的模板是

      [X]. Overall, it was a [Z] movie
      

      那么得到的x′x^{'}x′就应该是

      I love this movie. Overall, it was a [Z] movie
      

      在实际情况中,Prompt来填充答案的位置一般在句中或句末。如果在句中,一般称这种Prompt为Cloze Prompt;如果在句末,一般称这种Prompt为Prefix Prompt。[X][X][X]和[Z][Z][Z]的位置、数量以及使用模板句的不同,都有可能对结果造成影响,因此需要灵活调整

      上面讲的都是简单的情感分类任务的Prompt设计,读者看到这里自然而然的会想到,其他NLP任务的Prompt如何设计呢?实际上刘鹏飞大神在他的论文中给我们提供了一些参考

      Text Generation中摘要任务里有一个关键字TL;DR,这其实是Too Long; Don't Read的缩写

      Prompt的选择非常重要且困难

      有上述Prompt的基础后,我们可以得知Prompt的设计主要包含两部分:

      1. 模板T:例如[X]. Overall, It was [Z]
      2. 标签词映射:即[Z][Z][Z]位置预测输出的词汇集合与真实标签yyy构成的映射关系。例如,标签positive对应单词great,标签negative对应单词terrible

      在基于Prompt的微调方法中,不同的模板和标签词对最终结果影响很大,下图是陈丹琦团队论文中的实验结果

      从上图我们可以看出两点:

      1. 使用相同的「模板」,不同的「标签词」会产生不一样的效果。例如great/terribel和cat/dog这两组标签词的效果不一样,而且即便是相同标签词,互换顺序也会导致最终效果有所变化,例如cat/dog和dot/cat
      2. 使用相同「标签词」,对「模板」进行小改动(例如增删标点)也会呈现不同的结果

      Prompt的设计

      Prompt大概可以从下面三个角度进行设计:

      • Prompt的形状
      • 人工设计模板
      • 自动学习模板

      Prompt的形状

      Prompt的形状主要指的是[X][X][X]和[Z][Z][Z]的位置和数量。上文提到的Cloze Prompt与Maksed Language Model的训练方式非常类似,因此对于MLM任务来说,Cloze Prompt更合适;对于生成任务或者使用自回归LM解决的任务,Prefix Prompt更合适

      人工设计模板

      Prompt的模板最开始是人工设计的,人工设计一般基于人类的自然语言知识,力求得到语义流畅且高效的「模板」。例如,Petroni等人在著名的LAMA数据集中为知识探针任务人工设计了Cloze Templates;Brown等人为问答、翻译和探针等任务设计了Prefix Templates。人工设计模板的优点是直观,但缺点是需要很多实验、经验以及语言专业知识。下图是GPT Understands, Too论文中的一个实验结果

      可以看到不同的Prompt只有细微的区别,有的甚至只是增加减少一个词,但是最后的结果会差几十个点

      自动学习模板

      为了解决人工设计模板的缺点,许多研究员开始探究如何自动学习到合适的模板。自动学习的模板又可以分为离散(Discrete Prompts)和连续(Continuous Prompts)两大类。离散方法主要包括:Prompt Mining,Prompt Paraphrasing,Gradient-based Search,Prompt Generation和Prompt Scoring;连续的则主要包括Prefix Tuning,Tuning Initialized with Discrete prompts,Hard-Soft Prompt Hybrid Tuning,P-Tuning v2

      离散Prompts

      简单说一下上述几种方法,首先是离散的Prompt Mining,这篇文章发表在TACL 2020,讲的是如何拿预训练语言模型当作「知识库」使用,并且引入了依存树和Paraphrase(转述)等方法来挖掘更好的「模板」,下图是实验结果

      可以看到,被挖掘出来的若干「连接谓词」相比于人工设计的「模板」结果提升还是很明显的

      有很多种方法可以实现Prompt Paraphrsing,例如「回译」,我们通过DeepL翻译看个例子:

      这样我们就得到了x shares a border with y的一个Prompt Paraphrasing:x and y share a boundary

      论文BARTScore干脆给我们提供了一张表,里面有各种词组的同义替换,这个我再熟悉不过了,因为以前英语考试我也背过类似的东西

      Gradient-based Search(基于梯度的搜索)是由论文AUTOPROMPT提出的,这篇文章发表在EMNLP 2020,它的主要思想用下面这张图就可以表示

      上图中,a real joy是原始的输入句子xinpx_{\text{inp}}xinp​,红色的Trigger tokens是由xinpx_{\text{inp}}xinp​「激发」的相关词汇集合xtrigx_{\text{trig}}xtrig​,根据Template λ\lambdaλ的配置,将xtrigx_{\text{trig}}xtrig​和xinpx_{\text{inp}}xinp​组合起来构造最终的输入xpromptx_{\text{prompt}}xprompt​,送入Masked LM预测情感标签。下面的表格增加了很多NLP其他任务的例子

      关于如何生成xtrigx_{\text{trig}}xtrig​集合,实际上主要使用的是HotFlip和对抗训练的思想,感兴趣的同学可以看原论文以及HotFlip: White-box adversarial examples for text classification、Universal Adversarial Triggers for Attacking and Analyzing NLP这两篇论文

      Prompt Generation是陈丹琦团队的一项工作,主要是把Seq2Seq预训练模型T5应用到模板搜索的过程。T5基于多种无监督目标进行预训练,其中最有效的一个无监督目标就是:利用<X>或<Y>替换一个或多个连续span,然后生成对应输出。例如:

      Thank you <X> me to your party <Y> week
      

      T5会在<X>生成for inviting,在<Y>生成last。很显然,T5这种方式很适合生成模板,而且不需要指定模板的token数。具体来说,有三种可能的生成方式
      $$
      \begin{align}
      \langle S_1\rangle &\to \langle \text{X}\rangle \ \mathcal{M}(y) \ \langle \text{Y} \rangle\ \langle S_1\rangle\
      \langle S_1\rangle &\to \langle S_1\rangle\ \langle \text{X}\rangle \ \mathcal{M}(y) \ \langle \text{Y} \rangle\
      \langle S_1\rangle ,\langle S_2 \rangle &\to \langle S_1\rangle\ \langle \text{X}\rangle \ \mathcal{M}(y) \ \langle \text{Y} \rangle\ \langle S_2\rangle\
      \end{align}
      $$
      具体的模板生成过程如下图所示:

      首先在标签词前后添加填充位<X>和<Y>(上面提到的三种生成方式),然后将其送入T5模型中,T5会自动在填充位生成序列,最后将标签词(great或terribel)转换为[MASK]标签,形成多个模板。具体过程中采用Beam Search的方法生成多个候选模板,然后对每一个候选模板利用dev集进行微调,选择其中一个最佳模板

      我还想说一下这篇论文中另外一个有意思的点,最后送入模型进行预测的句子还拼接上了每种类别的「示例」(Demonstration),如下图所示

      这种Prompt的设计有点像是在做语义相似度任务,XXX为原始Input句子,已知YYY为正例,ZZZ为负例,构造了如下形式的输入:

      X是[MASK]例?Y为正例;Z为负例
      

      这有点像是编程语言中的三目运算符,或者说相当于让模型比较XXX与YYY、ZZZ的语义相似度。这里我们自然而然会想问:YYY、ZZZ是如何挑选出来的?实际上是依据下面两条规则:

      1. 对于每个原始输入句子,从每个类别中随机采样一个样本「示例」拼接到Prompt中
      2. 对于每个原始输入句子,在每个类别中,通过与Sentence-BERT进行相似度计算,从相似度最高的前50%样本中随机选择一个样本「示例」

      连续Prompts

      构造Prompt的初衷是能够找到一个合适的方法,让Pre-trained Language Model(PLM)更好地输出我们想要的结果,但其实并不一定要将Prompt的形式设计成人类可以理解的自然语言,只要机器理解就行了。因此,还有一些方法探索连续型Prompts——直接作用到模型的Embedding空间。连续型Prompts去掉了两个约束条件:

      1. 模版中词语的Embedding可以是整个自然语言的Embedding,不再只是有限的一些Embedding
      2. 模版的参数不再直接取PLM的参数,而是有自己独立的参数,可以通过下游任务的训练数据进行调整

      Prefix Tuning最开始由Li等人提出,这是一种在输入句子前添加一组连续型向量的方法,该方法保持PLM的参数不动,仅训练前缀(Prefix)向量。Prefix Tuning的提出主要是为了做生成任务,因此它根据不同的模型结构定义了不同的Prompt拼接方式,在GPT类的Auto-Regressive(自回归)模型上采用的是[Prefix;x;y][\text{Prefix}; x; y][Prefix;x;y]的方式,在T5类的Encoder-Decoder模型上采用的是[Prefix;x;Prefix′;y][\text{Prefix};x;\text{Prefix}';y][Prefix;x;Prefix′;y]的方式

      输入部分Prefix,x,y\text{Prefix},x,yPrefix,x,y的Position id分别记作P<em>idx,X</em>idx,Y<em>idx\text{P}<em>{\text{idx}},\text{X}</em>{\text{idx}},\text{Y}<em>{\text{idx}}P<em>idx,X</em>idx,Y<em>idx。Prefix Tuning初始化一个可训练的矩阵,记作P</em>θ∈R∣Pidx∣×dim⁡(hi)P</em>\theta \in \mathbb{R}^{\lvert P_{\text{idx}}\rvert \times \dim (h_i)}P</em>θ∈R∣Pidx​∣×dim(hi​),其中
      $$
      h_i = \begin{cases}P_\theta [i,:],\quad &\text{if}\ i \in \text{P}{\text{idx}}\
      \mathbf{LM}
      {\phi}(z_i,h_{<i}), \quad &\text{otherwise}
      \end{cases}
      $$
      上述公式的含义是,索引iii如果属于前缀的部分,则从PθP_\thetaPθ​中抽取向量;iii如果不是前缀部分,则由参数固定的预训练模型生成对应的向量。训练目标为:
      $$
      \mathop{\text{max}}\limits_{\phi} \ \log p_{\phi}(y\mid x) = \sum\limits_{i\in \text{Y}{\text{idx}}} \log p{\phi} (z_i\mid h_{<i})
      $$

      PθP_{\theta}Pθ​本质上是一个矩阵,而生成一个矩阵的方法又很多,可以用nn.Embedding(),或者nn.Linear()

      同样是在连续空间上搜索Prompt,OptiPrompt构建的「模板」并不局限于前缀,也可以在句子的中间

      首先根据AutoPrompt定义一个Prompt模板:
      $$
      [x]\ [v]_1\ [v]_2\ …\ [v]_m\ [\text{MASK}]
      $$
      其中[v]i[v]_i[v]i​为一个连续型向量(与BERT的输入维度一致)。OptiPrompt还考虑以人工构建的离散Prompt作为起点,在连续空间上进行搜索以构建较优的Prompt。例如[x] is [MASK] citizen[x]\ \text{is}\ [\text{MASK}]\ \text{citizen}[x] is [MASK] citizen可以转换为
      $$
      [x]\ [v]_1\ [\text{MASK}]\ [v]_2
      $$
      将is和citizen对应的input Embedding作为[v]1[v]_1[v]1​和[v]2[v]_2[v]2​的初始化

      Hard-Soft Prompt Hybrid Tuning方法可以说是人工设计和自动学习的结合,它通常不单纯使用可学习的Prompt模板,而是在人工设计的模板中插入一些可学习的Embedding。实际上有了上面的基础我们都知道,连续的Prompt要比离散的Prompt好一点,但是在此基础上还有什么改进的余地吗?Liu等人提出的**P-Tuning**解决了Prompt token之间的关联性问题

      之前连续的Prompt生成方式无非都是训练一个矩阵,然后通过索引出矩阵的某几行向量拼起来。坦白地说,我们希望这些prompt token Embedding之间有一个比较好的关联性,而不是独立地学习,为了解决这个问题,P-Tuning引入了一个Prompt Encoder(如下图b所示)

      上图a是传统的离散型Prompt,我们把生成离散Prompt token的东西叫做Prompt Generator;上图b首先传入一些Virtual(Pseudo)token,例如BERT词表中的[unused1],[unused2],…当然,这里的token数目是一个超参数,插入的位置也可以调整。将这些Pseudo token通过一个Prompt Encoder得到连续的向量h0,…,hmh_0,…,h_mh0​,…,hm​,其中
      $$
      \begin{align}
      h_i &= \text{MLP}([\overrightarrow{\mathop{h_i}};\overleftarrow{\mathop{h_i}}])\
      &= \text{MLP}([\text{LSTM}(h_{0:i}):\text{LSTM}(h_{i:m})])
      \end{align}
      $$
      即,Prompt Encoder是由BiLSTM+MLP组成的一个简单网络。作者还发现加入一些anchor token(领域或者任务相关的token)可以有助于Template的优化。例如文本蕴含任务,输入是前提和假设,判断是否蕴含。一个连续的模版是
      $$
      \text{[PRE]} [\text{continuous tokens}][\text{HYP}][\text{continuous tokens}] [\text{MASK}]
      $$
      在其中加入一个anchor token:[?]效果会更好,此时模板变成
      $$
      \text{[PRE]} [\text{continuous tokens}][\text{HYP}]?[\text{continuous tokens}] [\text{MASK}]
      $$
      大家可能想问,如何优化P-tuning?实际上根据标注数据量的多少,分两种情况讨论

      1. 标注数据比较少。这种情况,我们固定PLM的参数,只优化[P0]∼[Pm][\text{P}_0]\sim [\text{P}_m][P0​]∼[Pm​]这几个token的Embedding。换句话说,我们只是要更新Prompt Encoder的参数
      2. 标注数据很充足。这种情况直接放开所有参数微调

      就在P-Tuning方法提出不久后,Liu等人又提出了P-Tuning v2,主要解决P-Tuning的两个问题:

      1. 当预训练模型的参数量低于100亿(10B)时,Prompt tuning会比传统的Fine-tuning差
      2. 诸如序列标注这样对推理和理解要求高的任务,prompt tuning效果会变差

      Liu等人认为先前的P-Tuning只用了一层BiLSTM来编码Pseudo token,这是其推理能力不足的原因之一,因此v2版本提出Deep Prompt Tuning,用Prefix Tuning中的深层模型替换BiLSTM,如下图所示

      P-Tuning v2相比于P-Tuning,区别在于:

      • 取消Reparameterization:以前的方法利用重参数化功能来提高训练速度和鲁棒性(例如,用于Prefix-Tuning的MLP和用于P-Tuning的LSTM)。在P-Tuning v2中,作者发现重参数化的改进很小,尤其是对于较小的模型,同时还会影响模型的表现
      • Multi-task Learning:Deep Prompt Tuning的优化难题可以通过增加额外的任务数据或者无标注数据来缓解,同时可微调的Prefix Continuous Prompt也可以用来做跨任务的知识共享。例如在NER中,可以同时训练多个数据集,不同数据集使用不同的顶层Classifier,但是Prefix Continuous Prompt是共享的
      • 取消verbalizer:v2取消了标签映射,完全变为生成模型,可以在[CLS]部分输出句子级别的标签(Sentence-level label),也可以在每个token位置输出token级别的标签(Token-level label),直接输出真实标签

      关于P-Tuning还有一些碎碎念,主要是从各个博客上看到的,汇总在这里。首先是v1版本的LSTM,实际上引入LSTM目的是为了帮助「模板」生成的token(某种程度上)更贴近自然语言,或者说token之间的语义更流畅,但更自然的方法应该是在训练下游任务的时候,不仅预测下游任务的目标token(例如"great"、“terrible”),还应该同时做其他token的预测

      比如,如果是MLM模型,那么也随机MASK掉其它的一些token来预测,如果是LM模型,则预测完整的序列,而不单单是目标词。这样做的理由是:因为我们的MLM/LM都是经过自然语言预训练的,所以我们认为它能够很好的完成序列的重构,即便一开始不能,随着迭代轮数的增加,模型也能很好完成这项任务。所以这本质上是让模型进行「负重训练」

      *为什么要引入Prompt?

      在标准的Fine-tune过程中(如上图b所示),新引入的参数量可能会很大(独立于原始预训练模型外的参数),例如基于RoBERTa-large的二分类任务会新引入2048个参数(nn.Linear(1024, 2)),如果你仅有例如64个标注数据这样的小样本数据集,微调会非常困难

      为解决这一问题,Prompt应运而生(如上图a所示),直接将下游任务转换为输出空间有限的MLM任务。值得注意的是:上述方法在预训练参数的基础上进行微调,并且没有引入任何新参数,同时还减少了微调和预训练任务之间的差距。总的来说,这可以更有效地用于小样本场景

      Prompt的挑战与展望

      尽管Prompt研究搞得如火如荼,但目前仍存在许多问题值得研究者们去探究

      1. Prompt的设计问题。目前使用Prompt的工作大多集中于分类任务和生成任务,其它任务则较少。另外,「模板」和「答案」的联系也亟待解决。模型的表现同时依赖于使用的「模板」和「答案」的映射,如何同时搜索或者学习出两者联合的最好效果仍然很具挑战性
      2. Prompt的理论分析和可解释性。尽管Prompt方法在很多情况下都取得了成功,但是目前Prompt-based Learning理论分析还很少,人们很难了解Prompt为什么能达到好的效果,又为什么在自然语言中意义相近的Prompt有时效果却相差很大
      3. Prompt在PLM debias方面的应用。由于PLM在预训练过程中见过了大量的人类世界的自然语言,所以很自然地会受到一些影响。举一个简单的例子,比如说训练语料中有非常多"The capital of China is Beijing",导致模型每次看到"capital"的时候都会预测出"Beijing",而不是去分析到底是哪个国家的首都。在应用的过程中,Prompt还暴露了PLM学习到的很多其它bias,比如种族歧视、性别对立等。这也许会是一个值得研究的方向

      One More Thing

      最后我还想提一个实际Code过程中存在的问题。我们知道MLM任务会输出句子中[MASK]位置最有可能的词,而Prompt也类似的,例如下面的例子

      这是一条__新闻。中国足球出线的可能性只有0.001%,留给中国队的时间不多了
      

      这是一个新闻分类问题,真实标签有"体育"、“财经”、“娱乐"等,上面的样本很明显是一条体育新闻,因此我们希望模型对[MASK]部分输出"体育”,但事实真的如此吗?实际情况模型的输出可能是"足球",但你认为模型预测的"足球"有问题吗?好像也没啥毛病,因此这就引申出了Prompt的一个问题,是否应该限制模型的输出空间?

      还是上面新闻分类的例子,我们是否应该限制模型输出的空间,让他固定只能预测"体育"、“财经”、"娱乐"这几个标签?或者我们干脆把这几个标签换成索引,那就是让模型从0,1,2这三个数字选一个。Wait Wait Wait,如果这么做的话,和Fine-Tune有什么区别,Fine-Tune也是把标签转换成索引,让模型看了句子之后,从这几个索引中选一个作为预测值

      这么说的话,那我们就不应该限制模型的输出空间,可是这样的话[MASK]位置的输出就限制的太死了,必须一定是"good"、“财经"才算对,如果输出"nice”、“财政"就算错。实际上输出近义词或者相似词,在零样本的情况下会经常出现,但是如果你用一些有标签的样本去训练,模型自己就会慢慢固定输出空间。例如"财经”,它不会预测成"财政",只会预测成其它类型的新闻,例如"体育"

      References

      • P-tuning:自动构建模版,释放语言模型潜能
      • 必须要GPT3吗?不,BERT的MLM模型也能小样本学习
      • NLP新宠——浅谈Prompt的前世今生
      • 超大规模新型预训练模型详解:少样本学习等近十个数据集取得第一
      • GPT-3的最强落地方式?陈丹琦提出小样本微调框架LM-BFF,比普通微调提升11%~
      • 基于Prompt的MLM文本分类
      • Prompt-based Language Models:模版增强语言模型小结
      • 鹏飞大神的Pre-train, Prompt, and Predict
      • P-tuning:用“连续提示微调”来增强“超大规模语言模型”的下游能力
      • Prompting: Better Ways of Using Language Models for NLP Tasks
      • Prompt Based Task Reformulation in NLP 调研
      • PromptPapers
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • CAN: 借助数据分布提升分类性能【EMNLP 2021】

      本文将介绍一种用于分类问题的后处理技巧(Trick),出自EMNLP 2021 Findings的一篇论文《When in Doubt: Improving Classification Performance with Alternating Normalization》。经过实测,CAN(Classification with Alternating Normalization)确实多数情况下能提升多分类问题的效果(CV、NLP通用),而且几乎没有增加预测成本,因为它仅仅只是对预测结果的重新归一化操作

      CAN的思想

      有趣的是,其实CAN的思想非常朴素,朴素到我们每个人几乎都用过。具体来说,假设考试中有10道选择题,前9道你都比较有信心,第10题完全不会只能瞎蒙,但是你发现前面9题选A、B、C、D的比例是3:3:2:2,那么第10题在蒙的时候,你会不会更倾向于C和D?如果是更极端的情况,你发现前面9题选A、B、C的都有,但就是没有选D的,那你蒙第10题的时候,会不会更倾向于D?

      回到分类任务上,假设现在有一个二分类问题,模型对于输入aaa给出的预测结果是p(a)=[0.05,0.95]p^{(a)}=[0.05, 0.95]p(a)=[0.05,0.95],那么我们就可以给出预测类别为1;接下来,对于输入bbb,模型给出的预测结果是p(b)=[0.5,0.5]p^{(b)}=[0.5,0.5]p(b)=[0.5,0.5],这种结果是最不确定的,我们也不知道应该输出哪个类别

      但是,假如我告诉你:

      1. 类别必然是0或1其中之一
      2. 两个类别出现的概率各为0.5

      在已知这两点「先验」信息的情况下,由于前一个样本的预测结果为1,那么基于朴素的均匀思想,我们是否会更倾向于将后一个样本预测为0,以得到一个满足第二点「先验」的预测结果?

      这些简单的例子背后,有着跟CAN同样的思想,其实就是用「先验分布」来校正「低置信度」的预测结果,使得新的预测结果的分布更接近先验分布

      Top-k熵

      准确地说,CAN是针对低置信度预测结果的后处理手段,所以我们首先要有一个衡量预测结果不确定性的指标。常见的度量是「熵」,对于p=[p1,p2,…,pm]p=[p_1,p_2,…,p_m]p=[p1​,p2​,…,pm​],定义为

      虽然熵是一个常见的选择,但其实它得出的结果并不总是符合我们的直观理解。例如对于p(a)=[0.5,0.25,0.25]p^{(a)}=[0.5, 0.25,0.25]p(a)=[0.5,0.25,0.25]和p(b)=[0.5,0.5,0]p^{(b)}=[0.5,0.5,0]p(b)=[0.5,0.5,0],直接套用公式得到H(p(a))H(p^{(a)})H(p(a))> H(p(b))H(p^{(b)})H(p(b)),但如果让人主观去评价这两个概率分布,显然我们会认为p(b)p^{(b)}p(b)比p(a)p^{(a)}p(a)更不确定,所以直接用熵还不够合理

      客观地讲,熵值越大,表示这个系统内部越不稳定。如果要与置信度联系起来,熵值越大,置信度越低

      一个简单的修正是只用前top-k个概率值来算熵,假设p1,p2,…,pkp_1,p_2,…,p_kp1​,p2​,…,pk​是概率最高的kkk个值,那么

      其中,T\mathcal{T}T是一个取向量最大的前k个值的操作Rm→Rk\mathbb{R}^{m}\to \mathbb{R}^{k}Rm→Rk。我们可以将式(2)带入式(1)展开得

      其中,p~i=pi/∑i=1kpi\tilde{p}_i = p_i / \sum\limits_{i=1}^k p_ip~​i​=pi​/i=1∑k​pi​

      交替归一化(Alternating Normalization)

      这一部分我先给出论文中的算法步骤描述,下一节我会手动模拟一遍计算过程

      Step1

      设列向量b0∈Rm\mathbf{b}_0\in \mathbb{R}^mb0​∈Rm为输入样本xxx对应各类别的概率分布,mmm表示类别数。我们生成一个n×mn\times mn×m的概率矩阵A0A_0A0​,A0A_0A0​其实是nnn个置信度非常高的样本对各个类别的预测概率向量拼接而得,通过将A0A_0A0​和b0\mathbf{b}_0b0​进行拼接得到一个(n+1)×m(n+1)\times m(n+1)×m的矩阵L0L_0L0​

      Step2

      第二步是一个迭代的过程,具体来说,首先对矩阵L0L_0L0​进行列归一化(使得每列求和为1),然后进行行归一化(使得每行求和为1)。进行算法步骤前,先定义一个向量对角化操作:


      D(v)\mathcal{D}(\mathbf{v})D(v)会将列向量v∈Rn\mathbf{v}\in \mathbb{R}^nv∈Rn转换为n×nn\times nn×n的对角矩阵,对角线元素即原本的向量元素

      列归一化

      其中,参数α∈N+\alpha \in \mathbb{N}^+α∈N+控制着b0\mathbf{b}_0b0​收敛到高置信度的速度(越大速度越快,默认取1);e∈Rn+1\mathbf{e}\in \mathbb{R}^{n+1}e∈Rn+1是全1的列向量。经过式(4)的变换后,矩阵Sd∈R(n+1)×mS_d\in \mathbb{R}^{(n+1)\times m}Sd​∈R(n+1)×m是Ld−1L_{d-1}Ld−1​矩阵的列归一化形式;ΛS−1\Lambda_S^{-1}ΛS−1​是ΛS\Lambda_SΛS​的逆矩阵

      行归一化


      其中,e∈Rm\mathbf{e}\in \mathbb{R}^{m}e∈Rm仍然是全1的列向量,只不过此时它的维度是mmm维的;矩阵Ld∈R(n+1)×mL_d\in \mathbb{R}^{(n+1)\times m}Ld​∈R(n+1)×m是行归一化的(但LdL_dLd​并不是具体某个矩阵的行归一化形式);Λq∈Rm×m\Lambda_q \in \mathbb{R}^{m\times m}Λq​∈Rm×m是一个对角矩阵,对角线上的元素是各类别的分布占比

      例如
      ICysX9.png
      表示这是一个三分类问题,并且各个类别的比例为1:2:2

      Step3

      Step2循环迭代ddd次后得到的矩阵LdL_dLd​:

      其中,bd\mathbf{b}_dbd​就是根据「先验分布」调整后的新的概率分布

      注意,这个过程需要我们遍历每个低置信度的预测结果,也就是说逐个样本进行修正,而不是一次性修正的。并且虽然迭代过程中A0A_0A0​里对应的各个样本的预测概率也都随之更新了,但那只是临时结果,最后都是弃之不用的,每次修正都是用原始的A0A_0A0​

      模拟计算AN(Alternating Normalization)

      首先我们设置一些矩阵和参数
      ICyOtf.png
      稍微解释一下,A0A_0A0​根据原算法描述是nnn个置信度比较高的样本的预测概率分布进行拼接,可以看出只有3个样本置信度比较高,并且他们的预测类别分别为2,0,2;b0b_0b0​是某样本xxx的预测概率,因为是概率分布,所以必须满足求和为1;Λq\Lambda_qΛq​是三个类别的样本比例,可以看出第一个类别的数据非常多

      首先是列归一化

      仔细观察矩阵SdS_dSd​,它每列求和都是1,也就是列归一化,如果我们追根溯源的话,实际上SdS_dSd​就是L0L_0L0​对每列求和,然后将L0L_0L0​每列元素除以该和

      接着是行归一化
      IC69ns.png
      我们只需要L1L_1L1​的最后一行,即b1=[23/25,0,2/25]T\mathbf{b}_1=\begin{bmatrix}23/25,0,2/25\end{bmatrix}^Tb1​=[23/25,0,2/25​]T,可以看,原本b0\mathbf{b}_0b0​的概率分布是[0.5,0,0.5]T\begin{bmatrix}0.5 ,0,0.5\end{bmatrix}^T[0.5,0,0.5​]T,经过「先验」调整后的类别明显偏向数据占比比较多的第一类,并且b1\mathbf{b}_1b1​向量求和为1,符合概率的定义

      实际上这个过程用Python实现也非常简单,下面就是我自己写的一个代码,变量命名与公式中的变量名完全一致

      import numpy as np
      
      n, m, d, alpha = 3, 3, 5, 1
      # n: 样本数量
      # m: 类别数
      # d: 迭代次数
      # alpha: 次方
      
      def softmax(arr):
          return np.exp(arr) / np.sum(np.exp(arr))
      
      A_0 = np.array([[0.2, 0, 0.8], [0.9, 0.1, 0], [0, 0, 1]])
      # A_0 = softmax(np.random.randn(n, m))
      
      b_0 = np.array([0.5, 0, 0.5])
      # b_0 = softmax(np.random.randn(m))
      
      L_0 = np.vstack((A_0, b_0)) # (n+1) * m
      
      Lambda_q = np.diag( np.array([0.8, 0.1, 0.1]) )
      # Lambda_q = np.diag( softmax(np.random.randn(m)) )
      
      print("预测概率:", b_0)
      print("各类别样本数量分布:", np.diag(Lambda_q, 0))
      
      L_d_1 = L_0
      for _ in range(d):
          Lambda_S = np.diag( np.dot((L_d_1 ** alpha).T, np.ones((n + 1))) )
          S_d = np.dot((L_d_1 ** alpha), np.linalg.inv(Lambda_S))
          Lambda_L = np.diag( np.dot(np.dot(S_d, Lambda_q), np.ones((m))) )
          L_d_1 = np.dot( np.dot(np.linalg.inv(Lambda_L), S_d), Lambda_q )
          print("根据先验调整后的概率:", L_d_1[-1:])
      

      参考实现

      下面给出苏剑林大佬的实现,他的代码中将Top-k熵做了个归一化,保证Htop-k(p)∈[0,1]H_{\text{top-k}} (p) \in [0,1]Htop-k​(p)∈[0,1],这样比较好确定阈值(即代码中的threshold)

      import numpy as np
      
      # 预测结果,计算修正前准确率
      y_pred = np.array([[0.2, 0.5, 0.2, 0.1],
                [0.3, 0.1, 0.5, 0.1],
                [0.4, 0.1, 0.1, 0.4],
                [0.1, 0.1, 0.1, 0.8],
                [0.3, 0.2, 0.2, 0.3],
                [0.2, 0.2, 0.2, 0.4]])
      num_classes = y_pred.shape[1]
      y_true = np.array([0, 1, 2, 3, 1, 2])
      acc_original = np.mean([y_pred.argmax(1) == y_true])
      print('original acc: %s' % acc_original)
      
      # 从训练集统计先验分布
      # prior = np.zeros(num_classes)
      # for d in train_data:
      #     prior[d[1]] += 1.
      # prior /= prior.sum()
      prior = np.array([0.2, 0.2, 0.25, 0.35])
      
      
      # 评价每个预测结果的不确定性
      k = 3
      y_pred_topk = np.sort(y_pred, axis=1)[:, -k:]
      y_pred_topk /= y_pred_topk.sum(axis=1, keepdims=True) # 归一化
      y_pred_entropy = -(y_pred_topk * np.log(y_pred_topk)).sum(1) / np.log(k) # top-k熵
      print(y_pred_entropy)
      
      # 选择阈值,划分高、低置信度两部分
      threshold = 0.9
      y_pred_confident = y_pred[y_pred_entropy < threshold] # top-k熵低于阈值的是高置信度样本
      y_pred_unconfident = y_pred[y_pred_entropy >= threshold] # top-k熵高于阈值的是低置信度样本
      y_true_confident = y_true[y_pred_entropy < threshold]
      y_true_unconfident = y_true[y_pred_entropy >= threshold]
      
      # 显示两部分各自的准确率
      # 一般而言,高置信度集准确率会远高于低置信度的
      acc_confident = (y_pred_confident.argmax(1) == y_true_confident).mean()
      acc_unconfident = (y_pred_unconfident.argmax(1) == y_true_unconfident).mean()
      print('confident acc: %s' % acc_confident)
      print('unconfident acc: %s' % acc_unconfident)
      
      # 逐个修改低置信度样本,并重新评价准确率
      right, alpha, iters = 0, 1, 1 # 正确的个数,alpha次方,iters迭代次数
      for i, y in enumerate(y_pred_unconfident):
          Y = np.concatenate([y_pred_confident, y[None]], axis=0) # Y is L_0
          for _ in range(iters):
              Y = Y ** alpha
              Y /= Y.sum(axis=0, keepdims=True)
              Y *= prior[None]
              Y /= Y.sum(axis=1, keepdims=True)
          y = Y[-1]
          if y.argmax() == y_true_unconfident[i]:
              right += 1
      
      # 输出修正后的准确率
      acc_final = (acc_confident * len(y_pred_confident) + right) / len(y_pred)
      print('new unconfident acc: %s' % (right / (i + 1.)))
      print('final acc: %s' % acc_final)
      

      实验结果

      那么,这样简单的后处理,究竟能带来多大的提升呢?原论文给出的实验结果是相当可观的:

      大体来说,类别数越多,效果提升越明显,如果类别数比较少,那么提升可能比较微弱甚至会下降

      One More Thing

      一个很自然的疑问是为什么不直接将所有低置信度的结果跟高置信度的结果拼在一起进行修正,而是要逐个修正?其实很好理解,CAN本意是要借助「先验分布」,结合高置信度结果来修正低置信度,在这个过程中如果掺入的低置信度结果越多,最终的偏差可能就越大,因此理论上逐个修正会比批量修正更为可靠

      References

      • When in Doubt: Improving Classification Performance with Alternating Normalization
      • CAN:借助先验分布提升分类性能的简单后处理技巧
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • Flooding-X: 超参数无关的 Flooding 方法【ICML 2020】

      ICML2020的论文《Do We Need Zero Training Loss After Achieving Zero Training Error?》提出了一种Flooding方法,用于缓解模型过拟合,详情可以看我的文章《我们真的需要把训练集的损失降到零吗?》。这里简单过一下,论文提出了一个超参数bbb,并将损失函数改写为
      96e91984-7cc7-4c5c-b27f-687bf526019a-image.png
      其中,bbb是预先设定的阈值,当L(θ)\mathcal{L}(\boldsymbol{\theta})L(θ) > bbb时L~(θ)=L(θ)\tilde{\mathcal{L}}(\boldsymbol{\theta})=\mathcal{L}(\boldsymbol{\theta})L~(θ)=L(θ),这时就是执行普通的梯度下降;而L(θ)\mathcal{L}(\boldsymbol{\theta})L(θ)<bbb时L~(θ)=2b−L(θ)\tilde{\mathcal{L}}(\boldsymbol{\theta})=2b-\mathcal{L}(\boldsymbol{\theta})L~(θ)=2b−L(θ),注意到损失函数变号了,所以这时候是梯度上升。因此,总的来说就是以bbb为阈值,低于阈值时反而希望损失函数变大。论文把这个改动称为Flooding

      这样做有什么效果呢?论文显示,在某些任务中,训练集的损失函数经过这样处理后,验证集的损失能出现 “二次下降(Double Descent)”,如下图

      我们可以假设梯度先下降一步后上升一步,学习率为ε\varepsilonε,通过泰勒展开可以得到
      99e3a3d4-ba26-4e62-b8df-1fccd9dec208-image.png
      其中,θn\boldsymbol{\theta}_{n}θn​表示第nnn次迭代的参数,g(θn−1)=∇θL(θn−1)g(\boldsymbol{\theta}_{n-1})=\nabla_{\boldsymbol{\theta}}\mathcal{L}(\boldsymbol{\theta}_{n-1})g(θn−1​)=∇θ​L(θn−1​)表示损失对参数θn−1\boldsymbol{\theta}_{n-1}θn−1​的梯度。式(2)的结果相当于以ε22\frac{\varepsilon^2}{2}2ε2​为学习率、损失函数为梯度惩罚∣g(θ)∣∣2=∣∣∇θL(θ)∣∣2|g(\boldsymbol{\theta})||^2=||\nabla_{\boldsymbol{\theta}}\mathcal{L}(\boldsymbol{\theta})||^2∣g(θ)∣∣2=∣∣∇θ​L(θ)∣∣2的梯度下降

      详细的推导过程见《我们真的需要把训练集的损失降到零吗?》

      Achilles’ Heel of Flooding

      Flooding的阿喀琉斯之踵在于超参数bbb,我们需要花非常多的时间寻找最佳的阈值bbb,这并不是一件容易的事

      Achilles’ Heel(阿喀琉斯之踵)阿喀琉斯是古希腊神话故事中的英雄人物,刀枪不入,唯一的弱点是脚后跟(踵)。后用于来比喻某东西的致命缺陷

      下图展示了使用BERT在SST-2数据集上不同的阈值bbb对结果的影响(黄色区域是最佳结果)。可以看出,bbb的设置对结果的影响非常大

      Gradient Accordance

      ACL2022的投稿有一篇名为《Flooding-X: Improving BERT’s Resistance to Adversarial Attacks via Loss-Restricted Fine-Tuning》的文章,以"梯度一致性"作为开启Flooding的"阀门",而不再采用超参数bbb。具体来说,我们首先定义包含参数θ\boldsymbol{\theta}θ的模型fff,考虑一个样本xxx以及真实标签yyy,它们的损失为L(f(θ,x),y)\mathcal{L}(f(\boldsymbol{\theta}, x), y)L(f(θ,x),y),损失关于参数的梯度为
      6e12089a-046d-43f2-8efe-65f19406f9cc-image.png
      其中,式(3)的负值就是参数θ\boldsymbol{\theta}θ更新的方向。现在我们考虑两个样本(x1,y1),(x2,y2)(x_1,y_1), (x_2,y_2)(x1​,y1​),(x2​,y2​)的情况,根据上述定义,样本1的梯度为
      3ef63b8e-4f76-4b60-9b8e-d6bf4a79ded3-image.png
      对于样本1来说,参数更新所导致的损失变化为
      c6cbb15b-2d39-45e5-9a87-9225fc055f1b-image.png
      将f(θ,x1)f(\boldsymbol{\theta}, x_1)f(θ,x1​)通过泰勒展开变形得
      d9b66f6d-3fee-4f3e-a4cb-aafe6375911b-image.png

      f(θ−εg1,x1)−f(θ,x1)εg1=∂f∂θ\frac{f(\boldsymbol{\theta} - \varepsilon\boldsymbol{g_1}, x_1) -f(\boldsymbol{\theta}, x_1)}{\varepsilon \boldsymbol{g_1}}= \frac{\partial f}{\partial \boldsymbol{\theta}}εg1​f(θ−εg1​,x1​)−f(θ,x1​)​=∂θ∂f​

      我们将εg1∂f∂θ\varepsilon \boldsymbol{g_1}\frac{\partial f}{\partial \boldsymbol{\theta}}εg1​∂θ∂f​记作T(x1)T(x_1)T(x1​),并对L(f(θ,x1),y1)\mathcal{L}(f(\boldsymbol{\theta}, x_1), y_1)L(f(θ,x1​),y1​)做类似的泰勒展开得
      67f1a835-8ca5-48e6-9bf0-a90d9cf4ea76-image.png

      根据式(6)可以推出第一个等号,约等于是从泰勒展开推导的,具体来说

      L(A+T(x1),y1)−L(A,y1)T(x1)=L′\frac{\mathcal{L}(A + T(x_1), y_1) -\mathcal{L}(A, y_1)}{T(x_1)} = \mathcal{L}'T(x1​)L(A+T(x1​),y1​)−L(A,y1​)​=L′

      将式(7)带入式(5)得
      08861070-9639-4eca-b9fb-4db0d7ae540d-image.png
      类似的,参数根据样本(x1,y1)(x_1,y_1)(x1​,y1​)更新后,在样本(x2,y2)(x_2, y_2)(x2​,y2​)上的损失差为ΔL2=−εg1⋅g2\Delta \mathcal{L}_2 = -\varepsilon \boldsymbol{g_1}\cdot \boldsymbol{g_2}ΔL2​=−εg1​⋅g2​

      值得注意的是,根据定义,ΔL1\Delta \mathcal{L}_1ΔL1​是负的,因为模型是对于(x1,y1)(x_1,y_1)(x1​,y1​)更新的,自然就会导致其损失的降低。如果ΔL2\Delta \mathcal{L_2}ΔL2​也是负的,那么在(x1,y1)(x_1, y_1)(x1​,y1​)上更新的模型被认为对(x2,y2)(x_2, y_2)(x2​,y2​)有积极的影响。上面的等式表明,这种共同关系相当于两个样本的梯度g1,g2\boldsymbol{g_1},\boldsymbol{g_2}g1​,g2​之间的乘积,我们称其为梯度一致性(Gradient Accordance)

      Coarse-Grained Gradient Accordance

      上面提到的可以看作是样本级别的梯度一致性,由于其粒度太细,计算起来非常复杂,因此我们将其应用到batch级别的粗粒度上进行计算

      考虑训练过程中包含nnn个样本的mini-batch B0B_0B0​,其中样本X=x1,x2,…,xn\boldsymbol{X} = {x_1, x_2,…,x_n}X=x1​,x2​,…,xn​,标签y=y1,y2,…,yn\boldsymbol{y}={y_1, y_2,…,y_n}y=y1​,y2​,…,yn​,其中yi∈c1,c2,…,cky_i\in {c_1, c_2,…,c_k}yi​∈c1​,c2​,…,ck​,即有kkk个类别。这些样本可以根据它们的标签拆分成kkk组(每组内的样本标签是一样的)
      2681a51c-8e10-439b-8cf4-6645b43c2e46-image.png
      由此可以将B0B_0B0​拆分成多个子batch的并集,B0=B01∪B02∪⋯B0kB_0 = B_0^1\cup B_0^2\cup \cdots B_0^kB0​=B01​∪B02​∪⋯B0k​。我们定义两个子batch B01B_0^1B01​和B02B_0^2B02​的类一致性分数为
      6155cb2a-32e1-42ed-9d7c-8b991dbb5c4a-image.png
      其中,g1\boldsymbol{g}_1g1​是模型在样本集B01B_0^1B01​上的损失对参数的梯度,cos⁡(g1,g2)=(g1/∣g1∣)⋅(g2/∣g2∣)\cos(\boldsymbol{g_1}, \boldsymbol{g_2})=(\boldsymbol{g_1}/|\boldsymbol{g_1}|)\cdot (\boldsymbol{g_2}/|\boldsymbol{g_2}|)cos(g1​,g2​)=(g1​/∣g1​∣)⋅(g2​/∣g2​∣)

      类一致性可以用于判断:对类别c1c_1c1​的样本集B01B_0^1B01​进行梯度下降是否也会减少类别c2c_2c2​所对应的样本集B02B_0^2B02​的损失

      假设一个Epoch中有NNN个batch,那么BsB_sBs​与BtB_tBt​的批一致性分数定义如下:
      acd96062-153a-40ad-86dd-a9f67ff667a3-image.png
      批一致性可以通过评估一个批次的参数更新对另一个批次的影响,量化两个批次的学习一致性。更具体地说,Sbatch accdS_{\text{batch accd}}Sbatch accd​如果是正的,表示这两个批次处于相同的学习节奏下,每个批次更新的模型对它们都有好处

      任意一个Epoch的梯度一致性最终定义为
      afe3b532-9df2-4d68-bfc4-ab2deff60d88-image.png

      Analysis and Discussion

      实验结果这里就不放了,简单说一下就是作者使用了TextFooler、BERT-Attack、TextBugger三种攻击手段,以PGD、FreeLB、TAVAT等方法为Baseline进行对比,结果表明使用Flooding-X效果很好

      从下图可以看出,当梯度一致性指标从负数变为正数时,测试集损失也开始上升,说明梯度一致性这个指标可以很好的当作是过拟合的信号

      个人总结

      2020年提出的Flooding本身就是一个非常有意思的Trick,可惜原论文作者也苦于超参数bbb的选择,因此其应用不算广泛。ACL2022这篇论文提出了梯度一致性的概念,让模型自己感知什么时候该进行Flooding,避免了超参数的选择问题

      References

      • 我们真的需要把训练集的损失降到零吗?
      • Flooding-X: Improving BERT’s Resistance to Adversarial Attacks via Loss-Restricted Fine-Tuning
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • NLP中的对抗训练(附PyTorch实现)

      对抗样本的基本概念

      要认识对抗训练,首先要了解"对抗样本",它首先出现在论文Intriguing properties of neural networks之中。简单来说,它是指对于人类来说"看起来"几乎一样,但对于模型来说预测结果却完全不一样的样本,比如下面的经典例子(一只熊猫加了点扰动就被识别成了长臂猿)

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

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

      对抗训练的基本概念

      GAN之父lan Goodfellow在15年的ICLR中第一次提出了对抗训练的概念,简言之,就是在原始输入样本xxx上加一个扰动Δx\Delta xΔx,得到对抗样本之后,用其进行训练。也就是说,问题可以被抽象成这样一个模型:

      max⁡θP(y∣x+Δx;θ)\max\limits_{\theta} P(y|x+\Delta x;\theta)θmax​P(y∣x+Δx;θ)

      其中,yyy为ground truth,θ\thetaθ为模型参数。那扰动Δx\Delta xΔx如何计算呢?Goodfellow认为:神经网络由于其线性的特点,很容易受到线性扰动的攻击

      This linear behavior suggests that cheap, analytical perturbations of a linear model should also damage neural networks

      于是,他提出了Fast Gradinet Sign Method(FGSM),来计算输入样本的扰动。扰动可以被定义为:

      Δx=ϵ⋅sgn(∇xL(x,y;θ))\Delta x = \epsilon \cdot \text{sgn}(\nabla_x L(x, y;\theta))Δx=ϵ⋅sgn(∇x​L(x,y;θ))

      其中,sgn\text{sgn}sgn为符号函数,LLL为损失函数(很多地方也用JJJ来表示)。Goodfellow发现,ϵ=0.25\epsilon=0.25ϵ=0.25时,这个扰动能给一个单层分类器造成99.9%的错误率。看似这个扰动的发现有点拍脑门,但仔细想想,其实这个扰动计算的思想可以理解为:将输入样本想着损失上升的方向再进一步,得到的对抗样本就能造成更大的损失,提高模型的错误率

      为了帮助读者理解上面一段话的含义,我们首先回顾一下梯度下降:在神经网络中,为了使得降低模型的损失,我们有这么一个简单的式子:

      new_weights = old_weights - lr * gradients

      如果要我指出其中最重要的部分,那必然是减号。这个减号使得无论当前梯度gradients是正还是负,最终new_weights的前进方向必然是使得loss下降的方向。那么反过来,如果将减号改为加号,并且将weights改为xxx,对抗训练中使得损失上升的思想就出来了

      x=x+Δxx = x + \Delta xx=x+Δx

      上图中,我们看到两个箭头代表了两种不同的梯度调整策略。左侧的方程是训练神经网络最常见方程,它朝着梯度下降、损失下降的方向前进。右侧的方程则不是这样,它朝着梯度上升、损失上升的方向前进

      实际上公式中的sgn\text{sgn}sgn函数作用仅仅只是为了防止∇xL(x,y;θ)\nabla xL(x,y;\theta)∇xL(x,y;θ)过大所做的缩放,除了sgn\text{sgn}sgn函数以外,还有一种常见的方式是:
      Δx=ϵ⋅∇xL(x,y;θ)∣∣∇xL(x,y;θ)∣∣\Delta x = \epsilon·\frac{\nabla_x L(x,y;\theta)}{||\nabla_xL(x,y;\theta)||}Δx=ϵ⋅∣∣∇x​L(x,y;θ)∣∣∇x​L(x,y;θ)​

      最后,Goodfellow还总结了对抗训练的两个作用:

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

      Min-Max公式

      Madry在2018年的ICLR论文Towards Deep Learning Models Resistant to Adversarial Attacks中总结了之前的工作。总的来说,对抗训练可以统一写成如下格式:

      min⁡θE(x,y)∼D[max⁡Δx∈ΩL(x+Δx,y;θ)]\min\limits_{\theta}\mathbb{E}_{(x,y)\sim\mathcal{D}}\left[\max_{\Delta x\in\Omega}L(x+\Delta x, y;\theta)\right]θmin​E(x,y)∼D​[maxΔx∈Ω​L(x+Δx,y;θ)]

      其中D\mathcal{D}D代表数据集,xxx代表输入,yyy代表标签,θ\thetaθ是模型参数,L(x,y;θ)L(x,y;\theta)L(x,y;θ)是单个样本的loss,Δx\Delta xΔx是扰动,Ω\OmegaΩ是扰动空间。这个式子可以分步理解如下:

      1. 往xxx里注入扰动Δx\Delta xΔx,Δx\Delta xΔx的目标是让L(x+Δx,y;θ)L(x+\Delta x, y;\theta)L(x+Δx,y;θ)越大越好,也就是说尽可能让现有模型的预测出错
      2. 当然Δx\Delta xΔx也不是无约束的,它不能太大,否则达不到"看起来几乎一样"的效果,所以Δx\Delta xΔx要满足一定的约束,常规的约束是∣∣Δx∣∣≤ϵ||\Delta x||\leq \epsilon∣∣Δx∣∣≤ϵ,其中ϵ\epsilonϵ是一个常数
      3. 每个样本都构造出对抗样本x+Δxx+\Delta xx+Δx之后,用(x+Δ,y)(x+\Delta,y)(x+Δ,y)作为数据去最小化loss来更新参数θ\thetaθ(梯度下降)
      4. 反复交替执行1、2、3步

      从CV到NLP

      对于CV领域的任务,上述对抗训练的流程可以顺利执行下来,因为图像可以视为普通的连续实数向量,Δx\Delta xΔx也是一个实数向量,因此x+Δxx+\Delta xx+Δx依然可以是有意义的图像。但NLP不一样,NLP的输入是文本,它本质上是one-hot向量,而两个不同的one-hot向量,其欧式距离恒为2\sqrt{2}2​,因此对于理论上不存在什么"小扰动"

      一个自然的想法是像论文Adversarial Training Methods for Semi-Supervised Text Classification一样,将扰动加到Embedding层

      Because the set of high-dimensional one-hot vectors does not admit infinitesimal perturbation, we define the perturbation on continuous word embeddings instead of discrete word inputs.

      这个思路在操作上没有问题,但问题是,扰动后的Embedding向量不一定能匹配上原来的Embedding向量表,这样一来对Embedding层的扰动就无法对应上真实的文本输入,这就不是真正意义上的对抗样本了,因为对抗样本依然能对应一个合理的原始输入

      那么,在Embedding层做对抗扰动还有没有意义呢?有!实验结果显示,在很多任务中,在Embedding层进行对抗扰动能有效提高模型的性能

      Fast Gradient Method(FGM)

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

      Δx=ϵ⋅g∣∣g∣∣2\Delta x = \epsilon · \frac{g}{||g||_2}Δx=ϵ⋅∣∣g∣∣2​g​
      g=∇xL(x,y;θ)g = \nabla_x L(x,y;\theta) g=∇x​L(x,y;θ)

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

      class FGM():
          def __init__(self, model):
              self.model = model
              self.backup = {}
      
          def attack(self, epsilon=1., emb_name='emb'):
              # emb_name这个参数要换成你模型中embedding的参数名
              # 例如,self.emb = nn.Embedding(5000, 100)
              for name, param in self.model.named_parameters():
                  if param.requires_grad and emb_name in name:
                      self.backup[name] = param.data.clone()
                      norm = torch.norm(param.grad) # 默认为2范数
                      if norm != 0:
                          r_at = epsilon * param.grad / norm
                          param.data.add_(r_at)
      
          def restore(self, emb_name='emb'):
              # emb_name这个参数要换成你模型中embedding的参数名
              for name, param in self.model.named_parameters():
                  if param.requires_grad and emb_name in name: 
                      assert name in self.backup
                      param.data = self.backup[name]
              self.backup = {}
      

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

      # 初始化
      fgm = FGM(model)
      for batch_input, batch_label in data:
        # 正常训练
        loss = model(batch_input, batch_label)
        loss.backward() # 反向传播,得到正常的grad
        # 对抗训练
        fgm.attack() # embedding被修改了
        # optimizer.zero_grad() # 如果不想累加梯度,就把这里的注释取消
        loss_sum = model(batch_input, batch_label)
        loss_sum.backward() # 反向传播,在正常的grad基础上,累加对抗训练的梯度
        fgm.restore() # 恢复Embedding的参数
        # 梯度下降,更新参数
        optimizer.step()
        optimizer.zero_grad()
      

      Projected Gradient Descent(PGD)

      FGM的思路是梯度上升,本质上来说没有什么问题,但是FGM简单粗暴的"一步到位"是不是有可能并不能走到约束内的最优点呢?当然是有可能的。于是,一个新的想法诞生了,Madry在18年的ICLR中提出了Projected Gradient Descent(PGD)方法,简单的说,就是"小步走,多走几步",如果走出了扰动半径为ϵ\epsilonϵ的空间,就重新映射回"球面"上,以保证扰动不要过大:

      xt+1=∏x+S(xt+αg(xt)∣∣g(xt)∣∣2)x_{t+1}=\prod_{x+S}(x_t+\alpha\frac{g(x_t)}{||g(x_t)||_2})xt+1​=∏x+S​(xt​+α∣∣g(xt​)∣∣2​g(xt​)​)
      g(xt)=∇xL(xt,y;θ)g(x_t)=\nabla_xL(x_t,y;\theta)g(xt​)=∇x​L(xt​,y;θ)

      其中S=r∈Rd:∣∣r∣∣2≤ϵS={r\in \mathbb{R}^d:||r||_2\leq \epsilon}S=r∈Rd:∣∣r∣∣2​≤ϵ为扰动的约束空间,α\alphaα为小步的步长

      由于PGD理论和代码比较复杂,因此下面先给出伪代码方便理解,然后再给出代码

      对于每个x:
        1.计算x的前向loss,反向传播得到梯度并备份
        对于每步t:
          2.根据Embedding矩阵的梯度计算出r,并加到当前Embedding上,相当于x+r(超出范围则投影回epsilon内)
          3.t不是最后一步: 将梯度归0,根据(1)的x+r计算前后向并得到梯度
          4.t是最后一步: 恢复(1)的梯度,计算最后的x+r并将梯度累加到(1)上
        5.将Embedding恢复为(1)时的值
        6.根据(4)的梯度对参数进行更新
      

      可以看到,在循环中rrr是逐渐累加的,要注意的是最后更新参数只使用最后一个x+r算出来的梯度

      class PGD():
          def __init__(self, model):
              self.model = model
              self.emb_backup = {}
              self.grad_backup = {}
      
          def attack(self, epsilon=1., alpha=0.3, emb_name='emb', is_first_attack=False):
              # emb_name这个参数要换成你模型中embedding的参数名
              for name, param in self.model.named_parameters():
                  if param.requires_grad and emb_name in name:
                      if is_first_attack:
                          self.emb_backup[name] = param.data.clone()
                      norm = torch.norm(param.grad)
                      if norm != 0:
                          r_at = alpha * param.grad / norm
                          param.data.add_(r_at)
                          param.data = self.project(name, param.data, epsilon)
      
          def restore(self, emb_name='emb'):
              # emb_name这个参数要换成你模型中embedding的参数名
              for name, param in self.model.named_parameters():
                  if param.requires_grad and emb_name in name: 
                      assert name in self.emb_backup
                      param.data = self.emb_backup[name]
              self.emb_backup = {}
              
          def project(self, param_name, param_data, epsilon):
              r = param_data - self.emb_backup[param_name]
              if torch.norm(r) > epsilon:
                  r = epsilon * r / torch.norm(r)
              return self.emb_backup[param_name] + r
              
          def backup_grad(self):
              for name, param in self.model.named_parameters():
                  if param.requires_grad:
                      self.grad_backup[name] = param.grad.clone()
          
          def restore_grad(self):
              for name, param in self.model.named_parameters():
                  if param.requires_grad:
                      param.grad = self.grad_backup[name]
      

      使用的时候要麻烦一点:

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

      Virtual Adversarial Training

      除了监督任务,对抗训练还可以用在半监督任务中,尤其对于NLP任务来说,很多时候我们拥有大量的未标注文本,那么就可以参考Distributional Smoothing with Virtual Adversarial Training进行半监督训练

      首先,抽取一个随机标准正态扰动(d∼N(0,1)∈Rd)(d\sim \mathcal{N}(0, 1) \in \mathbb{R}^d)(d∼N(0,1)∈Rd),加到Embedding上,并用KL散度计算梯度:

      g=∇x’DKL(p(⋅∣x;θ)∣∣p(⋅∣x’;θ))g = \nabla_{x’} D_{KL}(p(·\mid x;\theta)||p(·\mid x’;\theta))g=∇x’​DKL​(p(⋅∣x;θ)∣∣p(⋅∣x’;θ))
      x’=x+ξdx’ = x + \xi dx’=x+ξd

      然后,用得到的梯度,计算对抗扰动,并进行对抗训练:

      min⁡θDKL(p(⋅∣x;θ)∣∣p(⋅∣x∗;θ))\min\limits_\theta D_{KL}(p(\cdot|x;\theta)||p(\cdot|x^*;\theta)) θmin​DKL​(p(⋅∣x;θ)∣∣p(⋅∣x∗;θ))
      x∗=x+ϵg∣∣g∣∣2x^* = x+\epsilon \frac{g}{||g||_2}x∗=x+ϵ∣∣g∣∣2​g​

      实现起来有很多细节,并且笔者对于NLP的半监督任务了解并不多,因此这里就不给出实现了

      实验对照

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

      任务 Metrics BERT-Base FGM PGD
      MRPC Accuracy 83.6 86.8 85.8
      CoLA Matthew’s corr 56.0 56.0 56.8
      STS-B Person/Spearmean corr 89.3/88.8 89.3/88.8 89.3/88.8
      RTE Accuracy 64.3 66.8 64.6

      可以看出,对抗训练还是有效的,在MRPC和RTE任务上甚至可以提高三四个百分点。不过,根据我们使用的经验来看,是否有效有时也取决于数据集,毕竟缘,妙不可言~

      为什么对抗训练有效?

      Adversarial Training 能够提升 Word Embedding 质量的一个原因是:

      有些词与比如(good 和 bad),其在语句中 Grammatical Role 是相近的,我理解为词性相同(都是形容词),并且周围一并出现的词语也是相近的,比如我们经常用来修饰天气或者一天的情况(The weather is good/bad; It’s a good/bad day),这些词的 Word Embedding 是非常相近的。文章中用 Good 和 Bad 作为例子,找出了其最接近的 10 个词:

      可以发现在 Baseline 和 Random 的情况下, good 和 bad 出现在了彼此的邻近词中,而喂给模型经过扰动之后的 X-adv 之后,也就是 Adversarial 这一列,这种现象就没有出现,事实上, good 掉到了 bad 接近程度排第 36 的位置

      我们可以猜测,在 Word Embedding 上添加的 Perturbation 很可能会导致原来的good变成bad,导致分类错误,计算的 Adversarial Loss 很大,而计算 Adversarial Loss 的部分是不参与梯度计算的,也就是说,模型(LSTM 和最后的 Dense Layer)的 Weight 和 Bias 的改变并不会影响 Adversarial Loss,模型只能通过改变 Word Embedding Weight 来努力降低它,进而如文章所说:

      Adversarial training ensures that the meaning of a sentence cannot be inverted via a small change, so these words with similar grammatical role but different meaning become separated.

      这些含义不同而语言结构角色类似的词能够通过这种 Adversarial Training 的方法而被分离开,从而提升了 Word Embedding 的质量,帮助模型取得了非常好的表现

      梯度惩罚

      这一部分,我们从另一个视角对上述结果进行分析,从而推出对抗训练的另一种方法,并且得到一种关于对抗训练更直观的几何理解

      假设已经得到对抗扰动Δx\Delta xΔx,那么我们在更新θ\thetaθ时,考虑对L(x+Δx,y;θ)L(x+\Delta x,y;\theta)L(x+Δx,y;θ)的泰勒展开:

      min⁡θE(x,y)∼D[L(x+Δx,y;θ)]\min\limits_{\theta}\mathbb{E}_{(x,y)\sim\mathcal{D}}\left[L(x+\Delta x, y;\theta)\right]θmin​E(x,y)∼D​[L(x+Δx,y;θ)]
      ≈min⁡θE(x,y)∼D[L(x,y;θ)+⟨∇xL(x,y;θ),Δx⟩]\approx\min\limits_{\theta}\mathbb{E}_{(x,y)\sim\mathcal{D}}\left[L(x, y;\theta)+\langle\nabla_x L(x, y;\theta), \Delta x\rangle\right]≈θmin​E(x,y)∼D​[L(x,y;θ)+⟨∇x​L(x,y;θ),Δx⟩]

      其中,⟨x,y⟩=x⋅y=xTy\langle x,y \rangle = x·y = x^Ty⟨x,y⟩=x⋅y=xTy

      对应θ\thetaθ的梯度为
      ∇θL(x,y;θ)+⟨\nabla_{\theta} L(x,y;\theta)+\langle∇θ​L(x,y;θ)+⟨ ∇θ∇xL(x,y;θ),Δx⟩\nabla_{\theta}\nabla{x}L(x,y;\theta), \Delta x\rangle∇θ​∇xL(x,y;θ),Δx⟩

      带入Δx=ϵ∇xL(x,y;θ)\Delta x = \epsilon \nabla_x L(x,y;\theta)Δx=ϵ∇x​L(x,y;θ),得到

      ∇θL(x,y;θ)+ϵ⟨∇θ∇xL(x,y;θ),∇xL(x,y;θ)⟩\nabla_{\theta}L(x, y;\theta)+\epsilon\langle\nabla_{\theta}\nabla_x L(x, y;\theta), \nabla_x L(x, y;\theta)\rangle∇θ​L(x,y;θ)+ϵ⟨∇θ​∇x​L(x,y;θ),∇x​L(x,y;θ)⟩
      =∇θ(L(x,y;θ)+12ϵ∥∇xL(x,y;θ)∥2)=\nabla_{\theta}\left(L(x, y;\theta)+\frac{1}{2}\epsilon\left\Vert\nabla_x L(x, y;\theta)\right\Vert^2\right)=∇θ​(L(x,y;θ)+21​ϵ∥∇x​L(x,y;θ)∥2)

      ⟨∂2L∂x∂θ,∂L∂x⟩\langle \frac{\partial ^2L}{\partial x \partial \theta}, \frac{\partial L}{\partial x}\rangle⟨∂x∂θ∂2L​,∂x∂L​⟩
      =∂(12(∂L∂x)2)∂θ=\frac{\partial (\frac{1}{2}(\frac{\partial L}{\partial x})^2)}{\partial \theta}=∂θ∂(21​(∂x∂L​)2)​
      =∇θ(12∣∣∇xL(x,y;θ)∣∣2)=\nabla_\theta(\frac{1}{2}||\nabla_xL(x,y;\theta)||^2)=∇θ​(21​∣∣∇x​L(x,y;θ)∣∣2)

      这个结果表示,对输入样本施加ϵ∇xL(x,y;θ)\epsilon \nabla_x L(x,y;\theta)ϵ∇x​L(x,y;θ)的对抗扰动,一定程度上等价于往loss里边加入**“梯度惩罚”**
      12ϵ∣∣∇xL(x,y;θ)∣∣2\frac{1}{2}\epsilon ||\nabla_x L(x,y;\theta)||^221​ϵ∣∣∇x​L(x,y;θ)∣∣2

      如果对抗扰动Δx=ϵ∇xL(x,y;θ)∣∣∇xL(x,y;θ∣∣\Delta x = \epsilon \frac{\nabla_x L(x,y;\theta)}{||\nabla_x L(x,y;\theta||}Δx=ϵ∣∣∇x​L(x,y;θ∣∣∇x​L(x,y;θ)​,那么对应的梯度惩罚项则是ϵ∣∣∇xL(x,y;θ)∣∣\epsilon ||\nabla_x L(x,y;\theta)||ϵ∣∣∇x​L(x,y;θ)∣∣

      总结

      这篇博客梳理了NLP对抗训练发展的来龙去脉,介绍了对抗训练的数学定义,并对于两种经典的对抗训练方法,提供了插件式的实现,做了简单的实验对照。由于笔者接触对抗训练的时间也并不长,如果文中有理解偏差的地方,希望读者不吝指出。另外还有一些对抗训练算法,读者有兴趣可以查看一文搞懂NLP中的对抗训练以及对抗训练的理解,以及FGM、PGD和FreeLB的详细介绍这两篇文章

      References

      • Adversarial Attacks on Neural Networks: Exploring the Fast Gradient Sign Method
      • 对抗训练浅谈:意义、方法和思考(附Keras实现)
      • 功守道:NLP中的对抗训练 + PyTorch实现
      • 一文搞懂NLP中的对抗训练
      • 关于 Adversarial Training 在 NLP 领域的一些思考
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • BPE 算法详解

      Byte Pair Encoding

      在NLP模型中,输入通常是一个句子,例如"I went to New York last week.",一句话中包含很多单词(token)。传统的做法是将这些单词以空格进行分隔,例如['i', 'went', 'to', 'New', 'York', 'last', 'week']。然而这种做法存在很多问题,例如模型无法通过old, older, oldest之间的关系学到smart, smarter, smartest之间的关系。如果我们能使用将一个token分成多个subtokens,上面的问题就能很好的解决。本文将详述目前比较常用的subtokens算法——BPE(Byte-Pair Encoding)

      现在性能比较好一些的NLP模型,例如GPT、BERT、RoBERTa等,在数据预处理的时候都会有WordPiece的过程,其主要的实现方式就是BPE(Byte-Pair Encoding)。具体来说,例如['loved', 'loving', 'loves']这三个单词。其实本身的语义都是"爱"的意思,但是如果我们以词为单位,那它们就算不一样的词,在英语中不同后缀的词非常的多,就会使得词表变的很大,训练速度变慢,训练的效果也不是太好。BPE算法通过训练,能够把上面的3个单词拆分成["lov","ed","ing","es"]几部分,这样可以把词的本身的意思和时态分开,有效的减少了词表的数量。算法流程如下:

      1. 设定最大subwords个数VVV
      2. 将所有单词拆分为单个字符,并在最后添加一个停止符</w>,同时标记出该单词出现的次数。例如,"low"这个单词出现了5次,那么它将会被处理为{'l o w </w>': 5}
      3. 统计每一个连续字节对的出现频率,选择最高频者合并成新的subword
      4. 重复第3步直到达到第1步设定的subwords词表大小或下一个最高频的字节对出现频率为1

      例如

      {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
      

      出现最频繁的字节对是**e和s**,共出现了6+3=9次,因此将它们合并

      {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}
      

      出现最频繁的字节对是**es和t**,共出现了6+3=9次,因此将它们合并

      {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}
      

      出现最频繁的字节对是**est和</w>**,共出现了6+3=9次,因此将它们合并

      {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
      

      出现最频繁的字节对是**l和o**,共出现了5+2=7次,因此将它们合并

      {'lo w </w>': 5, 'lo w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
      

      出现最频繁的字节对是**lo和w**,共出现了5+2=7次,因此将它们合并

      {'low </w>': 5, 'low e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}
      

      …继续迭代直到达到预设的subwords词表大小或下一个最高频的字节对出现频率为1。这样我们就得到了更加合适的词表,这个词表可能会出现一些不是单词的组合,但是其本身有意义的一种形式

      停止符</w>的意义在于表示subword是词后缀。举例来说:st不加</w>可以出现在词首,如st ar;加了</w>表明改字词位于词尾,如wide st</w>,二者意义截然不同

      BPE实现

      import re, collections
      
      def get_vocab(filename):
          vocab = collections.defaultdict(int)
          with open(filename, 'r', encoding='utf-8') as fhand:
              for line in fhand:
                  words = line.strip().split()
                  for word in words:
                      vocab[' '.join(list(word)) + ' </w>'] += 1
          return vocab
      
      def get_stats(vocab):
          pairs = collections.defaultdict(int)
          for word, freq in vocab.items():
              symbols = word.split()
              for i in range(len(symbols)-1):
                  pairs[symbols[i],symbols[i+1]] += freq
          return pairs
      
      def merge_vocab(pair, v_in):
          v_out = {}
          bigram = re.escape(' '.join(pair))
          p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
          for word in v_in:
              w_out = p.sub(''.join(pair), word)
              v_out[w_out] = v_in[word]
          return v_out
      
      def get_tokens(vocab):
          tokens = collections.defaultdict(int)
          for word, freq in vocab.items():
              word_tokens = word.split()
              for token in word_tokens:
                  tokens[token] += freq
          return tokens
      
      vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
      
      # Get free book from Gutenberg
      # wget http://www.gutenberg.org/cache/epub/16457/pg16457.txt
      # vocab = get_vocab('pg16457.txt')
      
      print('==========')
      print('Tokens Before BPE')
      tokens = get_tokens(vocab)
      print('Tokens: {}'.format(tokens))
      print('Number of tokens: {}'.format(len(tokens)))
      print('==========')
      
      num_merges = 5
      for i in range(num_merges):
          pairs = get_stats(vocab)
          if not pairs:
              break
          best = max(pairs, key=pairs.get)
          vocab = merge_vocab(best, vocab)
          print('Iter: {}'.format(i))
          print('Best pair: {}'.format(best))
          tokens = get_tokens(vocab)
          print('Tokens: {}'.format(tokens))
          print('Number of tokens: {}'.format(len(tokens)))
          print('==========')
      

      输出如下

      ==========
      Tokens Before BPE
      Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 17, 'r': 2, 'n': 6, 's': 9, 't': 9, 'i': 3, 'd': 3})
      Number of tokens: 11
      ==========
      Iter: 0
      Best pair: ('e', 's')
      Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'es': 9, 't': 9, 'i': 3, 'd': 3})
      Number of tokens: 11
      ==========
      Iter: 1
      Best pair: ('es', 't')
      Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 16, 'e': 8, 'r': 2, 'n': 6, 'est': 9, 'i': 3, 'd': 3})
      Number of tokens: 10
      ==========
      Iter: 2
      Best pair: ('est', '</w>')
      Tokens: defaultdict(<class 'int'>, {'l': 7, 'o': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
      Number of tokens: 10
      ==========
      Iter: 3
      Best pair: ('l', 'o')
      Tokens: defaultdict(<class 'int'>, {'lo': 7, 'w': 16, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'est</w>': 9, 'i': 3, 'd': 3})
      Number of tokens: 9
      ==========
      Iter: 4
      Best pair: ('lo', 'w')
      Tokens: defaultdict(<class 'int'>, {'low': 7, '</w>': 7, 'e': 8, 'r': 2, 'n': 6, 'w': 9, 'est</w>': 9, 'i': 3, 'd': 3})
      Number of tokens: 9
      ==========
      

      编码和解码

      编码

      在之前的算法中,我们已经得到了subword的词表,对该词表按照字符个数由多到少排序。编码时,对于每个单词,遍历排好序的子词词表寻找是否有token是当前单词的子字符串,如果有,则该token是表示单词的tokens之一

      我们从最长的token迭代到最短的token,尝试将每个单词中的子字符串替换为token。 最终,我们将迭代所有tokens,并将所有子字符串替换为tokens。 如果仍然有子字符串没被替换但所有token都已迭代完毕,则将剩余的子词替换为特殊token,如<unk>

      例如

      # 给定单词序列
      ["the</w>", "highest</w>", "mountain</w>"]
      
      # 排好序的subword表
      # 长度 6         5           4        4         4       4          2
      ["errrr</w>", "tain</w>", "moun", "est</w>", "high", "the</w>", "a</w>"]
      
      # 迭代结果
      "the</w>" -> ["the</w>"]
      "highest</w>" -> ["high", "est</w>"]
      "mountain</w>" -> ["moun", "tain</w>"]
      
      解码

      将所有的tokens拼在一起即可,例如

      # 编码序列
      ["the</w>", "high", "est</w>", "moun", "tain</w>"]
      
      # 解码序列
      "the</w> highest</w> mountain</w>"
      

      编码和解码实现

      import re, collections
      
      def get_vocab(filename):
          vocab = collections.defaultdict(int)
          with open(filename, 'r', encoding='utf-8') as fhand:
              for line in fhand:
                  words = line.strip().split()
                  for word in words:
                      vocab[' '.join(list(word)) + ' </w>'] += 1
      
          return vocab
      
      def get_stats(vocab):
          pairs = collections.defaultdict(int)
          for word, freq in vocab.items():
              symbols = word.split()
              for i in range(len(symbols)-1):
                  pairs[symbols[i],symbols[i+1]] += freq
          return pairs
      
      def merge_vocab(pair, v_in):
          v_out = {}
          bigram = re.escape(' '.join(pair))
          p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
          for word in v_in:
              w_out = p.sub(''.join(pair), word)
              v_out[w_out] = v_in[word]
          return v_out
      
      def get_tokens_from_vocab(vocab):
          tokens_frequencies = collections.defaultdict(int)
          vocab_tokenization = {}
          for word, freq in vocab.items():
              word_tokens = word.split()
              for token in word_tokens:
                  tokens_frequencies[token] += freq
              vocab_tokenization[''.join(word_tokens)] = word_tokens
          return tokens_frequencies, vocab_tokenization
      
      def measure_token_length(token):
          if token[-4:] == '</w>':
              return len(token[:-4]) + 1
          else:
              return len(token)
      
      def tokenize_word(string, sorted_tokens, unknown_token='</u>'):
          
          if string == '':
              return []
          if sorted_tokens == []:
              return [unknown_token]
      
          string_tokens = []
          for i in range(len(sorted_tokens)):
              token = sorted_tokens[i]
              token_reg = re.escape(token.replace('.', '[.]'))
      
              matched_positions = [(m.start(0), m.end(0)) for m in re.finditer(token_reg, string)]
              if len(matched_positions) == 0:
                  continue
              substring_end_positions = [matched_position[0] for matched_position in matched_positions]
      
              substring_start_position = 0
              for substring_end_position in substring_end_positions:
                  substring = string[substring_start_position:substring_end_position]
                  string_tokens += tokenize_word(string=substring, sorted_tokens=sorted_tokens[i+1:], unknown_token=unknown_token)
                  string_tokens += [token]
                  substring_start_position = substring_end_position + len(token)
              remaining_substring = string[substring_start_position:]
              string_tokens += tokenize_word(string=remaining_substring, sorted_tokens=sorted_tokens[i+1:], unknown_token=unknown_token)
              break
          return string_tokens
      
      # vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
      
      vocab = get_vocab('pg16457.txt')
      
      print('==========')
      print('Tokens Before BPE')
      tokens_frequencies, vocab_tokenization = get_tokens_from_vocab(vocab)
      print('All tokens: {}'.format(tokens_frequencies.keys()))
      print('Number of tokens: {}'.format(len(tokens_frequencies.keys())))
      print('==========')
      
      num_merges = 10000
      for i in range(num_merges):
          pairs = get_stats(vocab)
          if not pairs:
              break
          best = max(pairs, key=pairs.get)
          vocab = merge_vocab(best, vocab)
          print('Iter: {}'.format(i))
          print('Best pair: {}'.format(best))
          tokens_frequencies, vocab_tokenization = get_tokens_from_vocab(vocab)
          print('All tokens: {}'.format(tokens_frequencies.keys()))
          print('Number of tokens: {}'.format(len(tokens_frequencies.keys())))
          print('==========')
      
      # Let's check how tokenization will be for a known word
      word_given_known = 'mountains</w>'
      word_given_unknown = 'Ilikeeatingapples!</w>'
      
      sorted_tokens_tuple = sorted(tokens_frequencies.items(), key=lambda item: (measure_token_length(item[0]), item[1]), reverse=True)
      sorted_tokens = [token for (token, freq) in sorted_tokens_tuple]
      
      print(sorted_tokens)
      
      word_given = word_given_known 
      
      print('Tokenizing word: {}...'.format(word_given))
      if word_given in vocab_tokenization:
          print('Tokenization of the known word:')
          print(vocab_tokenization[word_given])
          print('Tokenization treating the known word as unknown:')
          print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
      else:
          print('Tokenizating of the unknown word:')
          print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
      
      word_given = word_given_unknown 
      
      print('Tokenizing word: {}...'.format(word_given))
      if word_given in vocab_tokenization:
          print('Tokenization of the known word:')
          print(vocab_tokenization[word_given])
          print('Tokenization treating the known word as unknown:')
          print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
      else:
          print('Tokenizating of the unknown word:')
          print(tokenize_word(string=word_given, sorted_tokens=sorted_tokens, unknown_token='</u>'))
      

      输出如下

      Tokenizing word: mountains</w>...
      Tokenization of the known word:
      ['mountains</w>']
      Tokenization treating the known word as unknown:
      ['mountains</w>']
      Tokenizing word: Ilikeeatingapples!</w>...
      Tokenizating of the unknown word:
      ['I', 'like', 'ea', 'ting', 'app', 'l', 'es!</w>']
      

      Reference

      • 3 subword algorithms help to improve your NLP model performance
      • Tokenizers: How machines read
      • Overview of tokenization algorithms in NLP
      • 一文读懂BERT中的WordPiece
      • Byte Pair Encoding
      • 深入理解NLP Subword算法:BPE、WordPiece、ULM
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • BERT Word Embeddings Tutorial

      本文译自BERT Word Emebddings Tutorial,我将其中部分内容进行了精简。转载请注明出处

      1. Loading Pre-Trained BERT

      通过Hugging Face安装BERT的PyTorch接口,该库还包含其它预训练语言模型的接口,如OpenAI的GPT和GPT-2

      如果您在Google Colab上运行此代码,每次重新连接时都必须安装此库

      !pip install transformers
      

      BERT是由Google发布的预训练模型,该模型使用Wikipedia和Book Corpus数据进行训练(Book Corpus是一个包含不同类型的10000+本书的数据集)。Google发布了一系列BERT的变体,但我们在这里使用的是两种可用尺寸(“base” 和 “large”)中较小的一种,并且我们设置忽略单词大小写

      transformers提供了许多应用于不同任务的BERT模型。在这里,我们使用最基本的BertModel,这个接口的输出不针对任何特定任务,因此用它提取embeddings是个不错的选择

      现在让我们导入PyTorch,预训练BERT 模型以及BERT tokenizer

      import torch
      from transformers import BertTokenizer, BertModel
      
      # OPTIONAL: if you want to have more information on what's happening, activate the logger as follows
      import logging
      # logging.basicConfig(level=logging.INFO)
      
      import matplotlib.pyplot as plt
      %matplotlib inline
      
      # Load pre-trained model tokenizer (vocabulary)
      tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
      

      2. Input Formatting

      由于BERT是一个预训练模型,需要输入特定格式的数据,因此我们需要:

      1. A special token, [SEP], to mark the end of a sentence, or the separation between two sentences
      2. A special token, [CLS], at the beginning of our text. This token is used for classification tasks, but BERT expects it no matter what your application is.
      3. Tokens that conform with the fixed vocabulary used in BERT
      4. The Token IDs for the tokens, from BERT’s tokenizer
      5. Mask IDs to indicate which elements in the sequence are tokens and which are padding elements
      6. Segment IDs used to distinguish different sentences
      7. Positional Embeddings used to show token position within the sequence

      幸运的是,使用tokenizer.encode_plus这个函数可以帮我们处理好一切。但是,由于这只是使用BERT的介绍,因此我们将主要以手动方式执行这些步骤

      有关tokenizer.encode_plus这个函数的使用示例,可以这篇文章

      2.1 Special Tokens

      BERT可以将一个或两个句子作为输入。如果是两个句子,则使用[SEP]将它们分隔,并且[CLS]标记总是出现在文本的开头;如果是一个句子,也始终需要两个标记,此时[SEP]表示句子的结束。举个例子

      2个句子的输入:

      [CLS] The man went to the store. [SEP] He bought a gallon of milk.

      1个句子的输入:

      [CLS] The man went to the store. [SEP]

      2.2 Tokenization

      BERT提供了tokenize方法,下面我们看看它是如何处理句子的

      text = "Here is the sentence I want embeddings for."
      marked_text = "[CLS] " + text + " [SEP]"
      
      # Tokenize our sentence with the BERT tokenizer.
      tokenized_text = tokenizer.tokenize(marked_text)
      
      # Print out the tokens.
      print (tokenized_text)
      
      # ['[CLS]', 'here', 'is', 'the', 'sentence', 'i', 'want', 'em', '##bed', '##ding', '##s', 'for', '.', '[SEP]']
      

      注意"embeddings"这个词是如何表示的:['em', '##bed', '##ding', '##s']

      原始单词已被拆分为较小的子词和字符。这些子词中前面两个##哈希符号表示该子词或字符是较大字的一部分。因此,例如’##bed’和’bed’这两个token不相同;第一个用于子词"bed"出现在较大词中时,第二个是独立的token

      为什么会这样?因为BERT的tokenizer是使用WordPiece模型创建的。这个模型贪婪地创建了一个固定大小的词汇表,其中包含了最适合我们语言的固定数量的字符、子词和单词。由于我们BERT模型的tokenizer限制词汇量为30000,因此WordPiece模型生成的词汇表包含所有英文字符以及该模型所训练英语预料库中找到的约30000个最常见的单词和子词。该词汇表包含四类东西:

      1. 整个词
      2. 出现在单词开头或单独出现的子词("embddings"中的"em"与"go get em"中的"em"向量相同)
      3. 不在单词开头的子词,前面会添加上"##"
      4. 单个字符

      具体来说,tokenzier首先检查整个单词是否在词汇表中,如果不在,它会尝试将单词分解为词汇表中最大可能的子词,如果子词也没有,它就会将整个单词分解为单个字符。所以我们至少可以将一个单词分解为单子字符的集合。基于此,不在词汇表中的单词不会分配给"UNK"这种万能的标记,而是分解为子词和字符标记

      因此,即使"embeddings"这个词不在词汇表中,我们也不会将这个词视为未知词汇,而是将其分为子词tokens [‘em’, ‘##bed’, ‘##ding’, ‘##s’],这将保留单词的一些上下文含义。我们甚至可以平均这些子词的嵌入向量以生成原始单词的近似向量。有关WordPeice的更多信息,请参考原论文

      下面是我们词汇表中的一些示例

      list(tokenizer.vocab.keys())[5000:5020]
      
      ['knight',
       'lap',
       'survey',
       'ma',
       '##ow',
       'noise',
       'billy',
       '##ium',
       'shooting',
       'guide',
       'bedroom',
       'priest',
       'resistance',
       'motor',
       'homes',
       'sounded',
       'giant',
       '##mer',
       '150',
       'scenes']
      

      将文本分解为标记后,我们必须将句子转换为词汇索引列表。从这开始,我们将使用下面的例句,其中两个句子都包含"bank"这个词,且它们的含义不同

      # Define a new example sentence with multiple meanings of the word "bank"
      text = "After stealing money from the bank vault, the bank robber was seen " \
             "fishing on the Mississippi river bank."
      
      # Add the special tokens.
      marked_text = "[CLS] " + text + " [SEP]"
      
      # Split the sentence into tokens.
      tokenized_text = tokenizer.tokenize(marked_text)
      
      # Map the token strings to their vocabulary indeces.
      indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
      
      # Display the words with their indeces.
      for tup in zip(tokenized_text, indexed_tokens):
          print('{:<12} {:>6,}'.format(tup[0], tup[1]))
      
      [CLS]           101
      after         2,044
      stealing     11,065
      money         2,769
      from          2,013
      the           1,996
      bank          2,924
      vault        11,632
      ,             1,010
      the           1,996
      bank          2,924
      robber       27,307
      was           2,001
      seen          2,464
      fishing       5,645
      on            2,006
      the           1,996
      mississippi   5,900
      river         2,314
      bank          2,924
      .             1,012
      [SEP]           102
      

      2.3 Segment ID

      BERT希望用0和1区分两个句子。也就是说,对于tokenized_text中的每个token,我们必须指明它属于哪个句子。如果是单句,只需要输入一系列1;如果是两个句子,请将第一个句子中的每个单词(包括[SEP])指定为0,第二个句子指定为1

      # Mark each of the 22 tokens as belonging to sentence "1".
      segments_ids = [1] * len(tokenized_text)
      

      3. Extracting Embeddings

      3.1 Running BERT on our text

      接下来,我们需要将数据转换为PyTorch tensor类型

      # Convert inputs to PyTorch tensors
      tokens_tensor = torch.tensor([indexed_tokens])
      segments_tensors = torch.tensor([segments_ids])
      

      调用from_pretrained函数将从互联网上获取模型。当我们加载bert-base-uncased时,我们会在logging记录中看到模型的定义。该模型是一个具有12层的深度神经网络,解释每层的功能不在本文的范围内,您可以查看我博客之前的内容来学习相关信息

      model.eval()会使得我们的模型处于测试模式,而不是训练模式。在测试模式下,模型将会关闭dropout regularization

      # Load pre-trained model (weights)
      model = BertModel.from_pretrained('bert-base-uncased',
                                        output_hidden_states = True, # Whether the model returns all hidden-states.
                                        )
      
      # Put the model in "evaluation" mode, meaning feed-forward operation.
      model.eval()
      

      接下来,让我们把示例文本传入模型,并获取网络的隐藏状态

      torch.no_grad()告诉PyTorch在前向传播的过程中不构造计算图(因为我们不会在这里反向传播),这有助于减少内存消耗并加快运行速度

      # Run the text through BERT, and collect all of the hidden states produced
      # from all 12 layers. 
      with torch.no_grad():
      
          outputs = model(tokens_tensor, segments_tensors)
      
          # Evaluating the model will return a different number of objects based on 
          # how it's  configured in the `from_pretrained` call earlier. In this case, 
          # becase we set `output_hidden_states = True`, the third item will be the 
          # hidden states from all layers. See the documentation for more details:
          # https://huggingface.co/transformers/model_doc/bert.html#bertmodel
          hidden_states = outputs[2]
      

      3.2 Understanding the Output

      hidden_states包含的信息有点复杂,该变量有四个维度,分别是:

      1. The Layer number(13 layers)
      2. The batch number(1 sentence)
      3. The word / token number(22 tokens in our sentence)
      4. The hidden unit / feature number(768 features)

      ちょっと待って,13层?前面不是说BERT只有12层吗?因为最前面的一层是Word Embedding层,剩下的是12个Encoder Layer

      第二个维度(batch size)是一次向模型提交多个句子时使用的;不过,在这里我们只有一个句子

      print ("Number of layers:", len(hidden_states), "  (initial embeddings + 12 BERT layers)")
      
      layer_i = 0
      print ("Number of batches:", len(hidden_states[layer_i]))
      
      batch_i = 0
      print ("Number of tokens:", len(hidden_states[layer_i][batch_i]))
      
      token_i = 0
      print ("Number of hidden units:", len(hidden_states[layer_i][batch_i][token_i]))
      
      Number of layers: 13   (initial embeddings + 12 BERT layers)
      Number of batches: 1
      Number of tokens: 22
      Number of hidden units: 768
      

      通过快速浏览指定token和网络层的数值范围,您会发现其中大部分值介于[-2, 2],少数在-12附近

      # For the 5th token in our sentence, select its feature values from layer 5.
      token_i = 5
      layer_i = 5
      vec = hidden_states[layer_i][batch_i][token_i]
      
      # Plot the values as a histogram to show their distribution.
      plt.figure(figsize=(10,10))
      plt.hist(vec, bins=200)
      plt.show()
      

      按层对值进行分组是有意义的,但是为了使用,我们希望它按token进行分组

      当前的维度:[layers, batchs, tokens, features]

      期望的维度:[tokens, layers, features]

      幸运的是,PyTorch的permute函数可以轻松的重新排列维度。但是目前hidden_states第一个维度是list,所以我们要先结合各层,使其成为一个tensor

      # Concatenate the tensors for all layers. We use `stack` here to
      # create a new dimension in the tensor.
      token_embeddings = torch.stack(hidden_states, dim=0)
      
      token_embeddings.size()
      # torch.Size([13, 1, 22, 768])
      

      接着我们消掉"batch"维度,因为我们不需要它

      # Remove dimension 1, the "batches".
      token_embeddings = token_embeddings.squeeze(dim=1)
      
      token_embeddings.size()
      # torch.Size([13, 22, 768])
      

      最后,我们使用permute函数来交换维度

      # Swap dimensions 0 and 1.
      token_embeddings = token_embeddings.permute(1,0,2)
      
      token_embeddings.size()
      # torch.Size([22, 13, 768])
      

      3.3 Creating word and sentence vectors from hidden states

      我们希望为每个词获取单独的向量,或者为整个句子获取单独的向量。但是对于输入的每个词,我们有13个向量,每个向量的长度为768。为了获得单个向量,我们需要将一些层的向量组合起来。但是,哪个层或组合哪些层比较好?

      Word Vectors

      我们用两种方式创建词向量。第一种方式是拼接最后四层,则每个单词的向量长度为4*768=3072

      # Stores the token vectors, with shape [22 x 3,072]
      token_vecs_cat = []
      
      # `token_embeddings` is a [22 x 12 x 768] tensor.
      
      # For each token in the sentence...
      for token in token_embeddings:
          
          # `token` is a [12 x 768] tensor
      
          # Concatenate the vectors (that is, append them together) from 
          # the last four layers.
          # Each layer vector is 768 values, so `cat_vec` is length 3072.
          cat_vec = torch.cat((token[-1], token[-2], token[-3], token[-4]), dim=0)
          
          # Use `cat_vec` to represent `token`.
          token_vecs_cat.append(cat_vec)
      
      print ('Shape is: %d x %d' % (len(token_vecs_cat), len(token_vecs_cat[0])))
      # Shape is: 22 x 3072
      

      第二种方式是将最后四层相加

      # Stores the token vectors, with shape [22 x 768]
      token_vecs_sum = []
      
      # `token_embeddings` is a [22 x 12 x 768] tensor.
      
      # For each token in the sentence...
      for token in token_embeddings:
      
          # `token` is a [12 x 768] tensor
      
          # Sum the vectors from the last four layers.
          sum_vec = torch.sum(token[-4:], dim=0)
          
          # Use `sum_vec` to represent `token`.
          token_vecs_sum.append(sum_vec)
      
      print ('Shape is: %d x %d' % (len(token_vecs_sum), len(token_vecs_sum[0])))
      # Shape is: 22 x 768
      

      Sentence Vectors

      有很多种策略可以获得一个句子的单个向量表示,其中一种简单的方法是将倒数第2层所有token的向量求平均

      # `hidden_states` has shape [13 x 1 x 22 x 768]
      
      # `token_vecs` is a tensor with shape [22 x 768]
      token_vecs = hidden_states[-2][0]
      
      # Calculate the average of all 22 token vectors.
      sentence_embedding = torch.mean(token_vecs, dim=0)
      
      print("Our final sentence embedding vector of shape:", sentence_embedding.size())
      # Our final sentence embedding vector of shape: torch.Size([768])
      

      3.4 Confirming contextually dependent vectors

      为了确认这些向量的值是上下文相关的,我们可以检查一下例句中"bank"这个词的向量

      “After stealing money from the bank vault, the bank robber was seen fishing on the Mississippi river bank.”

      for i, token_str in enumerate(tokenized_text):
          print(i, token_str)
      
      0 [CLS]
      1 after
      2 stealing
      3 money
      4 from
      5 the
      6 bank
      7 vault
      8 ,
      9 the
      10 bank
      11 robber
      12 was
      13 seen
      14 fishing
      15 on
      16 the
      17 mississippi
      18 river
      19 bank
      20 .
      21 [SEP]
      

      在这个例子中,我们通过累加最后四层的单词向量,然后打印出来进行比较

      print('First 5 vector values for each instance of "bank".')
      print('')
      print("bank vault   ", str(token_vecs_sum[6][:5]))
      print("bank robber  ", str(token_vecs_sum[10][:5]))
      print("river bank   ", str(token_vecs_sum[19][:5]))
      
      First 5 vector values for each instance of "bank".
      
      bank vault    tensor([ 3.3596, -2.9805, -1.5421,  0.7065,  ...])
      bank robber   tensor([ 2.7359, -2.5577, -1.3094,  0.6797,  ...])
      river bank    tensor([ 1.5266, -0.8895, -0.5152, -0.9298,  ...])
      

      很明显值不同,但是通过计算向量之间的余弦相似度可以更精确的进行比较

      from scipy.spatial.distance import cosine
      
      # Calculate the cosine similarity between the word bank
      # in "bank robber" vs "bank vault" (same meaning).
      same_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[6])
      
      # Calculate the cosine similarity between the word bank 
      # in "bank robber" vs "river bank" (different meanings).
      diff_bank = 1 - cosine(token_vecs_sum[10], token_vecs_sum[19])
      
      print('Vector similarity for  *similar*  meanings:  %.2f' % same_bank) # 0.94
      print('Vector similarity for *different* meanings:  %.2f' % diff_bank) # 0.69
      

      3.5 Pooling Strategy & Layer Choice

      BERT Authors

      BERT作者通过将不同的向量组合作为输入特征提供给NER任务,并观察所得的F1分数

      虽然最后四层拼接在此特定任务上产生了最佳结果,但许多其他方法效果也不差,通常建议针对特定应用测试不同版本,结果可能会有所不同

      Han Xiao’s BERT-as-service

      肖涵在Github上创建了一个名为bert-as-service的开源项目,该项目旨在使用BERT为您的文本创建单词嵌入。他尝试了各种方法来组合这些嵌入,并在项目的FAQ页面上分享了一些结论和基本原理

      肖涵的观点认为:

      1. 第一层是嵌入层,由于它没有上下文信息,因此同一个词在不同语境下的向量是相同的
      2. 随着进入网络的更深层次,单词嵌入从每一层中获得了越来越多的上下文信息
      3. 但是,当您接近最后一层时,词嵌入将开始获取BERT特定预训练任务的信息(MLM和NSP)
      4. 倒数第二层的词嵌入比较合理
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • LLD: 内部数据指导的标签去噪方法【ACL 2022】

      很多数据集中的标签都存在错误,即便它们是由人来标注的,错误标签的存在会给模型训练带来某些负面影响。目前缓解这种影响有诸如删除错误标签、降低其权重等方法。ACL2022有一篇名为《A Light Label Denoising Method with the Internal Data Guidance》的投稿提出了一种基于样本内部指导的方法解决这个问题

      先前有研究表明同一类别的样本在本质上是相似和相关的,不同类别的样本存在明显差异。在文本分类任务中,两个有着相似内容的句子应该被预测为同一个类别,但是实际情况并不总是这样。当训练数据面临一定程度的噪声时,这个问题可能会更加严重,因为模型只收到标签的指导/监督。这就自然而然提出了一个问题:除了标签之外,我们能否从训练样本之间的关系寻求指导?

      以文本分类数据为例,有nnn个样本的数据集可以被定义为
      e3bb5224-04f0-44fc-81aa-7741afc6cbb1-image.png
      其中,yi∈c1,c2,…,cmy_i\in {c_1, c_2,…,c_m}yi​∈c1​,c2​,…,cm​表示共有mmm类

      Contextual Representation

      我们首先需要一个指标判断两个句子是否相似。目前有两大类文本相似度计算方法,第一种是基于传统的符号表征,例如编辑距离、Jaccard Similarity Coeffieient以及Earth Mover’s Distance;第二种是将文本映射为稠密的向量,然后计算它们的向量相似度。第一种方法过于依赖token的表面信息,第二种方法需要使用外部数据对模型进行预训练,而这个外部数据和我们的任务数据可能不是同一领域的。因此作者基于Postive Pointwise Mutual Information (PPMI)提出了一个新的上下文表征方法

      首先,我们用一个长度为2的滑动窗口统计数据集中所有token的共现矩阵CCC。Cwi,wjC_{w_i, w_j}Cwi​,wj​​表示前一个词是wiw_iwi​,后一个词是wjw_jwj​出现的次数,然后我们计算CCC的PPMI矩阵EEE:
      efa9b490-55fe-4d50-97fe-12e6e7a9c1d3-image.png
      其中,P(wi),P(wj),P(wi,wj)P(w_i), P(w_j), P(w_i, w_j)P(wi​),P(wj​),P(wi​,wj​)分别是从共现矩阵CCC中计算得到的。最终,向量EwiE_{w_{i}}Ewi​​是词wiw_iwi​的表示

      Word Weight

      由于不同的词对于句子含义的贡献不同,我们更关注那些对分类更有帮助的词,而不是一些常见的词(例如a, the, of)。作者提出一个计算词wiw_iwi​权重的算法:
      88cf8cd3-4fc8-42ca-870f-999af31ba54d-image.png
      其中,ccc是词wiw_iwi​出现频率最高的类别,pcwip_c^{w_i}pcwi​​是类别ccc中单词wiw_iwi​的样本数,pc~wip_{\tilde{c}}^{w_i}pc~wi​​是除了类别ccc之外所有类别中单词wiw_iwi​的样本数,∣∣pc∣∣1||p_c||_1∣∣pc​∣∣1​是类别ccc的样本数,α\alphaα是一个小的平滑值(例如0.1)。

      Guiding the Training

      给定包含ddd个单词的句子aaa,以及包含eee个单词的句子bbb,它们的相似度为:
      467823a1-6cf1-4639-8941-f0e7cbb0d784-image.png
      很明显,Tsim(a,b)T_{\text{sim}}(a,b)Tsim​(a,b)总是大于0的,因为qwiq_{w_i}qwi​​一定大于等于0,向量EiE_{i}Ei​中的元素根据计算公式也都是大于等于0的,cos⁡(A,B)\cos(A,B)cos(A,B)中,当向量AAA和BBB中的元素都大于等于0时,结果一定大于0

      在含有mmm个类别的文本分类任务中,模型对于第iii个样本的预测概率分布可以记为
      b5c24fe1-eebe-4de1-9560-d976daf38982-image.png
      其中,likl_{ik}lik​>000并且∑k=1mlik=1\sum_{k=1}^m l_{ik}=1∑k=1m​lik​=1。因此模型对于样本aaa和bbb预测概率分布的相似度为
      4ecf834a-5383-491b-aa7a-ada310c661be-image.png
      在训练过程中,损失函数定义为:
      b74a16e4-1e21-4f86-8fa1-5e090169dcd6-image.png
      其中
      b2d4406a-0ad5-4e7f-9319-725b53005326-image.png
      换言之,当两个句子的相似度大于阈值β\betaβ时,我们就认为它们非常相似,那么它们的标签大概率应该是相同的,反映到预测概率分布上,它们预测概率分布向量的余弦相似度应该接近于1才对,如果单纯这么考虑的话,实际上我们有如下定义的损失函数
      bd8d75ba-22df-46e3-afe5-d27cb4b73ce4-image.png
      极端情况下,当Lsim(i,j)=1L_{\text{sim}}(i,j)=1Lsim​(i,j)=1时,加法后面所带来的损失就为0了;当Lsim(i,j)=0L_{\text{sim}}(i,j)=0Lsim​(i,j)=0时,后面是有损失的

      Result

      论文的实验阵容还算豪华,某种程度上来说让人比较意外的地方是这种简单修改损失函数的办法居然超过了R-Drop。其中LLD-DW将矩阵EEE用Word2vec进行替换,其他步骤保持不变,结果发现用Word2vec反而没有作者提出的简单统计方法好

      个人总结

      这篇文章本质上来讲,可以看作是多目标联合训练,除了传统的分类任务,引入了一个新的任务,这个任务的目标是希望两个句子的相似度与它们预测概率分布的相似度比较接近。反映到损失函数中来看就是在传统损失的后面添加了一项。阅读完这篇论文之后,说实话我不太确定它能否被ACL录用

      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • PRGC:基于潜在关系和全局对应的联合关系三元组抽取

      b3vf3j.png

      PRGC: Potential Relation and Global Correspondence Based Joint Relational Triple Extraction

      PRGC:基于潜在关系和全局对应的联合关系三元组抽取

      Abstract

      ​ 本文讲关系抽取任务分解为关系判断、实体提取和subject-object对齐三个子任务,提出了一种基于潜在关系和全局对应的联合关系三元组抽取框架(PRGC)。具体而言,首先设计一个预测潜在关系的组件,将后续实体提取限制在预测的关系子集上,而不是所有的关系;然后用特定于关系的序列标记组件处理subject-object之间的重叠问题;最后设计一个全局对应组件来以较低的复杂度将主客体对齐成三元组。在两个公共数据集上达到了新的SOTA。

      1 Introduction

      ​ 关系抽取是从非结构化文本中识别(subject,relation,object)三元组。本文将其分解为三个子任务:1.关系判断:识别句子中的关系;2.实体提取:识别句子中的subject和object;3.subject-object对齐:将subject-object对齐成一个三元组

      ​ 对于关系判断:本文==通过Potential Relation Prediction\mathcal{Potential\ Relation\ Prediction }Potential Relation Prediction组件来预测潜在关系==,而不是保留所有的冗余关系,这降低了计算复杂度,取得了更好的性能,特别是在实体提取方面。在实体提取方面:本文使用了一个更健壮的 Relation Specific Sequence Tag\mathcal{Relation\ Specific\ Sequence\ Tag}Relation Specific Sequence Tag组件(简称Rel-Spec Sequence Tag)来==分别提取subject和object==,以自然地处理subject和object之间的重叠。对于subject-object对齐:本文设计了==与一个关系无关的全局对应矩阵==来判断特定的subject-object对在三元组中是否有效。

      ​ 在给定句子的情况下,PRGC首先预测潜在关系的子集和包含所有subject-object之间对应分数的全局矩阵**;然后进行序列标注,并行地提取每个潜在关系的主客体**;最后枚举所有预测的实体对,然后通过全局对应矩阵进行剪枝。

      2 Method

      2.1 Problem Definition

      ​ 输入是具有n个token的句子S=x1,x2,…,xnS={x_1,x_2,…,x_n}S=x1​,x2​,…,xn​,期望的输出是关系三元组T(S)=(s,r,o)∣s,o∈E,r∈RT(S)={(s,r,o)|s,o \in E, r\in R}T(S)=(s,r,o)∣s,o∈E,r∈R,其中E、RE、RE、R分别表示实体集和关系集。

      2.1.1 Relation Judgement

      ​ 对于给定句子SSS,该子任务是预测它句子SSS包含的潜在关系,输出为:Yr(s)=r1,r2,…,rm∣ri∈RY_r(s)={r_1,r_2,…,r_m|r_i\in R}Yr​(s)=r1​,r2​,…,rm​∣ri​∈R,其中m为潜在关系子集的大小。

      2.1.2 Entity Extraction

      ​ 对于给定句子SSS和预测的潜在关系rir_iri​,该子任务是使用BIO标记方案识别每个token的tag,其中tjt_jtj​表示tag。输出为:Ye(S,ri∣ri∈R)=t1,t2,…,tnY_e(S,r_i|r_i\in R)={t_1,t_2,…,t_n}Ye​(S,ri​∣ri​∈R)=t1​,t2​,…,tn​。

      2.1.3 Subject-object Alignment

      ​ 对于给定句子SSS,该子任务预测主语和宾语的起始tokens之间的对应分数。即真正的三元组中的subject-object对的得分较高。输出为:Ys(S)=M∈Rn×nY_s(S)=M \in R^{n\times n}Ys​(S)=M∈Rn×n,其中MMM表示全局对应矩阵。

      2.2 PRGC Encoder

      ​ 通过BERT对句子S进行编码。encoder的输出:Yenc(S)=h1,h2,…,hnY_{enc}(S)={h_1,h_2,…,h_n}Yenc​(S)=h1​,h2​,…,hn​,其中n表示tokens数。

      2.3 PRGC Decoder

      2.3.1 Potential Relation Prediction

      b3voD0.png

      ​ 图中RpotR^{pot}Rpot表示潜在关系

      给定句子SSS,首先预测句子中可能存在的潜在关系的子集,然后只需要提取用到这些潜在关系的实体。给定n个tokens的句子嵌入h∈Rn×dh\in \mathbb{R}^{n\times d}h∈Rn×d,该潜在关系预测的每个元素为:
      429d8117-cbc1-4c1c-bab3-594757b40595-image.png

      其中AvgpollAvgpollAvgpoll是平均池化操作,Wr∈Rd×1\mathrm{W}_r\in \mathbb{R}^{d\times 1}Wr​∈Rd×1是可训练权重,σ\sigmaσ是sigmod函数。

      本文将其潜在关系预测建模为一个多标签二进制分类任务,如果概率超过某个阈值λ1\lambda _1λ1​,则为对应关系分配标签1,否则将对应的关系标签置为0;接下来只需要==将特定于关系的序列标签==应用于预测关系,而不要预测全部关系。

      2.3.2 Relation-Specific Sequence Tagging

      ​ 如图1所示,通过2.3.1节中的组件获得了描述的潜在关系的几个特定于关系的句子表示。然后,模型执行两个序列标注操作来分别提取主体和客体。

      ​ 作者之所以将主语和宾语分开提取,是为了处理一种特殊的重叠模式,即主语宾语重叠(SOO)。作者放弃了传统的LSTM-CRF网络,而采用了简单的全连接神经网络进行实体关系识别。该组件对每个token的具体操作如下:
      f1b400db-50e9-4a50-a15e-0997294e81cf-image.png
      其中uj∈Rd×1u_j\in \mathbb{R}^{d\times 1}uj​∈Rd×1是训练嵌入矩阵U∈Rd×nrU\in \mathbb{R}^{d\times n_r}U∈Rd×nr​中第j个关系表示,nrn_rnr​是全部关系集合的大小,hi∈Rd×1h_i\in \mathbb{R}^{d\times 1}hi​∈Rd×1是第i个token的编码表示,Wsub,Wobj∈Rd×3W_{sub},W_{obj}\in \mathbb{R}^{d\times 3}Wsub​,Wobj​∈Rd×3是训练权重

      2.3.3 Global Correspondence

      ​ 在序列标注之后,分别获得关于句子关系的所有可能的主语和宾语,然后使用全局对应矩阵来确定正确的主语和宾语对。应该注意的是,全局对应矩阵可以与潜在关系预测同时学习,因为它独立于关系。

      ​ 具体过程如下:首先枚举所有可能的subject-object对;然后在全局矩阵中检查每对subject-object对的对应分数,如果该值超过某个阈值λ2\lambda _2λ2​,则保留该分数,否则将其过滤掉。图1中的绿色矩阵M∈Rn×nM\in \mathbb{R}^{n\times n}M∈Rn×n即是全局对应矩阵,由n个token组成的句子。矩阵中的每个元素都与subject-object对的起始位置有1关,位置代表主客体对的置信度,值越高属于三元组的置信度就越高,矩阵中每个元素的值如下所示:
      2b11edbd-a5b5-459e-ad24-db8a1638def1-image.png
      其中hisub,hjobj∈Rd×1h_i^{sub},h_j^{obj}\in \mathbb{R}^{d\times 1}hisub​,hjobj​∈Rd×1是形成潜在subject-object对的输入语句中的第i个token和第j个token的编码表示,Wg∈R2d×1W_g\in \mathbb{R}^{2d\times 1}Wg​∈R2d×1是可训练权重,σ\sigmaσ是sigmod函数。

      2.4 Training Strategy

      373e27a8-60b9-4fc6-b63b-4199d9d29604-image.png

      其中nrn_rnr​表示关系集的大小,nrpotn_r^{pot}nrpot​表示潜在关系子集的大小,总的损失是:
      8957bc9f-0c5e-4952-8692-fb14eeb0ae3b-image.png

      3 Experiments

      b3vIuq.png

      本文通过了PRGCRandomPRGC_{Random}PRGCRandom​来验证的PRGC解码器的有效性,其中编码器BERT的所有参数都是随机初始化的。PRGCRandomPRGC_{Random}PRGCRandom​的性能表明,即使不利用预先训练的BERT语言模型,本文的解码器框架仍然比其他解码器框架更具竞争力和健壮性。

      模型的具体参数:使用BERT-base作为编码器、句子长度设为100,V100GPU,100个epochs

      4 启示

      1. 先抽取潜在关系再抽取与潜在关系有关的实体最后进行subject-object的对齐会提高模型的解码速度和算力资源。
      2. 潜在关系预测阈值λ1\lambda _1λ1​越高,模型的性能越好
      3. 三个损失函数的调参是一个工作量问题。
      4. 如果句子长度太长最后subject-object的对齐工作消耗的空间资源会很大。
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • CIFAR-10 数据集实战——构建ResNet18神经网络

      如果不了解ResNet的同学可以先看我的这篇博客ResNet论文阅读

      首先实现一个Residual Block

      import torch
      from torch import nn
      from torch.nn import functional as F
      
      class ResBlk(nn.Module):
          def __init__(self, ch_in, ch_out, stride=1):
              super(ResBlk, self).__init__()
              self.conv1 = nn.Conv2d(ch_in, ch_out, kernel_size=3, stride=stride, padding=1)
              self.bn1 = nn.BatchNorm2d(ch_out)
              
              self.conv2 = nn.Conv2d(ch_out, ch_out, kernel_size=3, stride=1, padding=1)
              self.bn2 = nn.BatchNorm2d(ch_out)
              
              if ch_out == ch_in:
                  self.extra = nn.Sequential()
              else:
                  self.extra = nn.Sequential(
                      
                      # 1×1的卷积作用是修改输入x的channel
                      # [b, ch_in, h, w] => [b, ch_out, h, w]
                      nn.Conv2d(ch_in, ch_out, kernel_size=1, stride=stride),
                      nn.BatchNorm2d(ch_out),
                  )
              
          def forward(self, x):
              out = F.relu(self.bn1(self.conv1(x)))
              out = self.bn2(self.conv2(out))
      
              # short cut
              out = self.extra(x) + out
              out = F.relu(out)
              
              return out
      

      Block中进行了正则化处理,以使train过程更快更稳定。同时要考虑,如果两元素的ch_in和ch_out不匹配,进行加法时会报错,因此需要判断一下,如果不想等,就用1×1的卷积调整一下

      测试一下

      blk = ResBlk(64, 128, stride=2)
      tmp = torch.randn(2, 64, 32, 32)
      out = blk(tmp)
      print(out.shape)
      

      输出的shape大小是torch.Size([2, 128, 16, 16])

      这里解释一下,为什么有的层要专门设置stride。先不考虑别的层,对于一个Residual block,channel从64增大到128,如果所有的stride都是1,padding也是1,那么图片的w和h也不会变,但是channel增大了,此时就会导致整个网络的参数增多。而这才仅仅一个Block,更不用说后面的FC以及更多Block了,所以stride不能全部设置为1,不要让网络的参数一直增大

      然后我们搭建完整的ResNet-18

      class ResNet18(nn.Module):
          def __init__(self):
              super(ResNet18, self).__init__()
              
              self.conv1 = nn.Sequential(
                  nn.Conv2d(3, 64, kernel_size=3, stride=3, padding=0),
                  nn.BatchNorm2d(64),
              )
              # followed 4 blocks
              
              # [b, 64, h, w] => [b, 128, h, w]
              self.blk1 = ResBlk(64, 128, stride=2)
              # [b, 128, h, w] => [b, 256, h, w]
              self.blk2 = ResBlk(128, 256, stride=2)
              # [b, 256, h, w] => [b, 512, h, w]
              self.blk3 = ResBlk(256, 512, stride=2)
              # [b, 512, h, w] => [b, 512, h, w]
              self.blk4 = ResBlk(512, 512, stride=2)
              
              self.outlayer = nn.Linear(512*1*1, 10)
          
          def forward(self, x):
              x = F.relu(self.conv1(x))
              
              # 经过四个blk以后 [b, 64, h, w] => [b, 512, h, w]
              x = self.blk1(x)
              x = self.blk2(x)
              x = self.blk3(x)
              x = self.blk4(x)
              
              x = self.outlayer(x)
              
              return x
      

      测试一下

      x = torch.randn(2, 3, 32, 32)
      model = ResNet18()
      out = model(x)
      print("ResNet:", out.shape)
      

      结果报错了,错误信息如下

      size mismatch, m1: [2048 x 2], m2: [512 x 10] at /pytorch/aten/src/TH/generic/THTensorMath.cpp:961
      

      问题在于我们最后定义线性层的输入维度,和上一层Block的输出维度不匹配,在ResNet18的最后一个Block运行结束后打印一下当前x的shape,结果是torch.Size([2, 512, 2, 2])

      解决办法有很多,可以修改线性层的输入进行匹配,也可以在最后一层Block后面再进行一些操作,使其与512匹配

      先给出修改后的代码,在做解释

      class ResNet18(nn.Module):
          def __init__(self):
              super(ResNet18, self).__init__()
              
              self.conv1 = nn.Sequential(
                  nn.Conv2d(3, 64, kernel_size=3, stride=3, padding=0),
                  nn.BatchNorm2d(64),
              )
              # followed 4 blocks
              
              # [b, 64, h, w] => [b, 128, h, w]
              self.blk1 = ResBlk(64, 128, stride=2)
              # [b, 128, h, w] => [b, 256, h, w]
              self.blk2 = ResBlk(128, 256, stride=2)
              # [b, 256, h, w] => [b, 512, h, w]
              self.blk3 = ResBlk(256, 512, stride=2)
              # [b, 512, h, w] => [b, 512, h, w]
              self.blk4 = ResBlk(512, 512, stride=2)
              
              self.outlayer = nn.Linear(512*1*1, 10)
          
          def forward(self, x):
              x = F.relu(self.conv1(x))
              
              # 经过四个blk以后 [b, 64, h, w] => [b, 512, h, w]
              x = self.blk1(x)
              x = self.blk2(x)
              x = self.blk3(x)
              x = self.blk4(x)
              
              # print("after conv:", x.shape) # [b, 512, 2, 2]
              
              # [b, 512, h, w] => [b, 512, 1, 1]
              x = F.adaptive_avg_pool2d(x, [1, 1])
              
              x = x.view(x.size(0), -1) # [b, 512, 1, 1] => [b, 512*1*1]
              x = self.outlayer(x)
              
              return x
      

      这里我采用的是第二种方法,在最后一个Block结束以后,接了一个自适应的pooling层,这个pooling的作用是将不论输入的宽高是多少,全部输出称宽高都是1的tensor,其他维度保持不变。然后再做一个reshape操作,将[batchsize, 512, 1, 1]reshape成[batchsize, 512*1*1]大小的tensor,这样就和接下来的线性层对上了,线性层的输入大小是512,输出是10。因此整个网络最终输出的shape就是[batchsize, 10]

      最后我们把之前训练LeNet5的代码拷贝过来,将里面的model=LeNet5()改为model=ResNet18()就行了。完整代码如下

      import torch
      from torch import nn, optim
      import torch.nn.functional as F
      from torch.utils.data import DataLoader
      from torchvision import datasets, transforms
      
      
      batch_size=32
      cifar_train = datasets.CIFAR10(root='cifar', train=True, transform=transforms.Compose([
          transforms.Resize([32, 32]),
          transforms.ToTensor(),
      ]), download=True)
      
      cifar_train = DataLoader(cifar_train, batch_size=batch_size, shuffle=True)
      
      cifar_test = datasets.CIFAR10(root='cifar', train=False, transform=transforms.Compose([
          transforms.Resize([32, 32]),
          transforms.ToTensor(),
      ]), download=True)
          
      cifar_test = DataLoader(cifar_test, batch_size=batch_size, shuffle=True)      
      
      class ResBlk(nn.Module):
          def __init__(self, ch_in, ch_out, stride=1):
              super(ResBlk, self).__init__()
              self.conv1 = nn.Conv2d(ch_in, ch_out, kernel_size=3, stride=stride, padding=1)
              self.bn1 = nn.BatchNorm2d(ch_out)
              
              self.conv2 = nn.Conv2d(ch_out, ch_out, kernel_size=3, stride=1, padding=1)
              self.bn2 = nn.BatchNorm2d(ch_out)
              
              if ch_out == ch_in:
                  self.extra = nn.Sequential()
              else:
                  self.extra = nn.Sequential(
                      
                      # 1×1的卷积作用是修改输入x的channel
                      # [b, ch_in, h, w] => [b, ch_out, h, w]
                      nn.Conv2d(ch_in, ch_out, kernel_size=1, stride=stride),
                      nn.BatchNorm2d(ch_out),
                  )
              
          def forward(self, x):
              out = F.relu(self.bn1(self.conv1(x)))
              out = self.bn2(self.conv2(out))
      
              # short cut
              out = self.extra(x) + out
              out = F.relu(out)
              
              return out
              
      class ResNet18(nn.Module):
          def __init__(self):
              super(ResNet18, self).__init__()
              
              self.conv1 = nn.Sequential(
                  nn.Conv2d(3, 64, kernel_size=3, stride=3, padding=0),
                  nn.BatchNorm2d(64),
              )
              # followed 4 blocks
              
              # [b, 64, h, w] => [b, 128, h, w]
              self.blk1 = ResBlk(64, 128, stride=2)
              # [b, 128, h, w] => [b, 256, h, w]
              self.blk2 = ResBlk(128, 256, stride=2)
              # [b, 256, h, w] => [b, 512, h, w]
              self.blk3 = ResBlk(256, 512, stride=2)
              # [b, 512, h, w] => [b, 512, h, w]
              self.blk4 = ResBlk(512, 512, stride=2)
              
              self.outlayer = nn.Linear(512*1*1, 10)
          
          def forward(self, x):
              x = F.relu(self.conv1(x))
              
              # 经过四个blk以后 [b, 64, h, w] => [b, 512, h, w]
              x = self.blk1(x)
              x = self.blk2(x)
              x = self.blk3(x)
              x = self.blk4(x)
              
              # print("after conv:", x.shape) # [b, 512, 2, 2]
              
              # [b, 512, h, w] => [b, 512, 1, 1]
              x = F.adaptive_avg_pool2d(x, [1, 1])
              
              x = x.view(x.size(0), -1) # [b, 512, 1, 1] => [b, 512*1*1]
              x = self.outlayer(x)
              
              return x
      
      def main():
      
          ##########  train  ##########
          #device = torch.device('cuda')
          #model = ResNet18().to(device)
          criteon = nn.CrossEntropyLoss()
          model = ResNet18()
          optimizer = optim.Adam(model.parameters(), 1e-3)
          for epoch in range(1000):
              model.train()
              for batchidx, (x, label) in enumerate(cifar_train):
                  #x, label = x.to(device), label.to(device)
                  logits = model(x)
                  # logits: [b, 10]
                  # label:  [b]
                  loss = criteon(logits, label)
                  
                  # backward
                  optimizer.zero_grad()
                  loss.backward()
                  optimizer.step()
              
              print('train:', epoch, loss.item())
              
              ########## test  ##########
              model.eval()
              with torch.no_grad():
                  total_correct = 0
                  total_num = 0
                  for x, label in cifar_test:
                      # x, label = x.to(device), label.to(device)
      
                      # [b]
                      logits = model(x)
                      # [b]
                      pred = logits.argmax(dim=1)
                      # [b] vs [b]
                      total_correct += torch.eq(pred, label).float().sum().item()
                      total_num += x.size(0)
                  acc = total_correct / total_num
                  print('test:', epoch, acc)
      
      if __name__ == '__main__':
          main()
      

      ResNet和LeNet相比,准确率提升的很快,但是由于层数增加,不可避免的会导致运行时间增加,如果没有GPU,运行一个epoch大概要15分钟。读者同样可以在此基础上修改网络结构,运用一些tricks,比方说一开始就对图片做一个Normalize等

      posted in CV领域
      155****7220
      155****7220

    Latest posts made by 155****7220

    • 通过摘要信息问题生成改进无监督问答

      bhTZM4.png

      Improving Unsupervised Question Answering via Summarization-Informed Question Generation

      通过摘要信息问题生成改进无监督问答

      1 Abstract

      问题生成(QG)是为给定的$<passage,answer>$pair 生成似是而非的问题的任务。基于模板的QG使用<u>语言信息启发式将陈述句转换为疑问句</u>,对于监督QG使用<u>现有的问答(QA)数据集来训练系统,以生成给定段落和答案的问题</u>。

      • 启发式缺点:生成的问题与它们的声明性对应问题紧密相关。
      • 监督方法:它们与用作训练数据的QA数据集的域/语言紧密相关。

      本文提出无监督的QG方法:使用从摘要中启发式生成的问题作为QG系统的训练数据的来源。(利用启发式方法将陈述性摘要句子转化为合适的问句)

      • 本文使用的启发式方法:依赖句法分析、命名实体识别、语义角色标注等。

      通过无监督QG产生问题,然后将产生的问题与原始文章结合,以端到端训练神经QG模型。

      1 Introduction

      问题生成的目的是在给定一组输入段落和相应答案的情况下产生有意义的问题。

      早期QG的研究基于模板生成,但这样的问题缺乏多样性,并且与相应的陈述句子有很高的词汇重叠度,例如:Stephen Hawking announced the party in the morningStephen\ Hawking\ announced\ the\ party\ in\ the\ morningStephen Hawking announced the party in the morning 的句子生成的问题,以Stephen Hawking(斯蒂芬·霍金)为候选答案跨度,可能是Who announced the party in the morning?(谁在早上宣布了聚会?),可以看到生成的问题和陈述句之间有很高的词汇重叠。这在问题系统中是不可取的,因为问题中强烈的词汇线索会使它成为一种很差的真正意义上的理解。

      后来神经seq2seq模型成为QG的主导,通常从人类创建的QA数据集获得$<passage,answer,query>$三元组训练,这种方法限制了对数据集的领域和语言的应用,并且需要大量的时间和资金。

      本文提出一种新的无监督方法,将QG描述成一个摘要-提问过程(summarization-questioning)。通过使用免费获得的摘要数据,对摘要进行依存关系分析、命名实体识别和语义角色标注,然后应用启发式方法根据解析的摘要生成问题。

      图一显示了一个实例(通过使用不同候选答案span的摘要句子的语义角色标注启发式生成的示例问题):

      bhTesJ.png

      问题要从摘要中产生而不是原始段落中,因此摘要是作为问题和段落之前的桥梁存在的,最后生成的问题和段落的词汇重叠部分也较少,这种方法是可行的,因为摘要中包含了段落中最重要的信息,在语义上也和段落接近。另外摘要数据要比QA数据集获取要容易的多,因为许多QA数据集是专门为训练QA系统而创建的。

      2 Realated Work

      在无监督QA中,使用基于QG模型的合成数据而不是现有的QA数据集来训练QA模型。代替求助于现有的QA数据集,采用了无监督的QG方法,例如无监督的神经机器翻译Unsupervised Question Answering by Cloze Translation、Template-Based Question Generation from Retrieved Sentences for Improved Unsupervised Question Answering。Harvesting and Refining Question-Answer Pairs for Unsupervised QA提出了基于模板/规则的问题生成方法,并将检索到的段落和被引用的段落作为源段落,以缓解段落和问题之间的词汇相似问题。

      3 Methodology

      本文提出的方法使用合成的QG数据,然后使用一些启发式方法从摘要数据创建QG数据来训练QG模型。

      图2中展示了本文的模型(答案和问题是基于问题生成启发式的摘要生成的,答案与文章结合形成编码器的输入,问题被用作解码器输出的ground-truth):

      bhTmL9.png

      3.1 Question Generation

      为了避免生成与相应说明性语句高度相似的琐碎问题,本文采用摘要数据作为连接生成的问题和原始文章的桥梁。

      • 对摘要句进行依存分析(DP),然后是命名实体识别和语义角色分析(SRL)
        • DP被用来识别主要动词(动词根)和其他成分(助动词)的一种手段。
        • NER负责摘要句子中的所有实体,以便于发现要生成的最合适的问句。
        • 语句分析的关键是SRL,被用来获取摘要句子的所有语义框架,每个框架有一个动词和一组论元组成,这些论元对应于句子中的短语。
          • 例如,参数可以包括AgentAgentAgent(其发起由动词描述的动作)、PatientPatientPatient(其进行该动作)以及一组修饰符参数,如ARG-TMP或ARG-LOC
        • 根据论元类型和NER标签从论元生成疑问句,这意味着可以共同确定wh-words

      图1中的示例:给出SRL分析[U2’s lead singer BonoU2’s\ lead\ singer\ BonoU2’s lead singer Bono ARG-0 ]has [hadhadhad VERB] [emergency spinal surgeryemergency\ spinal\ surgeryemergency spinal surgery ARG-1] [after suffering an injury while preparing for tour datesafter\ suffering\ an\ injury\ while\ preparing\ for\ tour\ datesafter suffering an injury while preparing for tour dates ARG-TMP]。根据这三个论点可以生成图1中所示的三个问题。

      bhTesJ.png

      3.2 Training a Question Generation Model

      本文使用的摘要数据由$<passage-summary>$对组成。问题是使用3.1节中描述的启发式方法从摘要中生成的,这样就有了$<passage-sumary>$对和$<summary-question-answer>$三元组,然后我们将它们组合成$<passage-answer-question>$三元组,以训练QG模型。

      本文训练一个端到端的seq2seq模型,而不是部署一个管道,首先生成摘要,然后再生成问题,以消除生成过程中错误积累的风险。通过使用这些QG数据来训练神经生成模型,期望该模型学习summary和问题生成的组合。换句话说,==这样的知识可以通过QG数据隐含地注入到神经生成模型中==。

      为了训练问题生成模型,本文将每个段落和答案连接起来,形成一个序列:$passage<SEP>answer<SEP>$,其中$<SEP>$是用于分隔段落和答案的特殊符号。这个序列是输入,目标输出(目标)是question。本文使用BART进行生成,通过以下负对数似然损失函数进行优化:
      1ae6416a-8d57-4836-b6a5-142622e0d453-image.png
      其中qiq_iqi​是question的第iii个token,C、AC、AC、A表示上下文和答案。

      4 Experiments

      4.1 Experiment Setup

      4.1.1 Question Generation

      Datasets本文使用BBC新闻网站抓取的XSUM的新闻摘要数据来测试提出的方法。XSUM包括226,711个$<passage-summary>$对,每个摘要包含一个句子。

      QG Details使用AllenNLP来获取摘要句子的依存关系树、命名实体和语义角色标签。

      删除满足以下三个条件中的三元组:

      1. 超过480个token的文章(超过最大BART输入长度);
      2. 文章中答案跨度中不超过55%的token的文章(以确保答案和短文之间有足够的词汇重叠)
      3. 5个记号以下的问题(非常短的问题可能删除了太多的信息);

      一共产生了14,830个$<passage-answer-question>$个三元组

      4.1.2 Unsupervised QA

      Datasets在六个抽取的问答数据集上进行了实验,分别是SQuAD1.1、NewsQA、Natural Questions、TriviaQA、BioASQ和DuoRC。

      本文使用SQuAD1.1、NewsQA和TriviaQA的官方数据,对于Natural Questions、BioASQ和DuoRC,使用MRQA发布的预处理数据。

      Unsupervised QA Training Details为了生成合成的QA训练数据,本文利用维基转储(Wikidumps),首先删除所有HTML标签和引用链接,然后提取长度超过500个字符的段落,从维基转储的所有段落中抽取60k个段落。使用Spacy和AllenNLP的NER工具包来提取段落中的实体提及。

      然后,删除满足以下三个条件中的一个或多个的段落,即答案对:

      1. 少于20个单词而超过480个单词的段落;
      2. 没有提取答案的段落,或者由于文本tokenization而提取的答案不在段落中;
      3. 由单个代词组成的答案。

      将段落和答案连接成形式$passage<SEP>answer<SEP>$的序列,然后输入到训练好的BART-QG模型中获得相应的问题。这产生了20k个合成QA对,然后将其用于训练无监督QA模型。

      4.2 Results

      使用生成的2万个合成问答对来训练BERT QA模型,并首先在基于维基百科的三个基准问答数据集SQuAD1.1、Natural Questions和TriviaQA的验证集上验证了该模型的性能。本文方法的结果如表1和表2所示。

      bhTEzF.png

      bhTARU.png

      无监督的基线:

      1. Unsupervised Question Answering by Cloze Translation采用无监督神经机器翻译训练QG模型,生成4M个合成QA实例来训练QA模型
      2. Harvesting and Refining Question-Answer Pairs for Unsupervised QA使用依存关系树来生成问题,并使用被引用的文档作为段落

      4.3 2 Effect of Different Heuristics不同启发式的效果

      • Naive−QG\mathrm{Naive-QG}Naive−QG 只使用摘要句作为上下文(不是原始段落 ),只用适当的问句替换答案的span。例如Stephen Hawking announced the party in the morningStephen\ Hawking\ announced\ the\ party\ in\ the\ morningStephen Hawking announced the party in the morning的句子,以partypartyparty为答案span,Naive−QG\mathrm{Naive-QG}Naive−QG产生的问题会是Stephen Hawking announced what in the morning?Stephen\ Hawking\ announced\ what\ in\ the\ morning?Stephen Hawking announced what in the morning?。采用摘要句作为输入,问题作为目标输出,形成QG训练数据。
      • Summary−QGSummary-QGSummary−QG使用摘要的原文作为段落,而不是摘要句,以避免段落和问题之间的词汇高度重叠。
        • Main Verb\mathrm{Main\ Verb}Main Verb主谓词:只根据摘要句依存关系树中主谓词的SRL框架生成问题,而在从句中使用动词;
        • Wh−Movement\mathrm{Wh-Movement}Wh−Movement将问题词移动到句子的开头;
        • Decomp−Verb\mathrm{Decomp-Verb}Decomp−Verb分解动词:主要动词被分解成基本形式和助词;
        • NER−Wh\mathrm{NER-Wh}NER−Wh:使用NER标签来获得更准确的问句来回答,例如,对于NBA player Michael JordanNBA\ player\ Michael\ JordanNBA player Michael Jordan的答案跨度,问题词将是which NBA playerwhich\ NBA\ playerwhich NBA player而不是who or whatwho\ or\ whatwho or what。

      bhTuZR.png

      5 启示

      1. 能不能根据一些描述生成问题呢?
      2. 启发式的算法可以是一些属性知识吗?

      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • 基于有限文本语料库的问答对比领域自适应

      b24w4J.png

      Contrastive Domain Adaptation for Question Answering using Limited Text Corpora

      基于有限文本语料库的问答对比领域自适应

      code

      Abstract

      问题生成在新领域定制QA系统方面取得了不错的成果,这些方法避免了来自新领域的人工标注的训练数据的需要,而是生成用于训练的合成问答对(synthetic question-answer pairs)。本文提出了一种新的领域自适应框架,称为用于QA的对比领域自适应(CAQA),具体来说,CAQA结合了问题生成和领域不变学习技术,在文本语料库有限的情况下回答域外问题。

      1 Introduction

      抽取式阅读理解的一个挑战是训练数据(源域)和测试数据(目标域)之间的分布变化,如果目标域出现的域外样本偏离了QA系统的训练语料库,那么QA系统的准确性一定会下降。解决上述问题的方法是使用问题生成的模型从目标领域的语料库生成合成数据,然后在训练期间使用该合成数据。将合成数据作为来自目标领域的替代物,从而可以用来自源域的数据和合成数据来训练QA系统,有助于在域外数据分布上取得更好的结果,这种方法的概述如图1所示:

      b24D3R.png

      然而大量的合成数据需要密集的计算资源,对于目标领域大小有限的情况生成合成数据就设置了障碍,本文借鉴了计算机视觉中的一种领域适应方法解决以上问题,即表示差异减少。通过设计一个自适应loss或对抗训练方法来学习领域不变特征,以便模型能够将学习到的知识从源域转移到目标域。

      本文开发了一个在有限文本语料库的问答环境下回答域外问题的框架,作者称为对比域外问答适应contrastive domain adaptation for question answering(CAQA)。CAQA结合问题生成和对比领域自适应来学习领域不变特征,从而能够捕获这两个领域,从而将知识转移到目标分布。现有的问题生成中,合成数据仅用于与源数据的联合训练,并没有考虑迁移,因此作者提出一种新的针对QA的对比适应损失。该对比适应损失使用最大平均差异(MMD)来度量源特征和目标特征在表示上的差异。

      2 Realated Work

      当训练数据(源域)不同于测试期间使用的数据(目标域)时,提取问答系统的性能恶化。使用QA系统适应特定领域的方法可以分为:1) 有监督的方法,人们可以访问来自目标领域的标记数据;2)无监督方法,没有标记的信息是不可访问的。后者是本文关注的重点,其中无监督方法主要基于问题生成技术,其中一个为目标领域生成合成训练数据。

      2.1 Qusetion generation(QG)

      问题生成是从原始文本数据中生成合成QA对的任务,本文利用生成的问题微调QA系统以适应新的目标领域。

      2.2 Unsupervised domain adaptation

      计算机视觉领域已经完成了大量关于无监督领域自适应的工作,其中减少了标记的源数据和未标记的目标数据集之间的表示差异。最近主要是基于对抗学习的方法,其中最小化源域和目标域中的特征分布之间的距离,同时最小化标记源域中的误差。==与对抗学习不同,对比学习是利用一种特殊的损失,该损失减少了来自同一类别的样本的差异,并增加了来自不同类别的样本的距离,这是通过使用距离度量或三元组loss和聚类技术实现的==。最近,对比适应网络contrastive adaptation network(CAN)被证明通过使用最大平均差异来构建一个目标函数来实现最先进的性能。

      3 The CAQA Framework: Contrastive Domain Adaptation for QA针对QA的对比性领域适应

      3.1 Setup

      3.1.1 Input

      本文的框架是使用基于分布变化下的QA数据。设Ds\mathcal{D}_sDs​表示源域,Dt\mathcal{D}_tDt​表示目标域,其中Ds≠Dt\mathcal{D}_s \ne \mathcal{D}_tDs​​=Dt​。输入是通过以下方式给出的:

      • Training data from source domain来自源域的训练数据:
        • 从源域获得标记数据XsX_sXs​:
          • 其中来自源域的Ds\mathcal{D}_ sDs​的每个样本xs(i)∈Xsx_s^{(i)}\in X_sxs(i)​∈Xs​由question xs,q(i)x_{s,q}^{(i)}xs,q(i)​、context xs,c(i)x_{s,c}^{(i)}xs,c(i)​、answer xs,a(i)x_{s,a}^{(i)}xs,a(i)​组成的三元组。
      • Target contexts目标上下文:
        • 可以访问目标域数据,但是这些数据都是没有标签的,也就是说只能访问上下文,进一步假设目标上下文的数量是有限的
        • 设Xt‘X_ t^{{}‘}Xt‘​表示未标记的目标数据,其中来自目标域Dt\mathcal{D}_ tDt​的每个样本xt(i)∈Xt’x_ t^{(i)}\in X_ t^{{}’}xt(i)​∈Xt’​,这个数据仅由上下文xt,c(i)x_ {t,c}^{(i)}xt,c(i)​组成。

      3.1.2 Objective

      本文的目标是在回答来自目标域Dt\mathcal{D}_ tDt​的问题时最大化QA系统的性能,即最小化来自目标域Dt\mathcal{D}_ tDt​的XtX_ tXt​的QA系统f\mathcal{f}f的交叉熵损失:
      fd2e30dc-dc4f-4c5a-99b6-811ca9345606-image.png
      在部署QA系统之前,来自目标域的实际question-answer对是未知的,此外,预计可用的上下文在大小上是有限的的,作者称之为有限的文本语料库。在作者的实验中一个上下文只有5个QA对,总共有10K个段落作为上下文。

      3.1.3 Overview

      CAQA框架主要有三个组件组成:

      • 问题生成模型
      • QA模型
      • 领域适应的对比适应损失

      b24rg1.png

      本文通过fgenf_{gen}fgen​引用问题生成模型,通过fff引用问答模型,问题生成模型fgenf_{gen}fgen​用于合成问答数据:
      30944a6e-adcd-4f67-859e-d22d55ae2b20-image.png
      由此产生了有xt,q(i)x_{t,q}^{(i)}xt,q(i)​和xt,a(i)x_{t,a}^{(i)}xt,a(i)​ for ∈Xt′\in X_t^{{}'}∈Xt′​组成的额外的QA对.

      最后**,使用源数据XsX_sXs​和合成数据XtX_tXt​通过提出的对比适应损失来训练QA模型**,其背后的思想是通过减少差异和答案分离来帮助将知识转移到目标领域。

      3.2 Question Generation

      问题生成模型QAGen-T5将语境作为输入,通过两个步骤生成问题和答案来建立合成数据:

      1. 首先基于目标领域的语境xcx_cxc​生成问题xqx_qxq​
      2. 以给定的xcx_cxc​和xqx_qxq​为条件生成对应的答案xax_axa​

      本文利用text-to-text transfer transformer(T5) encoder-decoder transformer作为QG模型,通过两个T5transformer生成两个不同的输出xqx_qxq​和xax_axa​。

      • 生成问题的 inputinputinput 是一个context段落,在开头加上token Generate Query:Generate\ Query:Generate Query:
        • input: generate question: python is a programming language…generate\ question:\ python\ is\ a\ programming\ language…generate question: python is a programming language…’
        • output: when was python released?when\ was\ python\ released?when was python released?
      • 对于答案生成,同时使用问题和上下文的 inputinputinput 是通过token question:question:question: 和 context:context:context:
        • the input: question: when was python released? context: python is a programming language…question:\ when\ was\ python\ released?\ context:\ python\ is\ a\ programming\ language…question: when was python released? context: python is a programming language…
        • output is the decoded answer输出是解码后的答案

      QAGen-T5的训练如下,分别通过以下方式最小化输出序列的负对数似然:
      1dd19ebb-8fc4-413d-ac2e-c865a00575df-image.png
      其中xq(i),xa(i),xc(i)x_q^{(i)},x_a^{(i)},x_c^{(i)}xq(i)​,xa(i)​,xc(i)​指的是XXX的第i个样本中的问题、答案和上下文。

      QAGen-T5的参数在SQuAD数据集上进行微调,对于选择QA对,本文利用LM-filtering Generating Diverse and Consistent QA pairs from Contexts with Information-Maximizing Hierarchical Conditional VAEs - ACL Antholog来为每个上下文选择最佳的k个QA对(例如,在本文的实验中选择k=5)。通过将每个token的分数乘以输出长度来计算答案的LM分数。这确保了只有在问题和答案的组合可能性很高的情况下才会生成合成的QA样本。

      3.4 Contrastive Adaptation Loss对比性适应损失

      通过对比性适应损失训练QA模型,通过对比性损失达到两个效果

      • 分别减少答案标记之间和其他标记之间的差异(“类内”),因此应该鼓励模型学习源域和目标域都具有的域不变特征
      • 扩大特征表征(类间)中answer-context和answer-question的差异

      本文的方法在某种程度上类似于但又不同于计算机视觉中的对比性领域适应,在计算机视觉中,类内差异也被减小,而类间差异被扩大。在计算机视觉中,标签是明确定义的,这样的标签在QA中是不可用的。一种自然的方法是==将答案span的每对开始/结束位置视为单独的类。然而,相应的空间会非常大,并且不会表示特定的语义信息==。相反,我们构建了一种不同的类概念:我们将所有答案令牌视为一个类,将问题和上下文令牌的组合集视为单独的类。然后,当<u>我们缩小类内差异,扩大类间差异时,知识就从源域转移到目标域</u>

      3.4.1 Discrepancy差异

      在对比适应损失中,本文使用平均差异(MMD)来度量标记类别之间的差异。MMD根据从两个数据分布中提取的样本来测量两个数据分布之间的距离。

      3.4.2 Contrastive adaptation loss

      将具有源域和目标域样本的混合batch XXX 的对比适应损失定义为:
      0a4f705b-0251-4575-a717-d03b74520bcb-image.png
      其中xax_axa​是回答token的平均向量,而xcqx_{cq}xcq​是context/question token的平均向量。ϕ\phiϕ是特征提取器(BERT),前两项分别估计所有答案标记和其他标记之间的平均距离,从而实现最小化类内差异;最后一项最大化了答案和rest token之间的距离,并使得答案提取更容易,从而实现最大限度地扩大类间差异。

      3.4.3 Overall objective

      将BERT-QA的交叉熵和对比性适应损失整合到QA模型的单个优化目标中:
      8232e8a1-4d31-48f3-b86a-cdbced20a5db-image.png
      β\betaβ是经验性超参数。

      4 Results

      b24BC9.png

      5 启示

      1. question generation 的方法解决zero-shot问题是不是很好呢?
      2. 对比性自适应方法可以尝试引入其他模型中

      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • 文本理解中的门控attention阅读器

      bDNQFx.png

      Gated-Attention Readers for Text Comprehension

      文本理解中的门控attention阅读器

      Abstract

      本文研究的是完形填空问题式MRC,作者提出的门控注意力阅读器集中了多跳结构和一种新的注意力计算机制(基于query嵌入和RNN文档阅读器中间状态之间的乘积计算),这使阅读器能够在文档中构建特定于query的token表示形式。

      1 Introduction

      最近MRC模型的成功主要归因于两个原因:(1)多跳架构,允许模型迭代地扫描文档和问题以获得多次跳转。(2)注意力机制,使模型能够专注于上下文适当的子部分。直观地说,多跳体系结构允许reader递增地改进token表示,并且注意力机制根据文档中不同部分与query的相关性来重新加权文档中的不同部分。本文设计了一种新颖的注意力机制将多跳推理和注意力以互补的方式结合起来,该注意力可以在跳数之间对不断变化的token进行进行门控,该门控注意力( GA )允许query在语义级上直接与token嵌入的每个维度交互

      2 Realated Work

      完形填空风格的QA设计(d,q,a,C)(d,q,a,\mathcal{C})(d,q,a,C)形式的元组,其中ddd是文档(上下文),qqq是对ddd的内容的query,其中用占位符替换短语,aaa是qqq的答案,其中aaa来自一组候选C\mathcal{C}C(完形填空:给一篇文章ddd,给每个空提问题qqq,在给出的答案集C\mathcal{C}C中选出正确答案aaa)。

      *LSTMs with Attention:*使用LSTM单元来计算组合document-query表示g(d,q)g(d,q)g(d,q),该document-query表示用于对候选答案进行rank。其中包括DeepLSTM Reader,其执行单次正向遍历连接的(document-query)对以获得g(d,q)g(d,q)g(d,q);Attention Reader,其首先根据q的attention通过词的加权聚集来计算文档向量d(q)d(q)d(q),然后组合d(q)d(q)d(q)和qqq以获得它们的联合表示g(d(q),q)g(d(q),q)g(d(q),q);Impatient Reader,document表示是递增构建的;Stanford Attentive Reader,简化了注意力reader的结构,使用较浅的循环单元与bilinear形式进行query-document attention。

      Attention Sum:Attention-Sum(AS) Reader,使用两个双向GRU将ddd和qqq都编码成向量,通过计算qqq和实体嵌入之间的点积并取softmax,获得ddd中实体的概率分布,然后进一步应用一种称为指针和注意力的聚合机制对同一实体的概率进行求和,从而使文档中的频繁实体比稀有实体更受关注;在AS Reader的基础上,又引入了双向注意力机制,形成Attention-over-Attention(AoA) Reader。

      Muliti-hop Architectures:Memory Networks,通过聚合附近的单词将文档中的每个句子编码到存储器中。

      3 Gated-Attention Reader

      本文提出的GA Reader在上下文(文档中的单词嵌入)上执行多个跳跃,跨跳迭代细化,直到到达最终的attention-sum模块,该模块将最后一跳中的上下文表示映射到候选答案上的概率分布。本文的门控注意力是通过query和上下文嵌入之间的multiplicative(乘性)交互实现,并在多步推理过程中以每跳作为细粒度信息filters。filters分别对文档中每个token的向量表示的各个分量进行加权。gated-attention layers的设计是基于向量空间表征之间乘法交互作用的有效性。

      3.1 Model Details

      该模型结构并不复杂,通过几个BiGRU进行交互,输入序列:X=[x1,x2,…,xT]X=[x_1,x_2,…,x_T]X=[x1​,x2​,…,xT​],输出序列:H=[h1,h2,…,hT]H=[h_1,h_2,…,h_T]H=[h1​,h2​,…,hT​],其通过GRU计算过程如下:
      e7987480-dc18-49e4-8bf1-aa92a784e2fc-image.png
      其中rtr_trt​和ztz_tzt​称为复位门和更新门,h∼t\overset{\sim}{h}_th∼t​称为候选输出

      将BiGRU两个序列进行连接:
      87276c67-0909-466a-8918-4f9af6874dfc-image.png
      设X(0)=[x10,x20,…,x∣D∣(0)]X^{(0)}=[x_1^{0},x_2^{0},…,x_{|D|}^{(0)}]X(0)=[x10​,x20​,…,x∣D∣(0)​]表示文档的token嵌入,也是document reader在第1层的输入;Y=[y1,y2,…,y∣Q∣]Y=[y_1,y_2,…,y_{|Q|}]Y=[y1​,y2​,…,y∣Q∣​]表示query的token嵌入,其中∣D∣|D|∣D∣和∣Q∣|Q|∣Q∣表示文档的document和query的长度

      3.1.1 Multi-Hop Architecture

      模型图如图1所示:

      bgDKaV.png

      该模型优K层GA计算(读取document和query),上一层的输出作为下一层输入。

      通过获取文档BiGRUns的全部输出转换为文档嵌入(蓝色部分):
      1f6504b3-c132-4e4c-b4fa-91380c529b21-image.png
      特定层的query表示被计算为query的BiGRU的完整输出(绿色部分):
      ef3e9ec4-2db6-4f19-9f41-8107714fa210-image.png
      通过将Gated-Attention应用于DkD^{k}Dk和QkQ^{k}Qk,以计算下一层XkX^{k}Xk的输入:
      6c3c9441-b2dd-4c7f-a848-bd7347e4c33b-image.png

      3.1.2 Gated-Attention Module

      为了表达方便,作者省略了section3.1.1的上标k,对于D中每个token did_idi​,GA模块使用soft attention形成query q∼i\overset{\sim}{q}_iq∼​i​ 的特定于token的表示,然后将该query表示与文档token表示逐个元素相乘:
      abfdc250-380f-4a60-8fd2-95aeaa1e6d02-image.png

      启示

      1. related work写的很好
      2. 采用记忆网络的编码结构可以多次迭代更新query表示,从而提升推理潜力
      3. 门控注意力按位相乘的计算方法可以尝试使用
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • RE: 少样本学习

      @188-7853 为什么不问问神奇的Prompt

      posted in 聊一会吧
      155****7220
      155****7220
    • Vault:面向机器阅读理解的可变统一长文本表示

      blCsQs.png

      VAULT: VAriable Unified Long Text Representation for Machine Reading Comprehension

      Vault:面向机器阅读理解的可变统一长文本表示

      Abstract

      ​ 对于长passage的阅读理解来说,MRC需要复杂的模型结构有效的对question和passage的表示进行建模,因此模型需要大量的算力资源。本文提出了一个轻量级、并行高效的段落表示法模型:Vault。该模型基于长文档输入的上下文表示,使用一种新的基于高斯分布的目标进行训练,该目标==密切关注接近==ground-truth的部分正确instance。在NQ(基于维基百科)上进行评估,达到了的当前SOTA的性能,但速度快了16倍。

      1 Introduction

      ​ 当前的MRC都是集中在较短语境下的,但是许多数据集专注于较长语境,因此对于长语境的数据集,512的序列长度限制了它们的发展。对于长语境zheng等人 2020 Document Modeling with Graph Attention Networks for Multi-grained Machine Reading Comprehension提出了图神经网络的方法对文档层次结构进行建模,这种方法虽然创建了文本的强表示,但是有一个明显的缺点:图神经网络的GPU等并行硬件上的效率低下,导致推理效率低下。基于此,本文提出了一个更轻量级和并行效率高的基于长上下文表示的段落表示来为问题提供段落答案。具体来说:在PLM上(本文使用Longformer: The Long-Document Transformer)用每个段落的轻量级表示建模更长的上下文,为了给模型提供相对于文本的段落位置的概念,本文引入了可使用特殊标记(makeup)的位置感知段落表示(PAPR),并将它们作为有效的段落分类的输入,这种方法允许在==文本中编码paragraph-level的位置,并教导模型将每个段落的信息输入到这些token的隐藏输出中==,可以利用这些tokens来确定答案驻留在哪个段落中。然后,从这个识别的段落预测答案跨度。

      ​ 一般的MRC方法从上下文中提取答案span时,仅使用ground-truth开始和结束的span位置作为训练目标,并将所有其他位置视为不正确的instance。然而与ground-truth重叠的span应该也认为是正确的,Li 2020Data-dependent Gaussian Prior Objective for Language Generation提出了一种新的基于同义词先验分布的机器翻译优化准则,在此基础上,作者做出了改进,具体来说:将答案区间的起始位置和结束位置视为类高斯分布,而不是单点分布,利用统计距离对模型进行优化(利用位置感知段落表示和高斯先验优化对否定instance进行长文本建模)。作者将这个模型称为:Vault(VAriable Unified Long Text Presentation),因为它可以在任何位置处理可变数量和长度的段落。

      ​ 贡献:

      1. 提出了一种新颖、有效、简单的段落表示方法。
      2. 在训练过程中,**本文引入了软标签来利用来自接近ground-truth的局部上下文的信息**,这对于MRC来说是新颖的。
      3. 模型在NQ上提供了与SOTA系统相似的性能,同时速度提高了16倍,并且还有效地适应了一个新的领域:TechQA。
      

      2 The Approach

      blCge0.png

      基于较长上下文的段落表示,在Longformer上训练位置感知段落表示,再基于高斯优化训练目标(考虑接近ground-truth位置的部分积分,而不仅仅是关注一个ground-truth)

      2.1 ABase “Paragraph” Predictor Model

      ​ 本文采用一个大窗口的PLM:LongFormer(突破了BERT512token的限制,最大输入长度达到4096)进行长上下文编码。

      2.1.1 Position-aware Paragraph Representation (PAPR)

      ​ 许多非结构化文本(如Wikipedia页面)具有相对标准的显示某些相关信息的方式(例如,生日通常在第一段中,而配偶的名字在“个人生活”段落中),本文通过在每个段落的开头用特殊的原子标记makeup段落([ paragraph=i ][\ \mathrm{paragraph=i}\ ][ paragraph=i ])来==向基本模型提供它正在阅读的文本的哪一部分的表示,==以指示该段落在文本中的位置(在表格和列表中添加类似的标签)。再此输入表示形式下,本文使用==由特殊段落标记输出的嵌入==直接执行长答案分类。形式上来说,对于每个段落li∈Pl_i \in Pli​∈P,其中PPP是文本中的所有段落,其对应的标记token为hiph_i^phip​,段落答案a的logit值为:
      04625aa5-e833-49ec-967b-f68e777053d0-image.png
      从标准[CLS]token获得额外的document-piece表示,以对不包含段落答案的document-pieces进行建模。在给定上下文c的情况下,选择段落的概率被计算为候选段落的logit(具有答案span)的softmax,并且不包含答案logit:
      42728c2b-35d6-480c-a162-202315d5d199-image.png
      对段落进行padding,确保batch统一。首先在所有候选段落中选择具有最高logit值的段落候选,然后使用指针网络在选定的段落答案候选中提取span答案。

      2.1.2 Gaussian Prior Optimization (GPO)高斯先验优化

      ​ 最大似然估计方法提高了ground-truth位置的概率,而抑制了所有其他位置的概率。然而,文本假设,对于所有这些负面的情况,靠近ground-truth的位置应该比距离更远的位置得到更高的信任,因为提取的答案将与ground-truth部分重叠。==本文构造了在ground-truth位置具有最高概率的分布==,并根据到相应ground-truth真实位置的距离来指数的丢弃该概率。具体来说,对于ysy_sys​处的ground-truth的开始和结束位置,其中s∈start,ends \in {start,end}s∈start,end,本文使用高斯分布N(ys,σ)\mathcal{N}(y_s,\sigma)N(ys​,σ)进行丢弃,其中平均值是位置ysy_sys​,方差σ\sigmaσ是超参数。把每个位置yyy的高斯分布的概率密度φ(y∣ys,σ)\varphi(y|y_s,\sigma)φ(y∣ys​,σ)作为相应位置的logit,然后使用带有温度的softmax重新定标logits以获得位置ysy_sys​处的ground-truth的类高斯分布q(y∣y∧s)q(y|\overset{\wedge}{y}_s)q(y∣y∧​s​):
      490a83eb-4c2d-4458-9967-2aa5ad4846d4-image.png
      在构造分布q(y∣ys)q(y|y_s)q(y∣ys​)和模型预测ps(y∣c)p_s(y|c)ps​(y∣c)时,增加了KL散度,可以指导模型遵循部分信用的类高斯分布:
      4bb6cf09-d7cf-41b2-ac4c-32fe5e6926d3-image.png

      3 Experiments

      3.1 Result on NQ

      ​ 在NQ上训练Vault,分别将段落和span答案预测为NQ的**LA(长答案)和SA(短答案)**并与RoBERTADMRoBERTA_{DM}RoBERTADM​(SOTA文档模型(DM)的RoBERTa的变体)进行比较。尽管在本文的表中包括了Longformer DM基线是公平的,但是资源有限,因此作者没有这么做,而是采用与其相似的RoBERTa。通过去除GPO和PAPR进行消融试验,展示对vault的影响。

      blCyyn.png

      不带GPO和PAPR的基本LM(实验中为Longform)是以先预测SA然后选择封闭LA的方式实现。可以观察到,Vault和ROBERTADM提供了类似的F1性能。然而,当涉及到解码时间时,可以发现Vault解码速度比ROBERTADM快16倍以上。此外,还在消融实验中看到,两种增强使F1指标都增加了多个点,但却牺牲了一些解码时间。特别注意到,Longformer的F1表现并不能与Vault相媲美。结论是,vault提供了F1和解码时间的最佳平衡,因为它有效地与F1捆绑在一起(使用ROBERTADM),并且解码速度仅比最快的型号慢约20分钟。

      3.2 Domain Adaptation: Results on TechQA

      ​ 由于vault已被证明对NQ有效,作者在一个新的域TechQA上对其进行评估。本文将其与用相同的超参数训练的Roberta基本模型进行比较;除了使用11个epochs而不是20个epochs。另外选择了Base而不是Large(如TechQA基线所使用的那样)以提供公平的比较,因为在使用Vault进行实验时使用的是base PLM。同样,使用Roberta而不是Bert,因为它更接近LongFormer。在NQ上已经建立了vault的运行时有效性之后,在这里将重点放在F1指标上,包括“has answer”(HA)F1。本文将HA F1视为主要的度量,因为在这项工作中正在探索段落答案提取,并且(如前所述)TechQA中的答案比其他数据集要长得多。作者相信HA F1的改善,至少部分来自GPO。

      blCDzj.png

      可以看到vault模型提供了0.7 F1和8.5HA F1的改进(表示有答案);因此显示了作者方法的有效性。特别是,这种将段落结构归类的方法在存在非空答案的情况下提供了很大的性能提升(HA F1)。

      启示

      • 对于长文本可以采用PAPR的方式先计算出段落答案
      • 高斯先验优化可以软化ground-truth周围的上下文信息

      为什么快了16倍?

      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • 两个都比一个好:表序编码器的联合实体和关系提取

      btRPSK.png

      Two are Better than One: Joint Entity and Relation Extraction with Table-Sequence Encoders

      两个都比一个好:表序编码器的联合实体和关系提取

      code

      Abstract

      ​ 对于联合实体关系抽取,许多研究者将联合任务归结为一个填表问题,他们主要专注于学习单个编码器来捕获同一空间内的两个任务所需的信息(一个表抽取实体和关系)。作者认为设计两个不同的编码器捕获这两种不同类型的信息更好,因此本文提出了一种新颖的Table-Sequence编码器,其中两个不同的编码器(Table和序列编码器)被设计成在表示学习过程中相互帮助,本文并证明了两个编码器比一个更有优势。仍然使用表格结构,引入BERT中的attention权重进行表格中元素表示的学习。

      1 Introduction

      ​ 在几种联合抽取的方法中,将NER和RE转化为一个填表问题,对于形成的2D表,表中每个条目捕获句子内两个独立单词的交互,NER任务再被转化为一个序列标签问题,即对角线的条目是标签,而RE被认为是标签表内其他条目的问题;这种方法将NER和RE整合到一个表格中,实现了两个任务之间的潜在有用的交互。

      bt2LyF.png

      ​ 作者认为一张表解决两个问题可能会受到特征混淆的影响(一个任务提取的特征可能与另一个任务的特征一致或冲突,从而导致学习模型变得混乱),其次这种结构没有充分利用到表结构,因为这种方法仍然是将表结构转化为序列,然后使用序列标签方法填表,因此在转化期间2D表中的关键结构信息可能会丢失(图1左下角共享相同的标签)。

      ​ 针对以上问题,本文提出一种新的方法解决上述限制。使用两种不同的结构(序列表示和表表示)单独表示NER和RE;

      bt2jeJ.png

      ​ 通过这种结构不仅可以将这两个单独的表示用于捕获特定于任务的信息,而且作者设计了一种机制使两个子任务进行交互,以便利用NER和RE任务背后的内在联系。

      2 Model

      2.1 Problem Formulation

      NER看做序列标注问题,其中gold entity tags yNERy^{NER}yNER是BIO;RE看做表格填充任务

      ​ 形式上,给定输入句子x=[xi]1≤i≤Nx=[x_ i]_ {1\le i\le N}x=[xi​]1≤i≤N​,维护标签表:yRE=[yi,jRE]i≤i,j≤Ny^{RE}=[y^{RE}_ {i,j}]_ {i\le i,j \le N}yRE=[yi,jRE​]i≤i,j≤N​

      假设从mention xib,…,xiex_{i^b},…,x_{i^e}xib​,…,xie​到mention xjb,…,xjex_{j^b},…,x_{j^e}xjb​,…,xje​ 有关系r,则:
      0fcf6e96-dcc2-4b8b-87bf-60398be669d6-image.png
      其中i∈[ib,ie]∧j∈[jb,je]i\in [i^b,i^e]\wedge j\in[j^b,j^e]i∈[ib,ie]∧j∈[jb,je],⊥\bot⊥表示没有关系的单词对

      表3显示了两个编码器在每一层的详细信息,以及如何交互

      bt2xoR.png

      在每一层中,表编码器使用序列表示法来构造表表示法,然后序列编码器使用表表示法来对序列表示法进行上下文处理。

      2.2 Text Embedder

      ​ 对于包含N个单词x=[xi]1≤i≤Nx=[x_i]_{1\le i \le N}x=[xi​]1≤i≤N​的句子,其中单词嵌入为:xw∈RN×d1x^w\in \mathbb{R}^{N\times d_1}xw∈RN×d1​,LSTM计算的字符嵌入为:xc∈RN×d2x^c\in \mathbb{R}^{N\times d_2}xc∈RN×d2​,由BERT产生的上下文单词嵌入:xl∈RN×d3x^l\in \mathbb{R}^{N\times d_3}xl∈RN×d3​

      将三个特征向量进行拼接,并使用linear来形成初始序列表示S0∈RN×HS_0\in \mathbb{R}^{N\times H}S0​∈RN×H:
      33deba81-4256-4d3c-a54e-e62bbd6ac096-image.png

      2.3 Table Encoder

      ​ 图3左侧所示的表格编码器是用于学习表格表示(N×N个向量表格)的神经网络,其中第i行和第j列的向量对应于输入句子的第i和第j个单词。本文首先通过将序列表示的两个向量进行连接,然后是通过一个全连接层来构建一个非上下文表,以将隐藏大小减半。

      ​ 形式化上,对于第l层(表表示和序列表示交互的第l层),有Xl∈RN×N×HX_l\in \mathbb{R}^{N\times N\times H}Xl​∈RN×N×H:
      519da941-863d-4a9e-8d1e-c71a784dfc19-image.png
      ​ 接下来使用GRU的多维递归神经网络来对XlX_lXl​进行上下文处理,迭代计算每个单元格的隐藏状态以形成上下文化的表格表示TlT_lTl​,其中:
      3cfce08d-1baa-4c2e-b70e-95b1627f5f40-image.png
      ​ 多维GRU通常沿着层、行和列的维度利用上下文,也就是说不仅考虑到了相邻行和列的单元格,还考虑上一层的单元格。对于表格法来说,句子长度为N,那么时间复杂度就变成了ON×NO^{N\times N}ON×N,对角线条目的计算可以同时计算(将对角线条目定义为位置(i,j)处的偏移),然后通过并行化进行优化,将复杂度降低到ONO^NON。

      bt2vw9.png

      ​ 由图4直观上可以看到,让网络能够全方位的方位周围环境能提高性能,因此需要4个RNN来完成这个工作,从4个方向上访问上下文以对2D表进行建模,然而作者经验上发现只考虑情况a和c的设置效果与四种情况全部考虑所得到的的性能差不多,因此为了减少计算量,作者使用了两个方向,最终的表格表示是两个RNN的隐藏状态的串联:
      75986f44-9a7e-4eeb-9881-b39b49ec0f55-image.png

      2.4 Sequence Encoder

      ​ 通过Sequence Encoder学习序列表示(a sequence of vectors),其中第i个向量表示输入句子中的第i个单词,该结构类似于transformer结构,见图3的右半部分,本文用表格引导注意力(Table-Guided Attention)取代按比例缩放的点积注意力。

      bt2xoR.png

      通用注意力的计算过程如下:

      bt2OL4.png

      对于每个query,输出是这些值的加权和,其中分配给每个value的权重由query与所有key的相关性确定:
      c12a7cf4-fb0a-4fbc-bfd6-7d0b9e29f101-image.png
      其中U是可学习向量,g是将每个query-key pair 映射到向量的函数,图5中f的输出即是query-key pair构造的注意力权重。

      本文的表格引导注意力中,输入即是上一层的序列表示Sl−1S_{l-1}Sl−1​,表格引导注意力是自注意力,注意力的得分函数f为:
      e644612c-2454-4022-b3d4-7a3e31db1a89-image.png
      ​ 表格引导注意力的优点:1.不比计算g函数,因为TlT_lTl​已经从表编码器中获得(2)TlT_lTl​是沿行、列和层维度的上下文得到的表征,分别对应于query、key、value,这样的上下文信息使网络能够更好的捕获更难的word-word依存关系(3)并且允许表编码器参与学习过程,从而形成两个编码器之间的双向交互。序列编码器的其余部件类似于transformer,对于lll层,在自注意力之后使用前馈神经网络FFNN,并使用残差连接和层标准化得到以获取输出序列表示:
      b6f28257-7654-4b83-a723-a6aedbfb32a3-image.png

      2.5 Exploit Pre-trained Attention Weights

      ​ 图2、3中的虚线来自预训练模型BERT的注意力权重形式的信息,本文将所有头部和所有层的注意力进行叠加,形成Tl∈RN×N×(Ll×Al)T^{l}\in \mathbb{R}^{N\times N \times (L^l\times A^l)}Tl∈RN×N×(Ll×Al),其中LlL^lLl是transformer的层数,AlA^lAl是每层中head的数量,本文利用TlT^lTl在表编码器中形成MD-RNN的输入,section2.3中的公式1被替换成:
      6442ad21-3185-41fb-89b7-d0f45079f8c4-image.png

      2.6 Traning and Evaluation

      ​ 本文使用SLS_LSL​和TLT_LTL​来预测实体标签和关系标签的概率分布:
      599feab9-dab3-43e1-8ce7-335cd097bacb-image.png
      其中YNER、YREY^{NER}、Y^{RE}YNER、YRE是预测标签的随机变量,PθP_\thetaPθ​是概率估计函数,θ\thetaθ是模型参数

      采用交叉熵计算损失:
      b6fef1b6-83f5-4b5c-a51b-9b1857ed9fb4-image.png

      其中,yNERy^{NER}yNER和yREy^{RE}yRE是gold tag,目标函数是:LNER+LREL_{NER}+L_{RE}LNER​+LRE​

      在评估过程中,关系的预测依赖于实体的预测,因此首先预测实测,然后查找关系概率表Pθ(YRE)P_\theta(Y^{RE})Pθ​(YRE),查看预测的实体之间是否存在有效的关系,具体地说,通过选择概率最高的类别来预测每个单词的实体tag:
      8cb2736d-8f36-447f-ab1e-4308b79b82f4-image.png
      整个tag序列可以被转换成具有其边界和类型的实体。对于给定的两个实体span :(ib,ie)和(jb,je)(i_b,i_e)和(j_b,j_e)(ib​,ie​)和(jb​,je​),其关系由下式给出:
      7ba8e242-7af5-4e39-8f9b-0da843e09ea3-image.png
      其中⊥\bot⊥表示没有无关系

      3 Experiments

      3.1 Model Step

      根据ACE05验证集调参,其他数据集使用相同的设置,Glove用于初始化词嵌入,BERT使用默认的参数,堆叠了三层具有独立参数的编码层(图2,每层中包含GRU单元),对于表编码器,使用两个独立的MD-RNN;对于序列编码器,使用8-head注意力表示,结果如下:

      btRpJx.png

      4 启示

      1. 论文是写的真的好,表达能力很强
      2. transformer框架的部分应用可以在其他地方使用
      3. 还是老生常谈的需要占用很多显存
      4. 表表示和序列表示的交互策略可以尝试修改成其他解码方式并实现交互
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • PRGC:基于潜在关系和全局对应的联合关系三元组抽取

      b3vf3j.png

      PRGC: Potential Relation and Global Correspondence Based Joint Relational Triple Extraction

      PRGC:基于潜在关系和全局对应的联合关系三元组抽取

      Abstract

      ​ 本文讲关系抽取任务分解为关系判断、实体提取和subject-object对齐三个子任务,提出了一种基于潜在关系和全局对应的联合关系三元组抽取框架(PRGC)。具体而言,首先设计一个预测潜在关系的组件,将后续实体提取限制在预测的关系子集上,而不是所有的关系;然后用特定于关系的序列标记组件处理subject-object之间的重叠问题;最后设计一个全局对应组件来以较低的复杂度将主客体对齐成三元组。在两个公共数据集上达到了新的SOTA。

      1 Introduction

      ​ 关系抽取是从非结构化文本中识别(subject,relation,object)三元组。本文将其分解为三个子任务:1.关系判断:识别句子中的关系;2.实体提取:识别句子中的subject和object;3.subject-object对齐:将subject-object对齐成一个三元组

      ​ 对于关系判断:本文==通过Potential Relation Prediction\mathcal{Potential\ Relation\ Prediction }Potential Relation Prediction组件来预测潜在关系==,而不是保留所有的冗余关系,这降低了计算复杂度,取得了更好的性能,特别是在实体提取方面。在实体提取方面:本文使用了一个更健壮的 Relation Specific Sequence Tag\mathcal{Relation\ Specific\ Sequence\ Tag}Relation Specific Sequence Tag组件(简称Rel-Spec Sequence Tag)来==分别提取subject和object==,以自然地处理subject和object之间的重叠。对于subject-object对齐:本文设计了==与一个关系无关的全局对应矩阵==来判断特定的subject-object对在三元组中是否有效。

      ​ 在给定句子的情况下,PRGC首先预测潜在关系的子集和包含所有subject-object之间对应分数的全局矩阵**;然后进行序列标注,并行地提取每个潜在关系的主客体**;最后枚举所有预测的实体对,然后通过全局对应矩阵进行剪枝。

      2 Method

      2.1 Problem Definition

      ​ 输入是具有n个token的句子S=x1,x2,…,xnS={x_1,x_2,…,x_n}S=x1​,x2​,…,xn​,期望的输出是关系三元组T(S)=(s,r,o)∣s,o∈E,r∈RT(S)={(s,r,o)|s,o \in E, r\in R}T(S)=(s,r,o)∣s,o∈E,r∈R,其中E、RE、RE、R分别表示实体集和关系集。

      2.1.1 Relation Judgement

      ​ 对于给定句子SSS,该子任务是预测它句子SSS包含的潜在关系,输出为:Yr(s)=r1,r2,…,rm∣ri∈RY_r(s)={r_1,r_2,…,r_m|r_i\in R}Yr​(s)=r1​,r2​,…,rm​∣ri​∈R,其中m为潜在关系子集的大小。

      2.1.2 Entity Extraction

      ​ 对于给定句子SSS和预测的潜在关系rir_iri​,该子任务是使用BIO标记方案识别每个token的tag,其中tjt_jtj​表示tag。输出为:Ye(S,ri∣ri∈R)=t1,t2,…,tnY_e(S,r_i|r_i\in R)={t_1,t_2,…,t_n}Ye​(S,ri​∣ri​∈R)=t1​,t2​,…,tn​。

      2.1.3 Subject-object Alignment

      ​ 对于给定句子SSS,该子任务预测主语和宾语的起始tokens之间的对应分数。即真正的三元组中的subject-object对的得分较高。输出为:Ys(S)=M∈Rn×nY_s(S)=M \in R^{n\times n}Ys​(S)=M∈Rn×n,其中MMM表示全局对应矩阵。

      2.2 PRGC Encoder

      ​ 通过BERT对句子S进行编码。encoder的输出:Yenc(S)=h1,h2,…,hnY_{enc}(S)={h_1,h_2,…,h_n}Yenc​(S)=h1​,h2​,…,hn​,其中n表示tokens数。

      2.3 PRGC Decoder

      2.3.1 Potential Relation Prediction

      b3voD0.png

      ​ 图中RpotR^{pot}Rpot表示潜在关系

      给定句子SSS,首先预测句子中可能存在的潜在关系的子集,然后只需要提取用到这些潜在关系的实体。给定n个tokens的句子嵌入h∈Rn×dh\in \mathbb{R}^{n\times d}h∈Rn×d,该潜在关系预测的每个元素为:
      429d8117-cbc1-4c1c-bab3-594757b40595-image.png

      其中AvgpollAvgpollAvgpoll是平均池化操作,Wr∈Rd×1\mathrm{W}_r\in \mathbb{R}^{d\times 1}Wr​∈Rd×1是可训练权重,σ\sigmaσ是sigmod函数。

      本文将其潜在关系预测建模为一个多标签二进制分类任务,如果概率超过某个阈值λ1\lambda _1λ1​,则为对应关系分配标签1,否则将对应的关系标签置为0;接下来只需要==将特定于关系的序列标签==应用于预测关系,而不要预测全部关系。

      2.3.2 Relation-Specific Sequence Tagging

      ​ 如图1所示,通过2.3.1节中的组件获得了描述的潜在关系的几个特定于关系的句子表示。然后,模型执行两个序列标注操作来分别提取主体和客体。

      ​ 作者之所以将主语和宾语分开提取,是为了处理一种特殊的重叠模式,即主语宾语重叠(SOO)。作者放弃了传统的LSTM-CRF网络,而采用了简单的全连接神经网络进行实体关系识别。该组件对每个token的具体操作如下:
      f1b400db-50e9-4a50-a15e-0997294e81cf-image.png
      其中uj∈Rd×1u_j\in \mathbb{R}^{d\times 1}uj​∈Rd×1是训练嵌入矩阵U∈Rd×nrU\in \mathbb{R}^{d\times n_r}U∈Rd×nr​中第j个关系表示,nrn_rnr​是全部关系集合的大小,hi∈Rd×1h_i\in \mathbb{R}^{d\times 1}hi​∈Rd×1是第i个token的编码表示,Wsub,Wobj∈Rd×3W_{sub},W_{obj}\in \mathbb{R}^{d\times 3}Wsub​,Wobj​∈Rd×3是训练权重

      2.3.3 Global Correspondence

      ​ 在序列标注之后,分别获得关于句子关系的所有可能的主语和宾语,然后使用全局对应矩阵来确定正确的主语和宾语对。应该注意的是,全局对应矩阵可以与潜在关系预测同时学习,因为它独立于关系。

      ​ 具体过程如下:首先枚举所有可能的subject-object对;然后在全局矩阵中检查每对subject-object对的对应分数,如果该值超过某个阈值λ2\lambda _2λ2​,则保留该分数,否则将其过滤掉。图1中的绿色矩阵M∈Rn×nM\in \mathbb{R}^{n\times n}M∈Rn×n即是全局对应矩阵,由n个token组成的句子。矩阵中的每个元素都与subject-object对的起始位置有1关,位置代表主客体对的置信度,值越高属于三元组的置信度就越高,矩阵中每个元素的值如下所示:
      2b11edbd-a5b5-459e-ad24-db8a1638def1-image.png
      其中hisub,hjobj∈Rd×1h_i^{sub},h_j^{obj}\in \mathbb{R}^{d\times 1}hisub​,hjobj​∈Rd×1是形成潜在subject-object对的输入语句中的第i个token和第j个token的编码表示,Wg∈R2d×1W_g\in \mathbb{R}^{2d\times 1}Wg​∈R2d×1是可训练权重,σ\sigmaσ是sigmod函数。

      2.4 Training Strategy

      373e27a8-60b9-4fc6-b63b-4199d9d29604-image.png

      其中nrn_rnr​表示关系集的大小,nrpotn_r^{pot}nrpot​表示潜在关系子集的大小,总的损失是:
      8957bc9f-0c5e-4952-8692-fb14eeb0ae3b-image.png

      3 Experiments

      b3vIuq.png

      本文通过了PRGCRandomPRGC_{Random}PRGCRandom​来验证的PRGC解码器的有效性,其中编码器BERT的所有参数都是随机初始化的。PRGCRandomPRGC_{Random}PRGCRandom​的性能表明,即使不利用预先训练的BERT语言模型,本文的解码器框架仍然比其他解码器框架更具竞争力和健壮性。

      模型的具体参数:使用BERT-base作为编码器、句子长度设为100,V100GPU,100个epochs

      4 启示

      1. 先抽取潜在关系再抽取与潜在关系有关的实体最后进行subject-object的对齐会提高模型的解码速度和算力资源。
      2. 潜在关系预测阈值λ1\lambda _1λ1​越高,模型的性能越好
      3. 三个损失函数的调参是一个工作量问题。
      4. 如果句子长度太长最后subject-object的对齐工作消耗的空间资源会很大。
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • DeFormer:分解预先训练好的Transformers以提高问题回答速度

      bncmGV.png

      DeFormer: Decomposing Pre-trained Transformers for Faster Question Answering

      DeFormer:分解预先训练好的Transformers以提高问题回答速度

      TF code

      Abstract

      ​ BERTQA中应用了大量的自注意力,导致了模型训练的速度很慢并且占用大量内存。本文提出一个Deformer模型用于分解transformer,具体来说用较低层的question-wide和passage-wide的self-attention(分别计算self-attention)替代question-passage的全局注意力,由于Deformer与原始模型很相似,因此用原始的transformer的预训练权重初始化Deformer。速度提升了4倍,通过简单的知识蒸馏,准确度仅下降1%。

      1 Introduction

      ​ 基于Transformer的模型中,大部分的计算开销都是来自每一层的自注意力计算。在MRC式的QA中,算力主要是消耗在问题和passage的自注意力计算,虽然自注意力有助于模型创建高效的问题上下文表示,但是构建context表示需要更多的时间,因为context的长度总是比question长的多,如果context可以独立于问题进行处理,那么最难计算的context表示就减少了一部分和question的attention计算,可以加速QA过程。有研究表明:transformer较低层编码倾向于关注一些局部特征,如词形、语法等;较高层才逐渐编码与下游任务相关的全局语义信息(远距离信息)。也就是说:在较低层passage编码对question的依赖不高,因此本文采用的在较低层对question和context分别编码,在较高层联合处理(形成question-context联合表征进行交互编码),如图1所示:

      bncV5q.png

      ​ 假设n层模型中的前K个较低层独立的处理question和context,Deformer将两个第K层的表示作为输入馈送到第K+1层,这种方法很显然能减少运算量和内存。Deformer的上层应该生成与transformer相应层相同类型信息的表示,因此本文增加了两个蒸馏式损失,目的是用于最小化分解模型和原始模型之间的高层表征和分类层logits。

      ​ 在三个QA数据集上进行评估模型,分别基于BERT和XLNet。速度提升了2.7 to 3.4倍,内存减少了65.8% to 72.9%,性能减少了0.6 to 1.8。BERT-large比BERT-base速度更快,精确度更高。

      2 The Approach

      ​ 基于transformer模型的MRC框架是计算question-context上的self-attention。这种方式产生了输入对的高效表示,因为从文本中提取什么信息通常取决于问题。想要降低复杂性,可以牺牲一些代表性能力来换取脱机处理文本的能力(脱机文件:一般指保存的网页,即不联网也能浏览网页的内容)。本文也测量了文本表示在与不同问题配对时的变化(计算了上下文与不同问题配对时的段落表征方差),得出结论:==在较低层中文本表示的变化不像在较高层中的那么大,这表明在较低层中忽略question-context的注意力计算影响不会太大==。先前的研究也表明:较低层倾向于对局部现象(词性、句法类别等)建模,较高层倾向于对依赖任务(实体共指)的更多语义现象进行建模。

      2.1 DeFormer

      定义两段文本表示Ta、TbT_a、T_bTa​、Tb​的配对任务的transformer计算。

      ​ TaTaTa嵌入的表示是:A=[a1;a2;…;aq]\mathrm{A}=[a_1;a_2;…;a_q]A=[a1​;a2​;…;aq​]

      ​ TbT_bTb​嵌入的表示是:B=[b1;b2;…;bp]\mathrm{B}=[b_1;b_2;…;b_p]B=[b1​;b2​;…;bp​]

      完整的输入序列X表示为:X=[A;B]\mathrm{X}=[\mathrm{A};\mathrm{B}]X=[A;B]

      Transformer如果有n层,第i层表示为LiL_iLi​,这些层按顺序转换表示为:

      将第i层到第j层的层叠表示为:Li:jL_{i:j}Li:j​

      完整transformer、An\mathrm{A}^nAn、Bn\mathrm{B}^nBn的输出为:

      模型图3所示:

      bncMMF.png

      简单的去除Ta\mathrm{T}_aTa​和Tb\mathrm{T}_bTb​表示之间的交叉交互,分解较低层计算(到第K层)。分解后的输出为:

      35c31608-6798-45a8-b7b4-2f164b368b35-image.png
      基于transformer的问答系统通过一组自我关注层将输入问题和上下文一起处理。因此,将此分解应用于Transformer for QA允许我们独立处理问题和上下文文本,这反过来又允许我们离线计算较低层的上下文文本表示。

      在较低层的时间复杂度从O((p+q)2)O((p+q)^2)O((p+q)2)到O(q2+c)O(q^2+c)O(q2+c),其中ccc表示加载缓存表示的成本。

      2.2 Auxiliary Supervision for DeFormer辅助监督

      ​ 使用transformer预训练的权重参数训练DeFormer。由于section2.1在较低层将question和passage进行独立编码,因此会丢失一些信息,本文通过对上层微调弥补这一缺点,并且还添加了辅助损失,使DeFormer的预测及其上层表示==更接近于==transformer的预测和相应的层表示。

      2.2.1 Knowledge Distillation Loss知识蒸馏损失

      将分解transformer的预测分布PAP_APA​和transformer预测分布PBP_BPB​之间的KL散度最小化:
      9e8848f9-3796-499f-a9d3-ba9a1760a0e5-image.png

      2.2.2 Layerwise Representation Similarity Loss

      通过最小化分解transformer上层的token表示与transformer之间的欧几里得距离使得DeFormer的上层表示更接近于transformer的token表示
      fc17aa55-34a1-4961-8c7d-b0c7d0f520ca-image.png
      vij\mathrm{v}_i^jvij​是transformer中第i层的第j个token的表示 uij\mathrm{u}_i^juij​是Deformer中对应的表示

      2.2.3 最终损失

      本文将知识蒸馏损失Lkd\mathcal{L_{kd}}Lkd​和分层表示相似性损失Llrs\mathcal{L_{lrs}}Llrs​与特定任务的监督损失Lts\mathcal{L_{ts}}Lts​相加,通过超参数来调整它们的相对重要性,超参数使用贝叶斯优化来调整的,这种方法减少了寻找最优化参数组合所需要的步骤数。
      29710957-eeb6-4a78-bbb8-ccbb2805d087-image.png

      3 Experiment

      3.1 Datasets

      SQuAD:众包工作者在维基百科上生成的超过10万个问答对

      RACE:从英语考试中收集到的阅读理解数据集,旨在评估初中生的阅读和理解能力。超过28K个段落和100K个问题

      BooIQ:15942个由是/否的问题组成的,这些问题出现在无提示、不受约束的环境中。

      MNLI:是一个由43.3万个句子对组成的众包语料库,用文本蕴涵信息进行标注

      QQP:由超过40W个来自Quora的潜在重复问题对组成

      3.2 Implementation Details

      作者基于原始的Bert和XLNet代码库实现了所有模型(TF1.15)。启用了bfloat16格式的TPU v3-8 node(8核,128 GB内存)上执行所有实验。

      对于DeFormer-Bert和DeFormer-XLNet,通过离线计算其中一个输入段的表示并缓存它。对于问答,缓存段落;对于自然语言推理,缓存前提;对于问题相似度,缓存第一个问题

      3.3 Result

      表1显示了使用9个下层和3个上层时BERT-BASE和DeFormer-BERT-BASE的性能、推理速度和内存需求的主要比较结果

      bncn2T.png

      在所有数据集中观察到显著的加速比和显著的内存减少,同时保持了原始模型的大部分有效性,XLNet在同一表格中的结果证明了不同预先训练的transformer架构的分解有效性。

      表2显示,在采用成对输入序列的QQP和MNLI数据集上,分解带来了2倍的推理加速和超过一半的内存减少。

      bnceP0.png

      3.3.1 Small Distilled or Large Decomposed?小蒸馏还是大蒸馏?

      表3比较了BERT-BASE、BERT-LARGE和DeFormer-BERTLarge的性能、速度和内存。

      bncEan.png

      DeFormer-Bert-Large比较小的Bert-Base模型快1.6倍。事实证明,分解较大的模型也比使用较小的基础模型(+2.3点)更有效。这表明,通过分解,大型transformer可以比尺寸为其一半的小型transformer运行得更快,同时也更精确。

      3.4 Divergence of DeFormer and original BERT representations

      最初的Bert和DeFormer-Bert之间的主要区别是在较低的层没有交叉注意。本文分析了这两个模型在所有层的表示之间的差异。

      为此本文从SQuAD开发人员数据集中==随机选择了100个段落==,并<u>随机选择了与每个段落相关的数据集中已经存在的5个不同的问题</u>。对于每一篇文章,我们使用精调的原始BERT-BASE模型和DeFormerBERT-BERT模型对所有5个question-passage对序列进行编码,并==计算它们在每一层的向量表示的距离==。

      图5显示了问题和文章在不同层次上的平均距离。

      bucsNn.png

      两个模型的pasage和question的低层表征保持相似,但上层表征有显著差异,缺乏交叉注意对低层的影响小于高层得到了证明。此外,使用上层的辅助监督可以强制DeFormer生成更接近原始模型的表示,从而达到预期效果。对于问题表征,这种影响不那么明显。

      4 启示

      1. 损失函数超参数的调整能够减少算力开销
      2. 可以通过RNN进行替代,先分开编码再联合编码。先用RNN分开编码,再用BERT学习联合特征。
      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • UNIRE:一种用于实体关系抽取的统一标签空间

      UNIRE: A Unified Label Space for Entity Relation Extraction

      UNIRE:一种用于实体关系抽取的统一标签空间

      https://github.com/Receiling/UniRE

      Abstract

      (Zhong and Chen,ACL2020 Two are better than one: Joint entity and relation extraction with table sequence encoders) 使用pipeline方法为实体检测和关系分类设置了两个独立的标签空间,并取得了SOTA。由于pipeline方法不能共享实体抽取和关系抽取的信息,因此作者为了促进两个任务的交互提出了一种可以共享标签空间的方法。

      作者采用表填充的方法实现,具体来说:输入一张s×ss\times ss×s大小的表,这张表包含一个句子中所有的单词对,实体和关系由表格中的正方形和矩形表示(实体:实体内部所有字符都是相同的实体类型标签例如PER;关系:有关系的两个实体所有的字符之间都有相同的关系标签),实体都在对角线,关系在非对角线。作者实现了SOTA并且只用了一半的参数,就达到了与最好的提取器相当的准确率,而且速度更快

      1645843441431

      Figure 1:联合实体关系提取表的示例。每个单元格对应一个单词对。实体是对角线上的正方形,关系是对角线外的矩形。请注意,PER-SOC是无向(对称)关系类型,而PHY和ORG-AFF是有向(非对称)关系类型。

      该表准确地表示重叠的关系,例如:PER实体DavidPerkins\mathrm{David Perkins}DavidPerkins参与两个关系(“DavidPerkins”,“Wife”,PER−SOC)\mathrm{(“David Perkins”,“Wife”,PER-SOC)}(“DavidPerkins”,“Wife”,PER−SOC)and(“DavidPerkins”,“California”,PHYS)\mathrm{(“David Perkins”,“California”,PHYS)}(“DavidPerkins”,“California”,PHYS)

      对于每个units,一个相同的双仿射模型预测其标签。联合解码器被设置为寻找最佳正方形和矩形

      1 Introduction

      人们认为联合模型可能会更好,因为它们可以减轻子模型之间的误差传播,具有更紧凑的参数集,并且统一地编码关于两个任务的先验知识。本文将已有的单独的标签空间转化为统一的标签空间,存在的难点:两个子任务通常被表述不同的学习问题(例如:作为序列标签的实体检测,作为多类分类的关系分类),并且它们的标签被放置在不同的事物上(例如,词与词对)。

      先前的一次尝试( Joint Extraction of Entities and Relations Based on a Novel Tagging Scheme - ACL Anthology )是用一个序列标记模型处理这两个子任务。 ==设计了一个复合标签集来同时对实体和关系进行编码。然而,该模型的表现力被牺牲了:它既不能检测重叠关系(即,参与多个关系的实体),也不能检测孤立的实体(即,没有出现在任何关系中的实体)。==

      作者定义一个新的统一标号空间的关键思想:==将实体检测看作关系分类的特例==。输入空间是一个二维表,每个条目对应于句子中的一个词对(图1)。联合模型从统一的标签空间(实体类型集和关系类型集的并集)为每个单元指定标签。在图形上,实体是对角线上的正方形,关系是对角线外的矩形。该公式保留了关于现有entity−ralationentity-ralationentity−ralation提取场景(例如,重叠关系、有向关系、无向关系)的完整模型表达能力。

      基于表格形式,联合实体关系提取器执行两个操作:填充和解码。首先,填表是预测每个词对的标签,类似于依存句法分析中的弧形预测任务。采用双仿射注意机制(Dozat和Manning,2016)来学习词对之间的互动。本文还对表施加了两个结构约束。然后,给出带有标签日志的表填充,本文设计了一种近似联合解码算法来输出最终提取的实体和关系。基本上,它高效地在表中找到分割点来识别正方形和矩形(这也与现有的表填充模型不同,现有的表填充模型仍然应用某些顺序解码并递增地填充表)。

      在三个基准测试(ACE04,ACE05,SciERC)上的实验结果表明,与目前最先进的提取器(zhong和Chen,2020)相比,该联合方法取得了与之相当的性能:在ACE04和Science ERC上性能更好,在ACE05.1上更具竞争力;同时,我们的新联合模型在解码速度上更快(比确切的流水线实现快10倍,与近似流水线相当,但性能较低)。它还有一个更紧凑的参数集:与单独的编码器相比,共享编码器只使用一半的参数。

      2 Approach

      2.1 Task Definition

      给定一个句子s=x1,x2,…,x∣s∣s=x_1,x_2,…,x_{|s|}s=x1​,x2​,…,x∣s∣​(xix_ixi​是word),目的是提取一组实体ε\varepsilonε和一组关系R\mathcal{R}R。对于关系三元组(e1,e2,l)(e_1,e_2,l)(e1​,e2​,l),其中l∈Yrl \in \mathcal{Y_{r}}l∈Yr​是预定义的关系类型,ye\mathcal{y_e}ye​、yr\mathcal{y_r}yr​表示预定义的实体类型和关系类型的集合。

      对于句子sss维护一个表格T∣s∣×∣s∣T^{|s| \times |s|}T∣s∣×∣s∣,其中∣s∣|s|∣s∣表示句子长度。对于表TTT中的每个单元格cell(i,j)cell(i,j)cell(i,j),为其分配一个标签yi,j∈yy_{i,j} \in \mathcal{y}yi,j​∈y,其中ye∪yr∪⊥\mathcal{y_e} \cup \mathcal{y_r} \cup {\perp}ye​∪yr​∪⊥,(⊥\perp⊥表示没有关系)

      1. 对于每个实体e:对应的标签yi,j(xi∈e.span,xj∈e.span)y_{i,j}(x_i\in e.span,x_j\in e.span)yi,j​(xi​∈e.span,xj​∈e.span)应填写成e.typee.typee.type
      2. 对于每个关系r=(e1,e2,l)r=(e_1,e_2,l)r=(e1​,e2​,l):对应的标签yi,j(xi∈e1.span,xj∈e2.span)y_{i,j}(x_i\in e_1.span,x_j\in e_2.span)yi,j​(xi​∈e1​.span,xj​∈e2​.span)应填写成lll
      3. 对于其他的单元格填写⊥\perp ⊥

      本文将解码实体和关系转化为一个矩形查找问题,查找问题采用联合译码方法来解决。

      2.2 Biaffine Model

      通过BERT获取上下文表示hih_ihi​如下所示:

      为了捕获长范围依存关系,将句子扩展成固定的窗口大小WWW(本文设为200),为了更好的编码表TT T中单词的方向信息,本文采用了深度双仿射注意力机制,其操作如下:

      start和end的span如下所示:

      计算每个词span的得分:gi,j∈R∣y∣g_{i,j} \in \mathbb{R^{|y|}}gi,j​∈R∣y∣

      其中U1∈R∣y∣×d×dU_1\in \mathbb{R^{|y|{\times d\times d}}}U1​∈R∣y∣×d×d,U2∈R∣y∣×2dU_2\in \mathbb{R^{|y|{\times 2d}}}U2​∈R∣y∣×2d,b∈R∣y∣b\in \mathbb{R^{|y|}}b∈R∣y∣

      2.3 Table Filling

      将gi,jg_{i,j}gi,j​馈送到Softmax中预测相应标签,从标签空间y\mathcal{y} y上产生概率分布:

      实验中发现对gi,jg_{i,j}gi,j​利用dropout可以进一步提高性能,作者称为logit dropout\mathbb{logit\ dropout}logit dropout

      使用交叉熵最小化目标函数:

      其中yi,jy_{i,j}yi,j​是gold label​

      2.4 Constraints

      目标函数简化了训练过程,实际上还存在一些结构上的约束,实体和关系对应于表中的正方形和矩形,但是目标函数没有显示该约束,本文提出了两个直观的约束:对称和隐含。

      使用记号P∈R∣s∣×∣s∣×∣y∣\mathcal{P}\in \mathbb{R}^{|s|\times |s| \times |\mathcal{y}|}P∈R∣s∣×∣s∣×∣y∣表示句子sss中所有单词对的P(yi,j∣s)P(y_{i,j}|s)P(yi,j​∣s)堆叠

      **Symmetry(对称)**与实体对应的正方形必须在对角线上,对于对称关系如(e1,e2,l)(e_1,e_2,l)(e1​,e2​,l)和(e2,e1,l)(e_2,e_1,l)(e2​,e1​,l)是等价的,因此在表格上对称关系也是关于对角线对称的(Figure 1所示(“his”,“wife”,PER−SOC)\mathrm{(“his”,“wife”,PER-SOC)}(“his”,“wife”,PER−SOC) and \mathrm{}(“wife”,“his”,PER−SOC)\mathrm{(“wife”,“his”,PER-SOC)}(“wife”,“his”,PER−SOC)矩形关于对角线对称)。

      标签集y\mathcal{y}y分为对称标签集ysymy_ {sym}ysym​和非对称标签集yasymy_ {asym}yasym​

      对于矩阵P:,:,t\mathcal{P}_ {:,:,t}P:,:,t​应该关于每个标签t∈ysymt\in \mathcal{y_{sym}}t∈ysym​的对角线对称,损失为:

      1645802597092

      Implication蕴含、包含一个关系存在,那么一定存在两个实体,反之就是,没有两个对应的实体,那么一定不可能存在。从概率的角度看,关系的概率大于每个实体的概率。通过蕴含思想,本文对P\mathcal{P}P施加如下约束:对于对角线上的每个单词,其在实体类型空间 ye\mathcal{y_e}ye​ 上的最大可能性不得低于关系类型空间 yry_ryr​ 上同一行或同一列中的其他单词的最大可能性。

      蕴含损失表示为:

      其中[u]∗=max(u,0)[u]_*=max(u,0)[u]∗​=max(u,0)是hinge loss.

      总的损失:

      2.5 Decoding

      在测试阶段,给定句子s的概率张量P∈R∣s∣×∣s∣×∣y∣\mathcal{P}\in \mathbb{R}^{|s| \times |s|\times |y|}P∈R∣s∣×∣s∣×∣y∣,从中解码实体的正方形和关系的矩形。受到sun et al 2019的启发,本文提出了一个三步解码算法:1. 解码span(实体或实体间span)。2. 解码每个span的实体类型。3.解码实体对的关系类型。

      1. span decoding一个实体包含的词的行列是相同的,如果相邻的两行/列不同,说明在此处一定有实体边界

        1. 从行的角度出发将P∈R∣s∣×∣s∣×∣y∣\mathcal{P}\in \mathbb{R}^{|s| \times |s|\times |y|}P∈R∣s∣×∣s∣×∣y∣展平为Prow∈R∣s∣×(∣s∣⋅∣y∣)\mathcal{P}^{row}\in \mathbb{R}^{|s| \times (|s|\cdot |y|)}Prow∈R∣s∣×(∣s∣⋅∣y∣),然后计算行的欧几里得距离l2\mathcal{l_2}l2​
        2. 类似的,从列的角度出发根据Pcol∈R(∣s∣⋅∣y∣)×∣s∣\mathcal{P}^{col}\in \mathbb{R}^{(|s|\cdot |y|) \times |s|}Pcol∈R(∣s∣⋅∣y∣)×∣s∣,然后计算列的欧几里得距离
        3. 将两个距离的平均值作为最终距离
        4. 如果距离大于默认的阈值α\alphaα(α=1.4\alpha=1.4α=1.4)则此位置为分割位置(实体边界),span解码的时间复杂度变成了O(∣s∣)\mathcal{O(|s|)}O(∣s∣)
      2. Entity Type Decoding通过span encoding得出span(i,j)span(i,j)span(i,j),将得到的i,ji,ji,j生成正方形来解码实体类型t∧\overset{\wedge}{t}t∧:

        ​ 如果t∧∈ye\overset{\wedge}{t}\in y_et∧∈ye​则解码为一个实体,如果t∧=⊥\overset{\wedge}{t}=\bott∧=⊥则span(i,j)span(i,j)span(i,j)不是实体。

        ​ 可以理解为:span(i,j)span(i,j)span(i,j)在表格中是一个正方形,利用argmax(⋅)argmax(\cdot)argmax(⋅)取出正方形中所有分数的最大值作为当前span的分数。

      3. Relation Type Decoding在实体类型解码后,给定一个实体e1=span(i,j)e_1=span(i,j)e1​=span(i,j)和另一个实体e2=span(m,n)e_2=span(m,n)e2​=span(m,n),解码一个关系(e1,e2,l∧)(e_1,e_2,\overset{\wedge}{l})(e1​,e2​,l∧),如果l∧=⊥\overset{\wedge}{l}=\botl∧=⊥,表示没有关系。

        形式上表现为:

        ​ 如果l∧∈yr\overset{\wedge}{l}\in y_rl∧∈yr​,解码为一个关系(e1,e2,l∧)(e_1,e_2,\overset{\wedge}{l})(e1​,e2​,l∧),如果l∧=⊥\overset{\wedge}{l}=\botl∧=⊥,则e1,e2e_1,e_2e1​,e2​没有关系。

        ​ 可以理解为:将得到的span两两匹配,得到实体对之间的关系矩形,将矩形中最大的那个位置对应的关系标签作为最终的关系标签。

        3 Experiments

        句子长度设置为200,对于MLP层,将隐藏大小设置为d=150,并使用Gelu作为激活函数。使用了β1\beta _1β1​=0.9和β2\beta _2β2​=0.9的AdamW优化器。批大小为32,学习率为5e-5,权值衰减为1e-5,线性预热学习率调度器,预热率为0.2。用最多200个epochs(对于SciERC为300个纪元)训练模型,并采用提前停止策略。在Intel Xeon W-3175X CPU和NVIDIA Quadro RTX 8000 GPU上进行所有实验。

        1645843381139

      劝退型模型

      总的来说,UNIRE在ACE04和SciERC上取得了最好的性能,在ACE05上取得了可比的结果。与之前最好的联合模型(Wang and Lu,2020)相比,该模型在ACE04和ACE05上显著提高了实体和关系的性能,即实体的绝对F1分别为+0.9和+0.7,关系的绝对F1分别为+3.4和+1.7。

      对于最好的流水线模相比,该模型在ACE04和SciERC上取得了优异的性能,在ACE05上取得了相当的性能。与ACE04/ACE05相比,SciERC的规模要小得多,因此在SciERC上的实体性能大幅下降。由于(钟和陈,2020)是一种流水线方法,其关系绩效受到较差的实体绩效的严重影响。然而,我们的模型在这种情况下受到的影响较小,并且获得了更好的性能。此外,在ACE04上,即使实体结果较差,我们的模型也能获得较好的关系性能。

      3.1 Ablation Study

      ​ 1645843859771

      具体地说,本文实现了一种朴素的比较解码算法,即“硬解码”算法,它以“中间表”作为输入。“中间表”是双仿射模型输出的概率张量P的硬形式,即选择概率最高的类作为每个单元的标签。

      为了找到对角线上的实体正方形,它首先尝试判断最大的正方形(∣s∣×∣s∣|s|×|s|∣s∣×∣s∣)是否为实体。标准只是计算出现在正方形中的不同实体标签的数量,并选择出现频率最高的一个。如果最常用的标签是⊥,我们将正方形的大小缩小1,然后在两个(∣s∣−1)×(∣s∣−1)(|s|−1)×(|s|−1)(∣s∣−1)×(∣s∣−1)正方形上执行相同的工作,依此类推。为避免实体重叠,如果实体与标识的实体重叠,则将丢弃该实体。为了找到关系,每个实体对都用对应矩形中最频繁的关系标签进行标记。

      从消融研究中,我们得到了以下观察结果:

      ​ 1. 移除其中一个额外损失后,性能将随不同程度下降(第2-3行)。具体地说,对称性损失对SCERC有显著影响(实体和关系绩效分别下降1.1分和1.4分)。而去除蕴涵损失会明显损害ACE05(1.0分)的关系绩效。它表明,这两种损失所包含的结构信息对这项任务是有用的。

      2. 与“Default”相比,“w/o logit Dropout”和“w/o CrossStatement Context”的性能下降幅度更大(第4-5行)。logit dropout可以防止模型过度拟合,**而跨句上下文为这项任务提供了更多的上下文信息**,特别是对于像SciERC这样的小型数据集。
      3. “hard decoding”的性能最差(其关系性能几乎是“default”的一半)(第6行)。最主要的原因是“硬解码”将实体和关系分开解码。
      

      结果表明,该译码算法综合考虑了实体和关系,对译码具有重要意义

      3.2 Error Analysis

      我们进一步分析了用于关系提取的其余错误,并给出了五种错误的分布情况:

      ​ 跨度拆分错误(SSE)、实体未找到(ENF)、实体类型错误(ETE)、关系未找到(RNF)、和关系类型错误(RTE)

      1645845156851

      SSE所占的比例相对较小,这证明了我们的跨度解码方法的有效性。

      此外,无论是实体还是关系,“未发现错误”的比例都明显大于“类型错误”的比例。最主要的原因是填表存在类不平衡问题,即⊥的数量远远大于其他类。

      posted in 语音识别与语义处理领域
      155****7220
      155****7220
    • RE: 从零训练一个超越预训练的 NLP 模型

      @131-7225 中午好中午好

      posted in 语音识别与语义处理领域
      155****7220
      155****7220