利用朴素贝叶斯模型进行中文文本类别预测

前言

这是之前的那个文章分类系统的完全重构的版本,之前那个预测的准确率不太行,为了让项目运行地更完善,决定通过训练 nlp 模型来更加精确地预测文章所属的类别,数据集是我们用爬虫精选的,模型也是通过训练集训练的,准确率能够达到 0.96,数据集的话就不分享了

贝叶斯原理

贝叶斯公式

$$ P(B_{i}|A) = \frac{P(B_{i})P(A|B_{i})}{\sum_{j=1}^{n}P(B_{j})P(A|B_{j}))} $$

朴素贝叶斯分类器

朴素贝叶斯是一个概率分类器,它通过计算两个不同类别 X 和 Y 之间的所属同类的可能性来进行类别的预测估计,P(Y|X)为 Y 的后验概率,P(Y)为 Y 的先验概率

将一系列训练数据通过贝叶斯公式进行计算,得出一个个 P(Y|X)后验概率,分类时,对于实例 x,比较所有的 P(Y|x),最大的概率的 Y 即为实例 x 的分类

后验概率:

$$ P(Y|X) = \frac{P(Y)P(X|Y)}{P(X)} $$

P(Y)可以通过数据集进行估计,P(X)为常数,在比较中可以忽略

文本分类算法

朴素贝叶斯分类器是一种有监督的分类器,即需要给出训练集并根据需要打好标签

对于每一个训练样本,均生成一个属性向量,各个属性相互独立,如文章的文本特征向量

计算 P(x|Y)条件概率,P(x|Y)= 包含此单词的文章总数 / 该类别下的文章数量

根据贝叶斯公式求出 P(Y|X),预测文本分类

具体实现

预处理

  • 训练集语料库
  • 测试集语料库

语料库可以根据需要自己生成,本文代码是基于复旦大学语料库的格式基础写的

复旦大学语料库:http://download.csdn.net/detail/github_36326955/9747927

语料库均为已分类好的文章作为预处理数据

中文分词

采用的是 jieba 分词

  • jieba.cut 方法接受四个输入参数: 需要分词的字符串;cut_all 参数用来控制是否采用全模式;HMM 参数用来控制是否使用 HMM 模型;use_paddle 参数用来控制是否使用 paddle 模式下的分词模式,paddle 模式采用延迟加载方式,通过 enable_paddle 接口安装 paddlepaddle-tiny,并且 import 相关代码;
  • jieba.cut_for_search 方法接受两个参数:需要分词的字符串;是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细
  • 待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8
  • jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用
  • jieba.lcut 以及 jieba.lcut_for_search 直接返回 list
  • jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。

curpus_segment:

 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
# -*- coding: UTF-8 -*-
import os
import jieba

def readfile(path): # 读取文件
	with open(path, "rb") as fp:
		content = fp.read()
	return content

def savefile(path, content): # 保存文件
	with open(path, "wb") as fp:
		fp.write(content)

def corpus_segment(corpus_path, corpus_seg_path):
	catelist = os.listdir(corpus_path) # 获取 corpus_path 下所有子目录
	for cate in catelist:
		cate_path = corpus_path + cate + '/'
		cate_seg_path = corpus_seg_path + cate + '/'
		if not os.path.exists(cate_seg_path): # 不存在分词目录则创建
			os.makedirs(cate_seg_path)
		textlist = os.listdir(cate_path) # 获取一个分类下的所有文本
		for text in textlist:
			text_path = cate_path + text
			content = readfile(text_path)
			content = content.replace('\r\n'.encode('utf-8'), ''.encode('utf-8')).strip() # 删除换行
			content = content.replace(' '.encode('utf-8'), ''.encode('utf-8')).strip() # 删除空行、多余的空格
			content_seg = jieba.cut(content)  # 分词
			savefile(cate_seg_path + text, ' '.join(content_seg).encode('utf-8'))
	print("分词结束")

if __name__ == "__main__":
	# 对训练集进行分词
	corpus_path = "train_corpus/" # 未分词已分类的语料库
	corpus_seg_path = "train_corpus_seg/" # 已分词已分类的语料库
	corpus_segment(corpus_path, corpus_seg_path)

	# 对测试集进行分词
	corpus_path = "test_corpus/" # 未分词已分类的语料库
	corpus_seg_path = "test_corpus_seg/" # 已分词已分类的语料库
	corpus_segment(corpus_path, corpus_seg_path)

Bunch 类型转换

对于每个分词的数据集,为了把它们变成一个个变量,可以转换为 Bunch 类型

corpus_to_Bunch:

 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
# -*- coding: UTF-8 -*-
import os
import pickle
from sklearn.datasets._base import Bunch

def readfile(path): # 读取文件
	with open(path, "rb") as fp:
		content = fp.read()
	return content

def corpus2Bunch(wordbag_path, seg_path):
	catelist = os.listdir(seg_path) # 获取分类信息
	bunch = Bunch(target_name = [], label = [], filenames = [], contents = [])
	bunch.target_name.extend(catelist) # 填充 bunch 中的 tatget_name list
	for cate in catelist:
		cate_path = seg_path + cate + '/'
		textlist = os.listdir(cate_path)
		for text in textlist:
			text_path = cate_path + text
			bunch.label.append(cate)
			bunch.filenames.append(text_path)
			bunch.contents.append(readfile(text_path))
	with open(wordbag_path, "wb") as fp:
		pickle.dump(bunch, fp)
	print("构建文本对象结束")

