上一节我们学习了朴素贝叶斯的原理,并且手动推导了计算方法,今天我们通过两个真实案例,来看看如何在工作中应用朴素贝叶斯。
朴素贝叶斯分类最适合的场景就是文本分类了,无论是情感分析还是文档分类及垃圾邮件识别,都是朴素贝叶斯最为擅长的地方,其也成为了自然语言处理 NLP 方向的重要工具。
文本到向量
既然说到了 NLP,那么就不得不提及从文本到向量的转换。我们都知道,计算机是比较擅长处理数字类型的数据的,而对于字符类型数据往往都需要转换成数字类型,再进行相关运算。在自然语言处理领域同样如此,拿到文本后,我们需要先把文本转化成向量,然后再做处理。
为现在较为流行的文本转向量的方式有两种,词袋和词频逆文档。在应用这两种方式的时候,都是需要有一个词典库的。这个就相当于,如果你想分析金融领域的文本,那么这个词典库中包含的单词就应该是与金融相关的;如果你想分析教育领域的文本,那么词典库应该是与教育相关的。
为了方便起见,我这里设置一个简单的词典库如下
词典库 [“我们”,“跑步”,“早饭”,“吃”,"去","出发","早上"]
词袋模型
词袋模型又可以理解为 count vector,就是查看词典库中的词语出现在文本中的次数,出现几次就标注为数字几
文本1:早上去跑步
文本2:我们吃早饭去跑步
文本3:我们出发吃早饭我们跑步
首先把三个文件进行单词分割,可以表示成如下形式:
文本1:早上|去|跑步
文本2:我们|吃|早饭|去|跑步
文本3:我们|出发|吃|早饭|我们|跑步
下面就可以查看各个文本中出现词库里单词的数量了
文本1:
早上去跑步 (0,1,0,0,1,0,1)
文本2:
我们吃早饭去跑步 (1,1,1,1,1,0,,0)
文本3:
我们出发吃早饭我们跑步 (2,1,1,1,0,1,0)
至此,通过词袋模型,我们已经成功的将一个文本,转换成了向量的形式。
当然也可以看到词袋模型的缺点,它抛弃了文档中的前后逻辑,语义结构等信息,仅仅以词语出现的次数作为评判标准。
正是为了解决这一缺点,又出现了词频逆文档模型(TF-IDF)
词频逆文档
词频逆文档又称为 TF-IDF,TF 就是词频的意思,为 IDF 则为逆向文档频率的意思。
TF:计算一个单词在文档中出现的频次
IDF:指一个单词在文档中的区分度,该模型认为一个单词出现的文档数越少,则越能够通过该单词区分不同的文档,IDF 越大就代表单词的区分度越高。
计算方式:
TF = 单词出现的次数/该文档的总单词数
IDF = log(文档总数/该单词出现的文档数 1)
TF-IDF = TF * IDF
这样你应该可以看出,一些出现频率很高的指向词,比如你我他等,虽然 TF 会很高,但是 IDF 的值会很低,所以它们的 TF-IDF 值也不高。而我们通过经验也可以知道,通过你我他这些词是很难区分不同文档的,即它们的重要性比较低。
情感分析
本节所有的数据集到保存在 GitHub 上
https://github.com/zhouwei713/DataAnalyse/tree/master/Naive_Bayes
下面我们就开始进入今天的实战部分,首先来看一个例子,根据用户评论,分析是正向评论还是负向评论。
我们先来看下语料库
分别是停用词,测试数据集,训练数据集(负向评论和正向评论)
停用词:在文本分析领域,一般都会把一些经常出现的但是又没有实际意思或者不影响语义的词语去除掉,就是停用词
测试数据集:我们看下它长什么样子
在标签中的就是测试数据,而 label 则表示该评论的正负向,能够看出1是正向的,0是负向的。
训练集:
数据格式也是类似的,文本保存在标签当中。
提取文本
我们首先要做的就是对语料库做处理,提取出我们需要的文本内容
我们先处理测试集数据
代码语言:javascript复制with open('sentiment/test.txt', 'r', encoding='utf-8') as f:
reviews, labels = [], []
start = False
for file in f:
file = file.strip()
if not file:
continue
if file.startswith("<review") and not start:
start = True
if "label" in file:
labels.append(file.split('"')[3])
continue
if start and file == r"</review>":
start = False
continue
if start:
reviews.append(file) print(reviews, labels)
通过 with 函数打开文件,初始化两个列表 reviews 和 labels,分别用来保存评论内容和对用的 label。 start = False 用来做判断,控制是否开始评论内容 file = file.strip() 去除字符串中的空格 file.startswith("<review") 表示该字符串是否以某个内容开头 file.split('"')[3] 切分字符串,并去除第4个原始(脚标从0开始)
接着再来处理训练集数据,由于训练集数据和测试集数据格式非常类似,我们可以把上面的代码做些修改,改写成函数供后面调用
代码语言:javascript复制def load_data(file, is_positive=None):
reviews, labels = [], []
with open(file, 'r', encoding='utf-8') as f:
start = False
review_text = []
for file in f:
file = file.strip()
if not file:
continue
if file.startswith("<review") and not start:
start = True
if "label" in file:
labels.append(int(file.split('"')[3]))
continue
if start and file == r"</review>":
start = False
reviews.append(" ".join(review_text))
review_text = []
continue
if start:
review_text.append(file)
if is_positive:
labels = [1] * len(reviews)
elif is_positive == False:
labels = [0] * len(reviews)
return reviews, labels
整体没有太多变化,只是增加了标签的管理。
下面再写一个函数,把训练集合并起来
代码语言:javascript复制def process_file():
train_pos_file = "sentiment/train.positive.txt"
train_neg_file = "sentiment/train.negative.txt"
test_comb_file = "sentiment/test.txt"
# 读取文件部分,把具体的内容写入到变量里面
train_pos_cmts, train_pos_lbs = load_data(train_pos_file, True)
train_neg_cmts, train_neg_lbs = load_data(train_neg_file, False)
train_comments = train_pos_cmts train_neg_cmts
train_labels = train_pos_lbs train_neg_lbs
test_comments, test_labels = load_data(test_comb_file)
return train_comments, train_labels, test_comments, test_labels
可以简单查看下训练数据
代码语言:javascript复制print(len(train_comments), len(test_comments))
print(train_comments[3], train_labels[3])
>>>
8064 2500
先付款的 有信用 1
数据预处理
下面可以进行一些简单的数据预处理,把文本中的中文字符和数字处理掉,并去掉停用词
代码语言:javascript复制def deal_text(text, stop_path):
stopwords = set()
with open(stop_path, 'r', encoding='utf-8') as in_file:
for line in in_file:
stopwords.add(line.strip())
text = re.sub('[!!] ', "!", text)
text = re.sub('[??] ', "?", text)
text = re.sub("[a-zA-Z#$%&'()* ,-./:;:<=>@,。★、…【】《》“”‘’[\]^_`{|}~] ", " UNK ", text)
text = re.sub(r"d ", ' NUM ', text)
text = re.sub(r"s ", " ", text)
text = " ".join([term for term in jieba.cut(text) if term and not term in stopwords])
return text
train_comments_new = [deal_text(comment, "sentiment/stopwords.txt") for comment in train_comments]
test_comments_new = [deal_text(comment, "sentiment/stopwords.txt") for comment in test_comments]print(train_comments_new[0], test_comments_new[0])
>>>
发短信 特别 不 方便 ! 背后 屏幕 很大 起来 不 舒服 UNK 手触 屏 ! 切换 屏幕 很 麻烦 ! 终于 找到 同道中人 初中 UNK 已经 喜欢 上 UNK 同学 都 鄙夷 眼光 看 UNK 人为 UNK 样子 古怪 说 " 丑 " 当场 气晕 现在 同道中人 UNK 好开心 ! UNK ! UNK
用到了正则表达式,我们在第一节 Python 基础课中已经介绍过了 还用到了 jieba 分词库,这里重点介绍下
jieba 分词库
jieba 分词库是中文语言处理中非常常用的分词工具,现在给出简单的用法
代码语言:javascript复制mytext = r"今天是个好日子,是一个适合学习的好日子"
fenci = jieba.cut(mytext)
print(type(fenci))
for i in fenci:
print(i)
>>>
<class 'generator'>
今天
是
个
好日子
,
是
一个
适合
学习
的
好日子
可以看到,cut 函数返回的是一个生成器,我们通过 for 循环逐个取出生成器中的内容,已经是一个个被切分的单词了。
还记得我们前面讲解文本到向量里提到的,把文本分割成单词,就可以(也是最常用)使用这里的 jieba 分词库工具。
文本向量化
接下来我们就需要把已经处理过的文本进行向量化
首先使用 count vector,在 sklearn 中直接导入使用即可
代码语言:javascript复制from sklearn.feature_extraction.text import CountVectorizer
然后就可以使用 CountVectorizer 来拟合数据,生成一个稀疏矩阵
代码语言:javascript复制稀疏矩阵是指大部分元素都是0的矩阵
count_vector = CountVectorizer()
X_train = count_vector.fit_transform(train_comments_new)
y_train = train_labels
print(X_train)
>>>
(0, 22983) 1
(0, 4011) 1
(0, 10618) 1
(0, 1) 1
(0, 18550) 1
最终我们得到的 X_train 就是一个稀疏矩阵,前面括号里的数字表示矩阵位置,后面的数字代表词频
对测试数据同样进行转换
代码语言:javascript复制X_test = count_vector.transform(test_comments_new)
y_test = test_labels
查看训练数据和测试数据的大小
代码语言:javascript复制print(np.shape(X_train), np.shape(X_test), np.shape(y_train), np.shape(y_test))
>>>
(8064, 23101) (2500, 23101) (8064,) (2500,)
总共有8064个样本,每个样本的维度是23101维,即词库中单词的个数是23101个
训练朴素贝叶斯分类器
在 sklearn 中,提供了三种朴素贝叶斯模型, 分别是 GaussianNB,MultinomialNB 和BernoulliNB。
GaussianNB 是先验概率属于高斯分布的朴素贝叶斯,适用于特征变量为连续变量,比如人的身高,物体的长度等。
MultinomialNB 是先验概率为多项式分布的朴素贝叶斯,也就是上一节我们推导的朴素贝叶斯,适用于特征变量是离散型变量,比如词袋模型中体现的词频。
BernoulliNB 是先验概率为伯努利分布的朴素贝叶斯,即符合0/1分布的变量,比如文档分类中检查单词是否出现。
本课程只介绍 MultinomialNB 算法,其他两个可以作为课后的拓展内容学习。MultinomialNB 的一些重要参数如下:
alpha:为平滑参数。还记得我们的贝叶斯公式吧,如果每个特征在训练样本中时没有出现的,那么这个特征的概率就是0,从而整体的概率也就是0了,这是不合理的,所以引入平滑参数来规避概率为0的情况。
fit_prior:默认为 True,表示十分要考虑先验概率,如果是 False,则所有的样本类别输出都有相同的类别先验概率。否则可以让 MultinomialNB 自己从训练集样本来计算先验概率。
这里我们使用 MultinomialNB 算法模型进行训练
代码语言:javascript复制from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score
clf = MultinomialNB(alpha=1.0, fit_prior=True)
# 利用朴素贝叶斯做训练
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("accuracy on test data: ", accuracy_score(y_test, y_pred))
>>>
accuracy on test data: 0.7276
文档分类
数据集共分为4类,如下:
下面就通过训练一个朴素贝叶斯分类器,来判别测试集中的文档类别。
首先还是处理文本,把文档和对应的分类读入内存
首先写一个切割文档的函数
代码语言:javascript复制def cut_word(file):
text = open(file, 'r', encoding='gb18030').read()
stopword = [line.strip() for line in open(r'text/stop/stopword.txt', encoding='utf-8').readlines()]
text_segd = jieba.cut(text.strip())
seg_word = ''
for word in text_segd:
if word not in stopword:
seg_word = word ' '
return seg_word
依然使用 jieba 做分词,同时处理停用词,返回一个大的字符串
下面再编写导入文件的函数,在函数中调用 cut_word 函数做分词
代码语言:javascript复制def load_file(file_path, label):
file_list = os.listdir(file_path)
words_list, labels_list = [], []
for f in file_list:
file = file_path '/' f
words_list.append(cut_word(file))
labels_list.append(label)
return words_list, labels_list
设想是,总共四个分类文件,依次读入并设置标签
依次读入文件
代码语言:javascript复制# 训练数据
train_words_list1, train_labels1 = load_file('text/train/女性', '女性')
train_words_list2, train_labels2 = load_file('text/train/体育', '体育')
train_words_list3, train_labels3 = load_file('text/train/文学', '文学')
train_words_list4, train_labels4 = load_file('text/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 = load_file('text/test/女性', '女性')
test_words_list2, test_labels2 = load_file('text/test/体育', '体育')
test_words_list3, test_labels3 = load_file('text/test/文学', '文学')
test_words_list4, test_labels4 = load_file('text/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
这次我们使用 TF-IDF 的方式来做文本转向量的操作
代码语言:javascript复制tf = TfidfVectorizer()train_features = tf.fit_transform(train_words_list)
test_features = tf.transform(test_words_list)
模型预测
代码语言:javascript复制clf = MultinomialNB(alpha=0.001)
clf.fit(train_features, train_labels)
predicted_labels = clf.predict(test_features)# 计算准确率
print('准确率为:', metrics.accuracy_score(test_labels, predicted_labels))
>>>
准确率为: 0.91
可以看到,准确率还是相当不错的
混淆矩阵
下面再来介绍一个概念,混淆矩阵。
我们先来看下如何打印混淆矩阵
代码语言:javascript复制from sklearn.metrics import confusion_matrix
# 混淆矩阵
print(confusion_matrix(test_labels, predicted_labels, labels=['女性', '体育', '文学', '校园']))
>>>
[[ 36 1 1 0]
[ 3 106 5 1]
[ 0 1 30 0]
[ 1 3 2 10]]
为了方便理解,我们可以把混淆矩阵改表格的形式
女性 | 体育 | 文学 | 校园 | |
---|---|---|---|---|
女性 | 36 | 1 | 1 | 0 |
体育 | 3 | 106 | 5 | 1 |
文学 | 0 | 1 | 30 | 0 |
校园 | 1 | 3 | 2 | 10 |
对于“女性”这个测试分类,总共有38个测试数据,其中36个分类正确,有3个测试数据分类错误,分别错误的分类到了体育、文学和校园下。
同理,对于“体育”测试分类,总共有115个测试数据,其中106个是正确分类的,有3个错误的分类到了女性当中,有5个错误的分类到文学当中,有1个错误的分类到校园当中。
另外两个类似。
可以看出在对角线上的36,106,30,10是正确的分类,而其余位置则是各数据的错误分类,可以清楚的通过混淆矩阵看出哪些分类是容易混淆的,比如有5个体育类文档错误的分类到了文学当中。
这样可以帮助我们更好的分析数据和分类器的效果,对容易混淆的分类做一些更加细致的处理。
完整代码
https://github.com/zhouwei713/DataAnalyse/tree/master/Naive_Bayes
总结
本节我们列举了两个实战例子,希望你能从实战中更好的体会朴素贝叶斯的应用方法。
同时我们还知道,自然语言处理是朴素贝叶斯应用的最为广泛的领域,一般的流程为,先分析文本的类型、内容组成等信息,再对文本进行处理,如果是中文文本可以使用 jieba 工具做分成并打标签,再次通过词袋模型或者 TF-IDF 模型来处理分词的权重,进行文本向量化,得到特征矩阵,最后就可以构建分类器,进行训练和预测了。