TF使用例子-LSTM实现序列标注

本文主要改写了一下"Sequence Tagging with Tensorflow"(https://link.jianshu.com?t=https://guillaumegenthial.github.io/sequence-tagging-with-tensorflow.html)程序。

原文是基于英文的命名实体识别(named entity recognition)问题,由于博主找不到相应的中文数据集(其实是没备份数据丢了,如果有同学提供,万分感谢)。因此,本文用了msra的分词数据(https://link.jianshu.com?t=http://sighan.cs.uchicago.edu/bakeoff2005/)。

另外,由于用到了词向量,所以用了搜狗实验室发布的2008新闻数据(https://link.jianshu.com?t=https://www.sogou.com/labs/resource/ca.php),提前训练了300维度的字向量(用的gensim包训练word2vector,另外后续可以尝试Glove)。

1、序列标注

序列标注就是给定一串序列,对序列中的每个元素做一个标记。比如我们希望识别一句话里面的人名,地名,组织机构名(命名实体识别)。有如下的句子:

琪斯美是日本的“东方project”系列弹幕游戏及其衍生作品的登场角色之一。

为每个字做标注之后的结果就是:

琪(B-PER)斯(I-PER)美(E-PER)是(O)日(B-LOC)本(E-LOC)的(O)“(O)东(B-ORG)方(I-ORG)project(E-ORG)”(O)系(O)列(O)弹(O)幕(O)游(O)戏(O)及(O)其(O)衍(O)生(O)作(O)品(O)的(O)登(O)场(O)角(O)色(O)之(O)一(O)。(O)*

这里标注采用的是BIEO,即Begin, Intermediate, End, Other(?我也不知道O是什么)

琪(B-PER)斯(I-PER)美(E-PER) 表示的含义就是 “琪”是人名开始,“斯”是人名中间的字,“美”是人名的末尾的字。其它符号同理。

这里可以看到,实际上就是用一串符号来标注出你感兴趣的部分。那么对于分词问题也是同理:

琪斯美是日本的“东方project”系列弹幕游戏及其衍生作品的登场角色之一。 琪斯美 是 日本 的 “ 东方project ” 系列 弹幕 游戏 及 其 衍生 作品 的 登场 角色 之一。 琪(B)斯(I)美 (E)是(S) 日(B)本(E) 的(S) “(S) 东(B)方(I)project(E) ”(S) 系(B)列(E) 弹(B)幕(E) 游(B)戏(E) 及(S) 其(S) 衍(B)生(E) 作(B)品(E) 的(S) 登(B)场(E) 角(B)色(E) 之(B)一(E)。(S)

当然,你可能想把“弹幕游戏”作为一个词,这取决于你如何标注这个数据,但是标注的时候要统一和规范。比如网上有PKU的数据标注规范(http://sighan.cs.uchicago.edu/bakeoff2005/data/pku_spec.pdf)。

其它比如像词性的标注都属于同一类问题。

2、常用方法

常用方法有MEMM (Maximum Entropy Markov Model)【1】,CRF (Conditional Random Field)【2】与 LSTM+CRF【3】。

【1】【2】原理待补充。 【3】类型的模型大致如图:

这是一个双向的LSTM,这里的英文单词可以类比成中文的字,在输出结果的时候再用crf对输出结果进行调整(排除不太可能的标注顺序)。

本文简单的用tensorflow实现了双向LSTM+CRF在中文文本分词上标注问题结果。

3、TF实现简单的序列标注

预处理

首先,我们需要为每个字建立一个id,另外可以设置一个阈值把出现次数小于该阈值的字用UNK(unknown)来统一表示。另外数字可以同义用NUM来代替。

然后我们把训练集按照6:3:1的比例分成训练集,验证集,测试集。并把格式整理成一列句子,一列标注。这里用的是BIEs标注方案。

李 B 元 E 与 s

卞 B 德 I 培 E 初 s 识 s 于 s 1 B 9 I 4 I 7 I 年 E 。 s

建模

这部分主要是翻译了原文。

由于tensorflow是batch处理数据样本的,所以我们需要对句子做padding,让它们一样长,所以我们需要先对其定义2个placeholders,一个表示句子,一个表示每个句子除去padding的实际长度:

#shape = (batch size, max length of sentence in batch)
word_ids = tf.placeholder(tf.int32, shape=[None, None])
#shape = (batch size)`
sequence_lengths = tf.placeholder(tf.int32, shape=[None])

假设embeddings是我们预先训练好的词向量,那么我么可以这样load词向量。

L = tf.Variable(embeddings, dtype=tf.float32, trainable=False)
# shape = (batch, sentence, word_vector_size)
pretrained_embeddings = tf.nn.embedding_lookup(L, word_ids)

这里trainable设置成False而不是tf.constant,否则会有内存问题。(另外如果不需要训练embedding层的话也没必要设置成True)

原文把每个英文单词作为一个词,并考虑了这个词当中的字母的特征,而我们这里直接只考虑每个字,所以省略了字母特征这一块。

一旦我们有了词的表示之后,我们只用跑一个LSTM或者bi-LSTM,得到另一串向量(LSTM的隐藏层,或者bi-LSTM的前向后向的隐藏层的组合)。

对于序列标注问题,前后字对于当前字的标注结果都会有影响,所以用双向的LSTM是很有意义的。这次我们用每个time step的隐藏层状态,代码如下:

word_embeddings = pretrained_embeddings
lstm_cell = tf.contrib.rnn.LSTMCell(hidden_size)

(output_fw, output_bw), _ = tf.nn.bidirectional_dynamic_rnn(lstm_cell, 
    lstm_cell, word_embeddings, sequence_length=sequence_lengths, 
    dtype=tf.float32)

context_rep = tf.concat([output_fw, output_bw], axis=-1)

解码

这一步,我们可以用两种方式来为每个tag打分:

方法一: 用softmax,然后argmax选择score值最大的那个tag,这种方法是基于字级别的。

方法二: 用条件随机场(Conditional Random Field, CRF)在句子层面做预测。

两种方法的目的都是为了让最后的序列标注结果的概率最大。先来计算scores:

W = tf.get_variable("W", shape=[2*self.config.hidden_size, self.config.ntags], 
                dtype=tf.float32)

b = tf.get_variable("b", shape=[self.config.ntags], dtype=tf.float32, 
                initializer=tf.zeros_initializer())

ntime_steps = tf.shape(context_rep)[1]
context_rep_flat = tf.reshape(context_rep, [-1, 2*hidden_size])
pred = tf.matmul(context_rep_flat, W) + b
scores = tf.reshape(pred, [-1, ntime_steps, ntags])

对于softmax, 实际上是使得每个字属于某个tag的概率最大,最后一串序列的结果就是序列中每个字的标注概率相乘得到的。这种结果都是局部的,也就是说某个字在标注的时候并没有考虑前后面字的标注结果的影响。

对于linear-chain CRF: 定义了一个全局的score,考虑了标注结果之间的转移。如下图:

如果我们不考虑转移情况,都选取局部最大的值,我们就会标注为PER-PER-LOC了。

训练

用CRF得到loss, 另外tf.contrib.crf.crf_log_likelihood还会返回转移矩阵T,方便我们在做预测的时候用它:

# shape = (batch, sentence)
labels = tf.placeholder(tf.int32, shape=[None, None], name="labels")

log_likelihood, transition_params = tf.contrib.crf.crf_log_likelihood(
scores, labels, sequence_lengths)

loss = tf.reduce_mean(-log_likelihood)

用local softmax的到的loss, 这里用mask过滤掉pad上去的token带来的loss:

losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=scores, labels=labels)
# shape = (batch, sentence, nclasses)
mask = tf.sequence_mask(sequence_lengths)
# apply mask
losses = tf.boolean_mask(losses, mask)

loss = tf.reduce_mean(losses)

最后定义我们的train_op:

optimizer = tf.train.AdamOptimizer(self.lr) train_op = optimizer.minimize(self.loss)

预测

对于local softmax直接选择每个time step最高的值就可以:

labels_pred = tf.cast(tf.argmax(self.logits, axis=-1), tf.int32)

对于CRF,传递一下训练时候得到的转移矩阵T,用viterbi的方法搜索到最优解即可:

# shape = (sentence, nclasses) score = ... viterbi_sequence, viterbi_score = tf.contrib.crf.viterbi_decode( score, transition_params

结果

楼主按照上述方法对msra的分词数据跑了60个epoch后在(验证集和测试集)上的准确率是96%左右,f1大概也在95%的样子。以下是分出来的结果:

琪斯美是日本的“东方project”系列弹幕游戏及其衍生作品的登场角色之一。 ['B', 'I', 'E', 's', 'B', 'E', 's', 's', 'B', 'E', 'B', 'I', 'I', 'I', 'I', 'I', 'E', 's', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 's', 'B', 'E', 'B', 'E', 'B', 'E', 's']

附分词实例代码

戳这里(https://link.jianshu.com?t=https://github.com/Slyne/tf_tagging.git) 数据见README

附keras实现的简易版本代码

keras官方版本目前还木有实现crf层,但是网上有同学自己实现了,戳这里(https://link.jianshu.com?t=https://github.com/phipleg/keras/blob/crf/keras/layers/crf.py)

例子:

n_words = 10000
maxlen = 32
(X_train, y_train), (X_test, y_test) = load_treebank(nb_words=n_words, maxlen=maxlen)

n_samples, n_steps, n_classes = y_train.shape

model = Sequential()
model.add(Embedding(n_words, 128, input_length=maxlen, dropout=0.2))
model.addBidirectional(LSTM(64, dropout_W=0.2, dropout_U=0.2, return_sequences=True),merge_mode='concat'))
model.add(Dropout(0.2))
model.add(TimeDistributed(Dense(n_classes)))
model.add(Dropout(0.2))
crf = ChainCRF()
model.add(crf)
model.compile(loss=crf.loss, optimizer='rmsprop', metrics=['accuracy'])

local softmax的代码如下:

model = Sequential()
# keras.layers.embeddings.Embedding(input_dim, output_dim, init='uniform', input_length=None, W_regularizer=None, activity_regularizer=None, W_constraint=None, mask_zero=False, weights=None, dropout=0.0)
model.add(Embedding(max_features, 128, dropout=0.2))
model.add(Bidirectional(LSTM(64, dropout_W=0.2, dropout_U=0.2, return_sequences=True),merge_mode='concat'))  # try using a GRU instead, for fun
model.add(TimeDistributed(Dense(num_class)))
model.add(Activation('softmax'))


# try using different optimizers and different optimizer configs
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=batch_size, nb_epoch=50,
          validation_data=(X_test, y_test))
score, acc = model.evaluate(X_test, y_test,
                            batch_size=batch_size)

相关文献

【1】McCallum, Andrew, Dayne Freitag, and Fernando CN Pereira. "Maximum Entropy Markov Models for Information Extraction and Segmentation."Icml. Vol. 17. 2000.

【2】Lafferty, John, Andrew McCallum, and Fernando Pereira. "Conditional random fields: Probabilistic models for segmenting and labeling sequence data."Proceedings of the eighteenth international conference on machine learning, ICML. Vol. 1. 2001.

【3】Huang, Zhiheng, Wei Xu, and Kai Yu. "Bidirectional LSTM-CRF models for sequence tagging."arXiv preprint arXiv:1508.01991(2015).

原文发布于微信公众号 - 人工智能LeadAI(atleadai)

原文发表时间:2017-12-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏拭心的安卓进阶之路

Java 集合深入理解(12):古老的 Vector

今天刮台风,躲屋里看看 Vector ! 都说 Vector 是线程安全的 ArrayList,今天来根据源码看看是不是这么相...

2447
来自专栏xingoo, 一个梦想做发明家的程序员

20120918-向量实现《数据结构与算法分析》

#include <iostream> #include <list> #include <string> #include <vector> #include...

1736
来自专栏ml

朴素贝叶斯分类器(离散型)算法实现(一)

1. 贝叶斯定理:        (1)   P(A^B) = P(A|B)P(B) = P(B|A)P(A)   由(1)得    P(A|B) = P(B|...

3467
来自专栏刘君君

JDK8的HashMap源码学习笔记

3068
来自专栏项勇

笔记68 | 切换fragmengt的replace和add方法笔记

1444
来自专栏desperate633

LeetCode Invert Binary Tree题目分析

Invert a binary tree. 4 / \ 2 7 / \ / \1 3 6 9 to4 / \ 7 2 / \ / \9 6 3 1 Tri...

871
来自专栏开发与安全

算法:AOV网(Activity on Vextex Network)与拓扑排序

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网(Activity on Vextex ...

2607
来自专栏alexqdjay

HashMap 多线程下死循环分析及JDK8修复

1K4
来自专栏xingoo, 一个梦想做发明家的程序员

AOE关键路径

这个算法来求关键路径,其实就是利用拓扑排序,首先求出,每个节点最晚开始时间,再倒退求每个最早开始的时间。 从而算出活动最早开始的时间和最晚开始的时间,如果这两个...

2527
来自专栏学海无涯

Android开发之奇怪的Fragment

说起Android中的Fragment,在使用的时候稍加注意,就会发现存在以下两种: v4包中的兼容Fragment,android.support.v4.ap...

3165

扫码关注云+社区