Skip to content

处理文本数据

本教程的目的是探索scikit-learn 的一些重要的工具在实际任务:分析有关二十个不同主题的文本文档(新闻帖子)中的使用。

在本节中,我们将看到如何:

  • 加载文件的内容和类别
  • 提取适合机器学习的特征向量
  • 训练线性模型来进行分类
  • 使用网格搜索策略为特征提取器和分类器找到好的参数

教程设置

为了要开始学习本教程,您必须首先安装 scikit-learn及其所有必需的依赖。

请参阅安装说明 页面以获取更多信息和各操作系统的说明。

您可以在scikit-learn的安装目录下中找到本教程的源代码:

scikit-learn/doc/tutorial/text_analytics/

源代码也可以在Github上找到。

教程文件夹应包含以下子文件夹:

  • *.rst files - 用sphinx编写的教程文档的源码
  • data - 用于放置教程中使用的数据集
  • skeletons - 不完整的练习脚本示例
  • solutions - 练习的答案

您可以将skeletons 文件夹复制到硬盘上的新文件夹中,该文件夹名可命名为sklearn_tut_workspace,您在练习中可以编辑自己的文件,同时保持原始的skeletons文件夹下面的文件不变:

% cp -r skeletons work_directory/sklearn_tut_workspace

机器学习算法需要数据。进入到$TUTORIAL_HOME/data 子文件夹下,然后运行fetch_data.py脚本文件(请在阅读完脚本文件之后再运行)。

例如:

% cd $TUTORIAL_HOME/data/languages
% less fetch_data.py
% python fetch_data.py

加载20个新闻组数据集

该数据集称为“二十个新闻组”(Twenty Newsgroups)。这是官方的描述,引用自网站

20个新闻组数据集是大约20,000个新闻文档的集合,几乎可以平均分为20个不同的新闻组。据我们所知,它最初是由Ken Lang收集的,可能是因为他的论文“ Newsweeder:学习过滤网络新闻”,尽管他没有明确提及是否是他收集的。20个新闻组数据集已成为机器学习文本技术应用(例如文本分类和文本聚类)实验的流行数据集。

在下文中,我们将使用内置的数据集加载器加载来自scikit-learn的20个新闻组数据集。或者可以在网站手动下载数据集并调用sklearn.datasets.load_files 函数将该数据集指向20news-bydate-train未压缩的文件夹的子文件夹。

为了在第一个示例中获得更快的执行速度,我们将处理部分数据集,该数据集中只有20个类别中的4个类别:

>>> categories = ['alt.atheism', 'soc.religion.christian',
...               'comp.graphics', 'sci.med']

现在,我们可以按以下方式来加载与这些类别匹配的文件列表:

>>> from sklearn.datasets import fetch_20newsgroups
>>> twenty_train = fetch_20newsgroups(subset='train',
...     categories=categories, shuffle=True, random_state=42)

返回的数据是一个scikit-learn“bunch”:一个简单的holder对象,该对象可以方便地像python的 dict对象一样使用, 利用keysobject属性进行访问。例如,该对象的target_names包含所请求类别名称的列表:

>>> twenty_train.target_names
['alt.atheism', 'comp.graphics', 'sci.med', 'soc.religion.christian']

文件本身将加载到data属性中的内存中。作为参考,文件名也可以用:

>>> len(twenty_train.data)
2257
>>> len(twenty_train.filenames)
2257

让我们打印第一个已加载文件的第一行:

>>> print("\n".join(twenty_train.data[0].split("\n")[:3]))
From: sd345@city.ac.uk (Michael Collier)
Subject: Converting images to HP LaserJet III?
Nntp-Posting-Host: hampton

>>> print(twenty_train.target_names[twenty_train.target[0]])
comp.graphics

监督学习算法要求训练集中的每个文档都要有一个类别标签。在这种情况下,类别是新闻组的名称,新闻组的名称也恰好是存放各个文档的文件夹的名称。

