天天看點

k均值聚類算法優缺點_聚類之K均值(K-means)算法

這個事起源于跟朋友一次聚會,他比較忙,一刻也不能停地處理工作。我就看他一直在看Excel表格,根據資料的特征不斷地做決定。我比較好奇就問什麼樣的資料要做什麼樣的處理,他說了一些名額,比如X名額、Y名額都高,就執行A方案;X名額高、Y名額低,就執行B方案。

我程式員思維就冒出來了,說這也不用你一條條地看吧,你把标準列出來,例如X、Y到多少算高,處理方案訂下來,寫個Python程式跑一遍Excel表格結果就出來了——這其實就是給資料分類打标簽。這就引出了本文想說的——聚類。

分類是指根據已經給出的正确的分類資料,通過其分類模型,對新資料進行分類。

而聚類,雖然也是将一堆資料分成幾“類”——在聚類中,這叫“簇”,但是相比分類有明确的定義,聚類則是将相似的資料“聚”成一“簇”,至于這一“簇”是什麼,我們不關心,我們隻關心如何計算相似度。

算法很多,這裡隻介紹K均值算法。算法很簡單,過程如下:

1、确定要聚成K個簇。

2、随機選擇K個對象,作為每個簇的質心。

3、周遊其他對象,計算其與每個質心的距離(距離算法後述),并将其劃分到最近質心的簇中。

4、計算每個簇中所有資料的平均值,将其設為新的質心。

5、重複3、4,直到新舊質心相等,我們視為其已經收斂。注意:不斷地重複3、4步驟,質心的位置一定會在某處收斂,這是經過數學證明了的。

6、輸出結果。

直接上代碼:

