基於Trie 樹實現簡單的中文分詞

中文分詞簡介

中文分詞是中文自然語言處理的基礎,中文分詞的正確率如何直接影響後續的詞性標註(也有些詞性標註算法不需要事先分詞,但標註效果往往比先分詞後標註差),實體識別、句法分析、語義分析。常用的分詞方法主要有依賴詞典的機械分詞和序列標註方法。

分詞算法分類

中文分詞算法大概分爲三大類:

  1. 第一類是基於字符串匹配,即掃描字符串,如果發現字符串的子串和詞典中的詞相同,就算匹配,比如機械分詞方法。這類分詞通常會加入一些啓發式規則,比如“正向/反向最大匹配”,“長詞優先”等。
  2. 第二類是基於統計以及機器學習的分詞方法,它們基於人工標註的詞性和統計特徵,對中文進行建模,即根據觀測到的數據(
    標註好的語料)
    對模型參數進行訓練,在分詞階段再通過模型計算各種分詞出現的概率,將概率最大的分詞結果作爲最終結果。常見的序列標註模型有HMM和CRF。這類分詞算法能很好處理歧義和未登錄詞問題,效果比前一類效果好,但是需要大量的人工標註數據,以及較慢的分詞速度。
  3. 第三類是通過讓計算機模擬人對句子的理解,達到識別詞的效果,目前基於深度學習以及目前比較火熱的預訓練模型效果非常好,能夠識別漢語複雜的語義。

機械分詞

機械分詞方法又叫基於字符串匹配的分詞方法,它是按照一定的策略將待分析的字符串與一個“充分大的”機器詞典中的詞條進行匹配,若在詞典中找到某個字符串,則匹配成功(識別出一個詞)。這是最簡單的分詞方法,但非常高效和常見。

機械分詞比較適用的場景是在某個小領域或者任務內,並且手中有一些積累的詞庫,可以快速構建一個簡單的分詞算法。

在自然語言處理相關的書籍資料中常提到的機械分詞方法主要有正向最大匹配、正向最小匹配、逆向最大匹配、逆向最小匹配四種,但是實際工程中用的比較多的還是正向最大匹配和逆向最大匹配。

假設我們已經有切詞詞典dict,要切詞的句子爲sentence; 爲便於理解,後面介紹兩種算法均以“南京市長江大橋”爲例說明算法。

正向最大匹配算法

正向最大匹配算法根據經驗設定切詞最大長度max_len(中文詞語多爲二字、三字、四字詞,少數五字短語,比如“坐山觀虎鬥”,因此max_len設爲4或5較合適),每次掃描的時候尋找當前開始的這個長度的詞來和字典中的詞匹配,如果沒有找到,就縮短長度繼續尋找,直到找到或者成爲單字
具體分詞算法如下:

custom_dict={"南京","南京市","市長","長江","大橋","江大橋",}
input_sentence="南京市長江大橋"
max_word_len=0
for word in custom_dict:
    if len(word)>max_word_len:
        max_word_len=len(word)

if len(input_sentence)<max_word_len:
    max_word_len=len(input_sentence)

start=0
seg_results=[]
while start<len(input_sentence):
    temp_len=max_word_len
    if len(input_sentence)-start<max_word_len:
        temp_len=len(input_sentence)-start
    while temp_len>0:
        sub_sentence=input_sentence[start:start+temp_len]
        if sub_sentence in custom_dict:
            seg_results.append(sub_sentence)
            start+=temp_len
            break
        else:
            temp_len-=1
    # 沒有子串匹配,則單獨成詞
    if temp_len==0:
        seg_results.append(input_sentence[start:start+1])
        start+=1
print(seg_results)

逆向最大匹配算法

逆向最大匹配算法和正向最大匹配算法不同的是,切分漢字時,逆向最大匹配算法不是按照漢字順序從左到右依次抽取子串,而是從漢字尾端開始抽取,算法代碼如下:

custom_dict={"南京","南京市","市長","長江","大橋","江大橋"}
input_sentence="南京市長江大橋"
max_word_len=0
for word in custom_dict:
    if len(word)>max_word_len:
        max_word_len=len(word)

if len(input_sentence)<max_word_len:
    max_word_len=len(input_sentence)

end=len(input_sentence)
seg_results=[]
while end>0:
    temp_len=max_word_len
    if end<max_word_len:
        temp_len=end
    while temp_len>0:
        sub_sentence=input_sentence[end-temp_len:end]
        if sub_sentence in custom_dict:
            seg_results.append(sub_sentence)
            end-=temp_len
            break
        else:
            temp_len-=1
    # 沒有子串匹配,則單獨成詞
    if temp_len==0:
        sub_sentence=input_sentence[end-1:end]
        seg_results.append(sub_sentence)
        end-=1
print(seg_results)

