朴素贝叶斯之实际应用

前言

这里我们完成两个任务,一个是垃圾邮件过滤,一个是新浪新闻分类。
垃圾邮件过滤用上一节学习的朴素贝叶斯来完成,新闻分类用sklearn中的朴素贝叶斯来完成。

垃圾邮件过滤

收集数据

我们已经有了一个电子邮件数据集,有两个文件夹ham和spam,spam文件下的txt文件为垃圾邮件。选取其中一份其内容如下:

1
2
3
4
5
6
7
8
Hi Peter,

With Jose out of town, do you want to
meet once in a while to keep things
going and do some interesting stuff?

Let me know
Eugene

分词处理

对于英文文本,我们可以以非字母、非数字作为符号进行切分,使用split函数即可:

1
2
3
4
5
def textParse(content,filename):
f = open(filename,errors='ignore')
emailWords = re.split(r'\W*',f.read())
words = [each.lower() for each in emailWords if len(each) > 0]
content.append(words)

划分测试集

这里我们采用交叉验证的方法,在所有的文本里随机挑选出10个为测试样本:

1
2
3
4
5
6
7
8
9
# 随机切分训练集和测试集
testContent = []
testLabel = []
for i in range(10):
randIndex = int(random.uniform(0,len(content)))
testContent.append(content[randIndex])
testLabel.append(classLabel[randIndex])
del content[randIndex]
del classLabel[randIndex]

建立词汇表

将用于训练的文本进行分词处理后,建立一个不重复的单词表:

1
2
3
4
5
6
def createVocabList(content):
myVocabList = []
for eachlist in content:
for each in eachlist:
myVocabList.append(each)
return list(set(myVocabList))

建立词向量

根据词汇表,将所有的分词文档转换为向量的形式:

1
2
3
4
5
6
7
8
def setOfWords2Vec(vocabList,inputSet):
retVec = np.zeros(len(vocabList))
for each in vocabList:
if each in inputSet:
retVec[vocabList.index(each)] = 1
else:
retVec[vocabList.index(each)] = 0
return retVec

训练函数

现在我们已经得到用于训练的向量以及对应的标签了,接下来要做的就是计算先验概率$p(W|C_1)$、$p(W|C_0)$和$p(C_1)$:

1
2
3
4
5
6
7
8
9
10
11
12
def trainNB(trainMatrix,classLabel):
pSpam = sum(classLabel) / float(len(classLabel)) # 计算p(spam)的概率
WSpam = np.ones(len(trainMatrix[0]))
WHam = np.ones(len(trainMatrix[0]))
for i in range(len(trainMatrix)):
if classLabel[i] == 1: # 垃圾邮件
WSpam += trainMatrix[i]
else: # 正常邮件
WHam += trainMatrix[i]
pWSpam = np.log(WSpam / (2+float(sum(WSpam)))) # 计算p(W0|Spam) p(W1|Spam) p(W2|Spam)...
pWHam = np.log(WHam / (2+float(sum(WHam)))) # 计算p(W0|Ham) p(W1|Ham) p(W2|Ham)...
return pWSpam,pWHam,pSpam

分类

利用之前计算出来的几个概率值,计算$p(C_1|W)$、$p(C_0|W)$,判断最终的分类结果:

1
2
3
4
5
6
7
8
9
def classify(inputVec,pWSpam,pWHam,pSpam):
classify1 = inputVec * pWSpam
classify0 = inputVec * pWHam
pSpamW = sum(classify1)+np.log(pSpam) # ln(A*B) =lnA+lnB
pHamW = sum(classify0)+np.log(1-pSpam)
if pSpamW > pHamW:
return 1
if pSpamW < pHamW:
return 0

测试

用之前分割出的测试集对该分类器进行测试,计算错误率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 开始测试
testMat = []
errorCount = 0
#testMat.append(setOfWords2Vec(myVocabList,each) for each in testContent)
for inputTestSet in testContent:
retTestVec = setOfWords2Vec(myVocabList,inputTestSet)
testMat.append(retTestVec)

