本系列是在作者学习《机器学习系统设计》([美] WilliRichert)过程中的思考与实践,全书通过Python从数据处理,到特征工程,再到模型选择,把机器学习解决问题的过程一一呈现。书中设计的源代码和数据集已上传到我的资源:http://download.csdn.net/detail/solomon1558/8971649
第3章通过词袋模型+K均值聚类实现相关文本的匹配。本文主要讲解K-均值聚类相关知识以及在20newsgroup数据集上使用K-均值聚类进行测试。
相关链接:《机器学习系统设计》之应用scikit-learn做文本分类(上)
K-均值是发现给定数据集的k个簇的算法。簇个数k由用户给定,每一个簇通过其质心(centroid),即簇中所有点的中心来描述。
K-均值算法的工作流程为:首先随机确定k个初始点作为质心;然后将数据集章的每个点分配到一个簇中,即为每个点找距其最近的质心,并将其分配给该质心对应簇;这一步完成后,每个簇的质心更新为该簇所有点的平均值。经过一定的迭代,当移动量低于一定阈值时,我们就认为聚类已经收敛了。
上述过程的伪代码表示如下:
创建k个点作为起始质心(一般随机选择)
当任意一个点的簇分配结果发生改变时
对数据集中每个数据点
对每个质心
计算质心与数据点之间的距离
将数据点分配到距其最近的簇
对每一个簇,计算簇中所有点的均值并将均值作为中心
K-均值聚类算法的优缺点:
优点:算法简单,易于实现
缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢。
本小节通过一个简单的例子来验证这个算法,这个例子包含只有两个词语的文档(源代码见附录)。
经过1次K均值迭代后,以任意3个向量作为起始点,经标签赋予余下的样本,然后更新簇的中心,使之成为该簇中所有数据点的中心点,我们得到以下聚类:
由于簇中心的移动,我们必须重新分配簇的标签,并重新计算簇的中心点。在第二轮迭代之后,得到以下聚类:
箭头显示了簇中心的移动。程序显示进过5次迭代后,簇中心点不在显著移动。(Scikit默认容许阈值为0.0001)。
在聚类停止之后,我们只需记录下簇中心及其标识。当每个新文档进来的时候,我们对其向量化,并与所有的簇中心进行比较。我们得到与新文档向量距离最小的簇中心所在的簇,然后把这个簇分配给该新文档。这样新文档的向量只需与同一簇中的向量比较、匹配,大大减少计算量。
20newsgroup数据集是机器学习中的一个标准数据集。它包含18828个文档,来自于20个不同的新闻组。如果把每个新闻组看作是一个簇,那么很容易测试出我们寻找相关文档的方法是否有效。
这个数据集可以从MLComp(http://mlcomp.org/datasets/379)下载,本人已将该数据集资源上传:http://download.csdn.net/detail/solomon1558/9007077
该资源包含一个原信息文件和3个目录:test、train和raw。测试和训练目录将整个数据集切分为60%的训练和40%的测试文档。
Scikit已经包含了定制的读取器来读取这个数据集。在读取数据时,可以设置环境变量MLCOMP_DATASETS_HOME,或者通过mlcomp_root参数直接指定路径:
import sklearn.datasets
MLCOMP_DIR = r"E:\py_Space\ML_C3\data" dataset = sklearn.datasets.load_mlcomp("20news-18828", "train", mlcomp_root=MLCOMP_DIR)
print (dataset.filenames)
print("Number of posts:", len(dataset.filenames))
['E:\\py_Space\\ML_C3\\data\\379\\train\\talk.politics.misc\\17860-178992'
'E:\\py_Space\\ML_C3\\data\\379\\train\\sci.med\\12836-58920'
'E:\\py_Space\\ML_C3\\data\\379\\train\\comp.graphics\\871-38995'...,
'E:\\py_Space\\ML_C3\\data\\379\\train\\sci.space\\14129-61228'
'E:\\py_Space\\ML_C3\\data\\379\\train\\soc.religion.christian\\15467-20879'
'E:\\py_Space\\ML_C3\\data\\379\\train\\comp.sys.mac.hardware\\3919-52046']
('Number of posts:', 13180)
通过设置函数load_mlcomp的第2个参数”train”、”predict”可以选取训练集或测试集。
为方便起见,把范围限制在某些新闻组中,使整个实验流程更短。我们可以通过设置categories参数实现这一点:
groups = [
'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.ma c.hardware', 'comp.windows.x', 'sci.space']
dataset = sklearn.datasets.load_mlcomp("20news-18828", "train", mlcomp_root=MLCOMP_DIR, categories=groups
)
由于真实的文本数据中存在很多噪声,甚至包含不合法的字符,这会导致UnicodeDecodeError报错,我们必须让向量化处理器忽略它们:
vectorizer = StemmedTfidfVectorizer(min_df=10, max_df=0.5, # max_features=1000, stop_words='english', #charset_error=None decode_error='replace' )
vectorized = vectorizer.fit_transform(dataset.data)
num_samples, num_features = vectorized.shape
print("#samples: %d, #features: %d" % (num_samples, num_features))
#samples: 3414, #features: 4330
注意书中采用参数charset_error,运行过程报错:
TypeError: __init__() gotan unexpected keyword argument 'charset_error'
应该换用decode_error=’replace’代替。
输出结果显示现有一个大小为3414的文档池,每个文档的特征向量的维度是4430,这个矩阵就是K均值算法的输入。本实验中把簇的大小固定在50:
num_clusters = 50 # sp.unique(labels).shape[0] from sklearn.cluster import KMeans
km = KMeans(n_clusters=num_clusters, init='k-means++', n_init=1, verbose=1)
clustered = km.fit(vectorized)
在拟合之后,我们可以从km的成员变量中获得聚类信息。针对每个拟合过的向量,km.labels_都给出了一个对应的标签:
[27 22 43 ..., 1 44 21]
本小节介绍如何通过km.predict给新文档分配一个簇。
首先将新文本向量化:
new_post = \
"""Disk drive problems. Hi, I have a problem with my hard disk. After 1 year it is working only sporadically now. I tried to format it, but now it doesn't boot any more. Any ideas? Thanks. """ new_post_vec = vectorizer.transform([new_post])
new_post_label = km.predict(new_post_vec)[0]
得到新文档的聚类信息后,我们就不需要用new_post_vec和所有的训练文档的向量进行比较。相反,我们只需专注与同一簇中的帖子。从原始数据集中取出它们的索引:
similar_indices = (km.labels_ == new_post_label).nonzero()[0]
括号中的比较操作可以得到一个布尔型数组,nonzero将这个数组转化为一个更小的数组,它只包含True元素索引。
然后用similar_indices构建了一个文档列表,以及它们的相似度分值,并对其按相似度升序排序。
similar = []
for i in similar_indices:
dist = sp.linalg.norm((new_post_vec - vectorized[i]).toarray())
similar.append((dist, dataset.data[i]))
similar = sorted(similar)
打印出最相似的文档(show_at_1),最不相似的文档(show_at_3),以及它们之间的帖子(show_at_3),它们都来自于同一个簇。
show_at_1 = similar[0]
show_at_2 = similar[len(similar) / 2]
show_at_3 = similar[-1]
print(show_at_1)
print(show_at_2)
print(show_at_3)
从聚类上的预处理,到把有噪文本转化为有意义的简洁向量表示的解决方案,这是一个艰难的过程。其中,我们为最终能够聚类所做的工作占了整个任务的一大部分。但是在这个过程中,我们学习到了很多关于文本处理的知识,以及简单词频统计在有噪声的真实数据上的有效应用。
# inspired by http://scikit- # learn.org/dev/auto_examples/cluster/plot_kmeans_digits.html#example- # cluster-plot-kmeans-digits-py import os import scipy as sp from scipy.stats import norm from matplotlib import pylab from sklearn.cluster import KMeans seed = 2 sp.random.seed(seed) # to reproduce the data later on num_clusters = 3 def plot_clustering(x, y, title, mx=None, ymax=None, xmin=None, km=None): pylab.figure(num=None, figsize=(8, 6)) if km: pylab.scatter(x, y, s=50, c=km.predict(list(zip(x, y)))) else: pylab.scatter(x, y, s=50) pylab.title(title) pylab.xlabel("Occurrence word 1") pylab.ylabel("Occurrence word 2") # pylab.xticks([w*7*24 for w in range(10)], ['week %i'%w for w in range(10)]) pylab.autoscale(tight=True) pylab.ylim(ymin=0, ymax=1) pylab.xlim(xmin=0, xmax=1) pylab.grid(True, linestyle='-', color='0.75') return pylab xw1 = norm(loc=0.3, scale=.15).rvs(20) yw1 = norm(loc=0.3, scale=.15).rvs(20) xw2 = norm(loc=0.7, scale=.15).rvs(20) yw2 = norm(loc=0.7, scale=.15).rvs(20) xw3 = norm(loc=0.2, scale=.15).rvs(20) yw3 = norm(loc=0.8, scale=.15).rvs(20) x = sp.append(sp.append(xw1, xw2), xw3) y = sp.append(sp.append(yw1, yw2), yw3) i = 1 plot_clustering(x, y, "Vectors") pylab.savefig(os.path.join("..", "1400_03_0%i.png" % i)) #pylab.show() pylab.clf() i += 1 #################### 1 iteration #################### mx, my = sp.meshgrid(sp.arange(0, 1, 0.001), sp.arange(0, 1, 0.001)) km = KMeans(init='random', n_clusters=num_clusters, verbose=1, n_init=1, max_iter=1, random_state=seed) km.fit(sp.array(list(zip(x, y)))) print(len(sp.array(list(zip(x, y))))) #print(sp.array(list(zip(x, y)))) Z = km.predict(sp.c_[mx.ravel(), my.ravel()]).reshape(mx.shape) plot_clustering(x, y, "Clustering iteration 1", km=km) pylab.imshow(Z, interpolation='nearest', extent=(mx.min(), mx.max(), my.min(), my.max()), cmap=pylab.cm.Blues, aspect='auto', origin='lower') c1a, c1b, c1c = km.cluster_centers_ pylab.scatter(km.cluster_centers_[:, 0], km.cluster_centers_[:, 1], marker='x', linewidth=2, s=100, color='black') pylab.savefig(os.path.join("..", "1400_03_0%i.png" % i)) #pylab.show() pylab.clf() i += 1 #################### 2 iterations #################### km = KMeans(init='random', n_clusters=num_clusters, verbose=1, n_init=1, max_iter=2, random_state=seed) km.fit(sp.array(list(zip(x, y)))) Z = km.predict(sp.c_[mx.ravel(), my.ravel()]).reshape(mx.shape) plot_clustering(x, y, "Clustering iteration 2", km=km) pylab.imshow(Z, interpolation='nearest', extent=(mx.min(), mx.max(), my.min(), my.max()), cmap=pylab.cm.Blues, aspect='auto', origin='lower') c2a, c2b, c2c = km.cluster_centers_ pylab.scatter(km.cluster_centers_[:, 0], km.cluster_centers_[:, 1], marker='x', linewidth=2, s=100, color='black') # import pdb;pdb.set_trace() pylab.gca().add_patch( pylab.Arrow(c1a[0], c1a[1], c2a[0] - c1a[0], c2a[1] - c1a[1], width=0.1)) pylab.gca().add_patch( pylab.Arrow(c1b[0], c1b[1], c2b[0] - c1b[0], c2b[1] - c1b[1], width=0.1)) pylab.gca().add_patch( pylab.Arrow(c1c[0], c1c[1], c2c[0] - c1c[0], c2c[1] - c1c[1], width=0.1)) pylab.savefig(os.path.join("..", "1400_03_0%i.png" % i)) pylab.clf() i += 1 #################### 3 iterations #################### km = KMeans(init='random', n_clusters=num_clusters, verbose=1, n_init=1, max_iter=10, random_state=seed) km.fit(sp.array(list(zip(x, y)))) Z = km.predict(sp.c_[mx.ravel(), my.ravel()]).reshape(mx.shape) plot_clustering(x, y, "Clustering iteration 10", km=km) pylab.imshow(Z, interpolation='nearest', extent=(mx.min(), mx.max(), my.min(), my.max()), cmap=pylab.cm.Blues, aspect='auto', origin='lower') pylab.scatter(km.cluster_centers_[:, 0], km.cluster_centers_[:, 1], marker='x', linewidth=2, s=100, color='black') pylab.savefig(os.path.join("..", "1400_03_0%i.png" % i)) pylab.clf() i += 1
import sklearn.datasets import scipy as sp new_post = \ """Disk drive problems. Hi, I have a problem with my hard disk. After 1 year it is working only sporadically now. I tried to format it, but now it doesn't boot any more. Any ideas? Thanks. """ MLCOMP_DIR = r"E:\py_Space\ML_C3\data" groups = [ 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.ma c.hardware', 'comp.windows.x', 'sci.space'] dataset = sklearn.datasets.load_mlcomp("20news-18828", "train", mlcomp_root=MLCOMP_DIR, categories=groups ) print (dataset.filenames) print (len(dataset.filenames)) print("Number of posts:", len(dataset.filenames)) labels = dataset.target num_clusters = 50 # sp.unique(labels).shape[0] import nltk.stem english_stemmer = nltk.stem.SnowballStemmer('english') from sklearn.feature_extraction.text import TfidfVectorizer class StemmedTfidfVectorizer(TfidfVectorizer): def build_analyzer(self): analyzer = super(TfidfVectorizer, self).build_analyzer() return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc)) vectorizer = StemmedTfidfVectorizer(min_df=10, max_df=0.5, # max_features=1000, stop_words='english', #charset_error=None decode_error='replace' ) vectorized = vectorizer.fit_transform(dataset.data) num_samples, num_features = vectorized.shape print("#samples: %d, #features: %d" % (num_samples, num_features)) from sklearn.cluster import KMeans km = KMeans(n_clusters=num_clusters, init='k-means++', n_init=1, verbose=1) clustered = km.fit(vectorized) from sklearn import metrics print("Homogeneity: %0.3f" % metrics.homogeneity_score(labels, km.labels_)) print("Completeness: %0.3f" % metrics.completeness_score(labels, km.labels_)) print("V-measure: %0.3f" % metrics.v_measure_score(labels, km.labels_)) print("Adjusted Rand Index: %0.3f" % metrics.adjusted_rand_score(labels, km.labels_)) print("Adjusted Mutual Information: %0.3f" % metrics.adjusted_mutual_info_score(labels, km.labels_)) print(("Silhouette Coefficient: %0.3f" % metrics.silhouette_score(vectorized, labels, sample_size=1000))) new_post_vec = vectorizer.transform([new_post]) new_post_label = km.predict(new_post_vec)[0] similar_indices = (km.labels_ == new_post_label).nonzero()[0] print new_post_label print km.labels_ similar = [] for i in similar_indices: dist = sp.linalg.norm((new_post_vec - vectorized[i]).toarray()) similar.append((dist, dataset.data[i])) similar = sorted(similar) show_at_1 = similar[0] show_at_2 = similar[len(similar) / 2] show_at_3 = similar[-1] print(show_at_1) print(show_at_2) print(show_at_3) import pdb pdb.set_trace()