转自:http://www.cnblogs.com/jerome-rong/archive/2012/05/22/2513262.html
另外推荐一本书:《Mahout in Action》
聚类分析
什么是聚类分析?
聚类 (Clustering) 就是将数据对象分组成为多个类或者簇 (Cluster),它的目标是:在同一个簇中的对象之间具有较高的相似度,而不同簇中的对象差别较大。所以,在很多应用中,一个簇中的数据对象可以被作为一个整体来对待,从而减少计算量或者提高计算质量。
其实聚类是一个人们日常生活的常见行为,即所谓“物以类聚,人以群分”,核心的思想也就是聚类。人们总是不断地改进下意识中的聚类模式来学习如何区分各个事物和人。同时,聚类分析已经广泛的应用在许多应用中,包括模式识别,数据分析,图像处理以及市场研究。通过聚类,人们能意识到密集和稀疏的区域,发现全局的分布模式,以及数据属性之间的有趣的相互关系。
聚类同时也在 Web 应用中起到越来越重要的作用。最被广泛使用的既是对 Web 上的文档进行分类,组织信息的发布,给用户一个有效分类的内容浏览系统(门户网站),同时可以加入时间因素,进而发现各个类内容的信息发展,最近被大家关注的主题和话题,或者分析一段时间内人们对什么样的内容比较感兴趣,这些有趣的应用都得建立在聚类的基础之上。作为一个数据挖掘的功能,聚类分析能作为独立的工具来获得数据分布的情况,观察每个簇的特点,集中对特定的某些簇做进一步的分析,此外,聚类分析还可以作为其他算法的预处理步骤,简化计算量,提高分析效率,这也是我们在这里介绍聚类分析的目的。
不同的聚类问题
对于一个聚类问题,要挑选最适合最高效的算法必须对要解决的聚类问题本身进行剖析,下面我们就从几个侧面分析一下聚类问题的需求。
聚类结果是排他的还是可重叠的
为了很好理解这个问题,我们以一个例子进行分析,假设你的聚类问题需要得到二个簇:“喜欢詹姆斯卡梅隆电影的用户”和“不喜欢詹姆斯卡梅隆的用户”,这其实是一个排他的聚类问题,对于一个用户,他要么属于“喜欢”的簇,要么属于不喜欢的簇。但如果你的聚类问题是“喜欢詹姆斯卡梅隆电影的用户”和“喜欢里奥纳多电影的用户”,那么这个聚类问题就是一个可重叠的问题,一个用户他可以既喜欢詹姆斯卡梅隆又喜欢里奥纳多。
所以这个问题的核心是,对于一个元素,他是否可以属于聚类结果中的多个簇中,如果是,则是一个可重叠的聚类问题,如果否,那么是一个排他的聚类问题。
基于层次还是基于划分
其实大部分人想到的聚类问题都是“划分”问题,就是拿到一组对象,按照一定的原则将它们分成不同的组,这是典型的划分聚类问题。但除了基于划分的聚类,还有一种在日常生活中也很常见的类型,就是基于层次的聚类问题,它的聚类结果是将这些对象分等级,在顶层将对象进行大致的分组,随后每一组再被进一步的细分,也许所有路径最终都要到达一个单独实例,这是一种“自顶向下”的层次聚类解决方法,对应的,也有“自底向上”的。其实可以简单的理解,“自顶向下”就是一步步的细化分组,而“自底向上”就是一步步的归并分组。
簇数目固定的还是无限制的聚类
这个属性很好理解,就是你的聚类问题是在执行聚类算法前已经确定聚类的结果应该得到多少簇,还是根据数据本身的特征,由聚类算法选择合适的簇的数目。
基于距离还是基于概率分布模型
在本系列的第二篇介绍协同过滤的文章中,我们已经详细介绍了相似性和距离的概念。基于距离的聚类问题应该很好理解,就是将距离近的相似的对象聚在一起。相比起来,基于概率分布模型的,可能不太好理解,那么下面给个简单的例子。
一个概率分布模型可以理解是在 N 维空间的一组点的分布,而它们的分布往往符合一定的特征,比如组成一个特定的形状。基于概率分布模型的聚类问题,就是在一组对象中,找到能符合特定分布模型的点的集合,他们不一定是距离最近的或者最相似的,而是能完美的呈现出概率分布模型所描述的模型。
下面图 1 给出了一个例子,对同样一组点集,应用不同的聚类策略,得到完全不同的聚类结果。左侧给出的结果是基于距离的,核心的原则就是将距离近的点聚在一起,右侧给出的基于概率分布模型的聚类结果,这里采用的概率分布模型是一定弧度的椭圆。图中专门标出了两个红色的点,这两点的距离很近,在基于距离的聚类中,将他们聚在一个类中,但基于概率分布模型的聚类则将它们分在不同的类中,只是为了满足特定的概率分布模型(当然这里我特意举了一个比较极端的例子)。所以我们可以看出,在基于概率分布模型的聚类方法里,核心是模型的定义,不同的模型可能导致完全不同的聚类结果。
Apache Mahout 中的聚类分析框架
Apache Mahout 是 Apache Software Foundation (ASF) 旗下的一个开源项目,提供一些可扩展的机器学习领域经典算法的实现,旨在帮助开发人员更加方便快捷地创建智能应用程序,并且,在 Mahout 的最近版本中还加入了对 Apache Hadoop 的支持,使这些算法可以更高效的运行在云计算环境中。
关于 Apache Mahout 的安装和配置请参考《基于 Apache Mahout 构建社会化推荐引擎》,它是笔者 09 年发表的一篇关于基于 Mahout 实现推荐引擎的 developerWorks 文章,其中详细介绍了 Mahout 的安装步骤。
Mahout 中提供了常用的多种聚类算法,涉及我们刚刚讨论过的各种类型算法的具体实现,下面我们就进一步深入几个典型的聚类算法的原理,优缺点和实用场景,以及如何使用 Mahout 高效的实现它们。
回页首
深入聚类算法
深入介绍聚类算法之前,这里先对 Mahout 中对各种聚类问题的数据模型进行简要的介绍。
数据模型
Mahout 的聚类算法将对象表示成一种简单的数据模型:向量 (Vector)。在向量数据描述的基础上,我们可以轻松的计算两个对象的相似性,关于向量和向量的相似度计算,本系列的上一篇介绍协同过滤算法的文章中已经进行了详细的介绍,请参考《“探索推荐引擎内部的秘密”系列 - Part 2: 深入推荐引擎相关算法 – 协同过滤》。
Mahout 中的向量 Vector 是一个每个域是浮点数 (double) 的复合对象,最容易联想到的实现就是一个浮点数的数组。但在具体应用由于向量本身数据内容的不同,比如有些向量的值很密集,每个域都有值;有些呢则是很稀疏,可能只有少量域有值,所以 Mahout 提供了多个实现:
DenseVector,它的实现就是一个浮点数数组,对向量里所有域都进行存储,适合用于存储密集向量。
RandomAccessSparseVector 基于浮点数的 HashMap 实现的,key 是整形 (int) 类型,value 是浮点数 (double) 类型,它只存储向量中不为空的值,并提供随机访问。
SequentialAccessVector 实现为整形 (int) 类型和浮点数 (double) 类型的并行数组,它也只存储向量中不为空的值,但只提供顺序访问。
用户可以根据自己算法的需求选择合适的向量实现类,如果算法需要很多随机访问,应该选择 DenseVector 或者 RandomAccessSparseVector,如果大部分都是顺序访问,SequentialAccessVector 的效果应该更好。
介绍了向量的实现,下面我们看看如何将现有的数据建模成向量,术语就是“如何对数据进行向量化”,以便采用 Mahout 的各种高效的聚类算法。
简单的整形或浮点型的数据
这种数据最简单,只要将不同的域存在向量中即可,比如 n 维空间的点,其实本身可以被描述为一个向量。
枚举类型数据
这类数据是对物体的描述,只是取值范围有限。举个例子,假设你有一个苹果信息的数据集,每个苹果的数据包括:大小,重量,颜色等,我们以颜色为例,设苹果的颜色数据包括:红色,黄色和绿色。在对数据进行建模时,我们可以用数字来表示颜色,红色 =1,黄色 =2,绿色 =3,那么大小直径 8cm,重量 0.15kg,颜色是红色的苹果,建模的向量就是 <8, 0.15, 1>。
下面的清单 1 给出了对以上两种数据进行向量化的例子。
清单 1. 创建简单的向量
// 创建一个二维点集的向量组
public static final double[][] points = { { 1, 1 }, { 2, 1 }, { 1, 2 },
{ 2, 2 }, { 3, 3 }, { 8, 8 }, { 9, 8 }, { 8, 9 }, { 9, 9 }, { 5, 5 },
{ 5, 6 }, { 6, 6 }};
public static List getPointVectors(double[][] raw) {
List points = new ArrayList();
for (int i = 0; i < raw.length; i++) {
double[] fr = raw[i];
// 这里选择创建 RandomAccessSparseVector
Vector vec = new RandomAccessSparseVector(fr.length);
// 将数据存放在创建的 Vector 中
vec.assign(fr);
points.add(vec);
}
return points;
}
// 创建苹果信息数据的向量组
public static List generateAppleData() {
List apples = new ArrayList();
// 这里创建的是 NamedVector,其实就是在上面几种 Vector 的基础上,
//为每个 Vector 提供一个可读的名字
NamedVector apple = new NamedVector(new DenseVector(
new double[] {0.11, 510, 1}),
“Small round green apple”);
apples.add(apple);
apple = new NamedVector(new DenseVector(new double[] {0.2, 650, 3}),
“Large oval red apple”);
apples.add(apple);
apple = new NamedVector(new DenseVector(new double[] {0.09, 630, 1}),
“Small elongated red apple”);
apples.add(apple);
apple = new NamedVector(new DenseVector(new double[] {0.25, 590, 3}),
“Large round yellow apple”);
apples.add(apple);
apple = new NamedVector(new DenseVector(new double[] {0.18, 520, 2}),
“Medium oval green apple”);
apples.add(apple);
return apples;
}
文本信息
作为聚类算法的主要应用场景 - 文本分类,对文本信息的建模也是一个常见的问题。在信息检索研究领域已经有很好的建模方式,就是信息检索领域中最常用的向量空间模型 (Vector Space Model, VSM)。因为向量空间模型不是本文的重点,这里给一个简要的介绍,有兴趣的朋友可以查阅参考目录中给出的相关文档。
文本的向量空间模型就是将文本信息建模为一个向量,其中每一个域是文本中出现的一个词的权重。关于权重的计算则有很多中:
最简单的莫过于直接计数,就是词在文本里出现的次数。这种方法简单,但是对文本内容描述的不够精确。
词的频率 (Team Frequency, TF):就是将词在文本中出现的频率作为词的权重。这种方法只是对于直接计数进行了归一化处理,目的是让不同长度的文本模型有统一的取值空间,便于文本相似度的比较,但可以看出,简单计数和词频都不能解决“高频无意义词汇权重大的问题”,也就是说对于英文文本中,“a”,“the”这样高频但无实际意义的词汇并没有进行过滤,这样的文本模型在计算文本相似度时会很不准确。
词频 - 逆向文本频率 (Term Frequency – Inverse Document Frequency, TF-IDF):它是对 TF 方法的一种加强,字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在所有文本中出现的频率成反比下降。举个例子,对于“高频无意义词汇”,因为它们大部分会出现在所有的文本中,所以它们的权重会大打折扣,这样就使得文本模型在描述文本特征上更加精确。在信息检索领域,TF-IDF 是对文本信息建模的最常用的方法。
对于文本信息的向量化,Mahout 已经提供了工具类,它基于 Lucene 给出了对文本信息进行分析,然后创建文本向量。下面的清单 2 给出了一个例子,分析的文本数据是路透提供的新闻数据,参考资源里给出了下载地址。将数据集下载后,放在“clustering/reuters”目录下。
清单 2. 创建文本信息的向量
public static void documentVectorize(String[] args) throws Exception{
//1. 将路透的数据解压缩 , Mahout 提供了专门的方法
DocumentClustering.extractReuters();
//2. 将数据存储成 SequenceFile,因为这些工具类就是在 Hadoop 的基础上做的,所以首先我们需要将数据写
// 成 SequenceFile,以便读取和计算
DocumentClustering.transformToSequenceFile();
//3. 将 SequenceFile 文件中的数据,基于 Lucene 的工具进行向量化
DocumentClustering.transformToVector();
}
public static void extractReuters(){
//ExtractReuters 是基于 Hadoop 的实现,所以需要将输入输出的文件目录传给它,这里我们可以直接把它映
// 射到我们本地的一个文件夹,解压后的数据将写入输出目录下
File inputFolder = new File(“clustering/reuters”);
File outputFolder = new File(“clustering/reuters-extracted”);
ExtractReuters extractor = new ExtractReuters(inputFolder, outputFolder);
extractor.extract();
}
public static void transformToSequenceFile(){
//SequenceFilesFromDirectory 实现将某个文件目录下的所有文件写入一个 SequenceFiles 的功能
// 它其实本身是一个工具类,可以直接用命令行调用,这里直接调用了它的 main 方法
String[] args = {“-c”, “UTF-8”, “-i”, “clustering/reuters-extracted/”, “-o”,
“clustering/reuters-seqfiles”};
// 解释一下参数的意义:
// -c: 指定文件的编码形式,这里用的是”UTF-8”
// -i: 指定输入的文件目录,这里指到我们刚刚导出文件的目录
// -o: 指定输出的文件目录
try {
SequenceFilesFromDirectory.main(args);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void transformToVector(){
//SparseVectorsFromSequenceFiles 实现将 SequenceFiles 中的数据进行向量化。
// 它其实本身是一个工具类,可以直接用命令行调用,这里直接调用了它的 main 方法
String[] args = {“-i”, “clustering/reuters-seqfiles/”, “-o”,
“clustering/reuters-vectors-bigram”, “-a”,
“org.apache.lucene.analysis.WhitespaceAnalyzer”
, “-chunk”, “200”, “-wt”, “tfidf”, “-s”, “5”,
“-md”, “3”, “-x”, “90”, “-ng”, “2”, “-ml”, “50”, “-seq”};
// 解释一下参数的意义:
// -i: 指定输入的文件目录,这里指到我们刚刚生成 SequenceFiles 的目录
// -o: 指定输出的文件目录
// -a: 指定使用的 Analyzer,这里用的是 lucene 的空格分词的 Analyzer
// -chunk: 指定 Chunk 的大小,单位是 M。对于大的文件集合,我们不能一次 load 所有文件,所以需要
// 对数据进行切块
// -wt: 指定分析时采用的计算权重的模式,这里选了 tfidf
// -s: 指定词语在整个文本集合出现的最低频度,低于这个频度的词汇将被丢掉
// -md: 指定词语在多少不同的文本中出现的最低值,低于这个值的词汇将被丢掉
// -x: 指定高频词汇和无意义词汇(例如 is,a,the 等)的出现频率上限,高于上限的将被丢掉
// -ng: 指定分词后考虑词汇的最大长度,例如 1-gram 就是,coca,cola,这是两个词,
// 2-gram 时,coca cola 是一个词汇,2-gram 比 1-gram 在一定情况下分析的更准确。
// -ml: 指定判断相邻词语是不是属于一个词汇的相似度阈值,当选择 >1-gram 时才有用,其实计算的是
// Minimum Log Likelihood Ratio 的阈值
// -seq: 指定生成的向量是 SequentialAccessSparseVectors,没设置时默认生成还是
// RandomAccessSparseVectors
try {
SparseVectorsFromSequenceFiles.main(args);
} catch (Exception e) {
e.printStackTrace();
}
}
这里补充一点,生成的向量化文件的目录结构是这样的:
df-count 目录:保存着文本的频率信息
tf-vectors 目录:保存着以 TF 作为权值的文本向量
tfidf-vectors 目录:保存着以 TFIDF 作为权值的文本向量
tokenized-documents 目录:保存着分词过后的文本信息
wordcount 目录:保存着全局的词汇出现的次数
dictionary.file-0 目录:保存着这些文本的词汇表
frequcency-file-0 目录 : 保存着词汇表对应的频率信息。
介绍完向量化问题,下面我们深入分析各个聚类算法,首先介绍的是最经典的 K 均值算法。
下面还有,请看原博客:http://www.cnblogs.com/jerome-rong/archive/2012/05/22/2513262.html