朴素贝叶斯

朴素贝叶斯

  • 朴素贝叶斯理论
    • 贝叶斯决策理论
    • 条件概率
    • 全概率公式
    • 贝叶斯公式
    • 朴素贝叶斯
  • 言论屏蔽
  • 新浪新闻分类
  • 朴素贝叶斯算法的优缺点

朴素贝叶斯算法是一种基于贝叶斯定理的有监督的机器学习算法,解决的是分类问题,如文本分类、垃圾邮件过滤、客户是否流失,是否值得投资、信用等级评定等领域,并在实际应用中表现出良好的性能。该算法简单易懂,学习效率高,在某些领域的分类问题中能够与决策树、神经网络等算法相媲美。但由于该算法以自变量之间的独立(假设特征之间相互独立)性和连续变量的正态性假设为前提,就会导致算法精度在某种程度上受影响。

下面是对朴素贝叶斯算法的详细阐述:

  1. 贝叶斯定理: 朴素贝叶斯算法基于贝叶斯定理进行概率推断。贝叶斯定理描述了在已知先验概率的情况下,如何根据新的证据来更新我们对事件发生概率的信念。
  2. 特征独立性假设: 朴素贝叶斯算法假设特征之间相互独立,即给定类别,特征之间的条件概率是相互独立的。虽然这个假设在实际中并不总是成立,但在许多情况下,这种简化可以带来较好的分类效果。
  3. 类别的先验概率: 在朴素贝叶斯算法中,需要计算每个类别的先验概率,即在没有任何信息的情况下,每个类别发生的概率。
  4. 特征的条件概率: 对于给定的类别,需要计算每个特征的条件概率,即在该类别下,每个特征取某个值的概率。这通常需要利用训练数据集来进行估计。
  5. 计算后验概率: 当有新的样本需要分类时,利用贝叶斯定理,可以计算出该样本属于每个类别的后验概率。最终选择具有最高后验概率的类别作为分类结果。
  6. 处理连续特征: 对于连续特征,可以使用概率密度函数来进行条件概率的估计,常见的方法包括高斯朴素贝叶斯和多项式朴素贝叶斯。

总体来说,朴素贝叶斯算法简单易懂,计算效率高,对小规模的数据集表现良好,但在处理特征之间相关性较强的情况下可能表现不佳。在实际应用中,朴素贝叶斯算法通常作为其他更复杂算法的基准进行对比,或者在特征独立性较强的问题上取得良好效果。

朴素贝叶斯理论

贝叶斯决策理论

朴素贝叶斯是贝叶斯决策理论的一部分,所以我们先了解一下贝叶斯决策理论。假设现在有两个类别, 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(AB) 来表示。

朴素贝叶斯_第1张图片

从上图可以清楚的得到条件概率 P ( A ∣ B ) P(A|B) P(AB) 的计算公式:
P ( A ∣ B ) = P ( A ∩ B ) P ( B ) P(A|B) = \frac{P(A \cap B)}{P(B)} P(AB)=P(B)P(AB)
因此,
P ( A ∩ B ) = P ( A ∣ B ) P ( B ) P(A \cap B) = P(A|B)P(B) P(AB)=P(AB)P(B)
同理,
P ( A ∩ B ) = P ( B ∣ A ) P ( A ) P(A \cap B) = P(B|A)P(A) P(AB)=P(BA)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(AB)P(B)=P(BA)P(A)P(AB)=P(B)P(BA)P(A)
这就是条件概率的计算公式。

全概率公式

假设样本空间 S S S,是两个事件 A A A A ′ A' A 的总和,如下图所示:

朴素贝叶斯_第2张图片

事件 B B B 也可以划分成两个部分,如下图所示:

朴素贝叶斯_第3张图片

