上一次我们了解了一个最基本的 clustering 办法 k-means ,这次要说的 k-medoids 算法,其实从名字上就可以看出来,和 k-means 肯定是非常相似的。事实也确实如此,k-medoids 可以算是 k-means 的一个变种。

k-medoids 和 k-means 不一样的地方在于中心点的选取,在 k-means 中,我们将中心点取为当前 cluster 中所有数据点的平均值:

Rough Collie

并且我们已经证明在固定了各个数据点的 assignment 的情况下,这样选取的中心点能够把目标函数  最小化。然而在 k-medoids 中,我们将中心点的选取限制在当前 cluster 所包含的数据点的集合中。换句话说,在 k-medoids 算法中,我们将从当前 cluster 中选取这样一个点——它到其他所有(当前 cluster 中的)点的距离之和最小——作为中心点。k-means 和 k-medoids 之间的差异就类似于一个数据样本的均值 (mean) 和中位数 (median) 之间的差异:前者的取值范围可以是连续空间中的任意值,而后者只能在给样本给定的那些点里面选。那么,这样做的好处是什么呢?
一个最直接的理由就是 k-means 对数据的要求太高了,它使用欧氏距离描述数据点之间的差异 (dissimilarity) ,从而可以直接通过求均值来计算中心点。这要求数据点处在一个欧氏空间之中。

然而并不是所有的数据都能满足这样的要求,对于数值类型的特征,比如身高,可以很自然地用这样的方式来处理,但是类别 (categorical) 类型的特征就不行了。举一个简单的例子,如果我现在要对犬进行聚类,并且希望直接在所有犬组成的空间中进行,k-means 就无能为力了,因为欧氏距离  在这里不能用了:一只 Samoyed 减去一只Rough Collie 然后在平方一下?天知道那是什么!再加上一只 German Shepherd Dog 然后求一下平均值?根本没法算,k-means 在这里寸步难行!

在 k-medoids 中,我们把原来的目标函数  中的欧氏距离改为一个任意的 dissimilarity measure 函数 

最常见的方式是构造一个 dissimilarity matrix  来代表 ,其中的元素  表示第  只狗和第  只狗之间的差异程度,例如,两只 Samoyed 之间的差异可以设为 0 ,一只 German Shepherd Dog 和一只 Rough Collie 之间的差异是 0.7,和一只 Miniature Schnauzer 之间的差异是 1 ,等等。

除此之外,由于中心点是在已有的数据点里面选取的,因此相对于 k-means 来说,不容易受到那些由于误差之类的原因产生的 Outlier 的影响,更加 robust 一些。

扯了这么多,还是直接来看看 k-medoids 的效果好了,由于 k-medoids 对数据的要求比 k-means 要低,所以 k-means 能处理的情况自然 k-medoids 也能处理,为了能先睹为快,我们偷一下懒,直接在上一篇文章中的 k-means 代码的基础上稍作一点修改,还用同样的例子。将代码的 45 到 47 行改成下面这样:

  1. 45
  2. 46
  3. 47
  4. 48
  5. 49
  6. 50
  1. for j in range(k):
  2. idx_j = (labels == j).nonzero()
  3. distj = distmat(X[idx_j], X[idx_j])
  4. distsum = ml.sum(distj, axis=1)
  5. icenter = distsum.argmin()
  6. centers[j] = X[idx_j[0][icenter]]

可以看到 k-medoids 在这个例子上也能得到很好的结果:

而且,同 k-means 一样,运气不好的时候也会陷入局部最优解中:

如果仔细看上面那段代码的话,就会发现,从 k-means 变到 k-medoids ,时间复杂度陡然增加了许多:在 k-means 中只要求一个平均值  即可,而在 k-medoids 中则需要枚举每个点,并求出它到所有其他点的距离之和,复杂度为  。

看完了直观的例子,让我们再来看一个稍微实际一点的例子好了:Document Clustering ——那个永恒不变的主题,不过我们这里要做的聚类并不是针对文档的主题,而是针对文档的语言。实验数据是从 Europarl 下载的包含 Danish、German、Greek、English、Spanish、Finnish、French、Italian、Dutch、Portuguese 和 Swedish 这些语言的文本数据集。

在 N-gram-based text categorization 这篇 paper 中描述了一种计算由不同语言写成的文档的相似度的方法。一个(以字符为单位的) N-gram 就相当于长度为 N 的一系列连续子串。例如,由 hello 产生的 3-gram 为:hel、ell 和 llo ,有时候还会在划分 N-gram 之前在开头和末尾加上空格(这里用下划线表示):_he、hel、ell、llo、lo_ 和 o__ 。按照 Zipf’s law :

The nth most common word in a human language text occurs with a frequency inversely proportional to n.

