利用Python机器学习框架scikit-learn,咱们本身作一个分类模型,对中文评论信息作情感分析。其中还会介绍中文停用词的处理方法。html
前些日子,我在微信后台收到了一则读者的留言。python
我一会儿有些懵——这怎么还带点播了呢?git
可是旋即我醒悟过来,好像是我本身以前挖了个坑。github
以前我写过《 如何用Python从海量文本抽取主题? 》一文,其中有这么一段:正则表达式
为了演示的流畅,咱们这里忽略了许多细节。不少内容使用的是预置默认参数,并且彻底忽略了中文停用词设置环节,所以“这个”、“若是”、“可能”、“就是”这样的停用词才会大摇大摆地出如今结果中。不过没有关系,完成比完美重要得多。知道了问题所在,后面改进起来很容易。有机会我会写文章介绍如何加入中文停用词的去除环节。浏览器
根据“本身挖坑本身填”的法则,我决定把这一部分写出来。bash
我可使用偷懒的办法。微信
例如在原先的教程里,更新中文停用词处理部分,打个补丁。app
可是,最近我发现,好像至今为止,咱们的教程历来没有介绍过如何用机器学习作情感分析。框架
你可能说,不对吧?
情感分析不是讲过了吗?老师你好像讲过《 如何用Python作情感分析? 》,《 如何用Python作舆情时间序列可视化? 》和《 如何用Python和R对《权力的游戏》故事情节作情绪分析? 》。
你记得真清楚,提出表扬。
可是请注意,以前这几篇文章中,并无使用机器学习方法。咱们只不过调用了第三方提供的文本情感分析工具而已。
可是问题来了,这些第三方工具是在别的数据集上面训练出来的,未必适合你的应用场景。
例若有些情感分析工具更适合分析新闻,有的更善于处理微博数据……你拿过来,倒是要对店铺评论信息作分析。
这就如同你本身笔记本电脑里的网页浏览器,和图书馆电子阅览室的网页浏览器,可能类型、版本彻底同样。可是你用起本身的浏览器,就是比公用电脑上的舒服、高效——由于你已经根据偏好,对本身浏览器上的“书签”、“密码存储”、“稍后阅读”都作了个性化设置。
我们这篇文章,就给你讲讲如何利用Python和机器学习,本身训练模型,对中文评论数据作情感分类。
# 数据
个人一个学生,利用爬虫抓取了大众点评网站上的数万条餐厅评论数据。
这些数据在爬取时,包含了丰富的元数据类型。
我从中抽取了评论文本和评星(1-5星),用于本文的演示。
从这些数据里,咱们随机筛选评星为1,2,4,5的,各500条评论数据。一共2000条。
为何只甩下评星数量为3的没有选择?
你先思考10秒钟,而后往下看,核对答案。
答案是这样的:
由于咱们只但愿对情感作出(正和负)二元分类,4和5星能够看做正向情感,1和2是负向情感……3怎么算?
因此,为了不这种边界不清晰形成的混淆,我们只好把标为3星的内容丢弃掉了。
整理好以后的评论数据,以下图所示。
我已经把数据放到了演示文件夹压缩包里面。后文会给你提供下载路径。
使用机器学习的时候,你会遇到模型的选择问题。
例如,许多模型均可以用来处理分类问题。逻辑回归、决策树、SVM、朴素贝叶斯……具体到我们的评论信息情感分类问题,该用哪种呢?
幸亏,Python上的机器学习工具包 scikit-learn 不只给咱们提供了方便的接口,供咱们调用,并且还很是贴心地帮咱们作了小抄(cheat-sheet)。
这张图看似密密麻麻,很是混乱,其实是一个很是好的迷宫指南。其中绿色的方框,是各类机器学习模型。而蓝色的圆圈,是你作判断的地方。
你看,我们要处理类别问题,对吧?
顺着往下看,会要求你判断数据是否有标记。咱们有啊。
继续往下走,数据小于100K吗?
考虑一下,咱们的数据有2000条,小于这个阈值。
接下来问是否是文本数据?是啊。
因而路径到了终点。
Scikit-learn告诉咱们:用朴素贝叶斯模型好了。
小抄都作得如此照顾用户需求,你对scikit-learn的品质应该有个预期了吧?若是你须要使用经典机器学习模型(你能够理解成深度学习以外的全部模型),我推荐你先尝试scikit-learn 。
《 如何用Python从海量文本抽取主题? 》一文里,咱们讲过天然语言处理时的向量化。
忘了?
不要紧。
子曰:
学而时习之,不亦乐乎?
这里我们复习一下。
对天然语言文本作向量化(vectorization)的主要缘由,是计算机看不懂天然语言。
计算机,顾名思义,就是用来算数的。文本对于它(至少到今天)没有真正的意义。
可是天然语言的处理,是一个重要问题,也须要自动化的支持。所以人就得想办法,让机器能尽可能理解和表示人类的语言。
假如这里有两句话:
I love the game.
I hate the game.
那么咱们就能够简单粗暴地抽取出如下特征(其实就是把全部的单词都罗列一遍):
对每一句话,都分别计算特征出现个数。因而上面两句话就转换为如下表格:
按照句子为单位,从左到右读数字,第一句表示为[1, 1, 0, 1, 1],第二句就成了[1, 0, 1, 1, 1]。
这就叫向量化。
这个例子里面,特征的数量叫作维度。因而向量化以后的这两句话,都有5个维度。
你必定要记住,此时机器依然不能理解两句话的具体含义。可是它已经尽可能在用一种有意义的方式来表达它们。
注意这里咱们使用的,叫作“一袋子词”(bag of words)模型。
下面这张图(来自 ~https://goo.gl/2jJ9Kp~ ),形象化表示出这个模型的含义。
你可能会问:“这样不是很不精确吗?充分考虑顺序和上下文联系,不是更好吗?”
没错,你对文本的顺序、结构考虑得越周全,模型能够得到的信息就越多。
可是,凡事都有成本。只须要用基础的排列组合知识,你就能计算出独立考虑单词,和考虑连续n个词语(称做 n-gram),形成的模型维度差别了。
为了简单起见,我们这里仍是先用一袋子词吧。有空我再给你讲讲……
打住,不能再挖坑了。
上一节我们介绍的,是天然语言向量化处理的通则。
处理中文的时候,要更加麻烦一些。
由于不一样于英文、法文等拉丁语系文字,中文自然没有空格做为词语之间的分割符号。
咱们要先将中文分割成空格链接的词语。
例如把:
“我喜欢这个游戏”
变成:
“我 喜欢 这个 游戏”
这样一来,就能够仿照英文句子的向量化,来作中文的向量化了。
你可能担忧计算机处理起中文的词语,跟处理英文词语有所不一样。
这种担忧不必。
由于我们前面讲过,计算机其实连英文单词也看不懂。
在它眼里,不论什么天然语言的词汇,都只是某种特定组合的字符串而已。 不论处理中文仍是英文,都须要处理的一种词汇,叫作停用词。
中文维基百科里,是这么定义停用词的:
在信息检索中,为节省存储空间和提升搜索效率,在处理天然语言数据(或文本)以前或以后会自动过滤掉某些字或词,这些字或词即被称为Stop Words(停用词)。
我们作的,不是信息检索,而已文本分类。
对我们来讲,你不打算拿它作特征的单词,就能够看成停用词。
仍是举刚才英文的例子,下面两句话:
I love the game.
I hate the game.
告诉我,哪些是停用词?
直觉会告诉你,定冠词 the 应该是。
没错,它是虚词,没有什么特殊意义。
它在哪儿出现,都是一个意思。
一段文字里,出现不少次定冠词都很正常。把它和那些包含信息更丰富的词汇(例如love, hate)放在一块儿统计,就容易干扰咱们把握文本的特征。
因此,我们把它看成停用词,从特征里面剔除出去。
触类旁通,你会发现分词后的中文语句:
“我 喜欢 这个 游戏”
其中的“这个”应该也是停用词吧?
答对了!
要处理停用词,怎么办呢?固然你能够一个个手工来寻找,可是那显然效率过低。
有的机构或者团队处理过许多停用词。他们会发现,某种语言里,停用词是有规律的。
他们把常见的停用词总结出来,聚集成表格。之后只须要查表格,作处理,就能够利用先前的经验和知识,提高效率,节约时间。
在scikit-learn中,英语停用词是自带的。只须要指定语言为英文,机器会帮助你自动处理它们。
可是中文……
scikit-learn开发团队里,大概缺乏足够多的中文使用者吧。
好消息是,你可使用第三方共享的停用词表。
这种停用词表到哪里下载呢?
我已经帮你找到了 一个 github 项目 ,里面包含了4种停用词表,来自哈工大、四川大学和百度等天然语言处理方面的权威单位。
这几个停用词表文件长度不一样,内容也差别很大。为了演示的方便与一致性,我们统一先用哈工大这个停用词表吧。
我已经将其一并存储到了演示目录压缩包中,供你下载。 # 环境 请你先到 这个网址 下载本教程配套的压缩包。
下载后解压,你会在生成的目录里面看到如下4个文件。
下文中,咱们会把这个目录称为“演示目录”。
请必定注意记好它的位置哦。
要装Python,最简便办法是安装Anaconda套装。
请到 这个网址 下载Anaconda的最新版本。
请选择左侧的 Python 3.6 版本下载安装。
若是你须要具体的步骤指导,或者想知道Windows平台如何安装并运行Anaconda命令,请参考我为你准备的 视频教程 。
打开终端,用cd命令进入演示目录。若是你不了解具体使用方法,也能够参考 视频教程 。 咱们须要使用许多软件包。若是每个都手动安装,会很是麻烦。
我帮你作了个虚拟环境的配置文件,叫作environment.yaml ,也放在演示目录中。
请你首先执行如下命令:
conda env create -f environment.yaml
这样,所需的软件包就一次性安装完毕了。
以后执行,
source activate datapy3
进入这个虚拟环境。
注意必定要执行下面这句:
python -m ipykernel install --user --name=datapy3
只有这样,当前的Python环境才会做为核心(kernel)在系统中注册。 确认你的电脑上已经安装了 Google Chrome 浏览器。若是没有安装请到这里 下载 安装。
以后,在演示目录中,咱们执行:
jupyter notebook
Google Chrome会开启,并启动 Jupyter 笔记本界面:
你能够直接点击文件列表中的demo.ipynb文件,能够看到本教程的所有示例代码。
你能够一边看教程的讲解,一边依次执行这些代码。
可是,我建议的方法,是回到主界面下,新建一个新的空白 Python 3 (显示名称为datapy3的那个)笔记本。
请跟着教程,一个个字符输入相应的内容。这能够帮助你更为深入地理解代码的含义,更高效地把技能内化。
准备工做结束,下面咱们开始正式输入代码。
咱们读入数据框处理工具pandas。
import pandas as pd
复制代码
利用pandas的csv读取功能,把数据读入。
注意为了与Excel和系统环境设置的兼容性,该csv数据文件采用的编码为GB18030。这里须要显式指定,不然会报错。
df = pd.read_csv('data.csv', encoding='gb18030')
复制代码
咱们看看读入是否正确。
df.head()
复制代码
前5行内容以下:
看看数据框总体的形状是怎么样的:
df.shape
复制代码
(2000, 2)
复制代码
咱们的数据一共2000行,2列。完整读入。
咱们并不许备把情感分析的结果分红4个类别。咱们只打算分红正向和负向。
这里咱们用一个无名函数来把评星数量>3的,当成正向情感,取值为1;反之视做负向情感,取值为0。
def make_label(df):
df["sentiment"] = df["star"].apply(lambda x: 1 if x>3 else 0)
复制代码
编制好函数以后,咱们实际运行在数据框上面。
make_label(df)
复制代码
看看结果:
df.head()
复制代码
从前5行看来,情感取值就是根据咱们设定的规则,从评星数量转化而来。
下面咱们把特征和标签拆开。
X = df[['comment']]
y = df.sentiment
复制代码
X 是咱们的所有特征。由于咱们只用文本判断情感,因此X实际上只有1列。
X.shape
复制代码
(2000, 1)
复制代码
而y是对应的标记数据。它也是只有1列。
y.shape
复制代码
(2000,)
复制代码
咱们来看看 X 的前几行数据。
X.head()
复制代码
注意这里评论数据仍是原始信息。词语没有进行拆分。
为了作特征向量化,下面咱们利用结巴分词工具来拆分句子为词语。
import jieba
复制代码
咱们创建一个辅助函数,把结巴分词的结果用空格链接。
这样分词后的结果就如同一个英文句子同样,单次之间依靠空格分割。
def chinese_word_cut(mytext):
return " ".join(jieba.cut(mytext))
复制代码
有了这个函数,咱们就可使用 apply 命令,把每一行的评论数据都进行分词。
X['cutted_comment'] = X.comment.apply(chinese_word_cut)
复制代码
咱们看看分词后的效果:
X.cutted_comment[:5]
复制代码
单词和标点之间都用空格分割,符合咱们的要求。
下面就是机器学习的常规步骤了:咱们须要把数据分红训练集和测试集。
为何要拆分数据集合?
在《贷仍是不贷:如何用Python和机器学习帮你决策?》一文中,我已解释过,这里复习一下:
若是期末考试以前,老师给你一套试题和答案,你把它背了下来。而后考试的时候,只是从那套试题里面抽取一部分考。你凭借超人的记忆力得到了100分。请问你学会了这门课的知识了吗?不知道若是给你新的题目,你会不会作呢?答案仍是不知道。因此考试题目须要和复习题目有区别。
一样的道理,假设我们的模型只在某个数据集上训练,准确度很是高,可是历来没有见过其余新数据,那么它面对新数据表现如何呢?
你内心也没底吧?
因此咱们须要把数据集拆开,只在训练集上训练。保留测试集先不用,做为考试题,看模型通过训练后的分类效果。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)
复制代码
这里,咱们设定了 random_state
取值,这是为了在不一样环境中,保证随机数取值一致,以便验证我们模型的实际效果。
咱们看看此时的 X_train
数据集形状。
X_train.shape
复制代码
(1500, 2)
复制代码
可见,在默认模式下,train_test_split
函数对训练集和测试集的划分比例为 3:1。
咱们检验一下其余3个集合看看:
y_train.shape
复制代码
(1500,)
复制代码
X_test.shape
复制代码
(500, 2)
复制代码
y_test.shape
复制代码
(500,)
复制代码
一样都正确无误。
下面咱们就要处理中文停用词了。
咱们编写一个函数,从中文停用词表里面,把停用词做为列表格式保存并返回:
def get_custom_stopwords(stop_words_file):
with open(stop_words_file) as f:
stopwords = f.read()
stopwords_list = stopwords.split('\n')
custom_stopwords_list = [i for i in stopwords_list]
return custom_stopwords_list
复制代码
咱们指定使用的停用词表,为咱们已经下载保存好的哈工大停用词表文件。
stop_words_file = "stopwordsHIT.txt"
stopwords = get_custom_stopwords(stop_words_file)
复制代码
看看咱们的停用词列表的后10项:
stopwords[-10:]
复制代码
这些大部分都是语气助词,做为停用词去除掉,不会影响到语句的实质含义。
下面咱们就要尝试对分词后的中文语句作向量化了。
咱们读入CountVectorizer
向量化工具,它依据词语出现频率转化向量。
from sklearn.feature_extraction.text import CountVectorizer
复制代码
咱们创建一个CountVectorizer()
的实例,起名叫作vect。
注意这里为了说明停用词的做用。咱们先使用默认参数创建vect。
vect = CountVectorizer()
复制代码
而后咱们用向量化工具转换已经分词的训练集语句,而且将其转化为一个数据框,起名为term_matrix
。
term_matrix = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())
复制代码
咱们看看term_matrix
的前5行:
term_matrix.head()
复制代码
咱们注意到,特征词语五花八门,特别是不少数字都被看成特征放在了这里。
term_matrix
的形状以下:
term_matrix.shape
复制代码
(1500, 7305)
复制代码
行数没错,列数就是特征个数,有7305个。
下面咱们测试一下,加上停用词去除功能,特征向量的转化结果会有什么变化。
vect = CountVectorizer(stop_words=frozenset(stopwords))
复制代码
下面的语句跟刚才同样:
term_matrix = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())
复制代码
term_matrix.head()
复制代码
能够看到,此时特征个数从刚才的7305个,下降为7144个。咱们没有调整任何其余的参数,所以减小的161个特征,就是出如今停用词表中的单词。
可是,这种停用词表的写法,依然会漏掉很多漏网之鱼。
首先就是前面那一堆显眼的数字。它们在此处做为特征毫无道理。若是没有单位,没有上下文,数字都是没有意义的。
所以咱们须要设定,数字不能做为特征。
在Python里面,咱们能够设定token_pattern
来完成这个目标。
这一部分须要用到正则表达式的知识,咱们这里没法详细展开了。
但若是你只是须要去掉数字做为特征的话,按照我这样写,就能够了。
另外一个问题在于,咱们看到这个矩阵,其实是个很是稀疏的矩阵,其中大部分的取值都是0.
这没有关系,也很正常。
毕竟大部分评论语句当中只有几个到几十个词语而已。7000多的特征,单个语句显然是覆盖不过来的。
然而,有些词汇做为特征,就值得注意了。
首先是那些过于广泛的词汇。尽管咱们用了停用词表,可是不免有些词汇几乎出如今每一句评论里。什么叫作特征?特征就是能够把一个事物与其余事物区别开的属性。
假设让你描述今天见到的印象最深入的人。你怎么描述?
我看见他穿着小丑的衣服,在繁华的商业街踩高跷,一边走还一边抛球,和路人打招呼。
仍是……
我看见他有两只眼睛,一只鼻子。
后者绝对不算是好的特征描述,由于难以把你要描述的个体区分出来。
物极必反,那些过于特殊的词汇,其实也不该该保留。由于你了解了这个特征以后,对你的模型处理新的语句情感判断,几乎都用不上。
这就如同你跟着神仙学了屠龙之术,然而以后一生也没有见过龙……
因此,以下面两个代码段所示,咱们一共多设置了3层特征词汇过滤。
max_df = 0.8 # 在超过这一比例的文档中出现的关键词(过于平凡),去除掉。
min_df = 3 # 在低于这一数量的文档中出现的关键词(过于独特),去除掉。
复制代码
vect = CountVectorizer(max_df = max_df,
min_df = min_df,
token_pattern=u'(?u)\\b[^\\d\\W]\\w+\\b',
stop_words=frozenset(stopwords))
复制代码
这时候,再运行咱们以前的语句,看看效果。
term_matrix = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())
复制代码
term_matrix.head()
复制代码
能够看到,那些数字全都不见了。特征数量从单一词表法去除停用词以后的7144个,变成了1864个。
你可能会以为,太惋惜了吧?好容易分出来的词,就这么扔了?
要知道,特征多,毫不必定是好事儿。
尤为是噪声大量混入时,会显著影响你模型的效能。
好了,评论数据训练集已经特征向量化了。下面咱们要利用生成的特征矩阵来训练模型了。
咱们的分类模型,采用朴素贝叶斯(Multinomial naive bayes)。
from sklearn.naive_bayes import MultinomialNB
nb = MultinomialNB()
复制代码
注意咱们的数据处理流程是这样的:
若是每次修改一个参数,或者换用测试集,咱们都须要从新运行这么多的函数,确定是一件效率不高,且使人头疼的事儿。并且只要一复杂,出现错误的概率就会增长。
幸亏,Scikit-learn给咱们提供了一个功能,叫作管道(pipeline),能够方便解决这个问题。
它能够帮助咱们,把这些顺序工做链接起来,隐藏其中的功能顺序关联,从外部一次调用,就能完成顺序定义的所有工做。
使用很简单,咱们就把 vect 和 nb 串联起来,叫作pipe。
from sklearn.pipeline import make_pipeline
pipe = make_pipeline(vect, nb)
复制代码
看看它都包含什么步骤:
pipe.steps
复制代码
看,咱们刚才作的工做,都在管道里面了。咱们能够把管道当成一个总体模型来调用。
下面一行语句,就能够把未经特征向量化的训练集内容输入,作交叉验证,算出模型分类准确率的均值。
from sklearn.cross_validation import cross_val_score
cross_val_score(pipe, X_train.cutted_comment, y_train, cv=5, scoring='accuracy').mean()
复制代码
我们的模型在训练中的准确率如何呢?
0.820687244673089
复制代码
这个结果,仍是不错的。
回忆一下,整体的正向和负向情感,各占了数据集的一半。
若是咱们创建一个“笨模型”(dummy model),即全部的评论,都当成正向(或者负向)情感,准确率多少?
对,50%。
目前的模型准确率,远远超出这个数值。超出的这30%多,其实就是评论信息为模型带来的肯定性。
可是,不要忘了,咱们不能光拿训练集来讲事儿,对吧?下面我们给模型来个考试。
咱们用训练集,把模型拟合出来。
pipe.fit(X_train.cutted_comment, y_train)
复制代码
而后,咱们在测试集上,对情感分类标记进行预测。
pipe.predict(X_test.cutted_comment)
复制代码
这一大串0和1,你看得是否眼花缭乱?
不要紧,scikit-learn给咱们提供了很是多的模型性能测度工具。
咱们先把预测结果保存到y_pred
。
y_pred = pipe.predict(X_test.cutted_comment)
复制代码
读入 scikit-learn 的测量工具集。
from sklearn import metrics
复制代码
咱们先来看看测试准确率:
metrics.accuracy_score(y_test, y_pred)
复制代码
0.86
复制代码
这个结果是否是让你很吃惊?没错,模型面对没有见到的数据,竟然有如此高的情感分类准确性。
对于分类问题,光看准确率有些不全面,我们来看看混淆矩阵。
metrics.confusion_matrix(y_test, y_pred)
复制代码
array([[194, 43],
[ 27, 236]])
复制代码
混淆矩阵中的4个数字,分别表明:
下面这张图(来自 https://goo.gl/5cYGZd )应该能让你更为清晰理解混淆矩阵的含义:
写到这儿,你大概能明白我们模型的性能了。
可是总不能只把我们训练出的模型和无脑“笨模型”去对比吧?这也太不公平了!
下面,咱们把老朋友 SnowNLP 呼唤出来,作个对比。
若是你把它给忘了,请复习《如何用Python作情感分析?》
from snownlp import SnowNLP
def get_sentiment(text):
return SnowNLP(text).sentiments
复制代码
咱们利用测试集评论原始数据,让 SnowNLP 跑一遍,得到结果。
y_pred_snownlp = X_test.comment.apply(get_sentiment)
复制代码
注意这里有个小问题。 SnowNLP 生成的结果,不是0和1,而是0到1之间的小数。因此咱们须要作一步转换,把0.5以上的结果看成正向,其他看成负向。
y_pred_snownlp_normalized = y_pred_snownlp.apply(lambda x: 1 if x>0.5 else 0)
复制代码
看看转换后的前5条 SnowNLP 预测结果:
y_pred_snownlp_normalized[:5]
复制代码
好了,符合咱们的要求。
下面咱们先看模型分类准确率:
metrics.accuracy_score(y_test, y_pred_snownlp_normalized)
复制代码
0.77
复制代码
与之对比,我们的测试集分类准确率,但是0.86哦。
咱们再来看看混淆矩阵。
metrics.confusion_matrix(y_test, y_pred_snownlp_normalized)
复制代码
array([[189, 48],
[ 67, 196]])
复制代码
对比的结果,是 TP 和 TN 两项上,我们的模型判断正确数量,都要超出 SnowNLP。
回顾一下,本文介绍了如下知识点:
但愿这些内容可以帮助你更高效地处理中文文本情感分类工做。
你以前用机器学习作过中文情感分类项目吗?你是如何去除停用词的?你使用的分类模型是哪一个?得到的准确率怎么样?欢迎留言,把你的经验和思考分享给你们,咱们一块儿交流讨论。
喜欢请点赞。还能够微信关注和置顶个人公众号“玉树芝兰”(nkwangshuyi)。
若是你对数据科学感兴趣,不妨阅读个人系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。