论文笔记-BERT

BERT(Bidirectional Encoder Representations from Transformers.)

对于 BERT 重点在于理解 Bidirectional 和 masked language model.

Why Bidirectional?

对于预训练的表示,单向语言模型因为无法融合下文的信息,其能力是非常有限的,尤其是对类似于 SQuAD 这样需要结合上下文信息的任务。

对比 OpenAI GPT 和 BERT. 为什么 OpenAI GPT 不能采用双向 self-attention 呢?

传统的语言模型的定义,计算句子的概率: \[P(S)=p(w_1,w_2, ..., w_n)=p(w1)p(w_2|w_1)...p(w_n|w_1...w_{n-1})=\prod_{i=1}^m p(w_i|w_1...w_{i-1})\]

前向 RNN 语言模型: \[P(S)=\prod_{i=1}^m p(w_i|w_1...w_{i-1})\]

也就是当前词的概率只依赖前面出现词的概率。

后向 RNN 语言模型
\[P(S)=\prod_{i=1}^m p(w_i|w_{i+1}...w_{m})\]

也就是当前词的概率只依赖后面出现的词的概率。

ELMo 就是这样的双向语言模型(BiLM)

但是 RNN 相比 self-attention 对上下文信息 (contextual information)的利用相对有限,而且 ELMo 只能是一层双向,并不能使用多层。其原因和 GPT 无法使用 双向 编码的原因一样。

对于 GPT 如果它使用双向,那么模型就能准确的学到到句子中的下一个词是什么,并能 100% 的预测出下一个词。比如 "I love to work on NLP." 在预测 love 的下一个词时,模型能看到 to,所以能很快的通过迭代学习到 "to" 100% 就是 love 的下一个词。所以,这导致模型并不能学到想要的东西(句法、语义信息)。

那么 BERT 是怎么处理双向这个问题的呢? 它改变了训练语言模型的任务形式。提出了两种方式 "masked language model" and "next sentence generation". 再介绍这两种训练方式之前,先说明下输入形式。

Input representation

  • position embedding: 跟 Transformer 类似
  • sentence embedding, 同一个句子的词的表示一样,都是 \(E_A\)\(E_B\). 用来表示不同的句子具有不同的含义
  • 对于 [Question, Answer] 这样的 sentence-pairs 的任务,在句子末尾加上 [SEP].
  • 对于文本分类这样的 single-sentence 的任务,只需要加上 [CLS], 并且 sentence embedding 只有 \(E_A\).

masked language model

何为 "masked LM"? idea 来源于 closed tasked. 原本的语言模型是预测所有语料中的下一个词,而 MLM 是在所有的 tokens 中随机选取 15% 的进行 mask,然后只需要预测被 mask 的词。这样以来,就能训练双向语言模型了。

但是存在一个问题,这样 pre-training 训练出来的语言模型并不能拿去做 fine-tune. 原因是在 fine-token 中从来没有见过 <MASK> 这个词。作者采用这样的策略:

具体的操作,以 "My dog is hairy" 为例,mask "hairy" 这个词:

  • "My dog is <MASK>". 80% 被 代替
  • "My dog is apple". 10% 被一个随机的 token 代替
  • "My dog is hairy". 10% 保持原来的样子

为什么不用 <MASK> 代替所有的 token?

If the model had been trained on only predicting ‘<MASK>’ tokens and then never saw this token during fine-tuning, it would have thought that there was no need to predict anything and this would have hampered performance. Furthermore, the model would have only learned a contextual representation of the ‘<MASK>’ token and this would have made it learn slowly (since only 15% of the input tokens are masked). By sometimes asking it to predict a word in a position that did not have a ‘<MASK>’ token, the model needed to learn a contextual representation of all the words in the input sentence, just in case it was asked to predict them afterwards.
如果模型在预训练的时候仅仅只预测 <MASK>, 然后在 fine-tune 的时候从未见过 <MASK> 这个词,那么模型就不需要预测任何词,在 fine-tune 时会影响性能。
更严重的是,如果仅仅预测 <MASK>, 那么模型只需要学习 <MASK> 的上下文表示,这会导致它学习的很慢。
如果让模型在某个位置去预测一个不是 <MASK> 的词,那么模型就需要学习所有 tokens 的上下文表示,因为万一需要预测这个词呢。

只需要 random tokens 足够吗?为什么还需要 10% 的完整的 sentence?

Well, ideally we want the model’s representation of the masked token to be better than random. By sometimes keeping the sentence intact (while still asking the model to predict the chosen token) the authors biased the model to learn a meaningful representation of the masked tokens.
使得模型具有偏置,更倾向于获得有意义的 masked token.

