本文主要是先对朴素贝叶斯算法进行简单介绍, 再使用python对算法进行实现
1. 算法介绍
首先假设训练数据集 T = { X , Y } = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . ( x n , y n ) } T = \{X, Y\}=\{(x_1, y_1), (x_2, y_2),...(x_n, y_n)\} T={X,Y}={(x1,y1),(x2,y2),...(xn,yn)}, 其中每个 x x x都有 m m m个维度: x = [ x 1 , x 2 , . . . , x M ] x = [x^1, x^2, ... ,x^M] x=[x1,x2,...,xM], 标签 Y Y Y有 K K K个类别: y 1 , y 2 , . . . , y n = c k ∈ [ c 1 , . . , c K ] y_1, y_2,..., y_n= c_k \in[c_1, .. ,c_K] y1,y2,...,yn=ck∈[c1,..,cK]
其次朴素贝叶斯法是基于贝叶斯定理和特征条件独立假设来进行分类的方法:
-
我们的目的是在给定的 x x x的条件下, 计算出判别每一类标签 y y y的概率, 其中m个类别中概率最大对应的那一类, 就是我们估计的 y y y, 也就是使得后验概率最大化.
y = f ( x ) = arg max c k P ( Y = c k ∣ X = x ) y = f(x) = \arg\max_{c_k}P(Y=c_k |X=x) y=f(x)=argckmaxP(Y=ck∣X=x) -
想要计算后验概率, 就需要计算x与y的联合分布函数, 也就需要计算先验概率和条件概率
P ( Y = c k ∣ X = x ) = P ( Y = c k , X = x ) P ( X = x ) = P ( X = x ∣ Y = c k ) P ( Y = c k ) P ( X = x ) = P ( X = x 1 , x 2 , . . . , x m ∣ Y = c k ) P ( Y = c k ) ∑ K [ P ( X = x ∣ Y = c k ) P ( Y = c k ) ] \begin{aligned} P(Y=c_k |X=x) &= \frac{P(Y=c_k, X=x)}{P(X=x)} \\ &=\frac{P(X=x|Y=c_k)P(Y=c_k)}{P(X=x)} \\ &=\frac{P(X=x^1, x^2, ... ,x^m|Y=c_k)P(Y=c_k)}{\sum_K\bigg[P(X=x|Y=c_k)P(Y=c_k)\bigg]} \end{aligned} P(Y=ck∣X=x)=P(X=x)P(Y=ck,X=x)=P(X=x)P(X=x∣Y=ck)P(Y=ck)=∑K[P(X=x∣Y=ck)P(Y=ck)]P(X=x1,x2,...,xm∣Y=ck)P(Y=ck)
由于上式的分母在每一类的计算中值都是相同的, 故最大化后验概率的式子就等价于:
P ( X = x 1 , x 2 , . . . , x m ∣ Y = c k ) P ( Y = c k ) P(X=x^1, x^2, ... ,x^m|Y=c_k)P(Y=c_k) P(X=x1,x2,...,xm∣Y=ck)P(Y=ck)
对于上面的式子, 假设前半部分 P ( X = x 1 , x 2 , . . . , x m ∣ Y = c k ) P(X=x^1, x^2, ... ,x^m|Y=c_k) P(X=x1,x2,...,xm∣Y=ck)中 x x x的每个特征 x m x^m xm有 S m S_m Sm个取值, m = 1 , 2 , . . , M m = 1, 2,..,M m=1,2,..,M, Y Y Y的取值有 k k k个, 那么分子的参数量为: k ∏ m = 1 M S m k\prod_{m=1}^MS_m k∏m=1MSm, 是指数级数据量,在实际估计中是不现实的.因而朴素贝叶斯法作了条件独立性假设. 即 x x x各个特征之间相互独立:
P ( X = x 1 , x 2 , . . . , x m ∣ Y = c k ) = ∏ m = 1 M P ( x m ∣ Y = c k ) P(X=x^1, x^2, ... ,x^m|Y=c_k) = \prod_{m=1}^M P(x^m|Y=c_k) P(X=x1,x2,...,xm∣Y=ck)=m=1∏MP(xm∣Y=ck)
所以最终有了分类器:
y = arg max c k ∏ m = 1 M P ( x m ∣ Y = c k ) P ( Y = c k ) y = \arg \max_{c_k} \prod_{m=1}^M P(x^m|Y=c_k)P(Y=c_k) y=argckmaxm=1∏MP(xm∣Y=ck)P(Y=ck) -
有了分类器之后我们就要对其进行计算, 即对式中先验概率和条件概率进行估计
对于这两个式子的估计, 可以使用极大似然估计和贝叶斯估计进行计算.
(不进行具体推到, 可参考:朴素贝叶斯中的极大似然估计)
这里直接给出公式:
极大似然估计 : 先验概率 : P ( Y = c k ) = 第 k 类的样本数目 样本总数目 条件概率 : P ( x m ∣ Y = c k ) = 第 k 类中 x 的第 m 个特征等于某值的样本数目 第 k 类的样本数目 贝叶斯估计 : 先验概率 : P ( Y = c k ) = 第 k 类的样本数目 + λ 样本总数目 + K λ 条件概率 : P ( x m ∣ Y = c k ) = 第 k 类中 x 的第 m 个特征等于某值的样本数目 + λ 第 k 类的样本数目 + S m λ ( S m 就是 x 第 m 个维度的可能取值个数 ) \begin{aligned} 极大似然估计&:\\ 先验概率&:P(Y=c_k) = \frac{第k类的样本数目}{样本总数目}\\ 条件概率&:P(x^m|Y=c_k) = \frac{第k类中x的第m个特征等于某值的样本数目}{第k类的样本数目}\\ 贝叶斯估计&:\\ 先验概率&:P(Y=c_k) = \frac{第k类的样本数目+\lambda}{样本总数目+K\lambda}\\ 条件概率&:P(x^m|Y=c_k) = \frac{第k类中x的第m个特征等于某值的样本数目+\lambda}{第k类的样本数目+S_m\lambda} \\ &(S_m就是x第m个维度的可能取值个数) \\ \end{aligned} 极大似然估计先验概率条件概率贝叶斯估计先验概率条件概率::P(Y=ck)=样本总数目第k类的样本数目:P(xm∣Y=ck)=第k类的样本数目第k类中x的第m个特征等于某值的样本数目::P(Y=ck)=样本总数目+Kλ第k类的样本数目+λ:P(xm∣Y=ck)=第k类的样本数目+Smλ第k类中x的第m个特征等于某值的样本数目+λ(Sm就是x第m个维度的可能取值个数)
在极大似然估计中, 当样本的某个特征的值没在训练样本中出现时, 就会导致条件概率的分子为0, 从而使得条件概率为0,
故贝叶斯估计在其分子分母上都加上平滑项:- 在先验概率中, 相当于给每一类多加上 λ \lambda λ个样本
- 在条件概率中, 相当于给样本的每个维度的不同取值都加上 λ \lambda λ个样本
2. 代码实现
下面进行高斯, 多项式和伯努利贝叶斯算法的简单介绍和实现
class NaiveBayes():
def __init__(self, _type, _lambda=1) -> None:
assert _type in ['Polynomial','bernoulli', 'gaussian']
self._lambda = _lambda # 平滑因子, 一般为1
self._type = _type # 模型类型: poly多项式, bernoulli:伯努利, gaussian:高斯
self.trian_data = None
self.train_labels = None
(1). 先验概率
三个模型的先验概率的计算公式都相同, 故在一个函数中进行计算
公式:
P
(
Y
=
c
k
)
=
第
k
类的样本数目
样本总数目
P(Y=c_k) = \frac{第k类的样本数目}{样本总数目}
P(Y=ck)=样本总数目第k类的样本数目
def train(self, train_data, train_labels):
"""计算先验概率"""
self.trian_data = train_data # (5536, 12345): 5536个样本, 12345个特征
self.train_labels = train_labels # (5536,)
if self._type == 'bernoulli':
self.train_data = (self.train_data > 0).astype(int)
self.classes, self.class_count = np.unique(self.train_labels, return_counts=True)
# self.classes: 类别[0, 1], 0表示差评, 1表示好评
# self.class_count: 每个类别出现的次数[2640, 2896]: 差评有 2640 个, 好评有 2896 个
# 先验概率: [0.47688696 0.52311304]
self.prior_probs = (self.class_count + self._lambda) / (len(self.trian_data) + len(self.classes) * self._lambda)
(2). 后验概率与预测
y = ∏ m = 1 M P ( x m ∣ Y = c k ) P ( Y = c k ) log y = ∑ m = 1 M log P ( x m ∣ Y = c k ) + log P ( Y = c k ) y = \prod_{m=1}^M P(x^m|Y=c_k)P(Y=c_k)\\ \log y = \sum_{m=1}^M\log P(x^m|Y=c_k) + \log P(Y=c_k) y=m=1∏MP(xm∣Y=ck)P(Y=ck)logy=m=1∑MlogP(xm∣Y=ck)+logP(Y=ck)
def predect(self, test_data):
"""计算后验概率"""
if self._type == 'bernoulli':
likelihood_probs, negative_likelihood_probs = self.get_Bernoulli_likelihood_probs()
log_probs = np.dot(test_data, np.transpose(likelihood_probs)) + np.dot(1 - test_data, np.transpose(negative_likelihood_probs)) + np.log(self.prior_probs)
elif self._type == 'Polynomial':
likelihood_probs = self.get_Polynomial_likelihood_probs()
log_probs = np.dot(test_data, np.transpose(likelihood_probs)) + np.log(self.prior_probs)
elif self._type == 'gaussian':
likelihood_probs = self.get_Gaussian_likelihood_probs(test_data)
log_probs = likelihood_probs + np.log(self.prior_probs.reshape(1, -1))
predects = self.classes[np.argmax(log_probs, axis=1)]
return predects
1). 伯努利模型
对于样本取值是离散值, 并且样本每个特征取值只能为0或1(如在文本分类中就是某单词出现或不出现)的情况, 就可以用伯努利模型
故而在前面的代码中
if self._type == 'bernoulli':
self.trian_data = (self.trian_data > 0).astype(int)
将所有样本的特征值中大于0(即出现次数不为0)的值置为1
也正是因为伯努利模型中的特征值只有两种情况, 故在后验概率的计算中也需要考虑特征不出现的情况:
特征值
x
m
为
1
:
P
(
x
m
=
1
∣
Y
=
c
k
)
=
第
k
类中
x
的第
m
个特征等于
1
的样本数目
+
λ
第
k
类的样本数目
+
S
m
λ
特征值
x
m
不为
1
:
P
(
x
m
=
0
∣
Y
=
c
k
)
=
1
−
P
(
x
m
=
1
∣
Y
=
c
k
)
对数后验概率
:
log
y
=
∑
m
=
1
M
log
P
(
x
m
=
1
∣
Y
=
c
k
)
+
∑
m
=
0
M
log
P
(
x
m
=
1
∣
Y
=
c
k
)
+
log
P
(
Y
=
c
k
)
\begin{aligned} 特征值x^m为1&: P(x^m=1|Y=c_k) = \frac{第k类中x的第m个特征等于1的样本数目+\lambda}{第k类的样本数目+S_m\lambda}\\ \\ 特征值x^m不为1&: P(x^m=0|Y=c_k) = 1-P(x^m=1|Y=c_k) \\ 对数后验概率:\log y &= \sum_{m=1}^M\log P(x^m=1|Y=c_k) + \sum_{m=0}^M\log P(x^m=1|Y=c_k) + \log P(Y=c_k) \end{aligned}
特征值xm为1特征值xm不为1对数后验概率:logy:P(xm=1∣Y=ck)=第k类的样本数目+Smλ第k类中x的第m个特征等于1的样本数目+λ:P(xm=0∣Y=ck)=1−P(xm=1∣Y=ck)=m=1∑MlogP(xm=1∣Y=ck)+m=0∑MlogP(xm=1∣Y=ck)+logP(Y=ck)
def get_Bernoulli_likelihood_probs(self):
"""计算伯努利模型的条件概率"""
likelihood_probs = []
# 得到特征x^m值为 1 的条件概率
for y in self.classes:
temp = self.train_data[self.train_labels == y] # 取出所有标签为 y 的样本
likelihood_probs.append(
(np.sum(temp, axis=0) + self._lambda) / (len(temp) + 2 * self._lambda)
# (得到值为 1 的样本数 + lambda) / (样本数 + 2 * lambda)
)
likelihood_probs = np.array(likelihood_probs)
# 得到特征x^m值为 0 的条件概率
negative_likelihood_probs = np.log2(1 - likelihood_probs)
# 这里对概率取对数是因为 可以将乘法变成加法, 方便后续的计算
return (np.log2(likelihood_probs), negative_likelihood_probs)
计算出后验概率:
log_probs = np.dot(test_data, np.transpose(likelihood_probs)) + \
np.dot(1-test_data, np.transpose(negative_likelihood_probs)) + self.prior_probs
上面的代码就是下面图片中的过程
2). 多项式模型
当特征是离散的时候,可选择使用使用多项式模型
P ( x m ∣ y = c k ) = N k m + λ N k + λ ⋅ M P(x^m|y=c_k)=\frac{N^m_{k}+\lambda}{N_k+\lambda \cdot M} P(xm∣y=ck)=Nk+λ⋅MNkm+λ
- N k m N_{km} Nkm是在类别 c k c_k ck中, 特征 x m x^m xm出现的次数
- N k N_k Nk是在类别 c k c_k ck中,所有特征出现的总次数
- M M M是特征总数
def get_Polynomial_likelihood_probs(self):
likelihood_probs = []
for y in self.classes:
temp = self.train_data[self.train_labels == y]
likelihood_probs.append((np.sum(temp, axis=0) + self._lambda)/(np.sum(temp) + temp.shape[1] * self._lambda))
return np.array(np.log2(likelihood_probs))
3). 高斯模型
条件概率 : P ( x m ∣ c k ) = 1 2 π σ m k 2 exp ( − ( x m − μ m k ) 2 2 σ m k 2 ) 对数后验概率 : ∑ m = 1 M log P ( x m ∣ Y = c k ) + log P ( Y = c k ) 条件概率:P(x^m\mid c_k)=\frac1{\sqrt{2\pi\sigma_{mk}^2}}\exp\left(-\frac{(x^m-\mu_{mk})^2}{2\sigma_{mk}^2}\right) \\ 对数后验概率:\sum_{m=1}^M\log P(x^m|Y=c_k) + \log P(Y=c_k) 条件概率:P(xm∣ck)=2πσmk21exp(−2σmk2(xm−μmk)2)对数后验概率:m=1∑MlogP(xm∣Y=ck)+logP(Y=ck)
- σ m k \sigma_{mk} σmk是第k个类别下样本的第m个特征的标准差
- μ m k \mu_{mk} μmk是第k个类别下样本的第m个特征的均值
def get_avgs_and_stds(self):
"""得到不同类别下每个特征的均值和方差"""
_, m = self.train_data.shape
avgs = np.zeros((len(self.classes), m)) # (类别, 特征数量)
stds = np.zeros((len(self.classes), m))
for i, y in enumerate(self.classes):
temp = self.train_data[self.train_labels == y]
avgs[i] = np.mean(temp, axis=0)
stds[i] = np.std(temp, axis=0) + 1e-9 # 添加上极小值, 防止除0
return avgs, stds
得到方差和均值后带入条件概率进行计算
def get_Gaussian_likelihood_probs(self, test_data):
avgs, stds = self.get_avgs_and_stds()
likelihood_probs = [] # 最后得到的形状: (类别数量, 样本数量)
for i in self.classes:
# 将上面条件概率的式子通过 log 转换得到
likelihood_prob = np.sum(
-0.5 * np.log(2*np.pi*np.square(stds[i])) - 0.5 * np.square(test_data-avgs[i])/np.square(stds[i]),
axis=1
) # 形状:(样本数量,)
likelihood_probs.append(likelihood_prob)
return np.transpose(likelihood_probs) # 转置后形状变为(样本数量, 类别数量)
计算出后验概率:
log_probs = likelihood_probs + np.log(self.prior_probs.reshape(1, -1))
4).得到预测概率
predects = self.classes[np.argmax(log_probs, axis=1)]
将每一行中概率较大的那个值的标签作为预测标签
3. 自己代码和sklearn
代码实战比较
1. 自己的代码
import numpy as np
import pandas as pd
import re
from sklearn.feature_extraction.text import CountVectorizer # 词频统计
def load_text_cla_corpus(path):
'''
读取文本分类数据集
'''
df = pd.read_csv(path, sep='\t')
texts, labels = [],[] # 文本和对应类别
for line in df.itertuples():
text = re.sub(r"([,.!?])", r" \1 ", line.text)
text = re.sub(" {2,}", " ", text)
texts.append(text)
labels.append(int(line.target))
return np.array(texts), np.array(labels)
def shuffle_split_dataset(features, labels, random_seed, split_ratio):
np.random.seed(random_seed) # 设置随机数种子
# 随机打乱数据
shuffle_indices = np.random.permutation(features.shape[0])
features = features[shuffle_indices]
labels = labels[shuffle_indices]
# 划分训练、测试集
train_count = int(len(features)*split_ratio)
train_data, train_labels = features[:train_count], labels[:train_count]
test_data, test_labels = features[train_count:], labels[train_count:]
return (train_data, train_labels, test_data, test_labels)
# 读取数据
texts, labels = load_text_cla_corpus('E:\MyFile\DL\乱七八糟\书籍资料\统计学习方法\Basic4AI-master\Data\TextClassification\datasets.tsv')
# 打乱和划分数据
return_data = shuffle_split_dataset(texts, labels, 0, 0.8)
train_data, train_labels, test_data, test_labels = return_data
# 将文本转换为代表出现频率的数值特征
def get_feature_matrix(train_data, test_data, binary=False):
vectorizer = CountVectorizer(binary=binary)
train_xs = vectorizer.fit_transform(train_data).toarray()
test_xs = vectorizer.transform(test_data).toarray()
return train_xs, test_xs
train_data, test_data = get_feature_matrix(train_data, test_data)
class NaiveBayes():
def __init__(self, _type, _lambda=1) -> None:
assert _type in ['Polynomial','bernoulli', 'gaussian']
self._lambda = _lambda # 平滑因子, 一般为1
self._type = _type # 模型类型: poly多项式, bernoulli:伯努利, gaussian:高斯
self.train_data = None
self.train_labels = None
def train(self, train_data, train_labels):
"""计算先验概率"""
self.train_data = train_data # (5536, 12345): 5536个样本, 12345个特征
self.train_labels = train_labels # (5536,)
if self._type != 'gaussian':
self.train_data = (self.train_data > 0).astype(int)
self.classes, self.class_count = np.unique(self.train_labels, return_counts=True)
# self.classes: 类别[0, 1], 0表示差评, 1表示好评
# self.class_count: 每个类别出现的次数[2640, 2896]: 差评有 2640 个, 好评有 2896 个
# 先验概率: [0.47688696 0.52311304]
self.prior_probs = (self.class_count + self._lambda) / (len(self.train_data) + len(self.classes) * self._lambda)
def predect(self, test_data):
"""计算后验概率"""
if self._type == 'bernoulli': # 伯努利模型
likelihood_probs, negative_likelihood_probs = self.get_Bernoulli_likelihood_probs()
log_probs = np.dot(test_data, np.transpose(likelihood_probs)) + np.dot(1 - test_data, np.transpose(negative_likelihood_probs)) + np.log(self.prior_probs)
elif self._type == 'Polynomial': # 多项式模型
likelihood_probs = self.get_Polynomial_likelihood_probs()
log_probs = np.dot(test_data, np.transpose(likelihood_probs)) + np.log(self.prior_probs)
elif self._type == 'gaussian': # 高斯模型
likelihood_probs = self.get_Gaussian_likelihood_probs(test_data)
log_probs = likelihood_probs + np.log(self.prior_probs.reshape(1, -1))
predects = self.classes[np.argmax(log_probs, axis=1)]
return predects
def get_avgs_and_stds(self):
"""得到不同类别下每个特征的均值和方差"""
_, m = self.train_data.shape
avgs = np.zeros((len(self.classes), m)) # (类别, 特征数量)
stds = np.zeros((len(self.classes), m))
for i, y in enumerate(self.classes):
temp = self.train_data[self.train_labels == y]
avgs[i] = np.mean(temp, axis=0)
stds[i] = np.std(temp, axis=0) + 1e-9 # 添加上极小值, 防止除0
return avgs, stds
def get_Gaussian_likelihood_probs(self, test_data):
avgs, stds = self.get_avgs_and_stds()
likelihood_probs = [] # 最后得到的形状: (类别数量, 样本数量)
for i in self.classes:
# 将上面条件概率的式子通过 log 转换得到
likelihood_prob = np.sum(
-0.5 * np.log(2*np.pi*np.square(stds[i])) - 0.5 * np.square(test_data-avgs[i])/np.square(stds[i]),
axis=1
) # 形状:(样本数量,)
likelihood_probs.append(likelihood_prob)
return np.transpose(likelihood_probs) # 转置后形状变为(样本数量, 类别数量)
def get_Polynomial_likelihood_probs(self):
likelihood_probs = []
for y in self.classes:
temp = self.train_data[self.train_labels == y]
prob = (np.sum(temp, axis=0) + self._lambda) / (np.sum(temp) + temp.shape[1] * self._lambda)
likelihood_probs.append(np.log(prob))
return np.array(likelihood_probs)
def get_Bernoulli_likelihood_probs(self):
"""计算伯努利模型的条件概率"""
likelihood_probs = []
# 得到特征x^m值为 1 的条件概率
for y in self.classes:
temp = self.train_data[self.train_labels == y] # 取出所有标签为 y 的样本
likelihood_probs.append(
(np.sum(temp, axis=0) + self._lambda) / (len(temp) + 2 * self._lambda)
# (得到值为 1 的样本数 + lambda) / (样本数 + 2 * lambda)
)
likelihood_probs = np.array(likelihood_probs)
# 得到特征x^m值为 0 的条件概率
negative_likelihood_probs = np.log2(1 - likelihood_probs)
# 这里对概率取对数是因为 可以将乘法变成加法, 方便后续的计算
return (np.log2(likelihood_probs), negative_likelihood_probs)
for i in ['Polynomial', 'bernoulli', 'gaussian']:
naive_bayes = NaiveBayes(i)
naive_bayes.train(train_data, train_labels)
predect = naive_bayes.predect(test_data)
acc = sum(predect == test_labels) / len(test_labels)
print(f"{i}: {acc}")
"""
输出:
Polynomial: 0.7854046242774566
bernoulli: 0.7817919075144508
gaussian: 0.6517341040462428
"""
2.sklearn
代码
from sklearn.naive_bayes import GaussianNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.naive_bayes import BernoulliNB
def sk_learn_naive_bayes(model, test_data, test_labels):
model.fit(train_data,train_labels)
acc = model.score(test_data, test_labels)
return acc
gauss_nb = GaussianNB()
Multinomial_NB = MultinomialNB()
Bernoulli_NB = BernoulliNB()
for name, model in {"gauss_nb":gauss_nb, "Multinomial_NB":Multinomial_NB, "Bernoulli_NB":Bernoulli_NB}.items():
acc = sk_learn_naive_bayes(model, test_data, test_labels)
print(f"{name}: {acc}")
"""
输出:
Polynomial: 0.7839595375722543
bernoulli: 0.7832369942196532
gaussian: 0.666907514450867
"""
4. 结语
- 这篇文章大体框架依旧是一位大佬的代码,我对其进行了解释, 然后添加了实现高斯朴素贝叶斯模型的代码和
sklearn
的相关代码 - 但是值得注意的是, 本文使用的数据集是一个文本分类数据集, 并不适用于高斯朴素贝叶斯模型(适合数据连续分布的数据集), 从得到的准确率可以看出.
- 如果你需要数据集可以评论留下邮箱, 我会尽快发给你
- 如有发现错误, 请狠狠的告诉我! 感谢阅读.