我们经常有这样的需求:用户发送的消息是不是垃圾信息,某张图片是不是猫、狗等等。对于文本我们可以通过字符串匹配4:AC自动机 敏感词判断,使用杂:位图与布隆过滤器 布隆过滤器来检查。但是对于图片,我们通常需要训练一个分类模型来检测该图片是否是敏感图片。

那么,我们能否训练一个模型来判断文本是不是是垃圾信息,或者某张图片是不是敏感图片呢?

朴素贝叶斯算法

此类问题最原始的方法是使用朴素贝叶斯算法。朴素贝叶斯是一种基于贝叶斯定理概率分类算法

贝叶斯定理

我们有事件A和事件B。假设事件A(类别)是内容是垃圾短信这个事件。而事件B(特征向量)是包含免费、中奖等内容的短信

P(A) 表示在常规情况下A事件的发生概率(先验概率),比如一般情况下一条短信是垃圾短信的概率,我们一般统计垃圾短信占所有垃圾短信比重作视为该事件的概率。

P(B)表示B事件的发生概率(证据概率或边际概率或者归一化常数)。

P(B|A)表示发生事件A时,发生事件B的概率(似然概率条件概率)。比如,垃圾短信中出现包含免费中奖等内容的概率,一样的,我们一般统计包含这些内容的短信占垃圾短信的比重视为该事件的概率。

我们的目标是判断一条短信是否是垃圾短信。但是垃圾短信是一个抽象的概念,所以我们需要通过特征来判断一条短信是不是垃圾短信。在这里就是想知道通过内容是否包含免费中奖等内容的时候来判断一条短信是否是垃圾短信,也就是想知道P(A|B)后验概率),即事件B发生时,事件A发生的概率。

此时我们就需要使用贝叶斯定理来计算得到了:

P(A|B) = (P(B|A) * P(A)) / P(B) 

其中P(B|A)P(A)都能通过统计得到。

那么我们再扩展以下事件B。比如之前只是免费中奖等内容,现在我们可以增加限时、小妹之类的特征。即,包含免费、中奖、限时、小妹的短信。那么P(B)就是同时发生这些事件的概率。我们假设这些事件的发生时独立的P(B)不能通过统计得到,需要使用全概率公式计算,那么:

P(B) = P(B|A) * P(A) + P(B|非A) × P(非A)

又因为:

P(B|A) = P((免费、中奖、限时、小妹)|A) = P(免费|A) × P(中奖|A) × P(限时|A) × P(小妹|A) 

那么:

P(B) = P(免费、中奖、限时、小妹) = P((免费、中奖、限时、小妹)|A) = P(免费|A) × P(中奖|A) × P(限时|A) × P(小妹|A) × P(A) + P(免费|非A) × P(中奖|非A) × P(限时|非A) × P(小妹|非A) × P(非A) 

那么我们可以通过统计每个特征的概率在事件A发生的情况下每个特征的概率P(X|A) 和 在事件A没有发生的情况下的概率P(X|非A),结合事件A发生的概率P(A)通过计算得到在某些特征事件发生的情况下,事件A发生的概率P(A|B)。其中事件B是一个有多个特征事件组成的联合事件。

朴素?

“朴素”的核心假设是特征条件独立性,也就是免费、中奖、限时、小妹这些内容是没有关联,相互独立的。但是现实中他们常常是有关联的,比如限时免费活动等等这些短语通常会放到一起。

正是因为这个假设,使得我们不用统计每个特征组合的的联合概率,大大简化了计算,我们只需要统计每个特征单独的概率通过运算就能得到联合概率。

实现

朴素贝叶斯的原理非常简单。我们可以在[[字符串匹配4:AC自动机]] 的基础上为每一个关键字加上统计概率字段(P(X|¬A)和P(X|A))。得到每个敏感词的似然概率和边际概率之后,结合先验概率通过公式得到该输入是否敏感的概率:

