自然语言处理系列-2.分词

 

  在语言理解中,词是最小的能够独立活动的有意义的语言成分。将词确定下来是理解自然语言的第一步,只有跨越了这一步,计算机才能准确的进行短语划分、实体识别、概念抽取、文本摘要等基本的自然语言理解任务。

   尽管,现在用jieba python库就能够比较方便的获得较好的分词结果,但是分词的基本原理和发展历史,是每一个NLP工作者都应该掌握的。

 1分词简介?

   词的概念在不同的语系中可能会有不同的定义,本文就以我们熟悉的中文为例来进行讲述。“词”这个概念一直是汉语语言学界纠缠不清又绕不开的话题。在汉语中,词以字为基本单位,但是一篇文章的语义表达却是以词为基础的。所以,在进行语言处理及理解时,分词是前提和必要的基础。

   分词的过程,就是将句子转化为词的表示的过程。分词的过程可以描述为通过计算机自动识别出句子中的词,在词间加入边界标记符,分隔出各个词汇。从中可以看出,分词也是一个序列标注的过程。

   有人也许会想,找一堆人统计出所有用的词汇,然后构建一个中文词表,分词的任务根据这个词表不就很容易就能够得到解决了吗?这样的想法未免有点太天真了。一方面,随着互联网的发展,词汇永远在不断的更新,这个词表很难去维护。另外一个方面,也是分词的主要困难就是分词歧义。举个例子,“各国有企业相继蓬勃发展”,这个句子可以这样去分“各国/有/企业/相继/蓬勃发展”,也可以这样去分“各/国有企业/相继/蓬勃发展”。

 2.分词算法

   分词算法主要包括规则分词和统计分词,还有基于这两种的混合分词。

   2.1规则分词

 

  分词的HMM理论大致就是这样了,大家会不会觉得其实很简单,有一种酣畅淋漓的感觉?如果没有,那请看代码,哈哈。代码引用了《Python自然语言处理实战:核心技术与算法》。

 

class HMM(object):    def __init__(self):        import os        # 主要是用于存取算法中间结果,不用每次都训练模型        self.model_file = './data/hmm_model.pkl'        # 状态值集合        self.state_list = ['B', 'M', 'E', 'S']        # 参数加载,用于判断是否需要重新加载model_file        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)        # 统计状态出现次数,求p(o)        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='utf8') 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()}        #加1平滑        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, trans_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[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                newpath[y] = path[state] + [y]            path = newpath        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):        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:]

  了解分词的同学,可能都知道,通过jieba包能够非常的方便的进行分词。不了解的同学百度一下,很容易也能够学会的。所以为什么我们还要费这么大一堆来讲分词的原理呢?首先,jieba分词效果再你应用的领域未必是好的,有可能你也需要开发自己的分词系统;其次,通过对分词技术的了解你可以熟悉到序列标注,HMM等一系列的机器学习的知识,对你后续工作是有很大帮助的。

 

因为微信公众号不能输入公式,我只有把有公式的部分转成图片再发上公众号,各位看客忍耐以下,以后再想办法解决。

-----------------------------------------------------------------------

梦想是自由的,但是实现梦想、度过幸福一生的人,实在是少之又少。但是每个人都有自己的路,选择了就走下去吧。最后还是说一句没用的,梦想都成真。

posted on 2019-03-08 09:53  小Dream  阅读(1027)  评论(0编辑  收藏  举报

导航