这里我们用 N-gram 来代替 word 。这样,我们从一个文档中可以得到一个 N-gram 的频率分布,按照频率排序一下,只保留频率最高的前 k 个(比如,300)N-gram,我们把叫做一个“Profile”。正常情况下,某一种语言(至少是西方国家的那些类英语的语言)写成的文档,不论主题或长短,通常得出来的 Profile 都差不多,亦即按照出现的频率排序所得到的各个 N-gram 的序号不会变化太大。这是非常好的一个性质:通常我们只要各个语言选取一篇(比较正常的,也不需要很长)文档构建出一个 Profile ,在拿到一篇未知文档的时候,只要和各个 Profile 比较一下,差异最小的那个 Profile 所对应的语言就可以认定是这篇未知文档的语言了——准确率很高,更可贵的是,所需要的训练数据非常少而且容易获得,训练出来的模型也是非常小的。

不过,我们这里且撇开分类(Classification)的问题,回到聚类(Clustering)上,按照前面的说法,在 k-medoids 聚类中,只需要定义好两个东西之间的距离(或者 dissimilarity )就可以了,对于两个 Profile ,它们之间的 dissimilarity 可以很自然地定义为对应的 N-gram 的序号之差的绝对值,在 Python 中用下面这样一个类来表示:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  1. class Profile(object):
  2. def __init__(self, path, N=3, psize=400):
  3. self.N = N
  4. self.psize = psize
  5. self.build_profile(path)
  6.  
  7. sep = re.compile(r'\W+')
  8. def build_profile(self, path):
  9. grams = {}
  10. with open(path) as inf:
  11. for line in inf:
  12. for tok in self.sep.split(line):
  13. for n in range(self.N):
  14. self.feed_ngram(grams, tok, n+1)
  15. self.create_profile(grams.items())
  16.  
  17. def create_profile(self, grams):
  18. # keep only the top most psize items
  19. grams.sort(key=itemgetter(1), reverse=True)
  20. grams = grams[:self.psize]
  21.  
  22. self.profile = dict()
  23. for i in range(len(grams)):
  24. self.profile[grams[i][0]] = i
  25.  
  26. def __getitem__(self, key):
  27. idx = self.profile.get(key)
  28. if idx is None:
  29. return len(self.profile)
  30. return idx
  31.  
  32. def dissimilarity(self, o):
  33. dis = 0
  34. for tok in self.profile.keys():
  35. dis += abs(self[tok]-o[tok])
  36. for tok in o.profile.keys():
  37. dis += abs(self[tok]-o[tok])
  38. return dis
  39.  
  40. def feed_ngram(self, grams, tok, n):
  41. if n != 0:
  42. tok = '_' + tok
  43. tok = tok + '_' * (n-1)
  44. for i in range(len(tok)-n+1):
  45. gram = tok[i:i+n]
  46. grams.setdefault(gram, 0)
  47. grams[gram] += 1

europarl 数据集共有 11 种语言的文档,每种语言包括大约 600 多个文档。我为这七千多个文档建立了 Profile 并构造出一个 7038×7038 的 dissimilarity matrix ,最后在这上面用 k-medoids 进行聚类。构造 dissimilarity matrix 的过程很慢,在我这里花了将近 10 个小时。相比之下,k-medoids 的过程在内存允许的情况下,采用向量化的方法来做实际上还是很快的,并且通常只要数次迭代就能收敛了。实际的 k-medoids 实现可以在 mltk 中找到,今后如果有时间的话,我会陆续地把一些相关的比较通用的代码放到那里面。

最后,然我们来看看聚类的结果,由于聚类和分类不同,只是得到一些 cluster ,而并不知道这些 cluster 应该被打上什么标签,或者说,在这个问题里,包含了哪种语言的文档。由于我们的目的是衡量聚类算法的 performance ,因此直接假定这一步能实现最优的对应关系,将每个 cluster 对应到一种语言上去。一种办法是枚举所有可能的情况并选出最优解,另外,对于这样的问题,我们还可以用 Hungarian algorithm 来求解。

我们这里有 11 种语言,全排列有 11! = 39916800 种情况, 对于每一种排列,我们需要遍历一次 label list ,并数出真正的 label (语言)与聚类得出的结果相同的文档的个数,再除以总的文档个数,得到 accuracy 。假设每次遍历并求出 accuracy 只需要 1 毫秒的时间的话,总共也需要 11 个小时才能得到结果。看上去好像也不是特别恐怖,不过相比起来,用 Hungarian algorithm 的话,我们可以几乎瞬间得到结果。由于文章的篇幅已经很长了,就不在这里介绍具体的算法了,感兴趣的同学可以参考 Wikipedia ,这里我直接使用了一个现有的Python 实现

虽然这个实验非常折腾,不过最后的结果其实就是一个数字:accuracy ——在我这里达到了 88.97% ,证明 k-medoids 聚类和 N-gram Profile 识别语言这两种方法都是挺不错的。最后,如果有感兴趣的同学,代码可以从这里下载。需要最新版的 scipy, munkres.py 和 mltk以及 Python 2.6 。

