机器学习:朴素贝叶斯

朴素贝叶斯法

朴素贝叶斯法 (Naïve Bayes) 是基于贝叶斯定理与特征条件独立假设的分类方法. 对于给定的训练集, 首先基于特征条件独立假设学习输入/输出的联合概率分布; 然后基于此模型, 对于给定的输入 \(\mathbf{x}\) 利用贝叶斯定理求出后验概率最大的输出 \(y\).

基本方法

设输入空间 \(\mathcal{X} \subset \mathbb{R}^n\)\(n\) 维向量的集合, 输出空间为类标记集合 \(\mathcal{Y} = \{ c_1, c_2, \cdots, c_K \}\). 输入为特征向量 \(\mathbf{x}\), 输出为类标记 \(y \in \mathcal{Y}\). \(\mathbf{X}\) 为定义在输入空间 \(\mathcal{X}\) 上的随机向量, \(Y\) 为定义在输出空间 \(\mathcal{Y}\) 上的随机向量, \(P(\mathbf{X},Y)\)\(\mathbf{X}\)\(Y\) 的联合概率分布. 训练数据集 \(T = \{ (\mathbf{x}_1, y_1), (\mathbf{x}_2, y_2), \cdots, (\mathbf{x}_N, y_N) \}\)\(P(X,Y)\) 独立同分布产生.

朴素贝叶斯法通过训练数据集学习联合概率分布 \(P(X,Y)\), 具体来说, 学习先验概率分布

\[P(Y=c_k), \ k = 1,2, \cdots,K \]

和条件概率分布

\[P(\mathbf{X}=\mathbf{x}|Y=c_k) = P(X^{(1)} = x^{(1)}, \cdots, X^{(n)} = x^{(n)}| Y = c_k), \ k = 1,2, \cdots,K \]

进而学习到联合概率分布 \(P(\mathbf{X},Y)\), 这里的上标标示特征向量各维度的值, 下标标示特征向量.

然而条件概率分布 \(P(\mathbf{X}=\mathbf{x}|Y=c_k)\) 有指数级数量的参数, 假设 \(\mathbf{x}^{(j)}\) 可取值有 \(S_j\) 个, \(Y\) 可取值有 \(K\) 个, 那么参数个数为 \(K\prod_{j=1}^nS_j\). 朴素贝叶斯对条件概率分布作了条件独立性的假设 :

\[\begin{aligned} P(\mathbf{X}=\mathbf{x}|Y=c_k) &= P(X^{(1)} = x^{(1)}, \cdots, X^{(n)}| Y = c_k)\\ &= \prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k) \end{aligned} \]

条件独立性的假设意味着用于分类的特征在类确定的条件下都是条件独立的, 但有时会牺牲一定的分类准确性, 例如两个特征向量在某一个类确定条件下互相有关联. 朴素贝叶斯法属于生成模型, 模型表示了给定输入 \(\mathbf{X}\) 产生输出 \(Y\) 的生成关系.

朴素贝叶斯分类时, 对给定的输入 \(\mathbf{x}\), 通过学习到的模型计算后验概率分布 \(P(Y=c_k|\mathbf{X}=\mathbf{x})\), 将后验概率最大的类作为 \(\mathbf{x}\) 的输出. 后验概率由贝叶斯定理计算 :

\[\begin{aligned} P(Y=c_k|\mathbf{X}=\mathbf{x}) &= \frac{P(\mathbf{X}=\mathbf{x}|Y=c_k)P(Y=c_k)}{\sum_k P(\mathbf{X}=\mathbf{x}|Y=c_k)P(Y=c_k)}\\ &= \frac{P(Y=c_k)\prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k)} {\sum_k P(Y=c_k)\prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k)}, \ k = 1,2,\cdots, K \end{aligned} \]

于是朴素贝叶斯分类器可以表示为

\[y = f(\mathbf{x}) = \arg \max_{c_k} \frac{P(Y=c_k)\prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k)} {\sum_k P(Y=c_k)\prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k)} \]

