1. 程式人生 > 其它 >中文分詞演算法

中文分詞演算法

中文分詞演算法的幾種方法(帶原始碼)

  正向最大匹配法(MM)

  逆向最大匹配法(RMM)

  雙向最大匹配法(BMM)

  隱含馬爾可夫模型(HMM)

  中文分詞工具——jieba

規則分詞

基於規則的分詞是一種機械分詞方法,主要通過維護字典,在切分語句時將語句的每個字串與詞表中的詞進行注意匹配,找到則切分,否則不予切分。

按照切分方式,主要有正向最大匹配法、逆向最大匹配法以及雙向最大匹配法三種

正向最大匹配法(MM):

  特徵:從左向右切分

示例程式碼:

"""
中文分詞技術
編輯者:饅頭
部落格:https://www.cnblogs.com/mantou0/
"""
# 正向最大匹配法
class MM(object):
    def __init__(self):
        self.windowSize = 3 # 詞典中最長的詞的詞長
        self.dic = ["研究","研究生","生命","命","的","起源"] # 詞典
    def cut(self,text):
        result = [] # 存放分割出的詞
        index = 0 # 初始化進行要分割的詞的初始索引
        text_length = len(text)

        while text_length > index:
            for size in range(self.windowSize+index,index,-1):
                piece = text[index:size]
                if piece in self.dic:
                    index = size -1
                    break
            index = index + 1
            result.append(piece+"---")
        print(result)
if __name__ == '__main__':
    text = "研究生命的起源"
    tokenizer = MM()
    tokenizer.cut(text)

 

逆向最大匹配法(RMM):

  特徵:從右向左切分

示例程式碼:

"""
中文分詞技術
編輯者:饅頭
部落格:https://www.cnblogs.com/mantou0/
"""
# 逆向最大匹配法
class RMM(object):
    def __init__(self):
        self.windowSize = 3 # 詞典中最長的詞的詞長
        self.dic = ["研究","研究生","生命","命","的","起源"] # 詞典
    def cut(self,text):
        result = [] # 存放分割出的詞
        index = len(text) # 初始化進行要分割的詞的初始索引
        while index > 0:
            for size in range(index - self.windowSize,index):
                piece = text[size:index]
                if piece in self.dic:
                    index = size + 1
                    break
            index = index - 1
            result.append(piece+"---")
        result.reverse()
        print(result)


if __name__ == '__main__':
    text = "研究生命的起源"
    tokenizer = RMM()
    tokenizer.cut(text)

  

雙向最大匹配法(BMM):

  特徵:正逆對比,然後按照最大匹配原則

示例程式碼:

"""
中文分詞技術
編輯者:饅頭
部落格:https://www.cnblogs.com/mantou0/
"""
class M(object):
    def __init__(self):
        self.windowSize = 3 # 詞典中最長的詞的詞長
        self.dic = ["研究","研究生","生命","命","的","起源"] # 詞典

    #正向最大匹配演算法
    def MM(self,text):
        result = [] # 存放分割出的詞
        index = 0 # 初始化進行要分割的詞的初始索引
        text_length = len(text)
        while text_length > index:
            for size in range(self.windowSize+index,index,-1):
                piece = text[index:size]
                if piece in self.dic:
                    index = size -1
                    break
            index = index + 1
            result.append(piece+"---")
        return result

    #逆向最大匹配演算法
    def RMM(self, text):
        result = []  # 存放分割出的詞
        index = len(text)  # 初始化進行要分割的詞的初始索引
        while index > 0:
            for size in range(index - self.windowSize, index):
                piece = text[size:index]
                if piece in self.dic:
                    index = size + 1
                    break
            index = index - 1
            result.append(piece + "---")
        result.reverse()
        return result

    def BMM(self, MM_result,RMM_result):
        """
           比較兩個分詞方法分詞的結果

           比較方法:
               1. 如果正反向分詞結果詞數不同,則取分詞數量較少的那個
               2. 如果分詞結果詞數相同:
                   2.1 分詞結果相同,說明沒有歧義,可返回任意一個
                   2.2 分詞結果不同,返回其中單字較少的那個

           :param MM_result: 正向最大匹配法的分詞結果
           :param RMM_result: 逆向最大匹配法的分詞結果
           :return:
               1.詞數不同返回詞數較少的那個
               2.詞典結果相同,返回任意一個(MM_result)
               3.詞數相同但是詞典結果不同,返回單字最少的那個
           """
        if len(MM_result) != len(RMM_result):
            # 如果兩個結果詞數不同,返回詞數較少的那個
            return MM_result if (len(MM_result) < len(RMM_result)) else RMM_result
        else:
            if MM_result == RMM_result:
                # 因為RMM的結果是取反了的,所以可以直接匹配
                # 詞典結果相同,返回任意一個
                return MM_result
            else:
                # 詞數相同但是詞典結果不同,返回單字最少的那個
                MM_word_1 = 0
                RMM_word_1 = 0
                for word in MM_result:
                    # 判斷正向匹配結果中單字出現的詞數
                    if len(word) == 1:
                        MM_word_1 += 1

                for word in RMM_result:
                    # 判斷逆向匹配結果中單字出現的詞數
                    if len(word) == 1:
                        RMM_word_1 += 1

                if (MM_word_1 < RMM_word_1):
                    return MM_result
                else:
                    return RMM_result

