CRF分词的纯Java实现

CRF分词的纯Java实现
2014-12-10 分类:自然语言处理 中文分词 阅读(5227) 评论(14)

目录

开源项目
CRF简介
CRF训练
CRF解码
实例
代码
标注结果
最终处理
新词识别

与基于隐马尔可夫模型的最短路径分词、N-最短路径分词相比,基于条件随机场(CRF)的分词对未登录词有更好的支持。本文(HanLP)使用纯Java实现CRF模型的读取与维特比后向解码,内部特征函数采用 双数组Trie树(DoubleArrayTrie)储存,得到了一个高性能的中文分词器。
开源项目

本文代码已集成到HanLP中开源:http://www.hankcs.com/nlp/hanlp.html
CRF简介

CRF是序列标注场景中常用的模型,比HMM能利用更多的特征,比MEMM更能抵抗标记偏置的问题。

[gerative-discriminative.png]
CRF训练

这类耗时的任务,还是交给了用C++实现的CRF++。关于CRF++输出的CRF模型,请参考《CRF++模型格式说明》。
CRF解码

解码采用维特比算法实现。并且稍有改进,用中文伪码与白话描述如下:

首先任何字的标签不仅取决于它自己的参数,还取决于前一个字的标签。但是第一个字前面并没有字,何来标签?所以第一个字的处理稍有不同,假设第0个字的标签为X,遍历X计算第一个字的标签,取分数最大的那一个。

如何计算一个字的某个标签的分数呢?某个字根据CRF模型提供的模板生成了一系列特征函数,这些函数的输出值乘以该函数的权值最后求和得出了一个分数。该分数只是“点函数”的得分,还需加上“边函数”的得分。边函数在本分词模型中简化为f(s’,s),其中s’为前一个字的标签,s为当前字的标签。于是该边函数就可以用一个4*4的矩阵描述,相当于HMM中的转移概率。

实现了评分函数后,从第二字开始即可运用维特比后向解码,为所有字打上BEMS标签。
实例

还是取经典的“商品和服务”为例,首先HanLP的CRFSegment分词器将其拆分为一张表:

商   null    
品   null    
和   null    
服   null    
务   null    

null表示分词器还没有对该字标注。
代码

上面说了这么多,其实我的实现非常简练:

/**
 * 维特比后向算法标注
 *
 * @param table
 */
public void tag(Table table)
{
    int size = table.size();
    int tagSize = id2tag.length;
    double[][] net = new double[size][tagSize];
    for (int i = 0; i < size; ++i)
    {
        LinkedList<double[]> scoreList = computeScoreList(table, i);
        for (int tag = 0; tag < tagSize; ++tag)
        {
            net[i][tag] = computeScore(scoreList, tag);
        }
    }

    if (size == 1)
    {
        double maxScore = -1e10;
        int bestTag = 0;
        for (int tag = 0; tag < net[0].length; ++tag)
        {
            if (net[0][tag] > maxScore)
            {
                maxScore = net[0][tag];
                bestTag = tag;
            }
        }
        table.setLast(0, id2tag[bestTag]);
        return;
    }

    int[][] from = new int[size][tagSize];
    for (int i = 1; i < size; ++i)
    {
        for (int now = 0; now < tagSize; ++now)
        {
            double maxScore = -1e10;
            for (int pre = 0; pre < tagSize; ++pre)
            {
                double score = net[i - 1][pre] + matrix[pre][now] + net[i][now];
                if (score > maxScore)
                {
                    maxScore = score;
                    from[i][now] = pre;
                }
            }
            net[i][now] = maxScore;
        }
    }
    // 反向回溯最佳路径
    double maxScore = -1e10;
    int maxTag = 0;
    for (int tag = 0; tag < net[size - 1].length; ++tag)
    {
        if (net[size - 1][tag] > maxScore)
        {
            maxScore = net[size - 1][tag];
            maxTag = tag;
        }
    }

    table.setLast(size - 1, id2tag[maxTag]);
    maxTag = from[size - 1][maxTag];
    for (int i = size - 2; i > 0; --i)
    {
        table.setLast(i, id2tag[maxTag]);
        maxTag = from[i][maxTag];
    }
    table.setLast(0, id2tag[maxTag]);
}

标注结果

标注后将table打印出来:

CRF标注结果
商   B   
品   E   
和   S   
服   B   
务   E

最终处理

将BEMS该合并的合并,得到:

[商品/null, 和/null, 服务/null]

然后将词语送到词典中查询一下,没查到的暂时当作nx,并记下位置(因为这是个新词,为了表示它的特殊性,最后词性设为null),再次使用维特比标注词性:

[商品/n, 和/cc, 服务/vn]

新词识别

CRF对新词有很好的识别能力,比如:

CRFSegment segment = new CRFSegment();
segment.enablePartOfSpeechTagging(true);
System.out.println(segment.seg("你看过穆赫兰道吗"));

输出:

CRF标注结果
你   S   
看   S   
过   S   
穆   B   
赫   M   
兰   M   
道   E   
吗   S   
[你/rr, 看/v, 过/uguo, 穆赫兰道/null, 吗/y]

null表示新词。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值