【NLP】将机器学习应用于情感分析

  🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 – 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

在现代互联网和社交媒体时代,人们的意见、评论和建议已成为政治科学和企业的宝贵资源。借助现代技术,我们现在能够最有效地收集和分析此类数据。在本章中,我们将深入研究自然语言处理NLP ) 的一个子领域,称为情感分析,并学习如何使用机器学习算法根据情感对文档进行分类:作者的态度。特别是,我们将使用来自Internet 电影数据库IMDb ) 的 50,000 条电影评论的数据集,并构建一个可以区分正面和负面评论的预测器。

我们将在本章中介绍的主题包括以下内容:

  • 清理和准备文本数据
  • 从文本文档构建特征向量
  • 训练机器学习模型来分类正面和负面的电影评论
  • 使用核外学习处理大型文本数据集
  • 从文档集合中推断主题以进行分类

为文本处理准备 IMDb 电影评论数据

作为提到,情感分析,有时还称为意见挖掘,是更广泛的 NLP 领域的一个流行的子学科;它与分析文档的情绪有关。情感分析中的一项流行任务是根据作者对特定主题的表达意见或情感对文档进行分类。

在本章中,我们将使用 Andrew Maas 等人收集的来自 IMDb 的大型电影评论数据集(Learning Word Vectors for Sentiment Analysis by AL MaasRE DalyPT PhamD. HuangAY Ng,和C. Potts第 49 届计算语言学协会年会论文集:人类语言技术,第 142-150 页,美国俄勒冈州波特兰市,协会对于计算语言学,2011 年 6 月)。电影评论数据集由 50,000 条极地电影评论组成,这些评论被标记为正面或负面;在这里,正面表示一部电影在 IMDb 上的评分超过 6 星,而负面表示一部电影在 IMDb 上的评分少于 5 星。在接下来的部分中,我们将下载数据集,将其预处理为可用于机器学习工具的格式,并从这些电影评论的子集中提取有意义的信息,以构建一个机器学习模型,该模型可以预测某个评论者是否喜欢或不喜欢某部电影。电影。

获取影评数据集