P(A|B) = ((P(X1|A) × P(X2|A) × ... × P(Xi|A)) × P(A)) / (((P(X1|A) × P(X2|A) × ... × P(Xi|A)) × P(A)) + ((P(X1|¬A) × P(X2|¬A) × ... × P(Xi|¬A)) × P(¬A)) )

实现如下:

package cn.lishiyuan.algorithm.strings;  
  
import java.util.*;  
import java.util.function.Function;  
  
public class NaiveBayes {  
    // 分词  
    private final Function<String, List<String>> split;  
    // 合并词  
    private final Function<List<String>,String> merge;  
  
    private final Node root;  
  
    private static class Node{  
        String word;  
  
        // 全局概率  
        double probabilityNotInA;  
        // A发生时的概率  
        double probabilityInA;  
  
        // 子节点  
        Map<String, Node> children;  
  
        // 是否是最后一个字母或者单词  
        boolean isEndWord = false;  
  
        int length = 0;  
  
        Node fail;  
  
        Node(String word){  
            this.word = word;  
            this.children = new HashMap<>();  
            fail = null;  
        }  
    }  
  
    public NaiveBayes(Function<String,List<String>> split, Function<List<String>,String> merge){  
        this.split = split;  
        this.merge = merge;  
        this.root = new Node(null);  
    }  
  
    public void insert(String words,double probabilityNotInA,double probabilityInA){  
        if (words == null || words.trim().isEmpty()){  
            return;  
        }  
  
        // 分词,构建树节点  
        List<String> wordsList = split.apply(words);  
        // 匹配字典数据  
        Node parent = root;  
  
        for (int i = 0; i < wordsList.size(); i++) {  
  
            String word = wordsList.get(i);  
            if (parent.children.containsKey(word)){  
                // 存在相同节点则继续向后查找  
                parent = parent.children.get(word);  
            }else {  
                Node node = new Node(word);  
                node.isEndWord = (i == (wordsList.size() - 1));  
                if(node.isEndWord){  
                    // 分词后的长度  
                    node.probabilityNotInA = probabilityNotInA;  
                    node.probabilityInA = probabilityInA;  
                    node.length = wordsList.size();  
                }  
                parent.children.put(word,node);  
                parent = node;  
            }  
        }  
  
    }  
  
    // 全部敏感词插入完成后  
    // 手动构建fail指针  
    public void buildFailure() {  
        // 通过广度优先遍历构建fail指针  
        Queue<Node> queue = new LinkedList<>();  
        root.fail = null;  
        for (Node child : root.children.values()) {  
            // root的子节点的fail指针是root  
            child.fail = root;  
            queue.add(child);  
        }  
        while (!queue.isEmpty()) {  
            Node current = queue.poll();  
            for (Map.Entry<String, Node> entry : current.children.entrySet()) {  
                Node child = entry.getValue();  
                // 父节点的失败指针  
                Node fail = current.fail;  
                while (fail != null && !fail.children.containsKey(entry.getKey())) {  
                    // 失败指针的失败指针  
                    fail = fail.fail;  
                }  
                // 如果没找到就指向root  
                child.fail = (fail != null) ? fail.children.get(entry.getKey()) : root;  
                queue.add(child);  
            }  
        }  
    }  
  
    public List<Event> find(String text){  
        if (text == null || text.trim().isEmpty()){  
            return Collections.emptyList();  
        }  
  
        // 如果不最后一个词(也就是中途break了),则返回空列表  
        List<Event> result = new ArrayList<>();  
  
        // 分词,构建树节点  
        List<String> wordsList = split.apply(text);  
        // 匹配字典数据  
        Node parent = root;  
  
        for (int startIndex = 0; startIndex < wordsList.size(); startIndex++) {  
  
            String key = wordsList.get(startIndex);  
            // index 只有一个方向  
            // 字典如果没有查找到就从下个字重新开始查找  
            if (parent.children.containsKey(key)) {  
                parent = parent.children.get(key);  
                // 查找下一个字符  
                extractWord(parent, wordsList,result, startIndex);  
            }else {  
                Node fail = parent.fail;  
                // 如果不为null且fail子节点不包含下个词,则还需要再从缩小查找范围  
                while (fail!=null && !fail.children.containsKey(key)){  
                    fail = fail.fail;  
                }  
                if (fail == null){  
                    // 不是敏感词  
                    parent = root;  
                }else {  
                    // 查到了,继续查找  
                    parent = fail.children.get(key);  
                    extractWord(parent, wordsList,result, startIndex);  
                }  
  
            }  
  
        }  
  
        return result.stream().distinct().toList();  
    }  
  
