Bert下游任务
徐徐 抱歉选手

Bert的预训练任务有两个。第一,Masked Language Model(MLM),也就是让模型预测masked word。第二,Next Sentence Prediction(NSP),也就是让模型判断句子A后面否是直接跟着句子B。执行这些任务的模块最终都会从预训练模型中移除,所需要的只是训练权重,因此这些预训练任务又被叫做fake tasks。

所谓下游任务(Downstream Task),就是在Bert等预训练模型之后接一些针对特定任务的网络结构,在训练的时候微调(fine-tune)已有参数,即可适应当前任务。

文本二分类Demo

使用SST2数据集进行文本二分类,数据格式如下:

sentence label
a stirring , funny and finally transporting re imagining of beauty and the beast and 1930s horror films 1

我们希望使用DistilbBert的预训练模型(已经训练好了权重),后接一个scikit-learn提供的二分类器,输出与label相匹配,1代表positive,0代表negative。

distilbert-bert-sentiment-classifier

使用transformer库加载预训练模型,可以使用from_pretrained()方法实例化一个预训练模型的权重与预训练模型的分词器。

1
2
3
4
5
6
7
8
9
10
import transformers as ppb
# For DistilBERT: 这里的pretrained_weights是一个字符串,代表有预训练模型的名称,稍后传入pre_trained函数,就能识别特定的预训练模型
model_class, tokenizer_class, pretrained_weights = (ppb.DistilBertModel, ppb.DistilBertTokenizer, 'distilbert-base-uncased')

## Want BERT instead of distilBERT? Uncomment the following line:
#model_class, tokenizer_class, pretrained_weights = (ppb.BertModel, ppb.BertTokenizer, 'bert-base-uncased')

# Load pretrained model/tokenizer
tokenizer = tokenizer_class.from_pretrained(pretrained_weights)
model = model_class.from_pretrained(pretrained_weights)

预训练的DistilBert会将SST2中的sentence都变成setence embedding,需要如下步骤:

  1. 将句子转变为token
  2. 在句子中添加[CLS][SEP]这样的special tokens,他们会随着模型一起训练,其实最后做分类的时候,分类的结果就是[CLS]
  3. 从embedding table中将每一个token都用对应的id替换,替换后的句向量(其实算不上句向量,这只是词向量的简单拼接,并没有语义)就能作为DistilBert的输入了。

token是怎么对应到id的?id又怎么映射到768维度的向量?

实际上bert拥有自己的词汇量,这个词汇量是一个词典dictionary,该词典的key就是每个token的id,该词典的value就是与该token对应的vector。假设bert有30k的词汇量,那么bert在做tokenize的时候就会从一个储存了30k个维度为768向量的词典中去对应该token的id。

如果某个单词不在bert的词汇量内怎么办?subword tokenization会让复杂单词先分解再去查表找id。

bert-distilbert-tokenization-2-token-ids

以上三个步骤对应到代码就只有简短的一句话:

1
2
tokenizer.encode("a visually stunning rumination on love", add_special_tokens=True)
# 这里的tokenizer是DistilBert预训练模型的一部分。

我们输入的数据是形状为2000*2的矩阵batch_1,其中2000代表有200条数据,2代表有两列,第一列index=0是sentence,第二列index=1是label。对这个矩阵做完tokenize前后的内容分别是:

1
2
3
4
5
6
# tokenize前
a stirring, funny and finally transporting re imagining of beauty and the beast and 1930s horror films
# tokenize后
[101, 1037, 18385, 1010, 6057, 1998, 2633, 18276, 2128, 16603, 1997, 5053, 1998, 1996, 6841, 1998, 5687, 5469, 3152, 102]
# tokenize后的长度
20

由于句子长度本身就不一样,tokenize后句子长短不一,因此要做padding,找出这2000条句子中tokenize后长度最长的作为总长度,不满这个长度的每条句子后面就补满0,获得padded矩阵。

