HanLP — 词性标注

简介: HanLP — 词性标注

目录


词性(Part-Of-Speech,POS)指的是单词的语法分类,也称为词类。同一个类别的词语具有相似的语法性质

所有词性的集合称为词性标注集

词性的用处

当下游应用遇到OOV时,可以通过OOV的词性猜测用法词性也可以直接用于抽取一些信息,比如抽取所有描述特定商品的形容词等

词性标注

词性标注指的是为句子中每个单词预测一个词性标签的任务

  • 汉语中一个单词多个词性的现象很常见(称作兼类词)
  • OOV是任何自然语言处理任务的难题

词性标注模型

联合模型

同时进行多个任务的模型称为联合模型(joint model)

商 B-名词
品 E-名词
和 S-连词
服 B-名词
务 E-名词

流水线式

中文分词语料库远远多于词性标注语料库

实际工程上通常在大型分词语料库上训练分词器

然后与小型词性标注语料库上的词性标注模型灵活组合为一个异源的流水线式词法分析器

词性标注语料库与标注集

目前还没有一个被广泛接受的汉语词性划分标准

本节选取其中一些授权宽松,容易获得的语料库作为案例,介绍其规模、标注集等特点

《人民日报》语料库与PKU标注集

http://file.hankcs.com/corpus/pku98.zip

https://www.fujitsu.com/cn/about/resources/news/press-releases/2001/0829.html

语料库中的一句样例为:

1997年/t 12月/t 31日/t 午夜/t ,/w 聚集/v 在/p 日本/ns 东京/ns 增上寺/ns 的/u 善男信女/i 放飞/v 气球/n ,/w 祈祷/v 新年/t 好运/n 。

国家语委语料库与863标注集

国家语言文字工作委员会建设的大型语料库

国家语委语料库的标注规范《信息处理用现代汉语词类标记集规范》在2006年成为国家标准

其词类体系分为20个一级类、29个二级类

《诛仙》语料库与CTB标注集

哈工大张梅山老师公开了网络小说《诛仙》上的标注语料

远处/NN ,/PU 小竹峰/NR 诸/DT 人/NN 处/NN ,/PU 陆雪琪/NR 缓缓/AD 从/P 张小凡/NR 身上/NN 收回/VV 目光/NN ,/PU 落到/VV 了/AS 前方/NN 碧瑶/NR 的/DEG 身上/NN ,/PU 默默/AD 端详/VV 著/AS 她/PN 。/PU

《诛仙》语料库采用的标注集与CTB(Chinese Treebank,中文树库)相同,一共33种词类

序列标注模型应用于词性标注

HanLP中词性标注由POSTagger接口提供

训练集转换成 Sentence 对像

IOUtility.loadInstance

/**
 * 将语料库中每行文字,字符、词性,转换成 Sentence,由具体的实现类去做 handler.process 处理
 * @param path
 * @param handler
 * @return
 * @throws IOException
 */
public static int loadInstance(final String path, InstanceHandler handler) throws IOException {
    ConsoleLogger logger = new ConsoleLogger();
    int size = 0;
    File root = new File(path);
    File allFiles[];
    if (root.isDirectory()) {
        allFiles = root.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile() && pathname.getName().endsWith(".txt");
            }
        });
    } else {
        allFiles = new File[]{root};
    }
    for (File file : allFiles) {
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
        String line;
        while ((line = br.readLine()) != null) {
            line = line.trim();
            if (line.length() == 0) {
                continue;
            }
            //line=迈向/v 充满/v  希望/n  的/u 新/a 世纪/n  ——/w  一九九八年/t 新年/t  讲话/n  (/w 附/v 图片/n  1/m 张/q )/w
            Sentence sentence = Sentence.create(line);
            //wordList[0].value="迈向"
            //wordList[0].label="v"
            if (sentence.wordList.size() == 0) continue;
            ++size;
            if (size % 1000 == 0) {
                logger.err("%c语料: %dk...", 13, size / 1000);
            }
            // debug
//                if (size == 100) break;
            if (handler.process(sentence)) break;
        }
    }
    return size;
}

基于隐马尔可夫模型的词性标注

HanLP-1.7.5\src\main\java\com\hankcs\hanlp\model\hmm\HMMTrainer.java