if __name__ == "__main__":
	# 对训练集进行Bunch化操作
	wordbag_path = "train_word_bag/train_set.dat" # Bunch 存储路径
	seg_path = "train_corpus_seg/" # 已分词已分类的语料库
	corpus2Bunch(wordbag_path, seg_path)

	# 对测试集进行Bunch化操作
	wordbag_path = "test_word_bag/test_set.dat" # Bunch 存储路径
	seg_path = "test_corpus_seg/" # 已分词已分类的语料库
	corpus2Bunch(wordbag_path, seg_path)

创建 TF-IDF 词向量空间模型

数据集变成变量了,接下来是结构化的过程,我们需要把所有的词向量放入一个词向量空间当中,这里我们采用的是 TF-IDF 的权重策略,生成一个个权重矩阵,并存入文件中,tfdifspace.dat 即生成的实例文件,里面包括词向量的空间坐标和 TF-IDF 的权重矩阵

TFIDF_space:

 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
# -*- coding: UTF-8 -*-
import pickle
from sklearn.datasets._base import Bunch
from sklearn.feature_extraction.text import TfidfVectorizer

def readfile(path): # 读取文件
	with open(path, "rb") as fp:
		content = fp.read()
	return content

def readbunchobj(path):
	with open(path, "rb") as fp:
		bunch = pickle.load(fp)
	return bunch

def writebunchobj(path, bunchobj):
	with open(path, "wb") as fp:
		pickle.dump(bunchobj, fp)

def vector_space(stopword_path, bunch_path, space_path, train_tfidf_path = None):
	stopwordlist = readfile(stopword_path).splitlines()
	bunch = readbunchobj(bunch_path)
	tfidfspace = Bunch(target_name = bunch.target_name, label = bunch.label, filenames = bunch.filenames, tdm = [], vocabulary = {})
	'''
	tdm	TF-IDF矩阵
	vocabulary 词典索引
	'''
	if train_tfidf_path is not None:
		trainbunch = readbunchobj(train_tfidf_path)
		tfidfspace.vocabulary = trainbunch.vocabulary
		vectorizer = TfidfVectorizer(stop_words = stopwordlist, sublinear_tf = True, max_df = 0.5, vocabulary = trainbunch.vocabulary)
		'''
		sublinear_tf 计算tf值采用亚线性策略
		max_df 设置阙值,达到直接加入临时停用词
		'''
		tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
	else:
		vectorizer = TfidfVectorizer(stop_words = stopwordlist, sublinear_tf = True, max_df = 0.5)
		tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
		tfidfspace.vocabulary = vectorizer.vocabulary_ # vocabulary_ 向量空间坐标轴信息
	writebunchobj(space_path, tfidfspace)
	print("tf-idf词向量空间实例创建成功!!!")

if __name__ == "__main__":
	stopword_path = "train_word_bag/stopwords.txt"
	bunch_path = "train_word_bag/train_set.dat"
	space_path = "train_word_bag/tfidfspace.dat"
	vector_space(stopword_path, bunch_path, space_path)

	bunch_path = "test_word_bag/test_set.dat"
	space_path = "test_word_bag/testspace.dat"
	train_tfidf_path = "train_word_bag/tfidfspace.dat"
	vector_space(stopword_path, bunch_path, space_path, train_tfidf_path)

贝叶斯分类器预测

NBayes_Predict:

 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
# -*- coding: UTF-8 -*-
import pickle
from sklearn.naive_bayes import MultinomialNB  # 导入多项式贝叶斯算法
from sklearn import metrics

def readbunchobj(path):
	with open(path, "rb") as fp:
		bunch = pickle.load(fp)
	return bunch

# 导入训练集
trainpath = "train_word_bag/tfidfspace.dat"
train_set = readbunchobj(trainpath)

#导入测试集
testpath = "test_word_bag/testspace.dat"
test_set = readbunchobj(testpath)

# 训练分类器:输入词袋向量和分类标签,alpha:0.001 alpha越小,迭代次数越多,精度越高
clf = MultinomialNB(alpha=0.001).fit(train_set.tdm, train_set.label)

# 预测分类结果
predicted = clf.predict(test_set.tdm)
for flabel, file_name, expct_cate in zip(test_set.label, test_set.filenames, predicted):
    if flabel != expct_cate:
        print(file_name, ": 实际类别:", flabel, " -->预测类别:", expct_cate)
print("预测完毕!!!")

# 计算分类精度:
def metrics_result(actual, predict):
    print('精度:{0:.3f}'.format(metrics.precision_score(actual, predict, average='weighted')))
    print('召回:{0:0.3f}'.format(metrics.recall_score(actual, predict, average='weighted')))
    print('f1-score:{0:.3f}'.format(metrics.f1_score(actual, predict, average='weighted')))
metrics_result(test_set.label, predicted)

总结

整体来说流程比较清晰,数据可观,生成的模型预测率较准,不过需要大量的数据支持而且每类分类总数,文章字数应相当,不然会造成文章多或者文章字数多的概率大,导致大量文章预测错误

本项目文章源代码在 Github 上已开源:Github 源码

参考文章:

基于朴素贝叶斯的文本分类算法

python 中文文本分类