探索推薦引擎內部的祕密,第 3 部分: 深刻推薦引擎相關算法 - 聚類

智能推薦大都基於海量數據的計算和處理,然而咱們發如今海量數據上高效的運行協同過濾算法以及其餘推薦策略這樣高複雜的算法是有很大的挑戰的,在面對解決這個問題的過程當中,你們提出了不少減小計算量的方法,而聚類無疑是其中最優的選擇之一。 聚類 (Clustering) 是一個數據挖掘的經典問題,它的目的是將數據分爲多個簇 (Cluster),在同一個簇中的對象之間有較高的類似度,而不一樣簇的對象差異較大。聚類被普遍的應用於數據處理和統計分析領域。Apache Mahout 是 ASF(Apache Software Foundation) 的一個較新的開源項目,它源於 Lucene,構建在 Hadoop 之上,關注海量數據上的機器學習經典算法的高效實現。本文主要介紹如何基於 Apache Mahout 實現高效的聚類算法,從而實現更高效的數據處理和分析的應用。html

聚類分析

什麼是聚類分析?

聚類 (Clustering) 就是將數據對象分組成爲多個類或者簇 (Cluster),它的目標是:在同一個簇中的對象之間具備較高的類似度,而不一樣簇中的對象差異較大。因此,在不少應用中,一個簇中的數據對象能夠被做爲一個總體來對待,從而減小計算量或者提升計算質量。 其實聚類是一我的們平常生活的常見行爲,即所謂「物以類聚,人以羣分」,核心的思想也就是聚類。人們老是不斷地改進下意識中的聚類模式來學習如何區分各個事物和人。同時,聚類分析已經普遍的應用在許多應用中,包括模式識別,數據分析,圖像處理以及市場研究。經過聚類,人們能意識到密集和稀疏的區域,發現全局的分佈模式,以及數據屬性之間的有趣的相互關係。 聚類同時也在 Web 應用中起到愈來愈重要的做用。最被普遍使用的既是對 Web 上的文檔進行分類,組織信息的發佈,給用戶一個有效分類的內容瀏覽系統(門戶網站),同時能夠加入時間因素,進而發現各個類內容的信息發展,最近被你們關注的主題和話題,或者分析一段時間內人們對什麼樣的內容比較感興趣,這些有趣的應用都得創建在聚類的基礎之上。做爲一個數據挖掘的功能,聚類分析能做爲獨立的工具來得到數據分佈的狀況,觀察每一個簇的特色,集中對特定的某些簇作進一步的分析,此外,聚類分析還能夠做爲其餘算法的預處理步驟,簡化計算量,提升分析效率,這也是咱們在這裏介紹聚類分析的目的。web

不一樣的聚類問題