/***
 * HMM 训练
 * @param corpus data/test/pku98/199801-train.txt
 * @throws IOException
 */
public void train(String corpus) throws IOException
{
    final List<List<String[]>> sequenceList = new LinkedList<List<String[]>>();
    IOUtility.loadInstance(corpus, new InstanceHandler()
    {
        @Override
        public boolean process(Sentence sentence)
        {
            sequenceList.add(convertToSequence(sentence));
            return false;
        }
    });
    TagSet tagSet = getTagSet();
    List<int[][]> sampleList = new ArrayList<int[][]>(sequenceList.size());
    for (List<String[]> sequence : sequenceList)
    {
        int[][] sample = new int[2][sequence.size()];
        int i = 0;
        for (String[] os : sequence)
        {
            sample[0][i] = vocabulary.idOf(os[0]);
            assert sample[0][i] != -1;
            sample[1][i] = tagSet.add(os[1]);
            assert sample[1][i] != -1;
            ++i;
        }
        sampleList.add(sample);
    }
    model.train(sampleList);
    vocabulary.mutable = false;
}

基于感知机的词性标注

/**
 * 训练
 *
 * @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
{
    if (developFile == null)
    {
        developFile = trainingFile;
    }
    // 加载训练语料
    TagSet tagSet = createTagSet();
    MutableFeatureMap mutableFeatureMap = new MutableFeatureMap(tagSet);
    ConsoleLogger logger = new ConsoleLogger();
    logger.start("开始加载训练集...\n");
    Instance[] instances = loadTrainInstances(trainingFile, mutableFeatureMap);
    tagSet.lock();
    logger.finish("\n加载完毕,实例一共%d句,特征总数%d\n", instances.length, mutableFeatureMap.size() * tagSet.size());
    // 开始训练
    ImmutableFeatureMap immutableFeatureMap = new ImmutableFeatureMap(mutableFeatureMap.featureIdMap, tagSet);
    mutableFeatureMap = null;
    double[] accuracy = null;
    if (threadNum == 1)
    {
        AveragedPerceptron model;
        model = new AveragedPerceptron(immutableFeatureMap);
        final double[] total = new double[model.parameter.length];
        final int[] timestamp = new int[model.parameter.length];
        int current = 0;
        for (int iter = 1; iter <= maxIteration; iter++)
        {
            Utility.shuffleArray(instances);
            for (Instance instance : instances)
            {
                ++current;
                int[] guessLabel = new int[instance.length()];
                model.viterbiDecode(instance, guessLabel);
                for (int i = 0; i < instance.length(); i++)
                {
                    int[] featureVector = instance.getFeatureAt(i);
                    int[] goldFeature = new int[featureVector.length];
                    int[] predFeature = new int[featureVector.length];
                    for (int j = 0; j < featureVector.length - 1; j++)
                    {
                        goldFeature[j] = featureVector[j] * tagSet.size() + instance.tagArray[i];
                        predFeature[j] = featureVector[j] * tagSet.size() + guessLabel[i];
                    }
                    goldFeature[featureVector.length - 1] = (i == 0 ? tagSet.bosId() : instance.tagArray[i - 1]) * tagSet.size() + instance.tagArray[i];
                    predFeature[featureVector.length - 1] = (i == 0 ? tagSet.bosId() : guessLabel[i - 1]) * tagSet.size() + guessLabel[i];
                    model.update(goldFeature, predFeature, total, timestamp, current);
                }
            }
            // 在开发集上校验
            accuracy = trainingFile.equals(developFile) ? IOUtility.evaluate(instances, model) : evaluate(developFile, model);
            out.printf("Iter#%d - ", iter);
            printAccuracy(accuracy);
        }
        // 平均
        model.average(total, timestamp, current);
        accuracy = trainingFile.equals(developFile) ? IOUtility.evaluate(instances, model) : evaluate(developFile, model);
        out.print("AP - ");
        printAccuracy(accuracy);
        logger.start("以压缩比 %.2f 保存模型到 %s ... ", compressRatio, modelFile);
        model.save(modelFile, immutableFeatureMap.featureIdMap.entrySet(), compressRatio);
        logger.finish(" 保存完毕\n");
        if (compressRatio == 0) return new Result(model, accuracy);
    }
    else
    {
        // 多线程用Structure Perceptron
        StructuredPerceptron[] models = new StructuredPerceptron[threadNum];
        for (int i = 0; i < models.length; i++)
        {
            models[i] = new StructuredPerceptron(immutableFeatureMap);
        }
        TrainingWorker[] workers = new TrainingWorker[threadNum];
        int job = instances.length / threadNum;
        for (int iter = 1; iter <= maxIteration; iter++)
        {
            Utility.shuffleArray(instances);
            try
            {
                for (int i = 0; i < workers.length; i++)
                {
                    workers[i] = new TrainingWorker(instances, i * job,
                                                    i == workers.length - 1 ? instances.length : (i + 1) * job,
                                                    models[i]);
                    workers[i].start();
                }
                for (TrainingWorker worker : workers)
                {
                    worker.join();
                }
                for (int j = 0; j < models[0].parameter.length; j++)
                {
                    for (int i = 1; i < models.length; i++)
                    {
                        models[0].parameter[j] += models[i].parameter[j];
                    }
                    models[0].parameter[j] /= threadNum;
                }
                accuracy = trainingFile.equals(developFile) ? IOUtility.evaluate(instances, models[0]) : evaluate(developFile, models[0]);
                out.printf("Iter#%d - ", iter);
                printAccuracy(accuracy);
            }
            catch (InterruptedException e)
            {
                err.printf("线程同步异常,训练失败\n");
                e.printStackTrace();
                return null;
            }
        }
        logger.start("以压缩比 %.2f 保存模型到 %s ... ", compressRatio, modelFile);
        models[0].save(modelFile, immutableFeatureMap.featureIdMap.entrySet(), compressRatio, HanLP.Config.DEBUG);
        logger.finish(" 保存完毕\n");
        if (compressRatio == 0) return new Result(models[0], accuracy);
    }
    LinearModel model = new LinearModel(modelFile);
    if (compressRatio > 0)
    {
        accuracy = evaluate(developFile, model);
        out.printf("\n%.2f compressed model - ", compressRatio);
        printAccuracy(accuracy);
    }
    return new Result(model, accuracy);
}

基于条件随机场的词性标注

词性标注评测

PosTagUtil.evaluate

/**
 * 评估词性标注器的准确率
 *
 * @param tagger 词性标注器
 * @param corpus 测试集  data/test/pku98/199801-test.txt
 * @return Accuracy百分比
 */
