avatarShuyi Wang

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

10039

Abstract

执行这些代码。</p><p id="ba46">但是,我<b>建议</b>的方法,是回到主界面下,新建一个新的空白 Python 3 (显示名称为datapy3的那个)笔记本。</p><figure id="86f1"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*9jHN6RtSUu1V0J2xMXJQ-Q.png"><figcaption></figcaption></figure><p id="19c1">请跟着教程,一个个字符输入相应的内容。这可以帮助你更为深刻地理解代码的含义,更高效地把技能内化。</p><figure id="cab2"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*MsnDUXbx2wqxF3Nr-tDOCg.png"><figcaption></figcaption></figure><p id="5150">准备工作结束,下面我们开始正式输入代码。</p><h1 id="31f5">代码</h1><p id="7a70">我们读入数据框处理工具 pandas。</p><div id="f475"><pre><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd</pre></div><p id="5d74">利用 pandas 的 csv 读取功能,把数据读入。</p><p id="bf53">注意为了与 Excel 和系统环境设置的兼容性,该 csv 数据文件采用的编码为 GB18030。这里需要显式指定,否则会报错。</p><div id="086c"><pre><span class="hljs-attr">df</span> = pd.read_csv(<span class="hljs-string">'data.csv'</span>, encoding=<span class="hljs-string">'gb18030'</span>)</pre></div><p id="1f94">我们看看读入是否正确。</p><div id="c064"><pre>df.head<span class="hljs-comment">()</span></pre></div><p id="7c9e">前 5 行内容如下:</p><figure id="9f08"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*WjQk4h770d6sbacz7-wm-w.png"><figcaption></figcaption></figure><p id="889a">看看数据框整体的形状是怎么样的:</p><div id="b86b"><pre>df.<span class="hljs-built_in">shape</span></pre></div><div id="3ace"><pre>(<span class="hljs-number">2000</span><span class="hljs-punctuation">,</span> <span class="hljs-number">2</span>)</pre></div><p id="f6a8">我们的数据一共 2000 行,2 列。完整读入。</p><p id="5c42">我们并不准备把情感分析的结果分成 4 个类别。我们只打算分成正向和负向。</p><p id="b6f4">这里我们用一个无名函数来把评星数量 > 3 的,当成正向情感,取值为 1;反之视作负向情感,取值为 0。</p><div id="0778"><pre><span class="hljs-function">def <span class="hljs-title">make_label</span><span class="hljs-params">(df)</span>: df[<span class="hljs-string">"sentiment"</span>] =</span> df[<span class="hljs-string">"star"</span>].<span class="hljs-built_in">apply</span>(lambda x: <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> x><span class="hljs-number">3</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>)</pre></div><p id="11cf">编制好函数之后,我们实际运行在数据框上面。</p><div id="b497"><pre><span class="hljs-function"><span class="hljs-title">make_label</span><span class="hljs-params">(df)</span></span></pre></div><p id="660d">看看结果:</p><div id="d110"><pre>df.head<span class="hljs-comment">()</span></pre></div><figure id="b9d6"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*GnO__hq_XIsSUX4WD8uubg.png"><figcaption></figcaption></figure><p id="6cb8">从前 5 行看来,情感取值就是根据我们设定的规则,从评星数量转化而来。</p><p id="3f21">下面我们把特征和标签拆开。</p><div id="364a"><pre><span class="hljs-attr">X</span> = df<span class="hljs-string">'comment'</span> <span class="hljs-attr">y</span> = df.sentiment</pre></div><p id="2d22">X 是我们的全部特征。因为我们只用文本判断情感,所以 X 实际上只有 1 列。</p><div id="413b"><pre>X.<span class="hljs-built_in">shape</span></pre></div><div id="c486"><pre>(<span class="hljs-number">2000</span><span class="hljs-punctuation">,</span> <span class="hljs-number">1</span>)</pre></div><p id="1ea6">而 y 是对应的标记数据。它也是只有 1 列。</p><div id="50a9"><pre>y.<span class="hljs-built_in">shape</span></pre></div><div id="f390"><pre>(<span class="hljs-number">2000</span><span class="hljs-punctuation">,</span>)</pre></div><p id="724e">我们来看看 X 的前几行数据。</p><div id="c5d7"><pre>X.<span class="hljs-built_in">head</span>()</pre></div><figure id="2793"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*VmOCTNRa5zEz0HMd7w36Yg.png"><figcaption></figcaption></figure><p id="2f71">注意这里评论数据还是原始信息。词语没有进行拆分。</p><p id="6690">为了做特征向量化,下面我们利用结巴分词工具来拆分句子为词语。</p><div id="860a"><pre><span class="hljs-keyword">import</span> jieba</pre></div><p id="e5b1">我们建立一个辅助函数,把结巴分词的结果用空格连接。</p><p id="334c">这样分词后的结果就如同一个英文句子一样,单次之间依靠空格分割。</p><div id="5802"><pre>def <span class="hljs-built_in">chinese_word_cut</span>(mytext): return <span class="hljs-string">" "</span>.<span class="hljs-built_in">join</span>(jieba.<span class="hljs-built_in">cut</span>(mytext))</pre></div><p id="b6cf">有了这个函数,我们就可以使用 apply 命令,把每一行的评论数据都进行分词。</p><div id="2e48"><pre>X<span class="hljs-selector-attr">[<span class="hljs-string">'cutted_comment'</span>]</span> = X<span class="hljs-selector-class">.comment</span><span class="hljs-selector-class">.apply</span>(chinese_word_cut)</pre></div><p id="02d7">我们看看分词后的效果:</p><div id="9e37"><pre>X<span class="hljs-selector-class">.cutted_comment</span><span class="hljs-selector-attr">[:5]</span></pre></div><figure id="a711"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*Gq6saJIW2ETmIVknWDf5zw.png"><figcaption></figcaption></figure><p id="925a">单词和标点之间都用空格分割,符合我们的要求。</p><p id="348c">下面就是机器学习的常规步骤了:我们需要把数据分成训练集和测试集。</p><p id="73d9">为什么要拆分数据集合?</p><p id="f9c5">在《<a href="https://medium.com/@wshuyi/%E8%B4%B7%E8%BF%98%E6%98%AF%E4%B8%8D%E8%B4%B7-%E5%A6%82%E4%BD%95%E7%94%A8python%E5%92%8C%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%B8%AE%E4%BD%A0%E5%86%B3%E7%AD%96-3d7d9beccc0f">贷还是不贷:如何用 Python 和机器学习帮你决策?</a>》一文中,我已解释过,这里复习一下:</p><blockquote id="7bcd"><p>如果期末考试之前,老师给你一套试题和答案,你把它背了下来。然后考试的时候,只是从那套试题里面抽取一部分考。你凭借超人的记忆力获得了 100 分。请问你学会了这门课的知识了吗?不知道如果给你新的题目,你会不会做呢?答案还是不知道。所以考试题目需要和复习题目有区别。</p></blockquote><p id="0edf">同样的道理,假设咱们的模型只在某个数据集上训练,准确度非常高,但是从来没有见过其他新数据,那么它面对新数据表现如何呢?</p><p id="bd51">你心里也没底吧?</p><p id="fe04">所以我们需要把数据集拆开,只在训练集上训练。保留测试集先不用,作为考试题,看模型经过训练后的分类效果。</p><div id="3f61"><pre><span class="hljs-keyword">from</span> sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, <span class="hljs-attribute">random_state</span>=1)</pre></div><p id="6b00">这里,我们设定了 <code>random_state</code> 取值,这是为了在不同环境中,保证随机数取值一致,以便验证咱们模型的实际效果。</p><p id="4686">我们看看此时的 <code>X_train</code> 数据集形状。</p><div id="5003"><pre>X_train.<span class="hljs-built_in">shape</span></pre></div><div id="c555"><pre>(<span class="hljs-number">1500</span><span class="hljs-punctuation">,</span> <span class="hljs-number">2</span>)</pre></div><p id="f80a">可见,在默认模式下,<code>train_test_split</code> 函数对训练集和测试集的划分比例为 3:1。</p><p id="09eb">我们检验一下其他 3 个集合看看:</p><div id="70d6"><pre>y_train.<span class="hljs-built_in">shape</span></pre></div><div id="381e"><pre>(<span class="hljs-number">1500</span><span class="hljs-punctuation">,</span>)</pre></div><div id="2224"><pre>X_test.<span class="hljs-built_in">shape</span></pre></div><div id="4a40"><pre>(<span class="hljs-number">500</span><span class="hljs-punctuation">,</span> <span class="hljs-number">2</span>)</pre></div><div id="6895"><pre>y_test.<span class="hljs-built_in">shape</span></pre></div><div id="22bb"><pre>(<span class="hljs-number">500</span><span class="hljs-punctuation">,</span>)</pre></div><p id="5e37">同样都正确无误。</p><p id="bae9">下面我们就要处理中文停用词了。</p><p id="2a18">我们编写一个函数,从中文停用词表里面,把停用词作为列表格式保存并返回:</p><div id="5710"><pre>def get_custom_stopwords(stop_words_file): <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(stop_words_file) <span class="hljs-keyword">as</span> f: stopwords = f.<span class="hljs-built_in">read</span>() stopwords_list = stopwords.<span class="hljs-built_in">split</span>(<span class="hljs-string">'\n'</span>) custom_stopwords_list = [i <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> stopwords_list] <span class="hljs-literal">return</span> custom_stopwords_list</pre></div><p id="c786">我们指定使用的停用词表,为我们已经下载保存好的哈工大停用词表文件。</p><div id="14cb"><pre><span class="hljs-attr">stop_words_file</span> = <span class="hljs-string">"stopwordsHIT.txt"</span> <span class="hljs-attr">stopwords</span> = get_custom_stopwords(stop_words_file)</pre></div><p id="71a5">看看我们的停用词列表的后 10 项:</p><div id="f937"><pre><span class="hljs-attribute">stopwords</span>[-<span class="hljs-number">10</span>:]</pre></div><figure id="1be3"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*XH-lg8k9ahwwjdExz7c5mg.png"><figcaption></figcaption></figure><p id="933c">这些大部分都是语气助词,作为停用词去除掉,不会影响到语句的实质含义。</p><p id="d9e4">下面我们就要尝试对分词后的中文语句做向量化了。</p><p id="d73a">我们读入 <code>CountVectorizer</code> 向量化工具,它依据词语出现频率转化向量。</p><div id="69e2"><pre><span class="hljs-keyword">from</span> sklearn.feature_extraction.text <span class="hljs-keyword">import</span> CountVectorizer</pre></div><p id="d491">我们建立一个 <code>CountVectorizer ()</code> 的实例,起名叫做 vect。</p><p id="b2e3">注意这里为了说明停用词的作用。我们先使用默认参数建立 vect。</p><div id="98ad"><pre><span class="hljs-attribute">vect</span> <span class="hljs-operator">=</span> CountVectorizer()</pre></div><p id="073b">然后我们用向量化工具转换已经分词的训练集语句,并且将其转化为一个数据框,起名为 <code>term_matrix</code></p><div id="e789"><pre><span class="hljs-attr">term_matrix</span> = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())</pre></div><p id="6917">我们看看 <code>term_matrix</code> 的前 5 行:</p><div id="78fd"><pre>term_matrix.head<span class="hljs-comment">()</span></pre></div><figure id="98f5"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*0EY-Oc1SWgJs2HCCo_TTqQ.png"><figcaption></figcaption></figure><p id="04a8">我们注意到,特征词语五花八门,特别是很多数字都被当作特征放在了这里。</p><p id="8198"><code>term_matrix</code> 的形状如下:</p><div id="dab5"><pre>term_matrix.<span class="hljs-built_in">shape</span></pre></div><div id="3c1e"><pre>(<span class="hljs-number">1500</span><span class="hljs-punctuation">,</span> <span class="hljs-number">7305</span>)</pre></div><p id="e16e">行数没错,列数就是特征个数,有 7305 个。</p><p id="bacf">下面我们测试一下,加上停用词去除功能,特征向量的转化结果会有什么变化。</p><div id="08a0"><pre><span class="hljs-attribute">vect</span> <span class="hljs-operator">=</span> CountVectorizer(stop_words<span class="hljs-operator">=</span>frozenset(stopwords))</pre></div><p id="8020">下面的语句跟刚才一样:</p><div id="f8bb"><pre><span class="hljs-attr">term_matrix</span> = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())</pre></div><div id="ecae"><pre>term_matrix.head<span class="hljs-comment">()</span></pre></div><figure id="4408"><img src="https://cdn-images-1.readmedium.com/v

