首页 > 代码库 > 《机器学习系统设计》之应用scikit-learn做文本分类(上)
《机器学习系统设计》之应用scikit-learn做文本分类(上)
前言:
本系列是在作者学习《机器学习系统设计》([美] WilliRichert)过程中的思考与实践,全书通过Python从数据处理。到特征project,再到模型选择,把机器学习解决这个问题的过程一一呈现。
书中设计的源码和数据集已上传到我的资源:http://download.csdn.net/detail/solomon1558/8971649
第3章通过词袋模型+K均值聚类实现相关文本的匹配。本文主要解说文本预处理部分内容。涉及切分文本、数据清洗、计算TF-IDF值等内容。
相关链接:《机器学习系统设计》之应用scikit-learn做文本分类(下)
1. 统计词语
使用一个简单的数据集进行实验,它包含5个文档:
01. txt This is a toy post about machine learning.Actually, it contains not much interesting stuff.
02. txt Imaging databases provide storagecapabilities.
03. txt Most imaging databases safe imagespermanently.
04. txt Imaging databases store data.
05. txt Imaging databases store data. Imagingdatabases store data. Imaging databases store data.
在这个文档数据集中。我们想要找到和文档”imaging database”最相近的文档。为了将原始文本转换成聚类算法能够使用的特征数据,首先须要使用词袋(bag-of-word)方法来衡量文本间相似性。终于生成每一个文本的特征向量。
词袋方法基于简单的词频统计。统计每个帖子中的词频,表示成一个向量,即向量化。Scikit-learn的CountVectorizer能够高效地完毕统计词语的工作,Scikit的函数和类能够通过sklearn包引入进来:
posts = [open(os.path.join(DIR, f)).read() for f in os.listdir(DIR)]
vectorizer = CountVectorizer(min_df=1, stop_words="english")
X_train = vectorizer.fit_transform(posts)
如果待训练的文本存放在文件夹DIR下,我们将数据集传给CountVectorizer。參数min_df决定了CounterVectorizer怎样处理那些不常常使用的词语(最小文档词频)。当min_df为一个整数时,全部出现次数小于这个值的词语都将被扔掉;当它是一个比例时,将整个数据集中出现比例小于这个值的词语都将被丢弃。
我们须要告诉这个想量化处理器整个数据集的信息,使它能够预先知道都有哪些词语:
X_train = vectorizer.fit_transform(posts)
num_samples, num_features = X_train.shape
print ("#sample: %d, #feature: %d" % (num_samples, num_features))
print(vectorizer.get_feature_names())
程序的输出例如以下。5个文档中包括了25个词语
#sample: 5, #feature: 25
[u‘about‘, u‘actually‘, u‘capabilities‘, u‘contains‘,u‘data‘, u‘databases‘, u‘images‘, u‘imaging‘, u‘interesting‘, u‘is‘, u‘it‘,u‘learning‘, u‘machine‘, u‘most‘, u‘much‘, u‘not‘, u‘permanently‘, u‘post‘,u‘provide‘, u‘safe‘, u‘storage‘, u‘store‘, u‘stuff‘, u‘this‘, u‘toy‘]
对新文档进行向量化:
#a new post
new_post = "imaging databases"
new_post_vec = vectorizer.transform([new_post])
把每一个样本的词频数组当做向量进行相似度计算,须要使用数组的所有元素[使用成员函数toarray()]。
通过norm()函数计算新文档与所有训练文档向量的欧几里得范数(最小距离),从而衡量它们之间的相似度。
#------- calculate raw distances betwee new and old posts and record the shortest one-------------------------
def dist_raw(v1, v2):
delta = v1 - v2
return sp.linalg.norm(delta.toarray())
best_doc = None
best_dist = sys.maxint
best_i = None
for i in range(0, num_samples):
post = posts[i]
if post == new_post:
continue
post_vec = X_train.getrow(i)
d = dist_raw(post_vec, new_post_vec)
print "=== Post %i with dist = %.2f: %s" % (i, d, post)
if d<best_dist:
best_dist = d
best_i = i
print("Best post is %i with dist=%.2f" % (best_i, best_dist))
=== Post 0 with dist = 4.00:This is a toy post about machine learning. Actually, it contains not muchinteresting stuff.
=== Post 1 with dist =1.73:Imaging databases provide storage capabilities.
=== Post 2 with dist =2.00:Most imaging databases safe images permanently.
=== Post 3 with dist =1.41:Imaging databases store data.
=== Post 4 with dist =5.10:Imaging databases store data. Imaging databases store data. Imaging databasesstore data.
Best post is 3 with dist=1.41
结果显示文档3与新文档最为相似。然而文档4和文档3的内容一样,但反复了3遍。所以,它和新文档的相似度应该与文档3是一样的。
#-------case study: why post 4 and post 5 different ?-----------
print(X_train.getrow(3).toarray())
print(X_train.getrow(4).toarray())
[[0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0]]
[[0 0 0 0 3 3 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0]]
2. 文本处理
2.1 词频向量归一化
对第2节中的dist_raw函数进行扩展,在归一化的向量上(向量各分量除以其模长)计算向量间的距离。
def dist_norm(v1, v2):
v1_normalized = v1 / sp.linalg.norm(v1.toarray())
v2_normalized = v2 / sp.linalg.norm(v2.toarray())
delta = v1_normalized - v2_normalized
return sp.linalg.norm(delta.toarray())
=== Post 0 with dist = 1.41: This is a toy post aboutmachine learning. Actually, it contains not much interesting stuff.
=== Post 1 with dist = 0.86: Imaging databases providestorage capabilities.
=== Post 2 with dist = 0.92: Most imaging databasessafe images permanently.
=== Post 3 with dist = 0.77:Imagingdatabases store data.
=== Post 4 with dist = 0.77:Imaging databases store data. Imaging databases store data. Imaging databasesstore data.
Best post is 3 with dist=0.77
词频向量归一化之后,文档3和文档4与新文档具有了同样的相似度。从词频统计的角度来说,这样处理更为正确。
2.2 排除停用词
文本中类似”the”、”of”这些单词常常出如今各种不同的文本中,被称为停用词。因为停用词对于区分文本没有多大帮助,因此删除停用词是文本处理中的一个常见步骤。
CountVectorizer中有一个简单的參数stop_words()能够完毕该任务:
vectorizer = CountVectorizer(min_df=1, stop_words=‘english‘)
2.3词干处理
为了将语义类似但形式不同的词语放在一起统计,我们须要一个函数将词语归约到特定的词干形式。自然语言处理工具包(NLTK)提供了一个很easy嵌入到CountVectorizer的词干处理器。
把文档传入CountVectorizer之前,我们须要对它们进行词干处理。
该类提供了几种钩子。能够用它们定制预处理和词语切分阶段的操作。
预处理器和词语切分器能够当作參数传入构造函数。
我们并不想把词干处理器放入它们不论什么一个其中,由于那样的话,之后还须要亲自对词语进行切分和归一化。相反。我们能够通过改写build_analyzer方法来实现:
import nltk.stem
english_stemmer = nltk.stem.SnowballStemmer(‘english‘)
class StemmedCountVectorizer(CountVectorizer):
def build_analyzer(self):
analyzer = super(StemmedCountVectorizer, self).build_analyzer()
return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))
vectorizer = StemmedCountVectorizer(min_df=1, stop_words=‘english‘)
依照例如以下步骤对每一个帖子进行处理:
(1) 在预处理阶段将原始文档变成小写字母形式(这在父类中完毕);
(2) 在词语切分阶段提取全部单词;
(3) 将每一个词语转换成词干形式。
3. 计算TF-IDF
至此,我们採用统计词语的方式,从充满噪声的文本中提取了紧凑的特征向量。这些特征的值就是对应词语在全部训练文本中出现的次数,我们默认较大的特征值意味着合格词语对文本更为重要。
可是在训练文本中,不同的词语对文本的可区分性贡献更大。
这须要通过统计每一个文本的词频,而且对出如今多个文本中的词语在权重上打折来解决。即当某个词语常常出如今一些特定的文本中,而在其它地方非常少出现时。应该赋予该词语更大的权值。
这正是词频-反转文档频率(TF-IDF)所要做的:TF代表统计部分,而IDF把权重折扣考虑了进去。一个简单的实现例如以下:
import scipy as sp
def tfidf(t, d, D):
tf = float(d.count(t)) / sum(d.count(w) for w in set(d))
idf = sp.log(float(len(D)) / (len([doc for doc in D if t in doc])))
return tf * idf
在实际应用过程中,scikit-learn已经将该算法封装进了TfidfVectorizer(继承自CountVectorizer)中。进行这些操作后,我们得到的文档向量不会再包括词语拥挤值,而是每一个词语的TF-IDF值。
代码清单:
import os import sys import scipy as sp from sklearn.feature_extraction.text import CountVectorizer DIR = r"../data/toy" posts = [open(os.path.join(DIR, f)).read() for f in os.listdir(DIR)] new_post = "imaging databases" import nltk.stem english_stemmer = nltk.stem.SnowballStemmer(‘english‘) class StemmedCountVectorizer(CountVectorizer): def build_analyzer(self): analyzer = super(StemmedCountVectorizer, self).build_analyzer() return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc)) #vectorizer = StemmedCountVectorizer(min_df=1, stop_words=‘english‘) from sklearn.feature_extraction.text import TfidfVectorizer class StemmedTfidfVectorizer(TfidfVectorizer): def build_analyzer(self): analyzer = super(StemmedTfidfVectorizer, self).build_analyzer() return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc)) vectorizer = StemmedTfidfVectorizer(min_df=1, stop_words=‘english‘) print(vectorizer) X_train = vectorizer.fit_transform(posts) num_samples, num_features = X_train.shape print("#samples: %d, #features: %d" % (num_samples, num_features)) new_post_vec = vectorizer.transform([new_post]) print(new_post_vec, type(new_post_vec)) print(new_post_vec.toarray()) print(vectorizer.get_feature_names()) def dist_raw(v1, v2): delta = v1 - v2 return sp.linalg.norm(delta.toarray()) def dist_norm(v1, v2): v1_normalized = v1 / sp.linalg.norm(v1.toarray()) v2_normalized = v2 / sp.linalg.norm(v2.toarray()) delta = v1_normalized - v2_normalized return sp.linalg.norm(delta.toarray()) dist = dist_norm best_dist = sys.maxsize best_i = None for i in range(0, num_samples): post = posts[i] if post == new_post: continue post_vec = X_train.getrow(i) d = dist(post_vec, new_post_vec) print("=== Post %i with dist=%.2f: %s" % (i, d, post)) if d < best_dist: best_dist = d best_i = i print("Best post is %i with dist=%.2f" % (best_i, best_dist))
4. 总结
文本预处理过程包括的步骤总结例如以下:
(1) 切分文本;
(2) 扔掉出现过于频繁。而又对匹配相关文档没有帮助的词语;
(3) 扔掉出现频率非常低。仅仅有非常小可能出如今未来帖子中的词语;
(4) 统计剩余的词语。
(5) 考虑整个预料集合,从词频统计中计算TF-IDF值。
通过这一过程。我们将一堆充满噪声的文本转换成了一个简明的特征表示。
然而,尽管词袋模型及其扩展简单有效,但仍然有一些缺点须要注意:
(1) 它并不涵盖词语之间的关联关系。採用之前的向量化方法,文本”Car hits wall”和”Wall hits car”会有同样的特征向量。
(2) 它没法捕捉否定关系。
比如”I will eat ice cream”和”I will not eat ice cream”。虽然它们意思截然相反。但从特征向量来看它们非常相似。这个问题事实上非常easy解决,仅仅须要既统计单个词语(又叫unigrams)。又考虑成队的词语(bigrams)或者trigrams(一行中的三个词语)就可以。
(3) 对于拼写错误的词语会处理失败。
《机器学习系统设计》之应用scikit-learn做文本分类(上)