在知乎上问了这个问题,大佬的回复跟这篇 blog 有点差异,但实际上意思是一样的:

总结下:
为什么不能完全只有 <MASK> ? 如果只有 <MASK>, 那么这个预训练模型是有偏置的,也就是学到一种方式,用上下文去预测一个词。这导致在 fine-tune 时,会丢一部分信息,也就是知乎大佬第一部分所说的。

所以加上 random 和 ture token 是让模型知道,每个词都是有意义的,除了上下文信息,还要用到它本身的信息,即使是 <MASK>. 也就是知乎上说的,提取这两方面的信息。

再回过头,从语言模型的角度来看,依然是需要预测每一个词,但是绝大多数词它的 cross entropy loss 会很小,而主要去优化得到 <MASK> 对应的词。而 random/true token 告诉模型,你需要提防每一个词,他们也需要好好预测,因为他们不一定就是对的。

感谢知乎大佬!

random tokens 会 confuse 模型吗?

不会, random tokens 只占 15% * 10% = 1.5%. 这不会影响模型的性能。

还有一个问题, <MASK> 所占的比例很小,主要优化对象迭代一次对整个模型影响会很小,因而需要更多次迭代.

next sentence generation

对于下游是 Question Answering(QA), Natural Language Inference(NLI) 这样需要理解句子之间的相关性的任务,仅仅通过语言模型并不能获得这方面的信息。为了让模型能够理解句子之间的关系,作者提出了一个 binarized next sentence prediction.

具体方式是:

50% 是正确的相邻的句子。 50% 是随机选取的一个句子。这个任务在预训练中能达到 97%-98% 的准确率,并且能很显著的提高 QA NLI 的任务。

pre-training procudure

作者预训练使用的语料:BooksCorpus (800M words),English Wikipedia (2,500M words)。 使用文档级别的语料很关键,而不是 shffule 的句子级别的语料,这样可以获得更长的 sentence.

获得训练样本:从预料库中抽取句子对,其中 50% 的两个句子之间是确实相邻的,50% 的第二个句子是随机抽取的。具体操作看代码吧

  • batch_size 256.
  • 每一个 sentences 对: 512 tokens
  • 40 epochs
  • Adam lr=1e-4, \(\beta_1=0.9\), \(\beta_2=0.999\), L2 weight decay 0.01
  • learning rate warmup 10000 steps
  • 0.1 dropout
  • gelu instead of relu

Fine-tune procedure

sequence-level tasks

  • 比如 sentences pairs 的 Quora Question Pairs(QQP) 预测两个句子之间语义是否相同。如下图中(a).
  • 如果是 single sentence classification 比如 Stanford Sentiment Treebank(SST-2)和 Corpus of Linguistic Acceptability(CoLA)这种分类问题。如下图(b)

只需要输出 Transformer 最后一层的隐藏状态中的第一个 token,也就是 [CLS]. 然后接上一个全链接映射到相应的 label 空间即可。

fine-tune 时的超参数跟 pre-training 时的参数大致相同。但是训练速度会很快

  • Batch size: 16, 32
  • Learning rate (Adam): 5e-5, 3e-5, 2e-5
  • Number of epochs: 3, 4

语料库越大,对参数的敏感度越小。

token-level tasks.

对于token-level classification(例如NER),取所有token的最后层transformer输出,喂给softmax层做分类。

如何使用 BERT

文本分类

https://github.com/huggingface/pytorch-pretrained-BERT/blob/master/examples/run_classifier.py

主要涉及到两个 类:
- 数据预处理
- 预训练模型加载

1
2
3
4
5
6
7
8
9
from pytorch_pretrained_bert import BertTokenizer, BertForSequenceClassification, BertConfig, BertAdam, PYTORCH_PRETRAINED_BERT_CACHE

tokenizer = BertTokenizer.from_pretrained(args.bert_model, do_lower_case=args.do_lower_case)
tokenizer = BertTokenizer.from_pretrained("./pre_trained_models/bert-base-uncased-vocab.txt")

model = BertForSequenceClassification.from_pretrained('bert-base-uncased',
cache_dir=PYTORCH_PRETRAINED_BERT_CACHE / 'distributed_{}'.format(args.local_rank),
num_labels = num_labels)
model = BertForSequenceClassification.from_pretrained("pre_trained_models/bert-base-uncased.tar.gz", num_labels=2)

其中 bert-base-uncased 可以分别用具体的 词表文件 和 模型文件 代替。从源代码中提供的链接下载即可。

数据处理

