1. 程式人生 > >聚類系列-層次聚類(Hierarchical Clustering)

聚類系列-層次聚類(Hierarchical Clustering)

    上篇k-means演算法卻是一種方便好用的聚類演算法,但是始終有K值選擇和初始聚類中心點選擇的問題,而這些問題也會影響聚類的效果。為了避免這些問題,我們可以選擇另外一種比較實用的聚類演算法-層次聚類演算法。顧名思義,層次聚類就是一層一層的進行聚類,可以由上向下把大的類別(cluster)分割,叫作分裂法;也可以由下向上對小的類別進行聚合,叫作凝聚法;但是一般用的比較多的是由下向上的凝聚方法。本文會對分裂法略作簡介和舉個例子說明,重點介紹凝聚法,並且附上程式碼。

    分裂法:

分裂法指的是初始時將所有的樣本歸為一個類簇,然後依據某種準則進行逐漸的分裂,直到達到某種條件或者達到設定的分類數目。用演算法描述:

    輸入:樣本集合D,聚類數目或者某個條件(一般是樣本距離的閾值,這樣就可不設定聚類數目

    輸出:聚類結果

    1.將樣本集中的所有的樣本歸為一個類簇;

    repeat:

        2.在同一個類簇(計為c)中計算兩兩樣本之間的距離,找出距離最遠的兩個樣本a,b;

        3.將樣本a,b分配到不同的類簇c1和c2中;

        4.計算原類簇(c)中剩餘的其他樣本點和a,b的距離,若是dis(a)<dis(b),則將樣本點歸到c1中,否則歸到c2中;

    util: 達到聚類的數目或者達到設定的條件

    舉個例子來說明問題:

    在平面上有6個點:p0(1,1), p1(1,2), p2(2,2), p3(4,4), p4(4,5), p5(5,6),我現在需要對這6個點進行聚類,對應著上邊的步驟我可以這樣做:

    1.將所有的點歸為一個類簇c(p0,p1,p2,p3,p4,p5)

    repeat:

        2.在類簇c中計算他們的距離(簡單的歐式距離)我們可以得到:

dis p0 p1 p2 p3 p4 p5
po 0 1 sqrt(2) sqrt(18) 5 sqrt(41)
p1 1 0 1 sqrt(13) sqrt(18) sqrt(32)
p2 sqrt(2) 1 0 sqrt(8) sqrt(13) 5
p3 sqrt(18) sqrt(13) sqrt(8) 0 1 sqrt(5)
p4 5 sqrt(18) sqrt(13) 1 0 sqrt(2)
p5 sqrt(41) sqrt(32) 5 sqrt(5) sqrt(2) 0
           由上邊的表格可以看出距離最遠的兩個點為p0和p5

        3.將p0分配到類簇c1,將p5分配到類簇c2;

        4.查表可以看出,剩餘的點中p1和p2與p0的距離小,所以將它們兩個歸到類簇c1中;p3和p4與p5的距離小,所以將它們兩個歸到類簇c2中。這樣我們得到了一次新的聚類 結果c1=(p1,p2,p3),c2=(p3,p4,p5);

        util: 若是我要求就聚類成兩個,則這個聚類到此結束,最終我們的聚類 結果是(p1,p2,p3)和(p3,p4,p5)。若是我要求同一個類中,最大樣本距離不大於sqrt(2),那麼上述的分類結果沒有到達要求,則需要返回到repeat處繼續聚類,因為c1中的樣本的距離都不大於sqrt(2),所以不需要再分了;而類簇c2中的dis(p3,p5)=sqrt(5)>sqrt(2),還需要繼續分,c2最後分聚類成兩個類(p3,p4)和(p5),這樣我們最終得到了三個類簇(p1,p2,p3)、(p3,p4)和(P5)

-----------------------------------------------------------------------------------------------------------------------------------------------------

簡單的介紹完分裂法以後,我們開始我們的重頭戲,凝聚層次聚類的介紹

      凝聚法:

凝聚法指的是初始時將每個樣本點當做一個類簇,所以原始類簇的大小等於樣本點的個數,然後依據某種準則合併這些初始的類簇,直到達到某種條件或者達到設定的分類數目。用演算法描述:

       輸入:樣本集合D,聚類數目或者某個條件(一般是樣本距離的閾值,這樣就可不設定聚類數目

       輸出:聚類結果

      1.將樣本集中的所有的樣本點都當做一個獨立的類簇;

       repeat:

            2.計算兩兩類簇之間的距離(後邊會做介紹),找到距離最小的兩個類簇c1和c2;

            3.合併類簇c1和c2為一個類簇;

       util: 達到聚類的數目或者達到設定的條件

       還是拿分裂法的例子來說具體的講解凝聚法。

      1.首先經所有的樣本看作是一個類簇,這樣我可以得到初始的類簇有6個,分別為c1(p0),c2(p1),c3(p2),c4(p3),c5(p4),c6(p5)

       repeat:

            2.由上邊的表可以得到兩兩類簇間的最小距離(並不是唯一,其他兩個類簇間距離也可能等於最小值,但是先選取一個)是1,存在類簇c1和c2之間

            注意:這個類簇間距離的計算方法有許多種。

(1).就是取兩個類中距離最近的兩個樣本的距離作為這兩個集合的距離,也就是說,最近兩個樣本之間的距離越小,這兩個類之間的相似度就越大

(2).取兩個集合中距離最遠的兩個點的距離作為兩個集合的距離

(3).把兩個集合中的點兩兩的距離全部放在一起求一個平均值,相對也能得到合適一點的結果。

(4).取兩兩距離的中值,與取均值相比更加能夠解除個別偏離樣本對結果的干擾。

            (5).把兩個集合中的點兩兩的距離全部放在一起求和然後除以兩個集合中的元素個數

            (6).求每個集合的中心點(就是將集合中的所有元素的對應維度相加然後再除以元素個數得到的一個向量),然後用中心點代替集合再去就集合間的距離

              前四種,在點選開啟連結中有介紹,後邊的兩種在工業界也經常使用,當然,還會有其他的一些方法。

            3.合併類簇c1和c2,得到新的聚類結果c1(p0,p1),c3(p2),c4(p3),c5(p4),c6(p5)。

       util:若是我們要求聚成5個類別的話,我們這裡就可以結束了。但是如果我們設定了一個閾值f,要求若存在距離小於閾值f的兩個類簇時則將兩個類簇合並並且繼續迭代,我們又會回到repeat繼續迭代從而得到新的聚類結果。

      好了,多了不說了,開始上程式碼,程式碼中我都寫好了註釋

      Cluster.java(用於存放類名和該類下的樣本)

import java.util.ArrayList;
import java.util.List;

public class Cluster {
	private List<DataPoint> dataPoints = new ArrayList<DataPoint>(); // 類簇中的樣本點
	private String clusterName;

	public List<DataPoint> getDataPoints() {
		return dataPoints;
	}

	public void setDataPoints(List<DataPoint> dataPoints) {
		this.dataPoints = dataPoints;
	}

	public String getClusterName() {
		return clusterName;
	}

	public void setClusterName(String clusterName) {
		this.clusterName = clusterName;
	}
}

    DataPoint.java(存放樣本點,包括樣本點的名字、所屬的類簇、向量空間的表示)
public class DataPoint {
	String dataPointName; // 樣本點名
    Cluster cluster; // 樣本點所屬類簇
    private double dimensioin[]; // 樣本點的維度

    public DataPoint(){

    }

    public DataPoint(double[] dimensioin,String dataPointName){
         this.dataPointName=dataPointName;
         this.dimensioin=dimensioin;
    }

    public double[] getDimensioin() {
        return dimensioin;
    }

    public void setDimensioin(double[] dimensioin) {
        this.dimensioin = dimensioin;
    }

    public Cluster getCluster() {
        return cluster;
    }

    public void setCluster(Cluster cluster) {
        this.cluster = cluster;
    }

    public String getDataPointName() {
        return dataPointName;
    }

	public void setDataPointName(String dataPointName) {
		this.dataPointName = dataPointName;
	}
}

    HCluster.java(主函式,包括資料的讀取整理,聚類的執行等等。。。)
/**
 * 我寫這個程式的目的是對我的次有進行聚類
 */
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import Cluster;
import DataPoint;

public class HCluster {

	public static void main(String[] args) {

		HCluster hc = new HCluster();

		// 使用連結串列存放樣本點
		ArrayList<DataPoint> dp = new ArrayList<DataPoint>();

		// 讀入樣本檔案
		dp = hc.readData("E:/test_cc/vec.txt");

		/*
		 * freq代表了聚類的終止條件,判斷還有沒有距離小於freq的兩個類簇,若有則合併後繼續迭代,否則終止迭代
		 */
		double freq = 0.5;
		List<Cluster> clusters = hc.startCluster(dp, freq);

		// 輸出聚類的結果,兩個類簇中間使用----隔開
		System.out.println();
		System.out.println("結果輸出---:");
		for (Cluster cl : clusters) {
			List<DataPoint> tempDps = cl.getDataPoints();
			for (DataPoint tempdp : tempDps) {
				System.out.println(tempdp.getDataPointName());
			}
			System.out.println("----");
		}
	}

	// 聚類的主方法
	private List<Cluster> startCluster(ArrayList<DataPoint> dp, double freq) {

		// 宣告cluster類,存放類名和類簇中含有的樣本
		List<Cluster> finalClusters = new ArrayList<Cluster>();
		// 初始化類簇,開始時認為每一個樣本都是一個類簇並將初始化類簇賦值給最終類簇
		List<Cluster> originalClusters = initialCluster(dp);
		finalClusters = originalClusters;
		// flag為判斷標誌
		boolean flag = true;
		int it = 1;
		while (flag) {
			System.out.println("第" + it + "次迭代");
			// 臨時表量,存放類簇間餘弦相似度的最大值
			double max = -1;
			// mergeIndexA和mergeIndexB表示每一次迭代聚類最小的兩個類簇,也就是每一次迭代要合併的兩個類簇
			int mergeIndexA = 0;
			int mergeIndexB = 0;
			/*
			 * 迭代開始,分別去計算每個類簇之間的距離,將距離小的類簇合並
			 */
			for (int i = 0; i < finalClusters.size() - 1; i++) {
				for (int j = i + 1; j < finalClusters.size(); j++) {
					// 得到任意的兩個類簇
					Cluster clusterA = finalClusters.get(i);
					Cluster clusterB = finalClusters.get(j);
					// 得到這兩個類簇中的樣本
					List<DataPoint> dataPointsA = clusterA.getDataPoints();
					List<DataPoint> dataPointsB = clusterB.getDataPoints();
					/*
					 * 定義臨時變數tempDis儲存兩個類簇的大小,這裡採用的計算兩個類簇的距離的方法是
					 * 得到兩個類簇中所有的樣本的距離的和除以兩個類簇中的樣本數量的積,其中兩個樣本 之間的距離用的是餘弦相似度。
					 * 注意:這個地方的類簇之間的距離可以 換成其他的計算方法
					 */
					double tempDis = 0;
					/*
					 * 此處計算距離可以優化,事先一次性將兩兩樣本點之間的餘弦距離計算好存放一個MAP中,
					 * 這個地方使用的時候直接取出來,就不用每次再去計算了,可節省很多時間。
					 * 注意:若是類簇間的距離計算換成了別的方法,也就沒有這種優化的說法了
					 */
					for (int m = 0; m < dataPointsA.size(); m++) {
						for (int n = 0; n < dataPointsB.size(); n++) {
							tempDis = tempDis + getDistance(dataPointsA.get(m), dataPointsB.get(n));
						}
					}
					tempDis = tempDis / (dataPointsA.size() * dataPointsB.size());

					if (tempDis >= max) {
						max = tempDis;
						mergeIndexA = i;
						mergeIndexB = j;
					}
				}
			}
			/*
			 * 若是餘弦相似度的最大值都小於給定的閾值, 那說明當前的類簇沒有再進一步合併的必要了,
			 * 當前的聚類可以作為結果了,否則的話合併餘弦相似度值最大的兩個類簇,繼續進行迭代 注意:這個地方你可以設定別的聚類迭代的結束條件
			 */
			if (max < freq) {
				flag = false;
			} else {
				finalClusters = mergeCluster(finalClusters, mergeIndexA, mergeIndexB);
			}
			it++;
		}
		return finalClusters;
	}

	private List<Cluster> mergeCluster(List<Cluster> finalClusters, int mergeIndexA, int mergeIndexB) {
		if (mergeIndexA != mergeIndexB) {
			// 將cluster[mergeIndexB]中的DataPoint加入到 cluster[mergeIndexA]
			Cluster clusterA = finalClusters.get(mergeIndexA);
			Cluster clusterB = finalClusters.get(mergeIndexB);

			List<DataPoint> dpA = clusterA.getDataPoints();
			List<DataPoint> dpB = clusterB.getDataPoints();

			for (DataPoint dp : dpB) {
				DataPoint tempDp = new DataPoint();
				tempDp.setDataPointName(dp.getDataPointName());
				tempDp.setDimensioin(dp.getDimensioin());
				tempDp.setCluster(clusterA);
				dpA.add(tempDp);
			}
			clusterA.setDataPoints(dpA);
			finalClusters.remove(mergeIndexB);
		}
		return finalClusters;
	}

	private double getDistance(DataPoint dataPoint, DataPoint dataPoint2) {
		double distance = 0;
		double[] dimA = dataPoint.getDimensioin();
		double[] dimB = dataPoint2.getDimensioin();
		if (dimA.length == dimB.length) {
			double mdimA = 0;// dimA的莫
			double mdimB = 0;// dimB的莫
			double proAB = 0;// dimA和dimB的向量積
			for (int i = 0; i < dimA.length; i++) {
				proAB = proAB + dimA[i] * dimB[i];
				mdimA = mdimA + dimA[i] * dimA[i];
				mdimB = mdimB + dimB[i] * dimB[i];
			}
			distance = proAB / (Math.sqrt(mdimA) * Math.sqrt(mdimB));
		}
		return distance;
	}

	// 初始化類簇
	private List<Cluster> initialCluster(ArrayList<DataPoint> dpoints) {
		// 宣告存放初始化類簇的連結串列
		List<Cluster> originalClusters = new ArrayList<Cluster>();

		for (int i = 0; i < dpoints.size(); i++) {
			// 得到每一個樣本點
			DataPoint tempDataPoint = dpoints.get(i);
			// 宣告一個臨時的用於存放樣本點的連結串列
			List<DataPoint> tempDataPoints = new ArrayList<DataPoint>();
			// 連結串列中加入剛才得到的樣本點
			tempDataPoints.add(tempDataPoint);
			// 宣告一個類簇,並且將給類簇設定名字、增加樣本點
			Cluster tempCluster = new Cluster();
			tempCluster.setClusterName("Cluster " + String.valueOf(i));
			tempCluster.setDataPoints(tempDataPoints);
			// 將樣本點的類簇設定為tempCluster
			tempDataPoint.setCluster(tempCluster);
			// 將新的類簇加入到初始化類簇連結串列中
			originalClusters.add(tempCluster);
		}

		return originalClusters;
	}

	private ArrayList<DataPoint> readData(String path) {

		ArrayList<DataPoint> dp = new ArrayList<DataPoint>();
		File file = new File(path);
		if (!file.exists()) {
			System.out.println("輸入檔案不存在");
			System.exit(1);
		}

		FileInputStream fis = null;
		InputStreamReader isr = null;
		BufferedReader br = null;

		try {
			fis = new FileInputStream(file);
			isr = new InputStreamReader(fis, "utf-8");
			br = new BufferedReader(isr);

			String line = br.readLine();
			String s[] = null;
			while (line != null) {
				/*
				 * 說明一下我的資料格式為 word v1 v2 ....v200,word代表要聚類的詞語,
				 * v1-v200是word的詞向量表示的每一個維度值,我實驗的樣本的維度為200,
				 * 所有下邊我聲明瞭一個200的double陣列
				 */
				s = line.split(" ");
				double[] b = new double[200];
				for (int i = 1; i < s.length; i++) {
					b[i - 1] = Double.parseDouble(s[i]);
				}
				dp.add(new DataPoint(b, s[0]));
				line = br.readLine();
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			try {
				br.close();
				isr.close();
				fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		System.out.println("載入資料完畢,資料大小為:" + dp.size());
		return dp;
	}
}
      來一張我自己做專案時的聚類的結果的截圖:

    好了,到此,層次聚類我就算講完了,希望大家批評指正。。。。。