基於Trie樹實現中文分詞

詞表的內存表示: 很顯然,匹配過程中是需要找詞前綴的,因此我們不能將詞表簡單的存儲爲Hash結構。在這裏我們考慮一種高效的字符串前綴處理結構——Trie樹。這種結構使得查找每一個詞的時間複雜度爲O(word.length)
,而且可以很方便的判斷是否匹配成功或匹配到了字符串的前綴。
Trie Tree分詞原理:
(1) 從根結點開始一次搜索,比如搜索【北京】;
(2) 取得要查找關鍵詞的第一個字符【北】,並根據該字符選擇對應的子樹並轉到該子樹繼續進行檢索;
(3) 在相應的子樹上,取得要查找關鍵詞的第二個字符【京】,並進一步選擇對應的子樹進行檢索。
(4) 迭代過程……
(5) 在直到判斷樹節點的isEnd節點爲true則查找結束(最小匹配原則),然後發現【京】isEnd=true,則結束查找。

圖片來源:https://www.jianshu.com/p/1d9e7b8663c1

具體實現代碼如下:
Trie數定義如下:

class TrieNode(object):
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.data = {}
        self.is_word = False


class Trie(object):
    """
    trie樹
    """

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.root = TrieNode()

    def insert(self, word):
        """
        Inserts a word into the trie.
        :type word: str
        :rtype: void
        """
        node = self.root
        for chars in word:  # 遍歷詞語中的每個字符
            child = node.data.get(chars)  # 獲取該字符的子節點,
            if not child:  # 如果該字符不存在於樹中
                node.data[chars] = TrieNode()  # 則創建該字符節點
            node = node.data[chars]  # 節點爲當前該字符節點
        node.is_word = True

    def search(self, word):
        """
        Returns if the word is in the trie.
        :type word: str
        :rtype: bool
        """
        node = self.root
        for chars in word:
            node = node.data.get(chars)
            if not node:
                return False
        return node.is_word  # 判斷單詞是否是完整的存在在trie樹中

    def startsWith(self, prefix):
        """
        Returns if there is any word in the trie that starts with the given prefix.
        :type prefix: str
        :rtype: bool
        """
        node = self.root
        for chars in prefix:
            node = node.data.get(chars)
            if not node:
                return False
        return True

    def get_start(self, prefix):
        """
          Returns words started with prefix
          返回以prefix開頭的所有words
          如果prefix是一個word,那麼直接返回該prefix
          :param prefix:
          :return: words (list)
        """

        def get_key(pre, pre_node):
            word_list = []
            if pre_node.is_word:
                word_list.append(pre)
            for x in pre_node.data.keys():
                word_list.extend(get_key(pre + str(x), pre_node.data.get(x)))
            return word_list

        words = []
        if not self.startsWith(prefix):
            return words
        if self.search(prefix):
            words.append(prefix)
            return words
        node = self.root
        for chars in prefix:
            node = node.data.get(chars)
        return get_key(prefix, node)



基於Trie樹分詞流程如下:

from trie import Trie
import time


class TrieTokenizer(Trie):
    """
    基於字典樹(Trie Tree)的中文分詞算法
    """

    def __init__(self, dict_path):
        """

        :param dict_path:字典文件路徑
        """
        super(TrieTokenizer, self).__init__()
        self.dict_path = dict_path
        self.create_trie_tree()
        self.punctuations = """!?。"#$%&':()*+,-/:;<=>@[\]^_`{|}~⦅⦆「」、、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏."""

    def load_dict(self):
        """
        加載字典文件
        詞典文件內容如下,每行是一個詞:
                    AA制
                    ABC
                    ABS
                    AB制
                    AB角
        :return:
        """
        words = []
        with open(self.dict_path, mode="r", encoding="utf-8") as file:
            for line in file:
                words.append(line.strip().encode('utf-8').decode('utf-8-sig'))
        return words

    def create_trie_tree(self):
        """
        遍歷詞典,創建字典樹
        :return:
        """
        words = self.load_dict()
        for word in words:
            self.insert(word)

    def mine_tree(self, tree, sentence, trace_index):
        """
        從句子第trace_index個字符開始遍歷查找詞語,返回詞語佔位個數
        :param tree:
        :param sentence:
        :param trace_index:
        :return:
        """
        if trace_index <= (len(sentence) - 1):
            if sentence[trace_index] in tree.data:
                trace_index = trace_index + 1
                trace_index = self.mine_tree(tree.data[sentence[trace_index - 1]], sentence, trace_index)
        return trace_index

    def tokenize(self, sentence):
        tokens = []
        sentence_len = len(sentence)
        while sentence_len != 0:
            trace_index = 0  # 從句子第一個字符開始遍歷
            trace_index = self.mine_tree(self.root, sentence, trace_index)

            if trace_index == 0:  # 在字典樹中沒有找到以sentence[0]開頭的詞語
                tokens.append(sentence[0:1])  # 當前字符作爲分詞結果
                sentence = sentence[1:len(sentence)]  # 重新遍歷sentence
                sentence_len = len(sentence)
            else:  # 在字典樹中找到了以sentence[0]開頭的詞語,並且trace_index爲詞語的結束索引
                tokens.append(sentence[0:trace_index])  # 命中詞語作爲分詞結果
                sentence = sentence[trace_index:len(sentence)]  #
                sentence_len = len(sentence)

        return tokens

    def combine(self, token_list):
        """
        TODO:對結果後處理:標點符號/空格/停用詞
        :param token_list:
        :return:
        """
        flag = 0
        output = []
        temp = []
        for i in token_list:
            if len(i) != 1:  # 當前詞語長度不爲1
                if flag == 0:
                    output.append(i[::])
                else:
                    # ['該', '方法']
                    # temp=['該']
                    output.append("".join(temp))
                    output.append(i[::])
                    temp = []
                    flag = 0
            else:
                if flag == 0:
                    temp.append(i)
                    flag = 1
                else:
                    temp.append(i)
        return output


