自然语言处理入门

分词流程及结果分析#

最长匹配#

以某个下标为起点递增查词的过程中,优先输出更长的单词,这种规则称为最长匹配算法。根据扫描顺序可分为正向最长匹配,逆向最长匹配。

流程#

正向最长匹配的中文分词算法

    /**
     * 正向最长匹配的中文分词算法
     *
     * @param text       待分词的文本
     * @param dictionary 词典
     * @return 单词列表
     */
    public static List<String> segmentForwardLongest(String text, Map<String, CoreDictionary.Attribute> dictionary)
    {
        List<String> wordList = new LinkedList<String>();
        for (int i = 0; i < text.length(); )
        {
            String longestWord = text.substring(i, i + 1);
            for (int j = i + 1; j <= text.length(); ++j)
            {
                String word = text.substring(i, j);
                if (dictionary.containsKey(word))
                {
                    if (word.length() > longestWord.length())
                    {
                        longestWord = word;
                    }
                }
            }
            wordList.add(longestWord);
            i += longestWord.length();
        }
        return wordList;
    }

我们可以看到i从0开始不断加,是正向。if判断word长度更新最长长度,是最长匹配。

逆向最长匹配的中文分词算法

    /**
     * 逆向最长匹配的中文分词算法
     *
     * @param text       待分词的文本
     * @param dictionary 词典
     * @return 单词列表
     */
    public static List<String> segmentBackwardLongest(String text, Map<String, CoreDictionary.Attribute> dictionary)
    {
        List<String> wordList = new LinkedList<String>();
        for (int i = text.length() - 1; i >= 0; )
        {
            String longestWord = text.substring(i, i + 1);
            for (int j = 0; j <= i; ++j)
            {
                String word = text.substring(j, i + 1);
                if (dictionary.containsKey(word))
                {
                    if (word.length() > longestWord.length())
                    {
                        longestWord = word;
                        break;
                    }
                }
            }
            wordList.add(0, longestWord);
            i -= longestWord.length();
        }
        return wordList;
    }

我们可以看到i从0开始不断减,是逆向。if判断word长度更新最长长度,是最长匹配。

双向最长匹配的中文分词算法

    /**
     * 双向最长匹配的中文分词算法
     *
     * @param text       待分词的文本
     * @param dictionary 词典
     * @return 单词列表
     */
    public static List<String> segmentBidirectional(String text, Map<String, CoreDictionary.Attribute> dictionary)
    {
        List<String> forwardLongest = segmentForwardLongest(text, dictionary);
        List<String> backwardLongest = segmentBackwardLongest(text, dictionary);
        if (forwardLongest.size() < backwardLongest.size())
            return forwardLongest;
        else if (forwardLongest.size() > backwardLongest.size())
            return backwardLongest;
        else
        {
            if (countSingleChar(forwardLongest) < countSingleChar(backwardLongest))
                return forwardLongest;
            else
                return backwardLongest;
        }
    }

    /**
     * 统计分词结果中的单字数量
     *
     * @param wordList 分词结果
     * @return 单字数量
     */
    public static int countSingleChar(List<String> wordList)
    {
        int size = 0;
        for (String word : wordList)
        {
            if (word.length() == 1)
                ++size;
        }
        return size;
    }

主函数

    /**
     * 最长匹配分词,字典使用的为HanLP的字典
     * @param testPath 测试集地址
     * @param resultPath 生成结果地址
     */
    public static void segmentLongest(String testPath, String resultPath) throws IOException {
        FileReader fileReader = new FileReader(testPath);
        List<String> testCollections = fileReader.readLines();

        // 加载词典
        TreeMap<String, CoreDictionary.Attribute> dictionary =
                IOUtil.loadDictionary("data/dictionary/CoreNatureDictionary.mini.txt");
        System.out.printf("词典大小:%d个词条\n", dictionary.size());
        System.out.println(dictionary.keySet().iterator().next());

        // 正向最长匹配,逆向最长匹配,双向最长匹配
        for (int i = 0; i < testCollections.size(); i++) {
            String result = StrFormatter.format("| {} | {} | {} | {} | {} |\n",
                    i + 1,
                    testCollections.get(i),
                    segmentForwardLongest(testCollections.get(i), dictionary),
                    segmentBackwardLongest(testCollections.get(i), dictionary),
                    segmentBidirectional(testCollections.get(i), dictionary));
            FileWriter fileWriter = new FileWriter(resultPath);
            fileWriter.append(result);
        }
    }

首先读取找的1200个句子:

image-20210529160603584

按行读取,放入list,加载hanLP的字典,分别对每行句子进行正向最长匹配,逆向最长匹配,双向最长匹配。将得到的结果存到另一个txt文件中。

结果分析#

image-20210529160547971

由于当初每个句子选取较长,导致看起来比较混乱。每句前面有序号,每种最长匹配用 | 分割。

我们看第三句,所有的对于正向最长匹配,结果为:所有,的

对于逆向最长匹配,结果为:所,有的

从正向来说,自然是希望左边的词长度更长,对于逆向来说,自然是希望右边的词长度更长。所以才有了以上结果。

我们看第5句,大人们对于正向和逆向的结果分别为:大人,们------大,人们

由此人们提出了双向最长匹配,同时执行正向和逆向最长匹配,若两者的词数不同,优先返回词数更少的哪一个。否则,返回两者中单字更少的那一个。当单字也相同,优先返回逆向最长匹配的结果。但是我们从第三和第五局中的双向匹配结果来看,其正确率并不高,所以规则集的维护有时是太东墙补西墙,帮倒忙。

二元语法#

