基於規則的中文分詞 - NLP學習(中文篇)
之前在其他博客文章有提到如何對英文進行分詞,也說後續會增加解釋我們中文是如何分詞的,我們都知道英文或者其他國家或者地區一些語言文字是詞與詞之間有空格(分隔符),這樣子分詞處理起來其實是要相對容易很多,但是像中文處理起來就沒有那麽容易,因為中文字與字之間,詞與詞之間都是緊密連接在一起的,所以第一件事需要處理的就是如何確認詞。中文文章的最小組成單位是字,但是獨立的字並不能很好地傳達想要表達整體的意思或者說欠缺表達能力,所以一篇成文的文章依舊是以詞為基本單位來形成有意義的篇章,所以詞是最小並且能獨立活動的語言成分。這也就說明在處理中文文本的時候,首先將句子轉化為特定的詞語結構(so-called 單詞),這就是中文分詞的重點。在這篇博客中,主要介紹常用的基於規則的中文分詞技術有哪幾種
前方高能:因代碼輸出結果我都打印出來了,篇幅占了不少,但是為了說明問題,沒辦法,還請各位看官和看友見諒。??????
規則分詞
規則分詞(Rule-based Tokenization)是通過設立詞典並不斷地對詞典進行維護以確保分詞準確性的分詞技術。基於規則分詞是一種匹配式的分詞技術,要進行分詞的時候通過在詞典中尋找相應的匹配,找到則進行切分,否則則不切分。傳統的規則式分詞主要有三種:正向最大匹配法(Maximum Match Method)、逆向最大匹配法(Reversed Maximum Match Method)以及雙向最大匹配(Bi-direction Matching Method)。
1.1 正向最大匹配法
正向最大匹配法基本思想是假定設定好的詞典中的最長詞(也就是最長的詞語)含有 i 個漢字字符,那麽則用被處理中文文本中當前字符串字段的前 i 個字作為匹配字段去跟詞典的這 i 個進行匹配查找(如處理的當前中文字符串中要查找“學海無涯”這四個字組成的詞語,我們就在詞典中去尋找相匹配的四個字並作為基本切分單位輸出),假如詞典中含有這麽一個 i 字詞語則匹配成功,切分該詞,反之則匹配失敗,那麽這個時候就會去掉需要被匹配字段中的最後一個漢字字符,並對剩下的字符串進行匹配處理(繼續把去掉最後一個字符的字符串跟詞典繼續匹配),循環往復直到所有字段成功匹配,循環終止的條件是切分出最後一個詞或者剩余匹配的字符串的長度為零。下面流程圖是正向最大匹配算法的描述過程(根據個人理解畫出,如果有哪裏不對的,還請指出!
下面是Python代碼實現的簡單的正向最大匹配算法:
1 class MaximumMatching(object): 2 3 def __init__(self): 4 self.window_size = 6 # 定義詞典中的最長單詞的長度 5 6 7 def tokenize(self, text): 8 tokens = [] # 定義一個空列表來保存切分的結果 9 index = 0 # 切分 10 text_length = len(text) 11 12 # 定義需要被維護的詞典,其中詞典最長詞的長度為6 13 maintained_dic = [‘研究‘, ‘研究生‘, ‘自然‘, ‘自然語言‘, ‘語言‘,‘自然語言處理‘, ‘處理‘, ‘是‘, ‘一個‘, ‘不錯‘, ‘的‘, ‘科研‘, ‘方向‘] 14 15 while text_length > index: # 循環結束判定條件 16 # print(text_length) 17 print(‘index 4: ‘, index) 18 for size in range(self.window_size + index, index, -1): # 根據窗口大小循環,直到找到符合的進行下一次循環 19 print(‘index 1: ‘, index) 20 print(‘window size: ‘, size) 21 piece = text[index: size] # 被匹配字段 22 if piece in maintained_dic: # 如果需要被匹配的字段在詞典中的話匹配成功,新的index為新的匹配字段的起始位置 23 index = size - 1 24 print(‘index 2: ‘, index) 25 break 26 index += 1 27 print(‘index 3‘, index, ‘\n‘) 28 tokens.append(piece) # 將匹配到的字段保存起來 29 return tokens 30 31 if __name__ == ‘__main__‘: 32 text = ‘研究生研究自然語言處理是一個不錯的研究方向‘ 33 tokenizer = MaximumMatching() 34 print(tokenizer.tokenize(text))
輸出結果是(這裏為了之後說明該算法的存在的一個短板,將循環的每一步都打印出來以便觀察,輸出結果可能有點長,為了說明問題所在,造成不便還請見諒):
index 4: 0 index 1: 0 window size: 6 index 1: 0 window size: 5 index 1: 0 window size: 4 index 1: 0 window size: 3 index 2: 2 index 3 3 index 4: 3 index 1: 3 window size: 9 index 1: 3 window size: 8 index 1: 3 window size: 7 index 1: 3 window size: 6 index 1: 3 window size: 5 index 2: 4 index 3 5 index 4: 5 index 1: 5 window size: 11 index 2: 10 index 3 11 index 4: 11 index 1: 11 window size: 17 index 1: 11 window size: 16 index 1: 11 window size: 15 index 1: 11 window size: 14 index 1: 11 window size: 13 index 1: 11 window size: 12 index 2: 11 index 3 12 index 4: 12 index 1: 12 window size: 18 index 1: 12 window size: 17 index 1: 12 window size: 16 index 1: 12 window size: 15 index 1: 12 window size: 14 index 2: 13 index 3 14 index 4: 14 index 1: 14 window size: 20 index 1: 14 window size: 19 index 1: 14 window size: 18 index 1: 14 window size: 17 index 1: 14 window size: 16 index 2: 15 index 3 16 index 4: 16 index 1: 16 window size: 22 index 1: 16 window size: 21 index 1: 16 window size: 20 index 1: 16 window size: 19 index 1: 16 window size: 18 index 1: 16 window size: 17 index 2: 16 index 3 17 index 4: 17 index 1: 17 window size: 23 index 1: 17 window size: 22 index 1: 17 window size: 21 index 1: 17 window size: 20 index 1: 17 window size: 19 index 2: 18 index 3 19 index 4: 19 index 1: 19 window size: 25 index 2: 24 index 3 25 [‘研究生‘, ‘研究‘, ‘自然語言處理‘, ‘是‘, ‘一個‘, ‘不錯‘, ‘的‘, ‘研究‘, ‘方向‘]
通過上述輸出結果的最後一行,我們可以看到通過正向最大匹配算法得到的切分結果還是很不錯的,但是這並不意味著實際運用都會十分精準,這裏主要有三個問題需要註意:
- 不斷維護詞典是非常困難的,新詞總是層出不窮,人工維護是費時費力的,並不能保證詞典能很好地覆蓋到所有可能出現的詞,特別是現在信息爆炸的時代每天新詞出現的速度更不是人工維護詞典能瞬間解決的,這對於解決實際問題就會造成一定程度的困擾;
- 直觀地觀察上述輸出結果,其實有一個問題也很明顯:執行效率並不好。可以看出在執行算法的時候,程序為了能找到一個合適的窗口,會循環往復的進行下去直到找到一個合適的。假設詞典非常的大,初始窗口也相對大的情況下,那麽匹配詞段尋找的時間和循環的次數也會相應的增加,執行效率也就變得非常低下;
- 無法很好的解決歧義問題。舉個例子,假設現在有一個最長詞長度為5的詞典,詞典中有“南京市長”和“長江大橋”兩個詞語,那麽“南京市長江大橋”通過上述正向最大匹配算法進行切分,我們首先通過對前五個字符進行匹配,發現沒有符合合適的,那麽此時就會去掉最後一個漢字,變成前面四個漢字進行匹配,發現匹配到了“南京市長”,然後我們用剩下的“江大橋”繼續匹配,可能得到的結果是“江”和“大橋”這兩個詞語,通過這個例子可以看出,這樣子的匹配結果並不是我們想要的,這個跟最初提到的第一個問題也有相關,維護詞典的完整性是一件困難的事,這涉及到句子切分理解歧義的問題。
1.2 逆向最大匹配算法
逆向最大匹配算法的實現過程基本跟正向最大匹配算法無差,唯一不同的點是分詞的切分是從後往前,跟正向最大匹配方法剛好相反。也就是說逆向是從字符串的最後面開始掃描,每次選取最末端的 i 個漢字字符作為匹配詞段,若匹配成功則進行下一字符串的匹配,否則則移除該匹配詞段的最前面一個漢字,繼續匹配。需要註意的是,分詞詞典為逆向詞典,即每個詞條都以逆序的方式存放。當然這個也不一定非得這麽處理,因為我們是逆向匹配,所以得到的結果是逆向的,只需要在最後反過來即可。
1 class ReversedMaximumMatching(object): 2 3 def __init__(self): 4 self.window_size = 6 5 6 def tokenize(self, text): 7 tokens = [] 8 index = len(text) 9 10 maintained_dic = [‘研究‘, ‘研究生‘, ‘自然‘, ‘自然語言‘, ‘語言‘,‘自然語言處理‘, ‘處理‘, ‘是‘, ‘一個‘, ‘不錯‘, ‘的‘, ‘科研‘, ‘方向‘] 11 12 while index > 0: 13 print(‘Index 1: ‘, index) 14 for size in range(index - self.window_size, index): 15 print(‘Window Check point: ‘, size) 16 w_piece = text[size: index] 17 print(‘Checked Words‘, w_piece) 18 if w_piece in maintained_dic: 19 index = size + 1 20 print(‘Index 2: ‘, index) 21 break 22 index -= 1 23 print(‘Index 3: ‘, index, ‘\n‘) 24 tokens.append(w_piece) 25 print(tokens) 26 tokens.reverse() 27 28 return tokens 29 30 31 if __name__ == ‘__main__‘: 32 33 text = ‘研究生研究自然語言處理是一個不錯的研究方向‘ 34 tokenizer = ReversedMaximumMatching() 35 print(tokenizer.tokenize(text))
上述代碼的輸出結果為:
Index 1: 21 Window Check point: 15 Checked Words 錯的研究方向 Window Check point: 16 Checked Words 的研究方向 Window Check point: 17 Checked Words 研究方向 Window Check point: 18 Checked Words 究方向 Window Check point: 19 Checked Words 方向 Index 2: 20 Index 3: 19 Index 1: 19 Window Check point: 13 Checked Words 個不錯的研究 Window Check point: 14 Checked Words 不錯的研究 Window Check point: 15 Checked Words 錯的研究 Window Check point: 16 Checked Words 的研究 Window Check point: 17 Checked Words 研究 Index 2: 18 Index 3: 17 Index 1: 17 Window Check point: 11 Checked Words 是一個不錯的 Window Check point: 12 Checked Words 一個不錯的 Window Check point: 13 Checked Words 個不錯的 Window Check point: 14 Checked Words 不錯的 Window Check point: 15 Checked Words 錯的 Window Check point: 16 Checked Words 的 Index 2: 17 Index 3: 16 Index 1: 16 Window Check point: 10 Checked Words 理是一個不錯 Window Check point: 11 Checked Words 是一個不錯 Window Check point: 12 Checked Words 一個不錯 Window Check point: 13 Checked Words 個不錯 Window Check point: 14 Checked Words 不錯 Index 2: 15 Index 3: 14 Index 1: 14 Window Check point: 8 Checked Words 言處理是一個 Window Check point: 9 Checked Words 處理是一個 Window Check point: 10 Checked Words 理是一個 Window Check point: 11 Checked Words 是一個 Window Check point: 12 Checked Words 一個 Index 2: 13 Index 3: 12 Index 1: 12 Window Check point: 6 Checked Words 然語言處理是 Window Check point: 7 Checked Words 語言處理是 Window Check point: 8 Checked Words 言處理是 Window Check point: 9 Checked Words 處理是 Window Check point: 10 Checked Words 理是 Window Check point: 11 Checked Words 是 Index 2: 12 Index 3: 11 Index 1: 11 Window Check point: 5 Checked Words 自然語言處理 Index 2: 6 Index 3: 5 Index 1: 5 Window Check point: -1 Checked Words Window Check point: 0 Checked Words 研究生研究 Window Check point: 1 Checked Words 究生研究 Window Check point: 2 Checked Words 生研究 Window Check point: 3 Checked Words 研究 Index 2: 4 Index 3: 3 Index 1: 3 Window Check point: -3 Checked Words Window Check point: -2 Checked Words Window Check point: -1 Checked Words Window Check point: 0 Checked Words 研究生 Index 2: 1 Index 3: 0 [‘方向‘, ‘研究‘, ‘的‘, ‘不錯‘, ‘一個‘, ‘是‘, ‘自然語言處理‘, ‘研究‘, ‘研究生‘] [‘研究生‘, ‘研究‘, ‘自然語言處理‘, ‘是‘, ‘一個‘, ‘不錯‘, ‘的‘, ‘研究‘, ‘方向‘]
倒數第二行輸出結果是逆向匹配得到的結果,此時我們僅需對其reverse一下即可得到最終需要的結果。除此之外,通過觀察上述的結果,亦可發現一個在正向最大匹配算法中同樣遇到的問題,那就是程序的執行效率並不高,因為算法需要不斷的去檢測匹配的字段,這個在需要維護的詞典是非常龐大的情況下是相當耗時間和耗資源的。
1.3 雙向最大匹配算法
雙向最大匹配算法其實是在正向最大匹配和逆向最大匹配兩個算法的基礎上延伸出來的,基本的思想很簡單,主要可以分為如下兩大步驟:
- 當正向和反向的分詞結果的詞語數目是不一樣的,那麽這個時候就選取分詞數量較少的那組分詞結果;
- 倘若分詞結果詞語數量是一樣的,又可以分為兩種子情況去考慮:
- 分詞結果完全一樣,那麽就不具備任何歧義,即表示正向反向的分詞結果皆可得到滿足;
- 如果不一樣,那麽就選取分詞結果中單個漢字數目較少的那一組。
下面Python代碼的實現的前面兩部分基本沿用上面兩個算法的代碼實現(為了保持完整性,就全部放出來了),第三部分是結合上述思想寫出來,測了幾個用例基本都沒問題,如果有問題,歡迎指出:
1 import operator 2 3 4 class BiDirectionMatching(object): 5 6 def __init__(self): 7 self.window_size = 6 8 self.dic = [‘研究‘, ‘研究生‘, ‘生命‘, ‘命‘, ‘的‘, ‘起源‘] 9 10 11 def mm_tokenize(self, text): 12 tokens = [] # 定義一個空列表來保存切分的結果 13 index = 0 # 切分 14 text_length = len(text) 15 16 while text_length > index: # 循環結束判定條件 17 for size in range(self.window_size + index, index, -1): # 根據窗口大小循環,直到找到符合的進行下一次循環 18 piece = text[index: size] # 被匹配字段 19 if piece in self.dic: # 如果需要被匹配的字段在詞典中的話匹配成功,新的index為新的匹配字段的起始位置 20 index = size - 1 21 break 22 index += 1 23 tokens.append(piece) # 將匹配到的字段保存起來 24 25 return tokens 26 27 def rmm_tokenize(self, text): 28 tokens = [] 29 index = len(text) 30 31 while index > 0: 32 for size in range(index - self.window_size, index): 33 w_piece = text[size: index] 34 if w_piece in self.dic: 35 index = size + 1 36 break 37 index -= 1 38 tokens.append(w_piece) 39 tokens.reverse() 40 41 return tokens 42 43 def bmm_tokenize(self, text): 44 mm_tokens = self.mm_tokenize(text) 45 print(‘正向最大匹配分詞結果:‘, mm_tokens) 46 rmm_tokens = self.rmm_tokenize(text) 47 print(‘逆向最大匹配分詞結果:‘, rmm_tokens) 48 49 if len(mm_tokens) != len(rmm_tokens): 50 if len(mm_tokens) > len(rmm_tokens): 51 return rmm_tokens 52 else: 53 return mm_tokens 54 elif len(mm_tokens) == len(rmm_tokens): 55 if operator.eq(mm_tokens, rmm_tokens): 56 return mm_tokens 57 else: 58 mm_count, rmm_count = 0, 0 59 for mm_tk in mm_tokens: 60 if len(mm_tk) == 1: 61 mm_count += 1 62 for rmm_tk in rmm_tokens: 63 if len(rmm_tk) == 1: 64 rmm_count += 1 65 if mm_count > rmm_count: 66 return rmm_tokens 67 else: 68 return mm_tokens 69 70 71 if __name__ == ‘__main__‘: 72 text = ‘研究生命的起源‘ 73 tokenizer = BiDirectionMatching() 74 print(‘雙向最大匹配得到的結果:‘, tokenizer.bmm_tokenize(text))
輸出結果為:
正向最大匹配分詞結果: [‘研究生‘, ‘命‘, ‘的‘, ‘起源‘] 逆向最大匹配分詞結果: [‘研究‘, ‘生命‘, ‘的‘, ‘起源‘] 雙向最大匹配得到的結果: [‘研究‘, ‘生命‘, ‘的‘, ‘起源‘]
通過上述結果可以看出,針對給出的例子切分詞語的最終數目是一樣,那麽就得進行進一步比較,而又可以得知切分結果並不是一一相同的,那麽就得比較分詞結果中哪一組中的單個漢字字符數較少,並且返回較少單個漢字的那一組結果。
總體來說,規則分詞一個核心的點就是需要有一個完整的詞典以便盡可能完整的覆蓋到可能會被用到的或者查詢的詞語,因為這牽扯到了能否很好的去進行分詞,而這是一個比較龐大的工程。在下一篇博客中將會看看是如何運用統計學來進行中文分詞的,主要會涉及到隱馬爾可夫模型,並看看如何用維特比算法是如何在隱馬爾可夫模型中起到作用的。
基於規則的中文分詞 - NLP學習(中文篇)