电影评论数据集的压缩存档 (84.1 MB) 可以从Sentiment Analysis作为 gzip 压缩的 tarball 存档下载:

  • 如果你是使用 Linux 或 macOS,您可以打开一个新的终端窗口,cd进入下载目录,然后执行tar -zxf aclImdb_v1.tar.gz解压缩数据集。
  • 如果您使用的是 Windows,则可以下载免费的存档程序,例如 7-Zip ( http://www.7-zip.org ),以从下载存档中提取文件。
  • 或者,您可以直接在 Python 中解压缩 gzip 压缩的 tarball 存档,如下所示:
    >>> import tarfile
    >>> with tarfile.open('aclImdb_v1.tar.gz', 'r:gz') as tar:
    ...     tar.extractall()

将电影数据集预处理为更方便的格式

已经成功提取数据集后,我们现在将解压缩下载存档中的各个文本文档组合成单个 CSV 文件。在下面的代码部分中,我们将把电影评论读入一个 pandasDataFrame对象,这在标准台式计算机上可能需要 10 分钟。

可视化进度和预计完成时间,我们将使用几年前为此目的开发的Python 进度指示器PyPrind,https ://pypi.python.org/pypi/PyPrind/)包。PyPrind 可以通过执行以下pip install pyprind命令来安装:

>>> import pyprind
>>> import pandas as pd
>>> import os
>>> import sys
>>> # change the 'basepath' to the directory of the
>>> # unzipped movie dataset
>>> basepath = 'aclImdb'
>>>
>>> labels = {'pos': 1, 'neg': 0}
>>> pbar = pyprind.ProgBar(50000, stream=sys.stdout)
>>> df = pd.DataFrame()
>>> for s in ('test', 'train'):
...     for l in ('pos', 'neg'):
...         path = os.path.join(basepath, s, l)
...         for file in sorted(os.listdir(path)):
...             with open(os.path.join(path, file),
...                       'r', encoding='utf-8') as infile:
...                 txt = infile.read()
...             df = df.append([[txt, labels[l]]],
...                            ignore_index=True)
...             pbar.update()
>>> df.columns = ['review', 'sentiment']
0%                          100%
[##############################] | ETA: 00:00:00
Total time elapsed: 00:00:25

在前面的代码中,我们首先初始化了一个新的进度条对象 ,pbar迭代次数为 50,000,这是我们要读入的文档数。使用嵌套for循环,我们遍历主目录中的train和子目录,并读取我们最终附加到pandas的和子目录中的单个文本文件,以及一个整数类标签(1 = 正和 0 = 负)。testaclImdbposnegdfDataFrame

由于类标签在组装的数据集被排序,我们现在将DataFrame使用子模块中的permutation函数np.random打乱 – 这将有助于在后面的部分将数据集拆分为训练和测试数据集,当我们直接从本地驱动器流式传输数据时。

为方便起见,我们还将组装和打乱的电影评论数据集存储为 CSV 文件:

>>> import numpy as np
>>> np.random.seed(0)
>>> df = df.reindex(np.random.permutation(df.index))
>>> df.to_csv('movie_data.csv', index=False, encoding='utf-8')

由于我们将在本章后面使用此数据集,因此让我们通过读取 CSV 并打印前三个示例的摘录,快速确认我们已成功以正确的格式保存数据:

>>> df = pd.read_csv('movie_data.csv', encoding='utf-8')
>>> # the following column renaming is necessary on some computers:
>>> df = df.rename(columns={"0": "review", "1": "sentiment"})
>>> df.head(3)

如果您在 Jupyter 笔记本中运行代码示例,您现在应该会看到数据集的前三个示例,如图 8.1所示:

                                                        图 8.1:电影评论数据集的前三行

作为一个健全的检查,在我们继续下一部分之前,让我们确保DataFrame包含所有 50,000 行:

>>> df.shape
(50000, 2)

引入词袋模型

您可能还记得第 4 章构建良好的训练数据集 – 数据预处理,我们必须将分类数据(例如文本或单词)转换为数字形式,然后才能将其传递给机器学习算法。在本节中,我们将介绍词袋模型,它允许我们将文本表示为数字特征向量。bag-of-words背后的想法很简单,可以总结如下:

  1. 我们从整组文档中创建一个唯一标记的词汇表,例如单词。
  2. 我们从每个文档构造一个特征向量,其中包含每个单词在特定文档中出现的频率。

由于每个文档中唯一的单词仅代表词袋词汇表中所有单词的一小部分,因此特征向量将主要由零组成,这就是我们称它们为稀疏的原因。如果这听起来太抽象,请不要担心;在以下小节中,我们将逐步介绍创建简单的词袋模型的过程。

将单词转换为特征向量

构建词袋基于字数的模型各自的文档,我们可以使用CountVectorizerscikit-learn 中实现的类。正如您将在以下代码部分中看到的那样,CountVectorizer获取一个文本数据数组,可以是文档或句子,并为我们构建词袋模型:

>>> import numpy as np
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count = CountVectorizer()
>>> docs = np.array(['The sun is shining',
...                  'The weather is sweet',
...                  'The sun is shining, the weather is sweet,'
...                  'and one and one is two'])
>>> bag = count.fit_transform(docs)

通过调用fit_transformon 方法CountVectorizer,我们构建了词袋模型的词汇表,并将以下三个句子转化为稀疏特征向量:

  • 'The sun is shining'
  • 'The weather is sweet'
  • 'The sun is shining, the weather is sweet, and one and one is two'

现在,让我们打印词汇表的内容,以更好地理解底层概念:

>>> print(count.vocabulary_)
{'and': 0,
'two': 7,
'shining': 3,
'one': 2,
'sun': 4,
'weather': 8,
'the': 6,
'sweet': 5,
'is': 1}

尽你所能从执行前面的命令可以看出,词汇表存储在 Python 字典中,该字典将唯一单词映射到整数索引。接下来,让我们打印刚刚创建的特征向量:

>>> print(bag.toarray())
[[0 1 0 1 1 0 1 0 0]
 [0 1 0 0 0 1 1 0 1]
 [2 3 2 1 1 1 2 1 1]]

此处显示的特征向量中的每个索引位置对应于作为字典项存储在CountVectorizer词汇表中的整数值。例如,索引位置的第一个特征0类似于单词 的计数'and',它只出现在最后一个文档中,而'is'索引位置的单词1(文档向量中的第二个特征)出现在所有三个句子中。特征向量中的这些值也称为原始词频tf ( t ,  d )——词条t在文档中出现的次数d。需要注意的是,在词袋模型中,句子或文档中的单词或术语顺序无关紧要。词频出现在特征向量中的顺序来自词汇索引,这些索引通常按字母顺序分配。

N-gram 模型

我们刚刚创建的词袋模型中的项目序列也称为 1-gram 或一元模型——词汇表中的每个项目或标记代表一个单词。更一般地,项目中的连续序列NLP(单词、字母或符号)也称为n-gram。n-gram 模型中数字n的选择取决于特定的应用;例如,Ioannis Kanaris 和其他人的一项研究表明,大小为 3 和 4 的 n-gram 在电子邮件的反垃圾邮件过滤中产生了良好的性能(Ioannis Kanaris的 Words vs character n-grams for anti-spam filtering , Konstantinos Kanaris , Ioannis HouvardasEfstathios Stamatatos ,国际人工智能工具杂志,世界科学出版公司, 16(06): 1047-1067, 2007)。

总结 n-gram 表示的概念,我们的第一个文档“太阳正在发光”的 1-gram 和 2-gram 表示将构造如下:

  • 1-gram: “the”, “sun”, “is”, “shining”
  • 2-gram: “the sun”, “sun is”, “is shining”

scikit-learn 中的CountVectorizer类允许我们通过其ngram_range参数使用不同的 n-gram 模型。虽然默认使用 1-gram 表示,但我们可以通过初始化一个新CountVectorizer实例来切换到 2-gram 表示ngram_range=(2,2)

通过词频-逆文档频率评估词的相关性

当我们分析文本数据时,我们经常会遇到两个类的多个文档中出现的单词。这些经常出现的词通常不包含有用的或歧视性的信息。在本小节中,您将了解一种有用的技术,称为频率逆文档频率tf-idf ),可用于降低特征向量中这些频繁出现的词的权重。tf-idf 可以定义为词频和逆文档频率的乘积:

tf-idf ( t ,  d ) =  tf ( t ,  d ) ×  idf ( t ,  d )

这里,tf ( t ,  d )是我们上一节介绍的词频,idf ( t ,  d )是逆文档频率,可以计算如下:

这里,d是文档总数,df ( d ,  t ) 是包含术语t的文档数d。请注意,将常数 1 添加到分母是可选的,其目的是为没有出现在任何训练示例中的术语分配非零值;日志用于确保低文档频率不会被赋予过多的权重。

scikit-learn图书馆实现了另一个转换器,TfidfTransformer类,它将类中的原始词频CountVectorizer作为输入并将它们转换为 tf-idfs:

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> tfidf = TfidfTransformer(use_idf=True,
...                          norm='l2',
...                          smooth_idf=True)
>>> np.set_printoptions(precision=2)
>>> print(tfidf.fit_transform(count.fit_transform(docs))
...       .toarray())
[[ 0.    0.43  0.    0.56  0.56  0.    0.43  0.    0.  ]
 [ 0.    0.43  0.    0.    0.    0.56  0.43  0.    0.56]
 [ 0.5   0.45  0.5   0.19  0.19  0.19  0.3   0.25  0.19]]

正如您在上一小节中看到的,该词'is'在第三个文档中的词频最高,是出现频率最高的词。然而,在将相同的特征向量转换为 tf-idfs 之后,该词'is'现在与第三个文档中相对较小的 tf-idf (0.45) 相关联,因为它也存在于第一个和第二个文档中,因此不太可能包含任何有用的歧视性信息。

但是,如果我们手动计算特征向量中各个项的 tf-idfs,我们会注意到TfidfTransformer与我们之前定义的标准教科书方程相比,计算 tf-idfs 的方法略有不同。在 scikit-learn 中实现的逆文档频率方程计算如下:

同样,在 scikit-learn 中计算的 tf-idf 与我们之前定义的默认方程略有偏差:

tf-idf ( t ,  d ) =  tf ( t ,  d ) × ( idf ( t ,  d ) + 1)

请注意,前面 idf 等式中的“+1”是由于smooth_idf=True前面代码示例中的设置,这有助于将零权重(即idf ( t ,  d ) = log(1) = 0)分配给满足以下条件的项出现在所有文档中。

虽然它也是更典型的标准化在计算 tf-idfs 之前的原始词频,TfidfTransformer该类直接对 tf-idfs 进行归一化。默认(norm='l2'TfidfTransformer_

为了确保我们理解如何工作,让我们通过一个示例来计算第三个文档TfidfTransformer中单词的 tf-idf 。'is'该词在第三个文档中的'is'词频为 3(tf  = 3),并且该词的文档频率为 3,因为该词'is'出现在所有三个文档中(df  = 3)。因此,我们可以计算逆文档频率如下:

现在,为了计算 tf-idf,我们只需将逆文档频率加 1 并乘以词频:

如果我们对第三个文档中的所有术语重复此计算,我们将获得以下 tf-idf 向量[3.39, 3.0, 3.39, 1.29, 1.29, 1.29, 2.0, 1.69, 1.29]:但是,请注意,此特征向量中的值与TfidfTransformer我们之前使用的值不同。我们在这个 tf-idf 计算中缺少的最后一步是 L2 归一化,它可以应用如下:

如您所见,现在的结果匹配 scikit-learn 返回的结果TfidfTransformer,既然您现在了解了如何计算 tf-idfs,让我们继续下一节并将这些概念应用于电影评论数据集。

清理文本数据

在上一个在小节中,我们了解了词袋模型、词频和 tf-idfs。然而,第一个重要步骤——在我们构建词袋模型之前——是通过去除所有不需要的字符来清理文本数据。

为了说明为什么这很重要,让我们显示重新洗牌的电影评论数据集中第一个文档的最后 50 个字符:

>>> df.loc[0, 'review'][-50:]
'is seven.<br /><br />Title (Brazil): Not Available'

正如您在此处看到的,文本包含 HTML 标记以及标点符号和其他非字母字符。虽然 HTML 标记不包含许多有用的语义,但标点符号可以表示某些 NLP 上下文中有用的附加信息。然而,为简单起见,我们现在将删除除表情符号之外的所有标点符号,例如:),因为这些符号对于情感分析肯定有用。

为了完成这个任务,我们将使用 Python 的正则表达式regex ) 库,re如下所示:

>>> import re
>>> def preprocessor(text):
...     text = re.sub('<[^>]*>', '', text)
...     emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',
...                            text)
...     text = (re.sub('[\W]+', ' ', text.lower()) +
...             ' '.join(emoticons).replace('-', ''))
...     return text

通过前面代码部分中的第一个正则表达式 ,<[^>]*>我们尝试从电影评论中删除所有 HTML 标记。尽管许多程序员通常建议不要使用正则表达式来解析 HTML,但这个正则表达式应该足以清理这个特定的数据集。由于我们只对删除 HTML 标记感兴趣并且不打算进一步使用 HTML 标记,因此使用正则表达式来完成这项工作应该是可以接受的。但是,如果您更喜欢使用复杂的工具从文本中删除 HTML 标记,您可以查看 Python 的 HTML解析器模块,在html.parser — Simple HTML and XHTML parser — Python 3.10.7 documentation中有描述。删除 HTML 标记后,我们使用了查找表情符号的正则表达式稍微复杂一些,我们将其临时存储为表情符号。接下来,我们通过正则表达式从文本中删除所有非单词字符,并将文本[\W]+转换为小写字符。

处理单词大写

在这种情况下分析,我们假设一个单词的大写——例如,它是否出现在句子的开头——不包含语义相关的信息。但是,请注意也有例外;例如,我们删除了专有名称的符号。但同样,在此分析的上下文中,字母大小写不包含与情绪分析相关的信息是一个简化的假设。

