最近在做一个推荐系统的时候,我们采用的方法是基于SVD的K-means聚类协同过滤算法,其中在实现Kmeans聚类算法的时候参考了一篇文章,里面给出了算法代码,并且很有新意的把最终的聚类结果以散点图的形式展示了一下。于是昨天我突发奇想,可不可以把整个Kmeans聚类过程可视化出来,这样还能更好的帮助我们理解Kmeans具体的过程细节,听起来很有意思,有了想法想尽快实现它,在午饭的时候还一直思考着该如何下手。由于很久没用过d3了,有了想法还得一边上网查d3的语法,看来还得好好学一下了。昨天在查阅了d3比例尺坐标轴等资料后,终于在晚上实现了动态可视化,真是太激动了!
Kmeans算法介绍
k-means算法是一种很常见的聚类算法,它的基本思想是:通过迭代寻找k个聚类的一种划分方案,使得用这k个聚类的均值来代表相应各类样本时所得的总体误差最小。
k-means算法的基础是最小误差平方和准则。其代价函数是:
式中,μc(i)表示第i个聚类的均值。我们希望代价函数最小,直观的来说,各类内的样本越相似,其与该类均值间的误差平方越小,对所有类所得到的误差平方求和,即可验证分为k类时,各聚类是否是最优的。
上式的代价函数无法用解析的方法最小化,只能有迭代的方法。k-means算法是将样本聚类成 k个簇(cluster),其中k是用户给定的,其求解过程非常直观简单,具体算法描述如下:
-
随机选取 k个聚类质心点
-
重复下面过程直到收敛 {
对于每一个样例 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