引言
前述学习进程中,已经学习了拉普拉斯平滑公式计算条件概率的简单应用,文章链接为:
python学智能算法(十二)|机器学习朴素贝叶斯方法初步-拉普拉斯平滑计算条件概率
在此基础上,今天更进一步,联系一个简单二元分类的项目。
项目介绍
简单二元分类,就是把数据分成两种样本,完成区分即可。
参数定义
开展项目之前,先来定义几个参数:
先验概率P(y):
P ( y ) = ∑ j = 1 j = n y j ∑ y P(y)=\frac{\sum_{j=1}^{j=n}y_{j}}{\sum_{}^{}y_{}} P(y)=∑y∑j=1j=nyj这里的yj代表特定样本,y代表任何样本,先验概率的意思就是特定样本yj占所有样本的比例。
条件概率P(xj|y):
P ( x j ∣ y ) = ∑ y 1 j m x j + 1 ∑ i = 1 n y i + y . s h a p e [ 1 ] P(x_{j}|y)=\frac{\sum_{y_1}^{j_m}x_{j}+1}{\sum_{i=1}^{n}y_{i}+y.shape[1]} P(xj∣y)=∑i=1nyi+y.shape[1]∑y1jmxj+1这里的xj代表特定样本中特征xj出现的次数,yi代表特定样本,y.shape[1]是指每一个样本所拥有的特征数。条件概率使用了拉普拉斯平滑公式。
具体的,如果给定矩阵X和标签数组y:
假设有3个样本,2个特征
样本数就是行数,特征数是列数
X = np.array([
1, 0\], # 样本1 \[1, 1\], # 样本2 \[0, 1\] # 样本3 \]) y = np.array(\[0, 1, 1\])
对于y=0,只有样本[1,0],对应的:
先验概率P(y=0)=1/3,y=0表示标签为0的第一个样本,这里计算的先验概率就是标签为0的第一个样本占所有样本的比例。
条件概率P(1|y)=(1+1)/(1+2)=0.67,P(1|y)表示标签为0的第一个样本中,特征1所占的条件概率,由于只有一个样本,每个样本的特征数都是2,所以分母上是1+2,这样计算得到的条件概率就是0.67。
可以这样理解,矩阵的行数就是样本数,矩阵的列数就是特征数。条件概率只在属于特定标签的范围内计算。
有时候标签也会被称作类别,即特定类别中计算条件概率。
拉普拉斯平滑公式
拉普拉斯平滑公式定义的目的是解决传统的条件概率计算结果为0的问题。
比如,对于正在计算的条件概率P(xj|y),在标签或者类别y下特征xj如果从来没有出现,那如果按照下式计算结果就是0:
P ( x j ∣ y ) = ∑ y 1 y m x j ∑ i = 1 n y i P(x_{j}|y)=\frac{\sum_{y_1}^{y_m}x_{j}}{\sum_{i=1}^{n}y_{i}} P(xj∣y)=∑i=1nyi∑y1ymxj
具体公式推导以后再开展学习,此处只需要记住条件概率P(xj|y)的定义采用了拉普拉斯平滑公式。
对数后验概率
对数后验概率:
l n P ( y ∣ x ) = l n P ( y ) + ∑ j = 1 n x j ⋅ P ( x j ∣ y ) lnP(y|x)=lnP(y)+{\sum_{j=1}^{n}x_{j}·P(x_{j}|y)} lnP(y∣x)=lnP(y)+j=1∑nxj⋅P(xj∣y)对数后验概率实际上可以细分为两部分:
对数先验概率: l n P ( x j ∣ y ) = l n P ( y ) lnP(x_{j}|y)=lnP(y) lnP(xj∣y)=lnP(y)
对数条件概率之和: s u m x j P ( x j ∣ y ) = ∑ j = 1 n x j ⋅ P ( x j ∣ y ) sumx_{j}P(x_{j}|y)={\sum_{j=1}^{n}x_{j}·P(x_{j}|y)} sumxjP(xj∣y)=j=1∑nxj⋅P(xj∣y)
所以,对数后验概率实际上是对数先验概率和对数条件概率之和。
具体的,如果给定矩阵X和标签数组y:
假设有3个样本,2个特征
样本数就是行数,特征数是列数
X = np.array([
1, 0\], # 样本1 \[1, 1\], # 样本2 \[0, 1\] # 样本3 \]) y = np.array(\[0, 1, 1\])
对于y=1,有2个样本[1,1]和[0,1],对应的先验概率P(y=1)=2/3,y=1表示标签为1的第2个样本,这里计算的先验概率就是标签为1的第一个样本占所有样本的比例。
条件概率P(1|y)=(1+1)/(1+2)=0.67,P(1|y)表示标签为1的2个样本中,特征1所占的条件概率。由于有2个样本,每个样本都有特征1,按照拉普拉斯平滑公式先计算条件概率,首先得到分子[1+1,1+1+1=[2,3],此外由于每个样本的特征数都是2,所以分母上是2+2求和后的[4,4],这样计算得到的条件概率就是[2/4,3/4]=[0.5,0.75]。
接下来就可以计算:
对数先验概率:lnP(1|y)=ln0.67=-0.4005
对数条件概率:
lnP(x1=1|y=1)=ln(0.5)=-0.6931,
lnP(x2=1|y=1)=ln(0.75)=-0.2877
对数后验概率要对每一个样本都开展:
样本[1,0],lnP(1|y)+sumx_{j}P(x_{j}|y)=-0.4005+1*-0.6931+0*-0.2877=-1.0936;
样本[1,1],lnP(1|y)+sumx_{j}P(x_{j}|y)=-0.4005+1*-0.6931+1*-0.2877=-1.3813;
样本[0,1],lnP(1|y)+sumx_{j}P(x_{j}|y)=-0.4005+0*-0.6931+1*-0.2877=-0.6882。
对数后验概率需要将每一个样本都代入计算,不管样本属于哪一个类别。
代码
首先引入库:
python
# 引入 numpy模块
import numpy as np
然后需要定义一个大类:
python
# 定义类
class NaiveBayes:
这个类里面分了很多子函数,先看第一个:
python
def __init__(self):
"""初始化朴素贝叶斯分类器"""
# classes参数用于存储类别标签
self.classes = None
# prior参数用于存储存储先验概率 P(y)
self.prior = None
# conditional参数用于存储条件概率 P(x|y)
self.conditional = None
这里的_ini_(self)函数定义了几个变量名,用于存储数据。
第二个函数:
python
# 定义子函数fit用于训练
def fit(self, X, y):
"""
训练朴素贝叶斯分类器
X: 训练数据特征,形状为 [样本数, 特征数]
y: 训练数据标签,形状为 [样本数]
"""
# n_samples为样本X的行数,n_features为样本X的列数
n_samples, n_features = X.shape
# np.unique()是一个合并同类项的函数,classes可以获得y具体的类
self.classes = np.unique(y)
# n_classes存储了classes的类的具体数量
n_classes = len(self.classes)
# 初始化先验概率和条件概率
# prior是一个纯0矩阵,矩阵为n_classes大小的一维数组
self.prior = np.zeros(n_classes)
# conditional是一个n_classes行n_features列的纯0矩阵
self.conditional = np.zeros((n_classes, n_features))
此处定义的fit(self, X, y)函数用于训练,实际上此处还没有开始训练,依然是定义了大量的常数,唯一对输入变量进行作用的执行是使用np.unique(y)对标签数组y合并同类项。
然后第三个函数:
python
# 计算先验概率和条件概率
# i代表枚举函数classes中各个类的位置索引,c代表具体的类别
for i, c in enumerate(self.classes):
# 计算属于类别 c 的样本
# X_c存储的结果是True和False,当y中的数据等于c时,取True,否则取False
X_c = X[y == c]
print(f"类别 {c} 的样本:\n{X_c}")
# 计算类别 c 的先验概率:类别 c 的样本数 / 总样本数
# X_c.shape[0]是X_c的行数
self.prior[i] = X_c.shape[0] / float(n_samples)
print(f"self.prior {i} =:\n{self.prior[i]}")
# 计算条件概率:每个特征在类别 c 下出现的频率
self.conditional[i] = (X_c.sum(axis=0) + 1) / (X_c.shape[0] + n_features) # 拉普拉斯平滑
print(f"self.conditional{i} =:\n{self.conditional[i]}")
此处定义的i, c in enumerate(self.classes)函数使用for循环+枚举的形式获得了先验概率self.prior[i] = X_c.shape[0] / float(n_samples)和拉普拉斯平滑条件概率:self.conditional[i] = (X_c.sum(axis=0) + 1) / (X_c.shape[0] + n_features)
之后是第四个函数:
python
def predict(self, X):
"""
对输入数据进行预测
X: 待预测数据特征,形状为 [样本数, 特征数]
返回预测结果,形状为 [样本数]
"""
# 对每个样本计算后验概率,选择后验概率最大的类别
return [self._predict(x) for x in X]
这里实际上只有一个输出和调用,调用了第五个子函数:
python
def _predict(self, x):
"""对单个样本进行预测"""
# 计算每个类别的后验概率对数
# posteriors是一个空矩阵,用于存储计算数据
posteriors = []
# for循环类别的位置索引和类别标签
for i, c in enumerate(self.classes):
# 先计算对数先验概率
prior = np.log(self.prior[i])
# 计算条件概率对数之和
conditional = np.sum(np.log(self.conditional[i]) * x)
# 获得对数后验概率
posterior = prior + conditional
# 顿出所有的对数后验概率
posteriors.append(posterior)
# 返回后验概率最大的类别索引
return self.classes[np.argmax(posteriors)]
实际上在理解了前面的参数定义后,后半段会比较好理解,在每一个类别下,计算所有样本的对数后验概率,取最大的后验概率对应的类别为预测值。
之后就比较简单,输入训练参数和待预测数据:
python
# 示例用法
if __name__ == "__main__":
# 创建简单的训练数据
X = np.array([[1, 1], [1, 0], [0, 1], [0, 0]])
y = np.array([1, 1, 0, 0])
# 初始化并训练模型
nb = NaiveBayes()
nb.fit(X, y)
# 进行预测
test_X = np.array([[1, 0], [0, 1]])
predictions = nb.predict(test_X)
print("预测结果:", predictions) # 输出应该是 [1, 0]
完整代码为:
python
# 引入 numpy模块
import numpy as np
# 定义类
class NaiveBayes:
# 定义子函数__init__(self)存储数据
def __init__(self):
"""初始化朴素贝叶斯分类器"""
# classes参数用于存储类别标签
self.classes = None
# prior参数用于存储存储先验概率 P(y)
self.prior = None
# conditional参数用于存储存储条件概率 P(x|y)
self.conditional = None
# 定义子函数fit用于训练
def fit(self, X, y):
"""
训练朴素贝叶斯分类器
X: 训练数据特征,形状为 [样本数, 特征数]
y: 训练数据标签,形状为 [样本数]
"""
# n_samples为样本X的行数,n_features为样本X的列数
n_samples, n_features = X.shape
# np.unique()是一个合并同类项的函数,classes可以获得y具体的类
self.classes = np.unique(y)
# n_classes存储了classes的类的具体数量
n_classes = len(self.classes)
# 初始化先验概率和条件概率
# prior是一个纯0矩阵,矩阵为n_classes大小的一维数组
self.prior = np.zeros(n_classes)
# conditional是一个n_classes行n_features列的纯0矩阵
self.conditional = np.zeros((n_classes, n_features))
# 计算先验概率和条件概率
# i代表枚举函数classes中各个类的位置索引,c代表具体的类别
for i, c in enumerate(self.classes):
# 计算属于类别 c 的样本
# X_c存储的结果是True和False,当y中的数据等于c时,取True,否则取False
X_c = X[y == c]
print(f"类别 {c} 的样本:\n{X_c}")
# 计算类别 c 的先验概率:类别 c 的样本数 / 总样本数
# X_c.shape[0]是X_c的行数
self.prior[i] = X_c.shape[0] / float(n_samples)
print(f"self.prior {i} =:\n{self.prior[i]}")
# 计算条件概率:每个特征在类别 c 下出现的频率
self.conditional[i] = (X_c.sum(axis=0) + 1) / (X_c.shape[0] + n_features) # 拉普拉斯平滑
print(f"self.conditional{i} =:\n{self.conditional[i]}")
def predict(self, X):
"""
对输入数据进行预测
X: 待预测数据特征,形状为 [样本数, 特征数]
返回预测结果,形状为 [样本数]
"""
# 对每个样本计算后验概率,选择后验概率最大的类别
return [self._predict(x) for x in X]
def _predict(self, x):
"""对单个样本进行预测"""
# 计算每个类别的后验概率对数
# posteriors是一个空矩阵,用于存储计算数据
posteriors = []
# for循环类别的位置索引和类别标签
for i, c in enumerate(self.classes):
# 先计算对数先验概率
prior = np.log(self.prior[i])
# 计算条件概率对数之和
conditional = np.sum(np.log(self.conditional[i]) * x)
# 获得对数后验概率
posterior = prior + conditional
# 顿出所有的对数后验概率
posteriors.append(posterior)
# 返回后验概率最大的类别索引
return self.classes[np.argmax(posteriors)]
# 示例用法
if __name__ == "__main__":
# 创建简单的训练数据
X = np.array([[1, 1], [1, 0], [0, 1], [0, 0]])
y = np.array([1, 1, 0, 0])
# 初始化并训练模型
nb = NaiveBayes()
nb.fit(X, y)
# 进行预测
test_X = np.array([[1, 0], [0, 1]])
predictions = nb.predict(test_X)
print("预测结果:", predictions) # 输出应该是 [1, 0]
实际输出为:
类别 0 的样本:
\[0 1
0 0\]
self.prior 0 =:
0.5
self.conditional0 =:
0.25 0.5
类别 1 的样本:
\[1 1
1 0\]
self.prior 1 =:
0.5
self.conditional1 =:
0.75 0.5
预测结果: [1, 0]
总结
学习了使用朴素贝叶斯方法实现简单二元分类的基本操作技巧。