最终,我们将临时存储的表情符号添加到处理后的文档字符串的末尾。此外,为了保持一致性,我们从表情符号中删除了鼻子字符(- in :-))。

常用表达

虽然正则表达式提供了一种高效便捷的方法来搜索字符串中的字符,它们还具有陡峭的学习曲线。不幸的是,对正则表达式的深入讨论超出了本书的范围。但是,您可以在https://developers.google.com/edu/python/regular-expressions上的 Google Developers 门户上找到很棒的教程,或者您可以查看 Pythonre模块的官方文档在re — Regular expression operations — Python 3.9.14 documentation。

尽管将表情符号字符添加到已清理文档字符串的末尾可能看起来不是最优雅的方法,但我们必须注意,如果我们的词汇表包含以下内容,那么在我们的词袋模型中单词的顺序并不重要只有一个词的标记。但在我们更多地讨论将文档拆分为单独的术语、单词或标记之前,让我们确认我们的preprocessor函数是否正常工作:

>>> preprocessor(df.loc[0, 'review'][-50:])
'is seven title brazil not available'
>>> preprocessor("</a>This :) is :( a test :-)!")
'this is a test :) :( :)'

最后,因为我们将在接下来的部分中一遍又一遍地使用清理preprocessor过的文本数据,现在让我们将我们的函数应用于我们的所有电影评论DataFrame

>>> df['review'] = df['review'].apply(preprocessor)

将文档处理成令牌

成功后准备电影评论数据集,我们现在需要考虑如何将文本语料库拆分为单个元素。标记文档的一种方法是通过在空白字符处拆分清理过的文档来将它们拆分为单个单词:

>>> def tokenizer(text):
...     return text.split()
>>> tokenizer('runners like running and thus they run')
['runners', 'like', 'running', 'and', 'thus', 'they', 'run']

