谷歌官宣:全面超越人类的最强NLP预训练模型BERT开源了!
公众号/AI前线
来源 | Google Research GitHub
编译 | 无明、Natalie
编辑 | Natalie
AI 前线导读: 近日,谷歌 AI 的一篇 NLP 论文引起了社区极大的关注与讨论,被认为是 NLP 领域的极大突破。谷歌大脑研究科学家 Thang Luong Twitter 表示,这项研究开启了 NLP 领域的新时代。该论文介绍了一种新的语言表征模型 BERT——来自 Transformer 的双向编码器表征。BERT 是首个在大批句子层面和 token 层面任务中取得当前最优性能的基于微调的表征模型,其性能超越许多使用任务特定架构的系统,刷新了 11 项 NLP 任务的当前最优性能记录。
刚刚,谷歌正式将其开源!这意味着所有NLP从业者都可以试用这个强大的NLP预训练模型并结合到自己的工作中。
更多优质内容请关注微信公众号“AI 前线”(ID:ai-front)
首先附上开源代码传送门:
https://github.com/google-research/bert
原论文链接:
https://arxiv.org/abs/1810.04805
该开源项目亮点如下:
- 独立的 TensorFlow 代码,有简单的 API 且无依赖关系。
- 链接到论文中的 BERT-Base 和 BERT-Large 预训练版本。
- 一键复制论文中的 MultiNLI 和 SQuAD v1.1 结果。
- 包含预训练数据生成和训练的代码。
- 可以链接到 Colab,从而使用免费的云端 TPU 运行 BERT。
几个常见问题解答:
- 我们计划很快发布一个多语言模型(在 60 种语言上训练的大型共享 WordPiece 词汇,并对中文做了特殊处理)。
- 现有的基于 PyTorch(或其他框架)的版本无法与检查点兼容(因为如果没有我们的代码,这是不可能做到的)。我们希望有人能够创建一个 op-for-op 重新实现 modeling.py,以便创建一个与我们的检查点兼容的 PyTorch 模型,尤其是我们计划在将来发布更多的检查点(例如,多语言模型)。
- 我们还没有在 SQuAD 2.0 上运行过这个模型,我们想把它作为一项练习留给读者来完成:)
- 你不一定要在云端 TPU 上进行训练,但是在 GPU 上训练 BERT-Large 模型可能会出现严重的内存不足问题。在 GPU 上运行 BERT-Base 通常可以正常工作(与我们在论文中使用的相比,你可能需要降低 Batch Size,但如果你同时也对学习率做了调整,那么最终结果应该是类似的)。我们正在尝试找出在 GPU 上运行 BERT-Large 的最佳解决方法。
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 .
Labe
l:
IsNextSentence
Sentence A: the man went
to
the store .
Sentence B: penguins are flightless .
Labe
l:
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-Base,它应该能够使用给定的超参数在配备至少 12GB RAM 的 GPU 上运行。
下面的大多数示例都假设你将使用 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)是一个非常流行的问答基准数据集。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
}
在某些情况下,相比对整个预训练模型进行端到端的微调,获得预训练的上下文嵌入可能会更好,这些嵌入是预训练模型隐藏层生成的每个输入标记的固定上下文表示。
例如,我们可能会这样使用 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)。
如果你需要对齐原始单词和标记化单词,请参阅下面的标记化部分。
对于句子(或句子对)任务,标记化是非常简单的。只需要遵循 run_classifier.py 和 extract_features.py 中的示例代码即可。句子级任务的基本流程:
- 实例化 tokenizer = tokenization.FullTokenizer;
- 使用 tokens = tokenizer.tokenize(raw_text)对原始文本进行标记;
- 截断到最大序列长度(最多可以使用 512,但处于内存和速度方面的考虑,最好使用短一点的);
- 在正确的位置添加 [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
标记化输出如下所示:
Token
s:
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_token
s:
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”,就会出现不匹配。如果有可能,你应该预处理数据,将这些数据转换回原始文本,如果不行,这种不匹配可能也不是什么大问题。
我们正在尝试在任意文本语料库上进行“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 预训练。
如果你想将 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 帐户,并且在未来可能无法再使用。
英文原文:
https://github.com/google-research/bert