for i in range(len(testMat)):
result = classify(testMat[i],pWSpam, pWHam, pSpam)
if result != testLabel[i]:
errorCount += 1
print('错误分类的测试集:{0}'.format(testContent[i]))
errorRate = float(errorCount)/len(testContent)
print("错误率是:{0}".format(errorRate))

由于划分测试集是随机的,所以最终的结果也不相同,可以经过重复实验,取平均错误率。

新浪新闻分类

通过这个例子,我们使用sklearn库中的朴素贝叶斯来进行新闻分类,

分词处理

这里我们要对中文进行分词,对于这一点可以使用jieba分词来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def TextProcessing(folder_path):
folder_list = listdir(folder_path)
data_list = []
class_list = []
# mac系统忽略'.DS_Store'文件
if '.DS_Store' in folder_list:
folder_list.remove('.DS_Store')
# 遍历每个文件夹
for folder in folder_list:
files = listdir(folder_path+'/'+folder)
# 遍历文件夹下的每个文件
j = 1
for file in files:
if j > 100: # 每个类别下的文件不超过100个(避免有的分类下样本太多)
break
with open(folder_path+'/'+folder+'/'+file,encoding='utf-8',) as f:
content = f.read()
# jieba分词,精准模式
word_cut = jieba.cut(content,cut_all=False) # 返回的是一个迭代器
word_list = list(word_cut)

data_list.append(word_list)
class_list.append(folder)
j += 1

这里限制了每个分类的样本数不超过100个,是为了避免某些类别的样本数太多,对分类结果造成影响。

划分数据集

在TextProcessing()的基础上,增加划分数据集的功能,方便交叉验证,这里我们采用train_test_split来完成。

1
2
#分割测试集和训练集
train_data,test_data,train_class,test_class = train_test_split(data_list,class_list,test_size=0.1,random_state=0)

词汇表排序

依然是在上述函数中,将所有分词出现的次数进行一个统计,并降序排序,得到一个包含所有词的列表,出现次数最高的排在最前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 统计词频
all_words_list = {}
for eachData in train_data:
for each in eachData:
if each not in all_words_list.keys():
all_words_list[each] = 0
all_words_list[each] += 1
# 根据词频,降序排序
sorted_all_words_list = sorted(all_words_list.items(),key=itemgetter(1),reverse=True)
all_words,words_num = zip(*sorted_all_words_list)
all_words_list = []
for each in all_words:
all_words_list.append(each)

# [',', '的', '\u3000', '。', '\n', ';', '&', 'nbsp', '、', '在', '“', '”', '了', ' ', '是', '和', ':', '\x00', '中国', '也', '有', ......

这样得到的all_words_list是一个包含降序排序的所有词的列表,所有的词不重复。

但是这样得到的list,可以发现排在前面的都是一些对分类没有意义的分词,还有一些诸如“在”、”了”之类的也对分类结果没有意义,所以应该将它们去除。

过滤优化词汇表

为了消除一些无用分词(特征)对新闻分类结果的影响,我们需要制定一条规则:首先去掉高频词,至于去掉多少个高频词,我们可以通过观察去掉高频词个数和最终检测准确率的关系来确定。除此之外,去除数字,不把数字作为分类特征。同时,去除一些特定的词语,比如:”的”,”一”,”在”,”不”,”当然”,”怎么”这类的对新闻分类无影响的介词、代词、连词。
这些特定的词语(停用词),具体可以见stopwords_cn.txt

其具体格式如下图所示:

1
2
3
4
5
6
7
8
9
# 加载停用词,以列表方式存放
def stop_words(stop_path):
stopwords = []
with open(stop_path,encoding='utf-8') as f:
for line in f.readlines():
word = line.strip()
if len(word)>0:
stopwords.append(word)
return stopwords

得到停用词列表后,要对之前的词表进行筛选,去除前100个高频词、去除数字、去除停用词,只留下长度大于1的前1000个特征词:

1
2
3
4
5
6
7
8
9
10
11
12
# 去除前100个高频词、去除停用词、去除数字、符号
def words_dict(all_words_list,deleteN,stopwords):
feature_words = []
n = 1 # 用于计数
for i in range(100,len(all_words_list)):
if n > deleteN: # 只取1000个特征
break
if all_words_list[i] not in stopwords and not all_words_list[i].isdigit() and len(all_words_list[i])>1:
feature_words.append(all_words_list[i])
n += 1
return feature_words
# ['黄金周', '五一', '目前', '作战', '主要', '增长', '支付', '可能', '工作', '选择', '复习', '很多', '问题', '仿制', '发展', '分析', '比赛', '一定', '远程',......

