简单对比一下这两者的区别。两者的主要区别主要在质心的选择中,k-means是样本点均值,k-medoids则是从样本点中选取。
首先给出两者的算法实现步骤:
K-means
1、随机选取K个质心的值
2、计算各个点到质心的距离
3、将点的类划分为离他最近的质心,形成K个cluster
4、根据分类好的cluster,在每个cluster内重新计算质心(平均每个点的值)
5、重复迭代2-4步直到满足迭代次数或误差小于指定的值
K-medoids
1、随机选取K个质心的值 (质心必须是某些样本点的值,而不是任意值)
2、计算各个点到质心的距离
3、将点的类划分为离他最近的质心,形成K个cluster
4、根据分类好的cluster,在每个cluster内重新计算质心:
4.1 计算cluster内所有样本点到其中一个样本点的曼哈顿距离和(绝对误差)
4.2 选出使cluster绝对误差最小的样本点作为质心
5、重复迭代2-4步直到满足迭代次数或误差小于指定的值
以上就可以看出两者之间的区别:
k-means的质心是各个样本点的平均,可能是样本点中不存在的点。k-medoids的质心一定是某个样本点的值。
这个不同使他们具有不同的优缺点。
1、k-medoids的运行速度较慢,计算质心的步骤时间复杂度是O(n^2),因为他必须计算任意两点之间的距离。而k-means只需平均即可。
2、k-medoids对噪声鲁棒性比较好。例:当一个cluster样本点只有少数几个,如(1,1)(1,2)(2,1)(100,100)。其中(100,100)是噪声。如果按照k-means质心大致会处在(1,1)(100,100)中间,这显然不是我们想要的。这时k-medoids就可以避免这种情况,他会在(1,1)(1,2)(2,1)(100,100)中选出一个样本点使cluster的绝对误差最小,计算可知一定会在前三个点中选取。
以上。虽然k-medoids也有优点,但是他只能对小样本起作用,样本一大,他的速度就太慢了,而且当样本多的时候,少数几个噪音对k-means的质心影响也没有想象中的那么重,所以k-means的应用明显比k-medoids多的多。
然后给出我实现的两者的Java代码。(呼,新年第一天我还在写代码,也真是不容易啊,注孤生 T-T)
花了一天的时间才写好调试好,这次是用的Java写的,自己实现的k-medoids和k-means。
K-medoids的大致流程是参考的网上的程序,在这:http://blog.sina.com.cn/s/blog_6adf9f370100qnet.html。不过我写的K-medoids跟他已经很不同了。
分别使用两种对11个二维数据进行分类,结果如下:
还只是简单的分析了下,之后会增加文件txt读入的功能,试着用稍微大点的数据来试试。不过白天因为调试也试了很多次。给我的感觉就是,k-medoids方法没有想象中对噪声鲁棒性那么好,或者说,他确实会对噪声鲁棒性好,但是他的结果偏差同样会很严重,在数据量不大的情况下。但是数据量一大,k-medoids方法就基本不能用了,因为这方法的时间复杂度是O(n^2),简直吓死人。。所以我个人分析的结果是k-medoids并没有想象中那么好,怪不得这方法一点都不常用,速度慢不说,误差也没有想象中降低那么明显。
注:上图中的minErr两者不同。k-medoids的是曼哈顿距离和,k-means的则是欧式距离和
代码如下:
package cluster;
import static java.lang.System.*;
import java.util.ArrayList;
//簇:包含质心和围绕着质心的样本点。
public class Cluster{
private DataPoint center;
private String name;
private ArrayList<DataPoint> dataPoints;
private double minErr=-1; //记录每个类簇的绝对误差(曼哈顿距离和)
public Cluster(){this.name=" ";this.center=null;dataPoints=new ArrayList<>();}
public Cluster(String name){this.name=name;this.center=null;dataPoints=new ArrayList<>();}
public Cluster(DataPoint dp){this.center=dp;this.name=dp.name;dataPoints=new ArrayList<>();}
public Cluster(DataPoint dp,String name){
this.center=dp;
this.name=name;
dataPoints=new ArrayList<>();
}
public DataPoint getCenter(){return this.center;}
public void setCenter(DataPoint dp){
this.center=dp;
this.name=dp.name;
for(int i=0;i<dataPoints.size();i++)
this.dataPoints.get(i).setCenter(this.center);
}
public void addDataPoint(DataPoint dp){
dp.setCenter(this.center);
dataPoints.add(dp);
}
public ArrayList<DataPoint> getDataPoints(){ return dataPoints;}
public void copyDataPoints(ArrayList<DataPoint> dps){this.dataPoints=dps;}
public void setMinErr(double min){this.minErr=min;}
public void printCluster(){
out.println("Center:");
out.println(this.center);
out.print("num:"+dataPoints.size());
out.println(" minErr:"+minErr);
for(int i=0;i<dataPoints.size();i++){
out.println(dataPoints.get(i));
}
}
}
//数据点格式
class DataPoint{ //数据点
public double[] values;
public String name;
public DataPoint center;
public static boolean hasSet=false; //设置点的维度
private static int dimension=0; //注意维度是静态的!!也就是每次调用时,对于所有的样本点,都应当是维度一致的!
public DataPoint(double[] values,String name){
if (!hasSet){
DataPoint.setDimension(values.length);
}
if(values.length==this.dimension){
this.values=values;
this.name=name;
}
else{out.println("this may be a wrong data with uncommon dimension.");}
}
public static void setDimension(int dimen){dimension=dimen;hasSet=true;}
public static int getDimension(){return dimension;}
public void setCenter(DataPoint center){this.center=center;}
public DataPoint getCenter(){ return this.center;}
public static boolean equals(DataPoint dp1,DataPoint dp2){
for(int i=0;i<dimension;i++){
if(dp1.values[i]!=dp2.values[i]){return false;}
}
return true;
}
public String toString(){
String val="";
for(int i=0;i<dimension;i++){val=val+" "+values[i];}
return this.name+":"+val;
}
}
package cluster;
import static java.lang.System.*;
import java.util.ArrayList;
import java.lang.Math;
public class Distance{ //下回查一下,怎么将函数作为返回值的,实现通过选定不同的值来调用不同的距离函数
private Distance(){} //将他作为静态类,不需要构造器
public static double allEuclideans(Cluster clus,ArrayList<DataPoint> dataPoints){
DataPoint center=clus.getCenter();
double sum=0;
double dist=0;
for(int i=0;i<dataPoints.size();i++){
dist=Distance.euclidean(center,dataPoints.get(i));
sum+=dist;
}
return sum;
}
public static double euclidean(DataPoint dp1,DataPoint dp2){
double sum=0;
double dist=0;
for(int i=0;i<DataPoint.getDimension();i++){
dist=Math.pow(dp1.values[i]-dp2.values[i],2);
sum+=dist;
}
return sum;
}
public static double mahalanobis(DataPoint dp1,DataPoint dp2){
double sum=0;
double dist=0;
for(int i=0;i<DataPoint.getDimension();i++){
dist=Math.abs(dp1.values[i]-dp2.values[i]);
sum+=dist;
}
return sum;
}
//记录两点之间的曼哈顿距离的矩阵(可以改变其中的距离函数来该表)
//用矩阵来记录距离,可以减少重读计算,如C12=C21,C11=0
public static double[][] matrixDistance(ArrayList<DataPoint> dataPoints){
int length=dataPoints.size();
double[][] matrixDist=new double[length][length];
for(int i=0;i<length;i++){
for(int j=i+1;j<length;j++){
matrixDist[i][j]=Distance.mahalanobis(dataPoints.get(i),dataPoints.get(j));
matrixDist[j][i]=matrixDist[i][j];
}
}
for(int i=0;i<length;i++){matrixDist[i][i]=0;} //跟自己的距离当然是0
return matrixDist;
}
public static double[] infinitNorm(double[][] matrixDist){ //矩阵的无穷范数,其实就是每行之和的最小值,也就是曼哈顿距离的最小,选出该行
int length=matrixDist.length;
int temp;
int currentMin=0;
int itsRow=-1;
boolean flag=true;
double[] result=new double[2];
for(int i=0;i<length;i++){
temp=0;
for(int j=0;j<length;j++){
temp+=matrixDist[i][j];
}
if (temp<currentMin || flag){
currentMin=temp;
itsRow=i;
flag=false;
}
}
result[0]=itsRow;
result[1]=currentMin;
if(itsRow==-1){out.println("Error:cannot find infinitNorm!!please check the inputMatrix.");}
return result;
}
}
package cluster;
import static java.lang.System.*;
import java.util.ArrayList;
import java.util.Random;
import java.util.Collections;
//计算质心的类函数
public class KMedoids{
private int iter=5;
private int kClusters=3;
private ArrayList<DataPoint> dataPoints;
private Cluster[] clusters;
private final ArrayList<DataPoint> originalDataPoints;
private void init(){ //要始终记得,数组初始化不代表元素初始化了!
this.clusters=new Cluster[kClusters];
for(int i=0;i<kClusters;i++){
this.clusters[i]=new Cluster();
}
}
public KMedoids(ArrayList<DataPoint> dps){
this.dataPoints=dps;
originalDataPoints=dps;
this.init();
}
public KMedoids(ArrayList<DataPoint> dps,int iter,int kClusters){
this.iter=iter;
this.kClusters=kClusters;
this.dataPoints=dps;
originalDataPoints=dps;
this.init();
}
//计算质心流程的启动函数
public void start(){
this.initCenter();
for(int i=0;i<iter;i++){
out.println("iter: "+i);
this.classify();
Cluster[] newClusters=this.repickCenter();
if(judgeEqual(newClusters,this.clusters)){ break;}
else{
this.clusters=newClusters;
}
}
}
//打印结果的函数
public void getResult(){
out.println("******************************************************");
out.println("Now show you kMedoids' result:");
for(int i=0;i<kClusters;i++)
clusters[i].printCluster();
}
//初始化随机质心
public void initCenter(){
ArrayList<Integer> randNum=new ArrayList<>();
Random rand=new Random();
for(int i=0;i<kClusters;i++){
int randInt=rand.nextInt(dataPoints.size());
if(randNum.contains(randInt)){
i--;
continue;
}
this.clusters[i].setCenter(dataPoints.get(randInt));
randNum.add(randInt);
}
Collections.sort(randNum);
for(int i=kClusters-1;i>=0;i--){ //从最大的i开始反向删除,才不会因为先删除了小的i导致列表长度改变
int k=randNum.get(i); //哭笑不得,最开始使用dataPoints.remove(randNum,get(i));可是发现一只删不掉,
this.dataPoints.remove(k); //最后才发现randNum返回的里面的是Integer对象,要使用自动拆箱功能才可以
}
}
//对每个数据点分类质心
public void classify(){
for(int i=0;i<kClusters;i++){clusters[i].getDataPoints().clear();} //每次开始前要清楚cluster中的样本
int itsClus;
boolean flag;
double minDist,tempDist;
for(int i=0;i<dataPoints.size();i++){
flag=true;
minDist=0;
itsClus=-1;
for(int j=0;j<kClusters;j++){
tempDist=Distance.euclidean(clusters[j].getCenter(),dataPoints.get(i));
if (tempDist < minDist || flag){
minDist=tempDist;
itsClus=j;
flag=false;
}
}
clusters[itsClus].addDataPoint(dataPoints.get(i));
}
}
//重新确定质心
public Cluster[] repickCenter(){
Cluster[] newClusters=new Cluster[kClusters];
ArrayList<DataPoint> tempList=new ArrayList<>();
for(int i=0;i<kClusters;i++){newClusters[i]=new Cluster();}
for(int i=0;i<kClusters;i++){
ArrayList<DataPoint> clusDataPoints=clusters[i].getDataPoints();
clusDataPoints.add(clusters[i].getCenter());
double[][] matrixDist=Distance.matrixDistance(clusDataPoints);
double[] minErr=Distance.infinitNorm(matrixDist);
newClusters[i].setMinErr(minErr[1]);
int index1=(int)minErr[0];
newClusters[i].setCenter(clusDataPoints.get(index1));
newClusters[i].copyDataPoints(clusDataPoints);
newClusters[i].getDataPoints().remove(index1); //将质心从样本点中去除
tempList.addAll(newClusters[i].getDataPoints());
}
this.dataPoints=tempList;
//for(Cluster clus:newClusters){clus.printCluster();} //打印每次迭代的过程,需要的话可去掉注释
return newClusters;
}
//判断新旧质心是否一样
public boolean judgeEqual(Cluster[] clus1,Cluster[] clus2){
boolean bool;
for(int i=0;i<kClusters;i++){
bool=DataPoint.equals(clus1[i].getCenter(),clus2[i].getCenter());
if(!bool){return false;}
}
return true;
}
}
package cluster;
import static java.lang.System.*;
import java.util.ArrayList;
import java.util.Random;
import java.util.Collections;
public class KMeans{
private int iter=5;
private int kClusters=3;
private ArrayList<DataPoint> dataPoints;
private Cluster[] clusters;
public void init(){ //要始终记得,数组初始化不代表元素初始化了!
this.clusters=new Cluster[kClusters];
for(int i=0;i<kClusters;i++){
this.clusters[i]=new Cluster();
}
}
public KMeans(){}
public KMeans(ArrayList<DataPoint> dataPoints){
this.dataPoints=dataPoints;
this.init();
}
public KMeans(ArrayList<DataPoint> dataPoints,int iter,int kClusters){
this.iter=iter;
this.kClusters=kClusters;
this.dataPoints=dataPoints;
this.init();
}
//KMeans的启动函数
public void start(){
int i=0;
this.initCenter();
while(i<iter){
this.classify();
Cluster[] newClusters=this.repickCenter();
if (judgeEqual(newClusters,this.clusters)){break;}
else{ this.clusters=newClusters; }
i++;
}
}
//打印结果的函数
public void getResult(){
out.println("******************************************************");
out.println("Now show you kMeans' result:");
for(int i=0;i<kClusters;i++)
clusters[i].printCluster();
}
//初始化质心
public void initCenter(){
this.randomCenter();
}
//生成初始质心的一种方法,随机法
public void randomCenter(){
ArrayList<Integer> randNum=new ArrayList<>();
Random rand=new Random();
for(int i=0;i<kClusters;i++){
int randInt=rand.nextInt(dataPoints.size());
if(randNum.contains(randInt)){
i--;
continue;
}
this.clusters[i].setCenter(dataPoints.get(randInt));
randNum.add(randInt);
}
}
//生成初始质心的第二种方法,距离最大法
public void maxCenter(){}
//对样本点分簇
public void classify(){
for(int i=0;i<kClusters;i++){clusters[i].getDataPoints().clear();} //每次开始前要清楚cluster中的样本
int itsClus;
boolean flag;
double minDist,tempDist;
for(int i=0;i<dataPoints.size();i++){
flag=true;
minDist=0;
itsClus=-1;
for(int j=0;j<kClusters;j++){
tempDist=Distance.euclidean(clusters[j].getCenter(),dataPoints.get(i));
if (tempDist < minDist || flag){
minDist=tempDist;
itsClus=j;
flag=false;
}
}
clusters[itsClus].addDataPoint(dataPoints.get(i));
}
for(int i=0;i<kClusters;i++){
double minErr=Distance.allEuclideans(clusters[i],clusters[i].getDataPoints());
clusters[i].setMinErr(minErr);
}
}
//重新确定质心
public Cluster[] repickCenter(){
Cluster[] newClusters=new Cluster[kClusters];
for(int i=0;i<kClusters;i++){newClusters[i]=new Cluster();}
for(int k=0;k<kClusters;k++){
double[] sumValues=new double[DataPoint.getDimension()];
double[] centerValues=new double[DataPoint.getDimension()];
ArrayList<DataPoint> clusDataPoints=clusters[k].getDataPoints();
int length=clusDataPoints.size();
for(int i=0;i<length;i++){
for(int j=0;j<sumValues.length;j++){
sumValues[j]+=clusDataPoints.get(i).values[j];
}
}
for(int i=0;i<sumValues.length;i++){centerValues[i]=sumValues[i]/length;}
DataPoint dp=new DataPoint(centerValues,""+k);
newClusters[k].setCenter(dp);
}
//for(Cluster clus:newClusters){clus.printCluster();} //打印每次迭代的过程,需要的话可去掉注释
return newClusters;
}
//判断新旧质心是否一样
public boolean judgeEqual(Cluster[] clus1,Cluster[] clus2){
boolean bool;
for(int i=0;i<kClusters;i++){
bool=DataPoint.equals(clus1[i].getCenter(),clus2[i].getCenter());
if(!bool){return false;}
}
return true;
}
}
package cluster;
import static java.lang.System.*;
import java.util.ArrayList;
public class TestKM{
public static void main(String[] args){
ArrayList<DataPoint> data=new ArrayList<>();
DataPoint[] dps=new DataPoint[11];
dps[0]=new DataPoint(new double[]{1,3},"0");
dps[1]=new DataPoint(new double[]{1,2},"a");
dps[2]=new DataPoint(new double[]{3,4},"b");
dps[3]=new DataPoint(new double[]{2,1},"c");
dps[4]=new DataPoint(new double[]{3,3},"d");
dps[5]=new DataPoint(new double[]{18,22},"e");
dps[6]=new DataPoint(new double[]{19,20},"f");
dps[7]=new DataPoint(new double[]{20,22},"g");
dps[8]=new DataPoint(new double[]{18,19},"h");
dps[9]=new DataPoint(new double[]{20,22},"i");
dps[10]=new DataPoint(new double[]{23,20},"j");
for(int i=0;i<11;i++){
data.add(dps[i]);
}
KMedoids km1=new KMedoids(data,5,2);
km1.start();
km1.getResult();
KMeans km2=new KMeans(data,5,2);
km2.start();
km2.getResult();
}
}