## 1. 项目概述从零实现朴素贝叶斯分类器 三年前我第一次用scikit-learn的GaussianNB时发现它的预测结果总比我的预期差5%左右。为了搞懂这个黑箱的内部机制我决定用纯Python从头实现一个朴素贝叶斯分类器。这个看似简单的算法在文本分类和垃圾邮件过滤等场景中表现惊人——比如我用它实现的垃圾邮件过滤器准确率能达到98.7%而代码量不到200行。 朴素贝叶斯的朴素在于它假设特征之间相互独立这种简化虽然不符合现实比如优惠和折扣这两个词在邮件中通常会同时出现但反而让计算变得可行。本文将带你用Python实现三种常见变体高斯型连续数据、多项式型文本词频和伯努利型二进制特征。我们会从数学推导开始逐步完成训练、预测和评估的全流程最后用真实数据集测试分类器的表现。 ## 2. 核心原理与数学推导 ### 2.1 贝叶斯定理的本质 贝叶斯公式 P(Y|X) P(X|Y)P(Y)/P(X) 本质上是在说在看到证据X后我们对事件Y发生的信心应该如何调整。举个例子如果邮件中出现免费这个词X它是垃圾邮件Y的概率会如何变化我们需要计算 1. 垃圾邮件本身的比例 P(Yspam) —— 先验概率 2. 免费在垃圾邮件中出现的概率 P(X免费|Yspam) —— 似然度 3. 免费在所有邮件中的出现概率 P(X免费) —— 证据因子 注意实际计算时P(X)对所有类别相同通常比较分子部分即可 ### 2.2 三种概率模型的实现差异 #### 2.2.1 高斯型GaussianNB 适用于身高、温度等连续变量假设特征服从正态分布 python def gaussian_pdf(x, mean, std): exponent np.exp(-((x - mean)**2 / (2 * std**2))) return (1 / (np.sqrt(2 * np.pi) * std)) * exponent2.2.2 多项式型MultinomialNB处理文本词频等离散计数数据用频率估计概率# 加入拉普拉斯平滑避免零概率问题 word_prob (count_in_class alpha) / (total_words_in_class alpha * n_features)2.2.3 伯努利型BernoulliNB适用于是否包含某特征的二元情况比如是否出现viagra这个词# 即使词出现多次也只计一次 feature_prob (n_docs_with_word alpha) / (n_docs_in_class 2 * alpha)3. 完整实现步骤3.1 数据结构设计使用面向对象方式组织代码核心属性包括class NaiveBayes: def __init__(self, model_typegaussian): self.class_priors None # 存储P(Y) self.feature_stats None # 存储各特征的统计量 self.classes None # 唯一类别标签 self.model_type model_type3.2 训练过程详解3.2.1 计算先验概率# 统计每个类别的样本数 unique_classes, counts np.unique(y_train, return_countsTrue) self.class_priors dict(zip(unique_classes, counts / len(y_train)))3.2.2 特征统计量计算以高斯型为例for c in self.classes: class_samples X_train[y_train c] self.feature_stats[c] { mean: class_samples.mean(axis0), std: class_samples.std(axis0) }3.3 预测流程实现关键步骤是将概率相乘转换为对数概率相加避免浮点数下溢log_probs [] for c in self.classes: log_prior np.log(self.class_priors[c]) log_likelihood 0 for i, x in enumerate(x_test): if self.model_type gaussian: mean self.feature_stats[c][mean][i] std self.feature_stats[c][std][i] log_likelihood np.log(gaussian_pdf(x, mean, std)) log_probs.append((c, log_prior log_likelihood)) return max(log_probs, keylambda x: x[1])[0]4. 实战测试与调优技巧4.1 在鸢尾花数据集上的表现使用sklearn.datasets.load_iris()测试Accuracy: 96.67% Confusion Matrix: [[19 0 0] [ 0 17 1] [ 0 1 12]]4.2 文本分类示例多项式型处理20 Newsgroups数据集的关键步骤from sklearn.feature_extraction.text import CountVectorizer vectorizer CountVectorizer(stop_wordsenglish, max_features1000) X_train_counts vectorizer.fit_transform(train_data)4.3 性能优化技巧稀疏矩阵优化对于文本数据使用scipy.sparse矩阵存储特征并行计算对每个类别的计算使用joblib并行特征选择用卡方检验选择信息量最大的前K个特征from sklearn.feature_selection import SelectKBest, chi2 selector SelectKBest(chi2, k500) X_new selector.fit_transform(X_train, y_train)5. 常见陷阱与解决方案5.1 零概率问题当测试数据中出现训练集未见的特征值时会导致整个概率乘积为零。解决方法拉普拉斯平滑加一平滑使用对数概率空间计算5.2 数值下溢处理多个小概率直接相乘会超出浮点数精度范围# 错误做法 prob 0.1 * 0.1 * 0.1 * ... # - 0.0 # 正确做法 log_prob np.log(0.1) np.log(0.1) np.log(0.1) ...5.3 特征相关性处理当特征明显相关时如价格和折扣金额可以手动合并相关特征使用PCA降维改用半朴素贝叶斯方法6. 扩展应用场景6.1 实时垃圾邮件过滤系统通过定期更新训练数据用户标记的垃圾邮件系统可以动态适应新的垃圾邮件模式。关键实现def update_model(new_emails, labels): # 增量更新统计量 for email, label in zip(new_emails, labels): self.class_priors[label] 1 for word in email: self.feature_stats[label][word] 16.2 混合型特征处理当数据集同时包含连续型和离散型特征时可以对不同列采用不同的概率模型if feature.dtype float: prob gaussian_pdf(...) else: prob multinomial_prob(...)在实现过程中我发现当特征维度超过1000时最好先进行特征选择。有次直接对5000维的文本数据做训练预测速度慢了近10倍而准确率只提升了1.2%。后来改用卡方检验选择前1000个特征后不仅速度恢复由于去除了噪声特征准确率反而提高了2.3%。