数据竞赛/阿里天池 NLP 入门赛 Bert 方案 -2 Bert 源码讲解
前言
这篇文章用于记录阿里天池 NLP 入门赛,详细讲解了整个数据处理流程,以及如何从零构建一个模型,适合新手入门。
赛题以新闻数据为赛题数据,数据集报名后可见并可下载。赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐的文本数据。实质上是一个 14 分类问题。
赛题数据由以下几个部分构成:训练集20w条样本,测试集A包括5w条样本,测试集B包括5w条样本。
比赛地址:https://tianchi.aliyun.com/competition/entrance/531810/introduction
数据可以通过上面的链接下载。
代码地址:https://github.com/zhangxiann/Tianchi-NLP-Beginner
分为 3 篇文章介绍:
在上一篇文章中,我们介绍了数据预处理的流程。
这篇文章主要讲解 Bert 源码。
Bert 代码讲解
Bert 模型来自于 Transformer 的编码器,主要代码是 modeling.py。
BertConfig
我们先看下 BertConfig 类,这个类用于配置 BertModel 的超参数。用于读取 bert_config.json 文件中的参数。
bert_config.json 中的参数以及解释如下:
1  | {  | 
BertModel
BertModel 的输入参数如下:
- config:
BertConfig对象,用于配置超参数 - input_ids:经过 mask 的 token,形状是 [batch_size, seq_length]
 - input_mask:句子的 mask 列表,1 表示对应的位置有 token,0 表示对应的位置没有 token,形状是 [batch_size, seq_length]
 - token_type_ids:句子 id,形状是 [batch_size, seq_length]
 - use_one_hot_embeddings:是否使用 one hot 来获取 embedding。如果词典比较大,使用 one hot 比较快;如果词典比较小,不使用 one hot 比较快。
 
主要流程如下:
- 调用 
embedding_lookup(),把input_ids转换为embedding,返回embedding_output,embedding_table。 - 调用 
embedding_postprocessor(),将embedding_output添加位置编码和句子 id 的编码。 - 调用 
create_attention_mask_from_input_mask(),根据input_mask创建attention_mask。 - 调用 
transformer_model,创建 Bert 模型,输入数据,得到最后一层 encoder 的输出sequence_output。 - 取出 
sequence_output中第一个 token 的输出,经过全连接层,得到pooled_output。 
1  | class BertModel(object):  | 
代码注释中给出了使用 BertModel 的 demo,如下所示:
1  | input_ids = tf.constant([[31, 51, 99], [15, 5, 0]])  | 
embedding_lookup()
embedding_lookup() 的作用是根据 input_ids,返回对应的词向量矩阵 embedding_output ,和整个词典的词向量矩阵 embedding_table。
主要流程如下:
- 首先获取词典的 embedding 矩阵 
embedding_table。 - 判断是否使用 one hot,根据 
input_ids获取对应的ouput。 - 返回 
ouput和embedding_table。 
1  | def embedding_lookup(input_ids,  | 
embedding_postprocessor()
embedding_postprocessor() 的作用是将 embedding_output 添加位置编码和句子 id 的编码。
 其中 Token Embeddings 对应 input_tensor;Segment Embedding 对应 token_type_embeddings;Position Embedding 对应 position_embeddings,3 者相加得到最终的输出。
流程如下:
- 首先把 
input_tensor赋值给output。 - 根据 
token_type_ids生成token_type_embeddings,表示句子 id 编码,加到output。 - 生成 
position_embeddings,表示位置编码,加到output。代码中position_embeddings部分与论文中的方法不同。此代码中position_embeddings是训练出来的,而论文中的position_embeddings是固定值:\(\begin{aligned} P E_{(p o s, 2 i)} &=\sin \left(p o s / 10000^{2 i / d_{\text {model }}}\right), P E_{(p o s, 2 i+1)} =\cos \left(p o s / 10000^{2 i / d_{\text {model }}}\right) \end{aligned}\)。 
1  | def embedding_postprocessor(input_tensor,  | 
create_attention_mask_from_input_mask()
create_attention_mask_from_input_mask() 的作用是根据 input_mask 创建 attention_mask,attention_mask 的形状是 [batch_size, from_seq_length, seq_length]。
1  | # 从 2D 的 from_tensor  | 
transformer_model
transformer_model 就是 Bert 模型的真正实现,模型结构就是下图中的 左边部分。
 主要流程如下:
- 输入 
input_tensor的形状是[batch_size, seq_length, hidden_size],转换为prev_output,形状是[batch_size * seq_length, hidden_size]。 - 进入 for 循环,经过每一层 encoder:
- 输入 
attention_layer,得到attention_head。attention_layer的作用是计算Self Attention。 attention_head经过一个全连接层和layer_norm层,得到attention_output。attention_output经过一个升维的全连接层和降维的全连接层,得到layer_output。- 将 
attention_output和layer_output相加作为残差连接 
 - 输入 
 
1  | def transformer_model(input_tensor,  | 
其中 attention_layer 真正实现了 Self Attention 的计算。
attention_layer()
attention_layer() 的作用就是计算 Self Attention。
主要输入参数有 2 个:from_tensor 和 to_tensor。
如果from_tensor 和 to_tensor 是一样的,那么表示计算 encoder 的 Attention;否则计算 decoder 的 Attention。
主要流程如下:
首先,把
from_tensor转换为from_tensor_2d,形状为[batch_size * from_seq_length, from_width]。把
to_tensor转换为to_tensor_2d,形状为[batch_size * to_seq_length, to_width]。首先根据
from_tensor,计算:query_layer(query矩阵),形状为[batch_size * from_seq_length, num_attention_heads * size_per_head]。然后根据
to_tensor,计算key_layer(key矩阵)和value_layer(value矩阵),形状为[batch_size * to_seq_length, num_attention_heads * size_per_head]。将
query_layer的形状转换为[batch_size, num_attention_heads, from_seq_length, size_per_head]。将
key_layer和value_layer的形状转换为[batch_size, num_attention_heads, to_seq_length, size_per_head]。将
query_layer和key_layer相乘,得到attention_scores,并除以 \(\sqrt{size\_per\_head}\) 做缩放,形状为[batch_size, num_attention_heads, from_seq_length, to_seq_length]。将
attention_scores根据attention_mask进行处理。这里采用了一个巧妙的处理:将attention_mask构造adder,其中 mask 为 1 的地方,adder 为 0;mask 为 0 的地方,adder 为 -1000。最后将attention_scores和adder相加。这样,mask 为 1 对应的 attention 会不变,而mask 为 0 对应的 attention 会变得非常小。attention_scores经过 softmax,mask 为 0 对应的 attention 会接近于 0,达到了 mask 的目的,得到attention_probs,再经过 dropout。将
attention_probs和value_layer相乘,得到context_layer,形状是[batch_size, num_attention_heads, from_seq_length, size_per_head]。最后将
context_layer的形状转换为[batch_size, from_seq_length, num_attention_heads * size_per_head],返回context_layer。
1  | def attention_layer(from_tensor,  | 
至此,Bert 源码就讲解完了。在下一篇文章中,我们会讲解如何预训练 Bert,以及使用训练好的 Bert 模型进行分类。
如果你有疑问,欢迎留言。
参考
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。 






