• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
思想人生从关注生活开始
博客园    首页    新随笔    联系   管理    订阅  订阅

机器学习算法之朴素贝叶斯(Naive Bayes):从贝叶斯公式、手动计算到垃圾邮件过滤的Python/Java实现

一句话答案:朴素贝叶斯是一种“假设特征互相独立”的概率分类算法,虽“天真”却极高效。10行代码就能实现垃圾邮件识别,准确率超85%!

如果你在搜索:

  • “朴素贝叶斯怎么算的?”
  • “Naive Bayes 手动计算例子”
  • “Python 和 Java 怎么实现朴素贝叶斯?”
  • “为什么叫‘朴素’?它真的准吗?”

那么,这篇文章就是为你写的——从数学推导到代码落地,一步不跳。


一、什么是朴素贝叶斯?为什么“朴素”反而好用?

朴素贝叶斯(Naive Bayes)是基于贝叶斯定理的分类算法,核心思想是:

“已知结果,反推最可能的原因”

比如:

  • 收到一封邮件,内容有“免费”“赢大奖” → 判断是否为垃圾邮件
  • 用户评论含“烂透了”“差评” → 判断情感是负面

🤔 为什么叫“朴素”(Naive)?

因为它做了一个强假设:

所有特征彼此独立(例如:“免费”和“赢大奖”出现互不影响)

现实中这显然不成立(垃圾邮件常同时出现这两个词),但神奇的是——即使假设错误,分类效果依然很好!

💡 原因:我们只关心“哪个类别概率最大”,不要求概率值绝对准确。只要排序对,结果就对。


二、数学原理:贝叶斯定理 + 独立性假设

我们要计算:

image

根据贝叶斯定理:

image

由于 (P(X)) 对所有类别相同,可忽略。目标变为:

image

再利用“特征独立”假设:

image

最终决策公式:

image


三、手工推演:一步步计算垃圾邮件分类(带完整数据)

📊 训练数据集(4封邮件)

邮件ID内容(分词后)类别
1 ["免费", "赢", "大奖"] 垃圾邮件
2 ["免费", "课程"] 垃圾邮件
3 ["会议", "安排", "明天"] 正常邮件
4 ["项目", "进展", "顺利"] 正常邮件

假设使用伯努利模型(只关心词是否出现,不计频次)

🔢 步骤1:计算先验概率 (P(C))

  • 总邮件数 = 4
  • 垃圾邮件数 = 2 → P(垃圾) = 2/4 = 0.5
  • 正常邮件数 = 2 → P(正常) = 2/4 = 0.5

🔢 步骤2:构建词汇表(Vocabulary)

所有唯一词:["免费", "赢", "大奖", "课程", "会议", "安排", "明天", "项目", "进展", "顺利"] → 共10个词

🔢 步骤3:计算条件概率 (P词|类别)

使用拉普拉斯平滑(α=1),避免零概率:

image

分母+2:因为伯努利模型只有“出现/未出现”两种状态

垃圾邮件类(2封):

  • “免费”出现2次 → (P(免费|垃圾) = (2+1)/(2+2) = 3/4 = 0.75)
  • “赢”出现1次 → (P(赢|垃圾) = (1+1)/4 = 0.5)
  • “课程”出现1次 → (P(课程|垃圾) = 2/4 = 0.5)
  • “会议”未出现 → (P(会议|垃圾) = (0+1)/4 = 0.25)

正常邮件类(2封):

  • “会议”出现1次 → (P(会议|正常) = 2/4 = 0.5)
  • “免费”未出现 → (P(免费|正常) = 1/4 = 0.25)

其他词同理,略。

🔢 步骤4:预测新邮件 ["免费", "会议"]

计算两类后验概率(忽略公共分母):

垃圾邮件:

image

正常邮件:

image

✅ 结论:0.09375 > 0.0625 → 判定为 垃圾邮件

尽管“会议”通常是正常词,但“免费”的权重更高,模型做出了合理判断。


四、Python 实现(scikit-learn + 手写版)

✅ 方式1:使用 scikit-learn(推荐生产环境)

from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer

# 数据
texts = [
    "免费 赢 大奖",
    "免费 课程",
    "会议 安排 明天",
    "项目 进展 顺利"
]
labels = [1, 1, 0, 0]  # 1=垃圾, 0=正常

# 向量化(二值化)
vectorizer = CountVectorizer(binary=True)
X = vectorizer.fit_transform(texts)

# 训练
clf = BernoulliNB(alpha=1.0)  # alpha=1 即拉普拉斯平滑
clf.fit(X, labels)

# 预测
new_text = vectorizer.transform(["免费 会议"])
print("预测结果:", "垃圾邮件" if clf.predict(new_text)[0] == 1 else "正常邮件")
# 输出: 垃圾邮件

 

✅ 方式2:手写核心逻辑

import numpy as np

class NaiveBayes:
    def __init__(self):
        self.vocab = {}
        self.prior = {}
        self.cond_prob = {}
    
    def fit(self, docs, labels, alpha=1):
        # 构建词汇表
        words = set(w for doc in docs for w in doc)
        self.vocab = {w: i for i, w in enumerate(words)}
        n_vocab = len(self.vocab)
        
        # 统计类别
        unique_labels = list(set(labels))
        label_count = {l: labels.count(l) for l in unique_labels}
        total = len(labels)
        
        # 先验概率
        self.prior = {l: count / total for l, count in label_count.items()}
        
        # 条件概率(伯努利)
        self.cond_prob = {l: np.zeros(n_vocab) for l in unique_labels}
        for doc, label in zip(docs, labels):
            doc_vec = np.zeros(n_vocab)
            for w in doc:
                if w in self.vocab:
                    doc_vec[self.vocab[w]] = 1
            self.cond_prob[label] += doc_vec
        
        # 拉普拉斯平滑
        for label in unique_labels:
            n_docs = label_count[label]
            self.cond_prob[label] = (self.cond_prob[label] + alpha) / (n_docs + 2 * alpha)
    
    def predict(self, doc):
        scores = {}
        for label in self.prior:
            log_prob = np.log(self.prior[label])
            for w in doc:
                if w in self.vocab:
                    idx = self.vocab[w]
                    log_prob += np.log(self.cond_prob[label][idx])
            scores[label] = log_prob
        return max(scores, key=scores.get)

# 使用
docs = [["免费","赢","大奖"], ["免费","课程"], ["会议","安排","明天"], ["项目","进展","顺利"]]
nb = NaiveBayes()
nb.fit(docs, [1,1,0,0])
print(nb.predict(["免费", "会议"]))  # 输出: 1

 


五、Java 实现(纯手写,无第三方库)

import java.util.*;

public class NaiveBayes {
    private Map<String, Integer> vocab = new HashMap<>();
    private Map<Integer, Double> prior = new HashMap<>();
    private Map<Integer, double[]> condProb = new HashMap<>();
    private int vocabSize;

    public void fit(List<List<String>> docs, List<Integer> labels, double alpha) {
        // 构建词汇表
        Set<String> wordSet = new HashSet<>();
        for (List<String> doc : docs) {
            wordSet.addAll(doc);
        }
        int idx = 0;
        for (String word : wordSet) {
            vocab.put(word, idx++);
        }
        vocabSize = vocab.size();

        // 统计标签
        Map<Integer, Integer> labelCount = new HashMap<>();
        for (int label : labels) {
            labelCount.put(label, labelCount.getOrDefault(label, 0) + 1);
        }

        // 先验概率
        int totalDocs = labels.size();
        for (int label : labelCount.keySet()) {
            prior.put(label, (double) labelCount.get(label) / totalDocs);
        }

        // 条件概率(伯努利)
        for (int label : labelCount.keySet()) {
            double[] prob = new double[vocabSize];
            int docsWithLabel = labelCount.get(label);
            
            // 统计每个词在该类中出现次数
            for (int i = 0; i < docs.size(); i++) {
                if (labels.get(i) == label) {
                    Set<String> docWords = new HashSet<>(docs.get(i));
                    for (String word : docWords) {
                        if (vocab.containsKey(word)) {
                            int wIdx = vocab.get(word);
                            prob[wIdx] += 1.0;
                        }
                    }
                }
            }
            
            // 拉普拉斯平滑
            for (int i = 0; i < vocabSize; i++) {
                prob[i] = (prob[i] + alpha) / (docsWithLabel + 2 * alpha);
            }
            condProb.put(label, prob);
        }
    }

    public int predict(List<String> doc) {
        Map<Integer, Double> scores = new HashMap<>();
        for (int label : prior.keySet()) {
            double logProb = Math.log(prior.get(label));
            Set<String> docWords = new HashSet<>(doc);
            for (String word : docWords) {
                if (vocab.containsKey(word)) {
                    int wIdx = vocab.get(word);
                    logProb += Math.log(condProb.get(label)[wIdx]);
                }
            }
            scores.put(label, logProb);
        }

        // 返回概率最大的标签
        return Collections.max(scores.entrySet(), Map.Entry.comparingByValue()).getKey();
    }

    // 测试
    public static void main(String[] args) {
        List<List<String>> docs = Arrays.asList(
            Arrays.asList("免费", "赢", "大奖"),
            Arrays.asList("免费", "课程"),
            Arrays.asList("会议", "安排", "明天"),
            Arrays.asList("项目", "进展", "顺利")
        );
        List<Integer> labels = Arrays.asList(1, 1, 0, 0);

        NaiveBayes nb = new NaiveBayes();
        nb.fit(docs, labels, 1.0); // alpha=1

        List<String> testDoc = Arrays.asList("免费", "会议");
        System.out.println("预测结果: " + (nb.predict(testDoc) == 1 ? "垃圾邮件" : "正常邮件"));
        // 输出: 垃圾邮件
    }
}

 


六、优缺点 & 适用场景总结

优点缺点
✅ 训练快,预测快(O(n)) ❌ 特征独立假设太强
✅ 小样本表现好 ❌ 无法建模特征交互(如“not good”)
✅ 对缺失值鲁棒 ❌ 概率输出可能不准
✅ 内存占用小,适合嵌入式 ❌ 连续特征需假设分布(如高斯)

🎯 最佳应用场景:

  • 文本分类(新闻、邮件、评论)
  • 垃圾信息检测
  • 情感分析(正面/负面)
  • 实时分类系统(如API过滤)

七、后续算法预告(均含手工推演 + 双语言代码)

本系列将持续更新以下算法,每篇均包含:

  • 真实数据手工一步步计算
  • Python + Java 完整可运行代码

即将发布:

  1. K近邻(KNN):从距离计算到手写数字识别
  2. 决策树(ID3/C4.5):信息增益如何分裂节点?
  3. 支持向量机(SVM):硬间隔、软间隔、核函数全解析
  4. 逻辑回归:从sigmoid到梯度下降

✅ 结语

朴素贝叶斯用最天真的假设,实现了最实用的分类。它不追求完美,只求快速、稳健、可解释。

记住:在AI世界,有时“简单有效”比“复杂精确”更重要。

现在,你已经能:

  • 手动计算朴素贝叶斯分类结果
  • 用Python或Java从零实现它
  • 理解为何它仍是工业界首选之一
posted @ 2026-03-29 12:16  JackYang  阅读(13)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3