中文分詞技術
中文自動分詞可主要歸納為“規則分詞”“統計分詞”和“混合分詞”,規則分詞主要是通過人工設立詞庫,按照一定方式進行匹配切分,實現簡單高效,但對新詞很難進行處理,統計分詞能夠較好應對新詞發現能特殊場景,但太過於依賴語料的質量,因此實踐中多是采用兩者的結合,即混合分詞。
1.1 規則分詞
基於規則的分詞是一種機械分詞方法,主要是通過維護詞典,在切分語句時,將語句的每個字符串與詞表中的詞進行逐一匹配,找到則切分,否則不予切分。
按照匹配切分的方式,主要有正向最大匹配法、逆向最大匹配法以及雙向最大匹配法三種方法。
1.1.1 正向最大匹配法
正向最大匹配(Maximum Match Method, MM)法的基本思想為:假定分詞詞典中的最長詞有 i 個漢字字符,則用被處理文檔的當前字串中的前 i 個字作為匹配字段,查找字典。若字典中存在這樣的一個 i 字詞,則匹配成功,匹配字段被作為一個詞切分出來。如果字典中找不到這樣的一個 i 字詞,則匹配失敗,將匹配字段中的最后一個字去掉。對剩下的字串重新進行匹配處理。如此進行下去,直到匹配成功,即切分出一個詞或剩余字串的長度為零為止,這樣就完成了一輪匹配,然后取下一個 i 字字串進行匹配處理,直到文檔被掃描完為止。示例代碼如下:
class MM(object): def __init__(self, dic_path): self.dictionary = set() self.maximum = 0 with open(dic_path, 'r', encoding="utf8") as f: for line in f: line = line.strip() if not line: continue self.dictionary.add(line) self.maximum = max(self.maximum, len(line)) def cut(self, text): result = [] index = 0 text_length = len(text) while text_length > index: for size in range(self.maximum + index, index, -1): piece = text[index:size] if piece in self.dictionary: index = size - 1 break index = index + 1 result.append(piece + '----') print(result) if __name__ == '__main__': text = '研究生命的起源' tokenizer = MM('./data/mm_dic.utf8') print(tokenizer.cut(text))
1.1.2 逆向最大匹配法
逆向最大匹配(Reverse Maximum Match Method, RMM 法)的基本原理與 MM 法相同,不同的是分詞切分的方向與 MM 法相反。逆向最大匹配法從被處理文檔的末端開始匹配掃描,每次取最末端的 i 個字符(i 為詞典中最長詞數)作為匹配字段,若匹配失敗,則去掉匹配字段最前面的一個字,繼續匹配。相應地,它使用的分詞詞典是逆序詞典,其中的每個詞都將按逆序方式存放。在實際處理時,先將文檔進行倒排處理,生成逆序文檔。然后,根據逆序詞典,對逆序文檔用正向最大匹配法處理即可。
1.1.3 雙向最大匹配法
雙向最大匹配法(Bi-direction Matching Method)是將正向最大匹配法得到的分詞結果和逆向最大匹配法得到的結果進行比較,然后按照最大匹配原則,選取詞數切分最少的作為結果。
雙向最大匹配的規則是:
- 如果正反向分詞結果詞數不同,則取分詞數量較少的那個(如“南京市/長江/大橋”的分詞數量為3,而“南京市/長江大橋”的分詞數量為2,所以返回分詞數量為2的)。
- 如果分詞結果詞數相同:1)分詞結果相同,就說明沒有歧義,可返回任意一個;2)分詞結果不同,返回其中單字較少的那個。
1.2 統計分詞
基於統計的中文分詞算法主要思想是把每個詞看做是由詞的最小單位的各個字組成的,如果相連的字在不同的文本中出現的次數越多,就證明這相連的字很可能就是一個詞。
- 基於統計的分詞,一般要做如下兩步操作:建立統計語言模型。
- 對句子進行單詞划分,然后對划分結果進行概率計算,獲得概率最大的分詞方式。這里就用到了統計學習算法,如隱含馬爾可夫(HMM)、條件隨機場(CRF)等。
1.2.1 HMM 模型
隱含馬爾可夫(HMM)是將分詞作為字在字串中的序列標注任務來實現的。其基本思路是:每個字在構造一個特定的詞語時都占據着一個特定的構詞位置,現規定每個字最多只有四個構詞位置:即B(詞首)、M(詞中)、E(詞尾)和 S(單獨成詞),那么下面句子1)的分詞結果可以表示成2)所示的逐字標注形式:
1)中文/分詞/是/.文本處理/不可或缺/的/一步!
2)中/B文/E分/B詞/E是/S文/B本/M處/M理/M不/B可/M或/M缺/E的/S一/B步/E!/S
用數學抽象表示如下:用 λ = λ1λ2...λn 代表輸入的句子,n 為句子長度, λi 表示字,ο = ο1ο2...οn 代表輸出的標簽,那么理想的輸出即為:
max = maxP(ο1ο2...οn|λ1λ2...λn)
在分詞任務上,ο 即為B、M、E、S 這 4 種標記,λ 為諸如“中”、“文”等句子中的每個字(包括標點等非中文字符)。引入觀測獨立性假設,即每個字的輸出僅僅與當前字有關,於是就能得到下式:
P(ο1ο2...οn|λ1λ2...λn) = P(ο1|λ1)P(ο2|λ2)...P(οn|λn)
通過貝葉斯公式得到:
P(ο|λ) = P(ο, λ)/P(λ) = P(λ|ο)P(ο)/P(λ)
λ 為給定的輸入,因此 P(λ) 計算為常數,可以忽略,因此最大化 P(ο|λ) 等價於最大化 P(λ|ο)P(ο)。針對 P(λ|ο)P(ο) 作馬爾可夫假設,得到:
P(λ|ο) = P(λ1|ο1)P(λ2|ο2)...P(λn|οn)
對 P(ο) 做齊次馬爾可夫假設,每個輸出僅僅與上一個輸出有關,那么:
P(ο) = P(ο1)P(ο2|ο1)P(ο3|ο2)...P(οn|οn-1)
於是:
P(λ|ο)P(ο) ~ P(λ1|ο1)P(ο2|ο1)P(λ2|ο2)P(ο3|ο2)...P(οn|οn-1)P(λn|οn)
在 HMM 中,將 P(λk|οk) 稱為發射概率,P(οk|οk-1) 稱為轉移概率。通過設置某些 P(οk|οk-1) = 0,可以排除類似 BBB、EM等不合理的組合。
在 HMM 中,求解 maxP(λ|ο)P(ο) 的常用方法是 Veterbi 算法。它是一種動態規划方法,核心思想是:如果最終的最優路徑經過某個 οi,那么從初始節點到 οi-1 點的路徑必然也是一個最優路徑——因為每一個節點 οi 只會影響前后兩個 P(οi-1|οi) 和 P(οi|οi+1)。
下面是通過 python 來實現的 HMM,將其封裝成一個類:
class HMM(object): def __init__(self): self.model_file = './data/hmm_model.pkl' self.state_list = ['B', 'M', 'E', 'S'] self.load_para = False def try_load_model(self, trained): if trained: import pickle with open(self.model_file, 'rb') as f: self.A_dic = pickle.load(f) self.B_dic = pickle.load(f) self.Pi_dic = pickle.load(f) self.load_para = True else: self.A_dic = {} self.B_dic = {} self.Pi_dic = {} self.load_para = False def train(self, path): self.try_load_model(False) Count_dic = {} def init_parameters(): for state in self.state_list: self.A_dic[state] = {s: 0.0 for s in self.state_list} self.Pi_dic[state] = 0.0 self.B_dic[state] = {} Count_dic[state] = 0 def makeLabel(text): out_text = [] if len(text) == 1: out_text.append('S') else: out_text += ['B'] + ['M'] * (len(text) - 2) + ['E'] return out_text init_parameters() line_num = -1 words = set() with io.open(path, encoding='utf8') as f: for line in f: line_num += 1 line = line.strip() if not line: continue word_list = [i for i in line if i != ' '] words |= set(word_list) linelist = line.split() line_state = [] for w in linelist: line_state.extend(makeLabel(w)) assert(len(word_list) == len(line_state)) for k, v in enumerate(line_state): Count_dic[v] += 1 if k == 0: self.Pi_dic[v] += 1 else: self.A_dic[line_state[k-1]][v] += 1 self.B_dic[line_state[k]][word_list[k]] = \ self.B_dic[line_state[k]].get(word_list[k], 0) + 1.0 self.Pi_dic = {k: v * 1.0 / line_num for k, v in self.Pi_dic.items()} self.A_dic = {k: {k1: v1 / Count_dic[k] for k1, v1 in v.items()} for k, v in self.A_dic.items()} self.B_dic = {k: {k1: (v1 + 1) / Count_dic[k] for k1, v1 in v.items()} for k, v in self.B_dic.items()} import pickle with open(self.model_file, 'wb') as f: pickle.dump(self.A_dic, f) pickle.dump(self.B_dic, f) pickle.dump(self.Pi_dic, f) return self def viterbi(self, text, states, start_p, trans_p, emit_p): V = [{}] path = {} for y in states: V[0][y] = start_p[y] * emit_p[y].get(text[0], 0) path[y] = [y] print(V) print(path) for t in range(1, len(text)): V.append({}) newpath = {} neverSeen = text[t] not in emit_p['S'].keys() and \ text[t] not in emit_p['M'].keys() and \ text[t] not in emit_p['E'].keys() and \ text[t] not in emit_p['B'].keys() for y in states: emitP = emit_p[y].get(text[t], 0) if not neverSeen else 1.0 (prob, state) = max( [(V[t-1][y0] * trans_p[y0].get(y, 0) * emitP, y0) for y0 in states if V[t-1][y0] > 0] ) V[t][y] = prob newpath[y] = path[state] + [y] path = newpath if emit_p['M'].get(text[-1], 0) > emit_p['S'].get(text[-1], 0): (prob, state) = max([(V[len(text) - 1][y], y) for y in ('E', 'M')]) else: (prob, state) = max([(V[len(text) - 1][y], y) for y in states]) return (prob, path[state]) def cut(self, text): import os if not self.load_para: self.try_load_model(os.path.exists(self.model_file)) prob, pos_list = self.viterbi(text, self.state_list, self.Pi_dic, self.A_dic, self.B_dic) begin, next = 0, 0 for i, char in enumerate(text): pos = pos_list[i] if pos == 'B': begin = i elif pos == 'E': yield text[begin: i+1] next = i + 1 elif pos == 'S': yield char next = i + 1 if next < len(text): yield text[next:] if __name__ == '__main__': hmm = HMM() # hmm.train('./data/trainCorpus.txt_utf8') text = '這是一個非常棒的方案' res = hmm.cut(text) print(list(res))