注意到分母对于所有 \(c_k\) 都是相同的, 因此

\[y = f(\mathbf{x}) = \arg \max_{c_k} P(Y=c_k)\prod^n_{j = 1} P(X^{(j)} = x^{(j)}| Y = c_k) \]

后验概率最大化

贝叶斯推断

对条件概率公式变形, 可以得到如下形式 :

\[P(Y=c_k|\mathbf{X}=\mathbf{x}) = P(Y=c_k)\frac{P(\mathbf{X}=\mathbf{x}|Y=c_k)}{P(\mathbf{X}=\mathbf{x})} \]

  • \(P(Y=c_k)\) 称为先验概率 (prior probability), 即在知道特征向量值之前对类别的判断概率.
  • \(P(Y=c_k|\mathbf{X}=\mathbf{x})\) 称为后验概率 (posterior probability), 即在知道特征向量值之后对类别概率的重新评估.
  • \(\frac{P(\mathbf{X}=\mathbf{x}|Y=c_k)}{P(\mathbf{X}=\mathbf{x})}\) 称为可能性函数 (likelihood function), 是一个调整因子, 使得预估概率更接近真实概率.

朴素贝叶斯法希望找到对给定的输入 \(\mathbf{x}\)有最大的后验概率的类.

期望风险最小化

假设 0-1 损失函数 :