if __name__ == '__main__':
    text = "研究生命的起源"
    token = M()
    token.BMM(token.MM(text),token.RMM(text))

 

統計分詞

建立統計語言模型,對句子進行單詞劃分,然後對劃分的結果進行概率計算,獲得概率最大的分詞方式。這裡就用到了統計學習演算法,隱含馬爾可夫模型(HMM)、條件隨機場(CRF)

隱含馬爾可夫模型(HMM):

  需要提前準備好詞典    需要詞典和原始碼可以去我的github下載,例如

示例程式碼:

"""
中文分詞技術
編輯者:饅頭
部落格:https://www.cnblogs.com/mantou0/
"""

# encoding=utf-8
class HMM(object):
    def __init__(self):
        """
            方法:初始化引數
        """
        # 主要是用於存取演算法中間結果,不用每次都訓練模型
        self.model_file = './data/hmm_model.pkl'

        # 狀態值集合
        self.state_list = ['B', 'M', 'E', 'S']
        # 引數載入,用於判斷是否需要重新載入model_file
        self.load_para = False

    def try_load_model(self, trained):
        """
            方法:用於載入已計算的中間結果,當需要重新訓練時,需初始化清空結果
            輸入: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):
        """
            方法:計算轉移概率、發射概率以及初始概率
            輸入:path:訓練材料路徑
        """

        # 重置幾個概率矩陣
        self.try_load_model(False)

        # 統計狀態出現次數,求p(o)
        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):
            """
                方法:為訓練材料每個詞劃BMES
                輸入:text:一個詞
                輸出:out_text:劃好的一個BMES列表
            """
            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 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()}
        # 加1平滑
        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):
        """
            方法:維特比演算法,尋找最優路徑,即最大可能的分詞方案
            輸入:text:文字
                 states:狀態集
                 start_p:第一個字的各狀態的可能
                 trans_p:轉移概率
                 emit_p:發射概率
            輸出:prob:概率
                 path:劃分方案
        """
        V = [{}]  # 路徑圖
        path = {}

        for y in states:  # 初始化第一個字的各狀態的可能性
            V[0][y] = start_p[y] * emit_p[y].get(text[0], 0)
            path[y] = [y]
        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  # 設定未知字單獨成詞
                # y0上一個字可能的狀態,然後算出當前字最可能的狀態,prob則是最大可能,state是上一個字的狀態
                (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:]


hmm = HMM()
hmm.train('./data/trainCorpus.txt_utf8')
text = '研究生命的起源'
res = hmm.cut(text)
print(text)
print(str(list(res)))

  

中文分詞工具——jieba

  今年來,隨著NLP技術的日益成熟,開源實現的分詞工具越來越多,如,Ansj,盤古分詞等。Jieba分詞結合了基於規則和基於統計這兩類方法。

Jieba提供了三種分詞模式:

精準模式:

  試圖將橘子最精準的切開,適合文字分析。

全模式:

  把句子中所有可以成詞的詞語都掃描出來,速度非常快,但是不能解決歧義。

搜尋引擎模式:

  在精準模式的基礎上,對長詞再次切分,提高召回率,適合用於搜尋引擎分詞。

示例程式碼:

"""
中文分詞技術
編輯者:饅頭
部落格:https://www.cnblogs.com/mantou0/
"""
import jieba
sent = '中文分詞工具是文字處理不可或缺的一步!'
seg_list = jieba.cut(sent,cut_all=True)
print('全模式:','/'.join(seg_list))
seg_list = jieba.cut(sent,cut_all=False)
print('精準模式:','/'.join(seg_list))
seg_list = jieba.cut(sent)
print('預設精準模式:','/'.join(seg_list))
seg_list = jieba.cut_for_search(sent)
print('搜尋引擎模式:','/'.join(seg_list))