谷歌官宣:全面超越人类的最强NLP预训练模型BERT开源了!

什么是 BERT?

BERT 是预训练语言表示的方法,也即我们基于大型文本语料库(如*)训练通用的“语言理解”模型,然后将模型用于下游的 NLP 任务(如问答) 。BERT 比之前的方法更优,因为它是第一个用于预训练 NLP 的无监督、深度双向系统。

无监督意味着 BERT 只使用纯文本语料库进行训练,这点很重要,因为网络上有很多公开的纯文本数据。

预训练表示也可以是无上下文或有上下文的,有上下文的表示又可以是单向或双向的。word2vec 或 GloVe 这类无上下文模型为词汇表中的每个单词生成单个“词袋”表示,因此“bank”与“bank deposit”和“river bank”具有相同的表示。相反,上下文模型基于句子中其他单词生成每个单词的表示。

BERT 建立在最近的预训练上下文表示工作的基础之上,包括半监督序列学习、生成预训练、ELMo 和 ULMFit,这些模型都是单向或浅双向的。也就是说,每个单词仅使用左侧(或右侧)的单词进行语境化。例如,在“I made a bank deposit”这个句子中,“bank”的单向表示基于“I made a”而不是“deposit”。之前的一些工作以“浅层”的方式将来自左上下文和右上下文模型的表示结合在一起,而 BERT 使用左右上下文来表示“bank”——从深度神经网络的最底部开始,所以它是深度双向的。

BERT 使用一种简单的方法:我们将输入的 15%的单词遮蔽掉,让整个序列通过深度双向 Transformer 编码器,然后仅预测被遮蔽的单词。例如:


Input: the man went to the [MASK1] . he bought a [MASK2] of milk.
Labels: [MASK1] = store; [MASK2] = gallon

为了学习句子之间的关系,我们还训练一个简单的任务:给定两个句子 A 和 B,那么 B 是 A 的下一个句子还是只是语料库中的一个随机句子?

Sentence A: the man went to the store .
Sentence B: he bought a gallon of milk .
Label: IsNextSentence


Sentence A: the man went to the store .
Sentence B: penguins are flightless .
Label: NotNextSentence

然后,我们基于大型语料库(Wikipedia + BookCorpus)训练了一个模型(12 层到 24 层 Transformer),花了很长一段时间(1 百万个更新步骤),那就是 BERT。

使用 BERT 需要两个阶段:预训练和微调。

预训练的成本相当高(在 4 到 16 个 Cloud TPU 上训练需要 4 天时间),而且对于每一种语言,都是一次性的程序(目前的模型仅限英语,更多语言模型将在不久的将来发布)。我们正在发布一些预训练的模型,这些模型是在 Google 上预先训练过的。大多数 NLP 研究人员不需要从头开始训练自己的模型。

微调的成本较低。论文中提到的所有结果都可以在单个 Cloud TPU 上进行训练,最多花 1 个小时,或者在 GPU 上花几个小时即可。

预训练模型

我们在论文中发布了 BERT-Base 和 BERT-Large 模型。Uncased 是指文本在 WordPiece 标记化之前已经转换成小写,例如“John Smith”转换成“john smith”。Uncased 模型还移除了重音标记。Cased 是指保留真实的大小写和重音标记。通常,除非你的任务需要大小写(例如,命名实体识别或词性标注),否则 Uncased 模型会更好。

这些模型都是基于 Apache 2.0 许可进行发行。

模型链接:

  • BERT-Base,Uncased:https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-12_H-768_A-12.zip;

  • BERT-Large,Uncased:https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-24_H-1024_A-16.zip;

  • BERT-Base,Cased:https://storage.googleapis.com/bert_models/2018_10_18/cased_L-12_H-768_A-12.zip

  • BERT-Large,Cased:尚不可用,需要重新生成。

每个.zip 文件包含三个项目:

  • 包含预训练的权重(实际上是 3 个文件)的 TensorFlow 检查点(bert_model.ckpt)。

  • 用于将 WordPiece 映射到 word id 的词汇文件(vocab.txt)。

  • 配置文件(bert_config.json),指定模型的超参数。

使用 BERT 进行微调

微调示例使用了 BERT-Base,它应该能够使用给定的超参数在配备至少 12GB RAM 的 GPU 上运行。

在 Cloud TPU 上进行微调

下面的大多数示例都假设你将使用 Titan X 或 GTX 1080 这样的 GPU 在本地计算机上运行训练 / 评估。

不过,如果你可以访问 Cloud TPU,只需将以下标志添加到 run_classifier.py 或 run_squad.py:


--use_tpu=True \
 --tpu_name=$TPU_NAME