    public double probability(String text,Double probabilityA){  
        List<Event> events = find(text);  
        if (events == null || events.isEmpty()){  
            return 0;  
        }  
        // 计算概率  
        double PBA = 1;  
        double PBNA = 1;  
        for (Event event : events) {  
            PBA *= event.probabilityInA();  
            PBNA *= event.probabilityNotInA();  
        }  
  
        double PB = (PBNA * (1-probabilityA)) +(PBA * probabilityA);  
  
        return (PBA * probabilityA) / PB;  
    }  
  
    private void extractWord(Node parent, List<String> wordsList, List<Event> result, int startIndex) {  
        Node current = parent;  
        while (current != root) {  
            if (current.isEndWord) {  
                // 记录单词  
                List<String> list = wordsList.subList(startIndex - current.length + 1, startIndex + 1);  
                String word = merge.apply(list);  
                Event event = new Event(word, current.probabilityNotInA, current.probabilityInA);  
                result.add(event);  
            }  
            current = current.fail;  
        }  
    }  
  
  
    public static record Event(String word,double probabilityNotInA,double probabilityInA){  
  
        @Override  
        public boolean equals(Object obj) {  
            if (obj instanceof Event event){  
                return word.equals(event.word);  
            }  
            return false;  
        }  
    }  
  
}

当需要严格风控的时候,我们把阈值调底,比如0.5便认为包含敏感信息。当风控需要宽松的时候就调高阈值。比如0.8的时候才认为是垃圾短信。

朴素贝叶斯算法在文本分类问题上表现还不错,我们需要根据不同的特征分布来调整我们的计算模型。

高斯朴素贝叶斯:

  • 假设连续特征服从高斯分布(正态分布)。
  • 适用于特征是连续数值的情况(如身高、体重、温度)。

    多项式朴素贝叶斯:

  • 假设离散特征代表计数或频率(例如,文本分类中单词出现的次数)。
  • 计算 P(Xi|A) 时,使用的是特征 Xi 在类别 A 的所有样本中出现的总频次。
  • 非常适用于文本分类问题(垃圾邮件识别、情感分析、主题分类)。

伯努利朴素贝叶斯:

  • 假设特征都是二元的(0 或 1,表示存在或不存在)。
  • 计算 P(Xi|A) 时,关注的是特征 Xi 存在(值为 1)的概率。即使一个特征在样本中出现多次,也只算一次。
  • 也常用于文本分类,特别是当只关心某个词是否在文档中出现(而不关心出现次数)时。

同样的原理,在我们识别敏感图的时候,也可以对每个像素进行评估,出现像素X的概率与发生事件A的概率关系。
但是图像识别的效果注定是很差的。文本本身的词语文字是由特定含义的,而像素本身没有结构信息,像素的排列组成表达了图像的含义,这也就意味着图像本身就是因为像素之间向关联而有意义。这与朴素贝叶斯的基本假设是相悖的:每个像素相互独立。

神经网络

就目前来说,不论是文本还是图片的分类领域。神经网络已经全面超越了简单的朴素贝叶斯算法的分类。但是最原始的思想内核依然影响了现代神经网络的方方面面。

概率与统计的渗透

  • 贝叶斯决策理论 → Softmax输出层(类别概率)
  • 高斯分布假设 → 损失函数设计(如均方误差)
  • 朴素贝叶斯的“思想继承”:特征自动组合替代了人工独立性假设。

标签: 算法基础

评论已关闭