bert是google在NLP方面的一个重要的工作,可以说一定程度上改变了NLP领域的研究方式。bert获得了2019 NAACL的最佳长论文奖。
bert,连同之前的ELMO和GPT,可以说开创了NLP的『预训练模型时代』。这3个模型,总体的思想都是采用通用模型架构在语料库(corpus)上预训练(pre-training);然后针对具体的NLP任务,在通用模型架构上增加几层,固定通用模型的参数,微调(fine-tuning)增加的若干层参数。区别在于,3个模型在通用模型选型和一些训练技术上有所不同。
ELMO | GPT | bert |
---|---|---|
bi-direction LSTM | single-direction transformer | bi-direction transformer |
bert模型,凭借其出色的performance,成为上述3个基于预训练的模型的代表。
这里可能要稍微插一下,目前的趋势是transformer渐渐替代以LSTM为代表的RNN模型成为NLP领域的基础模型。transformer是一种基于Attention机制的网络,由于transformer可以通过Attention将一个sentence中任何两个word联系起来,因此其建模能力强于以LSTM为代表的RNN类模型。另外,由于没有RNN的时序依赖的特点,transformer便于并行计算。具体关于transformer和Attention机制,读者可以移步别的文章,这里就不在旁逸斜出了。
最后想说的一点是,bert模型的pre-training需要消耗巨量的计算资源和计算时间,一般学校里的实验室都没有那么多计算资源可以进行bert这样的pre-training,但bert的fine-tuning和predicting相对来说消耗资源比较少,学校和个人都是可以跑的。类似CV领域中,研究者下载在ImageNet上预训练的模型参数,bert也开放了在不同语言上训练的模型供大家下载。
bert创新点主要在2个地方:
bert想要利用一个单词的前后文对单词进行预测,而不是仅仅利用单词的前文(这个motivation还是挺合理的),并在transformer模型中实现了这一点(RNN模型中,bi-direction LSTM就是干这个的)。实现方法是,随机遮住句子中的一个单词(用[Mask]
token替代),用句子里前文的单词和后文的单词预测被遮住的单词。
但这样会引入新的问题,即训练集和测试集之间有偏差,因为测试集中是不会出现[Mask]
token的。为此,作者的解决方案是,对于以15%随机概率被选中的成为[Mask]
的单词进行如下的变换:
[Mask]
(感觉就是集成了一下,算是一个训练技巧吧)
如果读者熟悉传统NLP的话,就会发现Masked LM的思想其实类似CBOW(就是与skip-gram相对应的那个),而论文中作者宣称Masked LM是受Cloze启发的。
NLP中有许多涉及sentence pair的关系的任务,例如Question-Answering和Natural Language Inference. 为此,bert在pre-training的过程中加入了对于sentence之间关系的训练。具体做法是训练集中每一个句子对(sa, sb),有50%的概率是前后文关系,50%是语料库里随机的两个句子的组合,然后用句子对进行0-1二分类(是否是前后文对)的训练。
作者做了4个数据集(GLUE, SQuAD v1.1, SQuAD v2.0, SWAG)上11个任务(GLUE数据集有MNLI, QQP, QNLI, SST-2, CoLA, STS-B, MRPC, RTE共8项任务)的实验,均取得state-of-the-art的效果,实验方法都是采样pre-trained模型在特定任务上做fine-tuning,显示出bert模型的通用性。
此外,作者还设计了消融实验证明了双向transformer以及NSP两个创新点的有效性。
下面写一下博主在中文数据集上的fine-tuning经历。总的来说,bert的github项目的文档就已经很全了,这里就当是做一个翻译和一些补充了。
由于是中文任务,因此采用bert的中文预训练模型:下载地址。虽然说是中文预训练模型,实际上是在一些中文语料库上训练的模型,不仅仅还有中文,还含有一些英文以及其他字符。具体可以解压打开中文模型,查看vocab.txt文件,里面存储了中文模型的所有token.
run_classifier.py
文件里的InputExample
类抽象了一个样本,guid
自动生成不用用户管,label
是类标,单文本任务用text_a
,文本对任务(例如QA)用text_a
和text_b
.
class InputExample(object):
"""A single training/test example for simple sequence classification."""
def __init__(self, guid, text_a, text_b=None, label=None):
"""Constructs a InputExample.
Args:
guid: Unique id for the example.
text_a: string. The untokenized text of the first sequence. For single
sequence tasks, only this sequence must be specified.
text_b: (Optional) string. The untokenized text of the second sequence.
Only must be specified for sequence pair tasks.
label: (Optional) string. The label of the example. This should be
specified for train and dev examples, but not for test examples.
"""
self.guid = guid
self.text_a = text_a
self.text_b = text_b
self.label = label
run_classifier.py
文件里的DataProcessor
类,是所有输入数据接口的基类。通过继承DataProcessor
类,我们实现自己的输入数据接口类,重载get_train_examples
, get_dev_examples
, get_test_examples
, get_labels
这4个抽象方法,自定义训练集、验证集、测试集和类标的输入格式。run_classifier.py
还提供了XnliProcessor
等几个预置的输入数据接口类可以供我们实现的时候参考。自定义的数据接口格式需要与数据集文件中数据格式一致。
class DataProcessor(object):
"""Base class for data converters for sequence classification data sets."""
def get_train_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the train set."""
raise NotImplementedError()
def get_dev_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the dev set."""
raise NotImplementedError()
def get_test_examples(self, data_dir):
"""Gets a collection of `InputExample`s for prediction."""
raise NotImplementedError()
def get_labels(self):
"""Gets the list of labels for this data set."""
raise NotImplementedError()
@classmethod
def _read_tsv(cls, input_file, quotechar=None):
"""Reads a tab separated value file."""
with tf.gfile.Open(input_file, "r") as f:
reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
lines = []
for line in reader:
lines.append(line)
return lines
bert的github项目上给出了fine-tuning的样例脚本,在该脚本的头部添加环境变量CUDA_VISIBLE_DEVICES="x"
,可以指定在第x
号GPU上运行。