朴素贝叶斯分类器 Python 实现:从零手写 2 个核心函数与拉普拉斯平滑

📅 2026/7/6 1:02:21 👁️ 阅读次数 📝 编程学习
朴素贝叶斯分类器 Python 实现:从零手写 2 个核心函数与拉普拉斯平滑

从零实现朴素贝叶斯分类器:核心函数与平滑技术实战

1. 朴素贝叶斯算法原理精要

朴素贝叶斯分类器是基于贝叶斯定理与特征条件独立假设的分类方法。其核心思想是通过先验概率和条件概率来计算后验概率,从而实现对样本的分类决策。

让我们先看一个简单的例子:假设我们要判断一封邮件是否为垃圾邮件。已知:

  • 垃圾邮件中出现"免费"一词的概率是80%
  • 正常邮件中出现"免费"一词的概率是10%
  • 整体邮件中垃圾邮件的占比是20%

当新邮件包含"免费"时,我们可以计算:

P(垃圾|免费) = P(免费|垃圾) * P(垃圾) / P(免费) = 0.8 * 0.2 / (0.8*0.2 + 0.1*0.8) = 0.16 / 0.24 ≈ 0.667

这个计算过程体现了贝叶斯定理的核心思想:利用已知信息更新概率估计。

关键数学公式

朴素贝叶斯的分类决策基于以下公式:

P(y|x₁,x₂,...,xₙ) ∝ P(y) * ∏ P(xᵢ|y)

其中:

  • P(y)是类先验概率
  • P(xᵢ|y)是特征条件概率
  • ∏表示连乘(基于特征独立假设)

2. 核心函数实现

2.1 数据准备

我们先定义一个简单的性别分类数据集:

import numpy as np # 特征:身高(cm), 体重(kg), 脚长(cm) X = np.array([ [180, 75, 42], # 男 [175, 70, 41], # 男 [170, 65, 38], # 女 [165, 55, 36], # 女 [185, 80, 43], # 男 [168, 60, 37] # 女 ]) y = np.array(['男', '男', '女', '女', '男', '女'])

2.2 基础版分类器实现

class NaiveBayesClassifier: def __init__(self): self.label_prob = {} # 类别先验概率 self.condition_prob = {} # 条件概率 def fit(self, X, y): """训练模型,计算先验概率和条件概率""" n_samples = len(X) n_features = X.shape[1] # 计算类先验概率 unique_labels, counts = np.unique(y, return_counts=True) self.label_prob = dict(zip(unique_labels, counts / n_samples)) # 计算条件概率 for label in unique_labels: # 获取当前类别的样本 X_label = X[y == label] # 初始化当前类别的条件概率结构 self.condition_prob[label] = {} # 对每个特征计算条件概率 for i in range(n_features): feature_values = X_label[:, i] unique_values, value_counts = np.unique(feature_values, return_counts=True) # 存储特征值的概率分布 self.condition_prob[label][i] = dict(zip( unique_values, value_counts / len(feature_values) )) def predict(self, X): """预测新样本的类别""" predictions = [] for sample in X: max_prob = -1 best_label = None # 对每个类别计算后验概率 for label in self.label_prob: # 初始化为类先验概率 prob = self.label_prob[label] # 乘以各个特征的条件概率 for i, value in enumerate(sample): if value in self.condition_prob[label][i]: prob *= self.condition_prob[label][i][value] else: # 遇到未见过的特征值,概率设为0 prob = 0 break # 选择概率最大的类别 if prob > max_prob: max_prob = prob best_label = label predictions.append(best_label) return np.array(predictions)

2.3 核心函数解析

fit函数实现了两个关键计算:

  1. 类先验概率:统计每个类别在训练集中的出现频率
  2. 条件概率:对每个特征,统计在给定类别下各特征值的出现频率

predict函数的工作流程:

  1. 对每个测试样本,初始化后验概率为类先验概率
  2. 乘以各特征的条件概率(基于训练集统计)
  3. 选择使后验概率最大的类别作为预测结果

3. 拉普拉斯平滑技术

3.1 零概率问题

当测试数据中出现训练集中未出现的特征值时,基础版分类器会将该特征的条件概率设为0,导致整个后验概率为0。例如:

# 训练数据中没有身高190cm的样本 test_sample = [190, 70, 40] # 预测时会因为P(身高=190|男)=0而导致分类失败

3.2 平滑实现方案

拉普拉斯平滑通过在分子加1、分母加类别数来解决零概率问题:

