本文主要改寫了一下"Sequence Tagging with Tensorflow"程序。原文是基于英文的命名實體識別(named entity recognition)問題,由于博主找不到相應的中文數據集(其實是沒備份數據丟了,如果有同學提供,萬分感謝)。因此,本文用了msra的分詞數據。另外,由于用到了詞向量,所以用了搜狗實驗室發布的2008新聞數據,提前訓練了300維度的字向量(用的gensim包訓練word2vector,另外后續可以嘗試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)
當然,你可能想把“彈幕游戲”作為一個詞,這取決于你如何標注這個數據,但是標注的時候要統一和規范。比如網上有PKU的數據標注規范(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實現了雙向LSTM+CRF在中文文本分詞上標注問題結果。
Part III tensorflow實現簡單的序列標注
預處理
首先,我們需要為每個字建立一個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']
附分詞實例代碼
戳這里 數據見README
附keras實現的簡易版本代碼
keras官方版本目前還木有實現crf層,但是網上有同學自己實現了,戳這里
例子:
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).