1
2
3
4
from pytorch_pretrained_bert import BertTokenizer, BertForSequenceClassification, BertConfig, BertAdam, PYTORCH_PRETRAINED_BERT_CACHE

tokenizer = BertTokenizer.from_pretrained(args.bert_model, do_lower_case=args.do_lower_case)
tokenizer = BertTokenizer.from_pretrained("./pre_trained_models/bert-base-uncased-vocab.txt")

前一种方式是根据代码中提供的 url 去下载词表文件,然后缓存在默认文件夹下 /home/panxie/.pytorch_pretrained_bert 。后者是直接下载词表文件后,放在本地。相对来说,后者更方便。

这部分代码相对比较简单,根据自己的任务,继承 DataProcessor 这个类即可。

作为模型的输入,features 主要包括三个部分:
- input_ids 是通过词典映射来的
- input_mask 在 fine-tune 阶段,所有的词都是 1, padding 的是 0
- segment_ids 在 text_a 中是 0, 在 text_b 中是 1, padding 的是 0

这里对应了前面所说的,input_idx 就是 token embedding, segment_ids 就是 Sentence Embedding. 而 input_mask 则表示哪些位置被 mask 了,在 fine-tune 阶段都是 1.

加载预训练模型

1
2
3
4
!tar -tf pre_trained_models/bert-base-uncased.tar.gz

./pytorch_model.bin
./bert_config.json

下载好的文件包中含有两个文件,分别是 config 信息,以及模型参数。

如果不用具体的文件,则需要从代码中提供的 url 下载,并缓存在默认文件夹 PYTORCH_PRETRAINED_BERT_CACHE = /home/panxie/.pytorch_pretrained_bert

作为分类任务, num_labels 参数默认为 2.

运行时会发现提取预训练模型会输出如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
12/26/2018 17:00:41 - INFO - pytorch_pretrained_bert.modeling -   
loading archive file pre_trained_models/bert-base-uncased.tar.gz
12/26/2018 17:00:41 - INFO - pytorch_pretrained_bert.modeling -
extracting archive file pre_trained_models/bert-base-uncased.tar.gz to temp dir /tmp/tmpgm506dcx
12/26/2018 17:00:44 - INFO - pytorch_pretrained_bert.modeling -
Model config {
"attention_probs_dropout_prob": 0.1,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
"type_vocab_size": 2,
"vocab_size": 30522
}

12/26/2018 17:00:45 - INFO - pytorch_pretrained_bert.modeling -
Weights of BertForSequenceClassification not initialized from pretrained model:
['classifier.weight', 'classifier.bias']
12/26/2018 17:00:45 - INFO - pytorch_pretrained_bert.modeling -
Weights from pretrained model not used in BertForSequenceClassification:
['cls.predictions.bias', 'cls.predictions.transform.dense.weight',
'cls.predictions.transform.dense.bias', 'cls.predictions.decoder.weight',
'cls.seq_relationship.weight', 'cls.seq_relationship.bias',
'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.LayerNorm.bias']

不得不去观察 from_pretrained 的源码:https://github.com/huggingface/pytorch-pretrained-BERT/blob/8da280ebbeca5ebd7561fd05af78c65df9161f92/pytorch_pretrained_bert/modeling.py#L448

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
missing_keys = []
unexpected_keys = []
error_msgs = []
# copy state_dict so _load_from_state_dict can modify it
metadata = getattr(state_dict, '_metadata', None)
state_dict = state_dict.copy()
if metadata is not None:
state_dict._metadata = metadata

def load(module, prefix=''):
local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {})
module._load_from_state_dict(
state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs)
for name, child in module._modules.items():
if child is not None:
load(child, prefix + name + '.')
load(model, prefix='' if hasattr(model, 'bert') else 'bert.')
if len(missing_keys) > 0:
logger.info("Weights of {} not initialized from pretrained model: {}".format(
model.__class__.__name__, missing_keys))
if len(unexpected_keys) > 0:
logger.info("Weights from pretrained model not used in {}: {}".format(
model.__class__.__name__, unexpected_keys))
if tempdir:
# Clean up temp dir
shutil.rmtree(tempdir)
return model

这部分内容解释了如何提取模型的部分参数.
missing_keys 这里是没有从预训练模型提取参数的部分,也就是 classifier ['classifier.weight', 'classifier.bias']层,因为这一层是分类任务独有的。
unexpected_keys 则是对于分类任务不需要的,但是在预训练的语言模型中是存在的。查看 BertForMaskedLM 的模型就能看到,cls 层,是专属于语言模型的,在下游任务中都需要去掉。

所以这部分代码实际上学到了如何选择预训练模型的部分参数~~棒啊!