\[L(Y,f(\mathbf{X})) = \left\{ \begin{aligned}1, \ Y \neq f(\mathbf{X})\\ 0, \ Y = f(\mathbf{X}) \end{aligned} \right. \]

期望风险函数为 :

\[R_{exp}(f) = E[L(Y,f(\mathbf{X}))] \]

在条件期望下为 :

\[R_{exp}(f) = E_{\mathbf{X}}[E[L(Y,f(\mathbf{X}))|\mathbf{X} = \mathbf{x}]] = E_{\mathbf{X}} \left[\sum_{k=1}^K L(c_k,f(\mathbf{X} = \mathbf{x})) P(c_k|\mathbf{X}=\mathbf{x})\right] \]

为了使期望风险最小, 只需每一个 \(\mathbf{X} = \mathbf{x}\) 条件下的期望损失函数最小, 由此 :

\[\begin{aligned} f(\mathbf{x}) &= \arg \min_{y \in \mathcal{Y}} \sum_{k=1}^K L(c_k,y) P(c_k|\mathbf{X}=\mathbf{x})\\ &= \arg \min_{y \in \mathcal{Y}} \sum_{k=1}^K P(y \neq c_k|\mathbf{X}=\mathbf{x})\\ &= \arg \min_{y \in \mathcal{Y}} (1 - P(y = c_k|\mathbf{X}=\mathbf{x}))\\ &= \arg \max_{y \in \mathcal{Y}} P(y = c_k|\mathbf{X}=\mathbf{x}) \end{aligned} \]

即期望风险最小等价于后验概率最大化准则 :

\[f(\mathbf{x}) = \arg \max_{y \in \mathcal{Y}} P(y = c_k|\mathbf{X}=\mathbf{x}) \]

参数估计

极大似然估计

朴素贝叶斯分类时, 对给定的输入 \(\mathbf{x}\), 通过学习到的模型计算后验概率分布 \(P(Y=c_k|\mathbf{X}=\mathbf{x})\), 将后验概率最大的类作为 \(\mathbf{x}\) 的输出, 即 :

\[y = f(\mathbf{x}) = \arg \max_{c_k} P(Y=c_k)\prod^n_{j = 1} P(\mathbf{X}^{(j)} = \mathbf{x}^{(j)}| Y = c_k) \]

因此朴素贝叶斯法中, 学习意味着估计 \(P(Y=c_k)\)\(P(\mathbf{X}_j=\mathbf{x}_j|Y=c_k)\), 可以利用极大似然估计估计相应的概率. 先验概率 \(P(Y=c_k)\) 的极大似然估计是

\[P(Y=c_k) = \frac{\sum^N_{i=1}I(y_i=c_i)}{N}, \ k=1,2,\cdots,K \]

设第 \(j\) 个特征 \(\mathbf{x}^{(j)}\) 可能取值集合为 \(\{ a_{j1},a_{j2},\cdots,a_{jS_j} \}\), 条件概率 \(P(\mathbf{X}^{(j)}=\mathbf{x}_j|Y=c_k)\) 的极大似然估计是

\[P(\mathbf{X}^{(j)}=a_{jl}|Y=c_k) = \frac{\sum^N_{i=1}I(\mathbf{x}^{(j)}_i=a_{jl}, y_i=c_k)}{\sum^N_{i=1}I(y_i=c_i)} \\ j = 1,2,\cdots,n; \ l = 1,2,\cdots,S_j; \ k=1,2,\cdots,K \]

贝叶斯估计

采用极大似然估计可能会出现条件概率 \(P(\mathbf{X}^{(j)} = \mathbf{x}^{(j)}| Y = c_k)\)\(0\) 的情况, 使分类出现偏差. 可以采用贝叶斯估计, 即条件概率的贝叶斯估计为:

\[P_{\lambda}(\mathbf{X}^{(j)}=a_{jl}|Y=c_k) = \frac{\sum^N_{i=1}I(\mathbf{x}^{(j)}_i=a_{jl}, y_i=c_k)+\lambda}{\sum^N_{i=1}I(y_i=c_k)+S_j \lambda} \]

显然有:

\[\sum_{l=1}^{S_j}P_{\lambda}(\mathbf{X}^{(j)}=a_{jl}|Y=c_k) = 1 \]

\(S_j\)\(\mathbf{x}^{(j)}\) 的可取值数, \(\lambda \geq 0\). 等价于在随机变量各个取值的频数上加上一个正数 \(\lambda\). 当取 \(\lambda=1\) 时, 称为拉普拉斯平滑 (Laplace smoothing). 同样先验概率的贝叶斯估计为:

\[P_{\lambda}(Y=c_k) = \frac{\sum^N_{i=1}I(y_i=c_k)+\lambda}{N+K\lambda} \]

算法小结

朴素贝叶斯的主要优点有:

  1. 朴素贝叶斯模型发源于古典数学理论, 有稳定的分类效率.

  2. 对小规模的数据表现很好, 能处理多分类任务, 适合增量式训练, 尤其是数据量超出内存时, 可以分批增量训练. 即增加了一个训练样本, 分别更新给定对应类别下的条件概率和先验概率.

  3. 对缺失数据不太敏感, 算法也比较简单, 常用于文本分类.

朴素贝叶斯的主要缺点有:    

  1. 朴素贝叶斯模型假设属性之间相互独立, 这个假设在实际应用中往往是不成立的, 在属性个数比较多或者属性之间相关性较大时, 分类效果不好. 可以考虑半朴素贝叶斯分类器 (semi-naïve bayes classifiers), 贝叶斯网等方法.

  2. 需要知道先验概率, 且先验概率很多时候取决于假设, 假设的模型可以有很多种, 因此在某些时候会由于假设的先验模型的原因导致预测效果不佳.

  3. 由于是通过先验和数据来决定后验的概率从而决定分类, 所以分类决策存在一定的错误率.

  4. 对输入数据的表达形式很敏感.

示例: 文本分类

这里的要点是计算条件概率 \(P(\mathbf{X}^{(j)} = \mathbf{x}^{(j)}| Y = c_k)\). 其中 \(c_k\) 有两个取值, 表示是否是侮辱性类别; \(\mathbf{x}\) 是词条向量, 为 0/1 向量, 表示词汇表中某单词是否出现在该样本中.

先创建训练集并向量化.

# 函数说明: 创建实验样本
def load_dataset():
    # 切分的词条
    posting_list = [['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代表侮辱性类别, 0代表不是
    class_vec = [0, 1, 0, 1, 0, 1]
    return posting_list, class_vec

# 函数说明: 将切分的实验样本词条整理成不重复的词条列表, 也就是词汇表
def create_vocab_list(dataset):
    vocab_set = set()
    for document in dataset:
        vocab_set = vocab_set | set(document)
    return list(vocab_set)

# 函数说明: 根据vocab_list词汇表, 将input_set向量化, 向量的每个元素为1或0
def set_of_words2vec(vocab_list, input_set):
    return_vec = [0] * len(vocab_list)
    for word in input_set:
        if word in vocab_list:
            return_vec[vocab_list.index(word)] = 1
        else:
            print("the word: %s is not in my Vocabulary!" % word)
    return return_vec

if __name__ == '__main__':
    posting_list, class_vec = load_dataset()
    print('postingList:\n', posting_list)
    vocab_list = create_vocab_list(posting_list)
    print('myVocabList:\n', vocab_list)
    train_mat = []
    for posting_doc in posting_list:
        train_mat.append(set_of_words2vec(vocab_list, posting_doc))
    print('trainMat:\n', train_mat)

myVocabList:
 ['has', 'stupid', 'my', 'is', 'ate', 'posting', 'mr', 'to', 'problems', 'park', 'steak', 'garbage', 'him', 'buying', 'dog', 'please', 'I', 'licks', 'take', 'stop', 'how', 'quit', 'not', 'love', 'so', 'worthless', 'food', 'help', 'cute', 'flea', 'maybe', 'dalmation']
trainMat:
 [[1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1], [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0]]

训练朴素贝叶斯分类器:

# 函数说明: 训练朴素贝叶斯分类器, 即计算贝叶斯推断公式右边的先验概率和条件概率
def train_nb0(train_matrix, train_category):
    num_train_docs = len(train_matrix)
    num_words = len(train_matrix[0])
    # 计算先验概率: 文档属于侮辱类的概率
    p_abusive = sum(train_category) / float(num_train_docs)
    # 初始化条件概率
    p0_num, p0_denom = np.zeros(num_words), 0.0
    p1_num, p1_denom = np.zeros(num_words), 0.0
    for i in range(num_train_docs):
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])
    p1_vec = p1_num / p1_denom
    p0_vec = p0_num / p0_denom
    return p_abusive, p1_vec, p0_vec

if __name__ == '__main__':
    posting_list, class_vec = load_dataset()
    vocab_list = create_vocab_list(posting_list)
    print('myVocabList:\n', vocab_list)
    train_mat = []
    for posting_doc in posting_list:
        train_mat.append(set_of_words2vec(vocab_list, posting_doc))
    p_ab, p1v, p0v = train_nb0(train_mat, class_vec)
    print('p0V:\n', p0v)
    print('p1V:\n', p1v)
    print('classVec:\n', class_vec)
    print('pAb:\n', p_ab)

根据贝叶斯分类器对给定文档进行分类时, 需要计算多个条件概率的乘积以获得 \(P(\mathbf{X}=\mathbf{x}|Y=c_k)\), 如果其中一个为 \(0\), 则结果为 \(0\). 为消除这种影响, 可以将所有词的出现数初始化为 \(1\), 并将分母初始化为 \(2\). 这种做法叫做拉普拉斯平滑(Laplace Smoothing), 又称为加1平滑, 是比较常用的平滑方法, 它就是为了解决 \(0\) 概率问题.

另一个问题是太多很小的数相乘会产生下溢出, 一种解决办法是对乘积取自然对数.

# 函数说明: 训练朴素贝叶斯分类器, 加入 Laplace 平滑与自然对数
def train_nb0(train_matrix, train_category):
    num_train_docs = len(train_matrix)
    num_words = len(train_matrix[0])
    # 计算先验概率: 文档属于侮辱类的概率
    p_abusive = sum(train_category) / float(num_train_docs)
    # 初始化条件概率
    p0_num, p0_denom = np.ones(num_words), 2.0
    p1_num, p1_denom = np.ones(num_words), 2.0
    for i in range(num_train_docs):
        if train_category[i] == 1:
            p1_num += train_matrix[i]
            p1_denom += sum(train_matrix[i])
        else:
            p0_num += train_matrix[i]
            p0_denom += sum(train_matrix[i])
    p1_vec = np.log(p1_num / p1_denom)
    p0_vec = np.log(p0_num / p0_denom)
    return p_abusive, p1_vec, p0_vec

验证测试文档:

# 函数说明: 对给定的文档向量, 用加入 Laplace 平滑与自然对数的贝叶斯分类器分类
def classify_nb(vec2classify, p0_vec, p1_vec, p_class1):
    p1 = sum(vec2classify * p1_vec) + np.log(p_class1)
    p0 = sum(vec2classify * p0_vec) + np.log(1 - p_class1)
    if p1 > p0:
        return 1
    else:
        return 0

def testing_nb():
    posting_list, class_vec = load_dataset()
    vocab_list = create_vocab_list(posting_list)
    train_mat = []
    for posting_doc in posting_list:
        train_mat.append(set_of_words2vec(vocab_list, posting_doc))
    p_ab, p1v, p0v = train_nb0(train_mat, class_vec)
    # 测试文档1
    test_entry = ['love', 'my', 'dalmation']
    this_doc = np.array(set_of_words2vec(vocab_list,test_entry))
    print(test_entry, 'classified as: ', classify_nb(this_doc, p0v, p1v, p_ab))
    # 测试文档2
    test_entry = ['stupid', 'garbage']
    this_doc = np.array(set_of_words2vec(vocab_list, test_entry))
    print(test_entry, 'classified as: ', classify_nb(this_doc, p0v, p1v, p_ab))

if __name__ == '__main__':
    testing_nb()

scikit-learn 朴素贝叶斯类库

在 scikit-learn 中, 一共有 3 个朴素贝叶斯的分类算法类, 分别是 GaussianNB, MultinomialNB 和 BernoulliNB.

这三个类适用的分类场景各不相同, 一般来说, 如果样本特征的分布大部分是连续值, 使用 GaussianNB 会比较好. 如果如果样本特征的分大部分是多元离散值, 使用 MultinomialNB 比较合适, 而如果样本特征是二元离散值或者稀疏的多元离散值, 应该使用BernoulliNB.

GaussianNB 类

GaussianNB 类假设特征的条件概率为正态分布, 即:

\[P(\mathbf{X}=\mathbf{x}|Y=c_k) = \frac{1}{\sqrt{2\pi} \sigma_k} \exp{-\frac{(\mathbf{x} - \mathbf{\mu}_k)^2}{2\sigma^2_k}} \]

其中 \(c_k\)\(Y\) 的第 \(k\) 类类别, GaussianNB 会根据训练集求出 \(\mu_k\)\(\sigma_k^2\), \(\mu_k\) 为在样本类别 \(c_k\) 中所有 \(\mathbf{X}_j\) 的平均值, \(\sigma_k^2\) 是其方差.

GaussianNB 类的主要参数仅有一个, 即先验概率, 未指定则默认为 \(P(Y=c_k) = \frac{\sum^N_{i=1}I(y_i=c_i)}{N}\).

在使用 GaussianNB 的 fit 方法拟合数据后, 我们可以进行预测. 此时预测有三种方法, 包括predict, predict_log_proba 和 predict_proba:

  1. predict 方法就是我们最常用的预测方法, 直接给出测试集的预测类别输出.
  2. predict_proba 则不同, 它会给出测试集样本在各个类别上预测的概率, 容易理解, predict_proba 预测出的各个类别概率里的最大值对应的类别, 也就是 predict 方法得到类别.
  3. predict_log_proba 与 predict_proba 类似, 它会给出测试集样本在各个类别上预测的概率的一个对数转化. 转化后 predict_log_proba 预测出的各个类别对数概率里的最大值对应的类别, 也就是 predict 方法得到类别.

此外, GaussianNB 一个重要的功能是有 partial_fit 方法, 这个方法的一般用在如果训练集数据量非常大, 一次不能全部载入内存的时候. 这时我们可以把训练集分成若干等分, 重复调用 partial_fit 来一步步的学习训练集, 非常方便. 后面讲到的 MultinomialNB 和 BernoulliNB 也有类似的功能.

GaussianNB 类适合符合高斯分布的连续变量, 例如身高长度等. 下面是应用GaussianNB 类对 iris 数据集进行分类的例子:

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import accuracy_score

# 导入 iris 数据
iris_data = datasets.load_iris()
# iris_data.shape=(150, 4)
x = iris_data.data
# y = {0, 1, 2}
y = iris_data.target

# 随机按比例选取 train_data 和 test_data(占20%)
# random_state: 随机数种子, 在需要重复试验的时候, 保证得到一组一样的随机数
x_train, x_test, y_train, y_test = \
    train_test_split(x, y, test_size=0.2, random_state=0)

# 指定分类器, 并拟合
clf = GaussianNB()
clf.fit(x_train, y_train)
y_predict_train = clf.predict(x_train)
y_predict_test = clf.predict(x_test)
# 求解准确率
accuracy_train = accuracy_score(y_predict_train, y_train)
accuracy_test = accuracy_score(y_predict_test, y_test)
print("训练集正确率: ", accuracy_train)
print("测试集正确率: ", accuracy_test)

训练集正确率:  0.95
测试集正确率:  0.9666666666666667

MultinomialNB 类

MultinomialNB 假设特征的条件概率为多项式分布, 即如下式:

\[P_{\lambda}(\mathbf{X}^{(j)}=a_{jl}|Y=c_k) = \frac{\sum^N_{i=1}I(\mathbf{x}^{(j)}_i=a_{jl}, y_i=c_k)+\lambda}{\sum^N_{i=1}I(y_i=c_k)+S_j \lambda} \]

其中 \(P_{\lambda}(\mathbf{X}^{(j)}=a_{jl}|Y=c_k)\) 为第 \(k\) 个类别的第 \(j\) 维特征的第 \(l\) 个取值的条件概率, \(\lambda > 0\). 这就是前面的贝叶斯估计.

MultinomialNB 有 3 个参数, 参数 alpha 为上面的 \(\lambda\), 一般取默认的 Laplace 平滑 \(\lambda=1\). 布尔参数 fit_prior 表示是否要考虑先验概率, 如果是 false, 则所有的样本类别输出都有相同的类别先验概率. 否则可以自己用第三个参数 class_prior 输入先验概率, 或者不输入第三个参数 class_prior 让 MultinomialNB 自己从训练集样本来计算先验概率. 在使用 MultinomialNB 的 fit 方法或者 partial_fit 方法拟合数据后, 我们可以进行预测, 和 GaussianNB 一样, 预测有三种方法, 包括 predict, predict_log_proba 和 predict_proba.

MultinomialNB 类适合处理符合多项分布的离散变量. 其中的样本数据通常表示为词向量的数量 (word count vectors), 例如词汇表为 [bayes, classification, word, count], 样本文档 [word, count, count] 可以表示为 \(\mathbf{x}= [ 0, 0, 1, 2]\). 在实际项目中也可以使用 TF-IDF 来表示 \(\mathbf{x}\), 此时特征不是整数.

BernoulliNB 类

BernoulliNB 假设特征的条件概率为二元 Bernoulli 分布, 即如下式:

\[P(\mathbf{X}^{(j)}=a_{jl}|Y=c_k) = P(j|Y=c_k) a_{jl} + (1-P(j|Y=c_k))(1-a_{jl}) \]

\(P_{\lambda}(\mathbf{X}^{(j)}=a_{jl}|Y=c_k)\) 为第 \(k\) 个类别的第 \(j\) 维特征的第 \(l\) 个取值的条件概率, \(a_{jl}\) 只能取值 \(0\)\(1\).

BernoulliNB 一共有 4 个参数, 其中 3 个参数的名字和意义和 MultinomialNB 完全相同, 唯一增加的一个参数是 binarize. BernoulliNB 类要求输入样本表示为二元形式, 如果传递其他类型的数据, binarize 参数可以将其二值化, 小于 binarize 的会归为一类, 大于 binarize 的会归为另外一类. 如果省略 binarize, 则 BernoulliNB 认为每个数据特征都已经是二元的. 拟合和预测方法与上面两种相同.

BernoulliNB 类适合处理符合 0/1 分布的布尔变量, 例如在文档分类中单词是否出现 (word occurrence vectors). BernoulliNB 可能在某些数据集上表现得更好, 特别是那些文档更短的数据集, 如果时间允许, 最好对 MultinomialNB 和 BernoulliNB 这两个模型都进行评估.

示例: 文本分类之 TF-IDF 值

TF-IDF (term frequency–inverse document frequency, 词频-逆向文件频率) 是一种用于信息检索 (information retrieval) 与文本挖掘 (text mining) 的常用加权技术. TF-IDF是一种统计方法, 用来评估某个词语对于一个文件集或一个语料库中的一份文件的重要程度. 词语的重要性随着它在文件中出现的次数成正比增加, 但同时会随着它在语料库中出现的频率成反比下降. TF-IDF的主要思想是: 如果某个单词在一篇文章中出现的频率 TF 高, 并且在其他文章中很少出现, 则认为此词或者短语具有很好的类别区分能力, 适合用来分类. TF-IDF 是词频 TF 和逆向文档频率 IDF 的乘积, 其中

\[ \text{词频} = \frac{ \text{单词出现的次数}}{ \text{该文档的总单词数}} \]

\[ \text{逆向文档频率} = \log \frac{ \text{文档总数}}{ \text{出现该单词的文档数} + 1} \]

在 sklearn 中直接使用 TfidfVectorizer 类计算单词 TF-IDF 向量的值, 其中取对数的底数为 \(e\), 创建 TfidfVectorizer 类的方法是:

TfidfVectorizer(stop_words=stop_words, token_pattern=token_pattern)

其中有两个构造参数, 可以自定义停用词 stop_words 和规律规则 token_pattern, 需要注意的是停用词 stop_words 是一个列表 List 类型, 而过滤规则 token_pattern 是正则表达式. 停用词指的是 TF 高但 IDF 地起不到分类效果的词 (例如 the, this 等). 当创建好 TF-IDF 向量类型后, 可以用 fit_transform 计算, 返回文本矩阵, 表示每个单词在每个文档中的 TF-IDF 值. 举例创建 4 个文档的 documents 列表, 并让创建好的 tfidf_vec 对 documents 进行拟合,得到 TF-IDF 矩阵:

from sklearn.feature_extraction.text import TfidfVectorizer
# 创建 TfidfVectorizer 类
tfidf_vec = TfidfVectorizer()
# 给出样本文档 documents
documents = [
    'this is the bayes document',
    'this is the second second document',
    'and the third one',
    'is this the document'
]
# 计算文档的 TF-IDF 矩阵
tfidf_matrix = tfidf_vec.fit_transform(documents)

print('不重复的词:', tfidf_vec.get_feature_names())
不重复的词: ['and', 'bayes', 'document', 'is', 'one', 'second', 'the', 'third', 'this']

print('每个单词的 ID:', tfidf_vec.vocabulary_)
每个单词的 ID: {'this': 8, 'is': 3, 'the': 6, 'bayes': 1, 'document': 2, 'second': 5, 'and': 0, 'third': 7, 'one': 4}

# 输出每个单词在每个文档中的 TF-IDF 值, 向量里的顺序按照词语的 id 顺序排列
# shape = 4 by 9, 4 个样本文档, 9 个特征取值
print('每个单词的 tfidf 值:\n', tfidf_matrix.toarray())
每个单词的 tfidf 值:
 [[0.         0.63314609 0.40412895 0.40412895 0.         0.
  0.33040189 0.         0.40412895]
 [0.         0.         0.27230147 0.27230147 0.         0.85322574
  0.22262429 0.         0.27230147]
 [0.55280532 0.         0.         0.         0.55280532 0.
  0.28847675 0.55280532 0.        ]
 [0.         0.         0.52210862 0.52210862 0.         0.
  0.42685801 0.         0.52210862]]

下面以文档为例, 对体育, 女性, 文学, 校园四种类别利用朴素贝叶斯分类器进行分类. 其主要步骤包括:

  1. 基于分词的数据准备, 包括分词, 单词权重计算, 去掉停用词. 分词阶段中, 英文文档常用 ntlk 包, 而中文文档常用 jieba 包.
  2. 应用朴素贝叶斯分类进行分类, 首先通过训练集得到朴素贝叶斯分类器, 然后将分类器应用于测试集, 并与实际结果做对比, 最终得到测试集的分类准确率.
# 中文文本分类
import os
import jieba
import warnings
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn import metrics

warnings.filterwarnings('ignore')

def cut_words(file_path):
    """
    对文本进行切词
    :param file_path: txt文本路径
    :return: 用空格分词的字符串
    """
    text_with_spaces = ''
    text=open(file_path, 'r', encoding='gb18030').read()
    textcut = jieba.cut(text)
    for word in textcut:
        text_with_spaces += word + ' '
    return text_with_spaces

def loadfile(file_dir, label):
    """
    将路径下的所有文件加载
    :param file_dir: 保存txt文件目录
    :param label: 文档标签
    :return: 分词后的文档列表和标签
    """
    file_list = os.listdir(file_dir)
    words_list = []
    labels_list = []
    for file in file_list:
        file_path = file_dir + '/' + file
        words_list.append(cut_words(file_path))
        labels_list.append(label)                                                                                                                 
    return words_list, labels_list

# 加载训练数据并对文档进行分词
train_words_list1, train_labels1 = loadfile('text classification/train/女性', '女性')
train_words_list2, train_labels2 = loadfile('text classification/train/体育', '体育')
train_words_list3, train_labels3 = loadfile('text classification/train/文学', '文学')
train_words_list4, train_labels4 = loadfile('text classification/train/校园', '校园')
# 数据拼接
train_words_list = train_words_list1 + train_words_list2 + train_words_list3 + train_words_list4
train_labels = train_labels1 + train_labels2 + train_labels3 + train_labels4
# 加载测试数据并对文档进行分词
test_words_list1, test_labels1 = loadfile('text classification/test/女性', '女性')
test_words_list2, test_labels2 = loadfile('text classification/test/体育', '体育')
test_words_list3, test_labels3 = loadfile('text classification/test/文学', '文学')
test_words_list4, test_labels4 = loadfile('text classification/test/校园', '校园')
test_words_list = test_words_list1 + test_words_list2 + test_words_list3 + test_words_list4
test_labels = test_labels1 + test_labels2 + test_labels3 + test_labels4

# 加载停用词表
stop_words = open('text classification/stop/stopword.txt', 'r', encoding='utf-8').read()
# 列表头部\ufeff处理
stop_words = stop_words.encode('utf-8').decode('utf-8-sig')
# 根据分隔符分隔
stop_words = stop_words.split('\n')

# 由 tf-idf 计算单词权重
tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5)
# 输出训练集文档每个单词在每个文档中的 TF-IDF 值, 组成特征向量 (等价于单词出现次数)
train_features = tf.fit_transform(train_words_list)

# 生成多项式贝叶斯分类器
clf = MultinomialNB(alpha=0.001).fit(train_features, train_labels)

# 测试集 TF-IDF 值转换 (输出测试集特征向量)
test_features = tf.transform(test_words_list)
# 使用生成的分类器做预测
predicted_labels = clf.predict(test_features)
# 计算准确率
print('准确率为:', metrics.accuracy_score(test_labels, predicted_labels))
准确率为: 0.91
posted @ 2020-12-07 21:11  肥嘟嘟左衛門  阅读(234)  评论(0)    收藏  举报