【机器学习之朴素贝叶斯】-含手动代码实现
前言
朴素贝叶斯算法是机器学习领域中经典的分类方法之一,它以贝叶斯定理为理论基础,结合 “特征条件独立” 的简化假设,实现了高效且直观的分类决策。尽管其假设看似理想化,但在文本分类、垃圾邮件识别等实际场景中,朴素贝叶斯凭借计算简单、泛化能力强的特点,依然展现出优异的性能。本文将从贝叶斯定理出发,拆解朴素贝叶斯分类器的核心原理,结合 “西瓜分类” 的经典案例,通过代码实现完整还原算法流程,帮助读者理解并掌握这一入门级分类算法的精髓。
一、贝叶斯定理
朴素贝叶斯(Naive Bayesian algorithm)是有监督学习的一种分类算法,它基于“贝叶斯定理”实现。
贝叶斯定理是关于随机事件 A 和 B 的条件概率:
P
(
A
∣
B
)
=
P
(
B
∣
A
)
P
(
A
)
P
(
B
)
P(A|B)=\frac{P(B|A)P(A)}{P(B)}
P(A∣B)=P(B)P(B∣A)P(A)
其中:
- P(A|B)是已知B为真,A为真的后验概率
- P(B|A)是如果A为真,B为真的似然概率
- P(A)是A为真的先验概率
- P(B)是B的全概率
二、朴素贝叶斯分类器
1.基本定义
朴素贝叶斯分类器是一种基于贝叶斯定理和特征条件独立假设的分类算法,其基于一个重要的假设”假设所有特征之间是相互独立的“。
2.特征处理
(1) 离散特征的处理
- 对于离散特征,朴素贝叶斯使用的是概率统计的方法
即计算条件概率 P(特征值|类别)=(该类别下该特征值出现的次数) / (该类别的总样本数) - 拉普拉斯平滑:
存在一个问题:如果某个特征 a j a_j aj在类别 w i w_i wi下从未出现过,会导致 P ( a j ∣ w i ) P(a_j|w_i) P(aj∣wi)=0,连乘其中一项为0,使得整个后验概率为0,导致分类结果不合理
则 P ( a j ∣ w i ) = N i , j + 1 N i + k j P(a_j|w_i)=\frac {N_{i,j}+1}{N_i+k_j} P(aj∣wi)=Ni+kjNi,j+1
其中:
N i N_i Ni:类别 w i w_i wi对应的样本数
N i , j N_{i,j} Ni,j: a j a_j aj在类别 w i w_i wi下出现的次数
k j k_j kj: a j a_j aj的种类数
可以利用拉普拉斯平滑计算概率,解决该问题。
(2) 连续特征的处理
- 对于连续特征,朴素贝叶斯通常假设其服从高斯分布;
高斯概率密度函数:
P ( x ∣ ) = 1 2 π σ e x p ( − ( x − μ ) 2 2 σ 2 ) P(x|)=\frac{1}{\sqrt{2\pi}\sigma}exp(-\frac{(x-\mu)^2}{2\sigma^2}) P(x∣)=2πσ1exp(−2σ2(x−μ)2)
其中:
μ \mu μ:均值
σ \sigma σ:标准差 - 计算步骤:
(1)计算每个类别下连续特征的均值和标准差
(2)使用高斯概率密度函数取计算概率
3.例子介绍
我们以一个简单的例子来理解:

以这个例子为例,我们要预测样例X={Sunny,Cool,High,Strong}的最终分类结果
利用朴素贝叶斯分类器,我们这里假设所有特征之间是相互独立的。
在实际的分类中·,我们要比较的其实是
P(Yes|X) ∝ P(Yes) × P(特征1|Yes) × P(特征2|Yes) × …
P(No|X) ∝ P(No) × P(特征1|No) × P(特征2|No) × …
- 先计算先验概率
P(Y=yes)=9/14,P(Y=No)=5/14 - 计算条件概率
P(Sunny|Yes)=2/9,P(Sunny|No)=3/5
P(Cool|Yes)=3/9,P(Cool|No)=1/5
P(High|Yes)=3/9,P(High|No)=4/5
P(Strong|Yes)=3/9,P(Strong|No)=3/5 - 计算后验概率
P(Yes|X)∝P(Yes)P(a1)P(a2)…=(9/14)×(2/9)×(3/9)×(3/9)×(3/9)≈0.0053
P(No|X)∝P(No)P(a1)P(a2)…=(5/14)×(3/5)×(1/5)×(4/5)×(3/5)≈0.0207 - 做出分类
因为0.0207>0.0053
不打网球的概率更大
P(No|X)= 0.0207 0.0053 + 0.0207 \frac {0.0207}{0.0053+0.0207} 0.0053+0.02070.0207=0.795
三、代码实现
接下来我们基于朴素贝叶斯算法,对一个西瓜的例子进行质量分类,根据西瓜的各种特征来判断是否为好瓜
1.数据准备
- 训练数据集

