参赛作品
自然语言处理(NLP)是机器学习的流行且重要领域。 在这个中心,我将描述我的第一个项目,该项目与以Python编写的电影评论的情感色彩分析有关。 情感分析的任务在想要掌握NLP基本概念的人们中很常见,并且可以成为该领域“ Hello world”的类似物。
在本文中,我们将经历数据科学过程的所有主要阶段:从创建自己的数据集,对其进行处理并使用NLTK库提取特征,最后使用scikit-learn学习和调整模型。 任务本身就是将评论分为三类:负面,中立和正面。
数据语料库的形成
为了解决这个问题,可以使用一些现成的,带有注释的数据主体以及IMDB的评论,其中有很多在GitHub上。 但是决定使用Kinopoisk的俄语评论来创建您自己的评论。 为了不手动复制它们,我们将编写一个Web解析器。 我将使用
请求库发送http
请求 ,并使用
BeautifulSoup处理html文件。 首先,让我们定义一个将链接到电影评论并检索它们的函数。 为了使Kinopoisk无法识别我们中的机器人,您需要在request.get函数中指定
headers参数,该函数将模拟浏览器。 必须使用User-Agent,Accept-language和Accept键将字典传递到其中,其值可以在浏览器开发人员工具中找到。 接下来,创建一个解析器,并从页面中检索评论,这些评论存储在_reachbanner_ html标记类中。
import requests from bs4 import BeautifulSoup import numpy as np import time import os def load_data(url): r = requests.get(url, headers = headers)
我们摆脱了html标记,但是,我们的评论仍然是
BeautifulSoup对象,但是我们需要将它们转换为字符串。
convert函数就是这样做的。 我们还将编写一个函数来检索电影的名称,该名称随后将用于保存评论。
def convert(reviews):
解析器的最后一个功能将带有指向电影主页的链接,评论类以及保存评论的方法。 该功能还定义了避免禁令所必需的请求之间的
延迟 。 该函数包含一个循环,该循环从第一页开始检索并存储评论,直到遇到不存在的页面为止,
load_data函数将从该页面提取一个空列表,并且循环将中断。
def parsing(url, status, path): page = 1 delays = [11, 12, 13, 11.5, 12.5, 13.5, 11.2, 12.3, 11.8] name = get_name(url) time.sleep(np.random.choice(delays))
然后,使用以下循环,您可以从
urles列表中的电影中提取评论。 电影列表将需要手动创建。 例如,有可能通过编写一个函数来获取电影的链接列表,该函数将从前250部电影搜索电影中提取电影,而不是手动进行,但是15至20部电影就足以形成一个小数据集,每个班级都有一千条评论。 另外,如果您获得了禁令,则该程序将显示解析器在哪部影片和类上停止播放,以便在通过禁令后从同一位置继续。
path =
预处理
编写了一个解析器,为他召回了随机的电影和电影搜索的若干禁令后,我将评论混入了文件夹中,并从每个班级选择了900条评论进行培训,其余的则作为对照组的评论。 现在有必要对外壳进行预处理,即对其进行标记化和标准化。 标记化是指将文本分解为多个部分,在这种情况下为单词,因为我们将使用一袋单词的表示形式。 归一化包括将单词转换为小写字母,消除停用词和过多的噪音,阻塞以及其他有助于减少符号空间的技巧。
我们导入必要的库。
隐藏文字 from nltk.corpus import PlaintextCorpusReader from nltk.stem.snowball import SnowballStemmer from nltk.probability import FreqDist from nltk.tokenize import RegexpTokenizer from nltk import bigrams from nltk import pos_tag from collections import OrderedDict from sklearn.metrics import classification_report, accuracy_score from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import GridSearchCV from sklearn.utils import shuffle from multiprocessing import Pool import numpy as np from scipy.sparse import csr_matrix
我们首先定义一些用于文本预处理的小函数。 第一个称为
lower_pos_tag,它将获取一个包含单词的列表,将其转换为小写,然后将每个标记及其词性保存到一个元组中。 将词性添加到单词的操作称为词性(POS)标记,通常在NLP中用于提取实体。 在我们的例子中,我们将在以下功能中使用词性来过滤单词。
def lower_pos_tag(words): lower_words = [] for i in words: lower_words.append(i.lower()) pos_words = pos_tag(lower_words, lang='rus') return pos_words
文本中包含大量的单词,这些单词经常被发现对模型没有用(所谓的停用词)。 基本上,这些是介词,连词,代词,通过它们无法确定召回的类别。
clean函数仅保留名词,形容词,动词和副词。 请注意,由于模型本身不需要这些部分,因此它会删除部分词性。 您还可以注意到该函数使用了填充功能,其实质是从单词中删除后缀和前缀。 这使您可以减小符号的尺寸,因为具有不同属和大小写的单词将被简化为相同的标记。 有一个更强大的词干比喻-lemmatization,它使您可以恢复单词的初始形式。 但是,它的工作速度比填塞要慢,此外,NLTK还没有俄语lemmatizer。
def clean(words): stemmer = SnowballStemmer("russian") cleaned_words = [] for i in words: if i[1] in ['S', 'A', 'V', 'ADV']: cleaned_words.append(stemmer.stem(i[0])) return cleaned_words
接下来,我们编写最终函数,该函数将带有类标签并检索该类的所有评论。 为了阅读案例,我们将使用
PlaintextCorpusReader对象的
raw方法,该方法允许您从指定的文件中提取文本。 接下来,在正则表达式的基础上使用RegexpTokenizer进行标记化。 除了单个单词之外,我还在模型bigrams中添加了所有相邻单词的组合。 此函数还使用
FreqDist对象,该对象返回单词出现的频率。 它在此处用于删除在特定类别的所有评论中仅出现一次的单词(它们也称为hapaks)。 因此,该函数将返回一个字典,其中包含以单词袋形式呈现的文档以及特定类的所有单词列表。
corpus_root =
预处理阶段是最长的,因此并行处理我们的案件是有意义的。 这可以使用
多处理模块来完成。 在下一部分程序代码中,我将启动三个进程,这些进程将同时处理具有不同类的三个文件夹。 接下来,将结果收集在一个字典中。 预处理完成。
if __name__ == '__main__': data = {} labels = ['neutral', 'bad', 'good'] p = Pool(3) result = p.map(process, labels) for i in result: data.update(i) p.close()
向量化
在对案例进行预处理之后,我们将拥有一本字典,其中的每个类别标签都包含一个列表,其中包含我们对标记进行了标记化,规范化和丰富化的评论,以及该类所有评论中的单词列表。 由于模型无法像我们一样感知自然语言,因此现在的任务是以数字形式显示我们的评论。 为此,我们将创建一个由唯一标记组成的通用词汇表,并通过它对每个评论进行矢量化处理。
首先,我们创建一个列表,其中包含所有类别的评论及其标签。 接下来,我们创建一个通用词汇表,使用同一
FreqDist的
most_common方法从每个类中提取10,000个最常用的单词。 结果,我得到了约17,000个单词的词汇。
有几种矢量化文本的方法。 其中最受欢迎的是:TF-IDF,直接和频率编码。 我使用了频率编码,其本质是将每个评论作为矢量呈现,其要素是词汇中每个单词出现的次数。
NLTK拥有自己的分类器,您可以使用它们,但是它们的工作速度比
scikit-learn的分类器慢,并且设置较少。 以下是用于
NLTK编码的代码。 但是,我将使用
scikit-learn的朴素贝叶斯模型并对评论进行编码,将属性存储在
SciPy的稀疏矩阵中,并将类标签存储在单独的
NumPy数组中。
由于在数据集中带有特定标签的评论会依次出现,也就是说,所有评论都是中性的,然后都是负面的,依此类推,因此您需要将它们混合在一起。 为此,您可以使用
scikit-learn中的
shuffle函数。 它仅适用于符号和类标签位于不同数组中的情况,因为它允许您将两个数组统一使用。
模型训练
现在,仍然需要训练模型并在对照组中检查其准确性。 作为模型,我们将使用朴素贝叶斯分类器的模型。
Scikit-learn根据数据分布具有三种朴素贝叶斯模型:二进制,离散和连续。 由于特征的分布是离散的,因此我们选择
MultinomialNB 。
贝叶斯分类器具有
alpha hyper
参数 ,该
参数负责平滑模型。 朴素贝叶斯(Naive Bayes)计算属于所有类别的每个评论的概率,如果所有评论单词都属于特定类别,则乘以所有评论单词出现的条件概率。 但是,如果在训练数据集中未找到某个复习词,则其条件概率等于零,这会使该复习属于任何类别的概率无效。 为了避免这种情况,默认情况下,将一个单位添加到所有条件词概率中,即
alpha等于1。 但是,此值可能不是最佳的。 您可以尝试使用网格搜索和交叉验证来选择
Alpha 。
parameter = [1, 0, 0.1, 0.01, 0.001, 0.0001] param_grid = {'alpha': parameter} grid_search = GridSearchCV(MultinomialNB(), param_grid, cv=5) grid_search.fit(X, Y) Alpha, best_score = grid_search.best_params_, grid_search.best_score_
在我的情况下,网格炉床给出的最佳超参数值为0,精度为0.965。 但是,该值对于控制数据集显然不是最佳的,因为在训练集中以前没有找到大量单词。 对于参考数据集,此模型的精度为0.598。 但是,如果将
alpha增加到0.1,则训练数据的准确性将下降到0.82,而在控制数据上的准确性将增加到0.62。 在更大的数据集上,差异很有可能会更大。
model = MultinomialNB(0.1) model.fit(X, Y)
结论
假定该模型应用于预测其单词未用于形成词汇表的评论。 因此,可以通过模型在数据控制部分的准确性(即0.62)来评估模型的质量。 这几乎比仅仅猜测要好两倍,但是准确性仍然很低。
根据分类报告,很明显,该模型在具有中性色的评论中表现最差(准确度为0.47,正面为0.68,负面为0.76)。 确实,中立的评论包含正面和负面评论都具有的特征。 可能通过增加数据集的体积来提高模型的准确性,因为第3,000个数据集相对较小。 同样,可以将问题简化为评论的肯定和否定的二进制分类,这也将提高准确性。
感谢您的阅读。
PS:如果您想练习,可以在链接下方下载我的数据集。
链接到数据集