1
2
3
[101 1037 18385 1010 6057 1998 2633 18276 2128 16603 1997 5053 1998 1996 6841 1998 5687 5469 3152 102 0 0 0 0 0 0 0     0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
# padding后的长度
59

但是padding后的张量不能直接传入BERT,这会影响attention的计算结果,因此我们要创建一个mask矩阵,让预训练模型忽略padding的部分。这个attention-mask矩阵就是一个维度和padded矩阵完全相同的1-0矩阵。

1
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

输入进预训练模型的句子,最终输出的每一个token都是同一个emb_size=768,且padded后的长度59,那么一个句子就要用59个维度位768的向量来表示。

bert-model-input-output-1

这样的句子有2000个,也就是2000个768行66列的矩阵从上往下堆叠在一起。顶层标有数字的从前向后的那一行,就是训练好的CLS,它可以看作是整个句子的句向量;它的后一行就是第一个句子的第一个单词的词向量,维度为768。

bert-output

阅读理解Demo

这里引入了segment embedding的概念。参考自文章Question Answering with a Fine-Tuned BERT,youtube也有视频,colab中也有代码。

在使用Bert做阅读理解的时候,句子对输入由问题和包含答案的文本组成,形式为[CLS] query [SEP] reference,这一部分输入称为token embeddings。还有segment embeddings,主要用于区分两种句子,有0和1组成;对于只有一个句子的任务,用来区分真正的句子和句子padding的内容。还有position embeddings,用于保留每个token的位置信息。

image-20210409152126275

BERT在训练的时候,input embeddings就是token embeddings, segmentation embeddings和position embeddings的总和。

BERT的output 是每个token的encoding vector。

tokenizer方式总结

在跟着二文本分类的Demo做的时候我就发现,图中演示的Tokenize的过程,并不是直接将句子变成words,也就是说token并不等于words,比如rumination就被拆分成了rum和##ination两个部分。token只是按照某种规则将句子拆分多个部分的结果——spliting a text into smaller chunks。

一种最简单的tokenize就是利用空格分词,将标点符号也作为一个token;如果直接把puctuation标点与单词放在一起,就会出现同一个单词有不同的embedding的问题(如Transofrmer? & Transformer.)。这被叫做space and punctuation tokenization/词粒度。

还有一种方式就是利用字母分词——tokenize on characters/字粒度。虽然这样很简单且能降低复杂度,但是这对模型学习语义没有好处。

Transformer中用到的tokenize的方式就是取上面两种方法的混合—— a hybrid between word-level and character-level tokenization called subword tokenization,也就是subword tokenization/subword粒度。

subword tokenization的准则就是常用词不会被分为更小的chunk,少见词应当被分解为有意义的subword。例如annoyingly可以被理解为annoying和ly这两个subword的组成。

例如BertTokenizer会将“I have a new GPU”变为如下分词。

1
2
3
4
>>> from transformers import BertTokenizer
>>> tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
>>> tokenizer.tokenize("I have a new GPU!")
["i", "have", "a", "new", "gp", "##u", "!"]

其中,GPU被分解为gp和##u,##代表了这个subword前面要跟着其他subword才能组成有意义的单词。

所有的subword都以##开头。

为了更好地理解BERT Vocabulary,可以参考这个代码笔记Inspect BERT Vocabulary。在加载开源的BERT预训练模型的时候可以载入与该模型匹配的Tokenizer,通过for token intokenizer.vocab.keys()循环输出就可以得到BERT的所有词汇量,共计30552个。组成如下:1-999号分别是保留位,除了特殊意义的1-[PAD],101-[UNK],102-[CLS],103-[SEP],104-[MASK],其他都是unused;1000-1996号都是单个char;从1997开始的所有单词似乎都是按照出现频率由高到低排序的,如the就在1997号。

参考

A Visual Guide to Using BERT for the First Time

Summary of the tokenizers

NLP领域中的token和tokenization到底指的是什么? - ICY星星的回答 - 知乎

Question Answering with a Fine-Tuned BERT

Inspect BERT Vocabulary

  • 本文标题:Bert下游任务
  • 本文作者:徐徐
  • 创建时间:2021-04-08 17:22:35
  • 本文链接:https://machacroissant.github.io/2021/04/08/bert-downstream-task/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论