中文分词原理和代码实践

1 简介和分类

中文分词(Chinese Word Segmentation) 指的是将一个汉字序列切分成一个一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。

现有的分词算法可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法(本文主要讲述该方法)

1.1基于字符串匹配的分词方法:

这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个"充分大的"机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)

1)正向最大匹配法(由左到右的方向)

2)逆向最大匹配法(由右到左的方向):

3)最少切分(使每一句中切出的词数最小)

4)双向最大匹配法(进行由左到右、由右到左两次扫描)

1.2基于理解的分词方法:

这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。

1.3基于统计的分词方法:

给出大量已经分词的文本,利用统计机器学习模型学习词语切分的规律(称为训练),从而实现对未知文本的切分。例如最大概率分词方法和最大熵分词方法等。 随着大规模语料库的建立,统计机器学习方法的研究和发展,基于统计的中文分词方法渐渐成为了主流方法。

主要统计模型:N元文法模型(N-gram),隐马尔可夫模型(Hidden Markov Model ,HMM),最大熵模型(ME),条件随机场模型(Conditional Random Fields,CRF)等

2 正向最大匹配法和逆向最大匹配法

使用trie树建立词典,进行正向和反向查找

 

例如:大学生活动中心

正向最大匹配法:根据中文句子,从左到右去trie树匹配查找长度最大的词语。

如上图,大学生/活动/中心,先匹配出"大学生",然后"活动",最后"中心"

 

逆向最大匹配法:根据中文句子,从右(句子末尾)到左去trie树匹配查找长度最大的词语。

如上图,大学生/活动/中心,先匹配出"心中",然后"动活",最后"生学大"

3 基于统计分词介绍

3.1 概率语言模型

假设要分出来的词在语料库和词表中都存在,则最简单的切方法是按词计算概率,而不是按字计算概率(没有意义)。

分词问题,可以看作:输入一个字串C=c1,c2,c3...cn,输出一个词串S=w1,w2,w3...wm,m<=n,即分出的单词数m<=原来字串的长度。对于一个特定的字串C都有多种切分方案S,分词的目标就是找出最有可能出现的S方案,即使得P(S|C)概率最大,P(S|C)就是由字符串C切分出S方案的概率,也就是对输入字符串切分出最有可能的词序列。

例如,对于输入字符串"南京市长江大桥",有下面2中切分方案S:

S1: 南京市/长江/大桥

S2: 南京/市长/江大桥

计算条件概率P(S1|C),P(S2|C),选择出概率较大的作为切分方案。

P(C)表示字串C在语料库中出现的概率。比如语料库中有1万的句子,"南京市长江大桥"在该语料库中出现了1次,则P(C)=P(南京市长江大桥)=万分之一。

由条件概率公式得知:p(C,S)=P(S|C)*P(C)=P(C|S)*P(S),

故贝叶斯公式:p(S|C)=p(C|S)*p(S)/p(C)。

其中:P(C)是一个固定值,另外从词串恢复到字串的概率只有唯一一种方式,故P(C|S)=1

所以比较P(S1|C),P(S2|C)的大小,可以化简为:

P(S1|C)/P(S2|C)=P(S1)/P(S2)

