数据竞赛/阿里天池 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 模型进行分类。
如果你有疑问,欢迎留言。
参考
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。