Kmeans聚類過程的動態可視化


最近在做一個推薦系統的時候,我們采用的方法是基於SVD的K-means聚類協同過濾算法,其中在實現Kmeans聚類算法的時候參考了一篇文章,里面給出了算法代碼,並且很有新意的把最終的聚類結果以散點圖的形式展示了一下。於是昨天我突發奇想,可不可以把整個Kmeans聚類過程可視化出來,這樣還能更好的幫助我們理解Kmeans具體的過程細節,聽起來很有意思,有了想法想盡快實現它,在午飯的時候還一直思考着該如何下手。由於很久沒用過d3了,有了想法還得一邊上網查d3的語法,看來還得好好學一下了。昨天在查閱了d3比例尺坐標軸等資料后,終於在晚上實現了動態可視化,真是太激動了!

Kmeans算法介紹

k-means算法是一種很常見的聚類算法,它的基本思想是:通過迭代尋找k個聚類的一種划分方案,使得用這k個聚類的均值來代表相應各類樣本時所得的總體誤差最小。

k-means算法的基礎是最小誤差平方和准則。其代價函數是:
Kmeans代價函數

式中,μc(i)表示第i個聚類的均值。我們希望代價函數最小,直觀的來說,各類內的樣本越相似,其與該類均值間的誤差平方越小,對所有類所得到的誤差平方求和,即可驗證分為k類時,各聚類是否是最優的。

上式的代價函數無法用解析的方法最小化,只能有迭代的方法。k-means算法是將樣本聚類成 k個簇(cluster),其中k是用戶給定的,其求解過程非常直觀簡單,具體算法描述如下:

  1. 隨機選取 k個聚類質心點

  2. 重復下面過程直到收斂 {

    對於每一個樣例 i,計算其應該屬於的類:
    計算樣例所屬類

    對於每一個類 j,重新計算該類的質心:
    計算質心
    }

下面用文字描述一下Kmeans的偽代碼:

創建k個點作為初始的質心點(隨機選擇)

當任意一個點的簇分配結果發生改變時

對數據集中的每一個數據點

對每一個質心

計算質心與數據點的距離

將數據點分配到距離最近的簇

對每一個簇,計算簇中所有點的均值,並將均值作為質心

下面上Kmeans聚類用js實現的代碼:

在Kmeans聚類中,我們一般使用歐氏距離作為質心與數據點的誤差度量,因此我們在定義一個距離函數:

function euclDistance(vector1, vector2) {
var dx = vector1.x - vector2.x;
var dy = vector1.y - vector2.y;
return Math.sqrt(dx*dx + dy*dy);
}

按照第一步,我們首先要隨機選取k個質心,k就是我們要聚類的類的個數,data是數據點的數組,會在下一節中提到。下面看代碼:

function initCentroids(data, k) {
var centroids = new Array();
var indices = [];
var i = 0;
while(i < k) {
var index = getRandomNum(0, length);
if(contains(indices, index)) {
continue;
} else {
indices.push(index);
i++;
var node = {};
node.x = data[index].x;
node.y = data[index].y;
node.index = index;
centroids.push(node);
}
}
console.log(centroids);
return centroids;
}

這里用到的getRandomNum函數是用來獲取0~length-1之間的隨機數的,代碼見下:

function getRandomNum(min, max) {
var range = max - min;
var rand = Math.random();
return(min + Math.floor(rand * range));
}

並且為了保證在隨機選取質心的時候不會重復選擇,我們需要確保每次獲取的隨機數並沒使用過,於是我們用一個數據保存已經使用了的隨機數,然后每次檢查一下數組是否包含了(contains)得到的新隨機數:

function contains(arr, obj) {
var i = arr.length;
while (i--) {
if (arr[i] === obj) {
return true;
}
}
return false;
}

第二步,重復計算知道收斂,即所有數據點所屬類別不再變化。代碼(代碼中出現的畫圖函數將會在下一節中描述):

function kmeans(data, k) {

var centroids = initCentroids(data, k);
var clusterChanged = true;
var clusterAssment = [];
var clusters = [];

showNodes(data); //畫數據的散點圖
showCentroids(centroids); //畫質心

for(var i=0; i<k; i++) {
clusters.push(new Array());
}
for(var i=0; i<length; i++) {
clusterAssment.push(-1);
}
while(clusterChanged) {
console.log("kmeans!!!");
clusters = [];
for(var i=0; i<k; i++) {
clusters.push(new Array());
}
clusterChanged = false;
for(var i=0; i<length; i++) {
var minDistance = 10000;
var cluster = -1;
for(var j=0; j<k; j++) {
var distance = euclDistance(centroids[j], data[i]);
if(distance < minDistance) {
minDistance = distance;
cluster = j;
}
}
if(cluster != clusterAssment[i]) {
clusterChanged = true;
clusterAssment[i] = cluster;
changeCluster(i, cluster); //數據所屬類別變化
}
}
for(var i=0; i<length; i++) {
clusters[clusterAssment[i]].push(data[i]);
}
for(var i=0; i<k; i++) {
var sumX = 0;
var sumY = 0;
var len = clusters[i].length;
for(var j=0; j<len; j++) {
sumX += clusters[i][j].x;
sumY += clusters[i][j].y;
}
centroids[i].x = sumX / len;
centroids[i].y = sumY / len;
changeCentroid(centroids, i); //質心改變,重新畫
}
}

alert("kmeans will be completed in " + delay/1000 +" seconds!");
setTimeout("alert('Kmeans is completed!')", delay);
}

