继续NLP的学习,看完理论之后再看看实践,然后就可以上手去kaggle做那个入门的project了orz。
参考:
1810.04805.pdf (arxiv.org)
BERT 论文逐段精读【论文精读】_哔哩哔哩_bilibili
(强推!)2023李宏毅讲解大模型鼻祖BERT,一小时带你读懂Bert顶级论文!_哔哩哔哩_bilibili
BERT 论文逐段精读【论文精读】_哔哩哔哩_bilibili
在计算机视觉中,很早的时候我们就可以在一个大的数据集上(比如ImageNet上)训练好一个CNN的模型,这个模型可以用来帮助很多计算机视觉的任务提升性能,但是NLP在BERT之前,一直没有一个深的神经网络,使得训练好之后能够帮助很多NLP任务->在NLP里面,还是每个人构造自己的神经网络,然后自己做训练。
BERT的出现使得我们可以在一个大的数据集上,训练好一个比较深的神经网络,然后英语在很多NLP的任务上面,既简化了这些NLP任务的训练,又提升了它的性能。所以BERT和它之后的一系列工作使得NLP有一个质的飞跃。
BERT是一个深的双向的transformer,是用来做预训练的,针对的是一般的语言理解的任务。
摘要
BERT,Bidirectional Encoder Representations from Transformers(transformer这个模型的双向的编码器表示,注意到这里和标题内容是不一样的,很可能这个名字是凑出来的(李宏毅在NLP课程中提过这个,表示就是凑出来的,吐槽说论文也喜欢蹭热点,比如有些用三个动词,比如xxx is all you need这样,还举例说后面的其他模型也凑热闹,硬用芝麻街里的人名凑),这篇论文的想法基于一个重要的工作ELMo(这是一个基于RNN的架构),来自于芝麻街的人物)。不同于其他最近的论文(指当时),BERT设计用于在所有层中通过联合左右的(这里与GPT不同,GPT是基于左边的上下文)上下文信息,训练没有标记的文本深的双向表示。因为这一设计,导致训练好的BERT可以只增加一个输出层,就可以在很多NLP任务中(比如问答、语言推理)得到很多不错的结果,并且不需要对仍无做很多特别的架构上的改动。
BERT在概念上更加简单,在实验上也更好。
导言
在语言模型中,预训练可以用来提升很多自然语言的任务。包括句子层面(sentence-level)的任务(主要用来建模句子之间的关系,比如对句子情绪的识别,或者两个句子之间的关系),词源层面(token-level)的任务(比如实体命名的识别->指的是是不是人名?街道名?这种任务需要产生一些fine-grained的token层面的输出)。
也就是说,预训练在NLP中已经流行了一阵子了。
在使用预训练模型做特征表示的时候,一般有两类策略:feature-based(基于特征的,代表ELMo,对每一个下游的任务,构造一个跟这个任务相关的神经网络,其实用的是RNN的架构,在预训练好的这些表示作为一个额外的特征和输入一起输入到模型中->有一个统一的模型,但是模型里面有不同的黑箱,训练的任务不同会使用不同的黑箱)和fine-tuning(微调的。比如GPT,把训练好的模型放到下一个任务的时候不需要改变很多,只要改一点儿就行了。模型预训练好的参数会在新的数据上再进行微调)。
这两个策略在与训练队时候都是使用一个相同的目标函数,都是使用一个单向的语言模型来学习general的语言表示。
不过这里语言模型本身就是单向的,给一些词,预测下一个词是什么,是属于预测模型(既然是预测肯定是单向了)。
这就导致了在预训练的时候存在一些限制,比如在GPT里,采用的事从左到右的结构(只能从左读到右),但是如果我们要做句子层面的一些分析的话,从左看到右或者从右看到左都是合法的。另外,就算是token-level上的一些任务(比如Q&A),其实也是可以看完整个句子去选答案的(不用一个词一个词看)。所以作者认为可以把两个方向的上下文都放进来的话,应该可以提升任务的性能的。
BERT正是为了解决之前提到的语言模型是单向的问题的,采用的是一个带掩码的语言模型(masked language model,MLM)。每一次从输入中随机选择一些token,然后把他们盖住(masked),目标就是去预测那些被盖住的token(就是完形填空orz)。与标准的语言模型从左看到右不同的是,MLM允许我们看左右的信息->就可以去预训练一个双向的深的Transformer。
除此之外,还训练了一个“next sentence prediction”(下一个句子的预测)。核心思想是给定两个句子,判断这两个句子在原文里是不是相邻的->让模型学习一些句子层面的信息。
贡献:
1. 展示了双向信息的重要性
2. 不用对特定的任务做特定模型的改动(BERT是基于微调的)
3. 开源
结论
好。
最近一些实验表明使用非监督的预训练是许多语言理解系统(language understanding system)的必要组成部分。特别是,即使是资源不多的任务也能够享受深度神经网络。
使得大量的NLP任务可以去使用同样的预训练模型。
双向(ELMo)+Transformer(GPT)
BERT模型
BERT里面有两个步骤:pre-training(预训练)和fine-tuning(微调)。
在pre-training中,模型是在一个没有标号的数据上训练的。在微调的时候,也是使用一个BERT模型,但是这个模型的权重被初始化成我们在pre-training的时候得到的权重。所有的权重在微调的时候都会参与训练,这个时候使用的就是有标号的数据了。每一个下游的任务都会创建一个新的BERT模型(虽然都是使用pre-training的参数来初始化模型,但是会各自对模型做fine-tuning)。会根据自己的数据训练自己的模型。
在预训练的时候我们的输入是一些没有标号的句子对(unlabeled sentence A and B pair),训练一个BERT模型。对每一个下游的任务,我们对每个任务创建一个同样的BERT模型,他们的权重的初始化值来自于前面训练好的权重,对每一个任务,我们有自己的有标号的数据,然后我们对BERT继续进行训练,得到针对这个任务的BERT的版本。
模型架构
BERT模型是一个multi-layer bidirectional Transformer encoder(多层的双向的Transformer的编码器)。
三个参数L、H、A
L:Transformer块的个数(the number of layers, i.e., Transfomer blocks)
H:隐藏层的大小(the hidden size)
A:在自注意力机制中multi-head的头的个数(the number of self-attention heads)
有两个模型:BERT_BASE(L=12, H=768, A=12,总共可以学习的参数是110M)和BERT_LARGE(L=24, H=1024, A=16,总共可以学习的参数是340M)。这里LARGE比BASE层数翻了一倍,宽度从768变成了1024(为什么是1024?因为BERT模型的复杂度和层数是线性关系,和宽度是一个平方的关系,因为深度变成BASE的两倍,那么在宽度上,也要使得增加的平方是之前的两倍)
(这里断一下,这里没太听明白意思,看了一下评论区的笔记BERT 论文逐段精读【论文精读】 - 哔哩哔哩
BERT 模型复杂度和层数 L 是 linear, 和宽度 H 是 平方关系。
因为 深度 变成了 以前的两倍,在宽度上面也选择一个值,使得这个增加的平方大概是之前的两倍。
大概这里计算了一下,指的是1024^2≈2×768^2)
头的数目变成了16,每个头的维度都固定在64(4*16),因为宽度增加了,所以头的个数也增加了。
我看到这里也有疑问,那么为什么增加之后的头的个数是16呢?似乎也没有相关的信息。不过翻到两个比较有意思的论文,考虑之后作为补充读物看看。
[1906.04341] What Does BERT Look At? An Analysis of BERT's Attention (arxiv.org)
[2106.09650] Multi-head or Single-head? An Empirical Comparison for Transformer Training (arxiv.org)
题外话,找到这张图了,ELMo和他的小伙伴们
怎么把超参数换成可学习的参数的大小。模型中的可学习参数主要来源于两块,一个是嵌入层,另一个是Transformer块。
首先看一下嵌入层,嵌入层就是一个矩阵,输入是字典的大小,这里字典的大小是30k(弹幕:30k是因为BERT用的是WordPiece embbeding(一文读懂BERT中的WordPiece - hyc339408769 - 博客园 (cnblogs.com)),vocabulary中有30k个token),输出等于隐藏单元的个数(即H),输入会进入Transformer块。Transformer块中有两个东西:一个是自注意力机制,一个是后面的MLP。自注意力机制本身是没有可学习参数的,但是对于multi-head attention mechanism的话,会把所有进入的K(key), V(value), Q(query)分别做一次投影,每一次投影的维度是等于64的(A×64=H)。进来的K Q V都有自己的投影矩阵,合并每个头的投影矩阵->得到H×H的矩阵。拿到输出之后还会做一次投影,也是一个H×H的矩阵。所以对于Transformer块的自注意力可学习的参数是4×H²。
(弹幕:去看multi-head attention的示意图,里面有4个Linear模块
4*H*H原因为,QKV各一个投影阵h*h*3,输出投影阵h*h)
在往上是MLP,MLP里面需要两个全连接层,第一层的输入是H,输出是4×H,另一个全连接层的输入是4×H,输出是H。所以每一个矩阵的大小是H×4H。所以两个就是8×H^2。(8+4)×H²就是一个Transformer块里的参数。还要乘以L(有L个block)
(弹幕:query, key和value投影的参数concat之后,就是H*H*3了,再加上concate之后的线性变换H*H,就是4倍关系了
乘以4是因为全连接层的中间层维度是4倍H
最后一层里每个头都是H/A)
所以最后的总的参数的个数就是30k×H+L×H²×12
评论区给的解释:
这里的参数就是各个W,比如将key,value,query降为低维,这三个维度矩阵都是(H, H/h),H就是我们字典输入embedding层后的特征数,h是多头中头的数量。 对于一个头而言,参数的总数是(H * (H / h) * 3),总共有h个头,所以这一部分的参数是3(H²),然后我们需要把所有头拼接后又输出,就又是一个H * H,相当于Attention层的参数个数是4H²,在下面的前馈网络里,隐藏层维度是(H,4*H),输出层是(4H,H),所有这一部分就是8H²
感觉还不是很看得懂,这里引用neural networks - How to account for the no:of parameters in the Multihead self-Attention layer of BERT - Cross Validated (stackexchange.com)
The Illustrated Transformer – Jay Alammar – Visualizing machine learning one concept at a time. (jalammar.github.io)
这里仅仅对mult-headed self-attention部分进行讨论。
如图所示。在transformer块中,输入向量先在multi-head里transform,然后用在self-attention,再串行,再连入fully connected dense forward layer
The input vector of dimension d_model (in X) gets multiplied by three matrices WQ, WK, WV, 12 (=attention heads, or A) times to give (
3A
) pairs of vectors (Q, K, V). These vectors (Z0 to Z7 in the image) are each of lengthd_model/A
. So dimension of each of these matrices isd_model * d_model/A
and we have3 * A
such matrices.维度为d_model的输入向量乘以三个矩阵WQ, WK, WV 12次(12=A=# of attention heads),得到3A个(Q, K, V)向量对(pairs of vectors)。这些向量(图中的Z0到Z7)每一个的长度都是
d_model/A
。所以每个矩阵的维度都是d_model * d_model/A
。并且我们有3A个这样的矩阵。(换句话说这里就是d_model * d_model/A * 3A,前面是矩阵的size(d_model * d_model/A),后面是矩阵的个数
)Including the bias for each of Q, K, V matrices, total weights till now =
d_model * d_model/A * 3A + d_model * 3
. By this point, we have Z0 to Zi vectors from above image. These are then concatenated, and passed through the dense layer W0 which would have dimensiond_model * d_model + d_model
(with bias).每个Q, K, V 矩阵都包含bias,所以目前用到的权重是
d_model * d_model/A * 3A + d_model * 3
。现在我们就有了Zi向量,把他们拼起来,通过dense layer,得到d_model * d_model + d_model。
So total dimension of transformer cell:
A * (d_model * d_model/A) * 3 + 3*d_model + (d_model * d_model + d_model)
. For BERT base, the values areA= 12, d_model = 786
. So total parameters =12 * ( 768 * 768/12) * 3 + 3*768 + 768*768 + 768 = 2,362,368
还是觉得难以理解,所以先拆开看multi-head attention。这一方法源自于Transformer。
多头注意力机制pytorch 多头注意力机制公式_轩辕的技术博客_51CTO博客
对于每个input,首先经过一次变换得到K、Q、V,一层K、Q、V就是3A(A:# of heads)个权重,比如这里一组K、Q、V,因为A=2,所以个数为6,因为要得到这6个向量,所以理所当然要有6个权重(参数)。注意力机制综述(图解完整版附代码) - 知乎
自注意力机制的基本思想是,在处理序列数据时,每个元素都可以与序列中的其他元素建立关联,而不仅仅是依赖于相邻位置的元素。它通过计算元素之间的相对重要性来自适应地捕捉元素之间的长程依赖关系。
具体而言,对于序列中的每个元素,自注意力机制计算其与其他元素之间的相似度,并将这些相似度归一化为注意力权重。然后,通过将每个元素与对应的注意力权重进行加权求和,可以得到自注意力机制的输出。
首先对一句话x1 x2 x3 x4做embedding处理,得到向量a1 a2 a3 a4:
这里W表示的是embedding的参数矩阵。
经过embedding操作后,向量a1 a2 a3 a4将会作为attention mechanism的输入。
紧接着每个a^i会乘以三个矩阵
q(Query) 的含义一般的解释是用来和其他单词进行匹配,更准确地说是用来计算当前单词或字与其他的单词或字之间的关联或者关系; k(Key) 的含义则是被用来和q进行匹配,也可理解为单词或者字的关键信息。
这里需要一个计算,得到关联性。
对计算的关联性做softmax操作,得到比较规整的关联性×
v表示当前单词或字的重要信息(重要特征),将关联性和v^i分别相乘,在求和(group by)。
以上是关于单头的介绍,对于多头来说,就不再满足于只使用一组kqv矩阵了:
向量a^i再乘以三个矩阵之后,还会被分配多个(K,Q,V)矩阵(解释本段开始部分对3A的描述,虽然都是同样的输入,但是被放进不同的变换对中,一个head就是3,A个head就是3A)
因为这里的例子是分成两个head,所以之前得到的qkv就被破开了(这里解释为什么是
d_model/A
),也就是说最开始的dimension是d_model * d_model
,如果是单头的那么就不用变了,但是这里是multi-head,所以就被平均地切开了。q^i1与其他标号有1的key比较相似度(点乘),用相似度作为权重乘以对应的value值。
输入和输出表示
下游任务有些是处理一个句子,有些是处理两个句子,所以为了使得BERT能处理所有这些任务,所以输入既可以是一个句子也可以是一个句子对(比如Q&A)。这里的句子指的是一段连续的文字,并不是语言学上定义的句子。输入称为一个“序列”sequence,这里是sequence可以是一个句子也可以是两个句子。(这里与Transformer有区别,Transformer训练的时候的输入是一个序列对->encoder和decoder分别会输入一个序列。)BERT这里只有encoder,为了处理两个句子需要把两个句子变成一个序列。
BERT采用的切词的方法是WordPiece。核心思想是假设按照空格切词,一个词作为一个token。因为数据量相对比较大,会导致词典的size也特别大,根据之前算模型参数的方法,词典很大(百万级别)会导致整个可学习参数都在嵌入层上面。->用WordPiece,如果一个词出现的概率不大,应该切开,看子序列,如果某一个子序列(很可能是词根),出现的概率比较大的话,就只保留这个子序列就好了(不记得在哪篇博客里看到,还提出另一个问题,有些文字压根就不用空格作为词语的分割),这样就可以把一个相对来说比较长的词,切成很多一段一段的经常出现的片段。这样就可以用相对来说比较小的30k的词典就能够表示一个比较大的文本了。
(弹幕:这里的wordpiece应该和字节对编码是一个东西)
切好之后,看怎么把两个句子放到一起。序列的第一个词永远是一个特殊的记号“[CLS]”,代表classification。BERT希望这个词最后的输出代表整个序列的信息(比如整个句子层面的信息)。因为BERT使用的是Transformer的encoder,所以自注意力层会去看每一个词都会去看输入里面所有词的关系,所以放第一个位置完全OK,不影响读后面的内容。
(弹幕:既然每一次词都能看到序列的所有信息,岂不是可以随便选一个词的输出来做分类?
如果随便选一个词的输出作为分类结果,那么就会让模型有了对应的偏好,会让模型认为只要有那个选定的词,就应该输出对应正确的分类结果。
选取一个与任意一个词都不直接相关的位置作为整个句子的语义向量对于所有单词来说都是公平的、没有偏爱的)
虽然把两个句子合在一起,但是因为要做的事句子层面的分类,所以需要区分开来两个句子。有两个办法来区分,第一个是在每个句子后面放一个特殊的词“[SEP]”表示separate;第二个是学一个嵌入层,来学习每一个token是属于哪个句子。
如图所示(看下面sentence之间的[SEP],第一个位置[CLS])。每一个token进入句子得到这个token的embedding表示。最后一个transformer块的输出就代表这个token的BERT的表示,再添加额外的输出层得到我们想要得到的结果。
对于每个给定的token,其输入是通过将相应的token、段和位置嵌入相加来构建的(token本身的embedding+在哪一个句子的embedding+位置的embedding)。
给一个token的序列,得到向量的序列。这里红色的是token。token embedding就是正常的embedding,segment embedding就是表示是第一句话还是第二句话。position embedding是位置,输入的大小是这个序列的长度(输入就是每一个token在这个序列中的位置信息),得到token位置的向量。在Transformer中,位置信息是手动构造出来的矩阵,但是在BERT中无论属于哪个句子、位置在哪里,对应的向量的表示都是通过学习得来的。
(弹幕:SEP可以理解成每句话的句号,用作分割句子,所以第一个SEP是A的,第二个是B的
高维语义空间的向量相加)
Pre-training BERT
在pre-training的时候,有两个东西比较关键,一个是目标函数,另一个是用来做预训练的数据。
Masked LM
对一个输入的token序列,如果一个token是用WordPiece生成的话,那么它有15%的概率会随机替换成一个掩码。但是对于一些比较特殊的 token(第一个token[CLS],中间的分割token[SEP])就不做替换了。因此假设输入序列长度是1000的话,我们就需要预测150个词。
这也有一些问题。因为我们在做掩码的时候,会把词源替换成一个特殊的token [MASK],在训练的时候,会看到有15%的token是[MASK],但是在微调的时候是没有这个东西的(因为微调的时候不用这个目标函数),导致在预训练和在微调的时候看到的数据是有一点不一样的。->解决方法是对于这15%的被选中的去做掩码的token,有80%的概率是真的替换成特殊的掩码符号,还有10%的概率是把它替换成一个随机的token,还有10%的概率什么都不干。(80% 10% 10%实验得到的)如下图:
(弹幕:这里的mismatch就是指输入数据的分布和后续finetune的分布不同)
中间其实相当于给数据加噪音,最后的使用的是和在做微调的时候一样的数据。
Next Sentence Prediction(NSP)
Q&A和自然语言推理都是基于理解两个句子的关系的,但是这一关系并不能被语言模型直接找到。->所以让模型学习一些句子层面的信息
具体来说,我们的一个输入序列中有两个句子,A和B,B在原文中间的概率是50%(B就是A的下一个句子,标记为IsNext),还有50%的概率B就是从其他的地方随机拿来的句子(标记为NotNext)->50%的样本是正例,50%的样本是负例。
注意在上面的例子中有一个“##”,其实在原文中“flight ##less”是一个词“flightless”但是这个词出现的概率不高,所以在WordPiece中把它砍成两个词,##表示的意思就是后面那个词在原文中应该和前面的词在一起。
Fine-tuning BERT
BERT和一些基于encoder-decoder的架构的区别。
之前的transformer是encoder-decoder,因为把整个句子对都一起输入进去,所以self-attention能够看到两端,但是在encoder-decoder的架构中,encoder实际上一般是看不到decoder的信息的。因此BERT在这里会更好。但是代价是不能像transformer一样做机器翻译。
(弹幕:我的理解,因为输入最好是一种语言,机器翻译句子对是两种不同语言,放进一个编码器处理上下文理解效果可能不好
ber能做机器翻译,就是加一个解码器,类似的比如说bart
<--我猜他是想说输入有个最大的长度限制(1024)吧,不像RNN可以无限长度(尽管可能会遗忘))
在做下游任务的时候,会根据任务设计任务相关的输入和输出,好处是模型其实不需要怎么变,只要把输入改成需要的句子对就可以了。如果这个时候我们恰好就是有句子对(A,B),那么直接用就好了,如果我们只有一个句子的话(假设我们要做句子的分类),就没有句子B了,根据下游任务的要求,要么是拿到第一个token对应的输出做分类,或者是拿到对应的token的输出做输出。
a degenerate text-∅ pair in text classification or sequence tagging. At the output, the token representations are fed into an output layer for token-level tasks, such as sequence tagging or question answering, and the [CLS] representation is fed into an output layer for classification, such as entailment or sentiment analysis.
在输出端,如果是token-level的任务token的表示被输入输出层。
(弹幕:其中对于前一类我们需要用到所有词元的表示, 后一类需要我们使用[CLS]的对应输出.
在预训练中我们的NSP对应后者, 而MLM代表前者.
就是文本对应一个空集)
与预训练相比,微调就比较便宜,所有的结果都可以使用一个TPU跑一个小时就好了。
实验
BERT怎么用在下游任务上。
GLUE
是一个句子层面的任务。BERT是把第一个特殊token[CLS]的最后的向量拿出来(记作C),然后学习一个输出层W,用softmax得到标号。
SQuAD
Q&A数据。阅读理解,给一段话,问一个问题,找到答案所在的句子。->对每一个token判断是不是答案的开头/结尾。具体而言就是学两个向量S和E,分别对应这个token是这个大难的开始的概率和这个token是这个答案的末尾的概率。具体而言,对第二句话(B)的每一个token,相乘再做softmax,就会得到这个句子里面每一个token是答案开始/末尾的概率
SWAG
用来判断两个句子之间的关系。
Ablation Studies
看每一块对结果的贡献。
去掉下一个句子的预测/只是从左看到右的+没有掩码/+双向的LSTM
模型大小的影响
不用微调
评论区笔记:
BERT 论文逐段精读【论文精读】 - 哔哩哔哩
BERT 论文逐段精读【论文精读】 - 哔哩哔哩
李宏毅-ELMO, BERT, GPT讲解_哔哩哔哩_bilibili
怎么让电脑看懂人类的文字
Introduction of ELMO, BERT, GPT
回顾
最早的做法是每一个不同的文字当做不同的符号,每一个符号都用一个独特的编码表示。但是这样不太好,词汇和词汇之间是完全没有任何关联的。比如这里cat和dog都是动物,也许他们相较于cat和bag的关系会更近。但是从1-of-N encoding是看不出来的
因此出现了Word Class的概念:把词汇做分类。但是这样分类显然太粗糙了,比如图中class 1,虽然cat dog和bird都是动物,但是cat和dog是哺乳类动物,但是鸟不是(还是有区别),直接这么归类太粗暴了。
因此就有了Word Embedding,可以看做是一个soft的word class。每一个词汇都用一个向量来表示,这个向量的某一个维度某一个维度可能就表示了这个词汇的某种意思。语音比较相近的词汇就会比较接近,比如这里的dog和cat和rabbit就比较接近。但是跟bird,tree,flower就比较有距离。所以从一个词汇的word embedding就可以知道这个词汇的意思。->怎么训练的呢?根据词汇的上下文找到的。
在之前的RNN作业中(指课程中)的例子中我们希望训练一个sentiment analysis的classifier,就不是使用1-of-N encoding来表示,而是采用的embedding。用word embedding来表示某一个词汇的feature。
一个词汇可能有不同的意思。比如这里的bank,这四个bank都是不同的token,但是是同样的type(type=bank)。过去在做word embedding的时候是,每一个type有一个embedding,所以如果不同的token属于同样的type,做embedding对应的那个vector就会是一样的(也就是假设只要type是一样的,语义就是一样的)。但是事实上并非如此。
比如这里前两个句子中的bank指的都是银行(前面有money),后面两个bank指的都是河堤(前面有river)。过去传统的embedding,每一个type都有一个embedding,那么这四个bank就会有一模一样的embedding。
事实上,我们希望机器可以给不同意思的token,即使属于同一个type不同的embedding。过去的做法是,去查一下词典,bank这个type有两种不同的意思,所以bank这个type应该要有两种不同的embedding。训练出来的结果我们希望是,第一个和第二个的type是bank的token有一种embedding,而第三个和第四个token有另一种embedding。 (弹幕:type就是spelling拼写,token就是meaning意义)
但是这么做显然是不够的,因为人类的语言是很微妙的。比如同样是查词典,有些词典会告诉我们bank有两个意思,有些词典会告诉我们bank有三个意思。比如这里举例blood bank(血库)。
老师这里举的例子略二刺螈orz,解释一下就是有些情况下我们很难分清两个名字/词的意思是不是一样的(虽然我现在脑子里狂飙的也是二刺螈orz)。我们可以说一些词汇是一样的,也可以说是不一样的。
所以我们期待机器可以给每一个word token都有一个embedding。之前是每一个type都有一个embedding或者是每一个type都有固定多个embedding。现在是希望每一个token都有一个embedding。
看token的上下文,越接近的token有越接近的embedding。比如图中三个bank,都属于同一个word type,但是这三个bank是不同的token。这些不同的token都会有不同的embedding。->contextualized word embedding。 不过在这个例子中下面两个bank的意思会比较接近,所以vector比较近,和上面那个bank的距离就会远一些。
ELMO
有一个技术可以做到这件事(ELMO)。ELMO是一个RNN-based的一个语言模型。做RNN-based的语言模型其实不需要做什么label,只要收集一大堆句子。比如我们放进来一个句子“潮水退了 就知道谁没有穿裤子”,现在我们就教我们的RNN-based language model加入看到<BOS>(begin of sentence),就输出潮水,假如给潮水,就输出退了,给潮水和退了,就输出就……RNN-based language model训练的时候所学习的技能就是预测下一个token是什么?学完以后就有一个contextualized word embedding。为什么说这个word embedding是contextualized的?同样都是退了这个词汇,上下文不同的话,RNN输出来的embedding是不同的。
可能有人会提出这样只考虑了一个词汇的前文,而没有考虑这个词汇的后文->再train一个反向的RNN,从句尾读过来,这个反向的RNN给吃知道就输出就,给吃就就预测退了,给吃退了就预测潮,这样不仅考虑前文也考虑后文。
(弹幕:老师讲的这部分输入的是词向量还是one-hot编码?
ELMo的输入是已经通过预训练的,是One-hot通过字符卷积和highway层得到的token
应该是随机初始化,one hot的向量size实在太大了
Embedding本身也在训练
应该有embedding层但是如果用这层就没上下文了,用后面的隐层
输入【b, seq_len] 输出:【b,seq_len,d_model])
比如这里就把“退了”这个词汇在正向的embedding和逆向的embedding都拿出来接起来,同样的词汇上下文不一样,得到的embedding就会不同。
现在我们train什么network都是要deep的,RNN可以是deep的。但是我们train deep nn的时候会遇到问题,因为有很多层,每一层都有embedding,到底用哪一层的embedding呢?
ELMO的思路是,全都要(orz)
ELMO的做法是,现在每一层都会给我们contextualized的word embedding,每一个词汇丢进去都会有不止一个embedding吐出来(每层RNN都会给一个embedding)->把这些embedding都加起来,一起用。
那么怎么加起来呢?最简单的策略是直接直接拼起来。ELMO会做weighted sum(加权和)。假设我们这里的RNN有两层,吐出了两个embedding,h1和h2,ELMO会把第一层吐出的embedding乘α1,第二层吐出来的embedding乘α2,加起来,得到蓝色的embedding,再把蓝色的embedding做下游的任务。
那么这个α1和α2是什么来的呢?learn出来的。怎么learn?在还没有接下游任务之前,在还没有使用ELMO token embedding之前,是不知道这些α的值应该是多少的,要先设定好接下来做什么application(比如Q&A),再把这些参数(α1和α2)和接下来的任务一起learn出来。
比如接下来要做Q&A,Q&A的model里面也有一些参数,会把α1和α2视为Q&A要学的参数的一部分,跟着network的其他部分一起学出来。所以不同的任务要用的α1和α2就不一样。在原始的ELMO的论文中,不同的任务学出来的α的结果如上图。
BERT
BERT是transformer的encoder。在BERT里面只要收集一大堆句子,这些句子不需要有annotation,就可以把这个encoder train出来。
BERT实际上在做什么?给一个句子给BERT,每一个token都会吐一个embedding出来。input一串sequence,output一串embedding,每一个embedding都对应到一个word。
虽然在例子中给的是中文的词,但是实际上,如果真的用于中文,可能字的效果会更好。中文的词的数量太多了,所以input的vector就太多了,如果换成字的话,因为常用的中文的字只有4000多个->可以穷举的。
如何训练BERT
方法一:Masked LM
现在要交给BERT的任务是,输入的句子的词汇以15%的概率置换成一个特殊的token [MASK](盖掉一个句子中15%的词汇),BERT要做的就是猜测这些盖住的地方,是哪个词汇。
那么BERT是怎么填回来的呢?假设输入的句子的第二个词汇是盖住的,接下来把输入都通过BERT,每一个input token都会得到一个embedding,接下来把盖住的地方的embedding丢到一个linear mult-class classifier里面,要求这个classifier预测现在被mask的词汇是哪一个词汇。因为这个classifier是一个linear的classifier,所以能力很弱,所以想要能预测出被mask的词汇,这个BERT就可能非常深->一定要能抽出来一个非常好的representation->如果两个词汇填在同一个地方不会有违和感,那么这两个词汇就有类似的embedding
方法二:Next Sentence Prediction
给两个句子,BERT预测这两个句子是不是接在一起的。在两个句子之间加上[SEP],告诉BERT两个句子的交界在哪里。怎么判断两个句子是不是相接的,还需要在加一个token在开头[CLS],代表在这个位置要做classification。从这个特殊的位置[CLS]输出的embedding丢到一个linear binary classifier里面,让它output现在输入的这两个句子是不是接在一起的。
为什么放在句子的开头呢?让BERT读完整个句子再决定这两个句子是不是接在一起的(我先盲答,是因为BERT是Bidirectional的所以不存在方向问题,再加上attention mechanism本身就能看到整个句子)?
仔细想想BERT的架构,如果BERT里面放的是RNN(正向,从左到右),[CLS]放在句子的尾部比较合理。BERT的内部并不是RNN,BERT的内部是一个Transformer,是self-attention。self-attention的特色就是“天涯若比邻”,两个相邻的word和两个距离很远的word,对BERT来说是一样的。对self-attention来说,假如不考虑position encoding的影响,一个token放在句子的开头或结尾是没有差别的。
这个linear classifier和整个network架构是一起被训练的,希望通过解决预测sentence的任务把BERT部分的参数学出来。
在文献中,这两个方法是同时使用的。
怎么用BERT
假设我们现在输入一串word(PPT上的例子是word,实际上应该是character),每一个word都有一个contextualized的word embedding。怎么使用呢?最简单的做法是把BERT当做一个抽feature的工具,通过一种新的embedding做想做的任务。
在论文中给出的方法是,把BERT和要做的任务一起训练。
case 1
假设现在是要输入一个句子,输出一个类别。比如,sentiment analysis,document classification(判断句子情绪,文章分类)。把现在要做分类的句子做一个分类,不过要在开头的地方加一个[CLS],接下来把代表分类的符号的位置所输出的embedding丢到一个linear classifier里面,去预测属于哪一类。
现在训练的时候linear classifier是随机初始化的(只有这里需要重头学),BERT的部分做fine-tuning(只要微调就好)。
case 2
假设现在要做的任务是,输入一个句子,决定句子中的每一个词汇属于哪一类。比如slot filling。把每一个embedding都丢到linear classifier里面,决定embedding属于哪个类别。
case 3
假设现在要做的是,输入两个句子,输出一个类别。比如nature language inference,给机器一个前提,再给一个假设,让机器判断,根据这个前提,这个假设对/错/不知道。
方法是先给model两个句子(加上[CLS][SEP]),把[CLS]的embedding丢到linear classifier里面,让决定T/F/unknown
case 4
Q&A问题,特别是里面的Extraction-based的Q&A问题。就是给model读一个文章,问一个问题,希望model给出答案,但是答案肯定出现在文章里面了。
怎么做呢?给model文章&问题,文章和问题都是用token sequence表示。model吃一个文章,吃一个问题,输出两个整数->这两个整数表示答案落在文章的第s个token和第e个token之间。
怎么用BERT解决刚刚的问题呢?input问题[SEP]文章,现在文章里面的每一个词汇都会有一个embedding,接下来让machine去learn另外两个vector(图中红色和蓝色的),这两个vector 的dimension和黄色vector的dimension是一样的,红色的vector拿去和文章中的每一个黄色的vector做点乘(类似做attention的动作)。
每一个算完点乘之后,softmax得到分数,然后看哪个分数最高。举例来说,这里就是第二个词汇的分数最高。
->s=2
同理,蓝色的vector也做点乘,softmax,得到分数。
->e=3
所以这里就是文章的第二个和第三个word就是答案。
但是如果出现s=3,e=2->没有答案了
为了适应中文的改动:
————————————————————————————————————
后面的内容都是GPT啦。介于我的motivation是完成kaggle nlp的"Getting Started" competitions,所以就先读到这里了。后面等我做完这个project再来啦。
后面会参考:
LeeMeng - 進擊的 BERT:NLP 界的巨人之力與遷移學習
KerasNLP starter notebook Disaster Tweets | Kaggle
(先容我吐槽一句,看到小贝的头变成BERT,我要应激啦,脑海里面开始我铠他超×)