智能推薦大都基於海量數據的計算和處理,然而咱們發如今海量數據上高效的運行協同過濾算法以及其餘推薦策略這樣高複雜的算法是有很大的挑戰的,在面對解決這個問題的過程當中,你們提出了不少減小計算量的方法,而聚類無疑是其中最優的選擇之一。 聚類 (Clustering) 是一個數據挖掘的經典問題,它的目的是將數據分爲多個簇 (Cluster),在同一個簇中的對象之間有較高的類似度,而不一樣簇的對象差異較大。聚類被普遍的應用於數據處理和統計分析領域。Apache Mahout 是 ASF(Apache Software Foundation) 的一個較新的開源項目,它源於 Lucene,構建在 Hadoop 之上,關注海量數據上的機器學習經典算法的高效實現。本文主要介紹如何基於 Apache Mahout 實現高效的聚類算法,從而實現更高效的數據處理和分析的應用。html
聚類 (Clustering) 就是將數據對象分組成爲多個類或者簇 (Cluster),它的目標是:在同一個簇中的對象之間具備較高的類似度,而不一樣簇中的對象差異較大。因此,在不少應用中,一個簇中的數據對象能夠被做爲一個總體來對待,從而減小計算量或者提升計算質量。 其實聚類是一我的們平常生活的常見行爲,即所謂「物以類聚,人以羣分」,核心的思想也就是聚類。人們老是不斷地改進下意識中的聚類模式來學習如何區分各個事物和人。同時,聚類分析已經普遍的應用在許多應用中,包括模式識別,數據分析,圖像處理以及市場研究。經過聚類,人們能意識到密集和稀疏的區域,發現全局的分佈模式,以及數據屬性之間的有趣的相互關係。 聚類同時也在 Web 應用中起到愈來愈重要的做用。最被普遍使用的既是對 Web 上的文檔進行分類,組織信息的發佈,給用戶一個有效分類的內容瀏覽系統(門戶網站),同時能夠加入時間因素,進而發現各個類內容的信息發展,最近被你們關注的主題和話題,或者分析一段時間內人們對什麼樣的內容比較感興趣,這些有趣的應用都得創建在聚類的基礎之上。做爲一個數據挖掘的功能,聚類分析能做爲獨立的工具來得到數據分佈的狀況,觀察每一個簇的特色,集中對特定的某些簇作進一步的分析,此外,聚類分析還能夠做爲其餘算法的預處理步驟,簡化計算量,提升分析效率,這也是咱們在這裏介紹聚類分析的目的。web
對於一個聚類問題,要挑選最適合最高效的算法必須對要解決的聚類問題自己進行剖析,下面咱們就從幾個側面分析一下聚類問題的需求。 聚類結果是排他的仍是可重疊的 爲了很好理解這個問題,咱們以一個例子進行分析,假設你的聚類問題須要獲得二個簇:「喜歡詹姆斯卡梅隆電影的用戶」和「不喜歡詹姆斯卡梅隆的用戶」,這實際上是一個排他的聚類問題,對於一個用戶,他要麼屬於「喜歡」的簇,要麼屬於不喜歡的簇。但若是你的聚類問題是「喜歡詹姆斯卡梅隆電影的用戶」和「喜歡里奧納多電影的用戶」,那麼這個聚類問題就是一個可重疊的問題,一個用戶他能夠既喜歡詹姆斯卡梅隆又喜歡里奧納多。 因此這個問題的核心是,對於一個元素,他是否能夠屬於聚類結果中的多個簇中,若是是,則是一個可重疊的聚類問題,若是否,那麼是一個排他的聚類問題。 基於層次仍是基於劃分 其實大部分人想到的聚類問題都是「劃分」問題,就是拿到一組對象,按照必定的原則將它們分紅不一樣的組,這是典型的劃分聚類問題。但除了基於劃分的聚類,還有一種在平常生活中也很常見的類型,就是基於層次的聚類問題,它的聚類結果是將這些對象分等級,在頂層將對象進行大體的分組,隨後每一組再被進一步的細分,也許全部路徑最終都要到達一個單獨實例,這是一種「自頂向下」的層次聚類解決方法,對應的,也有「自底向上」的。其實能夠簡單的理解,「自頂向下」就是一步步的細化分組,而「自底向上」就是一步步的歸併分組。 簇數目固定的仍是無限制的聚類 這個屬性很好理解,就是你的聚類問題是在執行聚類算法前已經肯定聚類的結果應該獲得多少簇,仍是根據數據自己的特徵,由聚類算法選擇合適的簇的數目。 基於距離仍是基於機率分佈模型 在本系列的第二篇介紹協同過濾的文章中,咱們已經詳細介紹了類似性和距離的概念。基於距離的聚類問題應該很好理解,就是將距離近的類似的對象聚在一塊兒。相比起來,基於機率分佈模型的,可能不太好理解,那麼下面給個簡單的例子。 一個機率分佈模型能夠理解是在 N 維空間的一組點的分佈,而它們的分佈每每符合必定的特徵,好比組成一個特定的形狀。基於機率分佈模型的聚類問題,就是在一組對象中,找到能符合特定分佈模型的點的集合,他們不必定是距離最近的或者最類似的,而是能完美的呈現出機率分佈模型所描述的模型。 下面圖 1 給出了一個例子,對一樣一組點集,應用不一樣的聚類策略,獲得徹底不一樣的聚類結果。左側給出的結果是基於距離的,核心的原則就是將距離近的點聚在一塊兒,右側給出的基於機率分佈模型的聚類結果,這裏採用的機率分佈模型是必定弧度的橢圓。圖中專門標出了兩個紅色的點,這兩點的距離很近,在基於距離的聚類中,將他們聚在一個類中,但基於機率分佈模型的聚類則將它們分在不一樣的類中,只是爲了知足特定的機率分佈模型(固然這裏我特地舉了一個比較極端的例子)。因此咱們能夠看出,在基於機率分佈模型的聚類方法裏,核心是模型的定義,不一樣的模型可能致使徹底不一樣的聚類結果。算法
Apache Mahout 是 Apache Software Foundation (ASF) 旗下的一個開源項目,提供一些可擴展的機器學習領域經典算法的實現,旨在幫助開發人員更加方便快捷地建立智能應用程序,而且,在 Mahout 的最近版本中還加入了對 Apache Hadoop 的支持,使這些算法能夠更高效的運行在雲計算環境中。 關於 Apache Mahout 的安裝和配置請參考《基於 Apache Mahout 構建社會化推薦引擎》,它是筆者 09 年發表的一篇關於基於 Mahout 實現推薦引擎的 developerWorks 文章,其中詳細介紹了 Mahout 的安裝步驟。 Mahout 中提供了經常使用的多種聚類算法,涉及咱們剛剛討論過的各類類型算法的具體實現,下面咱們就進一步深刻幾個典型的聚類算法的原理,優缺點和實用場景,以及如何使用 Mahout 高效的實現它們。apache
深刻介紹聚類算法以前,這裏先對 Mahout 中對各類聚類問題的數據模型進行簡要的介紹。編程
Mahout 的聚類算法將對象表示成一種簡單的數據模型:向量 (Vector)。在向量數據描述的基礎上,咱們能夠輕鬆的計算兩個對象的類似性,關於向量和向量的類似度計算,本系列的上一篇介紹協同過濾算法的文章中已經進行了詳細的介紹,請參考《「探索推薦引擎內部的祕密」系列 - Part 2: 深刻推薦引擎相關算法 -- 協同過濾》。 Mahout 中的向量 Vector 是一個每一個域是浮點數 (double) 的複合對象,最容易聯想到的實現就是一個浮點數的數組。但在具體應用因爲向量自己數據內容的不一樣,好比有些向量的值很密集,每一個域都有值;有些呢則是很稀疏,可能只有少許域有值,因此 Mahout 提供了多個實現:數組
用戶能夠根據本身算法的需求選擇合適的向量實現類,若是算法須要不少隨機訪問,應該選擇 DenseVector 或者 RandomAccessSparseVector,若是大部分都是順序訪問,SequentialAccessVector 的效果應該更好。 介紹了向量的實現,下面咱們看看如何將現有的數據建模成向量,術語就是「如何對數據進行向量化」,以便採用 Mahout 的各類高效的聚類算法。架構
// 建立一個二維點集的向量組 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<Vector> getPointVectors(double[][] raw) { List<Vector> points = new ArrayList<Vector>(); 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<Vector> generateAppleData() { List<Vector> apples = new ArrayList<Vector>(); // 這裏建立的是 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; }
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(); } }
介紹完向量化問題,下面咱們深刻分析各個聚類算法,首先介紹的是最經典的 K 均值算法。app
K 均值是典型的基於距離的排他的劃分方法:給定一個 n 個對象的數據集,它能夠構建數據的 k 個劃分,每一個劃分就是一個聚類,而且 k<=n,同時還須要知足兩個要求:框架
K 均值的基本原理是這樣的,給定 k,即要構建的劃分的數目,dom
當結果簇是密集的,並且簇和簇之間的區別比較明顯時,K 均值的效果比較好。對於處理大數據集,這個算法是相對可伸縮的和高效的,它的複雜度是 O(nkt),n 是對象的個數,k 是簇的數目,t 是迭代的次數,一般 k<<n,且 t<<n,因此算法常常以局部最優結束。 K 均值的最大問題是要求用戶必須事先給出 k 的個數,k 的選擇通常都基於一些經驗值和屢次實驗結果,對於不一樣的數據集,k 的取值沒有可借鑑性。另外,K 均值對「噪音」和孤立點數據是敏感的,少許這類的數據就能對平均值形成極大的影響。 說了這麼多理論的原理,下面咱們基於 Mahout 實現一個簡單的 K 均值算法的例子。如前面介紹的,Mahout 提供了基本的基於內存的實現和基於 Hadoop 的 Map/Reduce 的實現,分別是 KMeansClusterer 和 KMeansDriver,下面給出一個簡單的例子,就基於咱們在清單 1 裏定義的二維點集數據。
// 基於內存的 K 均值聚類算法實現 public static void kMeansClusterInMemoryKMeans(){ // 指定須要聚類的個數,這裏選擇 2 類 int k = 2; // 指定 K 均值聚類算法的最大迭代次數 int maxIter = 3; // 指定 K 均值聚類算法的最大距離閾值 double distanceThreshold = 0.01; // 聲明一個計算距離的方法,這裏選擇了歐幾里德距離 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 這裏構建向量集,使用的是清單 1 裏的二維點集 List<Vector> pointVectors = SimpleDataSet.getPointVectors(SimpleDataSet.points); // 從點集向量中隨機的選擇 k 個做爲簇的中心 List<Vector> randomPoints = RandomSeedGenerator.chooseRandomPoints(pointVectors, k); // 基於前面選中的中心構建簇 List<Cluster> clusters = new ArrayList<Cluster>(); int clusterId = 0; for(Vector v : randomPoints){ clusters.add(new Cluster(v, clusterId ++, measure)); } // 調用 KMeansClusterer.clusterPoints 方法執行 K 均值聚類 List<List<Cluster>> finalClusters = KMeansClusterer.clusterPoints(pointVectors, clusters, measure, maxIter, distanceThreshold); // 打印最終的聚類結果 for(Cluster cluster : finalClusters.get(finalClusters.size() -1)){ System.out.println("Cluster id: " + cluster.getId() + " center: " + cluster.getCenter().asFormatString()); System.out.println(" Points: " + cluster.getNumPoints()); } } // 基於 Hadoop 的 K 均值聚類算法實現 public static void kMeansClusterUsingMapReduce () throws Exception{ // 聲明一個計算距離的方法,這裏選擇了歐幾里德距離 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 指定輸入路徑,如前面介紹的同樣,基於 Hadoop 的實現就是經過指定輸入輸出的文件路徑來指定數據源的。 Path testpoints = new Path("testpoints"); Path output = new Path("output"); // 清空輸入輸出路徑下的數據 HadoopUtil.overwriteOutput(testpoints); HadoopUtil.overwriteOutput(output); RandomUtils.useTestSeed(); // 在輸入路徑下生成點集,與內存的方法不一樣,這裏須要把全部的向量寫進文件,下面給出具體的例子 SimpleDataSet.writePointsToFile(testpoints); // 指定須要聚類的個數,這裏選擇 2 類 int k = 2; // 指定 K 均值聚類算法的最大迭代次數 int maxIter = 3; // 指定 K 均值聚類算法的最大距離閾值 double distanceThreshold = 0.01; // 隨機的選擇 k 個做爲簇的中心 Path clusters = RandomSeedGenerator.buildRandom(testpoints, new Path(output, "clusters-0"), k, measure); // 調用 KMeansDriver.runJob 方法執行 K 均值聚類算法 KMeansDriver.runJob(testpoints, clusters, output, measure, distanceThreshold, maxIter, 1, true, true); // 調用 ClusterDumper 的 printClusters 方法將聚類結果打印出來。 ClusterDumper clusterDumper = new ClusterDumper(new Path(output, "clusters-" + maxIter -1), new Path(output, "clusteredPoints")); clusterDumper.printClusters(null); } //SimpleDataSet 的 writePointsToFile 方法,將測試點集寫入文件裏 // 首先咱們將測試點集包裝成 VectorWritable 形式,從而將它們寫入文件 public static List<VectorWritable> getPoints(double[][] raw) { List<VectorWritable> points = new ArrayList<VectorWritable>(); for (int i = 0; i < raw.length; i++) { double[] fr = raw[i]; Vector vec = new RandomAccessSparseVector(fr.length); vec.assign(fr); // 只是在加入點集前,在 RandomAccessSparseVector 外加了一層 VectorWritable 的包裝 points.add(new VectorWritable(vec)); } return points; } // 將 VectorWritable 的點集寫入文件,這裏涉及一些基本的 Hadoop 編程元素,詳細的請參閱參考資源裏相關的內容 public static void writePointsToFile(Path output) throws IOException { // 調用前面的方法生成點集 List<VectorWritable> pointVectors = getPoints(points); // 設置 Hadoop 的基本配置 Configuration conf = new Configuration(); // 生成 Hadoop 文件系統對象 FileSystem FileSystem fs = FileSystem.get(output.toUri(), conf); // 生成一個 SequenceFile.Writer,它負責將 Vector 寫入文件中 SequenceFile.Writer writer = new SequenceFile.Writer(fs, conf, output, Text.class, VectorWritable.class); // 這裏將向量按照文本形式寫入文件 try { for (VectorWritable vw : pointVectors) { writer.append(new Text(), vw); } } finally { writer.close(); } } 執行結果 KMeans Clustering In Memory Result Cluster id: 0 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[1.8,1.8,0.0],\"state\":[1,1,0], \"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0,\"highWaterMark\":1, \"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2,\"lengthSquared\":-1.0}"} Points: 5 Cluster id: 1 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0], \"values\":[7.142857142857143,7.285714285714286,0.0],\"state\":[1,1,0], \"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0,\"highWaterMark\":1, \"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2,\"lengthSquared\":-1.0}"} Points: 7 KMeans Clustering Using Map/Reduce Result Weight: Point: 1.0: [1.000, 1.000] 1.0: [2.000, 1.000] 1.0: [1.000, 2.000] 1.0: [2.000, 2.000] 1.0: [3.000, 3.000] Weight: Point: 1.0: [8.000, 8.000] 1.0: [9.000, 8.000] 1.0: [8.000, 9.000] 1.0: [9.000, 9.000] 1.0: [5.000, 5.000] 1.0: [5.000, 6.000] 1.0: [6.000, 6.000]
介紹完 K 均值聚類算法,咱們能夠看出它最大的優勢是:原理簡單,實現起來也相對簡單,同時執行效率和對於大數據量的可伸縮性仍是較強的。然而缺點也是很明確的,首先它須要用戶在執行聚類以前就有明確的聚類個數的設置,這一點是用戶在處理大部分問題時都不太可能事先知道的,通常須要經過屢次試驗找出一個最優的 K 值;其次就是,因爲算法在最開始採用隨機選擇初始聚類中心的方法,因此算法對噪音和孤立點的容忍能力較差。所謂噪音就是待聚類對象中錯誤的數據,而孤立點是指與其餘數據距離較遠,類似性較低的數據。對於 K 均值算法,一旦孤立點和噪音在最開始被選做簇中心,對後面整個聚類過程將帶來很大的問題,那麼咱們有什麼方法能夠先快速找出應該選擇多少個簇,同時找到簇的中心,這樣能夠大大優化 K 均值聚類算法的效率,下面咱們就介紹另外一個聚類方法:Canopy 聚類算法。
Canopy 聚類算法的基本原則是:首先應用成本低的近似的距離計算方法高效的將數據分爲多個組,這裏稱爲一個 Canopy,咱們姑且將它翻譯爲「華蓋」,Canopy 之間能夠有重疊的部分;而後採用嚴格的距離計算方式準確的計算在同一 Canopy 中的點,將他們分配與最合適的簇中。Canopy 聚類算法常常用於 K 均值聚類算法的預處理,用來找合適的 k 值和簇中心。 下面詳細介紹一下建立 Canopy 的過程:初始,假設咱們有一組點集 S,而且預設了兩個距離閾值,T1,T2(T1>T2);而後選擇一個點,計算它與 S 中其餘點的距離(這裏採用成本很低的計算方法),將距離在 T1 之內的放入一個 Canopy 中,同時從 S 中去掉那些與此點距離在 T2 之內的點(這裏是爲了保證和中心距離在 T2 之內的點不能再做爲其餘 Canopy 的中心),重複整個過程直到 S 爲空爲止。 對 K 均值的實現同樣,Mahout 也提供了兩個 Canopy 聚類的實現,下面咱們就看看具體的代碼例子。
//Canopy 聚類算法的內存實現 public static void canopyClusterInMemory () { // 設置距離閾值 T1,T2 double T1 = 4.0; double T2 = 3.0; // 調用 CanopyClusterer.createCanopies 方法建立 Canopy,參數分別是: // 1. 須要聚類的點集 // 2. 距離計算方法 // 3. 距離閾值 T1 和 T2 List<Canopy> canopies = CanopyClusterer.createCanopies( SimpleDataSet.getPointVectors(SimpleDataSet.points), new EuclideanDistanceMeasure(), T1, T2); // 打印建立的 Canopy,由於聚類問題很簡單,因此這裏沒有進行下一步精確的聚類。 // 有必須的時候,能夠拿到 Canopy 聚類的結果做爲 K 均值聚類的輸入,能更精確更高效的解決聚類問題 for(Canopy canopy : canopies) { System.out.println("Cluster id: " + canopy.getId() + " center: " + canopy.getCenter().asFormatString()); System.out.println(" Points: " + canopy.getNumPoints()); } } //Canopy 聚類算法的 Hadoop 實現 public static void canopyClusterUsingMapReduce() throws Exception{ // 設置距離閾值 T1,T2 double T1 = 4.0; double T2 = 3.0; // 聲明距離計算的方法 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 設置輸入輸出的文件路徑 Path testpoints = new Path("testpoints"); Path output = new Path("output"); // 清空輸入輸出路徑下的數據 HadoopUtil.overwriteOutput(testpoints); HadoopUtil.overwriteOutput(output); // 將測試點集寫入輸入目錄下 SimpleDataSet.writePointsToFile(testpoints); // 調用 CanopyDriver.buildClusters 的方法執行 Canopy 聚類,參數是: // 1. 輸入路徑,輸出路徑 // 2. 計算距離的方法 // 3. 距離閾值 T1 和 T2 new CanopyDriver().buildClusters(testpoints, output, measure, T1, T2, true); // 打印 Canopy 聚類的結果 List<List<Cluster>> clustersM = DisplayClustering.loadClusters(output); List<Cluster> clusters = clustersM.get(clustersM.size()-1); if(clusters != null){ for(Cluster canopy : clusters) { System.out.println("Cluster id: " + canopy.getId() + " center: " + canopy.getCenter().asFormatString()); System.out.println(" Points: " + canopy.getNumPoints()); } } } 執行結果 Canopy Clustering In Memory Result Cluster id: 0 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[1.8,1.8,0.0], \"state\":[1,1,0],\"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0, \"highWaterMark\":1,\"minLoadFactor\":0.2,\"maxLoadFactor\":0.5}, \"size\":2,\"lengthSquared\":-1.0}"} Points: 5 Cluster id: 1 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[7.5,7.666666666666667,0.0], \"state\":[1,1,0],\"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0, \"highWaterMark\":1,\"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2, \"lengthSquared\":-1.0}"} Points: 6 Cluster id: 2 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[5.0,5.5,0.0], \"state\":[1,1,0],\"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0, \"highWaterMark\":1,\"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2, \"lengthSquared\":-1.0}"} Points: 2 Canopy Clustering Using Map/Reduce Result Cluster id: 0 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[1.8,1.8,0.0], \"state\":[1,1,0],\"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0, \"highWaterMark\":1,\"minLoadFactor\":0.2,\"maxLoadFactor\":0.5}, \"size\":2,\"lengthSquared\":-1.0}"} Points: 5 Cluster id: 1 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0],\"values\":[7.5,7.666666666666667,0.0], \"state\":[1,1,0],\"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0, \"highWaterMark\":1,\"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2, \"lengthSquared\":-1.0}"} Points: 6 Cluster id: 2 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0], \"values\":[5.333333333333333,5.666666666666667,0.0],\"state\":[1,1,0], \"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0,\"highWaterMark\":1, \"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2,\"lengthSquared\":-1.0}"} Points: 3
模糊 K 均值聚類算法是 K 均值聚類的擴展,它的基本原理和 K 均值同樣,只是它的聚類結果容許存在對象屬於多個簇,也就是說:它屬於咱們前面介紹過的可重疊聚類算法。爲了深刻理解模糊 K 均值和 K 均值的區別,這裏咱們得花些時間瞭解一個概念:模糊參數(Fuzziness Factor)。 與 K 均值聚類原理相似,模糊 K 均值也是在待聚類對象向量集合上循環,可是它並非將向量分配給距離最近的簇,而是計算向量與各個簇的相關性(Association)。假設有一個向量 v,有 k 個簇,v 到 k 個簇中心的距離分別是 d1,d2… dk,那麼 V 到第一個簇的相關性 u1能夠經過下面的算式計算:
計算 v 到其餘簇的相關性只需將 d1替換爲對應的距離。 從上面的算式,咱們看出,當 m 近似 2 時,相關性近似 1;當 m 近似 1 時,相關性近似於到該簇的距離,因此 m 的取值在(1,2)區間內,當 m 越大,模糊程度越大,m 就是咱們剛剛提到的模糊參數。 講了這麼多理論的原理,下面咱們看看如何使用 Mahout 實現模糊 K 均值聚類,同前面的方法同樣,Mahout 同樣提供了基於內存和基於 Hadoop Map/Reduce 的兩種實現 FuzzyKMeansClusterer 和 FuzzyMeansDriver,分別是清單 5 給出了一個例子。
public static void fuzzyKMeansClusterInMemory() { // 指定聚類的個數 int k = 2; // 指定 K 均值聚類算法的最大迭代次數 int maxIter = 3; // 指定 K 均值聚類算法的最大距離閾值 double distanceThreshold = 0.01; // 指定模糊 K 均值聚類算法的模糊參數 float fuzzificationFactor = 10; // 聲明一個計算距離的方法,這裏選擇了歐幾里德距離 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 構建向量集,使用的是清單 1 裏的二維點集 List<Vector> pointVectors = SimpleDataSet.getPointVectors(SimpleDataSet.points); // 從點集向量中隨機的選擇 k 個做爲簇的中心 List<Vector> randomPoints = RandomSeedGenerator.chooseRandomPoints(points, k); // 構建初始簇,這裏與 K 均值不一樣,使用了 SoftCluster,表示簇是可重疊的 List<SoftCluster> clusters = new ArrayList<SoftCluster>(); int clusterId = 0; for (Vector v : randomPoints) { clusters.add(new SoftCluster(v, clusterId++, measure)); } // 調用 FuzzyKMeansClusterer 的 clusterPoints 方法進行模糊 K 均值聚類 List<List<SoftCluster>> finalClusters = FuzzyKMeansClusterer.clusterPoints(points, clusters, measure, distanceThreshold, maxIter, fuzzificationFactor); // 打印聚類結果 for(SoftCluster cluster : finalClusters.get(finalClusters.size() - 1)) { System.out.println("Fuzzy Cluster id: " + cluster.getId() + " center: " + cluster.getCenter().asFormatString()); } } public class fuzzyKMeansClusterUsingMapReduce { // 指定模糊 K 均值聚類算法的模糊參數 float fuzzificationFactor = 2.0f; // 指定須要聚類的個數,這裏選擇 2 類 int k = 2; // 指定最大迭代次數 int maxIter = 3; // 指定最大距離閾值 double distanceThreshold = 0.01; // 聲明一個計算距離的方法,這裏選擇了歐幾里德距離 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 設置輸入輸出的文件路徑 Path testpoints = new Path("testpoints"); Path output = new Path("output"); // 清空輸入輸出路徑下的數據 HadoopUtil.overwriteOutput(testpoints); HadoopUtil.overwriteOutput(output); // 將測試點集寫入輸入目錄下 SimpleDataSet.writePointsToFile(testpoints); // 隨機的選擇 k 個做爲簇的中心 Path clusters = RandomSeedGenerator.buildRandom(testpoints, new Path(output, "clusters-0"), k, measure); FuzzyKMeansDriver.runJob(testpoints, clusters, output, measure, 0.5, maxIter, 1, fuzzificationFactor, true, true, distanceThreshold, true); // 打印模糊 K 均值聚類的結果 ClusterDumper clusterDumper = new ClusterDumper(new Path(output, "clusters-" + maxIter ),new Path(output, "clusteredPoints")); clusterDumper.printClusters(null); } 執行結果 Fuzzy KMeans Clustering In Memory Result Fuzzy Cluster id: 0 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0], \"values\":[1.9750483367699223,1.993870669568863,0.0],\"state\":[1,1,0], \"freeEntries\":1,\"distinct\":2,\"lowWaterMark\":0,\"highWaterMark\":1, \"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2,\"lengthSquared\":-1.0}"} Fuzzy Cluster id: 1 center:{"class":"org.apache.mahout.math.RandomAccessSparseVector", "vector":"{\"values\":{\"table\":[0,1,0], \"values\":[7.924827516566109,7.982356511917616,0.0],\"state\":[1,1,0], \"freeEntries\":1, \"distinct\":2,\"lowWaterMark\":0,\"highWaterMark\":1, \"minLoadFactor\":0.2,\"maxLoadFactor\":0.5},\"size\":2,\"lengthSquared\":-1.0}"} Funzy KMeans Clustering Using Map Reduce Result Weight: Point: 0.9999249428064162: [8.000, 8.000] 0.9855340718746096: [9.000, 8.000] 0.9869963781734195: [8.000, 9.000] 0.9765978701133124: [9.000, 9.000] 0.6280999013864511: [5.000, 6.000] 0.7826097471578298: [6.000, 6.000] Weight: Point: 0.9672607354172386: [1.000, 1.000] 0.9794914088151625: [2.000, 1.000] 0.9803932521191389: [1.000, 2.000] 0.9977806183197744: [2.000, 2.000] 0.9793701109946826: [3.000, 3.000] 0.5422929338028506: [5.000, 5.000]
前面介紹的三種聚類算法都是基於劃分的,下面咱們簡要介紹一個基於機率分佈模型的聚類算法,狄利克雷聚類(Dirichlet Processes Clustering)。 首先咱們先簡要介紹一下基於機率分佈模型的聚類算法(後面簡稱基於模型的聚類算法)的原理:首先須要定義一個分佈模型,簡單的例如:圓形,三角形等,複雜的例如正則分佈,泊松分佈等;而後按照模型對數據進行分類,將不一樣的對象加入一個模型,模型會增加或者收縮;每一輪事後須要對模型的各個參數進行從新計算,同時估計對象屬於這個模型的機率。因此說,基於模型的聚類算法的核心是定義模型,對於一個聚類問題,模型定義的優劣直接影響了聚類的結果,下面給出一個簡單的例子,假設咱們的問題是將一些二維的點分紅三組,在圖中用不一樣的顏色表示,圖 A 是採用圓形模型的聚類結果,圖 B 是採用三角形模型的聚類結果。能夠看出,圓形模型是一個正確的選擇,而三角形模型的結果既有遺漏又有誤判,是一個錯誤的選擇。
Mahout 實現的狄利克雷聚類算法是按照以下過程工做的:首先,咱們有一組待聚類的對象和一個分佈模型。在 Mahout 中使用 ModelDistribution 生成各類模型。初始狀態,咱們有一個空的模型,而後嘗試將對象加入模型中,而後一步一步計算各個對象屬於各個模型的機率。下面清單給出了基於內存實現的狄利克雷聚類算法。
public static void DirichletProcessesClusterInMemory() { // 指定狄利克雷算法的 alpha 參數,它是一個過渡參數,使得對象分佈在不一樣模型先後能進行光滑的過渡 double alphaValue = 1.0; // 指定聚類模型的個數 int numModels = 3; // 指定 thin 和 burn 間隔參數,它們是用於下降聚類過程當中的內存使用量的 int thinIntervals = 2; int burnIntervals = 2; // 指定最大迭代次數 int maxIter = 3; List<VectorWritable> pointVectors = SimpleDataSet.getPoints(SimpleDataSet.points); // 初始階段生成空分佈模型,這裏用的是 NormalModelDistribution ModelDistribution<VectorWritable> model = new NormalModelDistribution(new VectorWritable(new DenseVector(2))); // 執行聚類 DirichletClusterer dc = new DirichletClusterer(pointVectors, model, alphaValue, numModels, thinIntervals, burnIntervals); List<Cluster[]> result = dc.cluster(maxIter); // 打印聚類結果 for(Cluster cluster : result.get(result.size() -1)){ System.out.println("Cluster id: " + cluster.getId() + " center: " + cluster.getCenter().asFormatString()); System.out.println(" Points: " + cluster.getNumPoints()); } } 執行結果 Dirichlet Processes Clustering In Memory Result Cluster id: 0 center:{"class":"org.apache.mahout.math.DenseVector", "vector":"{\"values\":[5.2727272727272725,5.2727272727272725], \"size\":2,\"lengthSquared\":-1.0}"} Points: 11 Cluster id: 1 center:{"class":"org.apache.mahout.math.DenseVector", "vector":"{\"values\":[1.0,2.0],\"size\":2,\"lengthSquared\":-1.0}"} Points: 1 Cluster id: 2 center:{"class":"org.apache.mahout.math.DenseVector", "vector":"{\"values\":[9.0,8.0],\"size\":2,\"lengthSquared\":-1.0}"} Points: 0
Mahout 中提供多種機率分佈模型的實現,他們都繼承 ModelDistribution,如圖 4 所示,用戶能夠根據本身的數據集的特徵選擇合適的模型,詳細的介紹請參考 Mahout 的官方文檔。
前面詳細介紹了 Mahout 提供的四種聚類算法,這裏作一個簡要的總結,分析各個算法優缺點,其實,除了這四種之外,Mahout 還提供了一些比較複雜的聚類算法,這裏就不一一詳細介紹了,詳細信息請參考 Mahout Wiki 上給出的聚類算法詳細介紹。
算法 | 內存實現 | Map/Reduce 實現 | 簇個數是肯定的 | 簇是否容許重疊 |
---|---|---|---|---|
K 均值 | KMeansClusterer | KMeansDriver | Y | N |
Canopy | CanopyClusterer | CanopyDriver | N | N |
模糊 K 均值 | FuzzyKMeansClusterer | FuzzyKMeansDriver | Y | Y |
狄利克雷 | DirichletClusterer | DirichletDriver | N | Y |