7.3 朴素贝叶斯实现#
经过前面两个小节内容的介绍,对于朴素贝叶斯算法的原理我们已经有了清晰的认识。在本节内容中,我们将开始分步对各个部分的实现进行详细地介绍。同时,需要说明的是以下实现代码均参考自sklearn 0.24.0 中的CategoricalNB模块,只是对部分处理逻辑进行了修改与简化,完整代码见 AllBooKCode/Chapter07/C01_naive_bayes_category.py 文件。
7.3.1 特征计数实现#
通过7.1节的内容可知,不管是计算先验概率还是条件概率,在这之前都需要先统计训练集中各个样本及样本特征取值的分布情况。因此,这里首先需要初始化相关的计数器,然后再对样本和特征取值的分布情况进行统计。
具体地,对于计数器的初始化工作实现过程,示例代码如下:
1 class MyCategoricalNB(object):
2
3 def __init__(self, alpha=1.0):
4 self.alpha = alpha
5
6 def _init_counters(self):
7 self.class_count_ = np.zeros(self.n_classes, dtype=np.float64)
8 self.category_count_ = [np.zeros((self.n_classes, 0))
9 for _ in range(self.n_features_)]在上述代码中,第3~4行是初始化平滑项系数alpha。第7行class_count_被初始化成了一个形状为[n_classes,]的全零向量,其中n_classes表示分类的类别数量,而每个维度分别表示每个类别的样本数量(例如[2,2,3]表示0、1、2这3个类别的样本数分别是2、2、3),其目的是后续用于计算每个类别的先验概率。第8行category_count_被初始化成了一个包含有n_features_个元素的列表,其中n_features_表示数据集的特征维度数量,同时category_count_中每个元素的形状是[n_classes,0](后续每个元素将会更新为[n_classes,len(X_i)]的形状, len(X_i)表示X_i这个特征的取值情况数量);而category_count_的作用是记录在各个类别下每个特征变量中各种取值情况的数量,例如category_count_[i][j][k]为10表示含义就是特征i在类别j下特征取值为k的样本数量为10个。
在初始化两个计数器之后,进一步便可以实现各个类别及特征分布的统计,示例代码如下:
1 def _count(self, X, Y):
2 def _update_cat_count(X_feature, Y, cat_count, n_classes):
3 for j in range(n_classes): # 遍历每个类别
4 mask = Y[:, j].astype(bool) # 取每个类别下对应样本的索引
5 counts = np.bincount(X_feature[mask]) # 统计当前类别下,特征X_feature中各个取值下的数量
6 indices = np.nonzero(counts)[0]
7 cat_count[j, indices] += counts[indices]
8
9 self.class_count_ += Y.sum(axis=0) # Y: shape(n,n_classes) Y.sum(): shape(n_classes,)
10 self.n_categories_ = X.max(axis=0) + 1
11 for i in range(self.n_features_): # 遍历每个特征
12 X_feature = X[:, i] # 取每一列的特征
13 self.category_count_[i] = np.pad(self.category_count_[i],
14 [(0, 0), (0, self.n_categories_[i])], 'constant')
15 _update_cat_count(X_feature, Y,self.category_count_[i],self.n_classes)在上述代码中,第1行参数Y是原始标签经过one-hot编码后的形式,例如3分类问题中类别1会被编码成[0,1,0]的形式,因此Y的形状为[n,n_classes]。第9行代码是计算得到每个类别对应的样本数量。第10行则是统计每个特征维度的取值数量(因为特征取值是从0开始的所以后面加了1),例如[3 3 3 3]表示四个特征维度的取值均有3种情况。第11~12行开始遍历每个特征并取对应的特征列。第13~14行是对category_count_中的每个元素填充self.n_categories_[i]列全0向量,此时category_count_中每个元素将会变成形状为[n_classes,len(X_i)]的全零矩阵。第15行则是根据输入的每一列特征等相关参数来更新category_count_计数器。
第3~5行为遍历每一个样本类别,并取每个类别下对应样本的索引,同时统计当前类别下特征列X_feature中各个取值下的数量。同时,第5行中np.bincount的作用的是统计每个值出现的次数,例如:
1 counts = np.bincount(np.array([0, 3, 5, 1, 4, 4]))
2 print(counts) # [1 1 0 1 2 1]
3 # 表示[0, 3, 5, 1, 4, 4]中0,1,2,3,4,5这个6个值的出现的频次分别是1,1,0,1,2,1第6~7行则是用来更新cat_count中当前输入特征每种取值情况的分布数量,例如cat_count[i,k]表示的是第i个类别下,特征X_feature的第k个取值的数量。
例如对于表7-1中的数据集来说,其对应的category_count_计数器的结果为:
1 [[[4., 1.],[3., 7.]],
2 [[4., 1.],[4., 6.]],
3 [[1., 1., 3.],[2., 3., 5.]]]其中[[4., 1.],[3., 7.]]分别表示的含义就是:对于第1个特征$X^{(1)}$来说,在$Y=0$这个类别下,$X^{(1)}$取值为0的情况有4种(表中第4~7号样本),取值为1的情况有1种(第14号样本);在$Y=1$这个类别下,$X^{(1)}$取值为0的情况有3种(第1~3号样本),取值为1的情况有7种(第8~13和第15号样本)。同理,对于第2个特征$X^{(2)}$来说,在$Y=0$这个类别下,$X^{(2)}$取值为0的情况有4种,取值为1的情况有1种;在$Y=1$这个类别下,$X^{(2)}$取值为0的情况有4种,取值为1的情况有6种。
到此,对于数据集中样本及特征分布情况的计数就算是统计完了,接下来开始实现计算先验概率和条件概率。
7.3.2 先验概率实现#
有了数据集中各个样本的分布情况后,计算先验概率就变得十分简单了,示例代码如下:
1 def _update_class_prior(self):
2 self.class_prior_ = (self.class_count_ + self.alpha) / # shape: [n_classes, ]
3 (self.class_count_.sum() + self.n_classes * self.alpha)在上述代码中,第2~3行便是用来计算各个类别的先验概率,其中带有alpha的地方便是第7.2.1节内容中的平滑处理项。