對於一個聚類問題,要挑選最適合最高效的算法必須對要解決的聚類問題自己進行剖析,下面咱們就從幾個側面分析一下聚類問題的需求。 聚類結果是排他的仍是可重疊的 爲了很好理解這個問題,咱們以一個例子進行分析,假設你的聚類問題須要獲得二個簇:「喜歡詹姆斯卡梅隆電影的用戶」和「不喜歡詹姆斯卡梅隆的用戶」,這實際上是一個排他的聚類問題,對於一個用戶,他要麼屬於「喜歡」的簇,要麼屬於不喜歡的簇。但若是你的聚類問題是「喜歡詹姆斯卡梅隆電影的用戶」和「喜歡里奧納多電影的用戶」,那麼這個聚類問題就是一個可重疊的問題,一個用戶他能夠既喜歡詹姆斯卡梅隆又喜歡里奧納多。 因此這個問題的核心是,對於一個元素,他是否能夠屬於聚類結果中的多個簇中,若是是,則是一個可重疊的聚類問題,若是否,那麼是一個排他的聚類問題。 基於層次仍是基於劃分 其實大部分人想到的聚類問題都是「劃分」問題,就是拿到一組對象,按照必定的原則將它們分紅不一樣的組,這是典型的劃分聚類問題。但除了基於劃分的聚類,還有一種在平常生活中也很常見的類型,就是基於層次的聚類問題,它的聚類結果是將這些對象分等級,在頂層將對象進行大體的分組,隨後每一組再被進一步的細分,也許全部路徑最終都要到達一個單獨實例,這是一種「自頂向下」的層次聚類解決方法,對應的,也有「自底向上」的。其實能夠簡單的理解,「自頂向下」就是一步步的細化分組,而「自底向上」就是一步步的歸併分組。 簇數目固定的仍是無限制的聚類 這個屬性很好理解,就是你的聚類問題是在執行聚類算法前已經肯定聚類的結果應該獲得多少簇,仍是根據數據自己的特徵,由聚類算法選擇合適的簇的數目。 基於距離仍是基於機率分佈模型 在本系列的第二篇介紹協同過濾的文章中,咱們已經詳細介紹了類似性和距離的概念。基於距離的聚類問題應該很好理解,就是將距離近的類似的對象聚在一塊兒。相比起來,基於機率分佈模型的,可能不太好理解,那麼下面給個簡單的例子。 一個機率分佈模型能夠理解是在 N 維空間的一組點的分佈,而它們的分佈每每符合必定的特徵,好比組成一個特定的形狀。基於機率分佈模型的聚類問題,就是在一組對象中,找到能符合特定分佈模型的點的集合,他們不必定是距離最近的或者最類似的,而是能完美的呈現出機率分佈模型所描述的模型。 下面圖 1 給出了一個例子,對一樣一組點集,應用不一樣的聚類策略,獲得徹底不一樣的聚類結果。左側給出的結果是基於距離的,核心的原則就是將距離近的點聚在一塊兒,右側給出的基於機率分佈模型的聚類結果,這裏採用的機率分佈模型是必定弧度的橢圓。圖中專門標出了兩個紅色的點,這兩點的距離很近,在基於距離的聚類中,將他們聚在一個類中,但基於機率分佈模型的聚類則將它們分在不一樣的類中,只是爲了知足特定的機率分佈模型(固然這裏我特地舉了一個比較極端的例子)。因此咱們能夠看出,在基於機率分佈模型的聚類方法裏,核心是模型的定義,不一樣的模型可能致使徹底不一樣的聚類結果。算法

圖 1 基於距離和基於機率分佈模型的聚類問題

圖 1 基於距離和基於機率分佈模型的聚類問題  

Apache Mahout 中的聚類分析框架

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 提供了多個實現:數組

  1. DenseVector,它的實現就是一個浮點數數組,對向量裏全部域都進行存儲,適合用於存儲密集向量。
  2. RandomAccessSparseVector 基於浮點數的 HashMap 實現的,key 是整形 (int) 類型,value 是浮點數 (double) 類型,它只存儲向量中不爲空的值,並提供隨機訪問。
  3. SequentialAccessVector 實現爲整形 (int) 類型和浮點數 (double) 類型的並行數組,它也只存儲向量中不爲空的值,但只提供順序訪問。

用戶能夠根據本身算法的需求選擇合適的向量實現類,若是算法須要不少隨機訪問,應該選擇 DenseVector 或者 RandomAccessSparseVector,若是大部分都是順序訪問,SequentialAccessVector 的效果應該更好。 介紹了向量的實現,下面咱們看看如何將現有的數據建模成向量,術語就是「如何對數據進行向量化」,以便採用 Mahout 的各類高效的聚類算法。架構

  1. 簡單的整形或浮點型的數據這種數據最簡單,只要將不一樣的域存在向量中便可,好比 n 維空間的點,其實自己能夠被描述爲一個向量。
  2. 枚舉類型數據這類數據是對物體的描述,只是取值範圍有限。舉個例子,假設你有一個蘋果信息的數據集,每一個蘋果的數據包括:大小,重量,顏色等,咱們以顏色爲例,設蘋果的顏色數據包括:紅色,黃色和綠色。在對數據進行建模時,咱們能夠用數字來表示顏色,紅色 =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<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; 
     }
  3. 文本信息做爲聚類算法的主要應用場景 - 文本分類,對文本信息的建模也是一個常見的問題。在信息檢索研究領域已經有很好的建模方式,就是信息檢索領域中最經常使用的向量空間模型 (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(); 
    	 } 
     }
    這裏補充一點,生成的向量化文件的目錄結構是這樣的:
    圖 2 文本信息向量化
    圖 2 文本信息向量化
    • df-count 目錄:保存着文本的頻率信息
    • tf-vectors 目錄:保存着以 TF 做爲權值的文本向量
    • tfidf-vectors 目錄:保存着以 TFIDF 做爲權值的文本向量
    • tokenized-documents 目錄:保存着分詞事後的文本信息
    • wordcount 目錄:保存着全局的詞彙出現的次數
    • dictionary.file-0 目錄:保存着這些文本的詞彙表
    • frequcency-file-0 目錄 : 保存着詞彙表對應的頻率信息。

