随着自然语言处理(NLP)和自然语言理解(NLU)领域的快速发展,算法越来越能够理解人类的交互方式。BERT(Bi-directional Encoder Representations from Transformers)就是这样一种算法,它激发了数据科学家对于实现高级准确性水平的新实践和开发的探索欲望。如今,BERT支撑着谷歌搜索的每一个查询,并且谷歌已经将其开源,使得开发者可以将其应用于多种任务,如:
以上是BERT可以用于的任务,而不是它实际执行的任务。在深入代码之前,让先了解BERT到底是什么,它与之前的模型有何不同,它是如何被训练的,以及有哪些变体可供选择。
BERT是“Bi-directional Encoder Representations from Transformers”的缩写,它是一种语言模型。根据其名称,可以关注三个重要的因素:
双向性意味着BERT能够从两个方向甚至词与词之间捕捉输入,这在以前的模型中是不可能的,因为它们是单向的(从左到右或从右到左),导致模型无法看到整个句子,从而产生有偏见的词表示(编码)。
BERT的主要目的是生成编码/嵌入,以便它们没有偏见,并在后续的任务中使用。为此,BERT模型由多个编码器层组成,其工作是对输入进行预处理,然后将输入编码成某种形式的嵌入,这些嵌入可以被后续的模型使用。
BERT来自变换器家族,但它只包含编码器块,并仅使用注意力和前馈层来生成段落嵌入。此外,与原始变换器模型相比,注意力头的超参数也有所不同(通常是12-16个注意力头)。
由于上述所有因素(特别是注意力头和双向性),BERT能够捕捉不同上下文中词的含义/语义,这在以前使用的最常见的模型Word2Vec中是不可能的。为了理解这一点,考虑两个句子:
1. 他们公平地玩。
2. 附近有一个集市。
在第一个句子中,“fair”意味着遵守规则,在第二个句子中,“fair”意味着一个活动。但是,像Word2Vec这样的算法会在两种情况下输出相同的嵌入,即它不理解句子的上下文,因为它只能一次处理一个输入,或者从右到左或从左到右并行处理(不允许看到整个句子)。
另一方面,BERT并行处理输入,可以看到整个句子,并使用其拥有的注意力头,它可以关注它认为对预测词影响最大的词,从而允许更一般的嵌入和相同词的不同表示。
BERT的不同用途归功于其训练过程。通常,训练模型涉及两个步骤:预训练和微调。让更详细地了解每一个步骤:
预训练:模型在两种半监督任务上进行训练,即MLM(掩码语言模型)和NSP(下一句预测)。
掩码语言模型(MLM):在这个半监督任务中,模型的输入被掩码(空白)标记为[MASK]标记,模型的工作是预测掩码标记替换的词,然后输入到一个softmax单元,该单元的输入为词汇表,并预测输出词。
下一句预测(NSP):在这个任务中,取两个句子(A和B),使得:50%的时间B是A的下一句,50%的时间B是训练数据中的某个随机句子,模型的工作是通过进行二元分类(是下一个/不是下一个)来学习每句话之间的关系/语义含义。
微调:在训练模型之后,它可以在多种任务上进行微调,如句子-句子预测、问答、单句分类、单句标记、语言翻译、特征提取(创建嵌入)等。
BERT有两种变体:
对于大多数用例,BERT_base就足够了,并在微调上提供了良好的结果。
让开始编码。为了理解目的,将使用模型创建词嵌入,并尝试以实际方式理解前面定义的编码器层的不同方面。
如上所述,在图中,模型有两个部分:预处理和编码,幸运的是TensorFlowHub允许直接使用模型进行微调。
下载必需品:前往TensorFlowHub网站——一个可以访问和使用预训练模型的地方。向下滚动,可以看到:
preprocess_url = "https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3"
encoder_url = "https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/4"
在这里,将这些URL存储为文本在preprocess_url和encoder_url中,以供后续参考。使用这些URL,可以创建预处理器和编码器对象。为此,需要使用以下语法:
bert_preprocess_model = hub.KerasLayer(preprocess_url)
bert_encoder_model = hub.KerasLayer(encoder_url)
刚刚使用了hub.KerasLayer()将保存的模型包装为Keras层并存储在变量中。
创建数据:要创建数据,可以简单地创建一个测试句子的Python列表:
test_text = ['爱披萨', '印度是国家', '意大利是旅游的好地方']
在这里,使用了印度和意大利——国家——相同的参考,以及意大利和披萨——某种程度上相同的上下文。工作是使数据有偏见,并看看BERT的表现如何。让继续下一部分。
预处理输入:如前所述,预处理器对象现在可以作为一个函数指针使用,它可以接受test_text并输出processed_text字典:
processed_text = bert_preprocess_model(test_text)
print(processed_text.keys())
>> dict_keys(['input_mask', 'input_word_ids', 'input_type_ids'])
可以看到,模型的输入已经被处理成编码器期望的方式。基本上,它为每个词和所有输入句子创建了一个掩码,添加了ID和标记。更多关于这个的内容在下面的“理解键”部分给出。
编码:要生成嵌入,现在只需要将这个processed_text传递给之前创建的编码器对象(bert_encoder_model):
bert_results = bert_encoder_model(text_preprocessed)
bert_results.keys()
>> dict_keys(['default', 'sequence_output', 'pooled_output', 'encoder_outputs'])
# input_mask
print(text_preprocessed['input_mask'])