NLP学习笔记06-维特比算法

本文介绍了NLP中的维特比算法,用于解决概率统计分词问题。通过将概率转换为log避免溢出,并利用动态规划求解最短路径。通过实例展示了如何基于词频数据构建DAG并找到最大概率的分词组合。代码示例使用Java实现,演示了分词过程。
摘要由CSDN通过智能技术生成

一序

  本文属于NLP学习笔记系列。

上一篇整理了前向最大匹配算法与所有组合算法缺点(时间复杂度太高了)。

二 维特比算法

log(x*y*z)= log(x)+log(y)+log(z)

概率上为了避免小数练乘出现的超范围溢出,改用log,改用-log,使得原来求概率最大的小数为-log结果最小。

画一条线,把各种结果对应路径标注上(定义f8 :从节点1到8 的最短路径的值),这里求总结果最小,演变成求最短路径。

如果不使用哪个DP算法,那么就是递归的过程:举例:f(8)=f(5)+3, f(8)=f(6)+1.6,f(8)= f(7)+20 . 要求f(8)需要知道前面的节点的数据。

所以,维护一个数组,从f(1)开始导f(8),分别求出每个节点的值,也就知道f(8)的最小值(记录下所选最小值节点),也知道了你选择的路径是哪条。

结果:经常、有意见、分歧。

词典的概率数据怎么有的,老师没有讲,先理解为统计出来的吧。

这个内容老师讲的很仔细,一步步讲解推导过程,有的文章:比如达观这篇(只说结果,推导过程需要自己理解)

https://www.jiqizhixin.com/articles/2019-09-29-5

0 (6)

对于到最终节点E的路径Route(E),有:

0 (7)              

这节课老师只是简单介绍下维特比算法。后面的HMM 会继续介绍。比这里复杂多了。

分词总结

  知识点总结:

  •   基于匹配规则:前向最大匹配算法
  •  基于概率统计方法: (我们学习了语言模型LM,还有HMM,CRF)
  • 分词可以认为已经解决的问题。

需要掌握:

前向最大匹配算法+ Unigram LM 方法。

  下面是从jieba分词 改了下代码,作为demo。







import com.hankcs.hanlp.collection.AhoCorasick.AhoCorasickDoubleArrayTrie;

import java.util.*;

/**
 * bohu83
 * jieba分词
 */
public class TestSpilit {


    static private AhoCorasickDoubleArrayTrie<Integer> acTrie;
    static TreeMap<String, Integer> freqMap = new TreeMap<>();
    static TreeMap<String, Integer> acTrieMap = new TreeMap<>();
    static private List<String> words = new ArrayList<>();
    static private List<Integer> wordFreqs = new ArrayList<>();

    static private Map<Pair<Integer, Integer>, Integer> subStringFreq = new HashMap<>();
    static {
        freqMap.put("经常", 10);
        freqMap.put("经",5);
        freqMap.put("常",1);
        freqMap.put("有",10);
        freqMap.put("有意见",10);
        freqMap.put("歧",1);
        freqMap.put("意见",20);
        freqMap.put("分歧",20);
        freqMap.put("见",5);
        freqMap.put("意",5);
        freqMap.put("见分歧",5);
        freqMap.put("分",10);
        freqMap.forEach((w, f) -> {
            acTrieMap.put(w, words.size());
            words.add(w);
            wordFreqs.add(f);
        });
        acTrie = new AhoCorasickDoubleArrayTrie<>();
        acTrie.build(acTrieMap);
    }

    /**
     * 基于trie查询前缀,生成DAG
     * @param sentence
     * @return
     */
    static HashMap<Integer, List<Integer>> makeDAG(String sentence) {
        int len = sentence.length();
        HashMap<Integer, List<Integer>> dag = new HashMap<>();
        acTrie.parseText(sentence)
                .forEach(hit -> dag.computeIfAbsent(hit.begin, k -> new ArrayList<>()).add(hit.end - hit.begin));
        for (int k = 0; k < len; k++) {
            if (!dag.containsKey(k)) {
                List<Integer> lis = new ArrayList<>();
                lis.add(1);
                dag.put(k, lis);
            }
        }
        return dag;
    }


    /**
     * 采用动态规划查找最大概率路径, 找出基于词频的最大切分组合
     */
    private static Map<Integer, Pair<Integer, Double>> calcMaxProbPath(String sentence, Map<Integer, List<Integer>> dag) {
        int len = sentence.length();
        Map<Integer, Pair<Integer, Double>> route = new HashMap<>();
        route.put(len, Pair.of(0, 0d));
        double logTotal = 100;
        for (int k = len - 1; k >= 0; k--) {
            int offset = 0;
            double maxProb = -Double.MAX_VALUE;
            for (Integer x : dag.get(k)) {
                int end = k + x;
                //查位置
                int idx = acTrie.exactMatchSearch((sentence.substring(k, end)));
                //查词频
                int freq =  idx < 0 ? 0: wordFreqs.get(idx);
                subStringFreq.put(Pair.of(k, end), freq);
                double prob = Math.log(freq > 0 ? freq : 1) - logTotal + route.get(end).getValue();
                if (maxProb < prob) {
                    maxProb = prob;
                    offset = end - 1;
                }
            }
            route.put(k, Pair.of(offset, maxProb));
        }
        return route;
    }


    public static void main(String[] args){
        List<String> result = new ArrayList<>();
        String sentence ="我们经常有意见分歧";
        HashMap<Integer, List<Integer>>  dag =  TestSpilit.makeDAG(sentence);
        System.out.println(dag);
        Map<Integer, Pair<Integer, Double>> route = TestSpilit.calcMaxProbPath(sentence,dag);
        int st = 0;
        int ed;
        StringBuilder sb = new StringBuilder();
        while (st < sentence.length()) {
            ed = route.get(st).getKey() + 1;
            if (ed - st == 1 && Character.isLetterOrDigit(sentence.charAt(st))) {
                sb.append(sentence.charAt(st));
            } else {
                if (sb.length() > 0) {
                    result.add(sb.toString());
                    sb.setLength(0);
                }
                result.add(sentence.substring(st, ed));
            }
            st = ed;
        }
        if (sb.length() > 0) {
            result.add(sb.toString());
        }
       System.out.println(result);

    }
}

输出:

{0=[1], 1=[1], 2=[1, 2], 3=[1], 4=[1, 3], 5=[1, 2], 6=[1, 3], 7=[1, 2], 8=[1]}
[我们, 经常, 有意见, 分歧]

这里有些词典数据初始化是写死的,没有真正的加载txt文件。但是关键的步骤我都摘取出来。

1 创建DAG:生成句子中汉字所有可能成词情况所构成的有向无环图

jieba是基于 trie 树结构实现高效词图扫描

2 采用动态规划算法计算最佳切词组合

这个可以结合上面的手推图来理解。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值