处理文本数据

本指南的目的是在一项实际任务上探索一些主要的scikit学习工具:分析有关二十个不同主题的文本文档(新闻组帖子)的集合。

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

  • 加载文件内容和类别

  • 提取适合机器学习的特征向量

  • 训练线性模型以执行分类

  • 使用网格搜索策略找到特征提取组件和分类器的良好配置

教程设置

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

请参阅安装说明页面以获取更多信息和特定于系统的说明。

您可以在scikit-learn文件夹中找到本教程的源代码:

scikit-learn/doc/tutorial/text_analytics/

这个资源也可以在Github上找到。

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

  • *.rst files - 用sphinx编写的教程文档的源文件

  • data - 数据,放置教程中使用的数据集的文件夹

  • skeletons - 框架,练习的例子,是不完整的脚本

  • solutions - 答案,练习的解决方案

您已经可以将框架复制到硬盘驱动器上名为sklearn_tut_workspace的某个位置的新文件夹中,您将在练习中编辑自己的文件,同时保持原始框架完整:

% 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个新闻组数据集

该数据集称为“二十个新闻组”。这是官方引用,引用自网站:http://people.csail.mit.edu/jrennie/20Newsgroups/

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'(可译为sklearn枝):一个简单的holder对象,其字段可以方便地用作python的字典的键或对象属性来访问,例如target_names包含所请求类别名称的列表:

输入:

twenty_train.target_names

输出:

['alt.atheism''comp.graphics''sci.med''soc.religion.christian']

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

输入:

len(twenty_train.data)

len(twenty_train.filenames)

输出:

2257

2257

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

print("\n".join(twenty_train.data[0].split("\n")[:3]))

print(twenty_train.target_names[twenty_train.target[0]])

输出:

From: sd345@city.ac.uk (Michael Collier)
Subject: Converting images to HP LaserJet III?
Nntp-Posting-Host: hampton
    
comp.graphics

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

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

twenty_train.target[:10]

输出:

array([1133333222])

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

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的值,其中j是词典中单词w的索引。

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

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

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

scipy.sparse矩阵是可以完成此操作的数据结构,而scikit-learn内置了对这些结构的支持。

使用scikit-learn对文本进行标记

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克单词或连续字符的计数。拟合后,矢量化器将建立特征索引字典:

count_vect.vocabulary_.get(u'algorithm'

输出:

4690

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

从发生到高频

出现次数是一个好的开始,但是存在一个问题:较长的文档将比较短的文档具有更高的平均计数值,即使它们可能谈论相同的主题。

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

在tf之上的另一种改进是降低了语料库中许多文档中出现的单词的权重,因此比仅在语料库中较小部分出现的单词信息少。

这种缩减称为“术语频率乘以文档的倒数频率”的tf–idf。

使用TfidfTransformer可以如下计算tf和tf–idf:

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

输出:

(225735788)

在上面的示例代码中,我们首先使用fit(..)方法使估算器拟合数据,其次使用transform(..)方法将计数矩阵转换为tf-idf表示形式。通过跳过冗余处理,可以将这两个步骤结合起来以更快地达到相同的最终结果。这是通过使用fit_transform(..)方法(如下所示)以及上一节中的注释中提到的来完成的:

tfidf_transformer = TfidfTransformer()
X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
X_train_tfidf.shape

输出:

(225735788)

训练一个分类器

现在我们有了功能,我们可以训练分类器以尝试预测帖子的类别。 让我们从朴素的贝叶斯分类器开始,它为该任务提供了一个很好的基准。 scikit-learn包含此分类器的多种变体; 多项式最适合单词计数:

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

建立管道

为了使vectorizer => Transformer =>分类器更易于使用,scikit-learn提供了类似于复合分类器的Pipeline类:

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

名称vect,tfidf和clf(分类器)是任意的。 我们将在下面使用它们对合适的超参数执行网格搜索。 现在,我们可以使用一个命令来训练模型:

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)

predicted = text_clf.predict(docs_test)
np.mean(predicted == twenty_test.target)

输出:

Pipeline(...)

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))

metrics.confusion_matrix(twenty_test.target, predicted)

输出:

                        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
            
array([[256,  11,  16,  36],
       [  4380,   3,   2],
       [  5,  35353,   3],
       [  5,  11,   4378]])   

不出所料,混乱矩阵显示来自无神论和基督教的新闻组中的帖子彼此之间的混淆比计算机图形更多。

使用网格搜索调参

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

无需调整链中各个组成部分的参数,而是可以在可能值的网格上详尽搜索最佳参数。我们对带有或不带有idf的单词或双字母组的所有分类器进行了试验,线性SVM的惩罚参数为0.01或0.001:

from sklearn.model_selection import GridSearchCV
parameters = {
    'vect__ngram_range': [(11), (12)],
    'tfidf__use_idf': (TrueFalse),
    'clf__alpha': (1e-21e-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的结果是一个分类器,我们可以用来预测:

twenty_train.target_names[gs_clf.predict(['God is love'])[0]]

输出:

'soc.religion.christian'

对象的best_score_和best_params_属性存储最佳平均得分和与该得分相对应的参数设置:

gs_clf.best_score_

for param_name in sorted(parameters.keys()):
    print("%s: %r" % (param_name, gs_clf.best_params_[param_name]))

输出:

0.9...

clf__alpha: 0.001
tfidf__use_idf: True
vect__ngram_range: (11)

有关更详细的搜索摘要,请访问gs_clf.cv_results_。

可以将cv_results_参数作为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-learn的感觉:

  • 尝试在CountVectorizer下使用分析器和标记规范化(token normalisation)。

  • 如果没有标签,请尝试对问题使用群集。

  • 如果每个文档有多个标签(例如类别),请查看“多类和多标签”部分。

  • 尝试使用截断的SVD进行潜在的语义分析。

  • 看一下使用核外分类(out-of-core Classification)从不适合计算机主内存的数据中学习。

  • 看看Hashing Vectorizer,这是CountVectorizer的一种内存有效替代方案。