NLP最简单中文分词介绍(二)

上一篇的中文分词,这一篇将发射概率矩阵,状态转移矩阵和Viterbi算法都用上了,然后使用代码实现最简单的中文分词功能.相比较于上一篇的遍历所有可能组合,这一篇使用了Viterbi算法排除掉了不符合要求的组合,减少了不必要的消耗.

前期准备

功能说明

如果一句话里面的所有字,都能分类到 BMES 里面(可重复),那么就可对它进行分词。

例:{陈佩斯吃西瓜}
分类
词头 B: 陈佩斯吃西瓜 比如 陈述,佩服,斯文,吃饭,西方,瓜壳
词中 M: 佩 比如 陈佩斯,其他字不行,我说的>_<。
词尾 E:佩斯吃西瓜 比如 钦佩,陈佩斯,好吃,东西,木瓜
单字 S:吃
挑选代码执行结果里面概率最大的组合
BMESBE 即 词头词中词尾 单字成词 词头词尾
对照 {陈佩斯吃西瓜} 进行分词
结果 {陈佩斯 吃 西瓜}

  • 这一篇使用了Viterbi算法实现最大概率路径查找,避免了遍历所有的组合.
  • 原理就是从左到右,对于每个字都取最大概率的隐藏状态.
  • 比如,对于第一个字 {陈},它对应于4种隐藏状态 {B, M, E, S},然后概率分别为{0.5, 0, 0, 0.5},因为作为句子的开头,只能是{B, S}中的一个.这个概率也是初始状态概率.
  • 对于第二个字 {佩}, 它也有4种隐藏状态 {B, M, E, S},和第一个字可能的状态组合起来就是{BB, BM, BE, BS, SB, SM, SE, SS}这几种,排除掉不可能的情况,剩下 {BM, BE, SB, SS}这三个.
  • {BM} 的概率为选择一个 B 骰子,并骰一次,正好是 {陈} 字;再选择M 骰子,并骰一次,正好是 {佩} 字的概率,计算过程为 0.5 * 1/6 * 0.6 * 1.
  • 0.5来自于代码里面的 initStatus[0], 1/6 来自于 emitProbMatrix[0][0], 0.6 来自于 transProbMatrix2[0][1], 1 来自于 emitProbMatrix[1][1].

Viterbi算法计算的最大概率路径
在这里插入图片描述

代码实现

