浅谈BERT预训练源码

“BERT” 这个词相信大家已经不在陌生了, 发布至今,BERT 已成为 NLP 实验中无处不在的基线。这里稍微扯一下什么是BERT毕竟不是今天的重点,BERT在模型架构方面沿用了Transformer的Encoder端(不知道什么是transformer的小伙伴们可以去阅读论文:),它是一个预训练模型,模型训练时两个任务分别是预测句子中被掩盖的词以及判断输入的两个句子是不是上下句。在预训练好的BERT模型后面根据特定任务加上相应的网络,可以完成NLP的下游任务,比如文本分类、机器翻译等。说的简单点核心就是通过上下文去增强对目标词的表达。

今天主要是想和大家扒一扒这两个预训练任务的源码,预估你的收获是:1)熟系BERT预训练代码,如果条件允许的话可以自己进行预训练 ;2)最近大火的Prompt范式,可以使用BERT源码实现。

一、Mask Launage Model

随机掩盖掉一些单词,然后通过上下文预测该单词。BERT中有15%的子词(BERT是以 wordpiece token为最小单位)会被随机掩盖,这15%的token中有80%的概率会被mask, 10%的概率用随机其他词来替换 (使得模型具有一定纠错能力)还有10%的概率不做操作(和下游任务统一)。那么这一部分具体是怎么操作的呢,接下来带着大家看看源码是如何实现的。

1.2 mlm源码

<br>def&#xA0;create_masked_lm_predictions(tokens,&#xA0;masked_lm_prob,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;max_predictions_per_seq,&#xA0;vocab_words,&#xA0;rng):<br>&#xA0;&#xA0;<span class="hljs-string">""</span><span class="hljs-string">"<br>&#xA0;&#xA0;tokens&#xFF1A;&#x8F93;&#x5165;&#x6587;&#x672C;<br>&#xA0;&#xA0;masked_lm_prob&#xFF1A;&#x63A9;&#x7801;&#x8BED;&#x8A00;&#x6A21;&#x578B;&#x7684;&#x63A9;&#x7801;&#x6982;&#x7387;<br>&#xA0;&#xA0;max_predictions_per_seq:&#x6BCF;&#x4E2A;&#x5E8F;&#x5217;&#x6700;&#x5927;&#x9884;&#x6D4B;&#x6570;&#x76EE;<br>&#xA0;&#xA0;vocab_words&#xFF1A;&#x6BCF;&#x4E2A;&#x5217;&#x8868;&#x7684;&#x6700;&#x5927;&#x9884;&#x6D4B;&#x6570;&#x76EE;<br>&#xA0;&#xA0;rng:&#xA0;&#x968F;&#x673A;&#x6570;&#x751F;&#x6210;&#x5668;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;"</span><span class="hljs-string">""</span><br><br>&#xA0;&#xA0;cand_indexes&#xA0;=&#xA0;[]&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;(i,&#xA0;token)&#xA0;<span class="hljs-keyword">in</span>&#xA0;enumerate(tokens):<br>&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;token&#xA0;==&#xA0;<span class="hljs-string">"[CLS]"</span>&#xA0;or&#xA0;token&#xA0;==&#xA0;<span class="hljs-string">"[SEP]"</span>:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">continue</span><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(FLAGS.do_whole_word_mask&#xA0;and&#xA0;len(cand_indexes)&#xA0;>=&#xA0;1&#xA0;and<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;token.startswith(<span class="hljs-string">"##"</span>)):<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;cand_indexes[-1].append(i)<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span>:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;cand_indexes.append([i])<br>&#xA0;<br>&#xA0;&#xA0;rng.shuffle(cand_indexes)<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;output_tokens&#xA0;=&#xA0;list(tokens)<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;num_to_predict&#xA0;=&#xA0;min(max_predictions_per_seq,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;max(1,&#xA0;int(round(len(tokens)&#xA0;*&#xA0;masked_lm_prob))))<br><br>&#xA0;&#xA0;masked_lms&#xA0;=&#xA0;[]<br>&#xA0;&#xA0;covered_indexes&#xA0;=&#xA0;<span class="hljs-built_in">set</span>()&#xA0;<br>&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;index_set&#xA0;<span class="hljs-keyword">in</span>&#xA0;cand_indexes:<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;len(masked_lms)&#xA0;>=&#xA0;num_to_predict:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">break</span><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;len(masked_lms)&#xA0;+&#xA0;len(index_set)&#xA0;>&#xA0;num_to_predict:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">continue</span><br>&#xA0;&#xA0;&#xA0;&#xA0;is_any_index_covered&#xA0;=&#xA0;False<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;index&#xA0;<span class="hljs-keyword">in</span>&#xA0;index_set:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;index&#xA0;<span class="hljs-keyword">in</span>&#xA0;covered_indexes:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;is_any_index_covered&#xA0;=&#xA0;True<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">break</span><br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;is_any_index_covered:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">continue</span><br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;index&#xA0;<span class="hljs-keyword">in</span>&#xA0;index_set:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;covered_indexes.add(index)<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;masked_token&#xA0;=&#xA0;None<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;rng.random()&#xA0;< 0.8:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;masked_token&#xA0;=&#xA0;<span class="hljs-string">"[MASK]"</span><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span>:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;rng.random()&#xA0;< 0.5:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;masked_token&#xA0;=&#xA0;tokens[index]<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span>:<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;masked_token&#xA0;=&#xA0;vocab_words[rng.randint(0,&#xA0;len(vocab_words)&#xA0;-&#xA0;1)]<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;output_tokens[index]&#xA0;=&#xA0;masked_token&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;masked_lms.append(MaskedLmInstance(index=index,&#xA0;label=tokens[index]))&#xA0;<br>&#xA0;&#xA0;assert&#xA0;len(masked_lms)&#xA0;<= num_to_predict<br>&#xA0;&#xA0;masked_lms&#xA0;=&#xA0;sorted(masked_lms,&#xA0;key=lambda&#xA0;x:&#xA0;x.index)<br><br>&#xA0;&#xA0;masked_lm_positions&#xA0;=&#xA0;[]&#xA0;<br>&#xA0;&#xA0;masked_lm_labels&#xA0;=&#xA0;[]&#xA0;<br>&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;p&#xA0;<span class="hljs-keyword">in</span>&#xA0;masked_lms:<br>&#xA0;&#xA0;&#xA0;&#xA0;masked_lm_positions.append(p.index)<br>&#xA0;&#xA0;&#xA0;&#xA0;masked_lm_labels.append(p.label)<br><br>&#xA0;&#xA0;<span class="hljs-built_in">return</span>&#xA0;(output_tokens,&#xA0;masked_lm_positions,&#xA0;masked_lm_labels)<br></= num_to_predict<br></ 0.5:<br></ 0.8:

以上就是创建训练数据的源码,接下来讲解下模型如何训练得到masked LM loss

def&#xA0;get_masked_lm_output(bert_config,&#xA0;input_tensor,&#xA0;output_weights,&#xA0;positions,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;label_ids,&#xA0;label_weights):<br>&#xA0;&#xA0;<span class="hljs-string">""</span><span class="hljs-string">"Get&#xA0;loss&#xA0;and&#xA0;log&#xA0;probs&#xA0;for&#xA0;the&#xA0;masked&#xA0;LM."</span><span class="hljs-string">""</span><br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;input_tensor&#xA0;=&#xA0;gather_indexes(input_tensor,&#xA0;positions)<br><br>&#xA0;&#xA0;with&#xA0;tf.variable_scope(<span class="hljs-string">"cls/predictions"</span>):<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;with&#xA0;tf.variable_scope(<span class="hljs-string">"transform"</span>):<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;input_tensor&#xA0;=&#xA0;tf.layers.dense(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;input_tensor,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;units=bert_config.hidden_size,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;activation=modeling.get_activation(bert_config.hidden_act),<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;kernel_initializer=modeling.create_initializer(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;bert_config.initializer_range))<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;input_tensor&#xA0;=&#xA0;modeling.layer_norm(input_tensor)<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;output_bias&#xA0;=&#xA0;tf.get_variable(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-string">"output_bias"</span>,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;shape=[bert_config.vocab_size],<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;initializer=tf.zeros_initializer())<br>&#xA0;&#xA0;&#xA0;&#xA0;logits&#xA0;=&#xA0;tf.matmul(input_tensor,&#xA0;output_weights,&#xA0;transpose_b=True)<br>&#xA0;&#xA0;&#xA0;&#xA0;logits&#xA0;=&#xA0;tf.nn.bias_add(logits,&#xA0;output_bias)<br><br>&#xA0;&#xA0;&#xA0;&#xA0;log_probs&#xA0;=&#xA0;tf.nn.log_softmax(logits,&#xA0;axis=-1)<br><br>&#xA0;&#xA0;&#xA0;&#xA0;label_ids&#xA0;=&#xA0;tf.reshape(label_ids,&#xA0;[-1])<br>&#xA0;&#xA0;&#xA0;&#xA0;label_weights&#xA0;=&#xA0;tf.reshape(label_weights,&#xA0;[-1])<br><br>&#xA0;&#xA0;&#xA0;&#xA0;one_hot_labels&#xA0;=&#xA0;tf.one_hot(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;label_ids,&#xA0;depth=bert_config.vocab_size,&#xA0;dtype=tf.float32)<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;per_example_loss&#xA0;=&#xA0;-tf.reduce_sum(log_probs&#xA0;*&#xA0;one_hot_labels,&#xA0;axis=[-1])<br>&#xA0;&#xA0;&#xA0;&#xA0;numerator&#xA0;=&#xA0;tf.reduce_sum(label_weights&#xA0;*&#xA0;per_example_loss)<br>&#xA0;&#xA0;&#xA0;&#xA0;denominator&#xA0;=&#xA0;tf.reduce_sum(label_weights)&#xA0;+&#xA0;1e-5<br>&#xA0;&#xA0;&#xA0;&#xA0;loss&#xA0;=&#xA0;numerator&#xA0;/&#xA0;denominator<br><br>&#xA0;&#xA0;<span class="hljs-built_in">return</span>&#xA0;(loss,&#xA0;per_example_loss,&#xA0;log_probs)

