【ML】从零到一:手写朴素贝叶斯分类器及Python实战(附代码)
1. 朴素贝叶斯分类器入门指南第一次听说朴素贝叶斯分类器时我完全被这个奇怪的名字搞懵了——朴素和贝叶斯这两个词放在一起到底是什么意思后来才发现这个看似简单的算法在实际应用中效果出奇地好特别是在文本分类和垃圾邮件过滤这些场景。朴素贝叶斯的核心思想其实很直观它通过计算某个特征在不同类别中出现的概率来预测新样本的类别。举个例子如果我们想判断一封邮件是不是垃圾邮件算法会分析邮件中每个词在垃圾邮件和非垃圾邮件中出现的频率然后综合这些信息做出判断。这个算法之所以被称为朴素是因为它做了一个大胆的假设所有特征都是相互独立的。也就是说它认为邮件中免费和赢取这两个词的出现是完全不相关的。虽然这个假设在现实中很少成立但神奇的是这个天真的算法在很多实际应用中表现非常出色。2. 贝叶斯定理的数学基础2.1 理解贝叶斯定理贝叶斯定理是概率论中的一个重要公式描述了在已知某些条件下事件发生的概率。公式看起来可能有点吓人P(A|B) [P(B|A) × P(A)] / P(B)但用生活中的例子来解释就简单多了。假设有一种罕见疾病人群中只有0.1%的人会得病。现在有一个检测方法如果确实有病检测结果为阳性的概率是99%如果没有病检测结果为阳性的概率是5%。如果有人检测结果为阳性他实际患病的概率是多少这就是典型的贝叶斯问题。通过计算我们会发现即使检测为阳性实际患病的概率也只有约2%。这个反直觉的结果展示了贝叶斯定理的威力。2.2 从贝叶斯定理到朴素贝叶斯朴素贝叶斯分类器将这个定理应用到了机器学习中。对于分类问题我们想求的是在观察到特征X的情况下样本属于类别C的概率即P(C|X)。根据贝叶斯定理P(C|X) [P(X|C) × P(C)] / P(X)其中P(C)是先验概率即在不知道任何特征信息时样本属于类别C的概率P(X|C)是似然即在类别C中观察到特征X的概率P(X)是证据因子可以看作归一化常数在实际计算中我们通常忽略P(X)因为它对所有类别都一样不影响比较。3. 手把手实现朴素贝叶斯分类器3.1 数据准备西瓜数据集我们先使用周志华《机器学习》中的西瓜数据集作为例子。这个数据集包含了17个西瓜样本每个样本有8个特征def load_watermelon_dataset(): dataSet [ [青绿, 蜷缩, 浊响, 清晰, 凹陷, 硬滑, 0.697, 0.460, 好瓜], [乌黑, 蜷缩, 沉闷, 清晰, 凹陷, 硬滑, 0.774, 0.376, 好瓜], # ...其他样本数据... [青绿, 蜷缩, 沉闷, 稍糊, 稍凹, 硬滑, 0.719, 0.103, 坏瓜] ] testSet [青绿, 蜷缩, 浊响, 清晰, 凹陷, 硬滑, 0.697, 0.460] labels [色泽, 根蒂, 敲声, 纹理, 脐部, 触感, 密度, 含糖率] return dataSet, testSet, labels这个数据集混合了离散特征如色泽、根蒂和连续特征密度、含糖率非常适合演示朴素贝叶斯的实现。3.2 计算先验概率先验概率P(C)表示在没有观察到任何特征的情况下样本属于某类的概率。计算方法是统计训练集中每个类别出现的频率def calculate_prior(dataSet): count_good 0 count_bad 0 total len(dataSet) for sample in dataSet: if sample[-1] 好瓜: count_good 1 else: count_bad 1 p_good count_good / total p_bad count_bad / total return p_good, p_bad对于西瓜数据集好瓜的先验概率约为8/17≈0.47坏瓜约为9/17≈0.53。3.3 处理离散特征的条件概率对于离散特征我们计算在给定类别下某个特征值出现的频率。例如计算色泽青绿在好瓜中出现的概率def discrete_conditional_prob(dataSet, feature_idx, feature_value, label): # 统计属于该label的样本数 label_samples [s for s in dataSet if s[-1] label] total len(label_samples) # 统计满足特征条件的样本数 count len([s for s in label_samples if s[feature_idx] feature_value]) # 使用拉普拉斯平滑避免零概率问题 prob (count 1) / (total len(set([s[feature_idx] for s in dataSet]))) return prob拉普拉斯平滑是为了防止某个特征值在训练集中从未出现过导致概率为零的情况。3.4 处理连续特征的条件概率对于连续特征我们通常假设它们服从正态分布计算均值和方差def calculate_continuous_params(dataSet, feature_idx, label): # 获取该类别下该特征的所有值 values [s[feature_idx] for s in dataSet if s[-1] label] mean np.mean(values) var np.var(values) return mean, var def gaussian_prob(x, mean, var): # 计算高斯概率密度 exponent np.exp(-((x - mean)**2) / (2 * var)) return (1 / np.sqrt(2 * np.pi * var)) * exponent3.5 综合计算后验概率将所有特征的条件概率和先验概率结合起来def predict(sample, dataSet, labels): p_good, p_bad calculate_prior(dataSet) # 计算离散特征的条件概率 for i in range(6): # 前6个是离散特征 p_good * discrete_conditional_prob(dataSet, i, sample[i], 好瓜) p_bad * discrete_conditional_prob(dataSet, i, sample[i], 坏瓜) # 计算连续特征的条件概率 for i in range(6, 8): # 后2个是连续特征 mean_good, var_good calculate_continuous_params(dataSet, i, 好瓜) mean_bad, var_bad calculate_continuous_params(dataSet, i, 坏瓜) p_good * gaussian_prob(sample[i], mean_good, var_good) p_bad * gaussian_prob(sample[i], mean_bad, var_bad) return 好瓜 if p_good p_bad else 坏瓜4. 实战鸢尾花数据集分类4.1 加载和准备数据我们使用sklearn自带的鸢尾花数据集from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split iris load_iris() X iris.data y iris.target # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42)4.2 实现高斯朴素贝叶斯鸢尾花数据集的所有特征都是连续的我们实现高斯朴素贝叶斯class GaussianNB: def fit(self, X, y): self.classes np.unique(y) self.n_classes len(self.classes) self.n_features X.shape[1] # 计算先验概率 self.priors np.zeros(self.n_classes) for i, c in enumerate(self.classes): self.priors[i] np.sum(y c) / len(y) # 计算每个类每个特征的均值和方差 self.means np.zeros((self.n_classes, self.n_features)) self.vars np.zeros((self.n_classes, self.n_features)) for i, c in enumerate(self.classes): X_c X[y c] self.means[i, :] X_c.mean(axis0) self.vars[i, :] X_c.var(axis0) def predict(self, X): posteriors [] for i, c in enumerate(self.classes): prior np.log(self.priors[i]) posterior np.sum(np.log(self._gaussian_pdf(X, self.means[i], self.vars[i])), axis1) posterior prior posteriors.append(posterior) return self.classes[np.argmax(np.array(posteriors), axis0)] def _gaussian_pdf(self, X, mean, var): # 避免方差为零 var np.where(var 0, 1e-9, var) return (1 / np.sqrt(2 * np.pi * var)) * np.exp(-(X - mean)**2 / (2 * var))4.3 评估模型性能# 训练模型 model GaussianNB() model.fit(X_train, y_train) # 预测测试集 y_pred model.predict(X_test) # 计算准确率 accuracy np.sum(y_pred y_test) / len(y_test) print(f测试集准确率: {accuracy:.2f})在我的测试中这个简单实现能达到约93%的准确率与sklearn的GaussianNB实现相当接近。5. 与sklearn的实现对比5.1 使用sklearn的GaussianNBfrom sklearn.naive_bayes import GaussianNB as SklearnGaussianNB from sklearn.metrics import accuracy_score sk_model SklearnGaussianNB() sk_model.fit(X_train, y_train) sk_pred sk_model.predict(X_test) print(fsklearn准确率: {accuracy_score(y_test, sk_pred):.2f})5.2 实现差异分析我们的实现与sklearn的主要区别在于sklearn使用了更复杂的数值稳定性处理sklearn支持样本权重sklearn在计算方差时使用了无偏估计分母是n-1而不是nsklearn提供了更多实用方法如predict_proba不过核心思想是完全一致的我们的简化实现已经抓住了朴素贝叶斯的精髓。6. 朴素贝叶斯的应用场景与优化6.1 适用场景朴素贝叶斯特别适合以下场景文本分类垃圾邮件过滤、情感分析高维数据集需要快速预测的场景训练数据较少的情况6.2 常见变种根据数据类型不同朴素贝叶斯有几种常见变体高斯朴素贝叶斯用于连续特征假设特征服从正态分布多项式朴素贝叶斯用于离散计数数据如文本分类中的词频伯努利朴素贝叶斯用于二值特征忽略出现次数6.3 性能优化技巧虽然朴素贝叶斯已经很高效但仍有优化空间特征选择去除不相关特征可以提高性能对数概率使用对数避免数值下溢平滑处理对未见过的特征值给予小概率并行计算特征间的独立性天然支持并行在实际项目中我经常先用朴素贝叶斯作为基线模型它的训练速度快能快速验证特征的有效性。即使后续使用更复杂的模型朴素贝叶斯给出的特征重要性分析也很有参考价值。