在 Cloud TPU 上,预训练模型和输出目录需要在 Google Cloud Storage 上。例如,如果你有一个名为 some_bucket 的桶,则可以使用以下标志:


--output_dir=gs://some_bucket/my_output_dir/

解压缩的预训练模型文件也可以在 Google Cloud Storage 文件夹 gs://bert_models/2018_10_18 中找到。例如:


export BERT_BASE_DIR=gs://bert_models/2018_10_18/uncased_L-12_H-768_A-12
句子(和句子对)分类任务

在运行这个示例之前,你必须通过这个脚本(https://gist.github.com/W4ngatang/60c2bdb54d156a41194446737ce03e2e)下载 GLUE 数据(https://gluebenchmark.com/tasks),并将其解压缩到一个目录中(目录变量可以设置为 $GLUE_DIR)。接下来,下载 BERT-Base 检查点并将其解压缩到另一个目录中(目录变量可以设置为 $BERT_BASE_DIR)。

这个示例针对微软 Research Paraphrase Corpus(MRPC)语料库对 BERT-Base 进行微调,这个语料库仅包含 3,600 个样本,在大多数 GPU 上只需要几分钟进行微调。


export BERT_BASE_DIR=/path/to/bert/uncased_L-12_H-768_A-12
export GLUE_DIR=/path/to/glue

python run_classifier.py \
 --task_name=MRPC \
 --do_train=true \
 --do_eval=true \
 --data_dir=$GLUE_DIR/MRPC \
 --vocab_file=$BERT_BASE_DIR/vocab.txt \
 --bert_config_file=$BERT_BASE_DIR/bert_config.json \
 --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
 --max_seq_length=128 \
 --train_batch_size=32 \
 --learning_rate=2e-5 \
 --num_train_epochs=3.0 \
 --output_dir=/tmp/mrpc_output/

你应该可以看到这样的输出:


***** Eval results *****
 eval_accuracy = 0.845588
 eval_loss = 0.505248
 global_step = 343
 loss = 0.505248

dev 集的准确率为 84.55%。MRPC 在 dev 集准确率方面有很大的差异,即使是从相同的预训练检查点开始。如果重新运行几次(确保要指向不同的 output_dir),你应该会看到结果在 84%到 88%之间。

其他一些预训练模型是在 run_classifier.py 中实现的,所以应该可以直接按照这些示例将 BERT 用于任何单句或句子对分类任务。

SQuAD

斯坦福问答数据集(SQuAD)是一个非常流行的问答基准数据集。BERT(在发布时)在 SQuAD 上获得了最好的结果,几乎没有进行特定任务的网络架构修改或数据增强。不过,它确实需要半复杂数据预处理和后处理来处理 SQUAD 上下文段落的可变长度性质,以及用于 SQuAD 训练的字符级答案注解。run_squad.py 实现并记录了处理过程。

要在 SQuAD 上运行训练,首先需要下载这个数据集。SQuAD 网站(https://rajpurkar.github.io/SQuAD-explorer/)不再提供 v1.1 数据集的链接,一些必要的文件可以在这里找到:

  • train-v1.1.json(https://rajpurkar.github.io/SQuAD-explorer/dataset/train-v1.1.json)

  • dev-v1.1.json(https://rajpurkar.github.io/SQuAD-explorer/dataset/dev-v1.1.json)

  • evaluate-v1.1.py(https://github.com/allenai/bi-att-flow/blob/master/squad/evaluate-v1.1.py)

将这些下载到某个目录(变量可以设置为 $SQUAD_DIR)。

由于内存限制,目前无法在 12GB-16GB 的 GPU 上再现最好的 SQuAD 结果。但是,可以使用下面这些超参数在 GPU 上训练 BERT-Base 模型:


python run_squad.py \
 --vocab_file=$BERT_BASE_DIR/vocab.txt \
 --bert_config_file=$BERT_BASE_DIR/bert_config.json \
 --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
 --do_train=True \
 --train_file=$SQUAD_DIR/train-v1.1.json \
 --do_predict=True \
 --predict_file=$SQUAD_DIR/dev-v1.1.json \
 --train_batch_size=12 \
 --learning_rate=5e-5 \
 --num_train_epochs=2.0 \
 --max_seq_length=384 \
 --doc_stride=128 \
 --output_dir=/tmp/squad_base/

dev 集预测结果将保存到 output_dir 目录的一个名为 predictions.json 的文件中:


python $SQUAD_DIR/evaluate-v1.1.py $SQUAD_DIR/dev-v1.1.json ./squad/predictions.json

应该产生这样的输出:


{"f1": 88.41249612335034, "exact_match": 81.2488174077578}

你应该看到论文中提到的 88.5%的 F1。

如果你可以访问 Cloud TPU,那么就可以训练 BERT-Large 模型。下面的超参数(与论文中稍有不同)可以获得大约 90.5%-91.0%的 F1(仅在 SQuAD 上训练):


python run_squad.py \
 --vocab_file=$BERT_LARGE_DIR/vocab.txt \
 --bert_config_file=$BERT_LARGE_DIR/bert_config.json \
 --init_checkpoint=$BERT_LARGE_DIR/bert_model.ckpt \
 --do_train=True \
 --train_file=$SQUAD_DIR/train-v1.1.json \
 --do_predict=True \
 --predict_file=$SQUAD_DIR/dev-v1.1.json \
 --train_batch_size=48 \
 --learning_rate=5e-5 \
 --num_train_epochs=2.0 \
 --max_seq_length=384 \
 --doc_stride=128 \
 --output_dir=gs://some_bucket/squad_large/ \
 --use_tpu=True \
 --tpu_name=$TPU_NAME

例如,使用这些参数随机进行一次会产生以下 dev 得分:


{"f1": 90.87081895814865, "exact_match": 84.38978240302744}
使用 BERT 提取固定的特征向量

在某些情况下,相比对整个预训练模型进行端到端的微调,获得预训练的上下文嵌入可能会更好,这些嵌入是预训练模型隐藏层生成的每个输入标记的固定上下文表示。

例如,我们可能会这样使用 extract_features.py 脚本:


# Sentence A and Sentence B are separated by the ||| delimiter.
# For single sentence inputs, don't use the delimiter.
echo 'Who was Jim Henson ? ||| Jim Henson was a puppeteer' > /tmp/input.txt

python extract_features.py \
 --input_file=/tmp/input.txt \
 --output_file=/tmp/output.jsonl \
 --vocab_file=$BERT_BASE_DIR/vocab.txt \
 --bert_config_file=$BERT_BASE_DIR/bert_config.json \
 --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
 --layers=-1,-2,-3,-4 \
 --max_seq_length=128 \
 --batch_size=8

这将创建一个 JSON 文件,其中包含由 layers 指定的每个 Transformer 层的 BERT 激活(-1 是 Transformer 的最后隐藏层,并以此类推)。

请注意,这个脚本将生成非常大的输出文件(默认情况下,每个输入标记大约 15kb)。

如果你需要对齐原始单词和标记化单词,请参阅下面的标记化部分。

标记化(tokenization)

对于句子(或句子对)任务,标记化是非常简单的。只需要遵循 run_classifier.py 和 extract_features.py 中的示例代码即可。句子级任务的基本流程:

  1. 实例化 tokenizer = tokenization.FullTokenizer;

  2. 使用 tokens = tokenizer.tokenize(raw_text)对原始文本进行标记;

  3. 截断到最大序列长度(最多可以使用 512,但处于内存和速度方面的考虑,最好使用短一点的);

  4. 在正确的位置添加 [CLS] 和 [SEP] 标记。

单词级和 span 级的任务(例如 SQuAD 和 NER)会复杂一些,因为你需要对齐输入文本和输出文本。SQuAD 是一个特别复杂的例子,因为输入标签是基于字符的,而 SQuAD 段落通常比我们的最大序列长度要长。请参阅 run_squad.py 中的代码,了解我们如何处理这个问题。

在我们描述处理单词级任务的一般方法之前,需要先了解我们的标记器都做了哪些事情。它有三个主要步骤:

文本规范化:将所有空白字符转换为空格,(对于 Uncased 模型)将输入转换为小写并删除重音标记。例如,“John Johanson’s”变成“john johanson’s”。

标点符号拆分:拆分两侧的所有标点符号(即在所有标点符号周围添加空格)。标点符号是指具有 P* Unicode 内容或任何非字母 / 数字 / 空格 ASCII 字符。例如,“johanson’s,”变成“john johanson ' s ,”。

WordPiece 标记化:对上一步骤的输出进行空格标记化,并对每个标记进行 WordPiece 标记化。例如,“john johanson ' s , ”变成“john johan ##son ' s ,”。

这个方案的优点是它与大多数现有的英语标记符“兼容”。例如,假设你有一个词性标记任务,如下所示:


Input:  John Johanson 's   house
Labels: NNP  NNP      POS NN

标记化输出如下所示:


Tokens: john johan ##son ' s house

如果你有一个带有单词级注解的预标记表示,你可以单独标记每个输入单词,并对齐原始单词和标记化单词:


### Input
orig_tokens = ["John", "Johanson", "'s",  "house"]
labels      = ["NNP",  "NNP",      "POS", "NN"]

### Output
bert_tokens = []

# Token map will be an int -> int mapping between the `orig_tokens` index and
# the `bert_tokens` index.
orig_to_tok_map = []

tokenizer = tokenization.FullTokenizer(
   vocab_file=vocab_file, do_lower_case=True)

bert_tokens.append("[CLS]")
for orig_token in orig_tokens:
 orig_to_tok_map.append(len(bert_tokens))
 bert_tokens.extend(tokenizer.tokenize(orig_token))
bert_tokens.append("[SEP]")

# bert_tokens == ["[CLS]", "john", "johan", "##son", "'", "s", "house", "[SEP]"]
# orig_to_tok_map == [1, 2, 4, 6]

现在 orig_to_tok_map 可用于将 labels 投影到标记化表示。

有一些常见的英语标记化方案会导致 BERT 预训练之间的轻微不匹配。例如,如果输入标记化分离了缩略形式,如“do n’t”,就会出现不匹配。如果有可能,你应该预处理数据,将这些数据转换回原始文本,如果不行,这种不匹配可能也不是什么大问题。

使用 BERT 进行预训练

我们正在尝试在任意文本语料库上进行“masked LM”和“下一个句子预测”。请注意,这些代码不同于论文中所述的代码(原始代码是用 C++ 编写的,有一些额外的复杂性),但可以生成论文中所述的预训练数据。

输入是纯文本文件,一行一个句子。文档使用空行进行分隔。输出是一组序列化为 TFRecord 文件格式的 tf.train.Example。

脚本将整个输入文件的样本保存在内存中,对于大型数据文件,需要将其分片并多次调用脚本。

max_predictions_per_seq 是每个序列的 masked LM 预测的最大数量。你应该将其设置为 max_seq_length * masked_lm_prob。


python create_pretraining_data.py \
 --input_file=./sample_text.txt \
 --output_file=/tmp/tf_examples.tfrecord \
 --vocab_file=$BERT_BASE_DIR/vocab.txt \
 --do_lower_case=True \
 --max_seq_length=128 \
 --max_predictions_per_seq=20 \
 --masked_lm_prob=0.15 \
 --random_seed=12345 \
 --dupe_factor=5

如果你是从头开始进行预训练,请不要包含 init_checkpoint。模型配置(包括词汇大小)在 bert_config_file 中指定。演示代码仅预训练少量步骤(20 个),但在实际当中你可能需要将 num_train_steps 设置为 10000 步或更多。传给 run_pretraining.py 的 max_seq_length 和 max_predictions_per_seq 参数必须与 create_pretraining_data.py 相同。


python run_pretraining.py \
 --input_file=/tmp/tf_examples.tfrecord \
 --output_dir=/tmp/pretraining_output \
 --do_train=True \
 --do_eval=True \
 --bert_config_file=$BERT_BASE_DIR/bert_config.json \
 --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
 --train_batch_size=32 \
 --max_seq_length=128 \
 --max_predictions_per_seq=20 \
 --num_train_steps=20 \
 --num_warmup_steps=10 \
 --learning_rate=2e-5

这将产生如下输出:


***** Eval results *****
 global_step = 20
 loss = 0.0979674
 masked_lm_accuracy = 0.985479
 masked_lm_loss = 0.0979328
 next_sentence_accuracy = 1.0
 next_sentence_loss = 3.45724e-05

请注意,由于 sample_text.txt 文件非常小,这个示例将在几个步骤之内出现过拟合,并产生不切实际的高准确率。

预训练数据

我们将无法发布论文中使用的预处理数据集。 对于 Wikipedia,建议下载最新的转储(https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles.xml.bz2),使用 WikiExtractor.py 提取文本,然后进行必要的清理将其转换为纯文本。

可惜的是,收集 BookCorpus 的研究人员不再提供公开下载。 Guttenberg 数据集(https://web.eecs.umich.edu/~lahiri/gutenberg_dataset.html)是公开可用的一个较小(2 亿个单词)的旧书集合。

Common Crawl(http://commoncrawl.org/)是另一个非常大的文本集合,但你可能需要进行预处理和清理才能提取可用的语料库以进行 BERT 预训练。

在 Colab 中使用 BERT

如果你想将 BERT 与 Colab 一起使用,可以从“BERT FineTuning with Cloud TPU”(https://colab.sandbox.google.com/github/tensorflow/tpu/blob/master/tools/colab/bert_finetuning_with_cloud_tpus.ipynb)开始。在撰写本文时(2018 年 10 月 31 日),Colab 用户可以完全免费访问一个 Cloud TPU。每个用户可以使用一个,可用性有限,需要一个带有存储空间的 Google Cloud Platform 帐户,并且在未来可能无法再使用。


上一篇:Bert 改进: 如何融入知识


下一篇:使用bert或者xlnet做预测类的事情