其实看下来和fine-tune差不多,如果想要实现Prompt无非是把fine-tune时create_model 函数替换为get_masked_lm_output,输入输出得改变下。

使用WordPiece的时候一个单词可能会被拆分成两部分,比如 loving 会被拆分成 lov ##ing 如果mask的时候可能只mask两者之一,那么如果只mask一部分的话很容易被模型预测到,比如”我很喜欢吃苹[MASK]”,模型很容易根据”苹”预测出果,那么我们希望mask整个单词,其实新版bert已经支持英文的整词mask了,中文整词mask需要先进行分词。

二、Next Sentence prediction

该任务其实就是分类任务,输入[CLS]a[SEP]b[SEP],预测b是否为a的下一句,即二分类问题。

原文中50%的概率两个句子来自于同一个文档中的上下文(正样本),50%的概率来自不同文档的句子(负样本)

def&#xA0;get_next_sentence_output(bert_config,&#xA0;input_tensor,&#xA0;labels):<br>&#xA0;&#xA0;<span class="hljs-string">""</span><span class="hljs-string">"Get&#xA0;loss&#xA0;and&#xA0;log&#xA0;probs&#xA0;for&#xA0;the&#xA0;next&#xA0;sentence&#xA0;prediction."</span><span class="hljs-string">""</span><br><br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;with&#xA0;tf.variable_scope(<span class="hljs-string">"cls/seq_relationship"</span>):<br>&#xA0;&#xA0;&#xA0;&#xA0;output_weights&#xA0;=&#xA0;tf.get_variable(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-string">"output_weights"</span>,<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;shape=[2,&#xA0;bert_config.hidden_size],<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;initializer=modeling.create_initializer(bert_config.initializer_range))<br>&#xA0;&#xA0;&#xA0;&#xA0;output_bias&#xA0;=&#xA0;tf.get_variable(<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-string">"output_bias"</span>,&#xA0;shape=[2],&#xA0;initializer=tf.zeros_initializer())<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;logits&#xA0;=&#xA0;tf.matmul(input_tensor,&#xA0;output_weights,&#xA0;transpose_b=True)<br>&#xA0;&#xA0;&#xA0;&#xA0;logits&#xA0;=&#xA0;tf.nn.bias_add(logits,&#xA0;output_bias)<br>&#xA0;&#xA0;&#xA0;&#xA0;log_probs&#xA0;=&#xA0;tf.nn.log_softmax(logits,&#xA0;axis=-1)<br>&#xA0;&#xA0;&#xA0;&#xA0;labels&#xA0;=&#xA0;tf.reshape(labels,&#xA0;[-1])<br>&#xA0;&#xA0;&#xA0;&#xA0;one_hot_labels&#xA0;=&#xA0;tf.one_hot(labels,&#xA0;depth=2,&#xA0;dtype=tf.float32)<br>&#xA0;&#xA0;&#xA0;&#xA0;per_example_loss&#xA0;=&#xA0;-tf.reduce_sum(one_hot_labels&#xA0;*&#xA0;log_probs,&#xA0;axis=-1)<br>&#xA0;&#xA0;&#xA0;&#xA0;loss&#xA0;=&#xA0;tf.reduce_mean(per_example_loss)<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">return</span>&#xA0;(loss,&#xA0;per_example_loss,&#xA0;log_probs)

源码这里也是相当的简单呀,不是就是拿cls位的向量,经过一次线下变换,输入softmax得到一个概率(0-1),判断是否是上下文。

三、总结

  • 效果好,横扫了11项NLP任务。bert之后基本全面拥抱transformer。微调下游任务的时候,即使数据集非常小(比如小于5000个标注样本),模型性能也有不错的提升。
  • [MASK]标记在实际预测中不会出现,训练时用过多[MASK]影响模型表现
  • 每个batch只有15%的token被预测,所以BERT收敛得比left-to-right模型要慢(它们会预测每个token)
  • BERT的预训练任务MLM使得能够借助上下文对序列进行编码,但同时也使得其预训练过程与中的数据与微调的数据不匹配,难以适应生成式任务
  • BERT没有考虑预测[MASK]之间的相关性,是对语言模型联合概率的有偏估计
  • 由于最大输入长度的限制,适合句子和段落级别的任务,不适用于文档级别的任务(如长文本分类)

Original: https://blog.csdn.net/justorderman/article/details/122144438
Author: CReep~
Title: 浅谈BERT预训练源码

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/542823/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球