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

发布时间:2026/7/6 0:44:23
朴素贝叶斯分类器 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_countsTrue) 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_countsTrue) # 存储特征值的概率分布 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函数实现了两个关键计算类先验概率统计每个类别在训练集中的出现频率条件概率对每个特征统计在给定类别下各特征值的出现频率predict函数的工作流程对每个测试样本初始化后验概率为类先验概率乘以各特征的条件概率基于训练集统计选择使后验概率最大的类别作为预测结果3. 拉普拉斯平滑技术3.1 零概率问题当测试数据中出现训练集中未出现的特征值时基础版分类器会将该特征的条件概率设为0导致整个后验概率为0。例如# 训练数据中没有身高190cm的样本 test_sample [190, 70, 40] # 预测时会因为P(身高190|男)0而导致分类失败3.2 平滑实现方案拉普拉斯平滑通过在分子加1、分母加类别数来解决零概率问题class NaiveBayesClassifierSmooth: def __init__(self, alpha1): 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_countsTrue) 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_countsTrue) 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_patternr\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 性能优化技巧对数概率计算将概率相乘转换为对数相加防止数值下溢特征选择使用卡方检验等方法选择信息量大的特征并行计算对大数据集可并行化概率统计过程稀疏矩阵优化对文本数据使用稀疏矩阵存储# 使用对数概率的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 评估指标朴素贝叶斯分类器的常用评估指标包括准确率正确分类样本的比例精确率与召回率特别适用于类别不平衡的场景F1分数精确率和召回率的调和平均ROC-AUC衡量分类器排序能力的指标5.2 不同变体比较朴素贝叶斯有几种常见变体适用于不同场景类型假设分布适用场景高斯朴素贝叶斯正态分布连续特征多项式朴素贝叶斯多项式分布文本分类、计数数据伯努利朴素贝叶斯伯努利分布二值特征5.3 优缺点分析优势训练和预测速度快对小规模数据表现良好对无关特征相对鲁棒实现简单易于理解局限特征独立性假设在实际中往往不成立对输入数据的分布形式敏感需要足够的训练数据来估计概率在实际项目中我经常将朴素贝叶斯作为基线模型它的快速训练和预测能力能帮助快速验证特征的有效性。特别是在文本分类任务中即使有更复杂的模型可选朴素贝叶斯往往也能提供不错的性能。