一、统计分词简介
随着大规模语料库的建立,统计机器学习方法的研究和发展,基于统计的中文分词算法渐渐成为主流,其主要思想是把每个词看作是词的最小单位的各个字组成的,如果相连的字在不同的文本中出现的而次数越多,就证明这相连的字很可能是一个词。因此我们就可以利用字与字相邻出现的频率来反应成词的可靠度,统计语料中相邻共现的各个字的组合的频度,当组合频度高于某一个临界值时,我们便可认为此字组可能会构成一个词语。
基于统计的分词,一般要做如下两步操作:
1)建立统计语言模型。
2)对句子进行单词的划分,然后对划分结果进行概率计算,获得概率最大的分词方式。其中用于分词的模型可以是隐含马尔可夫(HMM)或者是条件随机场(CRF)等统计学习的算法。
二、语言模型介绍
对于一个句子来说,用概率论的专业术语描述语言模型就是:为长度m的字符串确定其概率分布为,其中w1到wm依次表示文本中各个词语。一般采用链式法则计算其概率值
当文本过长的时候,公式右部从第三项起的每一项计算难度都很大。为解决问题,有人提出了n元模型(n-gram)降低该计算难度。所谓的n元模型就是在估算条件概率时,忽略距离大于等于n的上文词的影响,因此的计算可以简化成
对于这个公式的优化目的是保证一定的分词正确性的同时尽最大可能简化公式的计算代价。很明显对于n越大那么这个模型的分词正确率应该是越高的,相对应的是极大的计算代价,这样的计算代价很明显是无法接受的,因此在缩短n的长度的同时保证一定的分词准确性是模型最应该体现的一点。
三、隐含马尔可夫模型
隐含马尔可夫模型(HMM)是将分词作为字在字符串中的序列标注任务来实现的。其基本思路是:每个字在构造一个特定的词语时都占据着一个确定的构词位置(即词位),现规定每一个字最多只有四个构词位置:即B(词首)、M(词中)、E(词尾)、S(单独成词),对于下面句子1)的分词结果就可以直接表示成如2)所示的逐字标注形式:
1)中文/分词/是/文本处理/不可或缺/的/一步!
2)中/B文/E分/B词/E是/S文/B本/M处/M理/E不/B可/M或/M缺/E的/S一/B步/E!/S
class HMM_Cell(object):
def __init__(self, config):
self.n_classes = config.n_classes
self.n_feature = config.n_feature
# init define three probability
self.init_probability = np.zeros((self.n_classes, 1))
self.transition = np.zeros((self.n_classes, self.n_classes))
self.emission = np.zeros((self.n_feature, self.n_classes))
def fit(self, x, y):
"""
using train data and train target to fit three probabilities
:param x: example x is like [[12, 33, 44], [22, 332], [231]]
:param y: example y is like [[1, 2, 3], [0, 2], [3]]
:return: None
"""
for i in range(len(x)):
for j in range(len(x[i])):
# init probability
if j == 0:
self.init_probability[y[i][j]] += 1
# transition probability
else:
self.transition[y[i][j], y[i][j - 1]] += 1
# emission probability
self.emission[x[i][j]][y[i][j]] += 1
# laplace smoothing
self.init_probability = (self.init_probability + 1) / np.sum(self.init_probability)
self.transition = (self.transition + 1) / (np.sum(self.transition) + self.n_classes)
self.emission = (self.emission + 1) / (np.sum(self.emission) + self.n_feature)
# viterbi algorithm(without start node and stop node)
def predict_label(self, x):
"""
using Viterbi algorithm and probabilities to calculate max score path
:param x: example x is like [[12, 33, 44], [22, 332], [231]]
:return: predicted label, the shape is same as x
"""
best_path_ids, label_col = [], []
for i in range(len(x)):
# forward_var denote end this step each node will get max score
forward_var = np.log(self.init_probability)
for j in range(len(x[i])):
if j == 0:
forward_var += np.log(self.emission[x[i][j]].reshape(-1, 1))
else:
current_score, current_ids = np.zeros((self.n_classes, 1)), np.zeros(self.n_classes, dtype=int)
for next_tag in range(self.n_classes):
next_score = forward_var + np.log(self.transition[next_tag].reshape(-1, 1))
max_score_id = np.argmax(next_score.reshape(-1))
current_score[next_tag] = np.max(next_score)
current_ids[next_tag] = max_score_id
# update forward_var with each step
forward_var = current_score + np.log(self.emission[x[i][j]].reshape(-1, 1))
best_path_ids.append(current_ids.tolist())
start_node = np.argmax(forward_var)
label_col.append(self.find_path(start_node, best_path_ids))
best_path_ids = []
return label_col
def find_path(self, start_node, best_path_ids):
label = [start_node]
for i in range(len(best_path_ids) - 1, -1, -1):
label.append(best_path_ids[i][label[-1]])
label.reverse()
return label