本文为李弘毅老师【BERT and its family - Introduction and Fine-tune】的课程笔记,课程视频youtube地址,点这里(需)。
下文中用到的图片均来自于李宏毅老师的PPT,若有侵权,必定删除。
文章索引:
上篇 - 7-1 Overview of NLP Tasks
下篇 - 7-3_BERT and its family - ELMo, BERT, GPT, XLNet, MASS, BART, UniLM, ELECTRA, and more
总目录
我们这里所说的pre-train model就是输入一串tokens,能够输出一串vectors,且每个vector可以表示对应的token的语义的模型,这些vectors也被称作为embeddings。
以前常用的模型有耳熟能详的word2vec,Glove等等。但是英文的单词太多了,这个时候来一个新单词的话整个embedding的模型就要重新train了,为了解决整个问题,就有了fasttext。fasttext是针对英文的,针对中文的则是输入图片,让模型通过图片中文字的偏旁部首去预测出训练时没见过的文字的embedding。这种训练embedding的方式,根据语言的不同会有不同的方法。
不过,这里还有一个问题,就是按照上述的方法,如果输入的token是一样的,那么每次出来的vector也不然hi一样的。但实际情况下并非如此。比如下图中"养只狗"的“狗”和“单身狗”的“狗”显然是不同的意思。
于是,我们就希望模型可以在输出某个token的embedding的时候,去考虑上下文的信息,这个叫做contextualized word embedding 。这样的模型基本就是基于LSTM或者self-attention layer去搭建的一个seq2seq的模型(如Bert,Megatron,Turing NLG等),可以理解为之前的文章中讲过的encoder,至于训练的方法,这里暂时先不讲。也有用tree-based model去做的,但是因为效果没有那么强,所以不太流行。
为了让模型的效果变好,所使用的模型也就越来越大,参数已经大到只有那些大公司才可以使用了。
当然,穷人也有穷人的办法,我们可以再不过多地损失模型精度的情况下,把模型变小一点,这里有很多模型压缩方面的技术,这里不细讲了。
那么,我们如何把这个pre-train model应用到各式各样的NLP任务当中去呢?上篇中我们已经讨论过了,NLP的任务可以按输入和输出分别分类,如下图所示。那么接下来就来讲一下,如何去应对每一类。
(1) One sentence
对于输入只有一个句子的,那就直接输入就可以了,因为pre-train model也是一个句子的输入。
(2)multiple sentences
对于输入有多个句子的,我们需要有一个叫做"[SEP]"的分隔符来把两个句子拼成一个句子,然后再输入就可以了。
(1)One class
输出只有一个分类的时候,可以有两种做法,一种做法是,在输入的开头加一个叫做"[CLS]“的toekn,然后在这个”[CLS]“对应的输出的embedding后面加一个head,也就是比较浅的神经网络,可以是一层全连接,然后输出想要的类别数量;另一个做法是,把所有token的输入都输入到一个head当中去,然后输出想要的类别数量。
(2)Class for each token
当每个token都要做分类时,那在模型的后面加一个seq2seq的head的就可以了。
(2)Copy from input
当我们的任务是Extraction-based QA时,该怎么办呢?因为输入有question和document两个,所以我们需要加入一个”[SEP]"分隔符,然后输出是找出document中的哪个token为答案的start,哪个token为答案的end。这个时候,就要两个额外的向量去分别和document中每个token的输出做dot product,然后和start向量最相关的token就是start token,和end向量最相关的token就是end token。
(4)General Sequence
当我们希望模型的输出也是一个sequence的时候,我们可以把pre-train model的输出作为输入,在后面接一个decoder,让这个decoder去完成输出sequence的步骤。这样的一个坏处就是decoder会是一个比较大的模型,而我们是希望没有pre-train过的参数是越少越好的。
另一种做法是把pre-train的模型当作decoder去做。这就需要我们在input的后面加一个类似于"[SEP]“的特殊符号,然后把这个符号的输出丢到一个我们自定义的head当中去,输出一个token,然后把这个token当作输入,反复下去,直到输出”"
在训练的时候,我们可以把pre-trained model的weights都固定住,然后只去训练最后自定义加上去的head。
我们也可以直接训练整个模型,虽然整个模型很大,但大部分的weights是预训练过的,所以训练起来不会坏掉。实际经验也是这种方法要优于上面的方法。
如果我们要用第二种方法的话,不同的task我们就需要train一个不同的模型,而这样的模型往往非常大,这就很浪费资源(计算资源,存储资源),所以就有了Adaptor。Adaptor是我们在pretrained-model里去加一些layer,然后训练的时候就训练这些layer和最后的head。这样pretrained model还是不动的。
比如下图就是一种插入Adaptor的方式。实验证明,Adaptor可以让模型调很少的参数,却达到fine-tune整个模型的效果。当然,这个Adaptor如何加,还是一个值得深究的地方。
由于pre-trained model往往很大,不同层得到的feature代表的意义也不同,所以也可以把各层的feature抽出来加权后输入head中去,加权的weights可以是模型自己去学的。
那我们为什么要用fine-tune这样的方法呢?一方面是因为trainning loss可以更快收敛,下图是对比了随机初始化训练和预训练后训练的training loss随着epoch的变化过程,虚线为随机初始化,实线为预训练的。很明显,预训练的模型效果更好。
另一方面是因为fine-tune得到的模型有更好的泛化能力。下图就是讲模型变成二维下可视化的结果。可见右边预训练的模型找到的极小值点的区域更加平缓,也就意味着泛化能力更好。