第2关:统计分词原理与实战

任务描述

本关任务:根据所学有关统计分词的知识,完成基于 HMM 的中文分词算法程序的编写并通过所有测试用例。

相关知识

为了完成本关任务,你需要掌握:

  1. 统计分词的思想;

  2. 基于 HMM 的分词方法。

统计分词简介

1、主要思想

把每个词看做是由词的最小单位的各个字组成的,如果相连的字在不同的文本中出现的次数越多,就证明这相连的字很可能就是一个词。

因此我们就可以利用字与字相邻出现的频率来反应成词的可靠度,统计语料中相邻共现的各个字的组合的频度,当组合频度高于某一个临界值时,我们便可认为此字组可能会构成一个词语。

2、方法步骤

基于统计的分词,一般要做如下两步操作:

  • 建立统计语言模型;

  • 对句子进行单词划分,然后对划分结果进行概率计算,获得概率最大的分词。这里就用到了统计学习算法,如隐含马尔可夫( HMM )、条件随机场( CRF )等。

什么是语言模型

语言模型在信息检索、机器翻译、语音识别中承担着重要的任务。这种模型结构简单,直接,但同时也因为数据缺乏而必须采取平滑算法。这里主要介绍 n 元语言模型( n-gram )。

假设 S 表示长度为 i 由(W1​,W2​,....,Wi​)字序列组成的句子,则 S 的概率为:

P(S)=P(W1​,W2​,...,Wi​)=P(W1​)∗P(W2​∣W1​)∗P(W3​∣W2​,W1​)....P(Wi​∣W1​,W2​,...,Wi​−1​)

即每个字的出现都与它之前出现过的字有关,最后整个句子 S 的概率为这些字概率的乘积。但是这个计算量很大,所以在这里我们可以利用马尔科夫假设,即当前词只与最多前n-1个有限的词相关:

当 n=1 时,即出现在第i位上的词Wi​独立于历史词时,一元文法被记作 uni-gram ,一元语言模型可以记作:

P(W1​,W2​,...,Wm​)=∏i=1m​P(Wi​)

当 n=2 时,即出现在第i位上的词Wi​仅与它前面的一个历史词Wi−1​有关,二元文法模型被称为一阶马尔可夫链( Markov chain ),记作 bi-gram ,二元语言模型可以记作:

P(W1​,W2​,...,Wm​)=∏i=1m​P(Wi​∣Wi​−1​)

当 n=3 时,即出现在第i位置上的词Wi​仅与它前面的两个历史词Wi−2​和Wi−1​有关,三元文法模型被称为二阶马尔可夫链,记作 tri-gram ,三元语言模型可以记作:

P(W1​,W2​,...,Wm​)=∏i=1m​P(Wi​∣Wi−2​∗Wi−1​)

在实际应用中,一般使用频率计数的比例来计算 n 元条件概率。

基于 HMM 的分词法

在统计分词中,隐含马尔可夫模型( HMM )是我们常用的模型,它主要通过将分词作为字在字串中的序列标注任务来实现,它的基本思路是:每个字在构造一个特定的词语时都占据着一个确定的构词位置(即词位),规定每个字最多有四个构词位置: B (词首)、 M (词中)、 E (词尾)、 S (单独成词)。

如:

  1. 中文/分词/是/文本处理/不可或缺/的/一步/!

标注后的形式为:

  1. 中/B 文/E 分/B 词/E 是/S 文/B 本/M 处/M 理/E 不/B 可/M 或/M 缺/E 的/S 一/B 步/E !/S

其中,词位序列代表着 HMM 中不可见的隐藏状态序列,而训练集中的文本则为可见的观测序列。这样就变成了已知观测序列,求未知的隐藏序列的 HMM 问题。

基于 HMM 进行分词共包含以下步骤:

  1. 使用已经分好词的训练集去训练 HMM 模型,计算频数得到 HMM 的三要素(初始状态概率,状态转移概率和发射概率);

  2. 使用 Viterbi 算法以及训练好的三个概率矩阵,将待分词的句子转换为 BMES 类型的状态序列;

  3. 根据已经求出的状态序列,划分句子进行分词;

  4. 最后输出结果。

编程要求

