Skip to content

结构化感知机标注框架

hankcs edited this page Apr 10, 2020 · 35 revisions

基于感知机的中文分词、词性标注与命名实体识别框架

这是一套利用感知机做序列标注任务,并且应用到中文分词、词性标注与命名实体识别这3个问题的完整在线学习框架。该框架利用1个算法解决3个问题,是自治统一的系统。同时三个任务顺序渐进,构成流水线式的系统。

uml

快速上手

不想训练模型的用户可直接参考 DemoPerceptronLexicalAnalyzer

需要训练模型的用户不必关心算法细节,只需顺着本文档将【训练】【测试】两个步骤走通,就能解决上述3个问题。

本文档配有示例代码和示例语料,分别位于src/test/java/com/hankcs/hanlp/model/perceptronOpenCorpus/pku98/199801.txt,欢迎参考。

语料格式

为了简化语料预处理的工作,无论是中文分词、词性标注还是命名实体识别,本系统都采用同一种语料格式,即人民日报2014语料格式:

  1. 单词与词性之间使用/分割,如华尔街/nsf,且任何单词都必须有词性,包括标点等。
  2. 单词与单词之间使用空格分割,如美国/nsf 华尔街/nsf 股市/n
  3. 支持用[]将多个单词合并为一个复合词,如[纽约/nsf 时报/n]/nz,复合词也必须遵守1和2两点规范。

训练时将满足上述格式的语料以纯文本txt导出到一个目录下即可。OpenCorpus/pku98/199801.txt即为一个单文档的例子,可供参考。

  • 使用语料时请注意授权信息。
  • 该语料合并了原版中的姓+名=姓名,与原版或sighan2005的分词标准并不相同,不可混用。
  • 一些常见语料的分词标准对比可参考《多标准中文分词》
  • 数据包中data/model/perceptron/pku199801下提供了在该语料上训练的模型。

特别地,考虑到有许多中文分词语料不含词性,这种语料也被分词模块支持。

中文分词

训练

只需指定输入语料的路径(单文档时为文件路径,多文档时为文件夹路径,灵活处理),以及模型保存位置即可:

命令行

java -cp hanlp.jar com.hankcs.hanlp.model.perceptron.Main -task CWS -train -reference data/test/pku98/199801.txt -model data/test/perceptron/cws.bin

API

    public void testTrain() throws Exception
    {        
        PerceptronTrainer trainer = new CWSTrainer();
        PerceptronTrainer.Result result = trainer.train(
            "data/test/pku98/199801.txt",
            Config.CWS_MODEL_FILE
            );
        //        System.out.printf("准确率F1:%.2f\n", result.prf[2]);
    }

事实上,视语料与任务的不同,迭代数、压缩比和线程数都可以自由调整,以保证最佳结果:

    /**
     * 训练
     *
     * @param trainingFile  训练集
     * @param developFile   开发集
     * @param modelFile     模型保存路径
     * @param compressRatio 压缩比
     * @param maxIteration  最大迭代次数
     * @param threadNum     线程数
     * @return 一个包含模型和精度的结构
     * @throws IOException
     */
    public Result train(String trainingFile, String developFile,
                        String modelFile, final double compressRatio,
                        final int maxIteration, final int threadNum) throws IOException

单线程时使用AveragedPerceptron算法,收敛较好;多线程时使用StructuredPerceptron,波动较大。关于两种算法的精度比较,请参考下一小节。目前默认多线程,线程数为系统CPU核心数。请根据自己的需求平衡精度和速度。

准确率

sighan2005的msr数据集上的性能评估结果如下:

算法 P R F1 训练评测总耗时
AveragedPerceptron 96.41 96.26 96.34 128秒
StructuredPerceptron 96.33 96.32 96.33 59秒
  • 语料未进行任何预处理
  • 只使用了7种状态特征,未使用词典
  • 压缩比0.0,迭代数50
  • 总耗时包含语料加载与模型序列化

对任意PerceptronTagger,用户都可以调用准确率评估接口:


    /**
     * 性能测试
     *
     * @param corpora 数据集
     * @return 默认返回accuracy,有些子类可能返回P,R,F1
     * @throws IOException
     */
    public double[] evaluate(String corpora) throws IOException

速度

目前感知机分词是所有“由字构词”的分词器实现中最快的,比自己写的CRF解码快1倍。新版CRF词法分析器框架复用了感知机的维特比解码算法,所以速度持平。

感知机 HanLP-CRF CRF++
单线程(字符/秒) 56万 31万 6万
多线程(字符/秒) 270万 117万 40万

speed

  • 测试时需关闭词法分析器的自定义词典、词性标注和命名实体识别
  • 测试环境 Java8 i7-6700K

测试

测试时只需提供分词模型的路径即可:

public void testCWS() throws Exception
{
    PerceptronSegmenter segmenter = new PerceptronSegmenter(Config.CWS_MODEL_FILE);
    System.out.println(segmenter.segment("商品和服务"));
}

正常情况下对商品和服务的分词结果为[商品, 和, 服务]。建议在任何语料上训练时都试一试这个简单的句子,当作HelloWorld来测试。若这个例子都错了,则说明语料格式、规模或API调用上存在问题,须仔细排查,而不要急着部署上线。

另外,数据包中已经打包了在人民日报语料1998年1月份上训练的模型,不传路径时将默认加载配置文件中指定的模型。

在本系统中,分词器PerceptronSegmenter的职能更加单一,仅仅负责分词,不再负责词性标注或命名实体识别。这是一次接口设计上的新尝试,未来可能在v2.0中大规模采用这种思路去重构。

词性标注

训练

词性标注是分词后紧接着的一个任务,训练语料同上,接口如下:

命令行

java -cp hanlp.jar com.hankcs.hanlp.model.perceptron.Main -task POS -train -reference data/test/pku98/199801.txt -model data/test/perceptron/pos.bin

API

public void testTrain() throws Exception
{
    PerceptronTrainer trainer = new POSTrainer();
    trainer.train("data/test/pku98/199801.txt", Config.POS_MODEL_FILE);
}

测试

词性标注器接受的输入不再是纯文本,而是分词后的单词数组或列表:

public void testLoad() throws Exception
{
    PerceptronPOSTagger tagger = new PerceptronPOSTagger(Config.POS_MODEL_FILE);
    System.out.println(Arrays.toString(tagger.tag("中国 交响乐团 谭利华 在 布达拉宫 广场 演出".split(" "))));
}

正常情况下输出每个单词的词性:

[ns, n, nr, p, ns, n, v]

关于如何组合分词器和词性标注器,使其同时进行分词与词性标注,请参考接下来的章节。

命名实体识别

目前本系统默认支持人名(nr),地名(ns),机构名(nt)三种命名实体的识别,用户可以重载NERTrainercreateTagSet来支持任意NER类型。

训练

命名实体识别是词性标注的后续任务,训练语料依然同上,接口如下:

命令行

java -cp hanlp.jar com.hankcs.hanlp.model.perceptron.Main -task NER -train -reference data/test/pku98/199801.txt -model data/test/perceptron/ner.bin

API

public void testTrain() throws Exception
{
    PerceptronTrainer trainer = new NERTrainer();
    trainer.train("data/test/pku98/199801.txt", Config.NER_MODEL_FILE);
}

自定义NER类型

重载NERTrainercreateTagSet来支持自己的NER类型。当然,用户提供的语料必须满足2014人民日报格式。

        PerceptronTrainer trainer = new NERTrainer()
        {
            @Override
            protected TagSet createTagSet()
            {
                NERTagSet tagSet = new NERTagSet();
                tagSet.nerLabels.add("YourNER1");
                tagSet.nerLabels.add("YourNER2");
                tagSet.nerLabels.add("YourNER3");
                return tagSet;
            }
        };

测试

