中文分词算法

中文分词算法的几种方法(带源码

  正向最大匹配法(MM)

  逆向最大匹配法(RMM)

  双向最大匹配法(BMM)

  隐含马尔可夫模型(HMM)

  中文分词工具——jieba

规则分词

基于规则的分词是一种机械分词方法,主要通过维护字典,在切分语句时将语句的每个字符串与词表中的词进行注意匹配,找到则切分,否则不予切分。

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

正向最大匹配法(MM):

  特征:从左向右切分

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""
中文分词技术
编辑者:馒头
博客: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):

  特征:从右向左切分

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"""
中文分词技术
编辑者:馒头
博客: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):

  特征:正逆对比,然后按照最大匹配原则

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""
中文分词技术
编辑者:馒头
博客: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下载,例如

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
"""
中文分词技术
编辑者:馒头
博客: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提供了三种分词模式:

精准模式:

  试图将橘子最精准的切开,适合文本分析。

全模式:

  把句子中所有可以成词的词语都扫描出来,速度非常快,但是不能解决歧义。

搜索引擎模式:

  在精准模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""
中文分词技术
编辑者:馒头
博客: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))

  

github下载源码

 

 



如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。

本文作者:mt0u的Blog

本文链接:https://www.cnblogs.com/mt0u/p/16226532.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   mt0u  阅读(370)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示