首先声明本文章部分内容借鉴于OpenCV实现最大最小距离聚类算法_pan_jinquan的博客-CSDN博客_opencv 聚类算法
对其中的代码做了一定的优化修改,加了很多注释便于理解。
一、算法基本思想
最大最小距离法是模式识别中一种基于试探的类聚算法,它以欧式距离为基础,取尽可能远的对象作为聚类中心。因此可以避免K-means法初值选取时可能出现的聚类种子过于临近的情况,它不仅能智能确定初试聚类种子的个数,而且提高了划分初试数据集的效率。
该算法以欧氏距离为基础,首先初始一个样本对象作为第1个聚类中心,再选择一个与第1个聚类中心最远的样本作为第2个聚类中心,然后确定其他的聚类中心,直到无新的聚类中心产生。最后将样本按最小距离原则归入最近的类。
二、算法实现步骤
测试是在二维平面上选取十个样本点,坐标分别为:{x1(0 0), x2(3 8), x3(2 2), x4(1 1), x5(5 3), x6(4 8), x7(6 3), x8(5 4), x9(6 4), x10(7 5)},其样本分布如图所示(图1):
最大最小距离聚类算法步骤如下:(图2)
原博客给出图2的算法步骤和代码是能对上的,但是又紧接着配了一个表格,经过仔细分析,个人觉得表格中有些数据错误。在这里就不贴出原博客的表格。 本人将采取自己代码跑出的一套结果来分析。
三、代码实现
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
using namespace std;
/*
*计算两个数据之间的欧式距离
*cols:数据的维度(这里是二维,自然也可以是更高维)
*/
float calcuDistance(const uchar* ptr,const uchar* ptrCen, int cols)
{
float d = 0.0;
for (size_t j = 0; j < cols; j++)
{
d += (double)(ptr[j] - ptrCen[j])*(ptr[j] - ptrCen[j]);
}
d = sqrt(d);
return d;
}
/** @brief 最大最小距离算法
@param data:输入样本数据,每一行为一个样本,每个样本可以存在多个特征数据
@param Theta:阈值,一般设置为0.5,阈值越小聚类中心越多
@param centerIndex:聚类中心的下标
@return :返回每个样本的类别,类别从1开始,0表示未分类或者分类失败
*/
cv::Mat MaxMinDisFun(const cv::Mat& data, float Theta, vector<int>& centerIndex)
{
double maxDistance = 0;
int k = 0; //中心点计数,也即是类别
int dataNum = data.rows; //输入的样本数量
//表示所有样本到当前聚类中心的距离。假设样本总数为N,
//那么distanceMat、minDistanceMat是N行1列的向量,
cv::Mat distanceMat = cv::Mat::zeros(cv::Size(1, dataNum), CV_32FC1);
//取较小的距离。minDistanceMat是N行1列的向量,假设当前已经找出3个聚类中心,
//那么minDistanceMat的第4行代表min{第4个样本点分别到3个聚类中心的距离}
cv::Mat minDistanceMat = cv::Mat::zeros(cv::Size(1, dataNum), CV_32FC1);
//表示类别,是N行1列的向量,第N行的值表示第N个样本点所属的类别号(本文中类别号是从1开始的)
cv::Mat classes = cv::Mat::zeros(cv::Size(1, dataNum), CV_32SC1);
//选取第1个点作为第一个聚类的中心,index指示新中心点的样本索引
int index = 0;
centerIndex.push_back(index);
printf("\n\n");
printf("====== 选取第%d个样本做为第%d个聚类中心\n", index + 1,k + 1);
printf("===========================================\n");
//指向当前聚类中心的指针
uchar* ptrCen = (uchar*)data.ptr<uchar>(centerIndex.at(0));
/*
*经过这一轮遍历,所有的样本点均被归为类别1,并且计算max{所有样本点距离第一个聚类中心(第一个样本)的距离}。
*
*/
for (size_t i = 0; i < dataNum; i++)
{
uchar* ptr1 = (uchar*)data.ptr<uchar>(i);
float d= calcuDistance(ptr1, ptrCen, data.cols);
distanceMat.at<float>(i, 0) = d;
classes.at<int>(i, 0) = k + 1; //将样本点归类为样本1
printf("--- 第%d个样本点到第%d类聚类中心的距离为:%f\n",i +1, k + 1,d);
printf("--- 第%d个样本点被划分到第%d类\n",i +1, k + 1);
if (maxDistance < d)
{
maxDistance = d;
index = i; //与第一个聚类中心距离最大的样本
}
}
printf("\n");
printf("---- 与第%d个聚类中心最远的是第%d个样本点, 距离为%f\n", k + 1, index + 1, maxDistance);
minDistanceMat = distanceMat.clone();
double minVal;
double maxVal;
cv::Point minLoc;
cv::Point maxLoc;
maxVal = maxDistance;
double distThreshold = maxDistance*Theta;
printf(" ** 产生新聚类中心的距离阈值为distThreshold = maxDistance*Theta = %f*%f = %f\n",maxDistance, Theta, distThreshold);
while (maxVal > distThreshold)
{
k = k + 1;
centerIndex.push_back(index); //加入新的聚类中心
printf("\n\n");
printf("====== 选取第%d个样本做为第%d个聚类中心\n", index + 1,k + 1);
printf("===========================================\n");
//遍历每个样本点,计算其到当前聚类中心的欧式距离,假设样本总数为N,
//那么minDistanceMat是N行1列的向量,他的第m行表示min{第m个样本点分别到现有聚类中心的距离}
for (size_t i = 0; i < dataNum; i++)
{
uchar* ptr1 = (uchar*)data.ptr<uchar>(i);
ptrCen = (uchar*)data.ptr<uchar>(centerIndex.at(k));//指向当前聚类中心
//计算到当前聚类中心的欧式距离
float d = calcuDistance(ptr1, ptrCen, data.cols);
printf("--- 第%d个样本点到第%d类聚类中心的距离为:%f\n",i +1, k + 1,d);
//按照当前最近临方式分类,哪个近就分哪个类别
if (minDistanceMat.at<float>(i, 0) > d)
{
minDistanceMat.at<float>(i, 0) = d;
printf("--- 得知第%d个样本点到第%d类聚类中心更近\n",i +1, k + 1);
classes.at<int>(i, 0) = k + 1;
printf("--- 第%d个样本点被划分到第%d类\n",i +1, k + 1);
}
}
//查找minDistance中最大值,即求max{min{第i个样本点分别到现有每个聚类中心的距离}}
cv::minMaxLoc(minDistanceMat, &minVal, &maxVal, &minLoc, &maxLoc);
index = maxLoc.y;
if(maxVal > distThreshold)
{
printf(" *max{min{第i个样本点分别到每个聚类中心的距离}},挑出第%d个样本,其距离值为:%f > distThreshold(%f),故将第%d个样本作为新的聚类中心\n", \
index+1, maxVal,distThreshold,index+1 );
}
else
{
printf(" *max{min{第i个样本点分别到每个聚类中心的距离}},挑出第%d个样本,其距离值为:%f <= distThreshold(%f),算法迭代结束\n", index+1, maxVal,distThreshold );
}
}
return classes;
}
int main()
{
cv::Point offset(10,10);//所有样本均加上这个偏移、防止离原点太近影响视觉显示效果
// cv::Mat data = (cv::Mat_<uchar>(16, 2) <<0, 0,
// 3, 8,
// 2, 2,
// 1, 1,
// 5, 3,
// 4, 8,
// 6, 3,
// 5, 4,
// 6, 4,
// 7, 5,//第10个数据
// 5, 8,
// 12, 5,
// 12, 4,
// 12, 8,
// 13, 8,
// 14, 8);
cv::Mat data = (cv::Mat_<uchar>(10, 2) <<0, 0,
3, 8,
2, 2,
1, 1,
5, 3,
4, 8,
6, 3,
5, 4,
6, 4,
7, 5);
cout << "original data=\n" << data << endl;
//画出原始数据分布图
cv::Mat originalMat = cv::Mat::zeros(cv::Size(300, 300), CV_8UC3);
for (size_t i = 0; i < data.rows; i++)
{
cv::Point pt = offset + cv::Point(data.at<uchar>(i, 0) * 10, data.at<uchar>(i, 1) * 10);
cv::circle(originalMat, pt, 2, cv::Scalar(0,0,255),2);
}
cv::imshow("original data",originalMat);
vector<int> centerIndex;
float Theta = 0.35;//
//classes = cv::Mat::zeros(cv::Size(1, dataNum)....)
cv::Mat classes = MaxMinDisFun(data, Theta, centerIndex);
cout << "after classes=\n" << classes << endl;
//设置了6种不同的颜色
const int colorCnts = 6;
cv::Scalar colors[] ={cv::Scalar(255,100,80), cv::Scalar(0,255,0), cv::Scalar(0,0,255), cv::Scalar(0,255,255), cv::Scalar(255,0,255), cv::Scalar(255,255,0)};
//画出聚类后数据分布图
cv::Mat displayMat = cv::Mat::zeros(cv::Size(300, 300), CV_8UC3);
//displayMat.setTo(255);//鐧借壊鑳屾櫙
for (size_t i = 0; i < data.rows; i++)
{
cv::Point pt = offset + cv::Point(data.at<uchar>(i, 0) * 10, data.at<uchar>(i, 1) * 10);
cv::circle(displayMat, pt, 2, colors[classes.at<int>(i, 0) % colorCnts], 2);
}
cv::imshow("result display",displayMat);
waitKey(0);
return 0;
}
3.1 代码结果
3.2 算法流程具体剖析
1)随机选取某个样本点(这里选的是第一个样本点x1)作为聚类中心,分别计算每个样本点到x1的欧式距离,得知第6个样本点x6距离第一个聚类中心x1最远。 经过此轮操作,所有的样本点都被分到第一类。
此外,还计算了产生新聚类中心的距离阈值distThreshold.
2)因为第6个样本点x6距离第一个聚类中心x1最远,距离为8.94,大于distThreshold(3.13),所以选择第6个样本点作为第2个聚类中心。计算每个样本点到第2个聚类中心x6的欧式距离,并判断每个点离哪一个聚类中心(目前只有x1,x6)更近,离哪个聚类中心最近,就把当前样本点归到哪个聚类。
3)按照最大最小距离规则,max{min{第i个样本点分别到每个聚类中心的距离}},其中1 =< i <= 10,挑出第7个样本,其距离值为:5.385165 > distThreshold(3.130495),故将第7个样本作为新的聚类中心。
选取第7个样本x7作为第3个聚类中心。至此共有三个聚类中心了(x1 、 x6 、 x7)。
同理根据max{min{第i个样本点分别到每个聚类中心的距离}},挑出第3个样本,其距离值为:2.828427 <= distThreshold(3.130495),不满足生成新聚类中心的条件,算法迭代结束!!
四、小拓展
刚才例子是三个聚类结果,那么我们试试稍复杂的数据呢,那么在上个演示例子的基础上加几个数据吧。
cv::Mat data = (cv::Mat_<uchar>(16, 2) <<0, 0,
3, 8,
2, 2,
1, 1,
5, 3,
4, 8,
6, 3,
5, 4,
6, 4,
7, 5,//第10个数据
12, 4,
12, 5,
12, 6,
13, 4,
17, 8,
18, 9
);
将float Theta = 0.35改为float Theta = 0.20,阈值越小聚类中心越多,分类越多。请根据实际需要合适地进行设置。
其结果为:
作为对比,如果不 将float Theta = 0.35改为float Theta = 0.20,那么结果为:
显然并不是很理想(上图右侧黄色的点应该被分为2类)。