基于速度和空间效率的原因,scikit-learn将类别标签加载为与target_names列表中类别名称的索引对应的整数数组。每个样本的类别整数id存储在target属性中:

>>> twenty_train.target[:10]
array([1, 1, 3, 3, 3, 3, 3, 2, 2, 2])

可以按以下方式获取类别名称:

>>> for t in twenty_train.target[:10]:
...     print(twenty_train.target_names[t])
...
comp.graphics
comp.graphics
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
sci.med
sci.med
sci.med

您可能已经注意到,当我们调用fetch_20newsgroups(..., shuffle=True, random_state=42)方法时,获取到样本是随机的:如果您希望仅选择样本的子集来快速训练模型并希望对结果有一个初步了解,然后再对整个数据集进行训练的话,则这种做法很有用。

从文本文件中提取特征

为了对文本数据执行机器学习的算法,我们首先需要将文本内容转换为数字特征向量。

词袋

最直观的方法就是使用单词表示法:

  1. 对于训练集的每个文档中的每个单词都分配一个固定的整数ID(例如,通过建立一个从单词到整数索引的字典)。

  2. 对于每个文档#i,计算每个单词w的出现次数,并将其存储在X[i, j]中,其中#j的值是指在字典中单词w的索引是j

词袋表示意味着n_features是语料库中不同单词的数量:该数量通常大于100,000。

如果n_samples == 10000, 将其存储为float32类型的NumPy数组,则需要10000 x 100000 x 4字节= 4GB的RAM,这在当今的计算机上几乎无法管理。

幸运的是,X中的大多数值是零,因为对于给定的文档,将使用少于数千个不同的单词。出于这个原因,我们说词袋通常是 高维稀疏数据集。通过仅将特征向量中的非零部分存储在内存中,我们可以节省大量的内存。

scipy.sparse矩阵是可以精确执行此操作的数据结构,并且scikit-learn具有对这些结构有内置支持。

scikit-learn来tokenizing 文本

文本预处理,tokenizing和停用词的过滤都包含在中CountVectorizer,该工具可构建特征字典并将文档转换为特征向量:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count_vect = CountVectorizer()
>>> X_train_counts = count_vect.fit_transform(twenty_train.data)
>>> X_train_counts.shape
(2257, 35788)

CountVectorizer支持单词或连续字符的N-grams的计数。在训练(fit)之后,CountVectorizer将建立特征值索引字典:

>>> count_vect.vocabulary_.get(u'algorithm')
4690

词汇在词汇表中的索引值与整个训练语料库中的词频相关。

从出现次数到频率

出现次数的统计是一个好的方法,但是有一个问题:较长的文档将比较短的文档具有更高的平均统计值,即使它们是相同的主题。

为了避免这些潜在的差异,只需将文档中每个单词的出现次数除以文档中单词的总数即可:这些特征值被称为tf频率。

tf之上的另一种改进是降低了语料库中出现在许多文档中的单词的权重,因此比起仅在语料库中较少出现的单词来说它们(较多出现的单词)包含的信息较少。

这种改进被称为tf–idf“频率乘以文档频率的倒数”。

TFTF-IDF可以使用 TfidfTransformer进行计算:

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> tf_transformer = TfidfTransformer(use_idf=False).fit(X_train_counts)
>>> X_train_tf = tf_transformer.transform(X_train_counts)
>>> X_train_tf.shape
(2257, 35788)

在上面的示例代码中,我们首先使用将fit(..)方法去让估算器(TfidfTransformer)学习数据,然后通过transform(..)方法将计数矩阵转换为tf-idf表示。可以将这两个步骤结合起来以更快地获得相同的结果。这是通过使用fit_transform(..)的方法以及上一节中的注释中提到的方法完成的。如下所示:

>>> tfidf_transformer = TfidfTransformer()
>>> X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
>>> X_train_tfidf.shape
(2257, 35788)

训练分类器