在右侧编辑器中的 Begin-End 之间补充 Python 代码,完成基于 HMM 的统计分词算法,对所输入的文本进行统计分词,并输出分词结果。其中文本内容通过 input 从后台获取。

测试说明

平台将使用测试集运行你编写的程序代码,若全部的运行结果正确,则通关。

测试输入:

  1. 研究生命的起源

预期输出:

  1. model train done,parameters save to hmm_model.pkl # 接口附加信息,表明模型训练完成
  2. model parameters load done!
  3. 研究/生命/的/起源

参考资料

【1】基于统计的分词方法

【2】HMM与分词、词性标注、命名实体识别

参考代码:

class HMM(object):

    def __init__(self):
        self.state_list = ['B','M','E','S']
        self.start_p = {}
        self.trans_p = {}
        self.emit_p = {}

        self.model_file = 'hmm_model.pkl'
        self.trained = False

    def train(self,datas,model_path=None):
        if model_path == None:
            model_path = self.model_file
        #统计状态频数
        state_dict = {}

        def init_parameters():
            for state in self.state_list:
                self.start_p[state] = 0.0
                self.trans_p[state] = {s:0.0 for s in self.state_list}
                self.emit_p[state] = {}
                state_dict[state] = 0

        def make_label(text):
            out_text = []
            if len(text) == 1:
                out_text = ['S']
            else :
                out_text += ['B']+['M']*(len(text)-2)+['E']
            return out_text

        init_parameters()
        line_nb = 0

        #监督学习方法求解参数
        for line in datas:
            line = line.strip()
            if not line:
                continue
            line_nb += 1

            word_list = [w for w in line if w != ' ']
            line_list = line.split()
            line_state = []
            for w in line_list:
                line_state.extend(make_label(w))

            assert len(line_state) == len(word_list)

            for i,v in enumerate(line_state):
                state_dict[v] += 1

                if i == 0:
                    self.start_p[v] += 1
                else :
                    self.trans_p[line_state[i-1]][v] += 1
                    self.emit_p[line_state[i]][word_list[i]] = self.emit_p[line_state[i]].get(word_list[i],0)+1.0

        self.start_p = {k: v*1.0/line_nb for k,v in self.start_p.items()}
        self.trans_p = {k:{k1: v1/state_dict[k1] for k1,v1 in v0.items()} for k,v0 in self.trans_p.items()}
        self.emit_p = {k:{k1: (v1+1)/state_dict.get(k1,1.0) for k1,v1 in v0.items()} for k,v0 in self.emit_p.items()}

        with open(model_path,'wb') as f:
            import pickle
            pickle.dump(self.start_p,f)
            pickle.dump(self.trans_p,f)
            pickle.dump(self.emit_p,f)
        self.trained = True
        print('model train done,parameters save to ',model_path)

    #读取参数模型
    def load_model(self,path):
        import pickle
        with open(path,'rb') as f:
            self.start_p = pickle.load(f)
            self.trans_p = pickle.load(f)
            self.emit_p = pickle.load(f)
        self.trained = True
        print('model parameters load done!')

    #维特比算法求解最优路径 
    def __viterbi(self,text,states,start_p,trans_p,emit_p):
        V = [{}]
        path = {}
        for y in states:
            V[0][y] = start_p[y]*emit_p[y].get(text[0],1.0)
            path[y] = [y]

        for t in range(1,len(text)):
            V.append({})
            new_path = {}

            for y in states:
                emitp = emit_p[y].get(text[t],1.0)

                (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
                new_path[y] = path[state]+[y]
            path = new_path

        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):
        if not self.trained:
            print('Error:please pre train or load model parameters')
            return

        prob,pos_list = self.__viterbi(text,self.state_list,self.start_p,self.trans_p,self.emit_p)
        begin_,next_ = 0,0
        #任务:完成 HMM 中文分词算法
        # ********* Begin *********# 
        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_:]
        # ********* Begin *********#

if __name__ == '__main__':
    text = input()

    train_data = 'pku_training.utf8'
    model_file = 'hmm_model.pkl'
    hmm = HMM()
    hmm.train(open(train_data, 'r', encoding='utf-8'), model_file)
    hmm.load_model(model_file)
    print('/'.join(hmm.cut(text)))

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

畜牧当道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值