朴素贝叶斯分类器 Python 实现:从零构建核心函数与拉普拉斯平滑实战
1. 朴素贝叶斯算法原理精要
朴素贝叶斯分类器基于贝叶斯定理构建,其核心公式为:
P(y|x) = P(x|y) * P(y) / P(x)其中:
P(y|x)是给定特征x时类别y的后验概率P(x|y)是似然概率(类别y中特征x出现的概率)P(y)是类别y的先验概率P(x)是特征x的边缘概率
关键假设:所有特征条件独立,即:
P(x1,x2,...,xn|y) = P(x1|y)*P(x2|y)*...*P(xn|y)这种独立性假设虽然"朴素",但在实际应用中往往表现惊人地好,特别是在文本分类等场景。
2. 核心函数实现:从零构建分类器
2.1 初始化与数据结构设计
我们首先定义NaiveBayesClassifier类的基本结构:
import numpy as np class NaiveBayesClassifier: def __init__(self): self.label_prob = {} # 存储类别的先验概率 P(y) self.condition_prob = {} # 存储条件概率 P(x|y) def fit(self, X, y): """训练模型,计算先验概率和条件概率""" pass def predict(self, X): """预测新样本的类别""" pass2.2 fit函数实现:训练过程
fit函数需要完成两个核心计算:
def fit(self, X, y): # 计算类别的先验概率 unique_labels, counts = np.unique(y, return_counts=True) total_samples = len(y) self.label_prob = {label: count/total_samples for label, count in zip(unique_labels, counts)} # 计算条件概率 P(x|y) for label in unique_labels: # 获取当前类别的所有样本 label_samples = X[y == label] self.condition_prob[label] = {} for feature_idx in range(X.shape[1]): # 遍历每个特征 feature_values = label_samples[:, feature_idx] unique_values, value_counts = np.unique(feature_values, return_counts=True) # 计算当前特征在当前类别下的概率分布 self.condition_prob[label][feature_idx] = { value: count/len(label_samples) for value, count in zip(unique_values, value_counts) }2.3 predict函数实现:预测过程
预测时需要计算每个类别的后验概率并选择最大值:
def predict(self, X): predictions = [] for sample in X: max_prob = -1 predicted_label = None # 遍历所有可能的类别 for label in self.label_prob: # 初始化当前类别的概率为先验概率 current_prob = self.label_prob[label] # 计算所有特征的条件概率乘积 for feature_idx, feature_value in enumerate(sample): # 如果特征值在训练集中未出现过,跳过该特征 if feature_value in self.condition_prob[label][feature_idx]: current_prob *= self.condition_prob[label][feature_idx][feature_value] # 选择概率最大的类别 if current_prob > max_prob: max_prob = current_prob predicted_label = label predictions.append(predicted_label) return np.array(predictions)3. 零概率问题与拉普拉斯平滑
3.1 零概率问题分析
当测试集中出现训练集未出现过的特征值时,会导致条件概率为0,进而使整个后验概率为0。例如:
训练集中"购买金额=1000"在"类别=欺诈"中从未出现 则 P(购买金额=1000|欺诈) = 0 导致 P(欺诈|特征) = 03.2 拉普拉斯平滑实现
拉普拉斯平滑通过在分子加1,分母加类别数来避免零概率:
def fit_with_smoothing(self, X, y, alpha=1): # 计算带平滑的类先验概率 unique_labels, counts = np.unique(y, return_counts=True) total_samples = len(y) self.label_prob = { label: (count + alpha) / (total_samples + alpha * len(unique_labels)) for label, count in zip(unique_labels, counts) } # 计算带平滑的条件概率 for label in unique_labels: label_samples = X[y == label] self.condition_prob[label] = {} for feature_idx in range(X.shape[1]): feature_values = label_samples[:, feature_idx] unique_values = np.unique(np.concatenate([feature_values, np.unique(X[:, feature_idx])])) value_counts = {value: 0 for value in unique_values} for value in feature_values: value_counts[value] += 1 # 应用拉普拉斯平滑 total_count = len(label_samples) num_unique_values = len(unique_values) self.condition_prob[label][feature_idx] = { value: (count + alpha) / (total_count + alpha * num_unique_values) for value, count in value_counts.items() }3.3 平滑效果对比
| 场景 | 传统方法概率 | 平滑后概率 |
|---|---|---|
| 出现过的特征值 | count/N | (count+1)/(N+V) |
| 未出现过的特征值 | 0 | 1/(N+V) |
| 未出现过的类别 | 0 | 1/(总样本数+K) |
其中:
- N:类别中的样本数
- V:特征的可能取值数
- K:类别总数
- count:特征值出现次数
4. 实战应用:新闻分类示例
4.1 数据准备与预处理
from sklearn.datasets import fetch_20newsgroups from sklearn.feature_extraction.text import CountVectorizer # 加载数据 categories = ['sci.space', 'rec.sport.baseball', 'talk.politics.mideast'] newsgroups_train = fetch_20newsgroups(subset='train', categories=categories) newsgroups_test = fetch_20newsgroups(subset='test', categories=categories) # 文本向量化 vectorizer = CountVectorizer(stop_words='english', max_features=1000) X_train = vectorizer.fit_transform(newsgroups_train.data).toarray() y_train = newsgroups_train.target X_test = vectorizer.transform(newsgroups_test.data).toarray() y_test = newsgroups_test.target4.2 模型训练与评估
# 初始化并训练模型 nb = NaiveBayesClassifier() nb.fit_with_smoothing(X_train, y_train) # 预测并评估 from sklearn.metrics import accuracy_score y_pred = nb.predict(X_test) print(f"准确率: {accuracy_score(y_test, y_pred):.2f}")4.3 性能优化技巧
特征选择:使用TF-IDF替代词频统计
from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer(stop_words='english', max_features=1000)对数概率计算:防止数值下溢
# 在predict函数中使用对数概率 log_prob = np.log(self.label_prob[label]) for feature_idx, feature_value in enumerate(sample): if feature_value in self.condition_prob[label][feature_idx]: log_prob += np.log(self.condition_prob[label][feature_idx][feature_value])处理连续特征:高斯朴素贝叶斯
# 计算均值和方差 self.means = {label: np.mean(X[y == label], axis=0) for label in unique_labels} self.vars = {label: np.var(X[y == label], axis=0) for label in unique_labels} # 高斯概率密度计算 def gaussian_prob(self, x, mean, var): exponent = np.exp(-((x - mean)**2) / (2 * var)) return (1 / np.sqrt(2 * np.pi * var)) * exponent
5. 算法对比与工程实践
5.1 不同朴素贝叶斯变体对比
| 类型 | 特征假设 | 适用场景 | Scikit-learn实现类 |
|---|---|---|---|
| 高斯 | 连续值,正态分布 | 数值型数据 | GaussianNB |
| 多项式 | 离散值,多项式分布 | 文本分类(词频) | MultinomialNB |
| 伯努利 | 二元特征 | 文本分类(出现/不出现) | BernoulliNB |
5.2 实际应用中的注意事项
特征相关性处理:
- 使用特征选择降低相关性
- 考虑半朴素贝叶斯(放松独立性假设)
类别不平衡处理:
# 在fit方法中调整类先验 class_weights = {0: 0.7, 1: 0.3} # 根据业务需求调整 self.label_prob = {label: weight for label, weight in class_weights.items()}大数据集处理:
- 增量学习(partial_fit方法)
- 分布式实现(如Spark MLlib)
5.3 性能优化实战代码
from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline from sklearn.feature_extraction.text import TfidfTransformer # 构建优化后的文本分类管道 text_clf = Pipeline([ ('vect', CountVectorizer(stop_words='english', max_features=10000)), ('tfidf', TfidfTransformer()), ('clf', MultinomialNB(alpha=0.1)), ]) # 训练和评估 text_clf.fit(newsgroups_train.data, newsgroups_train.target) predicted = text_clf.predict(newsgroups_test.data) print(f"优化后准确率: {np.mean(predicted == newsgroups_test.target):.2f}")朴素贝叶斯分类器虽然理论基础简单,但在实际应用中通过合理的特征工程和参数调整,往往能获得与复杂模型相媲美的性能。特别是在文本分类、垃圾邮件过滤等场景中,它仍然是首选的基准模型之一。