来自书籍《Building Machine Learning Systems with Python 》
前两章觉得还是挺简单的,干货没有。下面来第三章,这一章主要是介绍文本处理方面,而且没有涉及到最新的word2vector方法等等(DL在NLP上的应用),本章节还是介绍词袋啊什么的,DL在NLP上最近的表现还是比传统的好很多的,比如谷歌方面,比如微软方面,还有中科院在中文上的努力。
第三章 聚类-查找相关的位置
就拿搜索引擎来说,就是用户询问,然后搜索将对应的类的结果呈现出来,并且按照相关性排序,如果说需要一个个的和服务器上存储的所有结果比较代价太大,所以需要更好的方法。就是先聚类,然后与询问词相近的那一堆位置进行处理就行。本章节还会介绍Scikit库,其中有不少的ML方法。
这一章介绍了文本部分,对于ML来说,原始的文本是没有任何意义的,只有将原始的文本变成有意义的数字,才能放入ML中处理,比如聚类。在文本相似性测量的时候,有个方法叫Levenshtein距离方法,也叫做编辑距离(edit distance),就是比如“machine”和“mchiene”,它两的编辑距离是2,也就是必须编辑的最小次数:将第二个在‘m’后面加‘a’;删除第一个‘e’,不过看的出来,这个算法的复杂度太大了。或者说对于语句'how to format my hard disk'和“hard disk format problems‘,这两个句子可以简单的看出每个单词扮演着前面单词中字符的角色,从而这两个句子之间的编译距离为6(文中是5),:删除’how‘,’to‘,’format‘,’my‘然后加上’format‘,’problems‘.也就是句子之间的编译距离为删除单词和增加单词的次数,不过对于’format‘来说,删除一次,增加一次,也就是该方法没考虑到单词的顺序问题,不够鲁棒。
一、词袋
比上面更鲁棒的方法叫做词袋(bag of word),就是先搜索有多少不同的词,建立特征索引表,然后统计每个样本的特征出现频率,从而形成一个向量,这一步就叫做向量化。不过可想而知这个向量在现实中会很长,对于搜索来说可以分下面四步:
1、对每个存储位置提取一个独特的特征;(词袋特征向量)
2、对所有的特征进行聚类;
3、通过用户的询问来决定需要查找哪一堆类;
4、从这一堆类中,对于每个不同的位置接着与问题进行对比。
不过在上面中会遇到几个问题:
a)有些词会重复很多次,这样对于那个位置来说词频上升了,可是对于匹配来说,会造成恶意强调的效益;(后面的对词频向量标准化解决这个问题)
b)有些词出现的频率很高,比如’is‘,等等几乎每个句子都是天然存在的,这些对于搜索来说没有任何帮助的词被称之为”stop word“,应该提前删除;(即删除不重要的词)。
c)对于’english‘这个集合来说,有些比如’imaging‘和’images’,还有’img‘‘等等都需要表示成同一个意思,因为这是相同词的不同变种,该处理方法在Scikit中没有包含这么一个stemmer,所以需要下载NLTK包(自然语言工具箱)。
d) 在所需要处理的文本中,如果某个词在所有样本中出现了90%次,那么该词无意义,需要删除;而对于某个词只出现1%或者更少,也需要删除,意思就是那些对于最后的分类贡献度不够,也就是不够特别的词,删除了有助于分类结果的改善,只是如何界定90%和10%才是更好,难道89%就不好吗,所以这就需要统计某个词在每个位置上出现的次数和在其他位置没出现的次数。也叫做TF-IDF(term frequency-inverse document frequency)。
import os import sys import scipy as sp #下面装载的就是计算单词词频,然后表示成向量的类 from sklearn.feature_extraction.text import CountVectorizer #可以help(CountVectorizer)查看内部的参数说明 DIR = r"../data/toy" posts = [open(os.path.join(DIR, f)).read() for f in os.listdir(DIR)] #上面就是用for搜索toy文件夹下不同的文件,然后逐个打开,并将每个文件的内容存 #储成一个列表的一个元素 new_post = "imaging databases" #用户询问的语句 import nltk.stem #c)装载nltk的stem english_stemmer = nltk.stem.SnowballStemmer('english') #c)提取‘english’集合的stemmer #c)例如输入english_stemer.stem(‘imaging’)返回的就是u’imag’,即相同意思不同表示 ##########用nltk的stemmer扩展计算词频向量函数############ class StemmedCountVectorizer(CountVectorizer): def build_analyzer(self): analyzer = super(StemmedCountVectorizer, self).build_analyzer() #c)提取计算词频类中方法? return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc)) #c)通过nltk逐个处理单词 # vectorizer = CountVectorizer(min_df=1, stop_words='english', # preprocessor=stemmer) #b)传递需要的参数:忽略低于出现1次的单词;采用‘english’集合的 stop word vectorizer = StemmedCountVectorizer(min_df=1, stop_words='english') #b)c)得到装有nltk的词频向量类扩展版 ########################################################## #上面是基于CountVectorizer的;下面是基于TfidfVectorizer的 ##########用nltk的stemmer扩展计算tf-idf的词频向量函数############ from sklearn.feature_extraction.text import TfidfVectorizer #d)装载sklearn中的tfidf向量类,而且该TfidfVectorize是继承自CountVectorizer类 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', charset_error='ignore') #d)得到加入nltk的tfidf版本的计算词频向量函数 ########################################################## print(vectorizer) #使用这个类计算得到的就不再是词频,而是TF-IDF值 X_train = vectorizer.fit_transform(posts) #将传递的参数表示的列表转换成对应的特征向量,书p53 num_samples, num_features = X_train.shape #样本个数:特征词个数(字典的维度) print("#samples: %d, #features: %d" % (num_samples, num_features)) new_post_vec = vectorizer.transform([new_post]) #将用户询问文本转换成词频(或者TF-IDF)向量 print(new_post_vec, type(new_post_vec)) #默认以稀疏矩阵存储 print(new_post_vec.toarray()) #呈现全矩阵形式,将其中0值也显示出来 print(vectorizer.get_feature_names()) #显示所包含的特征词,即形成字典中每个词的名称 #################计算两个词频向量之间的距离###################### def dist_raw(v1, v2): delta = v1 - v2 return sp.linalg.norm(delta.toarray()) #a)返回2范数的结果,因为计算范数的时候需要全矩阵形式 def dist_norm(v1, v2): v1_normalized = v1 / sp.linalg.norm(v1.toarray()) v2_normalized = v2 / sp.linalg.norm(v2.toarray()) #a)标准化了每个单独的词频向量,解决文本中故意重复的问题 #a)比如原本是‘abc’,故意重复就是‘abcabcabc’ 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) #读取第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))
ps:在上面装载完vectorizer的时候,可以通过:
>>> sorted(vectorizer.get_stop_words())[0:20]
这会显示在'english'这个集合中前20个stop word,比如’am‘,’about‘等等。
上面的d)中tf-idf的原理性的代码解释如下,通过这个处理之后得到样本的特征向量中的值的不是词频数,而是tf-idf特征:
1、拆分所有文本,统计字典中包含的特征词;(有stemmed过的,各种)
2、丢弃出现在每个样本中差不多都出现的,对检测没有帮助的词;(stop word,及大于max_df次数的词)
3、丢弃出现只有零星几个样本中出现的,对检测没有帮助的词;(小于min_df次数的词)
4、计算剩下的词;
5、计算每个样本中对应的向量上每个特征词的tf-idf,而不是出现的次数。(基于整个文本语料库)
# -*- coding: utf-8 -*- """ Created on Fri Nov 30 15:50:11 2012 @author: wilrich """ import scipy as sp def tfidf(t, d, D): tf = float(d.count(t)) / sum(d.count(w) for w in set(d)) #计算tf:在独立样本中某个词出现的次数所占所有词的比例 idf = sp.log(float(len(D)) / (len([doc for doc in D if t in doc]))) #计算idf:这里采用log函数,样本的总数/包含那个单词的样本的个数,即>1,越大那么这个词出现的概率就越低。 return tf * idf a, abb, abc = ["a"], ["a", "b", "b"], ["a", "b", "c"] #每个独立样本 D = [a, abb, abc] #总的样本 print(tfidf("a", a, D)) print(tfidf("b", abb, D)) print(tfidf("a", abc, D)) print(tfidf("b", abc, D)) print(tfidf("c", abc, D))
不过对于词袋来说,简单有效,可是带来的问题也不容忽视:
1、没有涉及到词之间的相关性,比如‘car hits wall’和‘wall hits car’,对于词袋来说是一样的;
2、不能辨别相反意思的句子,比如‘i am a boy’和‘i am not a boy’;该问题可以通过多个词组,比如二元组(一次处理2个词),或者三元组(一次处理3个词);
3、没法处理拼写错误的词,比如‘database’和‘databas’,后者只是写错了,即没法支持模糊识别。
二、聚类
经过上面的处理,现在每个原始文本样本都转换成了数值向量的形式,接下来就可以聚类了,一般来说大部分的聚类有两种:a)平铺聚类(flat clustering)和层次聚类(hierarchical clustering)。所谓的平铺聚类就是聚类之后,不同类别之间没有任何的关联,就是任何样本都只属于某一个类别,而且大部分的平铺聚类都需要提前设定类别个数;相比较来说,层次聚类就是类别的个数无需指定,可以想象是个二叉树一样,底层就是许多小类别,然后一层层往上,最后形成只有一个类别,包含了所有样本。即相似的样本放在一个类中,而相似的类放在另一个超类中,以此类推,不过该方法效率低下,代价较大。Scikit提供了很多聚类的方法,在sklearn.cluster中。具体的可以查阅:Http://scikit-learn.org/dev/modules/clustering.html,其中kmeans的具体细节。
这里介绍的是平铺聚类中的Kmean。在scikit中对于容忍阈值默认为0.0001,也就是当迭代kmean的时候,如果改变低于这个值,就算是收敛了,不然就接着迭代。
# 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 #装载KMeans类 seed = 2 #种子数 sp.random.seed(seed) # to reproduce the data later on num_clusters = 3 #初始设定的类别个数 ##############定义显示聚类的函数begin:######################## 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 ##############:end定义显示聚类的函数############################ ########用随机数生成不同的3类的数据点,每个类20个############### 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.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)))) 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_ #读取KMeans之后的三个聚类中心 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 #################### 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
下面就是基于数据集”20news-18828“上的kmean过程,该数据集可在:这里下载,mlcomp这个网站包含了几千个数据集,专为ml领域的建立的。
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"P:Dropboxpymlbookdata" #下载的20news-18828数据的解压路径 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("Number of posts:", len(dataset.filenames)) labels = dataset.target num_clusters = 50 # sp.unique(labels).shape[0] #################下面是文本数据转换成tf-idf的词袋向量形式################# 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='ignore' ) #防止非法字符的UnicodeDecodeError,所以需要忽略这些 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] #聚类后取出最接近询问的类 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) #对计算的相似性排序 #############下面是找出最相似的###################### import pdb pdb.set_trace() 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)
本文倒数两段代码并没有详细的注释,懒了!
(上面两个代码注解和本章节的”solving our initial challenge“部分 待续。。。。。)
2015/07/25,第0次修改!