k-means算法源于信号处理中的一种向量量化方法,现作为一种聚类分析方法。它的目的是把每个数据点划分到k个聚类中心范围里,使得每个点都属于离它最近的聚类中心对应的类别。
算法描述
已知数据集$(x_1,x_2,…,x_n)$,其中每个数据都是一个$p$维特征向量,k-means 算法就是要把这些输入数据划分到 k 个类别集合$S_i$中,目标函数为:
其中$\mu_i$是$S_i$中所有点的均值。
聚类中心的更新函数为:
由于上述最小化问题是一个 NP 困难)问题,因此只能采用启发式迭代方法来进行求解。
算法步骤
1 2 3 4 5
| 选择 k 个数据点作为初始聚类中心 Repeat 计算每个数据点到聚类中心的距离,并将其划分为离它最近的类别中,形成 k 个簇 根据目标函数重新计算每个类别的聚类中心 Until 聚类中心不发生变化或达到最大迭代此数
|
优点:原理简单,易于实现,可解释度较高,参数仅有聚类中心个数 k
缺点:初始化聚类中心的选取不好把握,可能收敛于局部极值,且在大规模数据中收敛速度较慢,对异常点比较敏感
距离度量
欧式距离
曼哈顿距离
切比雪夫距离
余弦距离
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| public class KMeans {
public double[][][] kmeans(double[][] data, int k, int epoch, double error) { int n = data.length; int p = data[0].length;
double[][] kCenter = new double[k][p]; for (int i = 0; i < k; i++) { for (int j = 0; j < p; j++) { kCenter[i][j] = data[i][j]; } }
double[][][] temp = new double[k][n][p];
for (int iter = 0; iter < epoch; iter++) {
double[][][] resData = new double[k][n][p]; int[] numOfCi = new int[k];
for (int i = 0; i < n; i++) { double minDistance = Double.MAX_VALUE; int minDisIndex = 0; for (int j = 0; j < k; j++) { double curDistance = distance(kCenter[j], data[i]); if (curDistance < minDistance) { minDistance = curDistance; minDisIndex = j; } } resData[minDisIndex][numOfCi[minDisIndex]++] = data[i]; }
for (int i = 0; i < k; i++) { for (int j = 0; j < p; j++) { double eachDataP = 0; for (int d = 0; d < numOfCi[i]; d++) { eachDataP += resData[i][d][j]; } kCenter[i][j] = eachDataP / numOfCi[i]; } }
double curError = 0.0; for (int i = 0; i < k; i++) { for (int j = 0; j < numOfCi[i]; j++) { curError += distance(resData[i][j], kCenter[i]); } } if (curError < error) { return resData; }
if (iter+1 == epoch) { return resData; } }
return temp; }
public double distance(double[] x, double[] y) { int feaLen = x.length; double dis = 0; for (int i = 0; i < feaLen; i++) { dis += Math.pow(x[i] - y[i], 2); } dis = Math.sqrt(dis); return dis; }
public static void main(String[] args) { KMeans kMeans = new KMeans(); double[][] data = { {1.0, 2.0}, {2.0, 7.0}, {3.0, 1.0} }; int k = 2; int epoch = 3; double error = 0.0; double[][][] res = kMeans.kmeans(data, k, epoch, error); for (int i = 0; i < k; i++) { System.out.println("Category "+i+" :"); for (int j = 0; j < res[i].length; j++) { for (int f = 0; f < res[i][j].length; f++) { if (res[i][j][f] != 0.0) { System.out.print(res[i][j][f]); System.out.print(" "); } } System.out.println(); } } } }
|
优化算法
k-means++
动机:初始化聚类中心对之后的聚类结果和运行速度都有着很大的影响,因此初始化合适的聚类中心就很有必要。
策略:
- 从输入数据集合中随机选择一个点作为第一个聚类中心$c_1$
- 计算每一个数据与已选择聚类中心的距离,并找到最短距离,代表了当前样本与当前已有聚类中心之间最短的距离,即$d(x_i)={\mathrm{argmin}}||x_i-c_s||^2_2$ $s=1,2,…,c_{selected}$
- 每个数据被选择为聚类中心的可能性为$\frac{d(x)^2}{\sum_{x_i\in X}d(x)^2}$,选择其中具有最大概率的数据点作为下一个聚类中心
- 重复2、3步选择出 k 个聚类中心,并使用这些点作为初始话的聚类中心执行 k-means 算法
mini batch k-means
动机:若输入数据量特别大,那么每次都要计算所有数据点到聚类中心的距离,导致算法非常耗时。
策略:
- 从数据集中选择一部分数据执行 k-means 算法,批样本大小为 batch size
- 批样本大小一般通过无放回的随机抽样得到的
- 为了提升算法的准确性,一般选择多个 batch size 执行多次 k-means 算法,选择其中最优的聚类中心