[译]处理文本数据(scikit-learn 教程3)
本教程的主要目标是通过分析包含二十个不同话题的文档集合这以实际任务,来介绍scikit-learn中文本数据处理相关的主要工具.
在这一章节我们将会看到:
- 如何加载文件内容及目录
- 如何提取适合机器学习的特征向量
- 如何训练一个线性模型来进行分类
- 如何使用网格搜索策略在特征提取和分类之间找到一个较好配置
教程准备
要开始本教程,你首先要在你的计算机上安装好scikit-learn及其相关依赖.
请参考安装介绍来了解不同系统上的更多安装细节
本教程中的源码可以在你的scikit-learn文件夹中找到
scikit-learn/doc/tutorial/text_analytics/
教程文件夹应该包含一下文件夹:
*.rst 文件
-教程文档的sphinx源代码data
-放置教程中使用的数据集的文件夹skeletons
-为练习准备的不完整样本
你也可以将skeletons
艹被你硬盘上的任意位置,并命名sklearn_tut_workspace
.就样你既可以任意编辑进行练习,也不会破坏原来的文件:
$ cp -r skeletons <工作目录>/sklearn_tut_workspace
机器学习算法需要数据.进入每一个$TUTORIAL_HOME/data
的子文件夹运行嗯fetch_data.py
脚本(运行之前你可以看一下脚本中的内容).
例如:
$ cd $TUTORIAL_HOME/data/languages
$ less fetcg_data.py
$ python fetch_data.py
加载20个新闻组数据集
数据集的名字是"Twenty Nesgroups",下面是应用自官网的描述
20新闻组数据集是一个接近被2000个新闻组文档的集合,几乎阔月20个不同的新闻组.据我们所知,它原先是由Ken Lang收集的,也许是为了他的论文:"Newsweeder:学习过滤新闻",尽管他没有明确的提到这个数据集.20新闻组集合在机器学习技术中的文本应用实验中已经成为一个流行的数据集.
接下来我们将使用scikit-learn中20新闻组的内建数据集加载器.另外,也可以直接从网站中下载数据集,使用sklearn.datasets.load_files
函数加载解压缩文件夹内的20news-bydata-train
子文件夹.
对于第一个例子,为了获得更块的执行时间,我们将从二十个类变种选出四类 来完成本实验.
>>> categoties = ['alt.altheism','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
"束":一个承载对象,可以通过python中dict
键进行访问,为了方便还具有对象的属性.例如,target_name
存储有分类名列表:
>>> twenty_train.target_name
['alt.altheism','soc.religion.christian', 'comp.graphics','sci.med']
文件本身被加载进内存,存储在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_list
中类别名的索引.每一个样例的分类整数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
你也许注意到了样例已经被随机排序了:如果你只选择第一个样例来快速训练模型,这非常有用.这样在稍后的整个数据集上训练之前可以获得一些灵感和启示.
从文本文件提取特征
为了在文本文档中应用机器学习,我们首先需要将文本内容转换成数字特征向量.
词袋
最直观的方式就是用词袋来表示:
- 1.为每一个在训练集任意文档出现的词语指派一个处理过的整数id(例如,建立一个词语到整数映射的字典)
- 2.对于每一个文档
#i
,统计每一个词语w
出现的次数,并将其存储在X[i,j]
作为特征#j
的值,其中j
是单词w
在字典中的索引.
词袋表示n_features
是预料库中不同词的数量:这个数字一般大于100 000.
如果n_samples = 10000
,将X
存储为一个numpy float32数组需要10000 x 100000 x 个字节 = 4G内存,在当今的普通电脑上几乎是不可处理的.
幸运的是,由于给定的文档中只有不到两千的不同词语将会被使用,所以X中的大多数值会是零
.由于这这个原因我们一般称词袋为高维稀疏数据集.我们可以在内存中只存储特征向量的非零部分,从而节约大量的内存.
使用scikit-learn进行文本标记
在scikit-learn中,文本预处理,标记,和停用词过滤被组装成一个高级组件,可以用来构建一个特征字典并将文档转换成特征向量.
>>> from sklearn.feature_extraction.text import COuntVectorzier
>>> count_vect = CountVectorizer()
>>> x_train_counts = count_vect.fit_transform(twenty_train.data)
>>> x_train_counts.shape
(2257, 35788)
CountVectorizer
支持词语和字符序列的N词(N-grams)计数.一旦被适配,向量转换器(vectorizer)已经构建了一个特征指示字典:
>>> count_vect.vocabulary_.get(u'algorithm')
4690
词汇表中一个词语的索引值对应在整个训练预料库中其出现的频率.
从频次到频率
频次计数是一个好的开始,但是这里有一点问题:相较于较短的文档,较长的文档将会有更高的品均计数值,尽管它们可能谈论的是相同话题.
为了避免这些潜在的偏差,可以让它除以文档中所有词语在文档出现的次数的和.这个新特征被称为tf
,即文档频率(Term Frequencies)
在tf
的上层再进行细化的一个办法是降低在预料苦衷多数文档出现的词语的权重,因为这些词语相较于那些只在较少语库文档中出现的词语所携带的类别指示信息要少.
这个降权方式被称为tf-idf
,意即文档频率-逆文档频率(Term Frequency times Inverse Document Frequency)
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
(2257,35788)
在上面的示例代码中,我们首先使用fit(...)
方法将评估器应用数据上,接着transform(...)
方法见我们的计数举证转换为tf-if形式.这两步完全可以结合起来,跳过冗余处理,以更快地获得相同的最终结果.者可以像下面这样使用fit_transform(...)
方法来实现:
>>> tfidf_transformer = TfidfTransformer()
>>> x_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
>>> x_train_tfidf.shape
(2257,35788)
训练一个分类器
既然我们已经有了我们的特征,我们就可以训练一个分类器来尝试预测文章的类别.让我们从使用朴素贝叶斯分类器开始,这可以为本次任务提供一个良好的基线.`scikit-learn包含这个分类器的多个变种,其中最适合进行词语计数的是多项式形式的变种:
>>> from sklearn.baive_bayes import MultinomialNB
>>> clf = MultinomiaNB().fit(X_train_tfidf,twenty_train.target)
要尝试预测一个新文档,我们需要像前面的特征提取流程那样提取几乎相同的特征.不同之处在于这里在做转换的时候我们使用transform
替代fit_transform
,因为他们已经适配训练集了:
>>> dics_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_tfodf)
>>> 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
构建一个管道
为了使 向量化 => 转换 => 分类整个流程更容易操作,scikit-learn
提供了一个Pipeline
类,其功能就像是一个符合分类器:
>>> from sklearn.pipeline importPipeline
>>> text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', MultinomialNB()),])
其中vect
,tfidf
和clf
(分类器)的命名是非常任意的.我们应该会在下面网格搜索的章节看到它们的使用.我们现在可以仅使用一条命令来训练我们的模型:
>>> text_clf = text_clf.fit(twenty_train.data, twenty_train.target)
在测试集上的性能评估
评估模型的预测精度同样非常简单:
>>> 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.834
额,我们获得83.4%的预测精度.让我们看看使用线性支持向量机(SVM)是不是会获得更好的效果.SVM被广泛认为是最好的文本分类算法之一(尽管它相较于朴素贝叶斯有点慢).我们可以通过仅仅插入一个不同的分类对象到我们的管道中来改变"学习者":
>>> from sklearn.linear_model import SGDClassifier
>>> text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', SGDClassifier(loss='hinge', penalty='l2',
alpha=1e-3, n_iter=5, random_state=42)),
])
>>> _ = text_clf.fit(twenty_train.data, twenty_train.target)
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.912...
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.81 0.87 319
comp.graphics 0.88 0.97 0.92 389
sci.med 0.94 0.90 0.92 396
soc.religion.christian 0.90 0.95 0.93 398
avg / total 0.92 0.91 0.91 1502
>>> metrics.confusion_matrix(twenty_test.target, predicted)
array([[258, 11, 15, 35],
[ 4, 379, 3, 3],
[ 5, 33, 355, 3],
[ 5, 10, 4, 379]])
正如预料那样,混淆矩阵显示来自无神论和基督教主题行文组的文章经常会和来自计算机图形雪的文章混淆.
使用网格搜索进行参数调整(调参)
我们已经遇到了一些参数,如TfidfTransformer
中的use_idf
.分类器也同样需要大量的参数.如MultinomiaNB
包含一个平滑参数alpha
,SGDClassfier
有一个惩罚参数alpha
,可用来配置损失和惩罚值(通过查阅相关模块文档或在Python中使用help
获得更多相关细节).
相较于调整工作链中各个组件的参数,在一个可能值的网格上运行一个搜索器进行彻底搜索获得最佳参数的方式也许更为可行.
我们在词语或字符,有无id的情况下尝试尽所有的分类器,对于线性SVM分类器尝试从0.1到0,001的惩罚参数:
>>> from sklearn.grid_search import GridSearchCV
>>> parameters = {'vect__ngram_range': [(1, 1), (1, 2)],
'tfidf__use_idf': (True, False),
'clf__alpha': (1e-2, 1e-3), }
很明显,这样的彻底搜索是非常昂贵的.如果我们可以使用多核CPU,我们可以通过n_jobs
参数告诉网格搜索器采用并行的方式尝试这八个参数的组合.如果我们给这个参数赋值为-1
,网格搜索将探测有多少核可用并将他们全部使用起来:
>>> GridSearchCV(text_clf, parameters, 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'])]
'soc.religion.christian'
虽然这是一个非常大而且笨拙的对象.但是我们可以通过检查对象的grid_scores
属性来获得最优参数.grid_score
是一个参数/分数对列表.要获得分支最高的属性,我们可以:
>>> best_parameters, score, _ = max(gs_clf.grid_scores_, key=lambda x: x[1])
>>> for param_name in sorted(parameters.keys()):
print("%s: %r" % (param_name, best_parameters[param_name]))
clf__alpha: 0.001
tfidf__use_idf: True
vect__ngram_range: (1, 1)
>>> score
0.900...
练习
复制'skeletons'文件夹并命名为'workspace':
$ cp -r sketletons workspace
接着你就可以编辑工作区中的东西,而不用害怕丢失原来的练习文件.
启动一个ipython shell,用下面的命令运行脚本:
[1] %run workspace/exercise_XX_script.py arg1 arg2 arg3
如果触发了异常,使用%debug
来启动一个验尸ipdb会话.
细化实现和迭代直到练习被解决.
针对每个练习,对应的skeleton文件都提供了所有必要的import语句,加载数据和评估模型预测精度的样板代码
练习一:语言识别
- 使用一个定制的预处理器和
CharNGramAnalyzer
编写一个文本分类管道.来自维基百科的文章数据作为训练集 - 在一些测试集上评估性能
ipython命令行:
%run workspace/exercise_01_language_train_model.py data/languages/paragraphs/
练习二:电影评论的情感分析
- 编写一个文本分类管道来来划分电影评论是正面情绪和负面情绪
- 使用网格搜索寻找一个好的训练及
- 在测试集上评估性能
ipython命令行:
%run workspace/exercise_02_sentiment.py data/movie_reviews/txt_sentoken/
练习三:命令行文本分类工具集
使用先前练习的结果和Python的标准库cPickle
模块编写一个命令行工具.探测标准输入提供的文本的所属语言,如果文本是英文写,估测其情绪极性(正面情绪,负面情绪).
如果程序可以提供其预测的置信水平就更棒了!
接下来去哪儿
这里有一些建议来帮助你在完成本教程后更进一步熟悉scikit-learn:
- 尝试玩玩
CountVectorizer
中的analyzer
和token normalisation
- 如果你没有标签,尝试用
聚类方法
解决你的问题 - 尝试使用
Truncated SVD
来解决潜在语义分析 - ave a look at using Out-of-core Classification to learn from data that would not fit into the computer main memory.
- Have a look at the Hashing Vectorizer as a memory efficient alternative to CountVectorizer.
还真有人点开啊🤣随意随意😂