基于词典匹配的中文分词
中文分词在中文的自然语言处理里非常重要,比如在中文的搜索引擎()或者语义分析中,词语粒度的分析效果是非常好的。
本篇介绍基于词典匹配的中文分词算法。
1. 使用效果
1.1 输入
输入待分词的句子或文章,如:
黑夜给了我黑色的眼睛,我却用它寻找光明。
1.2 输出
返回分词结果:
黑夜/给/了/我/黑色/的/眼睛/,/我/却/用/它/寻找/光明/。
2. 使用过程
2.1 模型训练
-
根据已经分好词的训练语料,统计词频,得到词典。
Done。
基于词典匹配的中文分词就是这么简单。
当然,其它的效果更好的模型,训练过程就复杂得多了,后续会不断分享。
2.2 模型测试
-
加载词典。
-
对输入语句进行分词。
2.3 模型评估
2.3.1 测试数据
自然语言是一个不好量化的领域,中文分词的结果没有事实上的标准,语言学家标注的分词结果有的时候也存在争议。
在对比不同的中文分词模型的效果与性能时,选择同样的标注数据进行评估,可以在一定程度上对比下模型之间的优劣。
本篇选用第二届国际汉语分词评测比赛(http://sighan.cs.uchicago.edu/bakeoff2005/)提供的数据中的msr数据,包括:
训练数据:training/msr_training.utf8
测试数据:testing/msr_test.utf8
测试数据基准数据:gold/msr_test_gold.utf8
2.3.2 评价指标
标准的中文分词评测的维度很多,包括词典内外的分词效果等,本篇只关注3个指标:
精准率: Precision
召回率: Recall
Precision与Recall的调和均值: F1_score
分词结果对比示例,
测试结果: [中国/人/真/伟大], 一共4个词语。
基准结果: [中国人/真/伟大],一共3个词语。
正确分出的词语是2个:[真,伟大]
Precision = 2 / 4
Recall = 2 / 3
2.3.3 测试过程
-
使用分词算法对待测试文件testing/msr_test.utf8分词。
-
将分词结果与测试数据基准数据gold/msr_test_gold.utf8进行比对。
3. 模型训练
3.1 生成词典
def genearte_dictionary(train_path=None, dict_path=None):
"""
train_data: Labeled words split by space.
word_dict: {word: word_count}
"""
# Word counting.
word_dict = {}
with open(train_path, 'r', encoding='utf-8') as f:
for line in f:
for word in line[:-1].split(' '):
word = word.strip()
if len(word) == 0:
continue
if word not in word_dict:
word_dict[word] = 1
else:
word_dict[word] += 1
# Save word_dict to disk.
with open(dict_path, 'wb') as fw:
pickle.dump(word_dict, fw)
4. 模型测试:分词
4.1 加载词典
以下正向、反向以及双向最大匹配算法均需要首先加载词典。
def load_dictionary(dict_path=None):
with open(dict_path, 'rb') as fr:
word_dict = pickle.load(fr)
return word_dict
return None
4.2 正向最大匹配算法
从左到右扫描待分词句子,从最大长度开始匹配,如假定最大分词长度为5。
这钟声,传递出中华民族从磨难中奋起的昂扬斗志,彰显出伟大民族精神在新时代焕发出的熠熠光辉。
-
依次判断“这钟声,传”、“这钟声,”、“这钟声”、“这钟”以及“这”是否在词典里,如果有一个在词典里,则在该词语处切分。
-
如果所有词语都不在词典里,则在第一个字处切分。
-
往右继续相同切分方式。
最终得到正向最大匹配分词结果:
这/钟声/,/传递/出/中华民族/从/磨难/中/奋起/的/昂扬/斗志/,/彰/显出/伟大/民族/精神/在/新时代/ 焕发/出/ 的/熠熠/光辉/。
def forward_maximum_matching(word_dict=None,
max_num_char=None,
sentence=None):
words = []
start = 0
while start < len(sentence):
cur_word = None
for len_word in range(max_num_char, 0, -1):
maybe_word = sentence[start: start+len_word]
if maybe_word in word_dict:
cur_word = maybe_word
start += len_word
break
if cur_word is None:
cur_word = sentence[start]
start += 1
words.append(cur_word)
return words
4.3 反向最大匹配算法
一些学者发现的现象是,在中文里,往往作者表达更含蓄,最想要表达的含义在句子的末尾部分;在现实中反向最大匹配算法有时候表现得更好。
反向最大匹配算法从右往左从句子的末尾开始匹配,假定仍然选择最大词语长度为5,
-
依次判断“熠熠光辉。”、“熠光辉。”、“光辉。”、“辉。”以及“。”是否在词典里,如果在,则在该词语处切分。
-
如果所有词语都不在词典里,则在最后一个字处切分。
-
往左继续相同切分方式。
反向最大匹配分词结果:
这/钟声/,/传递/出/中华民族/从/磨难/中/奋起/的/昂扬/斗志/,/彰/显出/伟大/民族/精神/在/新时代/ 焕/发出/ 的/熠熠/光辉/。
与正向最大匹配最明显的不同是,反向会把“焕发出”分成“焕”与“发出”;而正向分成“焕发”与“出”。方向性非常明显。
def backward_maximum_matching(word_dict=None,
max_num_char=None,
sentence=None):
words = []
start = len(sentence)
while start > 0:
cur_word = None
for len_word in range(max_num_char, 0, -1):
maybe_word = sentence[max(0, start-len_word): start]
if maybe_word in word_dict:
cur_word = maybe_word
start -= len_word
break
if cur_word is None:
cur_word = sentence[start-1]
start -= 1
words.append(cur_word)
words.reverse()
return words
4.4 双向最大匹配算法
双向最大匹配算法同时进行正向与反向最大匹配,从两个结果里启发式地选择最好的一个:
-
比较两种分词结果分出的词的总数量,选择词语数量少的那一个;如在“中华/民族/真/伟大”与“中华民族/真/伟大”里选择后者,前者有4个词语,后者有3个词语,认为后者对词典利用得更好。
-
如果两种分词词语数量相等,选择单字个数少的那一个;如在“中国人/真/伟/大”与“中国/人/真/伟大”中选择后者,二者都有4个词语,后者只有2个单字,而前者有3个单字,单字少的意味着两字以上的词语更多,多字词语比单字往往能表达更多含义。
def bidirectional_maximum_matching(word_dict=None,
max_num_char=None,
sentence=None):
forward_words = forward_maximum_matching(word_dict=word_dict,
max_num_char=max_num_char,
sentence=sentence)
backward_words = backward_maximum_matching(word_dict=word_dict,
max_num_char=max_num_char,
sentence=sentence)
"""
1. The less number of words, the better.
2. If equal number of words, the less number of single character word, the better.
"""
if len(forward_words) < len(backward_words):
return forward_words
elif len(forward_words) > len(backward_words):
return backward_words
else:
if count_single_char(forward_words) < count_single_char(backward_words):
return forward_words
else:
return backward_words
def count_single_char(words):
cnt = 0
for word in words:
if len(word) == 1:
cnt += 1
return cnt
5. 模型评估
5.1 对测试文件分词
对测试文件遍历每行分词。
5.2 模型评估
5.2.1 模型评估计算
同步对比测试与基准数据。
精准率Precision等于测试数据正确分词个数除以测试数据总的分词个数。
Precision = num_words_correct / num_words_predict
召回率Recall等于测试数据正确分词个数除以基准数据总的词语个数。
Recall = num_words_correct / num_words_baseline
因此,核心是要计算测试数据正确分词个数。
这里不能直接计算两个数组的交集个数,因为分词结果是有序的。如:
测试数据:['大', '家', '都是', '大学生']
基准数据:['大家', '都', '是', '大', '学生']
直接考虑交集,会认为'大'是一个正确的分词。然而一个'大'在句子的开头,一个在句子的靠后位置,实际上测试数据一个词也没有切分正确。
在计算交集的时候,必须要考虑词语在句子中的位置。
将每一个词在句子中的位置用起始位置表示出来,用python的区间表示方式,左边为闭区间,右边为开区间,则上述数据的位置表示为,
测试数据:[(0, 1), (1, 2), (2, 4), (4, 7)]
基准数据:[(0, 2), (2, 3), (3, 4), (4, 5), (5, 7)]
得到了互相不重叠的顺序的区间起始集,从而可以转换成集合set来取交集。
def compute_num_intersect(buf_1, buf_2):
if len(buf_1) == 0 or len(buf_2) == 0:
return 0
indexes_1 = to_index(buf_1)
indexes_2 = to_index(buf_2)
# Computing intersects between sets, using `&`.
intersects = indexes_1 & indexes_2
if intersects is not None:
return len(intersects)
return 0
def to_index(buf):
"""
buf: word_0, word1, ..., word_{N-1}
indexes(python style [start, end)):
[start_0, end_0], [end_0, end_1], ..., [end_{N-2}, end_{N-1}=end]
"""
indexes = []
start = 0
for word in buf:
end = start + len(word)
indexes.append((start, end))
start = end
return set(indexes)
计算起来就比较直接了。
def compute_fscore(y_true_path=None, y_pred_path=None):
fr_true = open(y_true_path, 'r', encoding='utf-8')
fr_pred = open(y_pred_path, 'r', encoding='utf-8')
n_correct = 0
n_true = 0
n_pred = 0
for true_line, pred_line in zip(fr_true, fr_pred):
true_buf = []
for word in true_line[:-1].split(' '):
if len(word) == 0:
continue
true_buf.append(word)
pred_buf = []
for word in pred_line[:-1].split(' '):
if len(word) == 0:
continue
pred_buf.append(word)
cur_n_correct = compute_num_intersect(true_buf, pred_buf)
n_correct += cur_n_correct
n_true += len(true_buf)
n_pred += len(pred_buf)
if n_correct == 0:
return False
precision = n_correct / n_pred
recall = n_correct / n_true
f1_score = 2 * precision * recall / (precision + recall)
print("precision\trecall\tf1_score")
print("%.4f\t\t%.4f\t%.4f" %
(precision, recall, f1_score))
fr_true.close()
fr_pred.close()
return True
5.2.2 模型评估结果
模型 | Precision | Recall | F1_score |
---|---|---|---|
正向最大匹配 | 0.9120 | 0.9541 | 0.9325 |
反向最大匹配 | 0.9101 | 0.9522 | 0.9306 |
双向最大匹配 | 0.9121 | 0.9536 | 0.9324 |
注:最大词语长度设置为6。
6. 总结
本篇介绍了简单的基于词典匹配的中文分词算法,简单易用。
当然,也因为过于简单,在工业界生产系统上使用效果会不够好;我们需要研究更好的中文分词算法。
7. 参考资料
-
jieba分词:https://github.com/fxsjy/jieba
-
HanLP分词:https://github.com/hankcs/HanLP
-
《自然语言处理入门》,何晗(HanLP作者)。
详细代码见我的Github,有问题欢迎一起讨论。