Bert语言模型fine-tune微调做中文NER命名实体识别

代码其实是去年十一月的Bert刚出来大火的时候写的,想起来也应该总结一下BERT的整体框架和微调思路

Bert

Transformer

NLP技术已经发展了几十年,当深度神经网络进入到这个领域的时候,RNN/LSTM/GRU都各自掀起过一场浪潮,CNN目前也有自成一派的趋势,但是去年Google Research这一篇论文又重新把他们之前提出的Transformer这个基础模型框架重新放在了大家面前

其实从实际经验出发,虽然有各路大牛都在推荐Transformer,RNN-based model还是NLP工程当中的首选,可能是基于以下原因:

  • Transformer 基于注意力机制擅长于抓取长句子长段落当中的信息,然而非常多的NLP工程任务如聊天机器人并不需要如此长篇大论的对话,反而效果会差
  • Transformer 需要有良好效果目前来看最好是有多层的叠加,这回导致训练资源开销很大,负担不起

当然这里不讨论其他框架的效果和实际使用

关于Transformer架构的详解,网上已经有了非常多很好的文章,推荐知乎的这篇文章,注意以下几个点即可:

  • 实际的multi-head attention其实不用掌握那么详细,但是必须要知道其本身完全利用注意力来实现
  • 每一层Encoder和Decoder内部都是使用multi-head attention以及残差网络来构造
  • Encoder和Decoder之间的联系同样也是通过multi-head attention来构造

Bert

Bert是由12层双向Transformer搭建而来,同样关于Bert推荐知乎另一篇文章,同样注意以下几个点即可:

  • 原论文中关于英文的模型是使用三种词向量相加而成的embedding,而中文是没有分词这个操作的,使用的是字向量
  • BERT以 $P(w_i|w_1, …,w_{i-1}, w_{i+1},…,w_n)$ 作为目标函数训练LM
  • 普通的分类任务使用第一个[CLS]token的final hidden state来预测,大部分常见的微调都是使用这种方式,当然可以修改为获取最后一层的所有参数做max-pool

Code

数据准备

进行NER任务,选取经典的BIO标注数据,总共有三类实体,人物,地点,组织。那么对于Bert模型来说需要进行的分类一共有

  • “O”
  • “B-PER”
  • “I-PER”
  • “B-ORG”
  • “I-ORG”
  • “B-LOC”
  • “I-LOC”
  • “X”
  • “[CLS]”
  • “[SEP]”

其中CLS和SEP是Bert数据组织的必备token,关于这个X的含义,已经在GitHub上有很多人问过我了,这是由于在做英文tokenize分词处理的时候有可能会出现[“do”, “ing”]这种情况,这时候为了避免冗余,tag只会加到第一个词上,所以后面的都使用X这个label来表示。中文当中虽然不会出现这种情况,但是很有可能用户在使用的时候会夹杂着英文使用,所以为了防止可能的bug,还是暂时保存这段代码

然后根据bert当中本身自带的数据处理参考代码,跟所有NLP工程一样,将文本转化为token和label的id组合即可,详细代码参考项目。这里不得不说一句Google真是越活越回去了,不求使用keras这种更高层的API,也不需要使用Estimator这种这么底层的东西吧,确实更加精细和精巧,但真是不方便

模型构建

真正到了模型最关键的地方反而没有什么可以赘述的了,所有的微调基本都在两层的MLP网络以内,跑起来速度也是很快的,这里我们直接使用一层网络接上softmax即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def create_model(bert_config, is_training, input_ids, input_mask,
segment_ids, labels, num_labels, use_one_hot_embeddings):
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
)

output_layer = model.get_sequence_output()

hidden_size = output_layer.shape[-1].value

output_weight = 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:
output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
output_layer = tf.reshape(output_layer, [-1, hidden_size])
logits = tf.matmul(output_layer, output_weight, transpose_b=True)
logits = tf.nn.bias_add(logits, output_bias)
logits = tf.reshape(logits, [-1, FLAGS.max_seq_length, 10])
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_sum(per_example_loss)
probabilities = tf.nn.softmax(logits, axis=-1)
predict = tf.argmax(probabilities,axis=-1)
return (loss, per_example_loss, logits,predict)

GitHub上已经有人实现了后面接上BiLSTM-CRF进行NER任务了,效果非常好,但是由于我的时间和计算资源有限,而且光是微调的效果就已经足够好了,故而没有进行尝试,有兴趣的同学可以去看看,同时包括Transformer XL这个号称inference速度提升很多的模型,也是很方便用来做fine-tune的

小结

eval的效果达到了95%以上,当然我知道实际使用的话必然达不到这个效果,但是90%+肯定是可以做到的,Bert的魅力在于里面的所有概念甚至都可以称之为冷饭,然而冷饭翻炒确实很香,但是在资源不够的情况下如何把顶尖的实验室产品工程化确实是一道难题