流程#

    public static void main(String[] args) throws IOException
    {
        trainBigram(MSR.TRAIN_PATH, MSR_MODEL_PATH);
        Segment segment = loadBigram(MSR_MODEL_PATH);
        evaluate(segment, MSR.OUTPUT_PATH, MSR.TEST_PATH, MSR.TRAIN_WORDS);
    }

首先根据训练集训练模型,然后根据模型得到segment,通过测试集得到预测集。

首先我们需要训练集,我是直接使用hanLP的训练集msr_training.utf8:

image-20210529202331806

我们使用训练集训练出模型:

image-20210529202703443

    /**
     * 训练bigram模型
     *
     * @param corpusPath 语料库路径
     * @param modelPath  模型保存路径
     */
    public static void trainBigram(String corpusPath, String modelPath)
    {
        List<List<IWord>> sentenceList = CorpusLoader.convert2SentenceList(corpusPath);
        for (List<IWord> sentence : sentenceList)
            for (IWord word : sentence)
                if (word.getLabel() == null) word.setLabel("n"); // 赋予每个单词一个虚拟的名词词性
        final NatureDictionaryMaker dictionaryMaker = new NatureDictionaryMaker();
        dictionaryMaker.compute(sentenceList);
        dictionaryMaker.saveTxtTo(modelPath);
    }

通过模型得到segment:

    /**
     * 加载bigram模型
     *
     * @param modelPath 模型路径
     * @param verbose   输出调试信息
     * @param viterbi   是否创建viterbi分词器
     * @return 分词器
     */
    public static Segment loadBigram(String modelPath, boolean verbose, boolean viterbi)
    {
//        HanLP.Config.enableDebug();
        HanLP.Config.CoreDictionaryPath = modelPath + ".txt";
        HanLP.Config.BiGramDictionaryPath = modelPath + ".ngram.txt";
        CoreDictionary.reload();
        CoreBiGramTableDictionary.reload();
        // 以下部分为兼容新标注集,不感兴趣可以跳过
        HanLP.Config.CoreDictionaryTransformMatrixDictionaryPath = modelPath + ".tr.txt";
        if (!modelPath.equals(MSR_MODEL_PATH))
        {
            IOUtil.LineIterator lineIterator = new IOUtil.LineIterator(HanLP.Config.CoreDictionaryTransformMatrixDictionaryPath);
            if (lineIterator.hasNext())
            {
                for (String tag : lineIterator.next().split(","))
                {
                    if (!tag.trim().isEmpty())
                    {
                        Nature.create(tag);
                    }
                }
            }
        }
    }

segment为分词器,得到分词器就可以把测试集分词了,改写evaluate:

    public static CWSEvaluator.Result evaluate(Segment segment, String outputPath, String testFile, String dictPath) throws IOException {
        IOUtil.LineIterator lineIterator = new IOUtil.LineIterator(testFile);
        BufferedWriter bw = IOUtil.newBufferedWriter(outputPath);
        for (String line : lineIterator) {
            List<Term> termList = segment.seg(line.replaceAll("\\s+", "")); // 一些testFile与goldFile根本不匹配,比如MSR的testFile有些行缺少单词,所以用goldFile去掉空格代替
            int i = 0;
            for (Term term : termList) {
                bw.write(term.word);
                if (++i != termList.size())
                    bw.write("  ");
            }
            bw.newLine();
        }
        bw.close();
        CWSEvaluator.Result result = CWSEvaluator.evaluate(testFile, outputPath, dictPath);
        return result;
    }

我们可以得到outputPath,我们能得到预测出的文件就行了。

得到的结果为:

image-20210529205529177

结果分析#

由于人名,地名和机构名等OOV无法识别造成分词过细,结论:OVV召回能力太差。

比如北凉王府,异姓王,徐蛮子,王府,徐骁,小王爷,六国,大柱国等都被分割,那么解决办法就是调整模型,向模型中添加:

北凉王府 ns 1
异姓王 nr 1
徐蛮子 nr 1
王府 ns 1
徐骁 nr 1

条件随机场#

流程#

Java训练模型太慢了,我训练了半小时,然后一看堆堆内存溢出了!!!!!!!

我们下载c++的CRF++来训练模型:

image-20210530202223838

crf_learn.exe用来训练模型,最终得到模型crfpp-model.txt:

根据我们找的1200句子训练模型:

image-20210530135509766

得到模型为:

image-20210530135532851

得到模型后,执行crf_test.exe进行预测:

image-20210530135700614

可以看到最后一列的预测结果与标准答案完全一致!!

结果分析:
条件随机场的各项指标全面胜过了结构化感知机,综合F1更高,是传统方法总最准确的分词模型!!!

总结#

我们从最基础的速度最快的字典分词,到准确率最高的条件随机场。一路走来,一步步对预测进行优化。

字典分词能达到每秒千万字的速度,但是准确率并不高,无法区分歧义,也无法召回新词。

从二元语法分词,我们掌握了NLP中最重要的概念之一:语言模型。相较于词典分词,二元语法分词器在MSR语料库上的F1值提高了0.025.二元语法分词有些问题,为了解决问题,我们需要调整模型,但是OOV召回仍然不足。

隐马尔可夫模型是最简单的序列标注模型,基本问题有三个:样本生成,参数估计,序列预测。中文分词效果并不理想,虽然找回了一半的OOV,但是综合F1甚至低于词典分词。我们要更高级的模型。

结构化感知机是更高级的模型,他的召回了百分之七十的为登录词,那么还有更高的准确率么?

条件随机场的各项指标都胜过结构化感知机,是传统方法中最准确的分词模型。至此,我们中文分词就基本完成了。

但这还不够,我们仍然通过机器学习结合语料库,成长成为自然语言处理工程师!

posted @   KeBoom  阅读(303)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示
主题色彩