朴素贝叶斯分类

问题引入

设一个数据集为D={(X1, Y1), (X2, Y2), , (Xn, Yn)},其中样本Xi的可由m个特征表示,即Xi=(Xi1, Xi2, , Xim)(一般要离散特征,对于连续特征的情况见后续的注意事项);而Yi为样本标签,Yi∈{C1,C2, , Ck},i=1,2, , n.

现有一个新样本X# = (X#1, X#2, , X#m),在给定的数据集D的基础上估计其所属的类别标签Y#(即对其进行分类,也即估计条件概率P(Y# | X#) )。


算法理论(问题分析)

1.贝叶斯定理

贝叶斯定理给出:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_垃圾短信

由于P(X#)对于所有类别都是相同的(因为这里是在比较不同类别的后验概率),所以选择忽略它,而只专注于计算

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_贝叶斯定理_02

2. 朴素贝叶斯的假设

朴素贝叶斯分类器假设样本特征之间相互独立,即:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_03

忽略分母P(X#)且应用该假设之后,

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_04

3. 计算概率

计算

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_sklearn_05

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_贝叶斯定理_06

…………

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_07

k个“概率值”的最大值对应的Ck即为最后所预测的样本X#的标签。

这里使用所给定的数据集D的“频率”代替“概率”:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_sklearn_08

其中,朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_09为D中标签为Ck的样本数量,n为D中的样本总数,

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_sklearn_10

计算的是类别Ck中样本的第j个特征的值为X#j的样本数(也即D中类别为Ck且第j个特征的值为X#j的样本的数量)。

另外,在实际应用中,为了防止出现“零概率”(D中不一定有足够的样本数量造成的),实际使用的计算中需要加入平滑处理,如拉普拉斯平滑:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_贝叶斯定理_11

其中,K为标签的种数(即样本的标签共用K种),Aj为X#j所有可能取值的个数(即样本的第j个特征共有Aj种取值),j=1,2, , m。

. 注意事项

朴素贝叶斯分类器假设特征之间相互独立,虽然这在实际应用中往往不成立,但它在许多情况下仍然表现良好。

对于连续特征,通常假设它们服从某种分布(如高斯分布),并计算该分布的参数(如均值和方差),然后使用这些参数来估计条件概率。

拉普拉斯平滑或其他平滑技术可以帮助处理零概率问题,提高模型的鲁棒性。


示例实验-垃圾信息识别

数据准备

原数据文件:SMSSpamCollection,每行表示一个样本(文本标签,文本单词,用Tab格隔开)。样本总数为5574,分两类(ham、spam分别表示正常短信、垃圾短信)。

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_12

SMSSpamCollection部分内容

加载数据集并拆分为训练集、测试集:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split

# 加载SMS垃圾短信数据集
with open('SMSSpamCollection', 'r', encoding='utf8') as f:
 sms = [line.split('\t') for line in f]
y, x = zip(*sms)
# 为了测试可以先取少量样本
# n = 100
# y, x = y[:n], x[:n]
print(len(x))
y = [1 if label == 'spam' else 0 for label in y] # 标签为spam的样本的整数标签为1,表示是垃圾短信,反之0则不是垃圾短信
X_train, X_test, y_train, y_test = train_test_split(x, y)

上述得到的样本特征数据还只是用单词文本表示的,需要进一步转换为数学特征

# SMS垃圾短信数据集特征提取
counter = CountVectorizer(token_pattern='[a-zA-Z]{2,}', max_features=2000) # token_pattern='[a-zA-Z]{2,}'指定了一个正则表达式,用于匹配由两个或两个以上连续英文字母(不区分大小写)组成的字符串序列。
X_train = counter.fit_transform(X_train) # 得到的结果是一个sklearn的稀疏矩阵,不能直接用len()方法直接获取其长度(样本数量)
X_test = counter.transform(X_test) # 稀疏矩阵的元素
X_train, X_test = X_train.toarray(), X_test.toarray() # 转为numpy数组形式(主要这会使得转换后的变量占据更多空间)
print(X_train.shape[0],X_test.shape[0])

这里介绍完sklearn.feature_extraction.text.CountVectorizer的用法和功能就能更深入了解上述代码在做什么工作。
CountVectorizer scikit-learn(通常简称为 sklearn)库中用于文本数据特征提取的一个工具类。它可以将文本数据(比如句子或文档集合)转换成数值型的特征向量,这些特征向量可以进一步用于机器学习模型的训练。
此次构造CountVectorizer实例使用的两个构造函数的参数:token_patternmax_feature,分别指定单词转换为特征向量的规则、特征向量的最大长度。

token_pattern参数(有默认值)需要指定一个正则表达式,用于匹配并定义什么是一个“token”(通常指一个词)。这个参数决定了什么样的字符序列会被视为词汇表中的独立项。

max_features参数用于限制词汇表中的最大单词数量。具体来说,它会根据词频(term frequency)对词汇表中的词进行排序,并保留出现频率最高的前max_features个词作为最终的词汇表。
这里还有一个核心参数ngram_range(本文的代码暂时没有指定,使用默认值)。参数类型:tuple (min_n, max_n),默认值:(1, 1);

ngram_range指定了n-gram中n的最小值和最大值。在这个范围内,所有的n值都会被用来生成n-gram。例如,ngram_range=(1, 3)表示同时生成unigrams(单个词)和bigrams(两个连续词组成的词组)。在文本处理任务中,使用n-gram可以帮助捕捉词汇之间的组合信息,从而提高模型的性能。通过调整ngram_range,可以控制生成的特征的复杂度和数量。

举例说明:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_垃圾短信_13

示例1

示例1代码:

from sklearn.feature_extraction.text import CountVectorizer
corpus = [
'This is the first document.',
'This document is the second document.',
'And this is the third one.',
'Is this the first document?']

vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
vectorizer.get_feature_names_out()
print(X.toarray())

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_贝叶斯定理_14

示例2

模型构建与训练、评估

调用sklearn接口提供的模型

代码:

import numpy as np
from sklearn.naive_bayes import MultinomialNB

np.random.seed(0)
from datasProcess import X_train, y_train, X_test, y_test

model = MultinomialNB()
model.fit(X_train, y_train)

trainScore = model.score(X_train, y_train)
testScore = model.score(X_test, y_test)
print(f'trainScore: {trainScore}, testScore: {testScore}')

运行结果:

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_sklearn_15

训练集、测试集样本数量分别为4180、1394,模型在两个数据集的预测准确率分别约为98.97%和98.13%。


调用本文自行实现的模型(速度未优化)

naiveBayes.py源码,根据上述的算法理论自行实现,主要靠for循环一一实现,所以预测新样本的速度会较慢,这里只是为了加深对该算法的理解,速度优化问题(一般要选择更合适的数据结构)暂时先不讨论。

import numpy as np

np.random.seed(0)


class NaiveBayesClassifier:
    def __init__(self):
        # 属性(变量或方法)名以__开头的会被设置为私有属性,类的实例不可访问
        self.__n = 0  # 样本总数
        self.__Nk = list()  # 存放每个标签对应的样本数(即不同类别样本的数量)
        self.__label2samples = dict()  # 字典,键值为:整数标签-样本特征列表
        self.__K = 0  # 标签的种数
        self.__P_Ck = list()  # 存放每个标签的出现频率
        self.__Aj = list()  # 记录样本特征的每个特征的所有可能取值的个数

    def testPrint(self):
        t = f"""
        样本总数:{self.__n}
        每个标签对应的样本数(即不同类别样本的数量):{self.__Nk}
        标签的种数:{self.__K}
        每个标签的出现频率:{self.__P_Ck}
        共有{len(self.__Aj)}个特征
        每个特征的所有可能取值的个数:{self.__Aj}
        """
        print(t)

    def fit(self, X, Y):
        """
        :param X: 形状为(n,m)的numpy数组,n为样本数量,m为特征维度
        :param Y: 形状为(n,)的数组,存放样本标签
        :return: None
        """
        self.__n = X.shape[0]
        self.__K = max(Y) + 1  # 标签的种类为0,1,2,..., __K - 1
        self.__Nk = [0] * self.__K  # 初始化列表及其元素初值为0
        self.__P_Ck = self.__Nk.copy()

        # 遍历每个样本及其标签,统计相关信息
        for i in range(self.__n):
            self.__Nk[Y[i]] += 1  # 标签为Y[i]的样本数量加 1
            if Y[i] not in self.__label2samples:
                self.__label2samples[Y[i]] = list()
            self.__label2samples[Y[i]].append(X[i])

        # 计算每个特征的可能取值的个数
        for j in range(X.shape[1]):
            A = len(set(X[:, j])) + 1  # X[:,j]取出(numpy)二维数组X的第j列数据
            self.__Aj.append(A)

        # 计算每个标签的出现频率(应用了拉普拉斯平滑)
        nAddK = float(self.__n + self.__K)
        for Ck in range(self.__K):
            self.__P_Ck[Ck] = (self.__Nk[Ck] + 1) / nAddK

    def predict(self, X):
        """
        :param X: 形状为(n,m)的数组,n为样本数量,m为特征维度;存放n个样本
        :return: 预测的标签Y:形状为(n,)的数组(确切地说是列表)
        """
        Y_pred = [self.__predictOne(X0) for X0 in X]
        return Y_pred

    def __predictOne(self, X0):
        """
        :param X0: 一个样本特征,X0 = [X01,X02,...,X0m]
        :return:
        """
        pMax = -1  # 记录最大“概率”值
        label = -1  # 最大“概率”值对应的标签
        p = 1  # 当前“概率”值
        m = X0.shape[0]

        for Ck in range(self.__K):
            # 取出属于当前标签Ck的样本,放入numpy数组是为了通过列索引快速获取特征数据
            samples_Ck = np.array(self.__label2samples[Ck])
            for j in range(m):
                p *= sum(samples_Ck[:, j] == X0[j]) / self.__Nk[Ck]
            p *= self.__P_Ck[Ck]
            if p > pMax:
                pMax = p
                label = Ck
            p = 1
        return label

    def score(self, X_samples, Y_labels):
        Y_pred = self.predict(X_samples)
        # 开始的一个错误:表达式“Y_pred == Y_labels”得到的是一个布尔值;纠正:np.array(Y_pred) == np.array(Y_labels)得到了一个布尔值数组
        acc = (np.array(Y_pred) == np.array(Y_labels)).sum() / len(Y_labels) * 1.0
        return acc


if __name__ == '__main__':
    from datasProcess import X_train, y_train, X_test, y_test
    import time

    start_time = time.time()

    model = NaiveBayesClassifier()
    model.fit(X_train, y_train)

    end_time = time.time()
    print(f"The fit() process took about {end_time - start_time} seconds")

    model.testPrint()

    start_time = time.time()
    trainAcc = model.score(X_train, y_train)
    print('trainAcc:', trainAcc)
    testAcc = model.score(X_test, y_test)
    print('testAcc:', testAcc)
    end_time = time.time()
    print(f"The two score() process took about {end_time - start_time} seconds")

运行结果(运行过程花费时间较长):

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_特征独立假设_16

类似地,训练集、测试集样本数量分别为4180、1394,模型在两个数据集的预测准确率分别约为99.47%和95.41%。这里每个样本的预测(每个样本有2000个特征)需要约0.74秒,5574个样本的预测花费超过一个小时。

小结

朴素贝叶斯分类是应用了贝叶斯定理和样本特征独立的条件假设,是一种基于统计的、有监督的机器学习方法。

1. 理论思想

贝叶斯定理描述了条件概率之间的关系。在分类问题中,利用贝叶斯定理来计算给定观测数据(样本特征)下,样本属于各个类别的概率,然后选择概率最大的类别作为预测结果。

样本特征之间的条件独立性一个关键假设,即一个特征的出现与另一个特征的出现无关,仅与类别有关。这个假设大大简化了计算过程,使得联合概率分布的计算分解为各个特征概率的乘积,从而降低了计算复杂度。

2. 优点

  1. 计算简单,效率高
  2. 对缺失数据不敏感
  3. 适合多分类问题

3. 缺点

  1. 特征条件独立假设具有一定的局限性
  2. 对输入数据的表达形式敏感:如特征的选择和权重分配等。
  3. 参数估计的敏感性

4. 应用领域

朴素贝叶斯分类器在信息检索、文本分类、垃圾邮件检测、情感分析等领域有着广泛的应用。主要适用于特征维度高、数据量大的分类问题。

5. 改进方向(了解)

为了克服朴素贝叶斯分类器的局限性,研究者们提出了许多改进方法,如半朴素贝叶斯分类器(放宽特征条件独立假设)、贝叶斯网络(利用特征之间的依赖关系)等。这些方法在一定程度上提高了分类性能,但同时计算复杂度和实现难度更高

注:关注微信公众号——分享之心,后台回复“机器学习基础实验”获取完整代码和相关文档资料的地址(不断更新)。

朴素贝叶斯分类实现垃圾短信识别——python自行实现和sklearn接口调用_贝叶斯定理_17

上一篇:基于决策树实现葡萄酒分类——sklearn接口调用实现