public static float evaluate(POSTagger tagger, String corpus)
{
    int correct = 0, total = 0;
    IOUtil.LineIterator lineIterator = new IOUtil.LineIterator(corpus);
    //循环测试集中的每一行数据
    for (String line : lineIterator)
    {
        Sentence sentence = Sentence.create(line);
        if (sentence == null) continue;
        //将字符,词性,转成二维数组
        String[][] wordTagArray = sentence.toWordTagArray();
        //根据测试字符获得预测的词性
        String[] prediction = tagger.tag(wordTagArray[0]);
        //看预测词性的长度和测试词性长度是否相等
        assert prediction.length == wordTagArray[1].length;
        // 199801-test.txt  中的所有字符个数
        total += prediction.length;
        for (int i = 0; i < prediction.length; i++)
        {
            //预测词性 = 测试词性,预测正确的标签总数 + 1
            if (prediction[i].equals(wordTagArray[1][i])){
                ++correct;
            }
        }
    }
    if (total == 0) return 0;
    //准确率  = 预测正确标签总数 / 标签总数
    return correct / (float) total * 100;
}

自定义词性

在工程上,许多用户希望将特定的一些词语打上自定义的标签,称为自定义词性

朴素实现

规则系统,用户将自己关心的词语以及自定义词性以词典的形式交给HanLP挂载

CustomDictionary.insert("苹果", "手机品牌 1")CustomDictionary.insert("iPhone X", "手机型号 1")analyzer = PerceptronLexicalAnalyzer()analyzer.enableCustomDictionaryForcing(True)print(analyzer.analyze("你们苹果iPhone X保修吗?"))print(analyzer.analyze("多吃苹果有益健康"))

你们/r 苹果/手机品牌 iPhone X/手机型号 保修/v 吗/y ?/w

