一、背景
煤礦地磅產生了一系列資料:
我想從這些資料中,取出最能反映當前車輛重量的資料(有很多資料是車輛上磅過程中產生的資料)。我於是想到了聚類演算法KMeans,該演算法思想比較簡單。
二、演算法步驟
1、從樣本中隨機取出k個值,作為初始中心
2、以k箇中心劃分這些資料,分為k個組
3、重新計算出每個組的中心,作為新中心
4、如果初始中心和新中心不相等,則把新中心作為初始中心,重複2,3。反之,結束
注意:
1、我沒有用嚴格的演算法定義,怕不好理解
2、KMeans善於處理球形資料,因此隨機取k個質心,每個質心吸引離它最近的資料
3、由於質心的取值是不科學的,所以需要不斷地計算調整,直到質心名副其實
三、演算法分析及特點
1、從演算法步驟當中可以看出有兩個問題,需要解決:
首先,如何計算每個組(簇)的質心?
其次,如何把值劃分到不同的組?
2、解決上面兩個問題,因場景和要求不同而有不同的小演算法,由於我的資料是一維的,而不是點,所以可以簡單處理:
a、以每個組的平均值作為質心
b、根據值離質心的距離(相減),選擇距離最近的組加入
3、此演算法有兩個缺點:
1)某個組(簇)劃分不充分,還可以再劃分為更小的組。(容易陷入區域性最優)
2)需要使用者指定k,聚類結果對初始質心的選擇較為敏感(初始選擇不同,聚類結果可能不同)
4、優點:簡單易理解和上手
四、實現
public class KMeans { /* * 聚類函式主體。 * 針對一維 decimal 陣列。指定聚類數目 k。 * 將資料聚成 k 類。 */ public static decimal[][] cluster(decimal[] p, int k) { // 存放聚類舊的聚類中心 decimal[] c = new decimal[k]; // 存放新計算的聚類中心 decimal[] nc = new decimal[k]; // 存放放回結果 decimal[][] g; // 初始化聚類中心 // 經典方法是隨機選取 k 個 // 本例中採用前 k 個作為聚類中心 // 聚類中心的選取不影響最終結果 for (int i = 0; i < k; i++) c[i] = p[i]; // 迴圈聚類,更新聚類中心 // 到聚類中心不變為止 while (true) { // 根據聚類中心將元素分類 g = group(p, c); // 計算分類後的聚類中心 for (int i = 0; i < g.Length; i++) { nc[i] = center(g[i]); } // 如果聚類中心不同 if (!equal(nc, c)) { c = nc; nc = new decimal[k]; } else break; } return g; } /* * 聚類中心函式 * 簡單的一維聚類返回其算數平均值 * 可擴充套件 */ public static decimal center(decimal[] p) { if (p.Length == 0) return 0; return sum(p) / p.Length; } /* * 給定 decimal 型陣列 p 和聚類中心 c。 * 根據 c 將 p 中元素聚類。返回二維陣列。 * 存放各組元素。 */ public static decimal[][] group(decimal[] p, decimal[] c) { // 中間變數,用來分組標記 int[] gi = new int[p.Length]; // 考察每一個元素 pi 同聚類中心 cj 的距離 // pi 與 cj 的距離最小則歸為 j 類 for (int i = 0; i < p.Length; i++) { // 存放距離 decimal[] d = new decimal[c.Length]; // 計算到每個聚類中心的距離 for (int j = 0; j < c.Length; j++) { d[j] = distance(p[i], c[j]); } // 找出最小距離 int ci = min(d); // 標記屬於哪一組 gi[i] = ci; } // 存放分組結果 decimal[][] g = new decimal[c.Length][]; // 遍歷每個聚類中心,分組 for (int i = 0; i < c.Length; i++) { // 中間變數,記錄聚類後每一組的大小 int s = 0; // 計算每一組的長度 for (int j = 0; j < gi.Length; j++) if (gi[j] == i) s++; // 儲存每一組的成員 g[i] = new decimal[s]; s = 0; // 根據分組標記將各元素歸位 for (int j = 0; j < gi.Length; j++) if (gi[j] == i) { g[i][s] = p[j]; s++; } } // 返回分組結果 return g; } /* * 計算兩個點之間的距離, 這裡採用最簡單得一維歐氏距離, 可擴充套件。 */ public static decimal distance(decimal x, decimal y) { return Math.Abs(x - y); } /* * 返回給定 decimal 陣列各元素之和。 */ public static decimal sum(decimal[] p) { decimal sum = 0.0M; for (int i = 0; i < p.Length; i++) sum += p[i]; return sum; } /* * 給定 decimal 型別陣列,返回最小值得下標。 */ public static int min(decimal[] p) { int i = 0; decimal m = p[0]; for (int j = 1; j < p.Length; j++) { if (p[j] < m) { i = j; m = p[j]; } } return i; } /* * 判斷兩個 decimal 陣列是否相等。 長度一樣且對應位置值相同返回真。 */ public static bool equal(decimal[] a, decimal[] b) { if (a.Length != b.Length) return false; else { for (int i = 0; i < a.Length; i++) { if (a[i] != b[i]) return false; } } return true; } }
客戶端呼叫:
1 static void Main(string[] args) 2 { 3 var path = string.Empty; 4 int k = 0; 5 try 6 { 7 path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "blanceTest.txt");//資料檔案路徑 8 k = 4; 9 } 10 catch (Exception) 11 { 12 Console.Write("引數錯誤"); 13 return; 14 } 15 16 decimal[] p = { 1, 2, 3, 5, 6, 7, 9, 10, 11, 20, 21, 22, 23, 27, 40, 41, 42, 43, 61, 62, 63, 100, 150, 200, 1000 }; 17 18 List<decimal> pList = new List<decimal>(); 19 20 var lines = File.ReadAllLines(path); 21 22 foreach (var line in lines) 23 { 24 var data = System.Text.RegularExpressions.Regex.Replace(line, @" +", " "); 25 var datas = data.Split(' '); 26 27 pList.AddRange(datas.Where(d => d != "").Select(d => Convert.ToDecimal(d))); 28 } 29 30 p = pList.ToArray(); 31 32 k = 5; 33 decimal[][] g; 34 g = KMeans.cluster(p, k); 35 for (int i = 0; i < g.Length; i++) 36 { 37 for (int j = 0; j < g[i].Length; j++) 38 { 39 Console.WriteLine(g[i][j]); 40 } 41 Console.WriteLine("----------------------"); 42 } 43 Console.ReadKey(); 44 45 }
注意:
1、如果資料檔案為空或不存在,則用初始化的p陣列,作為測試資料
2、檔案中的資料,見開篇截圖
參考文章:
深入理解K-Means聚類演算法