因为P(S1)=p(南京市,长江,大桥)=P(南京市)P(长江)P(大桥)>P(S2)=p(南京,市长,江大桥)=P(南京)P(市长)P(江(大桥),故选择S1

3.1.1一元模型

对于不同的S,m的值是不一样的,一般地,m越大,P(S)与越小,即分出的词越多,概率越小

P(S)计算公式:P(S)=P(w1)*P(w2)*P(w3)...P(wn),

P(wi)=(wi在语料库中出现的次数n) / (语料库中的总词数N)

该公式是基于独立假设的,即词与词之间是相互独立的,因此该公式也叫一元模型

3.1.2 N元模型

假设在日本,和服是一个常见词,按照一元模型,可能会把"产品和服务"切分为:产品/和服/务,为了切分更准确需要考虑词所处的上下文

N元模型使用n个单词组成的序列来衡量切分方案的合理性:

估计单词w1出现后,w2出现的概率P(w1,w2)= P(w1)P(w2|w1)

同理,P(w1,w2,w3)= P(w1,w2)*P(w3|w1,w2)=P(w1)P(w2|w1)*P(w3|w1,w2)

更加一般的形式:

P(w1,w2,w3...wn)= P(w1)P(w2|w1)*P(w3|w1,w2)...*P(wn|w1,w2,w3...wn-1)

如果简化成一个词的出现仅仅依赖前面一个词,则为二元模型(Bigram)

P(S)=P(w1,w2,w3...,wn)≈P(w1)P(w2|w1)*P(w3|w2)...P(wn|wn-1)

3.1.3 马尔可夫模型(MM:Markov Model)

N元模型又可以看作马尔可夫模型:每个状态只依赖之前有限个状态

参数:

状态,由数字表示,假设共有M个

初始概率,由表示

状态转移概率,由表示

k,l=1,2,...M

3.1.4 隐马尔可夫模型HMM(Hidden Markov Model)

马尔可夫模型是对一个序列建模,但有时我们需要对2个序列建模,

如机器翻译,源语言序列 -> 目标语言序列

语言识别,语言信号序列 -> 文字序列

通常一个序列称为观测序列,一个为隐藏序列,如语言识别中,声波信号为观测序列,记为O,实际上要表达的文字观测不到,记为S,我们要找的就是S,即"说了什么"。

观测序列O中的数据通常是由隐藏序列数据决定的,彼此间相互独立

隐藏序列数据间相互依赖,通常构成了马尔可夫序列

例如语言识别中,每段声波信号是相互独立的,有对应的文字决定

对应的文字序列中,文字间相互依赖,构成马尔可夫序列

观测和隐藏序列共同构成隐马模型:

O(O1,O2,...OT),观测序列,Ot只依赖St

S(S1,S2,...ST),状态序列(隐藏序列),S是马尔可夫序列,假设一阶马尔可夫,则St+1只依赖于St

HMM参数

状态由数字表示,假设共有M个

观测由数字表示,假设共有N个

初始概率(所有初始状态中k状态的样本数/所有初始状态的样本数),由表示

状态转移概率(k状态到l状态的概率a(k,l)=所有k状态下到l的频次/所有k状态的频次),由表示

k,l=1,2,...M

发射概率(由k状态发射到u观测值的概率=k状态下观测值u的频次/k状态的频次),由表示

u=1,2,...N k=1,2,...M

HMM生成过程:

先生成第一个状态,然后依次由当前状态生成下一个状态,最后每个状态发射出一个观测值

公式:

注意:p(S1)就是初始状态概率,当前观测值Ot依赖于当前状态St,当前状态St依赖于前一个状态St-1,该公式表示的是生成序列Ot和St的概率

问题:

根据给定的HMM参数(πk,a(k,l),bk(u))和观测序列O,如何求隐藏序列S

分析:根据公式,每个可能的S序列都能计算出一个概率,而概率最大的那个S序列即为解

 

4 中文分词代码实践

allfiles.txt 链接:https://pan.baidu.com/s/17Dx5dBQRf8UyLzbvZ1BrGQ
提取码:uggl

'''
隐马尔可夫模型
三个参数θ的计算:
假设隐藏状态共M个
初始状态概率(M)= sum(每篇文章第一个字为k状态的频次) / sum(每篇文章第一个字的状态的频次)
状态转移概率(M*M)= sum(每篇文章k状态到l状态的频次) / sum(每篇文章k状态的频次)
发射概率(M*N,N表示句子的长度)= sum(每篇文章k状态下某观测值Ot的频次) / sum(每篇文章k状态的频次)

训练:样本为allfiles.txt,该文件一行是一篇文章,且每篇文章已经分词
即在已知每篇文章的分词序列后,计算θ,然后根据θ和观测序列O(如一个未分词的句子),计算最佳的隐藏序列S,
知道S后即可分词

θ的计算:
1 遍历每篇文章,计算每篇文章各个字的隐藏状态S(S in {BMES})
2 统计各个初始状态的频次,某状态的频次,某状态到某状态的频次,某状态到某个文字的频次
3 根据公式计算3种概率
4 结果存储到磁盘
'''

import math
import jieba

'''读取数据'''
wiki_flag=False
pre = 'F:\\00-data\\'
if wiki_flag:
    train_file='F:\\00-data\word2vec\\wiki.txt'
else:
    train_file = pre + 'allfiles.txt'
save_file = pre + 'out\\hmm\\hyj_hmm.model'
# 隐藏状态数量
STATUS_NUM = 4
# 频次为0的默认无穷小概率
min_prob = -100000.0


def get_word_ch_list(word: str):
    '''
    将词语转换为文字列表
    :param word:
    :return:
    '''
    if word is None or word.strip() == '':
        return []
    res = []
    for ch in word:
        res.append(ch)

    return res


def get_status_list_by_chlist(ch_list: []):
    '''
    根据词语的文字列表获取隐藏状态列表:BMES ->[0,1,2,3]
    :param ch_list:
    :return: BMES ->[0,1,2,3]
    '''
    len_ch = len(ch_list)
    if len_ch == 0:
        return []
    if len_ch == 1:
        # S
        return [3]

    res = [-1 for i in range(len_ch)]
    # B
    res[0] = 0
    # E
    res[-1] = 2
    for j in range(1, len_ch - 1):
        # M
        res[j] = 1

    return res


def count_b(b_matrix, ch_list, i, status_list):
    '''
    发射频次统计
    :param b_matrix:
    :param ch_list:
    :param i:
    :param status_list:
    :return:
    '''
    # 下标为i的文字
    ch_i = ch_list[i]
    # 下标为i的状态
    status_i = status_list[i]
    # 如果某状态下某文字为空,则初始化计数为0
    if b_matrix[status_i].get(ch_i, -1) == -1:
        b_matrix[status_i][ch_i] = 0.
    b_matrix[status_i][ch_i] += 1


def train_theta():
    '''
    训练入口
    :return:
    '''
    # 初始状态概率 M个
    start_prob = [0. for i in range(STATUS_NUM)]
    # 状态转移概率矩阵 M*M
    a_matrix = [[0. for i in range(STATUS_NUM)] for i in range(STATUS_NUM)]
    # 发射概率矩阵 M*N,N表示某状态下的所有文字种数,由于N不确定,所以用dict存储(key=文字,value=概率)
    b_matrix = [dict() for i in range(STATUS_NUM)]
    # 初始状态总数
    total_start_status = 0.
    # 每个状态的总频次数
    total_status_dict = dict()
    with open(train_file, mode='r', encoding='utf-8') as f:
        '''
        readline(): 
        1、每次读取下一行文件。 
        2、可将每一行数据分离。 
        3、主要使用场景是当内存不足时,使用readline()可以每次读取一行数据,只需要很少的内存。 
        readlines(): 
        1、一次性读取所有行文件。 
        2、可将每一行数据分离,从代码中可以看出,若需要对每一行数据进行处理,可以对readlines()求得的结果进行遍历。 
        3、若内存不足无法使用此方法。
        '''
        # 一行是一篇文章
        line = f.readline()
        # 文件读完为止
        while line and line.strip() != '':
            print('while line==True')
            # 每篇文章的所有单词 split()默认分隔符为空格
            words = line.strip().split()
            if len(words) == 0:
                return

            # 统计初始状态频次
            start_word = words[0]
            start_ch_list = get_word_ch_list(start_word)
            start_status_list = get_status_list_by_chlist(start_ch_list)
            start_prob[start_status_list[0]] += 1.
            total_start_status += 1

            # 一篇文章相当于一个观测序列和一个隐藏序列,文章和文章之间没有关系,所以θ不会跨文章统计
            # 每篇文章的隐藏状态序列S
            article_ch_status_list = []
            # 每篇文章的文字序列O
            article_ch_list = []
            #  words[:-1]最后一个不是词语,需要过滤
            if wiki_flag:
                word_list=words
            else:
                word_list=words[:-1]

            for word in word_list:
                # 词语的文字序列
                ch_list = get_word_ch_list(word)
                # 词语的状态序列
                status_list = get_status_list_by_chlist(ch_list)
                if len(ch_list) == 0 or len(status_list) == 0:
                    continue

                # 将词语序列转换为文章序列
                article_ch_list.extend(ch_list)
                article_ch_status_list.extend(status_list)

            # 每篇文章的频次统计
            article_ch_len = len(article_ch_status_list)
            for i in range(0, article_ch_len - 1):
                # 状态转移频次统计
                k_status = article_ch_status_list[i]
                j_status = article_ch_status_list[i + 1]
                a_matrix[k_status][j_status] += 1.
                count_status(k_status, total_status_dict)

                # 发射频次统计
                count_b(b_matrix, article_ch_list, i, article_ch_status_list)
            # 由于i的最大长度减了1,故还要统计每篇文章最后一个字的状态频次和发射频次
            count_status(article_ch_status_list[-1], total_status_dict)
            count_b(b_matrix, article_ch_list, i + 1, article_ch_status_list)
            line = f.readline()

    print('count pi:', start_prob)
    print('count A:', a_matrix)
    print('count B:', b_matrix)

    '''计算概率'''
    print('compute  prob...')
    for i in range(STATUS_NUM):
        # 初始状态概率
        # 概率取对数
        prob = start_prob[i] / total_start_status
        if prob > 0:
            start_prob[i] = math.log(prob)
        else:
            # 如果频次为0 则给一个默认的无穷小概率
            start_prob[i] = min_prob

        # 状态转移概率
        for j in range(STATUS_NUM):
            prob = a_matrix[i][j] / total_status_dict[i]
            if prob > 0.:
                a_matrix[i][j] = math.log(prob)
            else:
                a_matrix[i][j] = min_prob

        # 发射概率
        for single, count in b_matrix[i].items():
            prob = count / total_status_dict[i]
            if prob > 0.:
                b_matrix[i][single] = math.log(prob)
            else:
                b_matrix[i][single] = min_prob

    print(' start-prob:', start_prob)
    print('a-prob:', a_matrix)
    print('b-prob:', b_matrix)

    print('save model...')
    with open(save_file, mode='w', encoding='utf-8') as f:
        f.write(str(start_prob) + '\n')
        f.write(str(a_matrix) + '\n')
        f.write(str(b_matrix) + '\n')
    print('save done:', save_file)


def count_status(k_status, total_status_dict):
    if total_status_dict.get(k_status, -1) == -1:
        total_status_dict[k_status] = 0.
    total_status_dict[k_status] += 1.


def viterbi(sentence, sep=' '):
    '''
    给定一个未分词的句子,使用维特比算法,寻找最优的切分序列S
    :param sentence:
    :param sep: 分隔符
    :return: 返回不同的分词方案
    '''
    res = []

    # 加载模型
    # 初始状态概率 M
    PI = None
    # 状态转移概率 M*M
    A = None
    # 发射概率 M*N N表示某状态下的文字种数
    B = None
    with open(save_file, mode='r', encoding='utf-8')as f:
        PI = eval(f.readline())
        A = eval(f.readline())
        B = eval(f.readline())

    # 获取句子的文字列表
    ch_list = get_word_ch_list(sentence)
    sentence_len = len(ch_list)
    # M*句子长度的矩阵,每个元素是一个数组[hmm概率值,上一列中最好的节点状态S]
    s_matrix = [[[0., 0] for j in range(sentence_len)] for i in range(STATUS_NUM)]

    # 找到h层的节点i,使得i到h+1层的j节点的hmm值最大
    get_max_hmm_matrix(PI, A, B, ch_list, s_matrix, sentence_len)

    print('s_matrix:', s_matrix)

    '''找到hmm概率最大的S序列'''
    # 最后一列中,hmm最大的节点
    best_end = None
    # 最后一列中,hmm的最大概率
    best_p = None
    best_end_status = None
    # 最后一列的节点列表
    last_node = []
    for i in range(STATUS_NUM):
        end_i = s_matrix[i][-1]
        last_node.append((end_i[0], end_i[1], i))
        if best_p is None or best_p < end_i[0]:
            best_p = end_i[0]
            best_end_status = i
            # best_end=end_i
    # 最后一列的节点列表以hmm概率倒序排列
    last_node = sorted(last_node, key=lambda x: x[0], reverse=True)
    print(last_node)

    # 从后往前遍历hmm概率矩阵,找出S序列
    s_list = [0 for i in range(sentence_len)]
    s_list[-1] = best_end_status
    for col_i in range(1, sentence_len + 1):
        s_list[sentence_len - col_i] = best_end_status
        i_node = s_matrix[best_end_status][sentence_len - col_i]
        best_end_status = i_node[1]
        # best_end=i_node

    print(s_list)
    # 切分句子 BMES 0123
    word = ''
    for ch_i in range(sentence_len):
        if s_list[ch_i] == 3:
            word = ch_list[ch_i]
            res.append(word)
            word = ''
            continue

        if s_list[ch_i] == 2:
            word += ch_list[ch_i]
            res.append(word)
            word = ''
            continue

        word += ch_list[ch_i]

    return res


def get_max_hmm_matrix2(PI, A, B, ch_list, s_matrix, sentence_len):
    '''
    最优路径动态规划
    找到最优路径 使得hmm概率最大
    分析:
    定义在m*n的S矩阵中,i=m-1, j=n-1 , p,p0 in (0,i), q in (0,j)
    dp[p][q][0],dp[p][q][1]分别表示从(p0,0)位置(第一列某个节点)到达pq位置(q列某个节点p)的最大hmm概率值和q-1列中hmm最大的节点
    则dp[p][q][0]= q-1列中的(hmm+ s到节点p的转移概率)最大值的节点s  + p节点到文字Oq的发射概率
    dp[p][q][0] = max(dp[s][q-1]+ A[s][p])  + B[p][Oq]
    dp[p][q][1]=s
    第一列初始值 dp[p][0]= PI[p]+ B[p][O(0)]
    :param A:
    :param B:
    :param ch_list:
    :param s_matrix:
    :param sentence_len:
    :return:
    '''
    # for p in range(STATUS_NUM):
    #     dp(PI, A, B, ch_list, s_matrix, sentence_len, p, sentence_len - 1)
    # 递归废弃 直接返回s_matrix
    return s_matrix


# def dp(PI, A, B, ch_list, s_matrix, sentence_len, p, q):
#     if q == 0:
#         return init_s_matrix(B, PI, ch_list, s_matrix)
#
#     max_arr = max_dps(PI, A, B, ch_list, s_matrix, sentence_len, p, q)
#
#     if B[p].get(ch_list[q], -1) != -1:
#         s_matrix[p][q][0] = max_arr[0] + B[p][ch_list[q]]
#     else:
#         s_matrix[p][q][0] = max_arr[0] + min_prob
#     s_matrix[p][q][1] = max_arr[1]
#
#     return [s_matrix[p][q][0], s_matrix[p][q][1]]
#
#
# def max_dps(PI, A, B, ch_list, s_matrix, sentence_len, p, q):
#     max_prob = None
#     max_status = None
#     for s in range(STATUS_NUM):
#         cur_prob = dp(PI, A, B, ch_list, s_matrix, sentence_len, s, q - 1)[0] + A[s][p]
#         if max_prob is None or max_prob < cur_prob:
#             max_prob = cur_prob
#             max_status = s
#
#     return [max_prob, max_status]
#
#
# def init_s_matrix(B, PI, ch_list, s_matrix):
#     # 初始化第一列
#     max_prob = None
#     max_status = None
#     for i in range(STATUS_NUM):
#         if B[i].get(ch_list[0], -1) != -1:
#             s_matrix[i][0][0] = PI[i] + B[i][ch_list[0]]
#         else:
#             s_matrix[i][0][0] = PI[i] + min_prob
#         # 第一列不需要存储上一列的最优节点 所以赋值为-1
#         s_matrix[i][0][1] = -1
#         cur_prob = s_matrix[i][0][0] + B[i][ch_list[0]]
#         if max_prob is None or max_prob < cur_prob:
#             max_prob = cur_prob
#             max_status = i
#     return [max_prob, max_status]


def get_max_hmm_matrix(PI, A, B, ch_list, s_matrix, sentence_len):
    '''
    找到最优路径 使得hmm概率最大
    :param A:
    :param B:
    :param ch_list:
    :param s_matrix:
    :param sentence_len:
    :return:
    '''
    # 计算第一列的hmm概率=PI(S1)*B(O1|S1)
    for i in range(STATUS_NUM):
        if B[i].get(ch_list[0], -1) == -1:
            s_matrix[i][0][0] = PI[i] + min_prob
        else:
            # 由于概率取了对数,相加即相乘
            s_matrix[i][0][0] = PI[i] + B[i][ch_list[0]]
        s_matrix[i][0][1] = i

    for ch_j in range(1, sentence_len):
        # 某一行某个文字可能有M种状态
        # 当前列的j
        for j in range(STATUS_NUM):
            max_p = None
            max_status = None
            # 上一列的i
            for i in range(STATUS_NUM):
                # i到j的转移概率
                a_ij = A[i][j]
                # 当前i到j的hmm概率
                cur_p = s_matrix[i][ch_j - 1][0] + a_ij
                if max_p is None or max_p < cur_p:
                    max_p = cur_p
                    max_status = i

            # j节点的hmm概率和i节点的状态S
            b_prob = min_prob
            if B[j].get(ch_list[ch_j], -1) != -1:
                b_prob = B[j][ch_list[ch_j]]

            s_matrix[j][ch_j][0] = max_p + b_prob
            s_matrix[j][ch_j][1] = max_status


def read_file(path):
    with open(path, mode='r', encoding='utf-8') as f:
       line=f.readline()
       while line:
        print(line)
        line=f.readline()

if __name__ == '__main__':
    # read_file('F:\\00-data\word2vec\\wiki.txt')
    # train_theta()
    st = '无边落木萧萧下,不尽长江滚滚来'
    st2='北京大学生活动中心'
    st3='南京市长江大桥'
    seq_list=[st,st2,st3]
    for s in seq_list:
        res = viterbi(s)
        print('hmm:'+str(res))
        print('jieba:'+str([x for x in jieba.cut(s,cut_all=False)]))
    # ch_list = get_word_ch_list(st3)
    # slist = get_status_list_by_chlist(ch_list)
    # print(ch_list)
    # print(slist)

 

 

      

posted @ 2021-12-04 20:29  霍橼甲  阅读(358)  评论(0)    收藏  举报