现在我们有了特征值,我们可以训练分类器来尝试预测帖子的类别。让我们从朴素的贝叶斯 分类器开始,它为该任务提供了一个很好的基准。scikit-learn包括该分类器的几种变体;多项式(MultinomialNB)最适合用于特征值是单词计数的情况:

>>> from sklearn.naive_bayes import MultinomialNB
>>> clf = MultinomialNB().fit(X_train_tfidf, twenty_train.target)

为了尝试预测一个新文档的结果,我们需要使用与以前几乎相同的特征提取链(器)来提取特征。区别在于我们调用的是transform而不是fit_transform ,因为特征提取器已经在训练集上做过训练了:

>>> docs_new = ['God is love', 'OpenGL on the GPU is fast']
>>> X_new_counts = count_vect.transform(docs_new)
>>> X_new_tfidf = tfidf_transformer.transform(X_new_counts)

>>> predicted = clf.predict(X_new_tfidf)

>>> for doc, category in zip(docs_new, predicted):
...     print('%r => %s' % (doc, twenty_train.target_names[category]))
...
'God is love' => soc.religion.christian
'OpenGL on the GPU is fast' => comp.graphics

建立管道

为了使矢量化器(CountVectorizer)=>变压器(TfidfTransformer)=>分类器(MultinomialNB)更易于使用,scikit-learn提供了一个Pipeline的类,该类的行为类似于复合分类器:

>>> from sklearn.pipeline import Pipeline
>>> text_clf = Pipeline([
...     ('vect', CountVectorizer()),
...     ('tfidf', TfidfTransformer()),
...     ('clf', MultinomialNB()),
... ])

其中名称vecttfidfclf(分类器)是任意的。我们将在下面使用它来执行网格搜索以便寻找超参数。现在,我们可以使用一个命令来训练模型:

>>> text_clf.fit(twenty_train.data, twenty_train.target)
Pipeline(...)

在测试集上评估性能

评估该模型的准确率也是同样的容易:

>>> import numpy as np
>>> twenty_test = fetch_20newsgroups(subset='test',
...     categories=categories, shuffle=True, random_state=42)
>>> docs_test = twenty_test.data
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.8348...

我们达到了83.5%的准确率。让我们看看是否可以使用线性支持向量机(SVM)来做得更好,线性支持向量机被广泛认为是最好的文本分类算法之一(尽管它比朴素的贝叶斯算法还慢一些)。我们可以通过简单地将另一个分类器对象插入我们的管道来改变学习算法:

>>> from sklearn.linear_model import SGDClassifier
>>> text_clf = Pipeline([
...     ('vect', CountVectorizer()),
...     ('tfidf', TfidfTransformer()),
...     ('clf', SGDClassifier(loss='hinge', penalty='l2',
...                           alpha=1e-3, random_state=42,
...                           max_iter=5, tol=None)),
... ])

>>> text_clf.fit(twenty_train.data, twenty_train.target)
Pipeline(...)
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.9101...

通过使用SVM算法,我们达到了91.3%的准确率。scikit-learn提供了更多实用工具来对结果进行更详细的性能分析:

>>> from sklearn import metrics
>>> print(metrics.classification_report(twenty_test.target, predicted,
...     target_names=twenty_test.target_names))
                        precision    recall  f1-score   support

           alt.atheism       0.95      0.80      0.87       319
         comp.graphics       0.87      0.98      0.92       389
               sci.med       0.94      0.89      0.91       396
soc.religion.christian       0.90      0.95      0.93       398

              accuracy                           0.91      1502
             macro avg       0.91      0.91      0.91      1502
          weighted avg       0.91      0.91      0.91      1502

>>> metrics.confusion_matrix(twenty_test.target, predicted)
array([[256,  11,  16,  36],
       [  4, 380,   3,   2],
       [  5,  35, 353,   3],
       [  5,  11,   4, 378]])

不出所料,混淆矩阵显示,无神论和基督教的新闻组的帖子比起计算机图形学来说经常彼此混淆。

使用网格搜索进行参数调整

