一、了解文本分类

文本分类用电脑对文本集(或其他实体或物件)按照一定的分类体系或标准进行自动分类标记。 它根据一个已经被标注的训练文档集合, 找到文档特征和文档类别之间的关系模型, 然后利用这种学习得到的关系模型对新的文档进行类别判断 。文本分类从基于知识的方法逐渐转变为基于统计 和机器学习的方法。

一般分为以下几个步骤:

(1) 预处理:将原始语料格式化为同一格式,便于后续的统一处理;

(2) 索引:将文档分解为基本处理单元,同时降低后续处理的开销;

(3) 统计:词频统计,项(单词、概念)与分类的相关概率;

(4) 特征抽取:从文档中抽取出反映文档主题的特征;

(5)分类器:分类器的训练;

(6) 评价:分类器的测试结果分析。

二、朴素贝叶斯分类算法

1. 贝叶斯公式:

根据先验概率和似然来计算后验概率

$$P ( c ∣ X ) = \frac{P(X|c)P(c)}{P(X)}$$

利用了古典的数学理论,通过贝叶斯公式,由先验概率计算出后验概率,即是该假设属于某一类别的概率,然后选择后验概率最大的类作为该假设的目标值。

2. 贝叶斯分类器

分类是机器学习和数据挖掘中最基础的一种工作。假设现在我们有一组训练样本,以及与其相对应的分类标签。每个元组都被表示为n维属性向量 $x=(x_1,x_1,…,x_n)$ 的形式,一共有k个类别 $c_1,c_2,…,c_k$。分类要做的就是模型可以预测数据属于哪个类别。
对于每个类别 $c_i$ ,利用贝叶斯公式来估计在给定训练元组X时的条件概率 $P(c_i|x)$

$$P(c_i|x)=\frac{P(c_i)P(x|c_i)}{P(x)}$$

当且仅当概率 $P(c_i|x)$ 在所有类别中取值最大时,数据 $x$ 属于 $c_i$ 。 $P(c_i)$ 是类先验概率,$P(x|c_i)$ 是样本 $x$ 相对于类 $c_i$ 的类条件概率,称为似然。因为 $P(x)$ 是用于归一化的证据因子,其对于所有的类别都是恒定的。所以只需要基于训练数据来估计 $P(c_i)$ 和 $P(x|c_i)$

3. 朴素贝叶斯算法的优缺点

  • 优点

    1. 对待预测样本进行预测,过程简单速度快(想想邮件分类的问题,预测就是分词后进行概率乘积,在log域直接做加法更快)。
    2. 对于多分类问题也同样很有效,复杂度也不会有大程度上升。
    3. 在分布独立这个假设成立的情况下,贝叶斯分类器效果奇好,会略胜于逻辑回归,同时我们需要的样本量也更少一点
    4. 对于类别类的输入特征变量,效果非常好。对于数值型变量特征,我们是默认它符合正态分布的。
  • 缺点

    1. 对于测试集中的一个类别变量特征,如果在训练集里没见过,直接算的话概率就是0了,预测功能就失效了。当然,我们前面的文章提过我们有一种技术叫做『平滑』操作,可以缓解这个问题,最常见的平滑技术是拉普拉斯估测。
    2. 朴素贝叶斯算出的概率结果,比较大小还凑合,但实际物理含义就别太当真了。

此外, $P(d| Ci)$ 之所以能展开成的连乘积形式

$P(d| Ci)=P(w1|Ci) P(w2|Ci) …P(wi|Ci) P(w1|Ci) …P(wm|Ci) $

就是假设一篇文章中的各个词之间是彼此独立的,其中一个词的出现丝毫不受另一个词的影响(回忆一下概率论中变 量彼此独立的概念就可以知道),但这显然不对,即使不是语言学专家的我们也知道,词语之间有明显的所谓“共现”关系,在不同主题的文章中,可能共现的次数 或频率有变化,但彼此间绝对谈不上独立。