- 测试样本

2.代码:
import math
from collections import defaultdict
train_data = [
{"色泽": "青绿", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "清晰", "脐部": "凹陷", "触感": "硬滑", "密度": 0.697, "含糖率": 0.460, "好瓜": "是"},
{"色泽": "乌黑", "根蒂": "蜷缩", "敲声": "沉闷", "纹理": "清晰", "脐部": "凹陷", "触感": "硬滑", "密度": 0.774, "含糖率": 0.376, "好瓜": "是"},
{"色泽": "乌黑", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "清晰", "脐部": "凹陷", "触感": "硬滑", "密度": 0.634, "含糖率": 0.264, "好瓜": "是"},
{"色泽": "青绿", "根蒂": "蜷缩", "敲声": "沉闷", "纹理": "清晰", "脐部": "凹陷", "触感": "硬滑", "密度": 0.608, "含糖率": 0.318, "好瓜": "是"},
{"色泽": "浅白", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "清晰", "脐部": "凹陷", "触感": "硬滑", "密度": 0.556, "含糖率": 0.215, "好瓜": "是"},
{"色泽": "青绿", "根蒂": "稍蜷", "敲声": "浊响", "纹理": "清晰", "脐部": "稍凹", "触感": "软粘", "密度": 0.403, "含糖率": 0.237, "好瓜": "是"},
{"色泽": "乌黑", "根蒂": "稍蜷", "敲声": "浊响", "纹理": "稍糊", "脐部": "稍凹", "触感": "软粘", "密度": 0.481, "含糖率": 0.149, "好瓜": "是"},
{"色泽": "乌黑", "根蒂": "稍蜷", "敲声": "浊响", "纹理": "清晰", "脐部": "稍凹", "触感": "硬滑", "密度": 0.437, "含糖率": 0.211, "好瓜": "是"},
{"色泽": "乌黑", "根蒂": "稍蜷", "敲声": "沉闷", "纹理": "稍糊", "脐部": "稍凹", "触感": "硬滑", "密度": 0.666, "含糖率": 0.091, "好瓜": "否"},
{"色泽": "青绿", "根蒂": "硬挺", "敲声": "清脆", "纹理": "清晰", "脐部": "平坦", "触感": "软粘", "密度": 0.243, "含糖率": 0.267, "好瓜": "否"},
{"色泽": "浅白", "根蒂": "硬挺", "敲声": "清脆", "纹理": "模糊", "脐部": "平坦", "触感": "硬滑", "密度": 0.245, "含糖率": 0.057, "好瓜": "否"},
{"色泽": "浅白", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "模糊", "脐部": "平坦", "触感": "软粘", "密度": 0.343, "含糖率": 0.099, "好瓜": "否"},
{"色泽": "青绿", "根蒂": "稍蜷", "敲声": "浊响", "纹理": "稍糊", "脐部": "凹陷", "触感": "硬滑", "密度": 0.639, "含糖率": 0.161, "好瓜": "否"},
{"色泽": "浅白", "根蒂": "稍蜷", "敲声": "沉闷", "纹理": "稍糊", "脐部": "凹陷", "触感": "硬滑", "密度": 0.657, "含糖率": 0.198, "好瓜": "否"},
{"色泽": "乌黑", "根蒂": "稍蜷", "敲声": "浊响", "纹理": "清晰", "脐部": "稍凹", "触感": "软粘", "密度": 0.360, "含糖率": 0.370, "好瓜": "否"},
{"色泽": "浅白", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "模糊", "脐部": "平坦", "触感": "硬滑", "密度": 0.593, "含糖率": 0.042, "好瓜": "否"},
{"色泽": "青绿", "根蒂": "蜷缩", "敲声": "沉闷", "纹理": "稍糊", "脐部": "稍凹", "触感": "硬滑", "密度": 0.719, "含糖率": 0.103, "好瓜": "否"},
]
test_sample = {
"色泽": "青绿", "根蒂": "蜷缩", "敲声": "浊响", "纹理": "清晰",
"脐部": "凹陷", "触感": "硬滑", "密度": 0.697, "含糖率": 0.460
}
#划分特征和标签
lisan_features=["色泽", "根蒂", "敲声", "纹理", "脐部", "触感"]
lianxu_features=["密度", "含糖率"]
label_name="好瓜" #标签的名字
class_name=["是","否"]
#统计每个类别的样本数
label_count={"是":0,"否":0}
for sample in train_data: #遍历每一个样本
label=sample[label_name] #从样本中提取标签值,则label应该是"是"或"否"
label_count[label]+=1
total_samples=len(train_data) #总的样本数
#离散特征的计数
feature_value={} #[标签][特征][特征值]=次数
#进行初始化
for label in class_name:
feature_value[label]={}
for feature in lisan_features:
feature_value[label][feature]={}
#统计样本的次数
for sample in train_data:
label=sample[label_name] #是好瓜还是坏瓜
for feature in lisan_features: #feature是特征
value=sample[feature] #value是特征值
if value not in feature_value[label][feature]:
feature_value[label][feature][value]=0
feature_value[label][feature][value]+=1
#计算离散特征的条件概率
lisan_prob={}
for label in class_name:
lisan_prob[label]={}
label_sample_cnt=label_count[label] #当前这个类别的样本数
for feature in lisan_features:
lisan_prob[label][feature]={}
all_value=set(sample[feature] for sample in train_data ) #获得当前这个特征的所有取值,比如色泽这个特征有青绿、乌黑、浅白等取值
value_len=len(all_value)
#直接计算极大似然
# for value in all_value:
# count=feature_value[label][feature].get(value,0) #没有则取0,也就是找label标签下,feature特征下,value的数量
# #计算极大似然
# prob=count/label_sample_cnt if label_sample_cnt>0 else 0
# lisan_prob[label][feature][value]=prob
#或者用拉普拉斯平滑
for value in all_value:
count=feature_value[label][feature].get(value, 0)
# 拉普拉斯平滑:分母是某个值在类别下出现的次数+1,分子是当前类别对应的样本数+这个类别下对应的值的数量
prob=(count+1)/(label_sample_cnt+value_len) if label_sample_cnt > 0 else 0
lisan_prob[label][feature][value] = prob
#计算连续特征的均值和标准差
lianxu_stats={}
for label in class_name:
lianxu_stats[label]={}
label_sample=[sample for sample in train_data if sample[label_name]==label] #选出训练集中标签为label的样本
for feature in lianxu_features: #遍历连续特征
value=[sample[feature] for sample in label_sample] #提取该特征下的取值,例如"密度"的所有取值
mean=sum(value)/len(value) #求出均值
s_sum=sum((v-mean)**2 for v in value) #计算平方差之和,遍历value里面的每个值v
std=math.sqrt((s_sum/len(value))) #计算标准差,s_sum/len(value))是方差,开根号是标准差
#保存均值和标准差
lianxu_stats[label][feature]={"mean":mean,"std":std}
#根据公式
#输入连续特征的具体取值
def lianxu_formula(x,mean,std):
if std==0:
return 1.0 if x==mean else 0.0
exponent=math.exp(-((x-mean)**2)/(2*std**2))
return (1/(math.sqrt(2*math.pi)*std))*exponent
#计算后验概率
posterior_prob={}
for label in class_name:
#先验概率
#标签的样本数/总的样本数
prior=label_count[label]/total_samples
#离散特征的概率
lisan_result_prob=1.0 #概率值初始化
for feature in lisan_features:
test_value=test_sample[feature]
if test_value in lisan_prob[label][feature]:
prob=lisan_prob[label][feature][test_value]
else:
prob=0
#所有概率连乘起来
lisan_result_prob*=prob
lianxu_result_prob=1.0
for feature in lianxu_features:
test_value=test_sample[feature]
stats=lianxu_stats[label][feature]
#计算出概率
prob=lianxu_formula(test_value,stats["mean"],stats["std"])
#进行连乘
lianxu_result_prob*=prob
#后验概率
posterior_prob[label]=prior*lisan_result_prob*lianxu_result_prob
for label in class_name:
print(f"P({label_name}={label} | 测试样本) = {posterior_prob[label]:.8f}")
predicted_label = max(posterior_prob,key=posterior_prob.get)
print(f"\n测试样本分类结果:{label_name} = {predicted_label}")
3.结果

可以看到测试样本最终的分类结果为好瓜
总结
本文从贝叶斯定理的基本概念入手,详细介绍了朴素贝叶斯分类器的核心思想 —— 通过先验概率与特征条件概率的结合,实现对样本类别的推断。针对离散特征的零概率问题,我们引入拉普拉斯平滑进行优化;对于连续特征,则采用高斯分布建模概率密度。结合西瓜分类的案例,代码实现直观展示了从数据预处理、概率计算到最终分类的全流程,验证了算法在实际场景中的有效性。
更多推荐
所有评论(0)