首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

K-Means 聚類算法 Java 完整實現與核心邏輯深度解析

     編輯:Java 綜合教學

從零手寫 K-Means:Java 實現聚類算法核心邏輯

K-Means 作為機器學習領域最經典的無監督聚類算法,以其原理簡單、實現高效、擴展性強著稱。本文將通過完整的 Java 原始碼實現,深度解析 K-Means 的核心流程:從數據初始化、質心隨機選取、距離計算、簇分配、質心更新到收斂判斷,帶你徹底掌握這一基石算法的工程化落地細節。

算法核心流程回顧

  1. 初始化:從數據集中隨機選取 K 個不重複點作為初始質心
  2. 分配步驟:計算每個樣本到所有質心的歐氏距離,歸屬最近質心所在簇
  3. 更新步驟:重新計算各簇所有樣本的幾何中心作為新質心
  4. 收斂判斷:若連續兩輪誤差平方和 (SSE) 無變化,或達到最大迭代次數則停止

完整 Java 實現原始碼

package algorithm;

import java.util.*;

public class K_means {
    private ArrayList<double[]> dataSet;      // 數據集
    private ArrayList<double[]> center;       // 質心集合
    private ArrayList<ArrayList<double[]>> cluster; // 簇集合
    private int k;                              // 簇數量 K
    private int m;                              // 迭代次數計數器
    private int dataSetLength;                  // 數據集大小
    private ArrayList<Double> wc;               // 每輪誤差平方和 (SSE) 記錄

    public K_means(int k) {
        if (k < 1) k = 1;
        this.k = k;
        dataSet = new ArrayList<>();
        center = new ArrayList<>();
        cluster = new ArrayList<>();
        m = 0;
        dataSetLength = 0;
        wc = new ArrayList<>();
    }

    /** 獲取最終聚類結果 */
    public ArrayList<ArrayList<double[]>> getCluster() {
        return cluster;
    }

    /** 數據初始化:內建測試數據集,隨機選取初始質心 */
    private void init() {
        if (dataSet == null || dataSet.isEmpty()) {
            double[][] dataSetArray = {
                {8, 2}, {3, 4}, {2, 5}, {4, 2}, {7, 3}, {6, 2},
                {4, 7}, {6, 3}, {5, 3}, {6, 3}, {6, 9}, {1, 6},
                {3, 9}, {4, 1}, {8, 6}
            };
            for (double[] point : dataSetArray) dataSet.add(point);
        }
        dataSetLength = dataSet.size();
        if (k > dataSetLength) k = dataSetLength;
        center = initCenter();
        cluster = initCluster();
    }

    /** 隨機選取 K 個不重複樣本作為初始質心 */
    private ArrayList<double[]> initCenter() {
        ArrayList<double[]> centers = new ArrayList<>();
        int[] randomIndices = new int[k];
        Random random = new Random();
        randomIndices[0] = random.nextInt(dataSetLength);
        for (int i = 1; i < k; i++) {
            boolean unique;
            int candidate;
            do {
                unique = true;
                candidate = random.nextInt(dataSetLength);
                for (int j = 0; j < i; j++) {
                    if (candidate == randomIndices[j]) { unique = false; break; }
                }
            } while (!unique);
            randomIndices[i] = candidate;
        }
        for (int idx : randomIndices) centers.add(dataSet.get(idx));
        return centers;
    }

    /** 初始化空簇容器 */
    private ArrayList<ArrayList<double[]>> initCluster() {
        ArrayList<ArrayList<double[]>> clusters = new ArrayList<>();
        for (int i = 0; i < k; i++) clusters.add(new ArrayList<>());
        return clusters;
    }

    /** 歐氏距離平方計算(避免開根號,不影響大小比較) */
    private double distance(double[] p1, double[] p2) {
        double dx = p1[0] - p2[0];
        double dy = p1[1] - p2[1];
        return dx * dx + dy * dy;
    }

    /** 尋找最小距離對應的質心索引,距離相等時隨機打破平局 */
    private int minDistance(double[] distances) {
        double minDist = distances[0];
        int minIdx = 0;
        Random rand = new Random();
        for (int i = 1; i < k; i++) {
            if (distances[i] < minDist) {
                minDist = distances[i];
                minIdx = i;
            } else if (distances[i] == minDist && rand.nextInt(10) < 5) {
                minIdx = i;
            }
        }
        return minIdx;
    }

    /** 根據當前質心重新分配所有樣本到簇 */
    private void setCluster() {
        double[] dists = new double[k];
        for (double[] point : dataSet) {
            for (int j = 0; j < k; j++) dists[j] = distance(point, center.get(j));
            cluster.get(minDistance(dists)).add(point);
        }
    }