在标记化的上下文中,另一个有用的技术是词干提取,即将一个词转换成它的词根形式的过程。它允许我们将相关的词映射到同一个词干。最初的词干提取算法由 Martin F. Porter 于 1979 年开发,因此被称为Porter 词干算法(Martin F. Porter后缀剥离算法程序:电子图书馆和信息系统,14(3): 130–137, 1980)。自然语言工具包( NLTK , http://www.nltk.org )Python 实现了 Porter 词干算法,我们将在下面的代码部分中使用该算法。要安装 NLTK,您只需执行conda install nltkpip install nltk.

NLTK 在线图书

虽然 NLTK 不是本章的重点,但我强烈建议您访问 NLTK 网站并阅读 NLTK 官方书籍,如果您是对 NLP 中更高级的应用感兴趣。

以下代码展示了如何使用 Porter 词干提取算法:

>>> from nltk.stem.porter import PorterStemmer
>>> porter = PorterStemmer()
>>> def tokenizer_porter(text):
...     return [porter.stem(word) for word in text.split()]
>>> tokenizer_porter('runners like running and thus they run')
['runner', 'like', 'run', 'and', 'thu', 'they', 'run']

使用PorterStemmerfromnltk包,我们修改了我们的tokenizer函数以减少单词它们的根形式,由前面的简单示例,其中单词'running'提取为词根'run'

词干算法

波特词干算法可能是最古老和最简单的词干提取算法。其他流行的词干提取算法包括较新的 Snowball 词干提取器(Porter2 或英语词干提取器)和 Lancaster 词干提取器(Paice/Husk 词干提取器)。虽然 Snowball 和 Lancaster 词干分析器都比原来的 Porter 词干分析器更快,但 Lancaster 词干分析器也因比 Porter 词干分析器更具攻击性而臭名昭著,这意味着它会产生更短、更晦涩的词。这些替代词干算法也可以通过 NLTK 包 ( NLTK :: nltk.stem package ) 获得。

虽然词干提取可以创建非真实的单词,例如'thu'(from 'thus'),如前面的示例所示,但称为词形还原的技术旨在获得单个词的规范(语法正确)形式,所谓的lemmas。然而,与词干提取相比,词形还原在计算上更加困难和昂贵,并且在实践中,已经观察到词干提取和词形还原对文本分类的性能影响很小(词归一化对文本分类的影响,作者: Michal TomanRoman Tesar,和Karel Jezek, InSciT会议记录,第 354-358 页,2006 年)。

在我们进入下一部分之前,我们将使用词袋模型训练机器学习模型,让我们简要讨论另一个有用的主题,称为停用词去除。停用词只是那些在各种文本中非常常见的词,可能没有(或只有很少)有用的信息,可用于区分不同类别的文档。停用词的例子areand , has , and like。如果我们使用原始或规范化的词频而不是 tf-idfs,删除停用词可能很有用,因为 tf-idfs 已经降低了频繁出现的词的权重。

从电影评论,我们将使用 127 的集合NLTK库中提供的英文停用词,可以通过调用nltk.download函数获取:

>>> import nltk
>>> nltk.download('stopwords')

下载停用词集后,我们可以加载并应用英文停用词集,如下所示:

>>> from nltk.corpus import stopwords
>>> stop = stopwords.words('english')
>>> [w for w in tokenizer_porter('a runner likes'
...  ' running and runs a lot')
...  if w not in stop]
['runner', 'like', 'run', 'run', 'lot']

训练用于文档分类的逻辑回归模型

在本节中,我们将培训后勤回归模型基于词袋模型将电影评论分为正面负面评论。首先,我们将DataFrame清洗后的文本文档分为 25,000 个用于训练的文档和 25,000 个用于测试的文档:

>>> X_train = df.loc[:25000, 'review'].values
>>> y_train = df.loc[:25000, 'sentiment'].values
>>> X_test = df.loc[25000:, 'review'].values
>>> y_test = df.loc[25000:, 'sentiment'].values

接下来,我们将使用一个GridSearchCV对象使用 5 折分层交叉验证为我们的逻辑回归模型找到最佳参数集:

>>> from sklearn.model_selection import GridSearchCV
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> tfidf = TfidfVectorizer(strip_accents=None,
...                         lowercase=False,
...                         preprocessor=None)
>>> small_param_grid = [
...     {
...         'vect__ngram_range': [(1, 1)],
...         'vect__stop_words': [None],
...         'vect__tokenizer': [tokenizer, tokenizer_porter],
...         'clf__penalty': ['l2'],
...         'clf__C': [1.0, 10.0]
...     },
...     {
...         'vect__ngram_range': [(1, 1)],
...         'vect__stop_words': [stop, None],
...         'vect__tokenizer': [tokenizer],
...         'vect__use_idf':[False],
...         'vect__norm':[None],
...         'clf__penalty': ['l2'],
...         'clf__C': [1.0, 10.0]
...     },
... ]
>>> lr_tfidf = Pipeline([
...     ('vect', tfidf),
...     ('clf', LogisticRegression(solver='liblinear'))
... ])
>>> gs_lr_tfidf = GridSearchCV(lr_tfidf, small_param_grid,
...                            scoring='accuracy', cv=5,
...                            verbose=2, n_jobs=1)
>>> gs_lr_tfidf.fit(X_train, y_train)

请注意,对于逻辑回归'lbfgs'分类器,我们使用 LIBLINEAR 求解器,因为对于相对较大的数据集,它的性能优于默认选择 ( )。

通过 n_jobs 参数进行多处理

请注意,我们强烈建议设置n_jobs=-1(而不是n_jobs=1之前的代码示例中的 )以利用您机器上的所有可用内核并加速网格搜索。但是,一些 Windows 用户在运行之前的代码时报告了问题,该代码的n_jobs=-1设置与在 Windows 上进行多处理的酸洗tokenizertokenizer_porter函数相关。另一种解决方法是将这两个函数 , 替换[tokenizer, tokenizer_porter][str.split]. 但是,请注意,用简单替换str.split不支持词干提取。

什么时候我们使用前面的代码初始化GridSearchCV对象及其参数网格,我们限制自己对于有限数量的参数组合,因为特征向量的数量以及大词汇量会使网格搜索在计算上非常昂贵。使用标准台式计算机,我们的网格搜索可能需要 5-10 分钟才能完成。

在前面的代码示例中,我们将前面小节中的CountVectorizerand替换为,它与. 我们由两个参数字典组成。在第一个字典中,我们使用其默认设置(、和)来计算 tf-idfs;在第二个字典中,我们将这些参数设置为、和,以便根据原始词频训练模型。此外,对于逻辑回归分类器本身,我们通过惩罚参数使用 L2 正则化训练模型,并通过定义逆正则化参数的值范围来比较不同的正则化强度TfidfTransformerTfidfVectorizerCountVectorizerTfidfTransformerparam_gridTfidfVectorizeruse_idf=Truesmooth_idf=Truenorm='l2'use_idf=Falsesmooth_idf=Falsenorm=NoneC. 作为一个可选练习,我们还鼓励您通过将 L1 正则化更改'clf__penalty': ['l2']为来将 L1 正则化添加到参数网格'clf__penalty': ['l2', 'l1']

网格搜索完成后,我们可以打印最佳参数集:

>>> print(f'Best parameter set: {gs_lr_tfidf.best_params_}')
Best parameter set: {'clf__C': 10.0, 'clf__penalty': 'l2', 'vect__ngram_range': (1, 1), 'vect__stop_words': None, 'vect__tokenizer': <function tokenizer at 0x169932dc0>}

正如您在前面的输出中看到的那样,我们使用tokenizer没有 Porter 词干提取、没有停用词库和 tf-idfs 的正则与使用 L2 正则化且正则化强度C10.0.

使用此网格搜索中的最佳模型,让我们打印训练数据集上的平均 5 倍交叉验证准确度得分和测试数据集上的分类准确度:

>>> print(f'CV Accuracy: {gs_lr_tfidf.best_score_:.3f}')
CV Accuracy: 0.897
>>> clf = gs_lr_tfidf.best_estimator_
>>> print(f'Test Accuracy: {clf.score(X_test, y_test):.3f}')
Test Accuracy: 0.899

结果揭示我们的机器学习模型可以以 90% 的准确率预测电影评论是正面还是负面。

朴素贝叶斯分类器

一个还是很受欢迎的用于文本分类的分类器是朴素贝叶斯分类器,它在电子邮件垃圾邮件过滤的应用中很受欢迎。朴素贝叶斯分类器易于实现、计算效率高,并且与其他算法相比,往往在相对较小的数据集上表现得特别好。虽然我们在本书中没有讨论朴素贝叶斯分类器,但有兴趣的读者可以在 arXiv 上免费找到一篇关于朴素贝叶斯文本分类的文章(朴素贝叶斯和文本分类 I – S. Raschka的介绍和理论,计算研究资料库) ( CoRR ), abs/1410.5329, 2014, http://arxiv.org/pdf/1410.5329v3.pdf )。na ï的不同版本本文引用的 ve 贝叶斯分类器是在 scikit-learn 中实现的。您可以在此处找到包含指向相应代码类的链接的概述页面:https ://scikit-learn.org/stable/modules/naive_bayes.html 。

处理更大的数据——在线算法和核外学习

如果您执行了上一节中的代码示例,您可能已经注意到在网格搜索期间为 50,000 部电影评论数据集构建特征向量的计算成本可能非常高。在许多现实世界的应用程序中,处理可能超出计算机内存的更大数据集的情况并不少见。

由于不是每个人都可以使用超级计算机设施,我们现在将应用一种技术称为核外学习,它允许我们通过在较小批量的数据集上逐步拟合分类器来处理如此大的数据集。

使用循环神经网络进行文本分类

第 15 章使用递归神经网络对序列数据建模,我们将重新审视这个数据集并训练基于深度学习的分类器(循环神经网络)对 IMDb 电影评论数据集中的评论进行分类。这种基于神经网络的分类器遵循相同的核外原理,使用随机梯度下降优化算法,但不需要构建词袋模型。

回到第 2 章训练用于分类的简单机器学习算法,随机梯度下降的概念是介绍;它是一种优化算法,一次使用一个示例更新模型的权重。在本节中,我们将利用 in scikit-learn 的partial_fit功能SGDClassifier直接从本地驱动器中流式传输文档并训练逻辑回归使用小批量文档的模型。

首先,我们将定义一个函数,从我们在本章开头构建tokenizer的文件中清除未处理的文本数据,并将其分离为单词标记,同时删除停用词:movie_data.csv

>>> import numpy as np
>>> import re
>>> from nltk.corpus import stopwords
>>> stop = stopwords.words('english')
>>> def tokenizer(text):
...     text = re.sub('<[^>]*>', '', text)
...     emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',
...                            text.lower())
...     text = re.sub('[\W]+', ' ', text.lower()) \
...                   + ' '.join(emoticons).replace('-', '')
...     tokenized = [w for w in text.split() if w not in stop]
...     return tokenized