Options

2/resize:fit:800/1*eFEuC_qTze_bKyytrK8Ngg.png"><figcaption></figcaption></figure><p id="15b6">可以看到,此时特征个数从刚才的 7305 个,降低为 7144 个。我们没有调整任何其他的参数,因此减少的 161 个特征,就是出现在停用词表中的单词。</p><p id="37f4">但是,这种停用词表的写法,依然会漏掉不少漏网之鱼。</p><p id="2f37">首先就是前面那一堆显眼的数字。它们在此处作为特征毫无道理。如果没有单位,没有上下文,数字都是没有意义的。</p><p id="fcc2">因此我们需要设定,数字不能作为特征。</p><p id="ffdb">在 Python 里面,我们可以设定 <code>token_pattern</code> 来完成这个目标。</p><p id="88cd">这一部分需要用到正则表达式的知识,我们这里无法详细展开了。</p><p id="36a5">但如果你只是需要去掉数字作为特征的话,按照我这样写,就可以了。</p><p id="09cc">另一个问题在于,我们看到这个矩阵,实际上是个非常稀疏的矩阵,其中大部分的取值都是 0.</p><p id="cfd4">这没有关系,也很正常。</p><p id="c168">毕竟大部分评论语句当中只有几个到几十个词语而已。7000 多的特征,单个语句显然是覆盖不过来的。</p><p id="7558">然而,有些词汇作为特征,就值得注意了。</p><p id="e5d9">首先是那些过于普遍的词汇。尽管我们用了停用词表,但是难免有些词汇几乎出现在每一句评论里。什么叫做特征?特征就是可以把一个事物与其他事物区别开的属性。</p><p id="6e83">假设让你描述今天见到的印象最深刻的人。你怎么描述?</p><blockquote id="8a44"><p>我看见他穿着小丑的衣服,在繁华的商业街踩高跷,一边走还一边抛球,和路人打招呼。</p></blockquote><p id="c124">还是……</p><blockquote id="7c06"><p>我看见他有两只眼睛,一只鼻子。</p></blockquote><p id="4b8e">后者绝对不算是好的特征描述,因为难以把你要描述的个体区分出来。</p><p id="3e23">物极必反,那些过于特殊的词汇,其实也不应该保留。因为你了解了这个特征之后,对你的模型处理新的语句情感判断,几乎都用不上。</p><p id="d8e8">这就如同你跟着神仙学了屠龙之术,然而之后一辈子也没有见过龙……</p><p id="bb3c">所以,如下面两个代码段所示,我们一共多设置了 3 层特征词汇过滤。</p><div id="dcb9"><pre><span class="hljs-attr">max_df</span> = <span class="hljs-number">0.8</span> <span class="hljs-comment"># 在超过这一比例的文档中出现的关键词(过于平凡),去除掉。</span> <span class="hljs-attr">min_df</span> = <span class="hljs-number">3</span> <span class="hljs-comment"># 在低于这一数量的文档中出现的关键词(过于独特),去除掉。</span></pre></div><div id="05e4"><pre>vect = CountVectorizer(max_df = max_df, min_df = min_df, token_pattern=<span class="hljs-string">u'(?u)\b[^\d\W]\w+\b'</span>, stop_words=<span class="hljs-built_in">frozenset</span>(stopwords))</pre></div><p id="a29e">这时候,再运行我们之前的语句,看看效果。</p><div id="63d4"><pre><span class="hljs-attr">term_matrix</span> = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())</pre></div><div id="3680"><pre>term_matrix.head<span class="hljs-comment">()</span></pre></div><figure id="66df"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*UF21iB78hKj8e9IUTIFX4w.png"><figcaption></figcaption></figure><p id="bfff">可以看到,那些数字全都不见了。特征数量从单一词表法去除停用词之后的 7144 个,变成了 1864 个。</p><p id="d695">你可能会觉得,太可惜了吧?好容易分出来的词,就这么扔了?</p><p id="68f5">要知道,特征多,绝不一定是好事儿。</p><p id="ab73">尤其是噪声大量混入时,会显著影响你模型的效能。</p><p id="7627">好了,评论数据训练集已经特征向量化了。下面我们要利用生成的特征矩阵来训练模型了。</p><p id="552e">我们的分类模型,采用朴素贝叶斯(Multinomial naive bayes)。</p><div id="0699"><pre>from sklearn.naive_bayes import MultinomialNB <span class="hljs-attribute">nb</span> <span class="hljs-operator">=</span> MultinomialNB()</pre></div><p id="e556">注意我们的数据处理流程是这样的:</p><ol><li>特征向量化;</li><li>朴素贝叶斯分类。</li></ol><p id="8ba4">如果每次修改一个参数,或者换用测试集,我们都需要重新运行这么多的函数,肯定是一件效率不高,且令人头疼的事儿。而且只要一复杂,出现错误的几率就会增加。</p><p id="2c29">幸好,Scikit-learn 给我们提供了一个功能,叫做管道 (pipeline),可以方便解决这个问题。</p><p id="ebe0">它可以帮助我们,把这些顺序工作连接起来,隐藏其中的功能顺序关联,从外部一次调用,就能完成顺序定义的全部工作。</p><p id="4671">使用很简单,我们就把 vect 和 nb 串联起来,叫做 pipe。</p><div id="47d2"><pre><span class="hljs-keyword">from</span> sklearn.pipeline <span class="hljs-keyword">import</span> make_pipeline pipe = make_pipeline(vect, nb)</pre></div><p id="2923">看看它都包含什么步骤:</p><div id="31e4"><pre>pipe.steps</pre></div><figure id="4db5"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*K8esJ9LLcgKh2rOQv9UiyA.png"><figcaption></figcaption></figure><p id="de25">看,我们刚才做的工作,都在管道里面了。我们可以把管道当成一个整体模型来调用。</p><p id="62e2">下面一行语句,就可以把未经特征向量化的训练集内容输入,做交叉验证,算出模型分类准确率的均值。</p><div id="554e"><pre><span class="hljs-keyword">from</span> sklearn.cross_validation import cross_val_score cross_val_score(pipe, X_train.cutted_comment, y_train, <span class="hljs-attribute">cv</span>=5, <span class="hljs-attribute">scoring</span>=<span class="hljs-string">'accuracy'</span>).mean()</pre></div><p id="f823">咱们的模型在训练中的准确率如何呢?</p><div id="034c"><pre><span class="hljs-attribute">0</span>.<span class="hljs-number">820687244673089</span></pre></div><p id="bc59">这个结果,还是不错的。</p><p id="4464">回忆一下,总体的正向和负向情感,各占了数据集的一半。</p><p id="836a">如果我们建立一个 “笨模型”(dummy model),即所有的评论,都当成正向(或者负向)情感,准确率多少?</p><p id="491f">对,50%。</p><p id="ddb0">目前的模型准确率,远远超出这个数值。超出的这30%多,其实就是评论信息为模型带来的确定性。</p><p id="8bae">但是,不要忘了,我们不能光拿训练集来说事儿,对吧?下面咱们给模型来个考试。</p><p id="aa04">我们用训练集,把模型拟合出来。</p><div id="d025"><pre>pipe<span class="hljs-selector-class">.fit</span>(X_train<span class="hljs-selector-class">.cutted_comment</span>, y_train)</pre></div><p id="b428">然后,我们在测试集上,对情感分类标记进行预测。</p><div id="2d84"><pre>pipe.predict<span class="hljs-comment">(X_test.cutted_comment)</span></pre></div><figure id="4f88"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*yRHBOruRmGsCzKThiRUPWw.png"><figcaption></figcaption></figure><p id="bbc5">这一大串 0 和 1,你看得是否眼花缭乱?</p><p id="6ed5">没关系,scikit-learn 给我们提供了非常多的模型性能测度工具。</p><p id="5da2">我们先把预测结果保存到 <code>y_pred</code></p><div id="27e1"><pre><span class="hljs-attr">y_pred</span> = pipe.predict(X_test.cutted_comment)</pre></div><p id="e3f8">读入 scikit-learn 的测量工具集。</p><div id="10f6"><pre><span class="hljs-keyword">from</span> sklearn <span class="hljs-keyword">import</span> metrics</pre></div><p id="8ec4">我们先来看看测试准确率:</p><div id="f87a"><pre>metrics.accuracy_score<span class="hljs-comment">(y_test, y_pred)</span></pre></div><div id="f620"><pre><span class="hljs-attribute">0</span>.<span class="hljs-number">86</span></pre></div><p id="eb93">这个结果是不是让你很吃惊?没错,模型面对没有见到的数据,居然有如此高的情感分类准确性。</p><p id="d5c3">对于分类问题,光看准确率有些不全面,咱们来看看混淆矩阵。</p><div id="4e3f"><pre>metrics<span class="hljs-selector-class">.confusion_matrix</span>(y_test, y_pred)</pre></div><div id="cfb5"><pre><span class="hljs-built_in">array</span>(<span class="hljs-number">194</span>, <span class="hljs-number">43</span>], [ <span class="hljs-number">27</span>, <span class="hljs-number">236</span>)</pre></div><p id="817b">混淆矩阵中的 4 个数字,分别代表:</p><ul><li>TP: 本来是正向,预测也是正向的;</li><li>FP: 本来是负向,预测却是正向的;</li><li>FN: 本来是正向,预测却是负向的;</li><li>TN: 本来是负向,预测也是负向的。</li></ul><p id="b1cd">下面这张图(来自 <a href="https://goo.gl/5cYGZd">https://goo.gl/5cYGZd</a> )应该能让你更为清晰理解混淆矩阵的含义:</p><figure id="158e"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*chA3gtdc49jI41NjkQo-nA.png"><figcaption></figcaption></figure><p id="3067">写到这儿,你大概能明白咱们模型的性能了。</p><p id="ba02">但是总不能只把咱们训练出的模型和无脑 “笨模型” 去对比吧?这也太不公平了!</p><p id="a10a">下面,我们把老朋友 SnowNLP 呼唤出来,做个对比。</p><p id="ac09">如果你把它给忘了,请复习《<a href="https://medium.com/@wshuyi/%E5%A6%82%E4%BD%95%E7%94%A8python%E5%81%9A%E6%83%85%E6%84%9F%E5%88%86%E6%9E%90-9df4d49acaa3">如何用 Python 做情感分析?</a></p><div id="de75"><pre><span class="hljs-keyword">from</span> snownlp <span class="hljs-keyword">import</span> SnowNLP def get_sentiment(<span class="hljs-type">text</span>): <span class="hljs-keyword">return</span> SnowNLP(<span class="hljs-type">text</span>).sentiments</pre></div><p id="6cc8">我们利用测试集评论原始数据,让 SnowNLP 跑一遍,获得结果。</p><div id="cf15"><pre><span class="hljs-attr">y_pred_snownlp</span> = X_test.comment.apply(get_sentiment)</pre></div><p id="16c5">注意这里有个小问题。 SnowNLP 生成的结果,不是 0 和 1,而是 0 到 1 之间的小数。所以我们需要做一步转换,把 0.5 以上的结果当作正向,其余当作负向。</p><div id="c144"><pre><span class="hljs-attribute">y_pred_snownlp_normalized</span> = y_pred_snownlp.apply(lambda x: <span class="hljs-number">1</span> if x><span class="hljs-number">0</span>.<span class="hljs-number">5</span> else <span class="hljs-number">0</span>)</pre></div><p id="ee3a">看看转换后的前 5 条 SnowNLP 预测结果:</p><div id="5fee"><pre><span class="hljs-attribute">y_pred_snownlp_normalized</span>[:<span class="hljs-number">5</span>]</pre></div><figure id="ed76"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*l4zuGGrDYz-yMAKo2wmkxw.png"><figcaption></figcaption></figure><p id="40a3">好了,符合我们的要求。</p><p id="247f">下面我们先看模型分类准确率:</p><div id="040e"><pre>metrics.accuracy_score<span class="hljs-comment">(y_test, y_pred_snownlp_normalized)</span></pre></div><div id="c810"><pre><span class="hljs-attribute">0</span>.<span class="hljs-number">77</span></pre></div><p id="b05a">与之对比,咱们的测试集分类准确率,可是 0.86 哦。</p><p id="b14e">我们再来看看混淆矩阵。</p><div id="54ec"><pre>metrics.confusion_matrix(y_test, y_pred_snownlp_<span class="hljs-params">normal</span>ized)</pre></div><div id="34f6"><pre><span class="hljs-built_in">array</span>(<span class="hljs-number">189</span>, <span class="hljs-number">48</span>], [ <span class="hljs-number">67</span>, <span class="hljs-number">196</span>)</pre></div><p id="feb9">对比的结果,是 TP 和 TN 两项上,咱们的模型判断正确数量,都要超出 SnowNLP。</p><h1 id="4247">小结</h1><p id="1221">回顾一下,本文介绍了以下知识点:</p><ol><li>如何用一袋子词(bag of words)模型将自然语言语句向量化,形成特征矩阵;</li><li>如何利用停用词表、词频阈值和标记模式 (token pattern) 移除不想干的伪特征词汇,降低模型复杂度。</li><li>如何选用合适的机器学习分类模型,对词语特征矩阵做出分类;</li><li>如何用管道模式,归并和简化机器学习步骤流程;</li><li>如何选择合适的性能测度工具,对模型的效能进行评估和对比。</li></ol><p id="43d4">希望这些内容能够帮助你更高效地处理中文文本情感分类工作。</p><h1 id="4865">讨论</h1><p id="e87a">你之前用机器学习做过中文情感分类项目吗?你是如何去除停用词的?你使用的分类模型是哪个?获得的准确率怎么样?欢迎留言,把你的经验和思考分享给大家,我们一起交流讨论。</p><p id="ae39">如果你觉得本文有用,请<b>点击 clap</b></p><p id="017f">如果本文可能对你的朋友有帮助,请<b>转发</b>给他们。</p><p id="70f6"><a href="https://wshuyi.medium.com/">欢迎<b>关注</b>我的专栏</a>,以便及时收到后续的更新内容。</p><p id="16fb">如果需要阅读我的文章全集,请关注我的微信公众号「玉树芝兰」,注意如果不<b>加星标</b>,会<b>错过新推送提示</b></p><figure id="2045"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*aDET_8dfw3mc1CzwGV7pmw.jpeg"><figcaption></figcaption></figure><p id="acce">知识星球目前已发布了数十篇精华文章。我把标题和链接做了个表格,放在了飞书文档。欢迎你 <a href="https://ebc9e17yfy.feishu.cn/sheets/shtcnPlx23mqQ57mVGUXH5bsxcx">点击这个链接查看</a></p><p id="4eaa">欢迎关注我的视频号,时常更新。</p><figure id="6cf7"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*JljqrPL7ml2P0C5e1stojA.jpeg"><figcaption></figcaption></figure><p id="b775">题图:Photo by <a href="https://unsplash.com/@artbyhybrid?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Hybrid</a> on <a href="https://unsplash.com/s/photos/feeling?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p></article></body>

如何用 Python 和机器学习训练中文文本情感分类模型?

利用 Python 机器学习框架 scikit-learn,我们自己做一个分类模型,对中文评论信息做情感分析。其中还会介绍中文停用词的处理方法。

疑惑

前些日子,我在微信后台收到了一则读者的留言。

我一下子有些懵 — — 这怎么还带点播了呢?

但是旋即我醒悟过来,好像是我自己之前挖了个坑。

之前我写过《 如何用 Python 从海量文本抽取主题? 》一文,其中有这么一段:

为了演示的流畅,我们这里忽略了许多细节。很多内容使用的是预置默认参数,而且完全忽略了中文停用词设置环节,因此“这个”、“如果”、“可能”、“就是”这样的停用词才会大摇大摆地出现在结果中。不过没有关系,完成比完美重要得多。知道了问题所在,后面改进起来很容易。有机会我会写文章介绍如何加入中文停用词的去除环节。

根据 “自己挖坑自己填” 的法则,我决定把这一部分写出来。

我可以使用偷懒的办法。

例如在原先的教程里,更新中文停用词处理部分,打个补丁。

但是,最近我发现,好像至今为止,我们的教程从来没有介绍过如何用机器学习做情感分析。

你可能说,不对吧?

情感分析不是讲过了吗?老师你好像讲过《 如何用 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.

那么我们就可以简单粗暴地抽取出以下特征(其实就是把所有的单词都罗列一遍):

  • I
  • love
  • 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 环境才会作为核心(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()

注意我们的数据处理流程是这样的:

  1. 特征向量化;
  2. 朴素贝叶斯分类。

如果每次修改一个参数,或者换用测试集,我们都需要重新运行这么多的函数,肯定是一件效率不高,且令人头疼的事儿。而且只要一复杂,出现错误的几率就会增加。

幸好,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 个数字,分别代表:

  • TP: 本来是正向,预测也是正向的;
  • FP: 本来是负向,预测却是正向的;
  • FN: 本来是正向,预测却是负向的;
  • TN: 本来是负向,预测也是负向的。

下面这张图(来自 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。

小结

回顾一下,本文介绍了以下知识点:

  1. 如何用一袋子词(bag of words)模型将自然语言语句向量化,形成特征矩阵;
  2. 如何利用停用词表、词频阈值和标记模式 (token pattern) 移除不想干的伪特征词汇,降低模型复杂度。
  3. 如何选用合适的机器学习分类模型,对词语特征矩阵做出分类;
  4. 如何用管道模式,归并和简化机器学习步骤流程;
  5. 如何选择合适的性能测度工具,对模型的效能进行评估和对比。

希望这些内容能够帮助你更高效地处理中文文本情感分类工作。

讨论

你之前用机器学习做过中文情感分类项目吗?你是如何去除停用词的?你使用的分类模型是哪个?获得的准确率怎么样?欢迎留言,把你的经验和思考分享给大家,我们一起交流讨论。

如果你觉得本文有用,请点击 clap

如果本文可能对你的朋友有帮助,请转发给他们。

欢迎关注我的专栏,以便及时收到后续的更新内容。

如果需要阅读我的文章全集,请关注我的微信公众号「玉树芝兰」,注意如果不加星标,会错过新推送提示

知识星球目前已发布了数十篇精华文章。我把标题和链接做了个表格,放在了飞书文档。欢迎你 点击这个链接查看

欢迎关注我的视频号,时常更新。

题图:Photo by Hybrid on Unsplash

Python
Sentiment Analysis
Recommended from ReadMedium