我们已经遇到了一些参数,例如 TfidfTransformer中的use_idf。分类器一般来说也具有许多参数。例如,在目标函数中MultinomialNB包含一个平滑参数alphaSGDClassifier具有一个惩罚参数alpha以及可配置的损失和惩罚项(请参阅模块文档,或使用Python help函数获取这些描述)。

无需调整链中各个组成部分的参数,而是在网格上进行详细的搜索来获取最佳参数。我们对带有或不带有idf的单词或bigrams的所有分类器进行了试验,得出线性SVM的惩罚参数为0.01或0.001:

>>> from sklearn.model_selection import GridSearchCV
>>> parameters = {
...     'vect__ngram_range': [(1, 1), (1, 2)],
...     'tfidf__use_idf': (True, False),
...     'clf__alpha': (1e-2, 1e-3),
... }

显然,这种详细的搜索可能很昂贵。如果我们有多个CPU内核可供使用的话,我们可以告诉网格搜索器尝试将这8个参数组合与n_jobs参数并行进行。如果我们将此参数的值设置为-1,则网格搜索检测有多少个内核就使用多少个:

>>> gs_clf = GridSearchCV(text_clf, parameters, cv=5, n_jobs=-1)

网格搜索对象的行为类似于普通scikit-learn 模型。让我们对训练数据中的较小子集执行搜索以加快计算速度:

>>> gs_clf = gs_clf.fit(twenty_train.data[:400], twenty_train.target[:400])

我们可以用在GridSearchCV对象上调用fit函数的结果来进行predict预测:

>>> twenty_train.target_names[gs_clf.predict(['God is love'])[0]]
'soc.religion.christian'

对象的best_score_best_params_属性中存储最好平均得分和与该得分相对应的参数设置:

>>> gs_clf.best_score_
0.9...
>>> for param_name in sorted(parameters.keys()):
...     print("%s: %r" % (param_name, gs_clf.best_params_[param_name]))
...
clf__alpha: 0.001
tfidf__use_idf: True
vect__ngram_range: (1, 1)

有关更详细的搜索摘要,请使用gs_clf.cv_results_

cv_results_参数可以轻松导入到pandas的 DataFrame作进一步查看。

练习

要进行练习的话,请将“ skeletons”文件夹的内容复制到一个名为“ workspace”的新文件夹:

% cp -r skeletons workspace

然后,您可以编辑工作区的内容,而不必担心丢失原始的练习说明。

然后使用一个ipython shell来运行以下脚本:

[1] %run workspace/exercise_XX_script.py arg1 arg2 arg3

如果触发了异常,则使用%debug命令来启动一个ipdb会话。

进行迭代并且优化,直到练习解决为止。

对于每次练习,skeletons文件都会提供所有必要的导入语句,用于加载数据的样板代码以及用于评估模型的预测准确率的示例代码。

练习1:语言识别

  • 使用自定义预处理器和CharNGramAnalyzer来编写文本分类管道,使用Wikipedia文章中的数据作为训练集 。

  • 在保留的(将数据集划分为训练集和测试集)测试集上评估性能。

ipython命令行:

%run workspace/exercise_01_language_train_model.py data/languages/paragraphs/

练习2:电影评论的情感分析

  • 编写文本分类管道以将电影评论分类为正面或负面。

  • 使用网格搜索找到一组好的参数。

  • 在保留的测试集上评估性能。

ipython命令行:

%run workspace/exercise_02_sentiment.py data/movie_reviews/txt_sentoken/

练习3:CLI文本分类应用程序

使用之前练习的结果和cPickle 标准库的模块,编写一个命令行应用程序。该应用程序将检测在stdin上提供的某些文本的语言,如果该文本是使用英语编写的话,并估计它的情感(正面或负面)。

如果该应用程序能够对其预测结果给出置信度的话,则可得到加分。

下一步

以下是一些建议,可帮助您在完成本教程后进一步提高scikit学习的感觉:

©2007-2019,scikit-learn开发人员(BSD许可证)。 显示此页面源码