HanLP中人名识别分析详解

在看源码以前,先看几遍论文《基于角色标注的中国人名自动识别研究》java

关于命名识别的一些问题,可参考下列一些issue:算法

  1. ·名字识别的问题 #387
  2. ·机构名识别错误
  3. ·关于层叠HMM中文实体识别的过程

HanLP参考博客:spa

词性标注3d

层叠HMM-Viterbi角色标注模型下的机构名识别code

分词orm

在HMM与分词、词性标注、命名实体识别中说:对象

分词:给定一个字的序列,找出最可能的标签序列(断句符号:[词尾]或[非词尾]构成的序列)。结巴分词目前就是利用BMES标签来分词的,B(开头),M(中间),E(结尾),S(独立成词)blog

分词也是采用了维特比算法的动态规划性质求解的,具体可参考:文本挖掘的分词原理get

角色观察源码

以“唱首张学友的歌情已逝”为例,

先将起始顶点 始##始,角色标注为:NR.A 和 NR.K,频次默认为1

iterator.next(); tagList.add(new EnumItem<NR>(NR.A, NR.K)); // 始##始 A K

对于第一个词“唱首”,它不存在于 nr.txt中,EnumItem<NR> nrEnumItem = PersonDictionary.dictionary.get(vertex.realWord);返回null,因而根据它自己的词性猜一个角色标注:

switch (vertex.guessNature()){

        case nr:

        case nnt:

    default:{

        nrEnumItem = new EnumItem<NR>(NR.A, PersonDictionary.transformMatrixDictionary.getTotalFrequency(NR.A));

    }

}

因为"唱首"的Attribute为 nz 16,不是nr 和 nnt,故默认给它指定一个角色NR.A,频率为nr.tr.txt中 NR.A 角色的总频率。

此时,角色列表以下:

接下来是顶点“张”,因为“张”在nr.txt中,所以PersonDictionary.dictionary.get(vertex.realWord)返回EnumItem对象,直接将它加入到角色列表中:

EnumItem<NR> nrEnumItem = PersonDictionary.dictionary.get(vertex.realWord);

tagList.add(nrEnumItem);

加入“张”以后的角色列表以下:

“唱首张学友的歌情已逝” 整句的角色列表以下:

至此,角色观察 部分 就完成了。

总结一下,对句子进行角色观察,首先是经过分词算法将句子分红若干个词,而后对每一个词查询人名词典(PersonDictionary)。

若这个词在人名词典中(nr.txt),则记录该词的角色,全部的角色在com.hankcs.hanlp.corpus.tag.NR.java中定义。

若这个词不在人名词典中,则根据该词的Attribute “猜一个角色”。在猜的过程当中,有些词在核心词典中可能已经标注为nr或者nnt了,这时会作分裂处理。其余状况下则是将这个词标上NR.A角色,频率为 NR.A 在转移矩阵中的总词频。

维特比算法(动态规划)求解最优路径

 

在上图中,给每一个词都打上了角色标记,能够看出,一个词能够有多个标记。而咱们须要将这些词选择一条路径最短的角色路径。参考隐马尔可夫模型维特比算法详解

List<NR> nrList = viterbiComputeSimply(roleTagList);

//some code....

return Viterbi.computeEnumSimply(roleTagList, PersonDictionary.transformMatrixDictionary);

而这个过程,其实就是:维特比算法解码隐藏状态序列。在这里,五元组是:

  1. 隐藏状态集合 com.hankcs.hanlp.corpus.tag.NR.java 定义的各我的名标签
  2. 观察状态集合 已经分好词的各个tagList中元素(至关于分词结果)    
  3. 转移几率矩阵 由 nr.tr.txt 文件生成获得。具体可参考:
  4. 发射几率 某我的名标签(隐藏状态)出现的次数 除以 全部标签出现的总次数

Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur)

  1. 初始状态(始##始) 和 结束状态(末##末)

维特比解码隐藏状态的动态规划求解核心代码以下:

 

            for (E cur : item.labelMap.keySet())

            {

                double now = transformMatrixDictionary.transititon_probability[pre.ordinal()][cur.ordinal()] - Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur));

                if (perfect_cost > now)

                {

                    perfect_cost = now;

                    perfect_tag = cur;

                }

            }