其二,使用某个词在某个类别训练文档中出现的次数来估计 $P(wi|Ci)$ 时,只在训练样本数量非常多的情况下才比较准确(考虑扔硬币的问题,得通过大量观 察才能基本得出正反面出现的概率都是二分之一的结论,观察次数太少时很可能得到错误的答案),而需要大量样本的要求不仅给前期人工分类的工作带来更高要求 (从而成本上升),在后期由计算机处理的时候也对存储和计算资源提出了更高的要求。

三、用python实现一种简单的朴素贝叶斯算法(以垃圾邮件识别为例)

SpamEmail.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
'''
Created on 2020-11-1
@author: Roc
'''
import jieba
import os
import re


class SpamEmail:
def __init__(self):
self.stopWords = []
self.getStopWords("./data/中文停用词表.txt") # 停用词
self.normDict, self.normFileNum = self.getWords("./data/normal/") # 记录正常邮件词频
self.spamDict, self.spamFileNum = self.getWords("./data/spam/") # 记录垃圾邮件词频

def getStopWords(self, path):
for line in open(path):
self.stopWords.append(line[:len(line)-1])

def getWordsList(self, content, wordsList):
cutRes = list(jieba.cut(content))
for w in cutRes:
if w not in self.stopWords and w.strip() != '' and w != None and w not in wordsList:
wordsList.append(w)

def getWords(self, path):
wordsDic = {}
fileList = os.listdir(path)
num = len(fileList)
for fileName in fileList:
wordsList = []
for line in open(path + fileName):
rule = re.compile(r"[^\u4e00-\u9fa5]")
line = rule.sub("", line)
self.getWordsList(line, wordsList)
for item in wordsList:
if item in wordsDic.keys():
wordsDic[item] += 1
else:
wordsDic.setdefault(item, 1)
return wordsDic, num

def getTestWords(self, wordsDic):
wordsProbDic = {}
for word, num in wordsDic.items():
if word in self.spamDict.keys() and word in self.normDict.keys():
pw_s = self.spamDict[word]/self.spamFileNum
pw_n = self.normDict[word]/self.normFileNum
elif word in self.spamDict.keys() and word not in self.normDict.keys():
pw_s = self.spamDict[word]/self.spamFileNum
pw_n = 0.0001 # 不在正常邮件词中
elif word not in self.spamDict.keys() and word in self.normDict.keys():
pw_s = 0.0001 # 不在垃圾邮件词中
pw_n = self.normDict[word]/self.normFileNum
else:
# 二者都不在,设置默认可能性,根据最新(2020-06)全球垃圾邮件比例设置
pw_s = 0.4816
pw_n = 0.5184
ps_w = pw_s / (pw_s + pw_n) # 计算 P(s|w)
wordsProbDic.setdefault(word, ps_w)
# res = {}
# for w in sorted(wordsProbDic.items(), key=lambda d: d[1], reverse=True)[0:15]:
# res.setdefault(w[0], w[1])
return wordsProbDic

def calBayes(self, wordsProbDic):
ps_w = 1
pn_w = 1
for word, prob in wordsProbDic.items():
ps_w *= prob
pn_w *= (1 - prob)
p = ps_w / (ps_w + pn_w)
return p

def judgeSpam(self, filename):
wordsDic = {}
wordsList = []
for line in open(filename):
rule = re.compile(r"[^\u4e00-\u9fa5]")
line = rule.sub("", line)
self.getWordsList(line, wordsList)
for item in wordsList:
if item in wordsDic.keys():
wordsDic[item] += 1
else:
wordsDic.setdefault(item, 1)
wordsProbDic = self.getTestWords(wordsDic)
p = self.calBayes(wordsProbDic)
return p > 0.9

main.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
'''
Created on 2020-11-1
@author: Roc
'''
import SpamEmail
import os

spam = SpamEmail.SpamEmail()


