本文主要改寫了一下"Sequence Tagging with Tensorflow"程序。原文是基于英文的命名實體識別(named entity recognition)問題,由于博主找不到相應(yīng)的中文數(shù)據(jù)集(其實是沒備份數(shù)據(jù)丟了,如果有同學(xué)提供,萬分感謝)。因此,本文用了msra的分詞數(shù)據(jù)。另外,由于用到了詞向量,所以用了搜狗實驗室發(fā)布的2008新聞數(shù)據(jù),提前訓(xùn)練了300維度的字向量(用的gensim包訓(xùn)練word2vector,另外后續(xù)可以嘗試Glove)。
Part I 序列標注
序列標注就是給定一串序列,對序列中的每個元素做一個標記。比如我們希望識別一句話里面的人名,地名,組織機構(gòu)名(命名實體識別)。有如下的句子:
琪斯美是日本的“東方project”系列彈幕游戲及其衍生作品的登場角色之一。
為每個字做標注之后的結(jié)果就是:
琪(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,這里的英文單詞可以類比成中文的字,在輸出結(jié)果的時候再用crf對輸出結(jié)果進行調(diào)整(排除不太可能的標注順序)。
本文簡單的用tensorflow實現(xiàn)了雙向LSTM+CRF在中文文本分詞上標注問題結(jié)果。
Part III tensorflow實現(xiàn)簡單的序列標注
預(yù)處理
首先,我們需要為每個字建立一個id,另外可以設(shè)置一個閾值把出現(xiàn)次數(shù)小于該閾值的字用UNK(unknown)來統(tǒng)一表示。另外數(shù)字可以同義用NUM來代替。
然后我們把訓(xùn)練集按照6:3:1的比例分成訓(xùn)練集,驗證集,測試集。并把格式整理成一列句子,一列標注。這里用的是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])
假設(shè)embeddings
是我們預(yù)先訓(xùn)練好的詞向量,那么我么可以這樣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
設(shè)置成False
而不是tf.constant
,否則會有內(nèi)存問題。(另外如果不需要訓(xùn)練embedding層的話也沒必要設(shè)置成True
)
原文把每個英文單詞作為一個詞,并考慮了這個詞當中的字母的特征,而我們這里直接只考慮每個字,所以省略了字母特征這一塊。
一旦我們有了詞的表示之后,我們只用跑一個LSTM或者bi-LSTM,得到另一串向量(LSTM的隱藏層,或者bi-LSTM的前向后向的隱藏層的組合)。
對于序列標注問題,前后字對于當前字的標注結(jié)果都會有影響,所以用雙向的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)在句子層面做預(yù)測。
兩種方法的目的都是為了讓最后的序列標注結(jié)果的概率最大。先來計算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的概率最大,最后一串序列的結(jié)果就是序列中每個字的標注概率相乘得到的。這種結(jié)果都是局部的,也就是說某個字在標注的時候并沒有考慮前后面字的標注結(jié)果的影響。
對于linear-chain CRF: 定義了一個全局的score,考慮了標注結(jié)果之間的轉(zhuǎn)移。如下圖:
如果我們不考慮轉(zhuǎn)移情況,都選取局部最大的值,我們就會標注為PER-PER-LOC了。
訓(xùn)練
用CRF得到loss, 另外tf.contrib.crf.crf_log_likelihood
還會返回轉(zhuǎn)移矩陣T,方便我們在做預(yù)測的時候用它:
# 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)
預(yù)測
對于local softmax直接選擇每個time step最高的值就可以:
labels_pred = tf.cast(tf.argmax(self.logits, axis=-1), tf.int32)
對于CRF,傳遞一下訓(xùn)練時候得到的轉(zhuǎn)移矩陣T,用viterbi的方法搜索到最優(yōu)解即可:
# shape = (sentence, nclasses)
score = ...
viterbi_sequence, viterbi_score = tf.contrib.crf.viterbi_decode(
score, transition_params)
結(jié)果
樓主按照上述方法對msra的分詞數(shù)據(jù)跑了60個epoch后在(驗證集和測試集)上的準確率是96%左右,f1大概也在95%的樣子。以下是分出來的結(jié)果:
琪斯美是日本的“東方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)上有同學(xué)自己實現(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)
相關(guān)文獻
【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).