transformMatrixDictionary.transititon_probability[pre.ordinal()][cur.ordinal()] 是前一个隐藏状态 pre.ordinal()转换到当前隐藏状态cur.ordinal()的转移几率。Math.log((item.getFrequency(cur) + 1e-8) / transformMatrixDictionary.getTotalFrequency(cur)是当前隐藏状态的发射几率。两者“相减”获得一个几率 保存在double now变量中,而后经过 for 循环找出 当前观察状态 对应的 最可能的(perfect_cost最小) 隐藏状态 perfect_tag。

至于为何是上面那个公式来计算转移几率和发射几率,可参考论文:《基于角色标注的中国人名自动识别研究》

在上面例子中,获得的最优隐藏状态序列(最优路径)K->A->K->Z->L->E->A->A 以下:

nrList = {LinkedList@1065} size = 8

"K" 始##始

"A" 唱首

"K" 张

"Z" 学友

"L" 的

"E" 歌

"A" 情已逝

"A" 末##末

例如:

​隐藏状态---观察状态

"K"----------始##始

最大匹配

有了最优隐藏序列:KAKZLEAA,接下来就是:后续的“最大匹配处理”了。

        PersonDictionary.parsePattern(nrList, pWordSegResult, wordNetOptimum, wordNetAll);

在最大匹配以前,会进行“模式拆分”。在com.hankcs.hanlp.corpus.tag.NR.java 定义了隐藏状态的具体含义。好比说,若最优隐藏序列中 存在 'U' 或者 'V',

U Ppf 人名的上文和姓成词 这里【有关】天培的壮烈

V Pnw 三字人名的末字和下文成词 龚学平等领导, 邓颖【超生】前

则会作“拆分处理”

switch(nr)

{

    case U:

        //拆分红K B

    case V:

        //视状况拆分

}

拆分完成以后,从新获得一个新的隐藏序列(模式)

String pattern = sbPattern.toString();

接下来,就用AC自动机进行最大模式匹配了,并将匹配的结果存储到“最优词网”中。固然,在这里就能够自定义一些针对特定应用的 识别处理规则

trie.parseText(pattern, new AhoCorasickDoubleArrayTrie.IHit<NRPattern>(){

    //.....

    wordNetOptimum.insert(offset, new Vertex(Predefine.TAG_PEOPLE, name, ATTRIBUTE, WORD_ID), wordNetAll);

}

将识别出来的人名保存到最优词网后,再基于最优词网调用一次维特比分词算法,获得最终的分词结果---细分结果。

            if (wordNetOptimum.size() != preSize)

            {

                vertexList = viterbi(wordNetOptimum);

                if (HanLP.Config.DEBUG)

                {

                    System.out.printf("细分词网:\n%s\n", wordNetOptimum);

                }

            }

总结

源码上的人名识别基本上是按照论文中的内容来实现的。对于一个给定的句子,先进行下面三大步骤处理:

  1. 角色观察
  2. 维特比算法解码求解隐藏状态(求解各个分词 的 角色标记)
  3. 对角色标记进行最大匹配(可作一些后处理操做)

最后,再使用维特比算法进行一次分词,获得细分结果,即为最后的识别结果。

 

这篇文章里面没有写维特比分词算法的详细过程,以及转移矩阵的生成过程,之后有时间再补上。看源码,对隐马模型的理解又加深了一点,感觉到了理论的东西如何用代码一步步来实现。因为我也是初学,对源码的理解不够深刻或者存在一些误差,欢迎批评指正。

关于动态规划的一个简单示例,可参考:动态规划之Fib数列类问题应用。

 

文章来源 hapjin 的博客  

相关文章
相关标签/搜索