/**
     * 使用Viterbi算法,减少循环次数.
     */
    @Test
    public void testHMM2() {
        // 1.隐藏状态集合,包括词头,词中、词尾、单字成词。
        String[] status = new String[]{"B", "M", "E", "S"};

        // 2.观察状态集合,即我们需要分词的句子。
        String str = "陈佩斯吃西瓜";

        // 3.发射概率矩阵,也称混淆矩阵, 包含了给定隐马尔科夫模型的某一个特殊的隐藏状态,观察到的某个观察状态的概率,通过语料训练获得。
        // 4*6 矩阵说明: 行号依次是 4 种隐藏状态{BMES},列号依次是 6 种观察状态{陈佩斯吃西瓜},且每列之和为 1。
        // 第一行第一列值为 1/6,它的意思是对于 B 状态即词头而言,有{陈佩斯吃西瓜}这 6 种观察状态,{陈}占其中 1/6。
        // 第二行第二列值为 1,它的意思是对于 M 状态即词头而言,有{佩}这 1 种观察状态,{佩}占其中 1/1,其他都为0。
        double[][] emitProbMatrix = new double[][] {
                {(double)1/6,(double)1/6,(double)1/6,(double)1/6,(double)1/6,(double)1/6},
                {0,1,0,0,0,0},
                {0,0.2,0.2,0.2,0.2,0.2},
                {0,0,0,1,0,0}
        };

        // 4.初始状态概率,一句话中第一个字符对应的状态概率
        double initStatus[] = new double[] {0.5, 0, 0, 0.5};

        // 状态转移矩阵,状态集合间的相互转换概率,4*4矩阵,通过语料训练获得。
        double transProbMatrix2[][] = new double[][] {
                {0, 0.6, 0.4, 0},
                {0, 0.4, 0.6, 0},
                {0.4, 0, 0, 0.6},
                {0.6, 0, 0, 0.4}
        };

        // 5.模拟分析过程.
        // 使用Viterbi算法计算最大概率
        // 第二个字是B的可能路径 BB, MB, EB, SB
        double sb = 0.6;
        // 第二个字是M的可能路径 BM, MM, EM, SM
        double bm = 0.6;
        // 第二个字是E的可能路径 BE, ME, EE, SE
        double me = 0.6;;
        // 第二个字是S的可能路径 BS, MS, ES, SS
        double es = 0.6;

        // 第三个字是B的可能路径 sbb, bmb, meb, esb
        double esb = 0.6 * 0.6;
        // 第三个字是M的可能路径 sbm, bmm, mem, esm
        double sbm = 0.6 * 0.6;
        //double bmm = 0.6 * 0.4;
        // 第三个字是E的可能路径 sbe, bme, mee, ese
        //double sbe = 0.6 * 0.4;
        double bme = 0.6 * 0.6;
        // 第三个字是S的可能路径 sbs, bms, mes, ess
        double mes = 0.6 * 0.6;
        //double ess = 0.6 * 0.4;

        // 第四个字是B的可能路径 esbb, sbmb, bmeb, mesb
        //double bmeb = 0.6 * 0.6 * 0.4;
        double mesb = 0.6 * 0.6 * 0.6;
        // 第四个字是M的可能路径 esbm, sbmm, bmem, mesm
        double esbm = 0.6 * 0.6 * 0.6;
        //double sbmm = 0.6 * 0.6 * 0.4;
        // 第四个字是E的可能路径 esbe, sbme, bmee, mese
        //double esbe = 0.6 * 0.6 * 0.4;
        double sbme = 0.6 * 0.6 * 0.6;
        // 第四个字是S的可能路径 esbs, sbms, bmes, mess
        double bmes = 0.6 * 0.6 * 0.6;
        //double mess = 0.6 * 0.6 * 0.4;

        // 第四个字是B的可能路径 mesbb, esbmb, sbmeb, bmesb
        //double sbmeb = 0.6 * 0.6 * 0.6 * 0.4;
        double bmesb = 0.6 * 0.6 * 0.6 * 0.6;

        // 6.代码实现
        // 存储所有可能的路径,有四种
        List<Route> list = new ArrayList<Route>();

        // 一共六个字
        for (int i = 0; i < 6; i++) {
            // 第一个字,需要进行初始化
            if (list.size() == 0) {
                // 根据Viterbi算法,假设不存在两条完全一样的路径,那么一共有四条可能路径.
                for (int j = 0; j < initStatus.length; j++) {
                    Route route = new Route();
                    route.setPath(j + "");
                    route.setValue(initStatus[j] * emitProbMatrix[j][i]);
                    list.add(route);
                }
            } else {
                // 一共有多少种可能路径
                for (int j = 0; j < list.size(); j++) {
                    Route route = list.get(j);

                    String path = route.getPath();
                    // 获取上一个隐藏状态
                    int lastStatus = Integer.parseInt(path) % 10;

                    // 用于记录状态转移最大的概率
                    double maxPosibility = 0;
                    // 用于记录最有可能转移的状态
                    int hiddenStatus = 0;
                    // 一共四种隐藏状态
                    for (int k=0; k<4; k++) {
                        // 根据状态转移矩阵和混淆矩阵挑选最有可能的转移情况
                        if (transProbMatrix2[lastStatus][k] * emitProbMatrix[k][i] > maxPosibility) {
                            maxPosibility = transProbMatrix2[lastStatus][k] * emitProbMatrix[k][i];
                            hiddenStatus = k;
                        }
                    }
                    // 将最可能的隐藏状态添加到路径当中.
                    route.setPath(path + hiddenStatus);
                    // 将最可能的路径的概率保存下来,用于后面比较
                    route.setValue(route.getValue() * maxPosibility);
                }
            }
        }

        System.out.println("===================================================");
        for (int j = 0; j < list.size(); j++) {
            Route route = list.get(j);
            if (route.getValue() == 0) {
                continue;
            }
            String path = route.getPath();
            char[] chars = path.toCharArray();
            String allStatus = status[Integer.parseInt(chars[0] + "")] + status[Integer.parseInt(chars[1] + "")] +
                    status[Integer.parseInt(chars[2] + "")] +
                    status[Integer.parseInt(chars[3] + "")] +
                    status[Integer.parseInt(chars[4] + "")] +
                    status[Integer.parseInt(chars[5] + "")];
            System.out.println(allStatus + "\t" + route.getValue());

        }
    }

    /**
     * 路径类
     */
    public class Route {
        // 包含Viterbi算法完整的路径
        private String path;
        // 当前路径对应的概率
        private double value;

        public String getPath() {
            return path;
        }

        public void setPath(String path) {
            this.path = path;
        }

        public double getValue() {
            return value;
        }

        public void setValue(double value) {
            this.value = value;
        }
    }

总结

  • 对于6个字的句子,上一篇需要循环 4^6=4096 次,这篇只需要循环 64 + 64=48 次,减少的次数还是很可观的.
  • 对于句子分词的代码实现主要还是计算各种组合的最大概率.
  • 而对这个概率影响最大的因素一个是隐藏状态相互之间的转换概率,例 B 可以 转 M, E, 但不能转 B, S,这样就造成了不同的转换概率,最后的结果就是形成了 transProbMatrix2 这个矩阵.
  • 另一个因素就是发射概率矩阵,也称混淆矩阵, 说人话就是有一个骰子 B, 它有六个面, 分别是 {陈, 佩, 斯, 吃, 西, 瓜}, 然后对于这六个字骰中的概率都是 1/6.
  • 对于另一个骰子 E, 它有五个面,分别是 {佩, 斯, 吃, 西, 瓜}, 骰中的概率都是 1/5.
  • 根据这些骰子(隐藏状态)骰出的结果,可以建立一个隐藏状态对应可观察状态的概率矩阵,也就是代码中的发射概率矩阵 emitProbMatrix.
  • 其实初始状态矩阵也是一个因素,这三个矩阵的数值是通过语料训练获得的,就是找一大堆的句子,去统计每个字对应的隐藏状态的概率.
  • 代码有个问题,就是这边是默认不同路径的概率不会相同,所以可以用四条路径来囊括所有的最大概率路径组合,事实上可能有两条路径它的概率就是一样的,这样的话,路径组合还不止 48 种.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值