CPPJieba分词学习

cppjieba分词包主要提供中文分词、关键词提取、词性标注三种功能

分词

cppjieba分词用的方法是最大概率分词(MP)和隐马尔科夫模型(HMM),以及将MP和HMM结合成的MixSegment分词器。除此之外,cppjieba支持三种模式的分词:

  • 精确模式,试图将句子最精确地切开,适合文本分析;
  • 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
    我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

  • 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词
    小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造

1.最大概率分词(MP)

  • 默认基于jieba.dict.utf8生成前缀词典,用户可以添加自己的字典,并且按照一定权重比重一起生成前缀词典。构建字典时将utf-8格式的输入转变为unicode格式
  • 分词器中有一个类Prefilter,pre_fileter会将输入的字符串转变为unicode格式,根据某些特殊的字符symbols_,将输入的字符串切分成一段一段,对每一段分别分词
  • 构建DAG图,从后往前的动态规划算法,回溯概率最大的切词方法

    void Cut(RuneStrArray::const_iterator begin, RuneStrArray::const_iterator end, vector& words, size_t max_word_len = MAX_WORD_LENGTH) const { vector dags; dictTrie_->Find(begin, end, dags, max_word_len); //构建DAG图 CalcDP(dags); //从后往前的动态规划算法 CutByDag(begin, end, dags, words);//回溯 }

2.隐藏马尔科夫分词(HMM)

cppjieba分词中提供了HMM模型的参数文件,保存在hmm_model.utf8中。cppjieba的HMM分词器,实际上就是加载HMM模型,然后根据输入的句子(观察序列),计算可能性最大的状态序列。状态空间由B(开始)、M(中间)、E(结束)、S(单个字)构成。下面是Viterbi算法的过程。

输入样例:

小明硕士毕业于中国科学院计算所
定义变量

二维数组 weight[4][15],4是状态数(0:B,1:E,2:M,3:S),15是输入句子的字数。比如 weight[0][2] 代表 状态B的条件下,出现’硕’这个字的可能性。

二维数组 path[4][15],4是状态数(0:B,1:E,2:M,3:S),15是输入句子的字数。比如 path[0][2] 代表 weight[0][2]取到最大时,前一个字的状态,比如 path[0][2] = 1, 则代表 weight[0][2]取到最大时,前一个字(也就是明)的状态是E。记录前一个字的状态是为了使用viterbi算法计算完整个 weight[4][15] 之后,能对输入句子从右向左地回溯回来,找出对应的状态序列。

使用InitStatus对weight二维数组进行初始化
已知InitStatus如下:

#B  
-0.26268660809250016  
#E  
-3.14e+100  
#M  
-3.14e+100  
#S  
-1.4652633398537678   且由EmitProbMatrix可以得出  

Status(B) -> Observed(小)  :  -5.79545  
Status(E) -> Observed(小)  :  -7.36797  
Status(M) -> Observed(小)  :  -5.09518  
Status(S) -> Observed(小)  :  -6.2475   所以可以初始化 weight[i][0] 的值如下:  

weight[0][0] = -0.26268660809250016 + -5.79545 = -6.05814  
weight[1][0] = -3.14e+100 + -7.36797 = -3.14e+100  
weight[2][0] = -3.14e+100 + -5.09518 = -3.14e+100  
weight[3][0] = -1.4652633398537678 + -6.2475 = -7.71276   注意上式计算的时候是相加而不是相乘,因为之前取过对数的原因。  

遍历句子计算整个weight二维数组
//遍历句子,下标i从1开始是因为刚才初始化的时候已经对0初始化结束了

    for(size_t i = 1; i < 15; i++)
    {
        // 遍历可能的状态
        for(size_t j = 0; j < 4; j++) 
        {
            weight[j][i] = MIN_DOUBLE;
            path[j][i] = -1;
            //遍历前一个字可能的状态
            for(size_t k = 0; k < 4; k++)
            {
                double tmp = weight[k][i-1] + _transProb[k][j] + _emitProb[j][sentence[i]];
                if(tmp > weight[j][i]) // 找出最大的weight[j][i]值
                {
                    weight[j][i] = tmp;
                    path[j][i] = k;
                }
            }
        }
    }

如此遍历下来,weight[4][15] 和 path[4][15] 就都计算完毕。

确定边界条件和路径回溯
边界条件如下:

对于每个句子,最后一个字的状态只可能是 E 或者 S,不可能是 M 或者 B。
所以在本文的例子中我们只需要比较 weight[1(E)][14] 和 weight[3(S)][14] 的大小即可。

在本例中:

weight[1][14] = -102.492;
weight[3][14] = -101.632; 所以 S > E,也就是对于路径回溯的起点是 path[3][14]。  

回溯的路径是:

SEBEMBEBEMBEBEB   倒序一下就是:  

BE/BE/BME/BE/BME/BE/S 所以切词结果就是:  

小明/硕士/毕业于/中国/科学院/计算/所
到此,一个HMM模型中文分词算法过程就阐述完毕了。

也就是给定我们一个模型,我们对模型进行载入完毕之后,只要运行一遍Viterbi算法,就可以找出每个字对 应的状态,根据状态也就可以对句子进行分词。

3.MixSegment是MP和HMM的结合,

首先使用MP分词,然后对MP分词的结果使用HMM分词。其实,第二次使用HMM再分对原有分词结果调整得并不多,只是将MP结果中单字顺序收集再分词。

    void Cut(RuneStrArray::const_iterator begin,
            RuneStrArray::const_iterator end, vector<WordRange>& res,
            bool hmm) const {
        if (!hmm) {
            mpSeg_.Cut(begin, end, res);
            return;
        }
        vector<WordRange> words;
        assert(end >= begin);
        words.reserve(end - begin);
        mpSeg_.Cut(begin, end, words);
        vector<WordRange> hmmRes;
        hmmRes.reserve(end - begin);
        for (size_t i = 0; i < words.size(); i++) {
            //if mp Get a word, it's ok, put it into result
            if (words[i].left != words[i].right
                    || (words[i].left == words[i].right
                            && mpSeg_.IsUserDictSingleChineseWord(
                                    words[i].left->rune))) {
                res.push_back(words[i]);
                continue;
            }
            // if mp Get a single one and it is not in userdict, collect it in sequence
            size_t j = i;
            while (j < words.size() && words[j].left == words[j].right
                    && !mpSeg_.IsUserDictSingleChineseWord(words[j].left->rune)) {
                j++;
            }
            // Cut the sequence with hmm
            assert(j - 1 >= i);
            // TODO
            hmmSeg_.Cut(words[i].left, words[j - 1].left + 1, hmmRes);
            //put hmm result to result
            for (size_t k = 0; k < hmmRes.size(); k++) {
                res.push_back(hmmRes[k]);
            }
            //clear tmp vars
            hmmRes.clear();
            //let i jump over this piece
            i = j - 1;
        }
    }

词性标注

cppjieba中的词性标注实现过程总的来说是基于词典的查询,词典中存有大约35万词汇的词性。单个词语的词性标注,直接查询词典,词典中不存在一个词语多个词性的问题。词典没有的词语,根据词语的特点的简单的标注为数字(m),英文(eng),以及x。对于整个句子的词性标注是用分词算法分词,然后用上述单个词语词性标注的方法逐个标注词性。由此可见cppjieba的词性标注非常依赖词典。对于句子的词性标注,没有考虑词性之间的关系。

打赏一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