接下来,我们将定义一个生成器函数 ,stream_docs它一次读入并返回一个文档:

>>> def stream_docs(path):
...     with open(path, 'r', encoding='utf-8') as csv:
...         next(csv) # skip header
...         for line in csv:
...             text, label = line[:-3], int(line[-2])
...             yield text, label

为了验证我们的stream_docs函数是否正常工作,让我们从文件中读取第一个文档movie_data.csv,它应该返回一个由评论文本和相应的类标签组成的元组:

>>> next(stream_docs(path='movie_data.csv'))
('"In 1974, the teenager Martha Moxley ... ',1)

我们现在定义一个函数,get_minibatch它将从stream_docs函数中获取文档流并返回参数指定的特定数量的文档size

>>> def get_minibatch(doc_stream, size):
...     docs, y = [], []
...     try:
...         for _ in range(size):
...             text, label = next(doc_stream)
...             docs.append(text)
...             y.append(label)
...     except StopIteration:
...         return None, None
...     return docs, y

不幸的是,我们不能CountVectorizer用于核外学习,因为它需要在记忆中保存完整的词汇。此外,TfidfVectorizer需要将训练数据集的所有特征向量保存在内存中,以计算逆文档频率。然而,在 scikit-learn 中实现的另一个用于文本处理的有用向量化器是HashingVectorizerHashingVectorizer与数据无关,并通过 Austin Appleby 的 32 位MurmurHash3函数利用散列技巧(您可以找到有关MurmurHash 在https://en.wikipedia.org/wiki/MurmurHash):

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> from sklearn.linear_model import SGDClassifier
>>> vect = HashingVectorizer(decode_error='ignore',
...                          n_features=2**21,
...                          preprocessor=None,
...                          tokenizer=tokenizer)
>>> clf = SGDClassifier(loss='log', random_state=1)
>>> doc_stream = stream_docs(path='movie_data.csv')

使用前面的HashingVectorizer代码,我们用我们的函数初始化tokenizer并将特征数设置为2**21loss此外,我们通过将参数设置为SGDClassifier来重新初始化逻辑回归分类器'log'。请注意,通过在 中选择大量特征HashingVectorizer,我们减少了导致哈希冲突的机会,但我们也增加了逻辑回归模型中的系数数量。

现在到了真正有趣的部分——设置了所有的补充功能后,我们可以使用以下代码开始核外学习:

>>> import pyprind
>>> pbar = pyprind.ProgBar(45)
>>> classes = np.array([0, 1])
>>> for _ in range(45):
...     X_train, y_train = get_minibatch(doc_stream, size=1000)
...     if not X_train:
...         break
...     X_train = vect.transform(X_train)
...     clf.partial_fit(X_train, y_train, classes=classes)
...     pbar.update()
0%                          100%
[##############################] | ETA: 00:00:00
Total time elapsed: 00:00:21

同样,我们使用 PyPrind 包来估计我们的学习算法的进度。我们用 45 次迭代初始化了进度条对象,在接下来的for循环中,我们迭代了 45 个 mini-batch 的文档,每个 mini-batch 包含 1000 个文档。完成增量学习过程后,我们将使用最后 5,000 个文档来评估我们模型的性能:

>>> X_test, y_test = get_minibatch(doc_stream, size=5000)
>>> X_test = vect.transform(X_test)
>>> print(f'Accuracy: {clf.score(X_test, y_test):.3f}')
Accuracy: 0.868

无类型错误

请注意,如果您遇到一个NoneType错误,你可能已经执行了X_test, y_test = get_minibatch(...)两次代码。通过前面的循环,我们有 45 次迭代,每次我们获取 1,000 个文档。因此,正好有 5,000 个文档需要测试,我们通过以下方式分配:

>>> X_test, y_test = get_minibatch(doc_stream, size=5000)

如果我们执行此代码两次,则生成器中没有足够的文档,并X_test返回None。因此,如果遇到NoneType错误,则必须重新从前面的stream_docs(...)代码开始。

如您所见,模型的准确度约为 87%,略低于我们在上一节中使用网格搜索进行超参数调整所达到的准确度。但是,核外学习非常节省内存,不到一分钟就可以完成。

最后,我们可以使用最后 5,000 个文档来更新我们的模型:

>>> clf = clf.partial_fit(X_test, y_test)

word2vec 模型

更现代的选择词袋模型是 word2vec,Google 于 2013 年发布的一种算法(T. MikolovK. ChenG. CorradoJ. Dean的向量空间中单词表示的有效估计,https ://arxiv .org/abs/1301.3781)。

word2vec 算法是一种基于神经网络的无监督学习算法,它试图自动学习单词之间的关系。word2vec 背后的想法是将具有相似含义的单词放入相似的簇中,并通过巧妙的向量间距,该模型可以使用简单的向量数学来重现某些单词,例如king  –  man  +  woman  =  queen

可以在https://code.google.com/p/word2vec/找到原始的 C 实现以及相关论文和替代实现的有用链接。

具有潜在 Dirichlet 分配的主题建模

主题建模描述将主题分配给未标记的文本文档的广泛任务。例如,一个典型的应用是对报纸文章的大型文本语料库中的文档进行分类。在主题建模的应用中,我们的目标是为这些文章分配类别标签,例如,体育、金融、世界新闻、政治和地方新闻。因此,在我们在第 1 章赋予计算机从数据中学习的能力”中讨论的广泛机器学习类别的背景下,我们可以将主题建模视为一种聚类任务,是无监督学习的一个子类别。

在本节中,我们将讨论一个流行的主题建模技术称为潜在狄利克雷分配LDA )。然而,请注意,虽然潜在的 Dirichlet 分配通常是缩写为 LDA,不要与线性判别分析相混淆,线性判别分析是第 5 章中介绍的一种监督降维技术,通过降维压缩数据

使用 LDA 分解文本文档

由于LDA背后的数学相当涉及并需要贝叶斯推理的知识,我们将从实践者的角度来处理这个主题,并使用外行的术语来解释 LDA。然而,感兴趣的读者可以在以下研究论文中阅读更多关于 LDA 的信息:Latent Dirichlet Allocation,作者David M. BleiAndrew Y. NgMichael I. Jordan机器学习研究杂志 3,页数:993-1022, 2003 年 1 月,https://www.jmlr.org/papers/volume3/blei03a/blei03a.pdf。

LDA 是一种生成概率模型,它试图找到在不同文档中经常一起出现的词组。这些频繁出现的词代表了我们的主题,假设每个文档都是不同词的混合。LDA 的输入是我们在本章前面讨论的词袋模型。

给定一个词袋矩阵作为输入,LDA 将其分解为两个新矩阵:

  • 文档到主题矩阵
  • 词到主题矩阵

LDA 分解词袋矩阵的方式是,如果我们将这两个矩阵相乘,我们将能够以尽可能低的误差重现输入,即词袋矩阵。在实践中,我们对 LDA 在词袋矩阵中找到的那些主题感兴趣。唯一的缺点可能是我们必须事先定义主题的数量——主题的数量是 LDA 的超参数,必须手动指定。

LDA 与 scikit-learn

在本小节中,我们将使用LatentDirichletAllocationscikit-learn 中实现的类来分解电影评论数据集并将其分类为不同的主题。在以下示例中,我们将分析限制为 10 个不同的主题,但鼓励读者尝试算法的超参数,以进一步探索可以在该数据集中找到的主题。

首先,我们将使用本章开头创建的电影评论DataFrame的本地文件将数据集加载到 pandas中:movie_data.csv

>>> import pandas as pd
>>> df = pd.read_csv('movie_data.csv', encoding='utf-8')
>>> # the following is necessary on some computers:
>>> df = df.rename(columns={"0": "review", "1": "sentiment"})

接下来,我们将使用已经熟悉CountVectorizer的方法来创建词袋矩阵作为 LDA 的输入。

为方便起见,我们将通过以下方式使用 scikit-learn 的内置英语停用词库stop_words='english'

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count = CountVectorizer(stop_words='english',
...                         max_df=.1,
...                         max_features=5000)
>>> X = count.fit_transform(df['review'].values)

请注意我们将要考虑的单词的最大文档频率设置为 10% ( max_df=.1) 以排除在文档中过于频繁出现的单词。删除频繁出现的词背后的基本原理是,这些词可能是出现在所有文档中的常见词,因此不太可能与给定文档的特定主题类别相关联。此外,我们将要考虑的单词数限制为最常出现的 5,000 个单词 ( max_features=5000),以限制该数据集的维度以改进 LDA 执行的推理。然而,两者max_df=.1都是max_features=5000任意选择的超参数值,鼓励读者在比较结果时调整它们。

以下代码示例演示了如何将LatentDirichletAllocation估计器拟合到词袋矩阵并从文档中推断出 10 个不同的主题(请注意,在笔记本电脑或标准台式计算机上,模型拟合可能需要 5 分钟或更长时间):

>>> from sklearn.decomposition import LatentDirichletAllocation
>>> lda = LatentDirichletAllocation(n_components=10,
...                                 random_state=123,
...                                 learning_method='batch')
>>> X_topics = lda.fit_transform(X)

通过设置learning_method='batch',我们让lda估算器根据所有可用的信息进行估算。在一次迭代中训练数据(词袋矩阵),这比替代'online'学习方法慢,但可以产生更准确的结果(设置learning_method='online'类似于我们在第 2 章中讨论的在线或小批量学习,训练用于分类的简单机器学习算法,以及本章前面的内容)。

期望最大化

scikit-learn 库的 LDA 实现使用期望最大化EM ) 算法来更新它的参数迭代估计。我们在本章中没有讨论 EM 算法,但是如果您想了解更多信息,请参阅 Wikipedia 上的精彩概述(https://en.wikipedia.org/wiki/Expectation–maximization_algorithm)和详细教程Colorado Reed 的教程Latent Dirichlet Allocation: Towards a Deeper Understanding中如何在 LDA 中使用它,该教程可在obphio.us免费获得。

装好后LDA,我们现在可以访问实例的components_属性,该属性lda存储了一个矩阵,其中包含 10 个主题中每个主题的单词重要性(此处5000为 ),按升序排列:

>>> lda.components_.shape
(10, 5000)

为了分析结果,让我们为 10 个主题中的每一个打印五个最重要的词。请注意,单词重要性值按升序排列。因此,要打印前五个单词,我们需要以相反的顺序对主题数组进行排序:

>>> n_top_words = 5
>>> feature_names = count.get_feature_names_out()
>>> for topic_idx, topic in enumerate(lda.components_):
...     print(f'Topic {(topic_idx + 1)}:')
...     print(' '.join([feature_names[i]
...                     for i in topic.argsort()\
...                     [:-n_top_words - 1:-1]]))
Topic 1:
worst minutes awful script stupid
Topic 2:
family mother father children girl
Topic 3:
american war dvd music tv
Topic 4:
human audience cinema art sense
Topic 5:
police guy car dead murder
Topic 6:
horror house sex girl woman
Topic 7:
role performance comedy actor performances
Topic 8:
series episode war episodes tv
Topic 9:
book version original read novel
Topic 10:
action fight guy guys cool

基于阅读每个主题的五个最重要的词,您可能会猜到 LDA 确定了以下主题:

  1. 一般烂电影(不是真正的主题类别)
  2. 关于家庭的电影
  3. 战争片
  4. 艺术电影
  5. 犯罪片
  6. 恐怖电影
  7. 喜剧电影评论
  8. 与电视节目有某种关联的电影
  9. 根据书籍改编的电影
  10. 动作片

为了根据评论确认类别有意义,让我们从恐怖电影类别中绘制三部电影(恐怖电影在索引位置属于类别 6 5):

>>> horror = X_topics[:, 5].argsort()[::-1]
>>> for iter_idx, movie_idx in enumerate(horror[:3]):
...     print(f'\nHorror movie #{(iter_idx + 1)}:')
...     print(df['review'][movie_idx][:300], '...')
Horror movie #1:
House of Dracula works from the same basic premise as House of Frankenstein from the year before; namely that Universal's three most famous monsters; Dracula, Frankenstein's Monster and The Wolf Man are appearing in the movie together. Naturally, the film is rather messy therefore, but the fact that ...
Horror movie #2:
Okay, what the hell kind of TRASH have I been watching now? "The Witches' Mountain" has got to be one of the most incoherent and insane Spanish exploitation flicks ever and yet, at the same time, it's also strangely compelling. There's absolutely nothing that makes sense here and I even doubt there ...
Horror movie #3:
<br /><br />Horror movie time, Japanese style. Uzumaki/Spiral was a total freakfest from start to finish. A fun freakfest at that, but at times it was a tad too reliant on kitsch rather than the horror. The story is difficult to summarize succinctly: a carefree, normal teenage girl starts coming fac ...

使用在前面的代码示例中,我们打印了前三部恐怖电影的前 300 个字符。这些评论——尽管我们不知道它们到底属于哪部电影——听起来像是对恐怖电影的评论(但是,有人可能会争辩说,Horror movie #2这也可能非常适合主题类别 1:一般糟糕的电影)。

概括

在本章中,您学习了如何使用机器学习算法根据文本文档的极性对文本文档进行分类,这是 NLP 领域情感分析的一项基本任务。您不仅学习了如何使用词袋模型将文档编码为特征向量,还学习了如何使用 tf-idf 通过相关性对词频进行加权。

由于在此过程中创建的大型特征向量,处理文本数据在计算上可能会非常昂贵;在上一节中,我们介绍了如何利用核外或增量学习来训练机器学习算法,而无需将整个数据集加载到计算机的内存中。

最后,向您介绍了使用 LDA 以无监督方式将电影评论分类为不同类别的主题建模概念。

到目前为止,在本书中,我们已经涵盖了许多机器学习概念、最佳实践和分类监督模型。在下一章中,我们将研究监督学习的另一个子类别,回归分析,它可以让我们在连续的尺度上预测结果变量,这与我们迄今为止使用的分类模型的分类类别标签形成对比。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年4月5日
下一篇 2023年4月5日

相关推荐