开源项目kcws代码分析--基于深度学习的分词技术
http://blog.csdn.net/pirage/article/details/53424544
?
分詞原理
本小節內容參考待字閨中的兩篇博文:
簡單的說,kcws的分詞原理就是:
另外,字符嵌入的表示可以是純預訓練的,但也可以在訓練模型的時候再fine-tune,一般而言后者效果更好。對于fine-tune的情形,可以在字符嵌入后,輸入雙向LSTM之前加入dropout進一步提升模型效果。
具體的解決方案,基于雙向LSTM與CRF的神經網絡結構:
如上圖所示,單層圓圈代表的word embedding輸入層,菱形代表學習輸入的決定性方程,雙層圓圈代表隨機變量。信息流將輸入層的word embedding送到雙向LSTM, l(i)代表word(i)和從左邊傳入的歷史信號,r(i)代表word(i)以及從右邊傳入的未來的信號,用c(i)連接這兩個向量的信息,代表詞word(i)。
先說Bi-LSTM這個雙向模型,LSTM的變種有很多,基本流程都是一樣的,文獻中同樣采用上一節說明的三個門,控制送入memory cell的輸入信息,以及遺忘之前階段信息的比例,再對細胞狀態進行更新,最后用tanh方程對更新后的細胞狀態再做處理,與sigmoid疊加相乘作為最終輸出。具體的模型公式見下圖,經過上一節的解釋,這些符號應該不太陌生了。
雙向LSTM說白了,就是先從左至右,順序學習輸入詞序列的歷史信息,再從右至左,學習輸入詞序列未來影響現在的信息,結合這兩種方式的最終表示有效地描述了詞的內容,在多種標注應用上取得了好效果。
如果說雙向LSTM并不特殊,這個結構中另一個新的嘗試,就是將深度神經網絡最后學出來的結果,作為特征,用CRF模型連接起來,用P來表示雙向LSTM神經網絡學習出來的打分輸出矩陣,它是一個 nxk 的矩陣,n是輸入詞序列個數,k是標記類型的數目, P(ij)指的是在一個輸入句子中,第i個詞在第j個tag標記上的可能性(打分)。另外一個特征函數是狀態轉移矩陣 A,A(ij) 代表從tag i轉移到tag j的可能性(打分),但這個轉移矩陣實際上有k+2維,其中包括句子的開始和結束兩個狀態,用公式表示如下圖:
?
s(X,y)=∑i=0nAyi,yi+1+∑i=1nPi,yi?
在給定輸入序列X,最終定義的輸出y序列的概率,則使用softmax函數表示如下:
?
p(y|X)=es(X,y)∑y?∈YXes(X,y?)?
而在訓練學習目標函數的時候,要優化的就是下面這個預測輸出標記序列的log概率,其中 Y(X)代表的是所有可能的tag標記序列集合,那么最后學習得到的輸出,就是概率最大的那個標記序列。如果只是模擬輸出的bigram交互影響方式,采用動態規劃即可求解下列方程。
?
log(p(y|X))=s(X,y)?log(∑y?∈YXes(X,y?))?
=s(X,y)?logaddy?∈YXs(X,y?)
?
至此,基于雙向LSTM與CRF的神經網絡結構已經介紹完畢,文獻中介紹的是在命名實體識別方面的一個實踐應用,這個思路同樣可以用在分詞上。具體的實踐和調參,也得應場景而異,Koth在上一篇博客中已經給出了自己的踐行,讀者們可以借鑒參考。
代碼結構與實踐
koth大神開源的項目地址為:https://github.com/koth/kcws
主要的代碼在目錄kcws/kcws/train路徑下。(寫這篇文章的時候發現K大神做了更新,我主要還是分析之前的代碼)
- process_anno_file.py 將人民網2014訓練語料進行單字切割,包括標點符號
- generate_training.py 生成單字的vector之后,處理每篇訓練語料,以“。”劃分為句子,對每個句子,如果長度大于MAX_LEN(默認設置為80,在代碼里改),不處理,Long lines加一,如果長度小于MAX_LEN,則生成長度為160的vector,前80為單字在字典中的index,不足80的補0,后80為每個字對應的SBME標記(S表示單字,B表示開始,M表示中間,E表示結尾)。
- filter_sentence.py 將語料切分為訓練集和測試集,作者將含有兩個字以下的句子過濾掉,剩下的按照二八分,測試集最多8000篇。
- train_cws_lstm.py 主要訓練代碼。
作者在項目主頁上很詳細的寫了構建和訓練的步驟,按照這些步驟來實踐一下不算難,我主要遇到了以下幾個問題:
主要代碼分析
def main(unused_argv):curdir = os.path.dirname(os.path.realpath(__file__))trainDataPath = tf.app.flags.FLAGS.train_data_pathif not trainDataPath.startswith("/"): trainDataPath = curdir + "/" + trainDataPath graph = tf.Graph() with graph.as_default(): model = Model(FLAGS.embedding_size, FLAGS.num_tags, FLAGS.word2vec_path, FLAGS.num_hidden) print("train data path:", trainDataPath) # 讀取訓練集batch大小的feature和label,各為80大小的數組 X, Y = inputs(trainDataPath) # 讀取測試集所有數據的feature和label,各為80大小的數組 tX, tY = do_load_data(tf.app.flags.FLAGS.test_data_path) # 計算訓練集的損失 total_loss = model.loss(X, Y) # 使用AdamOptimizer優化方法 train_op = train(total_loss) # 在測試集上做評測 test_unary_score, test_sequence_length = model.test_unary_score() # 創建Supervisor管理模型的分布式訓練 sv = tf.train.Supervisor(graph=graph, logdir=FLAGS.log_dir) with sv.managed_session(master='') as sess: # actual training loop training_steps = FLAGS.train_steps for step in range(training_steps): if sv.should_stop(): break try: _, trainsMatrix = sess.run( [train_op, model.transition_params]) # for debugging and learning purposes, see how the loss gets decremented thru training steps if step % 100 == 0: print("[%d] loss: [%r]" % (step, sess.run(total_loss))) if step % 1000 == 0: test_evaluate(sess, test_unary_score, test_sequence_length, trainsMatrix, model.inp, tX, tY) except KeyboardInterrupt, e: sv.saver.save(sess, FLAGS.log_dir + '/model', global_step=step + 1) raise e sv.saver.save(sess, FLAGS.log_dir + '/finnal-model') sess.close()1
?
Class Model:
def __init__(self, embeddingSize, distinctTagNum, c2vPath, numHidden):self.embeddingSize = embeddingSizeself.distinctTagNum = distinctTagNumself.numHidden = numHiddenself.c2v = self.load_w2v(c2vPath)self.words = tf.Variable(self.c2v, name="words") with tf.variable_scope('Softmax') as scope: self.W = tf.get_variable( shape=[numHidden * 2, distinctTagNum], initializer=tf.truncated_normal_initializer(stddev=0.01), name="weights", regularizer=tf.contrib.layers.l2_regularizer(0.001)) self.b = tf.Variable(tf.zeros([distinctTagNum], name="bias")) self.trains_params = None self.inp = tf.placeholder(tf.int32, shape=[None, FLAGS.max_sentence_len], name="input_placeholder") pass def length(self, data): used = tf.sign(tf.reduce_max(tf.abs(data), reduction_indices=2)) length = tf.reduce_sum(used, reduction_indices=1) length = tf.cast(length, tf.int32) return length def inference(self, X, reuse=None, trainMode=True): word_vectors = tf.nn.embedding_lookup(self.words, X) # 按照X順序返回self.words中的第X行,返回的結果組成tensor。 length = self.length(word_vectors) # length是shape為[batch_size]大小值為句子長度的vector length_64 = tf.cast(length, tf.int64) if trainMode: # 訓練的時候啟用dropout,測試的時候關鍵dropout word_vectors = tf.nn.dropout(word_vectors, 0.5) # 將word_vectors按照50%的概率丟棄某些詞,tf增加的一個處理是將其余的詞scale 1/0.5 with tf.variable_scope("rnn_fwbw", reuse=reuse) as scope: forward_output, _ = tf.nn.dynamic_rnn( tf.nn.rnn_cell.LSTMCell(self.numHidden), word_vectors, dtype=tf.float32, sequence_length=length, scope="RNN_forward") backward_output_, _ = tf.nn.dynamic_rnn( tf.nn.rnn_cell.LSTMCell(self.numHidden), inputs=tf.reverse_sequence(word_vectors, length_64, seq_dim=1), # 訓練和測試的時候,inputs的格式不同。訓練時,tensor shape是[batch_size, max_time,input_size] # 測試時,tensor shape是[max_time,batch_size,input_size]. # tf.reverse_sequence作用就是指定在列上操作(batch_dim表示按行操作) dtype=tf.float32, sequence_length=length, scope="RNN_backword") # tf.nn.dynamic_rnn(cell, inputs, sequence_length,time_major,...)主要參數: # cell:搭建好的網絡,這里用LSTMCell(num_cell),num_cell表示一個lstm單元輸出的維數(100) # inputs:word_vectors,它的shape由time_major決定,默認是false,即[batch_size,max_time,input_size],如果是測試 # 過程,time_major設置為True,shape為[max_time,batch_size,input_size],這里直接做了reverse,省去了time_major設置。 # 其中,batch_size=100, max_time=80句子最大長度,input_size字的向量的長度。 # sequence_length:shape[batch_size]大小的值為句子最大長度的tensor。 # 輸出: # outputs:[batch_size, max_time, cell.output_size] # state: shape取決于LSTMCell中state_size的設置,返回Tensor或者tuple。 backward_output = tf.reverse_sequence(backward_output_, length_64, seq_dim=1) # 這里的reverse_sequence同上。 output = tf.concat(2, [forward_output, backward_output]) # 連接兩個三維tensor,2表示按照列連接(0表示縱向,1表示行) # 連接后,output的shape:[batch_size, max_time, 2*cell.output_size],即[100, 80, 2*50] output = tf.reshape(output, [-1, self.numHidden * 2]) # reshape后,output的shape:[batch_size, self.numHidden * 2],即[100, 200] matricized_unary_scores = tf.batch_matmul(output, self.W) # 得到未歸一化的CRF輸出 # 點乘W的shape[ 100*2, 4],生成[batch_size, 4]大小的matricized_unary_scores unary_scores = tf.reshape( matricized_unary_scores, [-1, FLAGS.max_sentence_len, self.distinctTagNum]) # reshape后,unary_scores大小為[batch_size,80, 4] return unary_scores, length def loss(self, X, Y): P, sequence_length = self.inference(X) # CRF損失計算,訓練的時候使用,測試的時候用viterbi解碼 log_likelihood, self.transition_params = tf.contrib.crf.crf_log_likelihood( P, Y, sequence_length) # crf_log_likelihood參數(inputs,tag_indices, sequence_lengths) # inputs:大小為[100, 80, 4]的tensor,CRF層的輸入 # tag_indices:大小為[100, 80]的矩陣 # sequence_length:大小 [100]值為80的向量。 # 輸出: # log_likelihood:[batch_size]大小的vector,log-likelihood值 # transition_params:[4,4]大小的矩陣 loss = tf.reduce_mean(-log_likelihood) return loss def load_w2v(self, path): #返回(num+2)*50大小的二維矩陣,其中第一行全是0,最后一行是每個詞向量維度的平均值。 fp = open(path, "r") print("load data from:", path) line = fp.readline().strip() ss = line.split(" ") total = int(ss[0]) dim = int(ss[1]) assert (dim == (FLAGS.embedding_size)) ws = [] mv = [0 for i in range(dim)] # The first for 0 ws.append([0 for i in range(dim)]) for t in range(total): line = fp.readline().strip() ss = line.split(" ") assert (len(ss) == (dim + 1)) vals = [] for i in range(1, dim + 1): fv = float(ss[i]) mv[i - 1] += fv vals.append(fv) ws.append(vals) for i in range(dim): mv[i] = mv[i] / total ws.append(mv) fp.close() return np.asarray(ws, dtype=np.float32) def test_unary_score(self): P, sequence_length = self.inference(self.inp, reuse=True, trainMode=False) return P, sequence_length其他的代碼比較簡單,個人覺得不必要做深入分析。
總結
近一兩個月開始學習TensorFlow,代碼看了一些,但是總感覺臨門差那么一腳。革命尚未完成,同志們仍需努力
轉載于:https://www.cnblogs.com/DjangoBlog/p/6756449.html
總結
以上是生活随笔為你收集整理的开源项目kcws代码分析--基于深度学习的分词技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python与用户的交互 ,格式化输出的
- 下一篇: 如何成长?