多/ad 吃/v 苹果/手机品牌 有益健康/i

标注语料

PerceptronPOSTagger posTagger = trainPerceptronPOS(ZHUXIAN); // 训练AbstractLexicalAnalyzer analyzer = new AbstractLexicalAnalyzer(new PerceptronSegmenter(), posTagger); // 包装System.out.println(analyzer.analyze("陆雪琪的天琊神剑不做丝毫退避,直冲而上,瞬间,这两道奇光异宝撞到了一起。")); // 分词+标注

陆雪琪/NR 的/DEG 天琊神剑/NN 不/AD 做/VV 丝毫/NN 退避/VV ,/PU 直冲/VV 而/MSP 上/VV ,/PU 瞬间/NN ,/PU 这/DT 两/CD 道/M 奇光/NN 异宝/NN 撞/VV 到/VV 了/AS 一起/AD 。/PU

总结

隐马尔可夫模型、感知机和条件随机场三种词性标注器

为了实现自定义词性

依靠词典匹配虽然简单但非常死板,只能用于一词一义的情况

如果涉及兼类词,标注一份领域语料才是正确做法

目录
相关文章
|
自然语言处理 Python
【NLP Tool -- NLTK】NLTK进行英文情感分析、分词、分句、词性标注(附代码)
NLP自然语言处理之NLTK工具的使用,进行英文情感分析、分词、分句、词性标注(附代码)
718 0
|
自然语言处理 Java Python
自然语言处理hanlp------10HanLP的词典分词实现
自然语言处理hanlp------10HanLP的词典分词实现
自然语言处理hanlp------10HanLP的词典分词实现
NLTK词性标注
本文实现基于NLTK的布朗语料库词性标注任务。
74 0
|
机器学习/深度学习 自然语言处理 算法
Hanlp中使用纯JAVA实现CRF分词
与基于隐马尔可夫模型的最短路径分词、N-最短路径分词相比,基于条件随机场(CRF)的分词对未登录词有更好的支持。本文(HanLP)使用纯Java实现CRF模型的读取与维特比后向解码,内部特征函数采用 双数组Trie树(DoubleArrayTrie)储存,得到了一个高性能的中文分词器。
4755 1
|
自然语言处理
Ansj与hanlp分词工具对比
一、Ansj1、利用DicAnalysis可以自定义词库: 2、但是自定义词库存在局限性,导致有些情况无效:比如:“不好用“的正常分词结果:“不好,用”。 (1)当自定义词库”好用“时,词库无效,分词结果不变。
1101 0
HanLP-分类模块的分词器介绍
最近发现一个很勤快的大神在分享他的一些实操经验,看了一些他自己关于hanlp方面的文章,写的挺好的!转载过来分享给大家!以下为分享原文(无意义的内容已经做了删除)如下图所示,HanLP的分类模块中单独封装了适用分类的分词器,当然这些分词器都是对HanLP提供的分词器的封装。
5972 0
|
自然语言处理
HanLP-实词分词器详解
在进行文本分类(非情感分类)时,我们经常只保留实词(名、动、形)等词,为了文本分类的分词方便,HanLP专门提供了实词分词器类NotionalTokenizer,同时在分类数据集加载处理时,默认使用了NotionalTokenizer分词器。
1752 0
|
自然语言处理 算法 测试技术
分词工具Hanlp基于感知机的中文分词框架
结构化感知机标注框架是一套利用感知机做序列标注任务,并且应用到中文分词、词性标注与命名实体识别这三个问题的完整在线学习框架,该框架利用
2071 0
|
自然语言处理
如何在hanlp词典中手动添加未登录词
我们在使用hanlp词典进行分词的时候,难免会出现分词不准确的情况,原因是由于内置词典中并没有收录当前的这个词,也就是我们所说的未登录词,只要把这个词加入到内置词典中就可以解决类似问题,如何操作,下面我们就看一下具体的步骤
2652 0
|
缓存 自然语言处理
Hanlp自然语言处理中的词典格式说明
使用过hanlp的都知道hanlp中有许多词典,它们的格式都是非常相似的,形式都是文本文档,随时可以修改。本篇文章详细介绍了hanlp中的词典格式,以满足用户自定义的需要。
4840 0