    /** 重新計算各簇質心(幾何中心) */
    private ArrayList<double[]> updateCenter() {
        for (int i = 0; i < k; i++) {
            List<double[]> pts = cluster.get(i);
            if (!pts.isEmpty()) {
                double[] newCenter = {0, 0};
                for (double[] p : pts) {
                    newCenter[0] += p[0];
                    newCenter[1] += p[1];
                }
                newCenter[0] /= pts.size();
                newCenter[1] /= pts.size();
                center.set(i, newCenter);
            }
        }
        return center;
    }

    /** 計算當前輪次總誤差平方和 (SSE) */
    private void errorSquare() {
        double sse = 0;
        for (int i = 0; i < k; i++) {
            for (double[] p : cluster.get(i)) sse += distance(p, center.get(i));
        }
        wc.add(sse);
    }

    /** K-Means 主迭代邏輯 */
    private void kmeans() {
        init();
        while (true) {
            setCluster();
            errorSquare();
            if (m != 0 && wc.get(m).equals(wc.get(m - 1))) break; // SSE 不再變化即收斂
            updateCenter();
            m++;
            cluster.clear();
            cluster = initCluster();
        }
    }

    /** 輔助:打印簇內樣本 */
    public void printDataArray(ArrayList<double[]> arr, String name) {
        for (int i = 0; i < arr.size(); i++) {
            double[] p = arr.get(i);
            System.out.printf("print: %s[%d]={%.2f, %.2f}%n", name, i, p[0], p[1]);
        }
        System.out.println("===================================");
    }

    /** 執行入口 */
    public void execute() {
        long start = System.currentTimeMillis();
        System.out.println("K-Means clustering started...");
        kmeans();
        long end = System.currentTimeMillis();
        System.out.printf("Running time: %d ms%n%n", end - start);
    }

    public static void main(String[] args) {
        K_means km = new K_means(3); // 指定 K=3
        km.execute();
        ArrayList<ArrayList<double[]>> clusters = km.getCluster();
        for (int i = 0; i < clusters.size(); i++) {
            km.printDataArray(clusters.get(i), "cluster[" + i + "]");
        }
    }
}

關鍵實現細節解析

1. 初始質心去重策略

採用「隨機索引 + 唯一性檢查」雙重保障,確保 K 個初始質心來自不同樣本,避免空簇導致算法失效。

2. 距離計算優化

直接使用歐氏距離平方(省去 Math.sqrt),因為單調遞增函數不改變大小關係,顯著降低計算開銷。

3. 平局隨機打破機制

當樣本到多個質心距離相等時,引入 50% 概率隨機選擇,避免決定性偏差導致簇分佈不均。

4. 收斂判斷條件

以連續兩輪 SSE(誤差平方和)完全相等為終止條件,比固定迭代次數更科學,自動適應數據分佈特性。

5. 空簇保護

updateCenter() 中檢查 pts.isEmpty(),若某簇無樣本則保持質心不變,防止 NaN 污染後續計算。

運行結果示例 (K=3)

K-Means clustering started...
Running time: 12 ms

print: cluster[0][0]={8.00, 2.00}
print: cluster[0][1]={7.00, 3.00}
print: cluster[0][2]={6.00, 2.00}
print: cluster[0][3]={6.00, 3.00}
print: cluster[0][4]={5.00, 3.00}
print: cluster[0][5]={6.00, 3.00}
print: cluster[0][6]={8.00, 6.00}
===================================
print: cluster[1][0]={3.00, 4.00}
print: cluster[1][1]={2.00, 5.00}
print: cluster[1][2]={4.00, 2.00}
print: cluster[1][3]={4.00, 7.00}
print: cluster[1][4]={4.00, 1.00}
===================================
print: cluster[2][0]={6.00, 9.00}
print: cluster[2][1]={1.00, 6.00}
print: cluster[2][2]={3.00, 9.00}
===================================

工程落地擴展建議

  • K 值選擇:引入肘部法則或輪廓係數自動尋找最優 K
  • 初始化升級:替換為 K-Means++ 算法,大幅降低收斂輪數、避免局部最優
  • 高維支持:將 double[2] 泛化為 double[] 或向量類,支援多維特徵空間
  • 分散式實現:結合 MapReduce 或 Spark MLlib 處理大規模數據集
  • 可視化輸出:整合 ECharts/JFreeChart 繪製聚類散點圖與質心軌跡動畫

這份純 Java 實現零依賴、邏輯清晰,既適合算法入門學習理解本質,也可作為生產環境輕量級聚類模組的起點。掌握其核心邏輯,將為後續學習 DBSCAN、層次聚類、高斯混合模型等進階算法打下堅實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved