首页 > 代码库 > cppjieba分词学习笔记

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<WordRange>& words,
            size_t max_word_len = MAX_WORD_LENGTH) const {
        vector<Dag> dags;
        dictTrie_->Find(begin, end, dags, max_word_len);  //构建DAG图
        CalcDP(dags);  //从后往前的动态规划算法
        CutByDag(begin, end, dags, words);//回溯
    }

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

参考:中文分词之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;
        }
    }

 二、关键词提取

cpp结巴的提供了两种关键词提取方法,分别基于TF-IDF和TextRank算法,下面分别介绍

1.基于TF-IDF算法的关键词抽取

TF(term frequency):一个词语在单个文档出现的次数
IDF(Inverse document frequency): 逆文档频率,是一个词语普遍度的度量。某一特定词语的IDF,有总文件数目除以包含该词语的文件数目,再将取得商取对数的得到。
总的来说某一特定文件内的高频率词语,以及词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。
cppjieba中提供接近26万词语的IDF(idf.utf8),对于在idf.utf8中没出现的词语,idf权重取平局值。除此之外,有些词语在文档的出现的频率TF很高,但是对于文档来说没什么实在的意义比如“的”,“是”等,因此cppjieba还提供了了一个1534个词语的stop_words.utf8。
cppjieba中根据权重(tf*idf)排序,取出topN个关键词。
partial_sort(keywords.begin(), keywords.begin() + topN, keywords.end(),Compare);

参考:http://blog.csdn.net/awj3584/article/details/18604901

2.基于TextRank的关键词提取

  PageRank根据网页之间的链接关系,建立网页之间的有向图,图中的节点表示每个网页,然后根据这个有向图迭代计算出每个网页的PER值,作为网页重要性排名的依据。TextRank方法与PageRank的方法非常类似,当使用TextRank方法做文本的关键字提取时,首先在给定的共现窗口内(cppjieba中默认为5)根据词语之间的共现关系建立链接关系图,图中的每个节点代表每个节点,每条边的权重表示词语共现的次数。然后根据词语之间链接关系图,迭代计算出每个词语的权重值,根据权重值选出文本中权重值大的几个关键词。

  TextRank 一般模型可以表示为一个有向有权图 G =(V, E), 由点集合 V和边集合 E 组成, E 是V ×V的子集。图中任两点 Vi , Vj 之间边的权重为 wji , 对于一个给定的点 Vi, In(Vi) 为 指 向 该 点 的 点 集 合 , Out(Vi) 为点 V指向的点集合。点 Vi 的得分定义如下:

技术分享

  其中, d 为阻尼系数, 取值范围为 0 到 1, 代表从图中某一特定点指向其他任意点的概率, 一般取值为 0.85。使用TextRank 算法计算图中各点的得分时, 需要给图中的点指定任意的初值, 并递归计算直到收敛, 即图中任意一点的误差率小于给定的极限值时就可以达到收敛。cppjieba在实现默认迭代10次。

参考:http://www.cnblogs.com/clover-siyecao/p/5726480.html
参考:http://blog.sohu.com/s/MTAzMjM1NDY0/239636012.html(矩阵形式)

三、词性标注

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

参考: CppJieba代码详解

          "结巴"(Jieba)中文分词系列性能评测

cppjieba分词学习笔记