朴素贝叶斯算法是一种基于贝叶斯定理的有监督的机器学习算法,解决的是分类问题,如文本分类、垃圾邮件过滤、客户是否流失,是否值得投资、信用等级评定等领域,并在实际应用中表现出良好的性能。该算法简单易懂,学习效率高,在某些领域的分类问题中能够与决策树、神经网络等算法相媲美。但由于该算法以自变量之间的独立(假设特征之间相互独立)性和连续变量的正态性假设为前提,就会导致算法精度在某种程度上受影响。
下面是对朴素贝叶斯算法的详细阐述:
- 贝叶斯定理: 朴素贝叶斯算法基于贝叶斯定理进行概率推断。贝叶斯定理描述了在已知先验概率的情况下,如何根据新的证据来更新我们对事件发生概率的信念。
- 特征独立性假设: 朴素贝叶斯算法假设特征之间相互独立,即给定类别,特征之间的条件概率是相互独立的。虽然这个假设在实际中并不总是成立,但在许多情况下,这种简化可以带来较好的分类效果。
- 类别的先验概率: 在朴素贝叶斯算法中,需要计算每个类别的先验概率,即在没有任何信息的情况下,每个类别发生的概率。
- 特征的条件概率: 对于给定的类别,需要计算每个特征的条件概率,即在该类别下,每个特征取某个值的概率。这通常需要利用训练数据集来进行估计。
- 计算后验概率: 当有新的样本需要分类时,利用贝叶斯定理,可以计算出该样本属于每个类别的后验概率。最终选择具有最高后验概率的类别作为分类结果。
- 处理连续特征: 对于连续特征,可以使用概率密度函数来进行条件概率的估计,常见的方法包括高斯朴素贝叶斯和多项式朴素贝叶斯。
总体来说,朴素贝叶斯算法简单易懂,计算效率高,对小规模的数据集表现良好,但在处理特征之间相关性较强的情况下可能表现不佳。在实际应用中,朴素贝叶斯算法通常作为其他更复杂算法的基准进行对比,或者在特征独立性较强的问题上取得良好效果。
朴素贝叶斯理论
贝叶斯决策理论
朴素贝叶斯是贝叶斯决策理论的一部分,所以我们先了解一下贝叶斯决策理论。假设现在有两个类别, p 1 ( x , y ) p1(x, y) p1(x,y) 表示数据点 ( x , y ) (x, y) (x,y) 属于类别一的概率, p 2 ( x , y ) p2(x, y) p2(x,y) 表示数据点 ( x , y ) (x, y) (x,y) 属于类别二的概率,那么对于一个新的数据点 ( x , y ) (x, y) (x,y),我们就可以用下面的规则来判断它所属的类别:
- 如果 p 1 ( x , y ) > p 2 ( x , y ) p1(x, y) > p2(x, y) p1(x,y)>p2(x,y),那么判定 ( x , y ) (x, y) (x,y) 属于类别一
- 如果 p 1 ( x , y ) < p 2 ( x , y ) p1(x, y) < p2(x, y) p1(x,y)<p2(x,y),那么判定 ( x , y ) (x, y) (x,y) 属于类别二
也就是说,我们会选择最高概率对应的类别作为最终的分类结果,这就是贝叶斯决策理论的核心思想。
条件概率
条件概率指的是在事件 B B B 发生的情况下,事件 A A A 发生的概率,用 P ( A ∣ B ) P(A|B) P(A∣B) 来表示。
从上图可以清楚的得到条件概率
P
(
A
∣
B
)
P(A|B)
P(A∣B) 的计算公式:
P
(
A
∣
B
)
=
P
(
A
∩
B
)
P
(
B
)
P(A|B) = \frac{P(A \cap B)}{P(B)}
P(A∣B)=P(B)P(A∩B)
因此,
P
(
A
∩
B
)
=
P
(
A
∣
B
)
P
(
B
)
P(A \cap B) = P(A|B)P(B)
P(A∩B)=P(A∣B)P(B)
同理,
P
(
A
∩
B
)
=
P
(
B
∣
A
)
P
(
A
)
P(A \cap B) = P(B|A)P(A)
P(A∩B)=P(B∣A)P(A)
推理可得,
P
(
A
∣
B
)
P
(
B
)
=
P
(
B
∣
A
)
P
(
A
)
⟹
P
(
A
∣
B
)
=
P
(
B
∣
A
)
P
(
A
)
P
(
B
)
P(A|B)P(B) = P(B|A)P(A) \implies P(A|B) = \frac{P(B|A)P(A)}{P(B)}
P(A∣B)P(B)=P(B∣A)P(A)⟹P(A∣B)=P(B)P(B∣A)P(A)
这就是条件概率的计算公式。
全概率公式
假设样本空间 S S S,是两个事件 A A A 与 A ′ A' A′ 的总和,如下图所示:
事件 B B B 也可以划分成两个部分,如下图所示:
此时,有如下公式:
P
(
B
)
=
P
(
B
∩
A
)
+
P
(
B
∩
A
′
)
P(B) = P(B \cap A) + P(B \cap A')
P(B)=P(B∩A)+P(B∩A′)
因为,
P
(
B
∩
A
)
=
P
(
B
∣
A
)
P
(
A
)
P
(
B
∩
A
′
)
=
P
(
B
∣
A
′
)
P
(
A
′
)
P(B \cap A) = P(B|A)P(A) \\ P(B \cap A') = P(B|A')P(A')
P(B∩A)=P(B∣A)P(A)P(B∩A′)=P(B∣A′)P(A′)
因此有,
P
(
B
)
=
P
(
B
∣
A
)
P
(
A
)
+
P
(
B
∣
A
′
)
P
(
A
′
)
P(B) = P(B|A)P(A) + P(B|A')P(A')
P(B)=P(B∣A)P(A)+P(B∣A′)P(A′)
这就是全概率的计算公式。
我进一步详细阐述什么是全概率:
- 若 n n n 个事件两两互斥,且这 n n n 个事件的总和为 Ω \varOmega Ω,则称这 n n n 个事件组为完备事件组
- 在完备事件组下: A 1 + A 2 + A 3 + ⋅ ⋅ ⋅ + A n = Ω A_1 + A_2 + A_3+···+ A_n = \varOmega A1+A2+A3+⋅⋅⋅+An=Ω
- 此时,有一事件 X X X,要计算其发生概率,一般情况下无法直接求出,如果能找到一个伴随事件 X X X 发生的完备事件组,则能借助全概率公式求出事件 X X X 的发生概率
- P ( X ) = ∑ i = 1 n P ( X ∣ A i ) P ( A i ) P(X) = \displaystyle\sum_{i=1}^{n}P(X|A_i)P(A_i) P(X)=i=1∑nP(X∣Ai)P(Ai)
贝叶斯公式
全概率公式:已知事件 X X X 与伴随事件 X X X 发生的完备事件组( A 1 、 A 2 、 . . . 、 A n A_1、A_2、...、A_n A1、A2、...、An),根据完备事件组,求出事件 X X X 发生的概率。
贝叶斯公式:已知事件 X X X 与伴随事件 X X X 发生的完备事件组( A 1 、 A 2 、 . . . 、 A n A_1、A_2、...、A_n A1、A2、...、An),根据事件 X X X 发生的概率,求出完备事件组中某一事件 A i A_i Ai 发生的概率。
贝叶斯公式如下所示:
P
(
A
i
∣
X
)
=
P
(
A
i
∩
X
)
P
(
X
)
=
P
(
X
∣
A
i
)
P
(
A
i
)
∑
i
=
1
n
P
(
X
∣
A
i
)
P
(
A
i
)
P(A_i|X) = \frac{P(A_i \cap X)}{P(X)} = \frac{P(X|A_i)P(A_i)}{\displaystyle\sum_{i=1}^{n}P(X|A_i)P(A_i)}
P(Ai∣X)=P(X)P(Ai∩X)=i=1∑nP(X∣Ai)P(Ai)P(X∣Ai)P(Ai)
其中:
- P ( A i ) P(A_i) P(Ai) 叫做先验概率,即在事件 X X X 发生之前,我们对事件 A i A_i Ai 发生概率的一个判断
- P ( A i ∣ X ) P(A_i|X) P(Ai∣X) 叫做后验概率,即在事件 X X X 发生之后,我们对事件 A i A_i Ai 发生概率的一个重新评估;通俗点说,就是在样本空间为 X X X 的情况下, A i A_i Ai 所占的比重大小
- P ( X ∣ A i ) ∑ i = 1 n P ( X ∣ A i ) P ( A i ) = P ( X ∣ A i ) P ( X ) \frac{P(X|A_i)}{\displaystyle\sum_{i=1}^{n}P(X|A_i)P(A_i)} = \frac{P(X|A_i)}{P(X)} i=1∑nP(X∣Ai)P(Ai)P(X∣Ai)=P(X)P(X∣Ai) 叫做可能性函数,这是一个调整因子,使得预估概率更接近真实概率
条件概率可以理解成 后验概率 = 先验概率 × 调整因子
,这就是贝叶斯推断的含义。我们先预估一个“先验概率”,然后加入实验结果,看这个实验是增强还是削弱了“先验概率”,由此得到更接近事实的“后验概率”。
接下来举一个例子,来加深对贝叶斯推断的理解。假设有两个一模一样的碗,一号碗有 30 颗水果糖和 10 颗巧克力糖,二号碗有 20 颗水果糖和 20 颗巧克力糖,如下图所示。现在随机选择一个碗,从中摸出一颗糖,发现是水果糖,请问这颗水果糖来自一号碗的概率有多大?
我们假定, H 1 H_1 H1 表示一号碗, H 2 H_2 H2 表示二号碗,由于两个碗是一模一样的,因此 P ( H 1 ) = P ( H 2 ) = 0.5 P(H_1) = P(H_2) = 0.5 P(H1)=P(H2)=0.5,我们把这个概率就叫做“先验概率”,即在没有做实验之前,来自一号碗的概率是 0.5。
假定 E E E 表示水果糖,问题就变成了求条件概率 P ( H 1 ∣ E ) P(H_1|E) P(H1∣E),我们把这个概率叫做“后验概率”,即在事件 E E E 发生后,对 P ( H 1 ) P(H_1) P(H1) 的修正。
根据条件概率公式,得到:
P
(
H
1
∣
E
)
=
P
(
E
∣
H
1
)
P
(
H
1
)
P
(
E
)
=
P
(
E
∣
H
1
)
P
(
H
1
)
P
(
E
∣
H
1
)
P
(
H
1
)
+
P
(
E
∣
H
2
)
P
(
H
2
)
=
0.375
0.625
=
0.6
P(H_1|E) = \frac{P(E|H_1)P(H_1)}{P(E)} = \frac{P(E|H_1)P(H_1)}{P(E|H_1)P(H_1)+P(E|H_2)P(H_2)} = \frac{0.375}{0.625} = 0.6
P(H1∣E)=P(E)P(E∣H1)P(H1)=P(E∣H1)P(H1)+P(E∣H2)P(H2)P(E∣H1)P(H1)=0.6250.375=0.6
计算结果表示这颗水果糖来自一号碗的概率为 0.6。也就是说,取出水果糖后,事件
H
1
H_1
H1 发生的可能性得到了增强。
这里进一步思考一点,在使用该方法时,如果不需要知道具体的类别概率,而只需知道所属类别 H 1 H_1 H1 或 H 2 H_2 H2,那我们就没必要计算事件 E E E 的全概率,只需比较 P ( H 1 ∣ E ) P(H_1|E) P(H1∣E) 和 P ( H 2 ∣ E ) P(H_2|E) P(H2∣E) 的大小即可。
朴素贝叶斯
朴素贝叶斯与贝叶斯是两个不同的概念。贝叶斯算法是一种基于贝叶斯定理进行概率推断的统计学方法。贝叶斯算法通过利用先验概率和样本数据得到后验概率,从而对未知参数或未来事件进行推断。朴素贝叶斯算法是贝叶斯算法家族中的一员,它是基于贝叶斯定理和特征独立性假设的一种分类算法。与一般的贝叶斯算法相比,朴素贝叶斯算法做出了特征独立性的假设,简化了条件概率的计算,使得算法更加高效,并且适用于大规模的数据集。
朴素贝叶斯公式如下所示:
P
(
y
∣
X
)
=
P
(
X
∣
y
)
P
(
y
)
P
(
X
)
=
P
(
x
1
,
x
2
,
.
.
.
,
x
n
∣
y
)
P
(
y
)
P
(
x
1
,
x
2
,
.
.
.
,
x
n
)
=
P
(
x
1
∣
y
)
P
(
x
2
∣
y
)
⋅
⋅
⋅
P
(
x
n
∣
y
)
P
(
y
)
P
(
x
1
,
x
2
,
.
.
.
,
x
n
)
P(y|X) = \frac{P(X|y)P(y)}{P(X)}=\frac{P(x_1, x_2, ..., x_n|y)P(y)}{P(x_1, x_2, ..., x_n)}=\frac{P(x_1|y)P(x_2|y)···P(x_n|y)P(y)}{P(x_1, x_2, ..., x_n)}
P(y∣X)=P(X)P(X∣y)P(y)=P(x1,x2,...,xn)P(x1,x2,...,xn∣y)P(y)=P(x1,x2,...,xn)P(x1∣y)P(x2∣y)⋅⋅⋅P(xn∣y)P(y)
其中,
P
(
x
1
∣
y
)
、
P
(
x
2
∣
y
)
、
.
.
.
P(x_1|y)、P(x_2|y)、...
P(x1∣y)、P(x2∣y)、... 分别表示在类别为
y
y
y 的条件下,特征
x
1
,
x
2
,
.
.
.
,
x
n
x_1, x_2, ..., x_n
x1,x2,...,xn 的概率(占比)。
朴素贝叶斯算法通过计算每个类别的后验概率,并选择具有最高后验概率的类别作为分类结果。为了进行分类,需要事先估计先验概率和条件概率,通常通过训练数据集来进行参数估计。
接下来举一个例子,进一步理解朴素贝叶斯推断。某医院上午来了六个门诊病人,情况如下表所示:
症状 | 职业 | 疾病 |
---|---|---|
打喷嚏 | 护士 | 感冒 |
打喷嚏 | 农夫 | 过敏 |
头痛 | 建筑工人 | 脑震荡 |
头痛 | 建筑工人 | 感冒 |
打喷嚏 | 教师 | 感冒 |
头痛 | 教师 | 脑震荡 |
现在来了第七个病人,是一个打喷嚏的建筑工人,请问他患上感冒的概率有多大?
根据贝叶斯定理:
P
(
y
∣
X
)
=
P
(
X
∣
y
)
P
(
y
)
P
(
X
)
P(y|X) = \frac{P(X|y)P(y)}{P(X)}
P(y∣X)=P(X)P(X∣y)P(y)
推理可得,
P
(
感冒
∣
打喷嚏
,
建筑工人
)
=
P
(
打喷嚏
,
建筑工人
∣
感冒
)
P
(
感冒
)
P
(
打喷嚏
,
建筑工人
)
P(感冒|打喷嚏, 建筑工人)=\frac{P(打喷嚏, 建筑工人|感冒)P(感冒)}{P(打喷嚏, 建筑工人)}
P(感冒∣打喷嚏,建筑工人)=P(打喷嚏,建筑工人)P(打喷嚏,建筑工人∣感冒)P(感冒)
根据朴素贝叶斯的特征独立性假设可知,“打喷嚏”和“建筑工人”这两个特征是相互独立的,因此上述公式可写成如下:
P
(
感冒
∣
打喷嚏
,
建筑工人
)
=
P
(
打喷嚏
∣
感冒
)
P
(
建筑工人
∣
感冒
)
P
(
感冒
)
P
(
打喷嚏
)
P
(
建筑工人
)
=
0.66
×
0.33
×
0.5
0.5
×
0.33
=
0.66
P(感冒|打喷嚏, 建筑工人)=\frac{P(打喷嚏|感冒)P(建筑工人|感冒)P(感冒)}{P(打喷嚏)P(建筑工人)}=\frac{0.66×0.33×0.5}{0.5×0.33}=0.66
P(感冒∣打喷嚏,建筑工人)=P(打喷嚏)P(建筑工人)P(打喷嚏∣感冒)P(建筑工人∣感冒)P(感冒)=0.5×0.330.66×0.33×0.5=0.66
计算结果表示这个打喷嚏的建筑工人有 66% 的概率患上感冒,同理可计算这个人患上过敏和脑震荡的概率,比较这几个概率,就可以推测他最可能患上的疾病类别。
言论屏蔽
以在线社区留言为例,为了营造一个健康发展的社区,我们需要屏蔽一些带侮辱性的言论,如果某条留言使用了负面或侮辱性的词汇,我们就将其标记为内容不当。我们使用 1 和 2 分别表示内容不当和内容得当。
完整的代码如下:
import numpy as np
from functools import reduce
# 读取数据
def read_dataset() -> (list, list):
"""
:return: 返回样本数据集和样本标签
"""
# 将留言进行单词切分,并转换成词向量
samples = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
['stop', 'posting', 'stupid', 'worthless', 'garbage'],
['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
# 各样本对应标签,1 代表内容不当,2 代表内容得当
labels = [2, 1, 2, 1, 2, 1]
return samples, labels
# 依据数据集创建词汇表
def create_vocabulary(dataset: list) -> list:
"""
:param dataset: 样本数据集
:return: 数据集中出现的所有词汇集合,以列表形式返回
"""
vocab_set = set([]) # 创建一个空集
for sample in dataset:
vocab_set = vocab_set | set(sample) # 取并集
return list(vocab_set)
# 用词汇表的稀疏向量形式来表示每个词向量样本
def vocab_vector_to_vocabulary_vector(vocabulary: list, sample: list) -> list:
"""
:param vocabulary: 词汇表
:param sample: 样本数据集中的一个样本
:return: 词汇表的稀疏向量
"""
vocabulary_vector = [0] * len(vocabulary) # 元素个数与词汇表 vocabulary 一致
for vocab in sample:
if vocab in vocabulary: # 如果该词汇出现在词汇表中,则将 vocabulary_vector 对应位置的值置为 1
vocabulary_vector[vocabulary.index(vocab)] = 1
else:
print(f"{vocab} is not in vocabulary!")
return vocabulary_vector
# 训练朴素贝叶斯分类器
def train_naive_bayes_classifier(train_mat: list, train_labels: list) -> (np.ndarray, np.ndarray, float):
"""
:param train_mat: 训练样本数据,都已转成词汇表的稀疏向量形式
:param train_labels: 训练样本数据的对应标签
:return: 返回在类别 1 和 2 情况下各个词汇出现的概率以及类别 1 占样本集的概率
"""
num_samples = len(train_mat) # 训练样本数;6
num_vocabs = len(train_mat[0]) # 每个样本向量含有多少个元素;32
p_1 = float(sum([1 for label in train_labels if label == 1]) / num_samples) # 在训练集中,内容不当的概率
p_1_vocab = np.zeros(num_vocabs) # 当样本标签为 1 时,每个词出现的次数
p_2_vocab = np.zeros(num_vocabs) # 当样本标签为 2 时,每个词出现的次数
p_1_vocabs = 0.0 # 当样本标签为 1 时,所有词出现的次数和
p_2_vocabs = 0.0 # 当样本标签为 2 时,所有词出现的次数和
for i in range(num_samples):
if train_labels[i] == 1:
p_1_vocab += train_mat[i]
p_1_vocabs += sum(train_mat[i])
else:
p_2_vocab += train_mat[i]
p_2_vocabs += sum(train_mat[i])
p_1_vector = p_1_vocab / p_1_vocabs # 在类别 1 的情况下,各个词出现的概率;P(w1|1)、P(w2|1)、P(w3|1)、...
p_2_vector = p_2_vocab / p_2_vocabs # 在类别 2 的情况下,各个词出现的概率;P(w1|2)、P(w2|2)、P(w3|2)、...
return p_1_vector, p_2_vector, p_1
# 预测分类结果
def predict(predict_data: np.ndarray, p_1_vector: np.ndarray, p_2_vector: np.ndarray, p_1: float) -> int:
"""
:param predict_data: 预测数据,已转成词汇表的稀疏向量形式
:param p_1_vector: 在类别 1 的情况下,各个词出现的条件概率
:param p_2_vector: 在类别 2 的情况下,各个词出现的条件概率
:param p_1: 类别为 1 的先验概率
:return: 预测的类别
"""
p1 = reduce(lambda x, y: x * y, predict_data * p_1_vector) * p_1 # 类别为 1 的概率
p2 = reduce(lambda x, y: x * y, predict_data * p_2_vector) * (1 - p_1) # 类别为 2 的概率
print('p1:', p1)
print('p2:', p2)
if p1 > p2:
return 1
else:
return 2
if __name__ == '__main__':
# 获取样本数据和对应标签
samples, labels = read_dataset()
# 获取词汇表
vocabulary = create_vocabulary(samples)
# 获取训练的样本数值向量
train_mat = []
for sample in samples:
train_mat.append(vocab_vector_to_vocabulary_vector(vocabulary, sample))
# 获取在类别 1 和 2 情况下各个词汇出现的概率以及类别 1 占样本集的概率
# p_1_vector 中存放的是各个单词在类别 1 情况下出现的条件概率
# p_2_vector 中存放的是各个单词在类别 2 情况下出现的条件概率
# p_1 就是类别为 1 的先验概率
p_1_vector, p_2_vector, p_1 = train_naive_bayes_classifier(train_mat, labels)
# 测试
predict_data = ['love', 'my', 'dalmation'] # 预测样本
predict_data_vector = np.array(vocab_vector_to_vocabulary_vector(vocabulary, predict_data)) # 将预测样本转成词汇表的稀疏向量
result = predict(predict_data_vector, p_1_vector, p_2_vector, p_1)
if result == 1:
print(f'{predict_data} 属于内容不当')
else:
print(f'{predict_data} 属于内容得当')
"""
P(1|X)P(X)=P(X|1)P(1) => P(1|(w1, w2, ..., w32))P(w1, w2, ..., w32)=P((w1, w2, ..., w32)|1)P(1)=P(w1|1)P(w2|1)···P(w32|1)P(1)
P(2|X)P(X)=P(X|2)P(2) => P(2|(w1, w2, ..., w32))P(w1, w2, ..., w32)=P((w1, w2, ..., w32)|2)P(2)=P(w1|2)P(w2|2)···P(w32|2)P(2)
比较 P(1|X) 与 P(2|X) 的大小,将更大值所属类别作为最终的分类结果
"""
---------
p1: 0.0
p2: 0.0
['love', 'my', 'dalmation'] 属于内容得当
上述代码中存在一个问题,就是在计算 P ( 1 ∣ X ) P ( X ) = P ( w 1 ∣ 1 ) P ( w 2 ∣ 1 ) ⋅ ⋅ ⋅ P ( w 32 ∣ 1 ) P ( 1 ) P(1|X)P(X) = P(w1|1)P(w2|1)···P(w32|1)P(1) P(1∣X)P(X)=P(w1∣1)P(w2∣1)⋅⋅⋅P(w32∣1)P(1) 时,只要其中有一个 P ( w i ∣ 1 ) = 0 P(wi|1) = 0 P(wi∣1)=0,其最后的概率值都会等于 0,这显然不是我们想要的结果。
为了解决上述问题,我们可以将各单词的出现次数初始化为 1,将所有单词出现的总次数初始化为 2,这种做法就叫做拉普拉斯平滑(Laplace Smoothing),又称之为加 1 平滑,是比较常用的平滑方法,可以解决 0 概率问题。
除此之外,还有一个下溢出的问题,这是由于很多个很小的数相乘导致的。两个小于 1 的数值相乘,其结果将比两个数值中的任何一个都小,当很多个小于 1 的数值相乘时,结果会非常小,此时若对其进行四舍五入,计算结果很可能就变成 0 了。为了解决这个问题,我们可以对乘积结果取自然对数,通过求对数可以避免下溢出或浮点数舍入导致的错误。同时,采用自然对数进行处理不会产生什么损失。
函数 f ( x ) f(x) f(x) 与 l n f ( x ) lnf(x) lnf(x) 的曲线如下图所示:
如上图所示,函数 f ( x ) f(x) f(x) 与 l n f ( x ) lnf(x) lnf(x) 在相同区域内同增同减,并在相同点上取到极值,基于这些特性,在某些问题上可以使用 l n f ( x ) lnf(x) lnf(x) 替代 f ( x ) f(x) f(x) 进行相应处理。
修改后的代码如下:
import numpy as np
# 读取数据
def read_dataset() -> (list, list):
"""
:return: 返回样本数据集和样本标签
"""
# 将留言进行单词切分,并转换成词向量
samples = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
['stop', 'posting', 'stupid', 'worthless', 'garbage'],
['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
# 各样本对应标签,1 代表内容不当,2 代表内容得当
labels = [2, 1, 2, 1, 2, 1]
return samples, labels
# 依据数据集创建词汇表
def create_vocabulary(dataset: list) -> list:
"""
:param dataset: 样本数据集
:return: 数据集中出现的所有词汇集合,以列表形式返回
"""
vocab_set = set([]) # 创建一个空集
for sample in dataset:
vocab_set = vocab_set | set(sample) # 取并集
return list(vocab_set)
# 用词汇表的稀疏向量形式来表示每个词向量样本
def vocab_vector_to_vocabulary_vector(vocabulary: list, sample: list) -> list:
"""
:param vocabulary: 词汇表
:param sample: 样本数据集中的一个样本
:return: 词汇表的稀疏向量
"""
vocabulary_vector = [0] * len(vocabulary) # 元素个数与词汇表 vocabulary 一致
for vocab in sample:
if vocab in vocabulary: # 如果该词汇出现在词汇表中,则将 vocabulary_vector 对应位置的值置为 1
vocabulary_vector[vocabulary.index(vocab)] = 1
else:
print(f"{vocab} is not in vocabulary!")
return vocabulary_vector
# 训练朴素贝叶斯分类器
def train_naive_bayes_classifier(train_mat: list, train_labels: list) -> (np.ndarray, np.ndarray, float):
"""
:param train_mat: 训练样本数据,都已转成词汇表的稀疏向量形式
:param train_labels: 训练样本数据的对应标签
:return: 返回在类别 1 和 2 情况下各个词汇出现的概率以及类别 1 占样本集的概率
"""
num_samples = len(train_mat) # 训练样本数;6
num_vocabs = len(train_mat[0]) # 每个样本向量含有多少个元素;32
p_1 = float(sum([1 for label in train_labels if label == 1]) / num_samples) # 在训练集中,内容不当的概率
p_1_vocab = np.ones(num_vocabs) # 当样本标签为 1 时,每个词出现的次数
p_2_vocab = np.ones(num_vocabs) # 当样本标签为 2 时,每个词出现的次数
p_1_vocabs = 2.0 # 当样本标签为 1 时,所有词出现的次数和
p_2_vocabs = 2.0 # 当样本标签为 2 时,所有词出现的次数和
for i in range(num_samples):
if train_labels[i] == 1:
p_1_vocab += train_mat[i]
p_1_vocabs += sum(train_mat[i])
else:
p_2_vocab += train_mat[i]
p_2_vocabs += sum(train_mat[i])
p_1_vector = np.log(p_1_vocab / p_1_vocabs) # 在类别 1 的情况下,各个词出现的概率;P(w1|1)、P(w2|1)、P(w3|1)、...
p_2_vector = np.log(p_2_vocab / p_2_vocabs) # 在类别 2 的情况下,各个词出现的概率;P(w1|2)、P(w2|2)、P(w3|2)、...
return p_1_vector, p_2_vector, p_1
# 预测分类结果
def predict(predict_data: np.ndarray, p_1_vector: np.ndarray, p_2_vector: np.ndarray, p_1: float) -> int:
"""
:param predict_data: 预测数据,已转成词汇表的稀疏向量形式
:param p_1_vector: 在类别 1 的情况下,各个词出现的条件概率
:param p_2_vector: 在类别 2 的情况下,各个词出现的条件概率
:param p_1: 类别为 1 的先验概率
:return: 预测的类别
"""
p1 = sum(predict_data * p_1_vector) + np.log(p_1) # 类别为 1 的概率
p2 = sum(predict_data * p_2_vector) + np.log(1.0 - p_1) # 类别为 2 的概率
print('p1:', p1)
print('p2:', p2)
if p1 > p2:
return 1
else:
return 2
if __name__ == '__main__':
# 获取样本数据和对应标签
samples, labels = read_dataset()
# 获取词汇表
vocabulary = create_vocabulary(samples)
# 获取训练的样本数值向量
train_mat = []
for sample in samples:
train_mat.append(vocab_vector_to_vocabulary_vector(vocabulary, sample))
# 获取在类别 1 和 2 情况下各个词汇出现的概率以及类别 1 占样本集的概率
# p_1_vector 中存放的是各个单词在类别 1 情况下出现的条件概率
# p_2_vector 中存放的是各个单词在类别 2 情况下出现的条件概率
# p_1 就是类别为 1 的先验概率
p_1_vector, p_2_vector, p_1 = train_naive_bayes_classifier(train_mat, labels)
# 测试
predict_data = ['stupid', 'garbage'] # 预测样本
predict_data_vector = np.array(vocab_vector_to_vocabulary_vector(vocabulary, predict_data)) # 将预测样本转成词汇表的稀疏向量
result = predict(predict_data_vector, p_1_vector, p_2_vector, p_1)
if result == 1:
print(f'{predict_data} 属于内容不当')
else:
print(f'{predict_data} 属于内容得当')
---------
p1: -4.702750514326955
p2: -7.20934025660291
['stupid', 'garbage'] 属于内容不当
新浪新闻分类
我们可以使用 sklearn 来构建朴素贝叶斯分类器。在 scikit-learn 中,有三个朴素贝叶斯分类算法,分别为 GaussianNB、MultinomialNB、BernoulliNB。其中,GaussianNB 是先验为高斯分布的朴素贝叶斯,MultinomialNB 是先验为多项式分布的朴素贝叶斯,BernoulliNB 是先验为伯努利分布的朴素贝叶斯。
sklearn.naive_bayes 模块实现了朴素贝叶斯算法,其 MultinomialNB 函数实现如下所示:
sklearn.naive_bayes.MultinomialNB(alpha=1.0, force_alpha=True, fit_prior=True, class_prior=None)
- alpha:加法(拉普拉斯/利德斯通)平滑参数,默认为 1.0;如果设置为 0,则表示不平滑
- force_alpha:默认为 True;如果为 False,且 alpha 小于 1e-10,则会将 alpha 的值置为 1e-10;如果为 True,alpha 将保持不变;之所以设置这个参数,是因为如果 alpha 太接近 0,可能会导致数值错误
- fit_prior:是否学习类别先验概率,默认为 True;如果为 False,则所有的样本类别都有相同的先验概率
- class_prior:类别的先验概率,如果指定,则不会根据数据调整先验概率,默认为 None
由 MultinomialNB 创建的实例对象 clf 具有以下方法:
fit(X, y) # 根据训练集拟合 k 近邻分类器
- X:训练数据,形状为 (n_samples, n_features)
- y:目标值(训练样本对应的标签),形状为 (n_samples,)
- sample_weight:样本权重,如果为 None,则样本权重相同
返回拟合的朴素贝叶斯分类器
get_params(deep=True) # 以字典形式返回 MultinomialNB 类的参数
- deep:布尔值,默认为 True
返回参数
partial_fit(X, y, classes=None, sample_weight=None) # 对一批样本进行拟合,当整个数据集过大,无法一次性放入内存时,这种方法非常管用
- X:训练数据,形状为 (n_samples, n_features)
- y:目标值(训练样本对应的标签),形状为 (n_samples,)
- classes:y 向量中可能出现的所有类别的列表,默认为 None;必须在第一次调用 partial_fit 时提供,后续调用可以省略
- sample_weight:样本权重,如果为 None,则样本权重相同
返回实例本身
predict(X) # 预测所提供数据的类别标签
- X:预测数据,形状为 (n_samples, n_features)
以 np.ndarray 形式返回形状为 (n_samples,) 的每个数据样本的类别标签
predict_proba(X) # 返回预测数据 X 在各类别标签中所占的概率
- X:预测数据,形状为 (n_samples, n_features)
返回该样本在各类别标签中的预测概率,类别的顺序与属性 classes_ 中的顺序一致
predict_log_proba(X) # 返回预测数据 X 在各类别标签中所占的对数概率
- X:预测数据,形状为 (n_samples, n_features)
返回该样本在各类别标签中的预测对数概率,类别的顺序与属性 classes_ 中的顺序一致
predict_joint_log_proba(X) # 返回预测数据 X 的联合对数概率估计值。对于 X 的每一行 x 和类别 y,联合对数概率由 logP(x, y) = logP(y) + logP(x|y) 给出,其中 logP(y) 是类别先验概率,logP(x|y) 是类别条件概率
- X:预测数据,形状为 (n_samples, n_features)
返回该样本在各类别标签中的预测对数概率,类别的顺序与属性 classes_ 中的顺序一致
score(X, y, sample_weight=None) # 返回预测结果和标签之间的平均准确率
- X:预测数据,形状为 (n_samples, n_features)
- y:预测数据的目标值(真实标签)
- sample_weight:默认为 None
返回预测数据的平均准确率,相当于先执行了 self.predict(X),而后再计算预测值和真实值之间的平均准确率
完整的新浪新闻朴素贝叶斯分类模型代码实现如下:
import os
import random
import jieba
import numpy as np
from sklearn.naive_bayes import MultinomialNB
# 数据集处理
def text_process(dir_path: str, train_size=0.8) -> (list, list, list, list, list):
"""
:param dir_path: 数据集目录
:param train_size: 从数据集中划分训练集的比例
:return: 词汇表,训练数据,测试数据,训练标签,测试标签
"""
dir_list = os.listdir(dir_path) # ['C000008', ...]
data_list = []
labels_list = []
# 遍历每个存放了 txt 文件的子目录
for dir in dir_list:
new_dir_path = os.path.join(dir_path, dir)
files = os.listdir(new_dir_path) # ['10.txt', ...]
# 遍历每个存储了新闻文本的 txt 文件
for file in files:
file_path = os.path.join(new_dir_path, file)
with open(file_path, 'r', encoding='utf-8') as f:
raw = f.read()
word_cut = jieba.cut(raw, cut_all=False) # 精简模式,返回一个可迭代的生成器
word_list = list(word_cut)
data_list.append(word_list)
labels_list.append(dir)
# 划分训练集与测试集
data_labels_list = list(zip(data_list, labels_list)) # 将数据与标签对应压缩
random.shuffle(data_labels_list) # 将 data_labels_list 乱序
index = int(len(data_labels_list) * train_size) + 1 # 训练集与测试集划分的索引值
train_list = data_labels_list[:index] # 训练集,包括数据与标签
test_list = data_labels_list[index:] # 测试集,包括数据与标签
train_data_list, train_labels_list = zip(*train_list) # 解压训练集,得到训练数据和标签
train_data_list, train_labels_list = list(train_data_list), list(train_labels_list) # 转成列表
test_data_list, test_labels_list = zip(*test_list) # 解压测试集,得到测试数据和标签
test_data_list, test_labels_list = list(test_data_list), list(test_labels_list) # 转成列表
# 统计数据集词频
all_words_dict = {}
for words in train_data_list:
for word in words:
if word not in all_words_dict.keys():
all_words_dict[word] = 0
all_words_dict[word] += 1
# 根据字典中的值进行键值对的排序,排列顺序为降序
all_words_zip = sorted(all_words_dict.items(), key=lambda x: x[1], reverse=True) # 排序
all_words_tuple, all_words_frequency_tuple = zip(*all_words_zip) # 解压缩,得到元组形式的词汇表和频次表
all_words_list = list(all_words_tuple) # 转成列表
return all_words_list, train_data_list, test_data_list, train_labels_list, test_labels_list
# 一些特定的词语如“的”、“在”、“当然”等对新闻分类无实际意义,将这些词整理好并存储在了 stopwords_cn.txt 文件中
# 读取 stopwords_cn.txt 文件,并进行去重处理
def stop_words_set(file_path: str) -> set:
"""
:param file_path: stopwords_cn.txt 的路径
:return: 返回一个经过去重处理的词汇集合
"""
words_set = set()
with open(file_path, 'r', encoding='utf-8') as f:
for line in f.readlines():
word = line.strip()
if len(word) > 0:
words_set.add(word)
return words_set
# 词频最高的往往是一些对于分类无意义的符号,有必要删除它们
# 文本特征选取,删除词频最高的 n 个词,并选取合适的词作为特征词
def delete_words(all_words_list: list, stopwords_set: set, n=10) -> list:
"""
:param all_words_list: 训练集的词汇表
:param stopwords_set: 无意义的词汇集合
:param n: 要删除的前多少个高频词汇数
:return: 特征词汇表
"""
feature_words = []
for i in range(n, len(all_words_list)):
if all_words_list[i].isdigit() or all_words_list[i] in stopwords_set or len(all_words_list[i]) <= 1 or len(all_words_list[i]) >= 5:
continue
else:
feature_words.append(all_words_list[i])
return feature_words
# 根据 feature_words 将训练数据和测试数据向量化
def data_vector(feature_words: list, train_data_list: list, test_data_list: list) -> (list, list):
"""
:param feature_words: 数据集的特征词汇表
:param train_data_list: 训练数据,二维列表,每个元素表示一个新闻样本
:param test_data_list: 测试数据,二维列表,每个元素表示一个新闻样本
:return: 向量化的训练数据和测试数据
"""
train_feature_list = [] # train_data_list 的向量化形式
test_feature_list = [] # test_data_list 的向量化形式
# 将训练数据向量化
for sample in train_data_list:
train_sample_list = [] # 用于存储训练集单个样本的特征词汇,元素个数与 feature_words 一致
sample_set = set(sample) # 将样本数据进行去重
for word in feature_words:
if word in sample_set:
train_sample_list.append(1)
else:
train_sample_list.append(0)
train_feature_list.append(train_sample_list)
# 将测试数据向量化
for sample in test_data_list:
test_sample_list = [] # 用于存储测试集单个样本的特征词汇,元素个数与 feature_words 一致
sample_set = set(sample) # 将样本数据进行去重
for word in feature_words:
if word in sample_set:
test_sample_list.append(1)
else:
test_sample_list.append(0)
test_feature_list.append(test_sample_list)
return train_feature_list, test_feature_list
if __name__ == '__main__':
dir_path = r'D:\MachineLearning\SogouC\Sample' # 数据集存放目录
# 获取词汇表、训练数据、测试数据、训练标签、测试标签
all_words_list, train_data_list, test_data_list, train_labels_list, test_labels_list = text_process(dir_path)
# 生成 stopwords_set
stopwords_file_path = r'D:\MachineLearning\stopwords_cn.txt'
stopwords_set = stop_words_set(stopwords_file_path)
# 获取数据集的特征词汇表
feature_words = delete_words(all_words_list, stopwords_set)
# 获取向量化的训练数据和测试数据
train_feature_list, test_feature_list = data_vector(feature_words, train_data_list, test_data_list)
# print(np.array(train_feature_list).shape) # (73, 8747)
# print(np.array(test_feature_list).shape) # (17, 8747)
# 实例化 MultinomialNB 对象
clf = MultinomialNB()
# 使用训练数据和训练标签进行拟合
clf.fit(train_feature_list, train_labels_list)
# 预测
predict_result = clf.predict(test_feature_list)
# 准确率
accuracy = clf.score(test_feature_list, test_labels_list)
print('测试结果为:', predict_result)
print('准确率为:', accuracy)
---------
测试结果为: ['C000014' 'C000013' 'C000013' 'C000024' 'C000020' 'C000014' 'C000023' 'C000016' 'C000022' 'C000010' 'C000014' 'C000020' 'C000016' 'C000008' 'C000020' 'C000013' 'C000008']
准确率为: 0.8235294117647058
朴素贝叶斯算法的优缺点
优点
- 算法简单且易于实现。朴素贝叶斯算法做出了对特征之间条件独立性的假设,这使得算法的计算复杂度较低,适合处理大规模数据集。
- 对小规模数据表现良好。即使在数据量较少的情况下,朴素贝叶斯算法也能够有效地进行分类。
- 对缺失数据不敏感。朴素贝叶斯算法能够处理缺失数据,并利用已有的数据进行预测。
缺点
- 特征条件独立性假设限制了算法的表达能力。朴素贝叶斯算法无法考虑特征之间的相关性,因此在特征之间存在强相关性的情况下,算法的性能可能会受到影响。
- 对输入数据的分布假设较强。朴素贝叶斯算法假设输入特征之间服从独立同分布,但实际情况中可能存在违背这个假设的数据。
- 需要估计先验概率。朴素贝叶斯算法需要根据训练数据估计先验概率,如果样本量较小或者类别之间的先验概率差异较大,可能会导致分类结果不准确。