def calAccuracy(testDic):
normCount = 0
spamCount = 0
rightCount = 0
FNCount = 0 # 假阴性,垃圾邮件漏报
FPCount = 0 # 假阳性,正常邮件误报,一般情况下要求假阳性率应该低一点
for file, isSpam in testDic.items():
if int(file) > 1000:
spamCount += 1
if isSpam:
rightCount += 1
else:
FNCount += 1
else:
normCount += 1
if isSpam:
FPCount += 1
else:
rightCount += 1
acc = rightCount / (rightCount + FNCount + FPCount)
PFN = FNCount / spamCount
PFP = FPCount / normCount
return acc, PFN, PFP


if __name__ == "__main__":
testFileList = os.listdir("./data/test")
testDic = {}
for file in testFileList:
isSpam = spam.judgeSpam("./data/test/" + file)
testDic.setdefault(file, isSpam)
print(file, isSpam)
acc, PFN, PFP = calAccuracy(testDic)
print("The correct rate:", acc)
print("The False Negative rate:", PFN)
print("The False Positive rete:", PFP)

运行结果可以看到正确率可以达到98.97%(由于测试数据较少,这个正确率置信度不高,仅供参考),但假阳性的概率达到1.04%,效果不是很好。

1
2
3
The correct rate: 0.9897959183673469
The False Negative rate: 0.010050251256281407
The False Positive rete: 0.010362694300518135

数据集下载:https://blog-roc.oss-cn-hongkong.aliyuncs.com/blog/bayes/data.zip

测试数据中,文件名小于1000的是正常邮件,大于1000的是垃圾邮件。

四、注意事项和一些小技巧

注意事项:

  • 虽然很多特征是连续数值型的,但是它们不一定服从正态分布,要想办法把它们变换调整成满足正态分布!!
  • 对测试数据中的0频次项,一定要记得平滑,简单一点可以用『拉普拉斯平滑』。
  • 先处理处理特征,把相关特征去掉,因为高相关度的2个特征在模型中相当于发挥了2次作用。
  • 朴素贝叶斯分类器一般可调参数比较少,比如scikit-learn中的朴素贝叶斯只有拉普拉斯平滑因子alpha,类别先验概率class_prior和预算数据类别先验fit_prior。模型端可做的事情不如其他模型多,因此我们还是集中精力进行数据的预处理,以及特征的选择吧。
  • 那个,一般其他的模型(像logistic regression,SVM等)做完之后,我们都可以尝试一下bagging和boosting等融合增强方法。咳咳,很可惜,对朴素贝叶斯里这些方法都没啥用。原因?原因是这些融合方法本质上是减少过拟合,减少variance的。朴素贝叶斯是没有variance可以减小。

一些小技巧:

1. 对于概率为0的数据的处理

当特征属性为离散值时,只要统计训练样本中各个划分在每个类别中出现的频率即可用来估计 $P(w|s)$ ,若某一特征值的概率为0则会使整个概率乘积变为0,这会让分类器的准确性大幅下降。

这时候使用Laplace校准:即假定训练数据库很大,以至于对每个计数加1造成的估计概率的变化忽略不计。

2. 小数连续相乘

实际项目中,概率P往往是值很小的小数,连续的微小小数相乘容易造成下溢出使乘积为0或者得不到正确答案。一种解决办法就是对乘积取自然对数,将连乘变为连加, $ln⁡(AB)=ln⁡A+ln⁡B$ 。采用自然对数处理几乎不会带来任何损失,可以避免下溢出或者浮点数舍入导致的错误。


参考资料:

[1]百度百科-文本分类:https://baike.baidu.com/item/%E6%96%87%E6%9C%AC%E5%88%86%E7%B1%BB

[2]知乎-带你理解朴素贝叶斯分类算法:https://zhuanlan.zhihu.com/p/26262151

[3]CSDN-朴素贝叶斯分类器及python实现:https://blog.csdn.net/yinyu19950811/article/details/78060267

[4]CSDN-用朴素贝叶斯进行文本分类(上):https://blog.csdn.net/suibianshen2012/article/details/51613759

[5]GitHub-BayesSpam:https://github.com/shijing888/BayesSpam

[6]CSDN-NLP系列(4)_朴素贝叶斯实战与进阶:https://blog.csdn.net/longxinchen_ml/article/details/50629613