if __name__ == '__main__':
    now = lambda: time.time()
    trie_cws = TrieTokenizer('data/32w_dic.txt')
    start = now()
    print(f"Build Token Tree Time : {now() - start}")

    sentence = '該方法的主要思想:詞是穩定的組合,因此在上下文中,相鄰的字同時出現的次數越多,就越有可能構成一個詞。因此字與字相鄰出現的概率或頻率能較好地反映成詞的可信度。' \
               '可以對訓練文本中相鄰出現的各個字的組合的頻度進行統計,計算它們之間的互現信息。互現信息體現了漢字之間結合關係的緊密程度。當緊密程 度高於某一個閾值時,' \
               '便可以認爲此字組可能構成了一個詞。該方法又稱爲無字典分詞。'
    tokens = trie_cws.tokenize(sentence)
    combine_tokens = trie_cws.combine(tokens)
    end = now()
    print(tokens)
    print(combine_tokens)
    print(f"tokenize Token Tree Time : {end - start}")

分詞效果如下:

Build Token Tree Time : 0.0
['該', '方法', '的', '主要', '思想', ':', '詞', '是', '穩定', '的', '組合', ',', '因此', '在上', '下文', '中', ',', '相', '鄰', '的', '字', '同時', '出現', '的', '次數', '越', '多', ',', '就', '越', '有', '可能', '構成', '一個', '詞', '。', '因此', '字', '與', '字', '相', '鄰', '出現', '的', '概率', '或', '頻率', '能', '較好', '地', '反映', '成', '詞', '的', '可信度', '。', '可以', '對', '訓練', '文本', '中', '相', '鄰', '出現', '的', '各個', '字', '的', '組合', '的', '頻度', '進行', '統計', ',', '計算', '它們', '之', '間', '的', '互', '現', '信息', '。', '互', '現', '信息', '體現', '了', '漢字', '之', '間', '結合', '關係', '的', '緊密', '程度', '。', '當緊', '密', '程', ' ', '度', '高', '於', '某', '一個', '閾', '值', '時', ',', '便', '可以', '認爲', '此', '字', '組', '可能', '構成', '了', '一個', '詞', '。', '該', '方法', '又', '稱', '爲', '無字', '典', '分', '詞', '。']
['該', '方法', '的', '主要', '思想', ':詞是', '穩定', '的', '組合', ',', '因此', '在上', '下文', '中,相鄰的字', '同時', '出現', '的', '次數', '越多,就越有', '可能', '構成', '一個', '詞。', '因此', '字與字相鄰', '出現', '的', '概率', '或', '頻率', '能', '較好', '地', '反映', '成詞的', '可信度', '。', '可以', '對', '訓練', '文本', '中相鄰', '出現', '的', '各個', '字的', '組合', '的', '頻度', '進行', '統計', ',', '計算', '它們', '之間的互現', '信息', '。互現', '信息', '體現', '了', '漢字', '之間', '結合', '關係', '的', '緊密', '程度', '。', '當緊', '密程 度高於某', '一個', '閾值時,便', '可以', '認爲', '此字組', '可能', '構成', '了', '一個', '詞。該', '方法', '又稱爲', '無字']
tokenize Token Tree Time : 0.0005023479461669922

詞典以及語料庫

中文語料庫:包括情感詞典 情感分析 文本分類 單輪對話 中文詞典 知乎

中文相關詞典和語料庫。

中文詞典 / 中文詞典。Chinese / Chinese-English dictionaries.

中文漢語拼音辭典,漢字拼音字典,詞典,成語詞典,常用字、多音字字典數據庫

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章