本文主要改寫了一下"Sequence Tagging with Tensorflow"程序痴荐。原文是基于英文的命名實體識別(named entity recognition)問題暑认,由于博主找不到相應的中文數(shù)據(jù)集(其實是沒備份數(shù)據(jù)丟了,如果有同學提供粗合,萬分感謝)。因此,本文用了msra的分詞數(shù)據(jù)。另外暑脆,由于用到了詞向量,所以用了搜狗實驗室發(fā)布的2008新聞數(shù)據(jù)狐肢,提前訓練了300維度的字向量(用的gensim包訓練word2vector添吗,另外后續(xù)可以嘗試Glove)。
Part I 序列標注
序列標注就是給定一串序列份名,對序列中的每個元素做一個標記碟联。比如我們希望識別一句話里面的人名,地名僵腺,組織機構名(命名實體識別)鲤孵。有如下的句子:
琪斯美是日本的“東方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)
當然,你可能想把“彈幕游戲”作為一個詞往声,這取決于你如何標注這個數(shù)據(jù)擂找,但是標注的時候要統(tǒng)一和規(guī)范。比如網(wǎng)上有PKU的數(shù)據(jù)標注規(guī)范(http://sighan.cs.uchicago.edu/bakeoff2005/data/pku_spec.pdf)浩销。
其它比如像詞性的標注都屬于同一類問題贯涎。
Part II 常用方法
常用方法有MEMM (Maximum Entropy Markov Model)【1】,CRF (Conditional Random Field)【2】與 LSTM+CRF【3】慢洋。
【1】【2】原理待補充塘雳。
【3】類型的模型大致如圖:
這是一個雙向的LSTM,這里的英文單詞可以類比成中文的字普筹,在輸出結果的時候再用crf對輸出結果進行調整(排除不太可能的標注順序)败明。
本文簡單的用tensorflow實現(xiàn)了雙向LSTM+CRF在中文文本分詞上標注問題結果。
Part III tensorflow實現(xiàn)簡單的序列標注
預處理
首先太防,我們需要為每個字建立一個id妻顶,另外可以設置一個閾值把出現(xiàn)次數(shù)小于該閾值的字用UNK(unknown)來統(tǒng)一表示。另外數(shù)字可以同義用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處理數(shù)據(jù)樣本的棘伴,所以我們需要對句子做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的隱藏層狀態(tài)袋励,代碼如下:
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的方法搜索到最優(yōu)解即可:
# shape = (sentence, nclasses)
score = ...
viterbi_sequence, viterbi_score = tf.contrib.crf.viterbi_decode(
score, transition_params)
結果
樓主按照上述方法對msra的分詞數(shù)據(jù)跑了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']
附分詞實例代碼
戳這里 數(shù)據(jù)見README
附keras實現(xiàn)的簡易版本代碼
keras官方版本目前還木有實現(xiàn)crf層忍啤,但是網(wǎng)上有同學自己實現(xiàn)了加勤,戳這里
例子:
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).