public class KMeans {    public static void main(String[] args) {        List nodes = KMeans.buildNodes(100);        int k = 3;        // 因為是随機選初始質心,資料也是随機生成,是以本例就直接選前K個作為質心。        List centres = Lists.newArrayListWithCapacity(k);        Map> centreMap = Maps.newHashMap();        for (int i = 0; i < k; i++) {            Node n = nodes.get(i);            centres.add(n);            centreMap.put(i, new ArrayList());        }        //        System.out.println("初始質心:");        //        System.out.println(centres.toString());        //        System.out.println("資料:");        //        System.out.println(nodes.toString());        //        System.out.println("==================");        KMeans kmeans = new KMeans();        kmeans.kmean(nodes, centres, centreMap, k);    }    public void kmean(List nodes, List centres, Map> centreMap, int k) {        boolean flag = true;        while (flag) {            // 周遊所有對象,計算其與所有質心的距離,并分簇            for (int i = 0; i < nodes.size(); i++) {                Node node = nodes.get(i);                int tmpDistance = Integer.MAX_VALUE;                int tmpCode = Integer.MAX_VALUE;                //                System.out.println("計算Node" + node.toString() + "到各質心的距離");                for (int j = 0; j < centres.size(); j++) {                    Node centre = centres.get(j);                    //                    if (KMeans.nodeEquals(centre, node)) {                    //                        continue;                    //                    }                    //質心就是節點,不再計算距離                    if (centre.equals(node)) {                        continue;                    }                    int distance = Distance.euclidean(node, centre).intValue();                    //                    System.out.println("臨時距離=" + tmpDistance);                    //                    System.out.println(node.toString() + "到質心" + centre.toString() + "的距離" + distance);                    if (distance < tmpDistance) {                        tmpDistance = distance;                        tmpCode = j;                    }                }                centreMap.get(tmpCode).add(node);                //                System.out.println("=============");            }            // 分簇完畢,根據每簇的節點計算新的質心。質心可以為虛拟的坐标點            List newCentres = Lists.newArrayListWithCapacity(k);            Map> newCentreMap = Maps.newHashMap();            for (Map.Entry> entry : centreMap.entrySet()) {                if (entry.getValue().size() == 1) {                    // 如果每簇隻有一個Node,則不再計算,它就是質心                    newCentres.add(entry.getValue().get(0));                    newCentreMap.put(entry.getKey(), new ArrayList());                } else {                    // 計算新質心                    Node newCentre = KMeans.getCentre(entry.getValue());                    //某簇沒有節點,也就沒有新質心                    if (Objects.isNull(newCentre)) {                        continue;                    }                    newCentres.add(newCentre);                    newCentreMap.put(entry.getKey(), new ArrayList());                }            }            // 新質心與原質心清單完全相同,代表已經收斂,最終結果已出,跳出循環            if (newCentres.containsAll(centres) && centres.containsAll(newCentres)) {                flag = false;            }            //            System.out.println("原質心:");            //            System.out.println(centres.toString());            //            System.out.println("計算結果:");            //            System.out.println(centreMap.toString());            //            System.out.println("新質心:");            //            System.out.println(newCentres.toString());            //            //            System.out.println("是否收斂:" + converge);            // KMeans.show(centres, centreMap);            //            System.out.println("-----------------");            if (!flag) {                System.out.println("最終結果:");                KMeans.show(newCentres, centreMap);            }            centres.clear();            centreMap.clear();            centres = newCentres;            centreMap = newCentreMap;        }    }    /**     * 列印結果     *      * @param centres   質心清單     * @param centreMap 已分簇的資料     */    public static void show(List centres, Map> centreMap) {        int i = 0;        for (Map.Entry> entry : centreMap.entrySet()) {            for (Node n : entry.getValue()) {                System.out.println("[" + n.getX() + ", " + n.getY() + "," + i + "],");            }            i++;        }        for (Node node : centres) {            System.out.println("[" + node.getX() + ", " + node.getY() + "," + i++ + "],");        }    }    /**     * 根據坐标判斷是否為同一節點     *      * @param n1     * @param n2     * @return     */    public static boolean nodeEquals(Node n1, Node n2) {        return n1.getX() == n2.getX() && n1.getY() == n2.getY();    }    /**     * 計算每簇Node的平均值,擷取質心     *      * @param nodes     * @return     */    public static Node getCentre(List nodes) {        // 分簇之後,很有可能所有資料都離某一個質心最近,導緻有些質心周圍沒有資料        if (nodes.isEmpty()) {            return null;        }        int x = 0, y = 0;        int size = nodes.size();        for (Node node : nodes) {            x = x + node.getX();            y = y + node.getY();        }        return new Node(x / size, y / size);    }    /**     * 初始化散點資料     *      * @return     */    public static List buildNodes(int size) {        List nodes = Lists.newArrayListWithCapacity(size);        //        生成随機資料。Node除了X、Y的坐标外,後面還有一個Code值,這是我為了在圖表中标注顔色用的,實際不參與計算        for (int i = 0; i < size; i++) {            Node node = new Node((int) (Math.random() * 100), (int) (Math.random() * 100));            nodes.add(node);            System.out.println("[" + node.getX() + ", " + node.getY() + ",0],");        }        //        nodes.clear();        //        以上是随機生成節點清單,下面是為了示範質心的選擇會導緻分簇結果不同而寫的固定資料        //        nodes.add(new Node(42, 7));        //        nodes.add(new Node(94, 44));        //        nodes.add(new Node(96, 66));        //        nodes.add(new Node(62, 84));        //        nodes.add(new Node(29, 33));        return nodes;    }}
           

Node就坐标XY與Code三個值,Code是我為了在圖表中b區分顔色所用,實際不參與計算。

有些要單獨說說:

1、質心的選擇。文獻上都說初始質心要從資料中随機選取,因為我的資料都是随機生成的,是以就偷懶直接取前K個資料作為初始質心。

2、随機擷取初始質心,可能會導緻同一份資料,分簇結果不同,如下:

資料集合:[62, 84],[29, 33],[42, 7],[94, 44],[96, 66]。兩次運算,選取的初始質心不同,結果如下圖:

k均值聚類算法優缺點_聚類之K均值(K-means)算法

結果1

k均值聚類算法優缺點_聚類之K均值(K-means)算法

結果2

紅藍節點為分簇結果,黃綠為收斂最後的虛拟質心。

原因就是初始質心不同,導緻第一次分簇結果不同,導緻新計算的質心不同,導緻最終分簇結果不同。

為了解決此問題,就有了KMeans++算法,後續篇我會說一下,如果有的話~~。

3、距離計算。本文采用了歐幾裡得距離,詳情可參看前文。本文隻有XY二維資料,如果是XYZ三維資料,可以寫成√( (x1-x2)^2+(y1-y2)^2+(z1-z2)^2,高維資料以此類推。歐幾裡得距離隻适用于連續變量。

歐幾裡得距離計算的是點與點的距離,并沒有考慮方向這一因素。舉例:

A商品原價100,漲價到150;B商品原價1000,漲價到1500;C商品200,降價到150。

如果按照價格絕對值看,AC的價格差不多,歐幾裡得距離相近,應聚為一類。

但是按照價格波動來看,AB都是漲價,幅度一緻,C則是降價,是以AB應聚為一類。這時候就不适用歐幾裡得距離了,我們可以采用夾角餘弦距離,來計算其相似度。其公式為:

(X1*X2+Y1*Y2)/(√X1^2+Y1^2+√X2^2+Y2^2)。

同歐幾裡得距離一樣,也可以計算高維資料。

除了這兩個距離計算方式,還有其他的,後續篇我會說一下,如果有的話~~。

KMeans也有不少缺陷,故衍生了不少算法,例如KMeans++等,後續篇我會說一下,如果有的話~~

下圖是我某次跑的資料結果:

k均值聚類算法優缺點_聚類之K均值(K-means)算法