命名实体识别器的输入不再是纯文本,而是分词结果与词性标注结果:

public void testTag() throws Exception
{
    PerceptionNERecognizer recognizer = new PerceptionNERecognizer(Config.NER_MODEL_FILE);
    System.out.println(Arrays.toString(recognizer.recognize("吴忠市 乳制品 公司 谭利华 来到 布达拉宫 广场".split(" "), "ns n n nr p ns n".split(" "))));
}

正常情况下输出:

[B-nt, M-nt, E-nt, S, O, S, O]

7个标签代表上述7个词语所属的命名实体成分。

词法分析器

本系统将同时进行中文分词、词性标注与命名实体识别3个任务的子系统称为“词法分析器”。

加载

对应的类为PerceptronLexicalAnalyzer,其构造方法为递增的3个模型地址:

public PerceptronLexicalAnalyzer(String cwsModelFile) throws IOException
public PerceptronLexicalAnalyzer(String cwsModelFile, String posModelFile) throws IOException
public PerceptronLexicalAnalyzer(String cwsModelFile, String posModelFile, String nerModelFile) throws IOException

用户根据自己要进行的任务,训练3个模型中的任意个数,然后灵活传入此类构造即可。此处假设训练了3个模型,那么传入这3个模型的路径即可构造词法分析器:

public void testCWSandPOSandNER() throws Exception
{
    PerceptronLexicalAnalyzer segmenter = new PerceptronLexicalAnalyzer(Config.CWS_MODEL_FILE, Config.POS_MODEL_FILE, Config.NER_MODEL_FILE);
}

分析

词法分析器的分析接口如下:

public static final String SENTENCE = "香港特别行政区的张朝阳说商品和服务是三原县鲁桥食品厂的主营业务";
public void testCWSandPOSandNER() throws Exception
{
    PerceptronLexicalAnalyzer segmenter = new PerceptronLexicalAnalyzer(Config.CWS_MODEL_FILE, Config.POS_MODEL_FILE, Config.NER_MODEL_FILE);
    Sentence sentence = segmenter.analyze(SENTENCE);
    System.out.println(sentence);
}

正常情况下输出:

[香港/ns 特别/a 行政区/n]/ns 的/n 张朝阳/nr 说/v 商品/n 和/c 服务/vn 是/v [三原县/ns 鲁桥/nz 食品厂/n]/nt 的/z 主营/vn 业务/n

Sentence结构是一个对人民日报语料格式的实现,用户可以方便地用for循环去遍历单词,用instanceof来判断单词属于复合词还是简单词。此处演示输出句子中所有复合词内部的简单词:

for (IWord word : sentence)
{
    if (word instanceof CompoundWord)
        System.out.println(((CompoundWord) word).innerList);
}

结果:

[香港/ns, 特别/a, 行政区/n]
[三原县/ns, 鲁桥/nz, 食品厂/n]

通过此结构,我们可以捕捉语言的复合结构(简单词构成复合词)。此结构输出为文本后满足人民日报2014语料格式,形成了一个语料与文本之间的闭环。

与HanLP旧接口的兼容

本系统依然兼容HanLP的seg接口,与analyze接口比较如下:

System.out.println(segmenter.seg(SENTENCE));
System.out.println(segmenter.analyze(SENTENCE));

输出:

[香港特别行政区/ns, 的/n, 张朝阳/nr, 说/v, 商品/n, 和/c, 服务/vn, 是/v, 三原县鲁桥食品厂/nt, 的/z, 主营/vn, 业务/n]
[香港/ns 特别/a 行政区/n]/ns 的/n 张朝阳/nr 说/v 商品/n 和/c 服务/vn 是/v [三原县/ns 鲁桥/nz 食品厂/n]/nt 的/z 主营/vn 业务/n

注意上面两个结果中的命名实体有着本质的不同,seg接口无法输出层次结构,而analyze接口可以。

在线学习

本框架另一个特色功能是“在线学习”,或称“增量训练”。其适用场景如下: 线上系统的统计模型依然会犯错误,但重新训练的代价过大(比如耗时长,没有语料等等)。本系统支持在线学习新知识,实时修正统计模型的错误。这里举一个分词的例子,人民日报1998年1月份训练出来的模型无法分对“下雨天地面积水”这个句子:

PerceptronSegmenter segmenter = new PerceptronSegmenter(Config.CWS_MODEL_FILE);
System.out.println(segmenter.segment("下雨天地面积水"));

输出:

[下雨, 天地, 面积, 水]

但本系统支持在线学习这个句子的正确分词方式:

segmenter.learn("下雨天 地面 积水");
System.out.println(segmenter.segment("下雨天地面积水"));

通过learn接口,感知机模型学习到了这个句子的正确分词方式,并输出了正确结果:

[下雨天, 地面, 积水]

对于类似的句子,也拥有了举一反三的泛化能力:

System.out.println(segmenter.segment("下雨天地面积累了很多水"));

输出:

[下雨天, 地面, 积累, 了, 很多, 水]

词性标注器和命名实体识别器也有类似的learn接口,用户可举一反三类似地调用,不再赘述。

模型压缩与持久化

在线学习或训练后的模型可以序列化到某个路径,其接口是:

    /**
     * @param ratio 压缩比c(压缩掉的体积,压缩后体积变为1-c)
     * @return
     */
    public LinearModel compress(final double ratio)
    
    /**
     * 保存到路径
     *
     * @param modelFile
     * @throws IOException
     */
    public void save(String modelFile, final double ratio) throws IOException

比如压缩比为0.1,则压缩后的体积为原来的0.9。此处的“体积”指的是特征数量,并不一定等于文件体积。

命令行接口

如上文所述,本框架中的功能可以通过命令行调用:

$ java -cp hanlp.jar com.hankcs.hanlp.model.perceptron.Main
缺少必需参数: -model
用法: com.hankcs.hanlp.model.perceptron.Main
  -task	[TaskType]	任务类型:CWS|POS|NER (CWS)
  -train	[flag]	执行训练任务
  -test	[flag]	执行预测任务
  -evaluate	[flag]	执行评估任务
  -model	[String]	模型文件路径
  -input	[String]	输入文本路径
  -result	[String]	结果保存路径
  -gold	[String]	标准分词语料
  -reference	[String]	训练集
  -development	[String]	开发集
  -iter	[Integer]	迭代次数 (5)
  -compressRatio	[Double]	模型压缩比率 (0.0)
  -thread	[int]	线程数 (8)

当用户按照上文所述训练了13个模型后,可以通过命令行接口交互式地观察效果:

$ java -cp target/hanlp-1.6.0.jar:src/main/resources com.hankcs.hanlp.model.perceptron.Main -test
商品和服务
商品/n 和/c 服务/vn
上海华安工业(集团)公司董事长谭旭光和秘书胡花蕊来到美国纽约现代艺术博物馆参观
[上海/ns 华安/nz 工业/n (/w 集团/n )/w 公司/n]/nt 董事长/n 谭旭光/nr 和/c 秘书/n 胡花蕊/nr 来到/v [美国/ns 纽约/ns 现代/t 艺术/n 博物馆/n]/ns 参观/v
  • 默认加载配置文件指定的模型,可以通过-model your/cws.bin,your/pos.bin,your/ner.bin指定别的模型。
  • 还可以将输入输出重定向到文件,形成一个pipeline。
  • 更多信息,请参考《编译运行》

未来工作

  • 英文和数字最好要做特殊处理。
  • hanlp-lucene-plugin的集成。
  • 集成自定义词典。
  • 索引分词等功能。
  • 重构出新的分词、词性标注与命名实体识别接口,统一所有分词器,并逐步淘汰旧接口。

参考文献

  1. 邓知龙 《基于感知器算法的高效中文分词与词性标注系统设计与实现》
  2. 《基于结构化平均感知机的分词器Java实现》