class NaiveBayesClassifierSmooth: def __init__(self, alpha=1): self.alpha = alpha # 平滑系数 self.label_prob = {} self.condition_prob = {} def fit(self, X, y): n_samples = len(X) n_features = X.shape[1] # 计算平滑后的类先验概率 unique_labels, counts = np.unique(y, return_counts=True) total_labels = len(unique_labels) self.label_prob = { label: (count + self.alpha) / (n_samples + total_labels * self.alpha) for label, count in zip(unique_labels, counts) } # 计算平滑后的条件概率 for label in unique_labels: X_label = X[y == label] self.condition_prob[label] = {} for i in range(n_features): feature_values = X_label[:, i] unique_values, value_counts = np.unique(feature_values, return_counts=True) n_values = len(unique_values) # 应用拉普拉斯平滑 self.condition_prob[label][i] = { value: (count + self.alpha) / (len(X_label) + n_values * self.alpha) for value, count in zip(unique_values, value_counts) } # 添加一个"未知"项来处理未见过的特征值 self.condition_prob[label][i]["unknown"] = self.alpha / (len(X_label) + n_values * self.alpha) def predict(self, X): predictions = [] for sample in X: max_prob = -1 best_label = None for label in self.label_prob: prob = np.log(self.label_prob[label]) # 使用对数防止下溢 for i, value in enumerate(sample): # 如果特征值未见过,使用"unknown"概率 prob_dict = self.condition_prob[label][i] if value in prob_dict: prob += np.log(prob_dict[value]) else: prob += np.log(prob_dict["unknown"]) if prob > max_prob: max_prob = prob best_label = label predictions.append(best_label) return np.array(predictions)

3.3 平滑效果对比

我们通过一个对比表格展示平滑前后的差异:

情况基础版平滑版
处理未见特征值概率为0使用平滑概率
数值稳定性可能下溢使用对数更稳定
极端情况可能误判更鲁棒
计算复杂度略低略高

4. 实际应用与性能优化

4.1 文本分类示例

朴素贝叶斯在文本分类中表现优异。以下是一个简单的垃圾邮件分类实现:

from sklearn.feature_extraction.text import CountVectorizer # 示例数据 texts = [ "免费 赢取 百万大奖", # 垃圾邮件 "明天 开会 通知", # 正常邮件 "优惠 折扣 限时", # 垃圾邮件 "项目 进度 报告" # 正常邮件 ] labels = ["spam", "ham", "spam", "ham"] # 文本向量化 vectorizer = CountVectorizer(token_pattern=r'\b\w+\b') X = vectorizer.fit_transform(texts) # 训练分类器 nb = NaiveBayesClassifierSmooth() nb.fit(X.toarray(), labels) # 测试新样本 test_text = "免费 会议 通知" test_vec = vectorizer.transform([test_text]) print(nb.predict(test_vec.toarray())) # 输出预测类别

4.2 性能优化技巧

  1. 对数概率计算:将概率相乘转换为对数相加,防止数值下溢
  2. 特征选择:使用卡方检验等方法选择信息量大的特征
  3. 并行计算:对大数据集可并行化概率统计过程
  4. 稀疏矩阵优化:对文本数据使用稀疏矩阵存储
# 使用对数概率的predict实现示例 def predict_log_prob(self, X): predictions = [] for sample in X: max_log_prob = -np.inf best_label = None for label in self.label_prob: log_prob = np.log(self.label_prob[label]) for i, value in enumerate(sample): if value in self.condition_prob[label][i]: log_prob += np.log(self.condition_prob[label][i][value]) else: log_prob += np.log(self.alpha) - np.log(len(self.condition_prob[label][i]) * self.alpha + np.sum(list(self.condition_prob[label][i].values()))) if log_prob > max_log_prob: max_log_prob = log_prob best_label = label predictions.append(best_label) return np.array(predictions)

5. 算法评估与比较

5.1 评估指标

朴素贝叶斯分类器的常用评估指标包括:

  1. 准确率:正确分类样本的比例
  2. 精确率与召回率:特别适用于类别不平衡的场景
  3. F1分数:精确率和召回率的调和平均
  4. ROC-AUC:衡量分类器排序能力的指标

5.2 不同变体比较

朴素贝叶斯有几种常见变体,适用于不同场景:

类型假设分布适用场景
高斯朴素贝叶斯正态分布连续特征
多项式朴素贝叶斯多项式分布文本分类、计数数据
伯努利朴素贝叶斯伯努利分布二值特征

5.3 优缺点分析

优势

  • 训练和预测速度快
  • 对小规模数据表现良好
  • 对无关特征相对鲁棒
  • 实现简单,易于理解

局限

  • 特征独立性假设在实际中往往不成立
  • 对输入数据的分布形式敏感
  • 需要足够的训练数据来估计概率

在实际项目中,我经常将朴素贝叶斯作为基线模型,它的快速训练和预测能力能帮助快速验证特征的有效性。特别是在文本分类任务中,即使有更复杂的模型可选,朴素贝叶斯往往也能提供不错的性能。