此时,有如下公式:
P ( B ) = P ( B ∩ A ) + P ( B ∩ A ′ ) P(B) = P(B \cap A) + P(B \cap A') P(B)=P(BA)+P(BA)
因为,
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(BA)=P(BA)P(A)P(BA)=P(BA)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(BA)P(A)+P(BA)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=1nP(XAi)P(Ai)

朴素贝叶斯_第4张图片

贝叶斯公式

全概率公式:已知事件 X X X 与伴随事件 X X X 发生的完备事件组( A 1 、 A 2 、 . . . 、 A n A_1、A_2、...、A_n A1A2...An),根据完备事件组,求出事件 X X X 发生的概率。

贝叶斯公式:已知事件 X X X 与伴随事件 X X X 发生的完备事件组( A 1 、 A 2 、 . . . 、 A n A_1、A_2、...、A_n A1A2...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(AiX)=P(X)P(AiX)=i=1nP(XAi)P(Ai)P(XAi)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(AiX) 叫做后验概率,即在事件 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=1nP(XAi)P(Ai)P(XAi)=P(X)P(XAi) 叫做可能性函数,这是一个调整因子,使得预估概率更接近真实概率

条件概率可以理解成 后验概率 = 先验概率 × 调整因子,这就是贝叶斯推断的含义。我们先预估一个“先验概率”,然后加入实验结果,看这个实验是增强还是削弱了“先验概率”,由此得到更接近事实的“后验概率”。

接下来举一个例子,来加深对贝叶斯推断的理解。假设有两个一模一样的碗,一号碗有 30 颗水果糖和 10 颗巧克力糖,二号碗有 20 颗水果糖和 20 颗巧克力糖,如下图所示。现在随机选择一个碗,从中摸出一颗糖,发现是水果糖,请问这颗水果糖来自一号碗的概率有多大?

朴素贝叶斯_第5张图片

我们假定, 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(H1E),我们把这个概率叫做“后验概率”,即在事件 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(H1E)=P(E)P(EH1)P(H1)=P(EH1)P(H1)+P(EH2)P(H2)P(EH1)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(H1E) P ( H 2 ∣ E ) P(H_2|E) P(H2E) 的大小即可。

朴素贝叶斯

朴素贝叶斯与贝叶斯是两个不同的概念。贝叶斯算法是一种基于贝叶斯定理进行概率推断的统计学方法。贝叶斯算法通过利用先验概率和样本数据得到后验概率,从而对未知参数或未来事件进行推断。朴素贝叶斯算法是贝叶斯算法家族中的一员,它是基于贝叶斯定理和特征独立性假设的一种分类算法。与一般的贝叶斯算法相比,朴素贝叶斯算法做出了特征独立性的假设,简化了条件概率的计算,使得算法更加高效,并且适用于大规模的数据集。

朴素贝叶斯公式如下所示:
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(yX)=P(X)P(Xy)P(y)=P(x1,x2,...,xn)P(x1,x2,...,xny)P(y)=P(x1,x2,...,xn)P(x1y)P(x2y)⋅⋅⋅P(xny)P(y)
其中, P ( x 1 ∣ y ) 、 P ( x 2 ∣ y ) 、 . . . P(x_1|y)、P(x_2|y)、... P(x1y)P(x2y)... 分别表示在类别为 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(yX)=P(X)P(Xy)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) 的曲线如下图所示:

朴素贝叶斯_第6张图片

如上图所示,函数 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

朴素贝叶斯算法的优缺点

优点

  1. 算法简单且易于实现。朴素贝叶斯算法做出了对特征之间条件独立性的假设,这使得算法的计算复杂度较低,适合处理大规模数据集。
  2. 对小规模数据表现良好。即使在数据量较少的情况下,朴素贝叶斯算法也能够有效地进行分类。
  3. 对缺失数据不敏感。朴素贝叶斯算法能够处理缺失数据,并利用已有的数据进行预测。

缺点

  1. 特征条件独立性假设限制了算法的表达能力。朴素贝叶斯算法无法考虑特征之间的相关性,因此在特征之间存在强相关性的情况下,算法的性能可能会受到影响。
  2. 对输入数据的分布假设较强。朴素贝叶斯算法假设输入特征之间服从独立同分布,但实际情况中可能存在违背这个假设的数据。
  3. 需要估计先验概率。朴素贝叶斯算法需要根据训练数据估计先验概率,如果样本量较小或者类别之间的先验概率差异较大,可能会导致分类结果不准确。

你可能感兴趣的:(机器学习,机器学习,人工智能,算法)