漫谈 Clustering (2): k-medoids的更多相关文章

  1. 漫谈 Clustering (4): Spectral Clustering

    转:http://blog.pluskid.org/?p=287 如果说 K-means 和 GMM 这些聚类的方法是古代流行的算法的话,那么这次要讲的 Spectral Clustering 就可以 ...

  2. 漫谈 Clustering (番外篇): Dimensionality Reduction

    由于总是有各种各样的杂事,这个系列的文章竟然一下子拖了好几个月,(实际上其他的日志我也写得比较少),现在决定还是先把这篇降维的日志写完.我甚至都以及忘记了在这个系列中之前有没有讲过“特征”(featu ...

  3. 漫谈 Clustering (3): Gaussian Mixture Model

    上一次我们谈到了用 k-means 进行聚类的方法,这次我们来说一下另一个很流行的算法:Gaussian Mixture Model (GMM).事实上,GMM 和 k-means 很像,不过 GMM ...

  4. 漫谈 Clustering (1): k-means

    好久没有写 blog 了,一来是 blog 下线一段时间,而租 DreamHost 的事情又一直没弄好:二来是没有太多时间,天天都跑去实验室.现在主要折腾 Machine Learning 相关的东西 ...

  5. 漫谈 Clustering (4): Spectral Clustering<转载>

    转自http://blog.pluskid.org/?p=287 如果说 K-means 和 GMM 这些聚类的方法是古代流行的算法的话,那么这次要讲的 Spectral Clustering 就可以 ...

  6. 漫谈 Clustering (番外篇): Expectation Maximization

    Expectation Maximization (EM) 是一种以迭代的方式来解决一类特殊最大似然 (Maximum Likelihood) 问题的方法,这类问题通常是无法直接求得最优解,但是如果引 ...

  7. 漫谈 Clustering (番外篇): Vector Quantization

    在接下去说其他的聚类算法之前,让我们先插进来说一说一个有点跑题的东西:Vector Quantization.这项技术广泛地用在信号处理以及数据压缩等领域.事实上,在 JPEG 和 MPEG-4 等多 ...

  8. 漫谈 Clustering (5): Hierarchical Clustering

    系列不小心又拖了好久,其实正儿八经的 blog 也好久没有写了,因为比较忙嘛,不过觉得 Hierarchical Clustering 这个话题我能说的东西应该不多,所以还是先写了吧(我准备这次一个公 ...

  9. [Scikit-learn] 2.3 Clustering - kmeans

    参考: 2.3. Clustering 2.4. Biclustering 2.1.2.3. The Dirichlet Process Clusering, GMM, Variational Inf ...

随机推荐

  1. [转]AFNetworking 3.0迁移指南

    http://www.jianshu.com/p/047463a7ce9b?utm_campaign=hugo&utm_medium=reader_share&utm_content= ...

  2. 树状数组 洛谷P3616 富金森林公园

    P3616 富金森林公园 题目描述 博艾的富金森林公园里有一个长长的富金山脉,山脉是由一块块巨石并列构成的,编号从1到N.每一个巨石有一个海拔高度.而这个山脉又在一个盆地中,盆地里可能会积水,积水也有 ...

  3. JOS环境搭建

    想写一个OS很久了,今天开始正式动工了!! 大家都知道操作系统是计算机科学中十分重要的一门基础学科.但是以前在学习这门课时,仅仅只是把目光停留在课本上一些关于操作系统概念上的叙述,并不能对操作系统有着 ...

  4. POJ1442 Black Box 堆

    用大根堆和小根堆分别存放前$i-1$大的元素前$k-i$小的元素. 将当前序列的元素压入最小堆,如果最小堆的最小数大于最大堆的最大数则进行交换,保证最大堆中的所有数小于最小堆. 因为$i$值每进行一次 ...

  5. 牛客假日团队赛1 G.Superbull

    链接: https://ac.nowcoder.com/acm/contest/918/G 题意: Bessie and her friends are playing hoofball in the ...

  6. 通过Maven管理多个MapReduce项目

    1. 配置Maven环境 首先检查Windows是否配置了maven,进入cmd命令行,输入mvn -version命令,如果出现下图所示的 情形则表示满意配置maven. 从浏览器进入maven官网 ...

  7. <s:property>的用法

    1,访问Action值栈中的普通属性: <s:property value="attrName"/> 2,访问Action值栈中的对象属性(要有get set方法):  ...

  8. js的语法糖?

    ++“”里面的+“”默认被变成“0”了 前端多写了个+号导致的bug,网址后面多一个0.虽然不知道是什么原因,但是感觉是js的隐式替换

  9. mybatis-plus 异常 Invalid bound statement (not found)

    最近吧项目中添加使用了mybatis-plus,发现操作sql的时候出现异常: Invalid bound statement (not found) ,异常位置位于mybatis-plus的jar中 ...

  10. Java字节流与字符流的区别详解

    字节流与字符流 先来看一下流的概念: 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入 ...