介紹完向量化問題,下面咱們深刻分析各個聚類算法,首先介紹的是最經典的 K 均值算法。app

K 均值聚類算法

K 均值是典型的基於距離的排他的劃分方法:給定一個 n 個對象的數據集,它能夠構建數據的 k 個劃分,每一個劃分就是一個聚類,而且 k<=n,同時還須要知足兩個要求:框架

  • 每一個組至少包含一個對象
  • 每一個對象必須屬於且僅屬於一個組。

K 均值的基本原理是這樣的,給定 k,即要構建的劃分的數目,dom

  1. 首先建立一個初始劃分,隨機地選擇 k 個對象,每一個對象初始地表明瞭一個簇中心。對於其餘的對象,根據其與各個簇中心的距離,將它們賦給最近的簇。
  2. 而後採用一種迭代的重定位技術,嘗試經過對象在劃分間移動來改進劃分。所謂重定位技術,就是當有新的對象加入簇或者已有對象離開簇的時候,從新計算簇的平均值,而後對對象進行從新分配。這個過程不斷重複,直到沒有簇中對象的變化。

當結果簇是密集的,並且簇和簇之間的區別比較明顯時,K 均值的效果比較好。對於處理大數據集,這個算法是相對可伸縮的和高效的,它的複雜度是 O(nkt),n 是對象的個數,k 是簇的數目,t 是迭代的次數,一般 k<<n,且 t<<n,因此算法常常以局部最優結束。 K 均值的最大問題是要求用戶必須事先給出 k 的個數,k 的選擇通常都基於一些經驗值和屢次實驗結果,對於不一樣的數據集,k 的取值沒有可借鑑性。另外,K 均值對「噪音」和孤立點數據是敏感的,少許這類的數據就能對平均值形成極大的影響。 說了這麼多理論的原理,下面咱們基於 Mahout 實現一個簡單的 K 均值算法的例子。如前面介紹的,Mahout 提供了基本的基於內存的實現和基於 Hadoop 的 Map/Reduce 的實現,分別是 KMeansClusterer 和 KMeansDriver,下面給出一個簡單的例子,就基於咱們在清單 1 裏定義的二維點集數據。

清單 3. K 均值聚類算法示例
 // 基於內存的 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 中的點,將他們分配與最合適的簇中。Canopy 聚類算法常常用於 K 均值聚類算法的預處理,用來找合適的 k 值和簇中心。 下面詳細介紹一下建立 Canopy 的過程:初始,假設咱們有一組點集 S,而且預設了兩個距離閾值,T1,T2(T1>T2);而後選擇一個點,計算它與 S 中其餘點的距離(這裏採用成本很低的計算方法),將距離在 T1 之內的放入一個 Canopy 中,同時從 S 中去掉那些與此點距離在 T2 之內的點(這裏是爲了保證和中心距離在 T2 之內的點不能再做爲其餘 Canopy 的中心),重複整個過程直到 S 爲空爲止。 對 K 均值的實現同樣,Mahout 也提供了兩個 Canopy 聚類的實現,下面咱們就看看具體的代碼例子。