特征向量化

得到特征向量后,要将之前的训练集和测试集的分词都向量化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 将文本特征向量化
def TextFeatures(train_data, test_data, feature_words):
train_feature = []
test_feature = []
for eachTrain in train_data:
tmpTrain = [0]*len(feature_words)
for each in feature_words:
if each in eachTrain:
tmpTrain[feature_words.index(each)] = 1
# else:
# tmpTrain[feature_words.index(each)] = 0
train_feature.append(tmpTrain)
for eachTest in test_data:
tmpTest = [0]*len(feature_words)
for each in feature_words:
if each in eachTest:
tmpTest[feature_words.index(each)] = 1
# else:
# tmpTest[feature_words.index(each)] = 0
test_feature.append(tmpTest)
return train_feature,test_feature

使用Sklearn构建朴素贝叶斯分类器

上面已经得到了分词特征,接下来就用sklearn构造朴素贝叶斯分类器,具体的参数及用法可以见官方文档
在scikit-learn中,一共有三个朴素贝叶斯的分类算法:GaussianNB(先验为高斯分布的朴素贝叶斯)、MultinomialNB(先验为多项式分布的朴素贝叶斯)、BernouliNB(先验为伯努利分布的朴素贝叶斯).
我们之前一直在使用的就是先验概率为多项式分布的朴素贝叶斯;对于新闻分类问题,因为是多分类问题,可以用MultinmialNB来完成,它假设特征为多项式分布:

这里\lambda一般取为1,即拉普拉斯平滑。

对于sklearn.naive_bayes.MultinomialNB,有三个参数:
1.alpha:默认为1.0,就是添加拉普拉斯平滑
2.fit_prior:默认为Ture,表示是否要考虑先验概率,如果是false,则所有的样本类别输出都有相同的类别先验概率。
3.class_prior:默认为None,先验概率,如果没有自己设定,则在训练时分类器自己从训练集中进行计算。

MultinomialNB有很多方法,其中partial_fit()是一个比较重要的方法,如果训练集过大,一次不能全部载入内存的时候。这时我们可以把训练集分成若干等分,重复调用partial_fit来一步步的学习训练集。
训练过后,预测结果有三个方法:
1.predict:最常用,给出最终的分类
2.predict_log_proba:会给出测试集样本在各个类别上预测的概率的一个对数转化,预测出的各个类别对数概率里的最大值对应的类别
3.predict_proba:给出测试集样本在各个类别上预测的概率,预测出的各个类别概率里的最大值对应的类别

分类的代码很简单,直接调用MultionmialNB即可:

1
2
3
4
5
6
# 新闻分类器
def TextClassify(train_data, test_data, train_class, test_class):
clf = MultinomialNB()
classify = clf.fit(train_data,train_class)
accuracy = classify.score(test_data,test_class)
return accuracy

在deleteN=100时,得到的accuracy为77.8% (去除频率最高的前100个词),那么如果修改这个值,精确度会随之变化,下面来找一下相对最优精确度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def bestAccuracy(all_words_list,stopwords):
accuracy_list = []
deleteNs = range(0, 1000, 20)
for deleteN in deleteNs:
feature_words = words_dict(all_words_list, deleteN, stopwords)
print(feature_words)
train_feature, test_feature = TextFeatures(train_data, test_data, feature_words)
accuracy = TextClassify(train_feature, test_feature, train_class, test_class)
accuracy_list.append(accuracy)
print(accuracy_list)
plt.figure()
plt.plot(deleteNs, accuracy_list)
plt.title('the Relationship between deleteN and accuracy')
plt.xlabel('deleteNs')
plt.ylabel('accuracy')
plt.show()

运行这部分代码,最后得到的准确度与deleteN的关系如图所示:

一点说明

垃圾邮件分类的具体代码见email.py
新浪新闻分类的具体代码见news.py