K-Means 作為機器學習領域最經典的無監督聚類算法,以其原理簡單、實現高效、擴展性強著稱。本文將通過完整的 Java 原始碼實現,深度解析 K-Means 的核心流程:從數據初始化、質心隨機選取、距離計算、簇分配、質心更新到收斂判斷,帶你徹底掌握這一基石算法的工程化落地細節。
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 + "]");
}
}
}
採用「隨機索引 + 唯一性檢查」雙重保障,確保 K 個初始質心來自不同樣本,避免空簇導致算法失效。
直接使用歐氏距離平方(省去 Math.sqrt),因為單調遞增函數不改變大小關係,顯著降低計算開銷。
當樣本到多個質心距離相等時,引入 50% 概率隨機選擇,避免決定性偏差導致簇分佈不均。
以連續兩輪 SSE(誤差平方和)完全相等為終止條件,比固定迭代次數更科學,自動適應數據分佈特性。
updateCenter() 中檢查 pts.isEmpty(),若某簇無樣本則保持質心不變,防止 NaN 污染後續計算。
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}
===================================
double[2] 泛化為 double[] 或向量類,支援多維特徵空間這份純 Java 實現零依賴、邏輯清晰,既適合算法入門學習理解本質,也可作為生產環境輕量級聚類模組的起點。掌握其核心邏輯,將為後續學習 DBSCAN、層次聚類、高斯混合模型等進階算法打下堅實基礎。