在天然語言處理中(NLP,Natural Language ProcessingNLP,Natural Language Processing),分詞是一個較爲簡單也基礎的基本技術。經常使用的分詞方法包括這兩種:基於字典的機械分詞 和 基於統計序列標註的分詞。對於基於字典的機械分詞本文再也不贅述,可看字典分詞方法。在本文中主要講解基於深度學習的分詞方法及原理,包括一下幾個步驟:
1標註序列
,2雙向LSTM網絡預測標籤
,3Viterbi算法求解最優路徑
node
中文分詞的第一步即是標註字,字標註是經過給句子中每一個字打上標籤的思路來進行分詞,好比以前提到過的,經過4標籤來進行標註(single,單字成詞;begin,多字詞的開頭;middle,三字以上詞語的中間部分;end,多字詞的結尾。均只取第一個字母。)
,這樣,「爲人民服務」就能夠標註爲「sbebe」了。4標註不是惟一的標註方式,相似地還有6標註,理論上來講,標註越多會越精細,理論上來講效果也越好,但標註太多也可能存在樣本不足的問題,通常經常使用的就是4標註和6標註。前面已經提到過,字標註是經過給句子中每一個字打上標籤的思路來進行分詞,好比以前提到過的,經過4標籤來進行標註(single,單字成詞;begin,多字詞的開頭;middle,三字以上詞語的中間部分;end,多字詞的結尾。均只取第一個字母。),這樣,「爲人民服務」就能夠標註爲「sbebe」了。4標註不是惟一的標註方式,相似地還有6標註,理論上來講,標註越多會越精細,理論上來講效果也越好,但標註太多也可能存在樣本不足的問題,通常經常使用的就是4標註和6標註。標註實例以下:python
人/b 們/e 常/s 說/s 生/b 活/e 是/s 一/s 部/s 教/b 科/m 書/e
這裏所指的網絡主要是指神經網絡,再細化一點就是雙向LSTM(長短時記憶網絡),雙向LSTM是LSTM的改進版,LSTM是RNN的改進版。所以,首先須要理解RNN。git
RNN的意思是,爲了預測最後的結果,我先用第一個詞預測,固然,只用第一個預測的預測結果確定不精確,我把這個結果做爲特徵,跟第二詞一塊兒,來預測結果;接着,我用這個新的預測結果結合第三詞,來做新的預測;而後重複這個過程;直到最後一個詞。這樣,若是輸入有n個詞,那麼咱們事實上對結果做了n次預測,給出了n個預測序列。整個過程當中,模型共享一組參數。所以,RNN下降了模型的參數數目,防止了過擬合,同時,它生來就是爲處理序列問題而設計的,所以,特別適合處理序列問題。循環神經網絡原理見下圖:github
LSTM對RNN作了改進,使得可以捕捉更長距離的信息。可是不論是LSTM仍是RNN,都有一個問題,它是從左往右推動的,所以後面的詞會比前面的詞更重要,可是對於分詞這個任務來講是不妥的,由於句子各個字應該是平權的。所以出現了雙向LSTM,它從左到右作一次LSTM,而後從右到左作一次LSTM,而後把兩次結果組合起來。算法
在分詞中,LSTM能夠根據輸入序列輸出一個序列,這個序列考慮了上下文的聯繫,所以,能夠給每一個輸出序列接一個softmax分類器,來預測每一個標籤的機率。基於這個序列到序列的思路,咱們就能夠直接預測句子的標籤。假設每次輸入\(y_1\)-\(y_n\)由下圖所示每一個輸入所對應的標籤爲\(x_1\)-\(x_n\)。再抽象一點用$ x_{ij} \(表示狀態\)x_i$的第j個可能值。markdown
最終輸出結果串聯起來造成以下圖所示的網絡網絡
圖中從第一個可能標籤到最後一個可能標籤的任何一條路徑均可能產生一個新的序列,每條路徑可能性不同,咱們須要作的是找出可能的路徑。因爲路徑很是多,所以採用窮舉是很是耗時的,所以引入Viterbi算法。架構
維特比算法是一個特殊但應用最廣的動態規劃算法,利用動態規劃,能夠解決任何一個圖中的最短路徑問題。而維特比算法是針對一個特殊的圖——籬笆網絡的有向圖(Lattice )的最短路徑問題而提出的。app
而維特比算法的精髓就是,既然知道到第i列全部節點Xi{j=123…}的最短路徑,那麼到第i+1列節點的最短路徑就等於到第i列j個節點的最短路徑+第i列j個節點到第i+1列各個節點的距離的最小值,關於維特比算法的詳細能夠點擊函數
使用Keras構建bilstm網絡,在keras中已經預置了網絡模型,只須要調用相應的函數就能夠了。須要注意的是,對於每一句話會轉換爲詞向量(Embedding)以下圖所示:
embedded = Embedding(len(chars) + 1, word_size, input_length=maxlen, mask_zero=True)(sequence)
並將不足的補零。
建立網絡
from keras.layers import Dense, Embedding, LSTM, TimeDistributed, Input, Bidirectional from keras.models import Model def create_model(maxlen, chars, word_size): """ :param maxlen: :param chars: :param word_size: :return: """ sequence = Input(shape=(maxlen,), dtype='int32') embedded = Embedding(len(chars) + 1, word_size, input_length=maxlen, mask_zero=True)(sequence) blstm = Bidirectional(LSTM(64, return_sequences=True), merge_mode='sum')(embedded) output = TimeDistributed(Dense(5, activation='softmax'))(blstm) model = Model(input=sequence, output=output) return model
訓練數據
# -*- coding:utf-8 -*- import re import numpy as np import pandas as pd # 設計模型 word_size = 128 maxlen = 32 with open('data/msr_train.txt', 'rb') as inp: texts = inp.read().decode('gbk') s = texts.split('\r\n') # 根據換行切分 def clean(s): # 整理一下數據,有些不規範的地方 if u'「/s' not in s: return s.replace(u' 」/s', '') elif u'」/s' not in s: return s.replace(u'「/s ', '') elif u'‘/s' not in s: return s.replace(u' ’/s', '') elif u'’/s' not in s: return s.replace(u'‘/s ', '') else: return s s = u''.join(map(clean, s)) s = re.split(u'[,。!?、]/[bems]', s) data = [] # 生成訓練樣本 label = [] def get_xy(s): s = re.findall('(.)/(.)', s) if s: s = np.array(s) return list(s[:, 0]), list(s[:, 1]) for i in s: x = get_xy(i) if x: data.append(x[0]) label.append(x[1]) d = pd.DataFrame(index=range(len(data))) d['data'] = data d['label'] = label d = d[d['data'].apply(len) <= maxlen] d.index = range(len(d)) tag = pd.Series({'s': 0, 'b': 1, 'm': 2, 'e': 3, 'x': 4}) chars = [] # 統計全部字,跟每一個字編號 for i in data: chars.extend(i) chars = pd.Series(chars).value_counts() chars[:] = range(1, len(chars) + 1) # 保存數據 import pickle with open('model/chars.pkl', 'wb') as outp: pickle.dump(chars, outp) print('** Finished saving the data.') # 生成適合模型輸入的格式 from keras.utils import np_utils d['x'] = d['data'].apply(lambda x: np.array(list(chars[x]) + [0] * (maxlen - len(x)))) def trans_one(x): _ = map(lambda y: np_utils.to_categorical(y, 5), tag[x].reshape((-1, 1))) _ = list(_) _.extend([np.array([[0, 0, 0, 0, 1]])] * (maxlen - len(x))) return np.array(_) d['y'] = d['label'].apply(trans_one) import lstm_model model = lstm_model.create_model(maxlen, chars, word_size) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) batch_size = 1024 history = model.fit(np.array(list(d['x'])), np.array(list(d['y'])).reshape((-1, maxlen, 5)), batch_size=batch_size, nb_epoch=20, verbose=2) model.save('model/model.h5')
1080顯卡訓練每次須要耗時44s,訓練20個epoch後精度達到95%
測試
import pickle import lstm_model import pandas as pd with open('model/chars.pkl', 'rb') as inp: chars = pickle.load(inp) word_size = 128 maxlen = 32 model = lstm_model.create_model(maxlen, chars, word_size) model.load_weights('model/model.h5', by_name=True) import re import numpy as np # 轉移機率,單純用了等機率 zy = {'be': 0.5, 'bm': 0.5, 'eb': 0.5, 'es': 0.5, 'me': 0.5, 'mm': 0.5, 'sb': 0.5, 'ss': 0.5 } zy = {i: np.log(zy[i]) for i in zy.keys()} def viterbi(nodes): paths = {'b': nodes[0]['b'], 's': nodes[0]['s']} # 第一層,只有兩個節點 for layer in range(1, len(nodes)): # 後面的每一層 paths_ = paths.copy() # 先保存上一層的路徑 # node_now 爲本層節點, node_last 爲上層節點 paths = {} # 清空 path for node_now in nodes[layer].keys(): # 對於本層的每一個節點,找出最短路徑 sub_paths = {} # 上一層的每一個節點到本層節點的鏈接 for path_last in paths_.keys(): if path_last[-1] + node_now in zy.keys(): # 若轉移機率不爲 0 sub_paths[path_last + node_now] = paths_[path_last] + nodes[layer][node_now] + zy[ path_last[-1] + node_now] # 最短路徑,即機率最大的那個 sr_subpaths = pd.Series(sub_paths) sr_subpaths = sr_subpaths.sort_values() # 升序排序 node_subpath = sr_subpaths.index[-1] # 最短路徑 node_value = sr_subpaths[-1] # 最短路徑對應的值 # 把 node_now 的最短路徑添加到 paths 中 paths[node_subpath] = node_value # 全部層求完後,找出最後一層中各個節點的路徑最短的路徑 sr_paths = pd.Series(paths) sr_paths = sr_paths.sort_values() # 按照升序排序 return sr_paths.index[-1] # 返回最短路徑(機率值最大的路徑) def simple_cut(s): if s: r = model.predict(np.array([list(chars[list(s)].fillna(0).astype(int)) + [0] * (maxlen - len(s))]), verbose=False)[ 0][:len(s)] r = np.log(r) print(r) nodes = [dict(zip(['s', 'b', 'm', 'e'], i[:4])) for i in r] t = viterbi(nodes) words = [] for i in range(len(s)): if t[i] in ['s', 'b']: words.append(s[i]) else: words[-1] += s[i] return words else: return [] not_cuts = re.compile(u'([\da-zA-Z ]+)|[。,、?!\.\?,!]') def cut_word(s): result = [] j = 0 for i in not_cuts.finditer(s): result.extend(simple_cut(s[j:i.start()])) result.append(s[i.start():i.end()]) j = i.end() result.extend(simple_cut(s[j:])) return result print(cut_word('深度學習主要是特徵學習'))
結果:
['深度', '學習', '主要', '是', '特徵', '學習']
本例子使用 Bi-directional LSTM 來完成了序列標註的問題。本例中展現的是一個分詞任務,可是還有其餘的序列標註問題都是能夠經過這樣一個架構來實現的,好比 POS(詞性標註)、NER(命名實體識別)等。在本例中,單從分類準確率來看的話差很少到 95% 了,仍是能夠的。但是最後的分詞效果還不是很是好,但也勉強能達到實用的水平。