清單 4. 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 均值和 K 均值的區別,這裏咱們得花些時間瞭解一個概念:模糊參數(Fuzziness Factor)。 與 K 均值聚類原理相似,模糊 K 均值也是在待聚類對象向量集合上循環,可是它並非將向量分配給距離最近的簇,而是計算向量與各個簇的相關性(Association)。假設有一個向量 v,有 k 個簇,v 到 k 個簇中心的距離分別是 d1,d2… dk,那麼 V 到第一個簇的相關性 u1能夠經過下面的算式計算:

Figure xxx. Requires a heading

計算 v 到其餘簇的相關性只需將 d1替換爲對應的距離。 從上面的算式,咱們看出,當 m 近似 2 時,相關性近似 1;當 m 近似 1 時,相關性近似於到該簇的距離,因此 m 的取值在(1,2)區間內,當 m 越大,模糊程度越大,m 就是咱們剛剛提到的模糊參數。 講了這麼多理論的原理,下面咱們看看如何使用 Mahout 實現模糊 K 均值聚類,同前面的方法同樣,Mahout 同樣提供了基於內存和基於 Hadoop Map/Reduce 的兩種實現 FuzzyKMeansClusterer 和 FuzzyMeansDriver,分別是清單 5 給出了一個例子。

清單 5. 模糊 K 均值聚類算法示例
 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 是採用三角形模型的聚類結果。能夠看出,圓形模型是一個正確的選擇,而三角形模型的結果既有遺漏又有誤判,是一個錯誤的選擇。

圖 3 採用不一樣模型的聚類結果

圖 3 採用不一樣模型的聚類結果 Mahout 實現的狄利克雷聚類算法是按照以下過程工做的:首先,咱們有一組待聚類的對象和一個分佈模型。在 Mahout 中使用 ModelDistribution 生成各類模型。初始狀態,咱們有一個空的模型,而後嘗試將對象加入模型中,而後一步一步計算各個對象屬於各個模型的機率。下面清單給出了基於內存實現的狄利克雷聚類算法。

清單 6. 狄利克雷聚類算法示例
 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 的官方文檔。

圖 4 Mahout 中的機率分佈模型層次結構

圖 4 Mahout 中的機率分佈模型層次結構

Mahout 聚類算法總結

前面詳細介紹了 Mahout 提供的四種聚類算法,這裏作一個簡要的總結,分析各個算法優缺點,其實,除了這四種之外,Mahout 還提供了一些比較複雜的聚類算法,這裏就不一一詳細介紹了,詳細信息請參考 Mahout Wiki 上給出的聚類算法詳細介紹。

表 1 Mahout 聚類算法總結
算法 內存實現 Map/Reduce 實現 簇個數是肯定的 簇是否容許重疊
K 均值 KMeansClusterer KMeansDriver Y N
Canopy CanopyClusterer CanopyDriver N N
模糊 K 均值 FuzzyKMeansClusterer FuzzyKMeansDriver Y Y
狄利克雷 DirichletClusterer DirichletDriver N Y
 

總結

聚類算法被普遍的運用於信息智能處理系統。本文首先簡述了聚類概念與聚類算法思想,使得讀者總體上了解聚類這一重要的技術。而後從實際構建應用的角度出發,深刻的介紹了開源軟件 Apache Mahout 中關於聚類的實現框架,包括了其中的數學模型,各類聚類算法以及在不一樣基礎架構上的實現。經過代碼示例,讀者能夠知道針對他的特定的數據問題,怎麼樣向量化數據,怎麼樣選擇各類不一樣的聚類算法。 本系列的下一篇將繼續深刻了解推薦引擎的相關算法 -- 分類。與聚類同樣,分類也是一個數據挖掘的經典問題,主要用於提取描述重要數據類的模型,隨後咱們能夠根據這個模型進行預測,推薦就是一種預測的行爲。同時聚類和分類每每也是相輔相成的,他們都爲在海量數據上進行高效的推薦提供輔助。因此本系列的下一篇文章將詳細介紹各種分類算法,它們的原理,優缺點和實用場景,並給出基於 Apache Mahout 的分類算法的高效實現。 最後,感謝你們對本系列的關注和支持。  
 
相關文章
相關標籤/搜索