動態可視化

也是因為好久沒用過d3,想借助這個小例子在復習一下,所以可視化部分使用了d3.js。
首先是數據,為了方便可視化,在這里我使用了二維數據data.csv,格式如下:

x,y
1.658985,4.285136
3.453687,3.424321
4.838138,1.151539
5.379713,3.362104
0.972564,2.924086
3.567919,1.531611

可視化第一步,我們首先要把數據加載進來:

d3.csv("data.csv",function(error, data){

}

這樣我們得到的data就是一個字典的數組,例如data[0].x就是1.658985啦。

下面正式進入可視化部分,開工!
創建svg,以及比例尺和坐標軸:

var w = 1000;
var h = 600;
var xPadding = 200;
var yPadding = 50;
var xAxisHeight = 500;
var xAxisWidth = 600;
var edge = 15;
var radius = 5;
var delay = 0;

var chart = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);

var colors = ["red", "blue", "yellow", "green"];

var xScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.x; })])
.range([xPadding, xPadding + xAxisWidth]); //x軸

var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.y; })])
.range([xAxisHeight, yPadding]);//y軸

var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");

var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");

chart.append("g")
.attr("class","axis")
.attr("transform","translate("+0+","+xAxisHeight+")")
.call(xAxis);

chart.append("g")
.attr("class","axis")
.attr("transform","translate("+xPadding+","+0+")")
.call(yAxis);

接下來我們需要做的就是繪制所有數據點以及初始質心,然后在發生變化的時候用動畫的形式重新繪制就可以啦。

畫數據點,初始默認都是黑色的小圓點:

function showNodes(data) {
chart.selectAll(".circle")
.data(data)
.enter()
.append('circle')
.attr("class", function(d, i) {
return "node"+data[i].index;
})
.attr("cx", function(d) {
return xScale(d.x);
})
.attr("cy", function(d) {
return yScale(d.y);
})
.attr("r", function(d) {
return radius;
});
}

畫初始隨機的質心,不同質心用不同顏色區分,顏色數組見上面變量聲明部分:

function showCentroids(centroids) {
chart.selectAll(".circle")
.data(centroids)
.enter()
.append('rect')
.attr("class", function(d, i) {
return "cluster"+i;
})
.attr("fill", function(d, i) {
return colors[i];
})
.attr("x", function(d) {
return xScale(d.x)-edge/2;
})
.attr("y", function(d) {
return yScale(d.y)-edge/2;
})
.attr("width", function(d) {
return edge;
})
.attr("height", function(d) {
return edge;
});
}

本例子中最關鍵的一步,也就是在發生變化的時候如何動態的展示,先看代碼:

function changeCluster(nodeId, clusterId) {
chart.select(".node"+nodeId)
.transition()
.duration(100)
.delay(delay)
.attr("fill", colors[clusterId]);
delay+=100;
}

function changeCentroid(centroids, i) {
chart.select(".cluster"+i)
.transition()
.duration(1000)
.delay(delay)
.attr("x", function() {
return xScale(centroids[i].x)-edge/2;
})
.attr("y", function(d) {
return yScale(centroids[i].y)-edge/2;
});
delay += 1000;
}

其中最關鍵的一個變量是delay,我們為每個動畫設置duration,然后為之后的動畫增加delay。

當一個數據點所屬類別發生變化時,我們只需要改變這個點的顏色即可,工作量很小,所以我們為它設置duration為0.1s,相應的delay也要增加0.1s,這樣便使得之后的動畫開始的時間是在該動畫結束的那一刻。當一個類的質心發生變化時,為了有更好的顯示效果,我們可以讓這個質心點在1s的時間間隔里從原來的位置平移到新位置,同理,設置duration為1000,同時delay增加1000.

注:由於數據點順序隨機,這樣按照數據點的順序變換節點的顏色的時候會隨機變化,我們可以事先對數據點按照其距離原點的遠近進行排序,這樣再按照順序變化節點的顏色時就會出現從左下角逐漸到右上角的趨勢,利於觀察。

data.sort(sortByNorm);

function sortByNorm(vector1, vector2) {
var norm1 = vector1.x*vector1.x + vector1.y*vector1.y;
var norm2 = vector2.x*vector2.x + vector2.y*vector2.y;
return norm1 - norm2;
}

大功告成!我們終於可以看一下效果啦:

kmeans(data, 4);

下面是可視化效果,

初始時:

初始狀態

聚類完成后:

最終狀態

這是本例的鏈接:Kmeans聚類過程的動態可視化

參考資料

  1. 機器學習算法與Python實踐之(五)k均值聚類(k-means)

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com