(杂谈)世界上本没什么prompt,有的只是加权平均——关于NLP中embedding的一点思考
- 0. 写在前面
- 1. 问题的提出
- 2. 备受嫌弃的NSP,为什么效果不佳
- 2. 比句子更小的片段——Span Bert
- 3. 更加纯粹的表示方法——PURE
- 4. 风光无限的prompt,到底提示了什么
- 5. 重识transformer,也只是加权平均
- 6. 总结与感悟
0. 写在前面
一转眼,我练习NLP已经有两年半的时间,这篇文章是我近期对于NLP任务的一点宏观的思考。
我给文章起了这样一个标题,我承认是在博人眼球了,但是思来想去,还是概括不出一个恰当的标题。不管怎样,既然你看到了这篇文章,我还是希望请你试着读下去。作为一个入行没有多久的年轻工程师,我不敢保证我所讲的都是正确的,但是如果这篇总结性质的感悟,能够引起大家对于文本表征的些许思考,那我写下这些文字的目的也就达到了。
1. 问题的提出
如果有人问我,“你认为预训练模型是干什么的”,我可能会不加思考地随口说出,“获取编码”,或者“获取目标对象的表征”,因为不管是什么样的任务,都在获取表征的基础上,去设计具体的下游任务。
在token-level embedding的基础上,接一个Dense,可以做序列标注,可以做MLM;接一个pooler,再接一个Dense,可以做sentence-level的任务,可以做sequence classification,亦或是regression,可以做检索、召回;两个sequence拼一起,可以做sequence-pair类的任务,例如一直被吐槽的NSP;接一个decoder,可以做生成类任务。
当然上面所举的这些例子中,相互之间存在内容上的交叉,但不管是怎样的任务范式,似乎都绕不开一个话题——我们总是需要获取对目标对象的表征。
再把时间放的久一点,回顾技术的发展,似乎可以发现,整个机器学习的发展,好像一直都在围绕着“表征”进行的。
早在预训练模型兴起之前,甚至是深度学习兴起之前,TF-IDF其实是在获取“表征”,BM25是在获取“表征”,fasttext,word2vec,glove等一众方法亦是如此;再把目光跳出NLP领域,CNN对图像的编码,GCN对图的编码,也都是在做同样的事情,甚至是数据建模中,人工特征工程的构建,也是为了寻求一众更合理的特征表示方法。这并不是一个发现,而是天然的,应该出现的。
正是由于这种固有的特性,好多人,包括我自己在内,在提到BERT之类的模型时,总是给它加上一个后缀“编码器”,因为心里已经习惯了这样的思想,BERT在整个模型中,是用来做编码的。这种称呼很直观,这没什么问题。可是,为什么,为什么经过BERT编码之后的结果,可以用来代表这句话的特征,以及这个表征,表征了什么东西。
2. 备受嫌弃的NSP,为什么效果不佳
在我印象里,好像自从NSP被设计出以来,它都都备受嫌弃。作为一项预训练任务,NSP无疑是失败的,并且从名字开始就是失败的。
Next sentence prediction,当我一开始接触NLP的时候,看到这个名字,我以为它是一个生成式任务,目的是输入一句话,预测它下一句话的内容,结果呢,它是判断两句话的关系。
从效果来看,它也是失败的,以至于后来的预训练模型纷纷对这项任务进行修改,或者是干脆舍弃了这项任务。甚至某乎上出现了这样的问题:
- 为什么用bert计算出的CLS,用来计算文本相似度的效果很差?
其中,苏神的回答很有趣,大概意思是说,你为什么会对它抱有期待呢?
为什么呢?
提出问题的人,似乎没有深入的考虑文本表征的问题,习惯性地觉得,只要是得到了编码,就可以利用编码来计算相似度了,这个问题我也遇到过,例如在NER任务中,拿到两个实体的编码,就利用这两个实体的编码来计算两个实体的相似度。
稍微想想不难理解,CLS的编码,是通过NSP预训练得到的,你对它进行fine-tune了吗,没有。那为什么觉得它可以很好地适配另一个任务呢?讲道理,文本相似度计算应该是一个回归任务,至少应该是文本蕴含任务,NSP作为一个如此简单的二分类任务,怎么能指望它产生的结果能够适配到相似性计算呢?更不用说,它的样本产生的如此随意了(其实NSP训练出来的结果似乎更像是判断两句话是否属于同一主题)。
所以说,NSP是一项失败的预训练任务。但是,如果说其获得的CLS表征,是一种失败的表征,我认为就有点冤枉它了。在我看来,它是一个“很好的”表征,可以很好的用来描述两句话之间是否存在上下文的关系,但是也仅仅在这个场景下好。
举个不是特别恰当的例子,你教一个人做凶柿炒蛋,他学的很好,可是如果你忽然要求他去做一道松鼠桂鱼,那似乎是有点为难他了,或许他会先把火打开,然后倒油,然后呢?NSP的预训练也是类似的道理吧,不能说毫无用处,至少他学会打火倒油了呢。
可以说,NSP设计的出发点是好的,它是想增强模型对于sequence pair的表示能力,以扩展出更多的下游任务功能,只是任务设计的有点问题。
前段时间看到苏神介绍一篇文章:《曾被嫌弃的预训练任务NSP,做出了优秀的Zero Shot效果》,介绍的这个工作是叫NSP-BERT,可是在我看来,其结论并不能为NSP正名,NSP-BERT有效,是因为prompt有效,这与NSP有效是两码事,NSP是sequence pair的一种,但是不能代表整个sequence pair类的任务。
从NSP-BERT这个例子,我们也可以得到这样一个启发:
- 决定表征结果的,一是模型结构,二是任务范式。
同样的模型结构,换一种方式去描述这个任务,这种范式迁移的做法,便是prompt的思想了。
小结一下就是,NSP任务可以对整个句子的信息起到表征的作用,但是对于很多下游任务的应用场景,效果并不理想。
那么,讲道理,可不可以设计一个token,像[CLS]一样,用来表示整句话的特征,达到我们所期望的效果呢?
个人浅见,可以。但是需要好好设计训练目标,并且,仅仅利用一个token,似乎并不充分。
2. 比句子更小的片段——Span Bert
Span Bert,是2020年初发表的一篇预训练的工作。论文地址:https://arxiv.org/pdf/1907.10529.pdf
顾名思义,在预训练时聚焦在span上,怎么聚焦呢,就是在预训练的时候,引入了一项SBO(Span Boundary Objective)任务,试图用一段span前后的两个token,去回复mask的部分。
其核心的计算如下:
y i = f ( x s − 1 , x e + 1 , p i − s + 1 ) y_i = f(x_{s-1},x_{e+1}, p_{i-s+1}) yi=f(xs−1,xe+1,pi−s+1)
其中, f f f是Gelu激活的2层FFN, ( s , e ) (s, e) (s,e)是一个span的范围,例如上图中的 x 5 x_5 x5到 x 8 x_8 x8,上式的意思就是,利用一个span两侧的token的信息,去表征这个span的信息,尝试恢复被mask的这个span。并且,仅仅依靠两个token的信息,还不太充分,还引入了相对位置表征,即 p i − s + 1 p_{i-s+1} pi−s+1。
Span Bert在span-level的任务,例如span-level的问答、共指消解中,取得了很不错的效果,这足以说明,句中token可以充分地表征其上下文其他token的信息。
在span的表征中,除了前后两个token的信息,还引入了相对位置编码的信息。其实也很容易理解,因为对于同一个span中的不同token,例如上图中的 x 5 , x 6 , x 7 , x 8 x_5, x_6, x_7, x_8 x5,x6,x7,x8,如果仅仅依靠 x 4 x_4 x4和 x 9 x_9 x9,那么被mask的4个token的表征不就一样了吗,所以相对位置编码在此时就十分重要了,原文中设计的是span中的token距离start位置的相对位置编码,当然也可以设计换成距离end位置的相对位置编码,不过似乎并没有什么本质的区别。
不妨大胆的设想一下,经过SBO这样的预训练任务,会达到怎样的一个效果呢?经过这样的一个预训练任务,每个span前后的token就很好的蕴含着这个span的信息,那如果,训练针对的对象不是span,而是一个完整的句子呢?经过类似的训练句子首尾的两个token,是不是可以蕴含整个句子的“表征”呢? 这样想一想,似乎离我们在第1小节中末尾提出问题的答案,就更近了一步。
于是顺着span-bert的思想,我们提出两个问题:
- 以span前后紧邻的token( x 4 x_4 x4和 x 9 x_9 x9)表征span会不会有问题,是否会与该token自身的表征存在冲突,在学习的过程中,该token既要学习自身的表征,又要学习其相邻span的表征,这两项任务是否可能存在冲突,从而影响效果呢?
- 如果表征中,相对位置编码的引入,是为了定位到span中的不同token,那么对于sequence-level而非token-level的任务,例如对整个sentence的表征,相对位置编码是否就没有必要了呢?是不是可以仅仅使用两个token,就可以完成对一句话的表征呢?那是不是一个token也可以完成任务呢?(答案是肯定的,一个token表征的话,[CLS]就可以嘛,只是效果好坏的问题)
带着这些疑问,我们来到下一个章节——关系抽取模型PURE。
3. 更加纯粹的表示方法——PURE
The Princeton University Relation Extraction system,简称PURE,是2021年论文《A Frustratingly Easy Approach for Entity and Relation Extraction》中所提出的关系抽取模型,论文地址:https://arxiv.org/pdf/2010.12812.pdf.
其实我是去年就读了这篇论文,反而是发表时间更早的span bert是后读的。当我以看到span bert时,立刻就想到了pure,果不其然,这两项工作的作者中,都有Chen Danqi老师。
Pure是做关系抽取的,并且是一个pipeline任务,理解起来非常直观。即先抽取实体,然后再判断实体之间的关系。
具体而言,实体识别模型先抽取文中的实体span,然后再实体span的前后,各添加一个特殊token。如原文中给的例子所示,对于一个实体类型为“Method”的span,在它的前边增加一个特殊token“<O:Md>”,在它后边增加一个特殊token“</O:Md>”就可以用来找以它为object的关系;如果把增加的token换成“<S:Md>”和“</S:Md>”,就可以用来判断以它为subject的关系。
在此介绍这篇文章,并不是想把话题转移到关系抽取任务,我们的注意力还是在“表征”上,在PURE的论文中,对span的表征是这样的:
对于一个span s i s_i si,其表征为
h e ( s i ) = [ x s t a r t ( i ) ; x e n d ( i ) ; ϕ ( s i ) ] h_e(s_i)=[x_{start(i)};x_{end(i)};\phi(s_i)] he(si)=[xstart(i);xend(i);ϕ(si)]
其中, ϕ ( s i ) \phi(s_i) ϕ(si)是可学习的,表示span s i s_i si的宽度embedding。
发现什么问题没有,PURE对于span的表征与SpanBert如出一辙,只是在细节上存在差别:
- 区别1:在实体前后增加了与实体类型挂钩的特殊token,用来表征实体,而非SpanBert中直接用相邻的token;
- 区别2:采用了实体宽度embedding,而非SpanBert中的相对位置embedding。
这两个区别,就对应了我们在第2节末尾提出的两个问题。
对于第一个区别,说明我们提出的第一个问题中的担忧是有道理的,如果一个token既要表示它自身的信息,又要表示相邻span的信息,那它就不 “纯粹” 了,既然如此,干脆取一个干净的token,就像bert中的[CLS]和[SEP]一样,在词表中拿一个unused token,专门承担起表征的任务。更进一步地,对于一个关系抽取任务,对于不同关系类型的subject和object,实体类型是一个很重要的信息,既然如此,干脆对于不同类型的实体,采用不同的token,这样一来,即实现了“专token专用”,又有效地利用了实体类型信息,起到类似于CRF中transition的作用,何乐而不为呢?我想,PURE这个模型的“纯粹”之处,大概就在于此吧。
对于第二个区别,就更容易理解了,跟我们提出的第二个问题正好相符。既然关系抽取任务到了关系模型那一步,是判断实体两两之间的分类任务,那就没有必要关心实体内部每一个token的信息了,所以自然没有了实体内部每个token关于首尾位置相对位置编码的那一项,取而代之的是关于实体宽度的embedding,也就是说引入了实体宽度的信息。我不确定新增的这一项embedding能发挥多大作用,没有做实验就没有发言权。不过既然作者把它放在这儿,那应该就是有正面效果的。
到这里,我们讨论的话题文本“表征”,似乎已经有了比较明确的答案,即采用某个或某些token,去蕴含某个文本片段的信息,是合理且有效的。
再大胆一点,这个结论是否可以推广到更广泛的领域呢?自然是可以的,并且这个问题提的就有些废话。
回顾在CNN中,卷积网络形成的特征图是什么,每一层的特征图中的节点,不正是上一层特征图某些节点的“表征”吗。如果在卷积层最后,没有接全连接,而是一直卷一直卷,卷到最后只剩一个节点,那好像就类似于BERT中的[CLS]一样,可以用作整张图的“表征”了,只是它形成的方式与[CLS]不同,这是由CNN与transformer的结构不同所决定的。
CNN如此,那么更广泛地,对于GNN呢?是不是也有类似的做法,用来对表征整张图,然后用来做图分类等下游任务呢?随手查了一下,还真有,参考《Representing Long-Range Context for GraphNeural Networks with Global Attention》。
回顾我们前边的讨论,忽然有这样一种感觉,本文在第2节里提出的问题,“是否可以利用像[CLS]一样的token,用来表征整句话”这个问题,似乎是一个非常显而易见的问题,甚至是有点废话,那不如把目光收回到NLP任务,重新审视一下最初的问题,为什么可以用一个或几个token,来蕴含整句话的信息,以及,这种表征到底有什么实际含义。
4. 风光无限的prompt,到底提示了什么
在之前的博客《(杂谈)关于UIE的一点感想》中,我曾对prompt进行过一些讨论,其中一个比较重要的发现是,在我早期的“prompt”任务的尝试中,模板的构建似乎对结果的影响并不是很关键,例如,对于prompt处理事件抽取任务,提示模板写成“触发词为裁员的事件的被裁员方是[?]”,或者写成“这个事件的触发词是裁员,[?]被裁了”,对结果的影响并不大,甚至可以只写一个“裁员”作为prompt,这表明prompt的有效性,似乎并不是主要来自于模板近似自然语言这一特点,而是因为模板中的token,与原文中的token产生了交互,可以有效地对原文中的信息进行表征。
无独有偶,近期在学习苏神的博客的过程中,也看到了类似的结论,具体内容可参考《P-tuning:自动构建模版,释放语言模型潜能》。
就在昨天,同事发给我一篇今年ACL的论文:《Query and Extract: Refining Event Extraction as Type-oriented Binary Decoding》,其主要内容好像跟我们在20年底做的实验非常类似,也就是以QA拼接原文的形式,做pipeline的事件抽取,当然,这篇论文还做了一些其他的工作。看到这个论文我挺惊讶的,一是惊讶这个idea居然可以发ACL,二是惊讶这篇论文今年才出现。(声明:这篇文章还没有细看,代码也没有实验,或许与我的理解有所出入)。
所以,prompt真的是在“提示”吗?我认为并不尽然,换个角度理解,它有效的原因在于prompt模板提供了若干额外的token作为“锚点”,使得“锚点”token可以与原文中的token进行有效地交互,并表征一定的信息。
从这个角度思考,是不是好像这一系列的结论,都顺理成章了。
5. 重识transformer,也只是加权平均
既然问题是从预训练模型提出的,那最后肯定还是要回归transformer,本文的最开始我们提出这样的问题,我们利用预训练模型拿到的表征,为什么可以表征整个句子的信息,它又表征了什么东西,不妨跟随直观感受,定性的分析一下。
从结构上讲,预训练模型的核心无疑就是Transformer结构,而transformer主要又可以分为SA(self-attention)和FFN(feed forward network)。
后者比较容易理解,叠加一层的话,就是以前面的节点,加权平均来表示后边的节点,如果两层的话,那无非就是在加权平均的基础上,再做一次加权平均,其间再穿插一下增强非线性表征能力的激活函数。
那么self-attn又是在做什么呢?
在切入SA之前,先来看这样一段代码,这是sentence-transformer模块中对余弦相似度计算的实现,为了更好地扩展,它没有采用F.cos_similarity,而是采用了torch.mm,也就是矩阵乘法。
def pytorch_cos_sim(a: Tensor, b: Tensor):"""Computes the cosine similarity cos_sim(a[i], b[j]) for all i and j.:return: Matrix with res[i][j] = cos_sim(a[i], b[j])"""return cos_sim(a, b)def cos_sim(a: Tensor, b: Tensor):"""Computes the cosine similarity cos_sim(a[i], b[j]) for all i and j.:return: Matrix with res[i][j] = cos_sim(a[i], b[j])"""if not isinstance(a, torch.Tensor):a = torch.tensor(a)if not isinstance(b, torch.Tensor):b = torch.tensor(b)if len(a.shape) == 1:a = a.unsqueeze(0)if len(b.shape) == 1:b = b.unsqueeze(0)a_norm = torch.nn.functional.normalize(a, p=2, dim=1)b_norm = torch.nn.functional.normalize(b, p=2, dim=1)return torch.mm(a_norm, b_norm.transpose(0, 1))
流程很简单,unsqueeze,norm,然后矩阵乘法。
看最后一行:将norm之后的 t e n s o r a tensor_a tensora与norm之后的并且转置的 t e n s o r b tensor_b tensorb,做了矩阵乘法。假设tensor有 h h h个特征,那么这个矩阵乘法就是 ( 1 ∗ h ) (1 * h) (1∗h)乘以 ( h ∗ 1 ) (h * 1) (h∗1),就变成了一个标量,也就是相似度。
那假如 a a a和 b b b中,都有两个元素,乘法是 ( 2 ∗ h ) (2 * h) (2∗h)乘以 ( h ∗ 2 ) (h * 2) (h∗2)呢?那就得到了一个 2 ∗ 2 2*2 2∗2的矩阵,矩阵中的元素,代表 a 1 a_1 a1与 b 1 b_1 b1, a 1 a_1 a1与 b 2 b_2 b2, a 2 a_2 a2与 b 1 b_1 b1, a 2 a_2 a2与 b 2 b_2 b2之间的相似度。
那如果不是2个,而是 s e q − l e n seq_-len seq−len个呢?那是不是就得到了一个 s e q − l e n ∗ s e q − l e n seq_-len*seq_-len seq−len∗seq−len的矩阵,矩阵中的每一个元素,代表 a a a中的每一个元素和 b b b中每一个元素两两之间的相似度。
再如果, a a a和 b b b压根就是同一个呢?如果这 h h h个特征,可以分成若干个(比如12个)桶呢?想到什么没有,SA的核心,就快要显现了。
SA的公式,想必很多同学都已经熟记于心了:
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
这个式子可以拆解成几个步骤, Q Q Q与 K K K(的转置)相乘,除以 d k \sqrt{d_k} dk,取softmax,以及乘以 V V V,如果让我从中选择最核心的一步,我会选择第一步,也就是 Q Q Q与 K K K(的转置)相乘。
我把transformers模块中对应的一行代码放在下面:
# 代码出自transformers.models.bert.modeling_bert.BertSelfAttention
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
在结合刚刚余弦相似度的代码最后一行:
return torch.mm(a_norm, b_norm.transpose(0, 1))# 注:torch.matmul可以理解为高级版的torch.mm,可以处理高维张量
这分明就是一回事嘛,所以说,SA的核心,就是计算了序列中的每个token,与其自身中的所有token之间的相似度而已。
至于 d k \sqrt{d_k} dk,是为了防止方差偏移,将方差拉回1,在实际操作中,其实也可以省略这一步,例如大名鼎鼎的T5.
softmax就更容易理解了,是把相似度变成“概率”。
有同学可能要问了,“博主博主,既然相似度矩阵都已经算出来了,那最后怎么又乘了一个 V V V呢?”
也不难理解,从变量维度的角度考虑,相似度矩阵是的维度是 ( s e q − l e n , s e q − l e n ) (seq_-len, seq_-len) (seq−len,seq−len),而你接下来希望输出的特征的尺寸是怎样?是 ( s e q − l e n , h i d d e n − s i z e ) (seq_-len, hidden_-size) (seq−len,hidden−size)呀,当然需要一个额外的操作把其中的一个 s e q − l e n seq_-len seq−len变成 h i d d e n − s i z e hidden_-size hidden−size。
从实际意义的角度理解,我们刚刚说SA的核心是计算相似度,可没有说SA就是仅仅计算相似度,它计算相似度的目的,是以相似度为权重,将每个token的每个特征,表示为其它token的对应特征的加权平均。
前面的部分是计算权重,即attention score,而V就是被加权的对象。简单画一个矩阵相乘的图帮助大家理解一下,最后乘以 V V V的加权平均的过程:
除此之外,SA还对 Q , K , V Q,K,V Q,K,V做了一个linear变换,以增强其表示能力。而在多层SA之间穿插FFN,更进一步增强了模型的能力。所以说,FFN是加权平均,SA也是加权平均,这就代表着,我们的整个模型就是在利用每个token的特征,通过加权平均的方式,来表示其他token的特征,所以理论上,一个token当然可以用来涵盖其他token的总和信息了,只不过权重有大有小罢了。
当一个token的权重全部集中在其自身上时,其他位置的权重为0,那,它就不足以用来表征整个句子,所以,如果我们想要获得一个可以用来代表整个句子信息的token,那就是要通过设计一个合理的训练任务,使得这个token的注意力权重,可以落在句子中最重要的那些token上。
下图分别示意了在span-bert,prompt做分类,以及QA抽取论元的任务中,“锚点”token的注意力权重大概落在了哪些token上。
再回想一下在学校里学过的一点关于神经网络的知识,不仅是transformer在做加权平均,所有的神经网络,不都是在做这件事吗,只是我们这一通分析下来,使得这个认知更加清晰明确了一点。
所以说,从来就没有什么prompt,有的只是加权平均。
就算这是一句废话,但总归,没说错吧?
6. 总结与感悟
本文总结很多项工作之间存在的潜在的关联,从学习的角度,这让人很愉悦,可是从学科发展角度,又不免有些令人担忧,从这两年的NLP研究进展来看,尤其是NLU领域,同质化的工作越来越多,真正有颠覆性的核心创作越来越少见,尽管不得不承认有很多令人眼前一亮的工作出现,但仔细想想,似乎也是在比较微观的地方引入一些trick,或是修修补补,并没有跳出原来的整体大框架。
我隐隐感觉,如果一直以NLU习惯性的思维方式去看待问题,很难产生大的技术创新,或许我们应该以更高深的数学知识的视角,以另外的理论体系,去理解,去构建新的研究。但是很遗憾,以我薄弱的数学功底,无法就此发表任何有价值的见解了。
我无比期待下一个像预训练模型一样的颠覆性工作的出现,那一刻或许还很遥远,又或许正在发生。
感谢你读完我的文章,有过本文对你有一点帮助的话,记得留下一个免费的赞,也欢迎大家评论交流,转载转发,转载请注明出处。