2018年的10月11日,Google发布的论文《Pre-training of Deep Bidirectional Transformers for Language Understanding》,
成功在 11 项 NLP 任务中取得 state of the art 的结果,赢得自然语言处理学界的一片赞誉之声,BERT就出自该论文,
BERT模型的全称是 Bidirectional Encoder Representations from Transformers;
github:google-research/bert:用于 BERT 的 TensorFlow 代码和预训练模型
Bert 模型
Bert 只使用了 transformers 的 encode 模块,属于自编码语言模型,
论文中,作者分别用 12层 和 24层 transformers encoder 组装了两套 bert模型,分别是
层的数量(Transformer Encoder 块的数量)为L ,隐藏层的维度为H ,自注意头的个数为A;
在所有例子中,我们将前馈/过滤器(Transformer Encoder 端的 feed-forward 层)的维度设置为4H ,即当 H=768 时是3072 ;当 H=1024 是 4096;
网络结构如下
模型输入为
Token emd:文本中各个字/词的初始向量,可以是随机初始,也可以使用 word2vec 进行初始化 【为方便描述且与 BERT 模型的当前中文版本保持一致,统一以「字向量」作为输入】
Segment emd:文本向量,不同于单个字,该 emd 学习了全局语义信息,通过训练得到,一般初始化为 111222
Position emd:由于出现在文本不同位置的字/词所携带的语义信息存在差异(比如:“我爱你”和“你爱我”),因此,BERT 模型对不同位置的字/词分别附加一个不同的向量以作区分,通过训练得到
3个 emd 相加 sum 作为 input;
模型输出 包含 输入各字对应的 融合全文语义信息 后的向量表示
下面代码截取自 Bert官方源码,大致是 如何把原始数据 转换成 符合模型输入的格式,从源码看出 Bert有两种(代码加粗)输入格式;
def convert_single_example(ex_index, example, label_list, max_seq_length, tokenizer): """Converts a single `InputExample` into a single `InputFeatures`.""" if isinstance(example, PaddingInputExample): return InputFeatures( input_ids=[0] * max_seq_length, input_mask=[0] * max_seq_length, segment_ids=[0] * max_seq_length, label_id=0, is_real_example=False) label_map = {} for (i, label) in enumerate(label_list): label_map[label] = i tokens_a = tokenizer.tokenize(example.text_a) tokens_b = None if example.text_b: tokens_b = tokenizer.tokenize(example.text_b) if tokens_b: # Modifies `tokens_a` and `tokens_b` in place so that the total # length is less than the specified length. # Account for [CLS], [SEP], [SEP] with "- 3" _truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3) else: # Account for [CLS] and [SEP] with "- 2" if len(tokens_a) > max_seq_length - 2: tokens_a = tokens_a[0:(max_seq_length - 2)] # The convention in BERT is: # (a) For sequence pairs: # tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP] # type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1 # (b) For single sequences: # tokens: [CLS] the dog is hairy . [SEP] # type_ids: 0 0 0 0 0 0 0 # # Where "type_ids" are used to indicate whether this is the first # sequence or the second sequence. The embedding vectors for `type=0` and # `type=1` were learned during pre-training and are added to the wordpiece # embedding vector (and position vector). This is not *strictly* necessary # since the [SEP] token unambiguously separates the sequences, but it makes # it easier for the model to learn the concept of sequences. # # For classification tasks, the first vector (corresponding to [CLS]) is # used as the "sentence vector". Note that this only makes sense because # the entire model is fine-tuned. tokens = [] segment_ids = [] tokens.append("[CLS]") segment_ids.append(0) for token in tokens_a: tokens.append(token) segment_ids.append(0) tokens.append("[SEP]") segment_ids.append(0) if tokens_b: for token in tokens_b: tokens.append(token) segment_ids.append(1) tokens.append("[SEP]") segment_ids.append(1) input_ids = tokenizer.convert_tokens_to_ids(tokens) # The mask has 1 for real tokens and 0 for padding tokens. Only real # tokens are attended to. input_mask = [1] * len(input_ids) # Zero-pad up to the sequence length. while len(input_ids) < max_seq_length: input_ids.append(0) input_mask.append(0) segment_ids.append(0) label_id = label_map[example.label] feature = InputFeatures( input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids, label_id=label_id, is_real_example=True) return feature
下面代码截取自 Bert官方源码,大致是 在 Bert模型输出后 加上 全连接层 进行下游任务,该代码可以判断 Bert的输出是Emd;
def create_model(bert_config, is_training, input_ids, input_mask, segment_ids, labels, num_labels, use_one_hot_embeddings): """Creates a classification model.""" model = modeling.BertModel( config=bert_config, is_training=is_training, input_ids=input_ids, input_mask=input_mask, token_type_ids=segment_ids, use_one_hot_embeddings=use_one_hot_embeddings) # In the demo, we are doing a simple classification task on the entire # segment. # # If you want to use the token-level output, use model.get_sequence_output() # instead. output_layer = model.get_pooled_output() hidden_size = output_layer.shape[-1].value output_weights = tf.get_variable( "output_weights", [num_labels, hidden_size], initializer=tf.truncated_normal_initializer(stddev=0.02)) output_bias = tf.get_variable( "output_bias", [num_labels], initializer=tf.zeros_initializer()) with tf.variable_scope("loss"): if is_training: # I.e., 0.1 dropout output_layer = tf.nn.dropout(output_layer, keep_prob=0.9) logits = tf.matmul(output_layer, output_weights, transpose_b=True) logits = tf.nn.bias_add(logits, output_bias) probabilities = tf.nn.softmax(logits, axis=-1) log_probs = tf.nn.log_softmax(logits, axis=-1) one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32) per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1) loss = tf.reduce_mean(per_example_loss) return (loss, per_example_loss, logits, probabilities)
Bert 预训练
作者设计了两个任务来预训练模型,
预训练的目标是构建语言模型,P(我爱吃饭)=P(我|爱吃饭)P(爱|吃饭)P(吃|饭)
BERT模型采用的是bidirectional Transformer,为什么需要双向呢?因为在预训练模型处理下游任务时,不仅需要该词左侧的语言信息,还需要右侧的语言信息
MLM
随机掩盖部分输入词,然后基于上下文对被掩盖的词进行预测;
在实际训练过程中,每次从序列中随机选出15% token用于masked,也就是每次只预测15%的词,而不是像word2vec中的cbow预测所有词;
在被选中的 token 中,80%用 MASK 替代,10%保持不变,10%随机选一个token替代原来的token;
这个任务 类似于 人类语言学习中的 《完形填空》任务
NSP:Next Sentence Prediction
预测两个句子是否连续
样本如下
1. 从训练语料库中取出两个连续的句子作为正样本
2.从不同的文档中随机各取一个句子作为负样本
缺点:主题预测和连贯性预测 合并为一个单项任务
这个任务类似于 人类学习语言中的 《段落重排》任务
Bert 模型通过 Masked ML 任务和 NSP 任务联合训练,使模型输出的每个字/词的向量都尽可能全面、准确地刻画输入文本的整体信息,为后续的微调任务提供更好的模型参数初始值。
局限性
1. Bert在MLM训练任务中,把多个词MASK掉,并且认为这些词相互独立,然而有时候并不是独立的,比如 我爱吃饭 变成 我爱MASK MASK,吃和饭本身是有关系的
2. BERT 的在预训练时会出现特殊的[MASK],但是它在下游的 fine-tune 中不会出现,这就出现了预训练阶段和 fine-tune 阶段不一致的问题
但这两个问题在经过海量语料库训练后会得到缓解,对模型整体效果影响不大
下游任务 finetuning
目前将预训练的语言模型应用到NLP任务主要有两种策略,
一种是基于特征的语言模型,如ELMo模型;另一种是基于微调的语言模型,如OpenAI GPT。
这两类语言模型各有其优缺点,BERT基本上融合了它们的优点,因此才可以在诸多后续特定任务上取得最优的效果。
多标签分类
如输入 一件 L 尺寸的棉服,输出两个标签——型号:L,类型:冬装
BERT 模型解决多标签分类问题时,其输入与普通单标签分类问题一致,得到其 embedding 表示之后(也就是 BERT 输出层的 embedding),
有几个 label 就连接到几个全连接层(也可以称为 projection layer),然后再分别接上 softmax 分类层,最后再将所有的 loss 相加起来即可。
这种做法就相当于将 n 个分类模型的特征提取层参数共享,得到一个共享的表示(其维度可以视任务而定,由于是多标签分类任务,因此其维度可以适当
增大一些),最后再做多标签分类任务。
一对句子的分类任务:例如自然语言推断 (MNLI),句子语义等价判断 (QQP) 等,
如上图 (a) 所示,需要将两个句子传入 BERT,然后使用 [CLS] 的输出值 C 进行句子对分类。
单个句子分类任务:例如句子情感分析 (SST-2),判断句子语法是否可以接受 (CoLA) 等,
如上图 (b) 所示,只需要输入一个句子,无需使用 [SEP] 标志,然后也是用 [CLS] 的输出值 C 进行分类。
问答任务:如 SQuAD v1.1 数据集,样本是语句对 (Question, Paragraph),Question 表示问题,Paragraph 是一段来自 Wikipedia 的文本,Paragraph
包含了问题的答案。而训练的目标是在 Paragraph 找出答案的起始位置 (Start,End)。
如上图 (c) 所示,将 Question 和 Paragraph 传入 BERT,然后 BERT 根据 Paragraph 所有单词的输出预测 Start 和 End 的位置。
单个句子标注任务:例如命名实体识别 (NER),输入单个句子,然后根据 BERT 对于每个单词的输出 T 预测这个单词的类别,是属于 Person,
Organization,Location,Miscellaneous 还是 Other (非命名实体)。
模型对比
word2vec 作为 NLP 里程碑的模型,对NLP的发展起到了巨大的作用,但它本身是一种浅层结构,且学习到的语义信息收到窗口大小的影响;
lstm 部分解决了长距离依赖的问题,但 lstm 本身是单向学习,只能从前到后,或者从后往前学习,无法同时考虑上下文信息;
参考资料:
https://blog.csdn.net/weixin_44799217/article/details/115374101 BERT模型的详细介绍
https://www.jianshu.com/p/46cb208d45c3 彻底理解 Google BERT 模型