python_NLP实战之中文分词技术

本文深入探讨了中文分词技术,包括规则分词方法如正向最大匹配算法和逆向最大匹配算法,以及统计分词方法如隐马尔可夫模型(HMM)和条件随机场(CRF)。同时介绍了jieba分词工具的使用,该工具结合了规则和统计方法,通过HMM模型和Verterbi算法实现高效分词。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

一、规则分词

1.1 正向最大匹配算法

# 正向最大匹配算法 MM法 规则分词
class MM(object):
    def __init__(self):
        self.window_size=3

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

1.2 逆向最大匹配算法

# RMM逆向最大匹配算法   规则分词
class RMM(object):
    def __init__(self):
        self.window_size=3
    def cut(self,text):
        result=[]
        index=len(text)
        dic=['研究','研究生','生命','命','的','起源']
        while index>0:
            for size in range(index-self.window_size,index):
                piece=text[size:index]
                if piece in dic:
                    index=size+1
                    break
            index=index-1
            result.append(piece+'------')
        result.reverse()
        print(result)

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

二、统计分词

2.1 HMM模型

初始概率分布

      z1可能是状态1,状态2 ... 状态n,于是z1就有个N点分布:

Z1

状态1

状态2

...

状态n

概率

P1

P2

...

Pn

      即:Z1对应个n维的向量。

      上面这个n维的向量就是初始概率分布,记做π。

状态转移矩阵

      但Z2就不能简单的“同上”完事了,因为Z2和Z1不独立,所以Z2是状态1的概率有:Z1是状态1时Z2是状态1,Z1是状态2时Z2是状态1,..., Z1是状态n时Z2是状态1,于是就是下面的表

Z2

Z1

状态1

状态2

...

状态n

状态1

P11

P12

...

P1n

状态2

P21

P22

...

P2n

...

...

...

...

...

状态n

Pn1

Pn2

...

Pnn

       即:Z1->Z2对应个n*n的矩阵。

       同理:Zi -> Zi+1对应个n*n的矩阵。

      上面这些n*n的矩阵被称为状态转移矩阵,用An*n表示。

      当然了,真要说的话,Zi -> Zi+1的状态转移矩阵一定都不一样,但在实际应用中一般将这些状态转移矩阵定为同一个,即:只有一个状态转移矩阵。

      图1的第一行就搞定了,下面是第二行。

观测矩阵

      如果对于zi有:状态1, 状态2, ..., 状态n,那zi的每一个状态都会从下面的m个观测中产生一个:观测1, 观测2, ..., 观测m,所以有如下矩阵:

X

Z

观测1

观测2

...

观测m

状态1

P11

P12

...

P1m

状态2

P21

P22

...

P2m

...

...

...

...

...

状态n

Pn1

Pn2

...

Pnm

      这可以用一个n*m的矩阵表示,也就是观测矩阵,记做Bn*m。

      由于HMM用上面的π,A,B就可以描述了,于是我们就可以说:HMM由初始概率分布π、状态转移概率分布A以及观测概率分布B确定,为了方便表达,把A, B, π 用 λ 表示,即:

            λ = (A, B, π)

例子

      假设我们相对如下这行话进行分词:

           欢迎来到我的博客

      再假设我们是这样分的:找到“终止字”,然后根据终止字来分词。即:对于这行字,“迎、到、我、的、客”是终止字,于是最终这么分词:欢迎/来到/我/的/博客

      下面用上面的知识对这个例子建立HMM的A, B, π:

       初始概率分布的确定:

           1,对于每个样本,我们的目标是确定其是不是“终止字”,因此对于每个样本,其状态只有n=2个:状态1 -- 是、状态2 -- 不是。

            2,因此初始概率分布π为:

                 π = {p1,p2}

                 P1:整个句子中第一个字是非终止字的概率

                  P2:整个句子中第一个字是终止字的概率

      状态转移矩阵的确定:

           刚才已经知道状态有n=2个,于是状态转移矩阵就立马得出了,即状态转移矩阵是个n*n的矩阵,如下:

                 A=

                 p11:非终止字 -> 非终止字的概率。

                 p12:非终止字 -> 终止字的概率。

                 p21:终止字 -> 非终止字的概率。

                  p22:终止字 -> 终止字的概率。

       观测矩阵的确定:

           如果我们的目标文字使用Unicode编码,那么上面的任何一个字都是0~65535中的一个数,于是我们的观测就会有m=65536个,于是观测矩阵就是个n*m的矩阵,如下:

                 B=

                 p1,0:Unicode编码中0对应的汉字是非终止字的概率

                 p1,65535:Unicode编码中65535对应的汉字是非终止字的概率

                 p2,0:Unicode编码中0对应的汉字是终止字的概率

                 p2,65535:Unicode编码中65535对应的汉字是终止字的概率

            PS:为什么x会有65535个观测啊?“欢迎来到我的博客”这个明明只有8个字。原因是因为真正的HMM面临的情况,即:现有了 Z1=“非终止字”这个状态,然后根据这个状态从65535个字中选出x1=“欢”这个字,然后根据状态转移矩阵,下一次转移到了Z2 =“终止字”,然后根据Z2从65535个字中选出了x2=“迎”这个字,这样,最终生成了这句话。

可详细参考:https://www.cnblogs.com/sddai/p/8475424.html

# 统计分词
#  1、先建立语言模型
#  2、对句子进行单词划分,对划分结果进行概率计算,获得概率最大的分词方式
# HMM
class HMM(object):
    def __init__(self):
        import os
        # 保存训练的模型
        self.model_file='./data/hmm_model.pkl'
        # 状态特征值集合
        self.state_list=['B','M','E','S']
        # 判断是否需要重新加载模型
        self.load_para=False
    def try_load_model(self,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):
        # 重置几个概率矩阵
        self.try_load_model(False)
        # 统计状态出现次数
        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):
            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='utf-8') 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()}
        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,train_p,emit_p):
        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
                (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')

2.2 CRF

 

三、中文分词工具_JieBa

jieba分词结合了基于规则和基于统计的两种方法

基于汉字成词的HMM模型,采用了Verterbi算法进行推导

3.1高频词提取

高频词就是NLP中的TF策略

 

进行数据的读取
def get_content(path):
    with open(path,'r',encoding='utf-8',errors='ignore') as f:
        content=''
        for l in f:
            l=l.strip()
            content+=l
    return content
def stop_words(path):
    with open(path,encoding='utf-8') as f:
        return [l.strip() for l in f]
定义高频词统计的函数,输入是一个词的数组
def get_TF(words,topK=10):
    tf_dic={ }
    for w in words:
        tf_dic[w]=tf_dic.get(w,0)+1
    return sorted(tf_dic.items(),key =lambda x:x[1],reverse=True)[:topK]

def main():
    import glob
    import random
    import jieba
    files=glob.glob('./data/news/C000013/*.txt')
    corpus=[get_content(x) for x in files]

    sample_inx=random.randint(0,len(corpus))
    split_words=[x for x in jieba.cut(corpus[sample_inx]) if x not in stop_words('./data/stop_words.utf8')]
    print('样本之一:' + corpus[sample_inx])
    print('样本分词效果:' + '/ '.join(split_words))
    print('样本的topK(10)词:' + str(get_TF(split_words)))

main()

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值