最近在填之前的坑,尝试用C++实现一个ISODATA的聚类分析算法,目前代码已经码完了,就慢慢的把文档给补上,记录一下自己零零碎碎做的工作;给自己立的flag画一个句号吧。
Kmeans聚类算法的原理
- 在分析一些数据前,我们需要选取通过一些方式对数据的种类进行划分,“物以类聚,人以群分”,分类是为了进一步的分析数据的性质。
- Kmeans聚类算法的一般步骤为
- 随机选取k个中心点
- 遍历所有样本,将数据划分到最近的中心点中(所谓近,即欧式距离近)
- 计算每个聚类的平均值,得到新的聚类中心
- 重复上述2-3步骤,直至所有中心点趋于收敛(变化幅度较小)或达到一定的迭代次数
- kmeans的原理简单,应用也很多,但是k的选取很重要,如果没有数据的先验知识,可能需要多次尝试不同的k值进行迭代计算,观察效果。
ISODATA聚类算法的原理
- 可以看作Kmeans的一种衍生,既然我们有时候只能大致确定k的范围,而不能确定k,那么可以加入分裂、合并机制,通过更自动化的机制,灵活的确定k的值
ISODATA算法的基本步骤
-
参数设置,需要设置的参数包括:
- nc : 初始聚类中心个数
- c : 预期的聚类个数
- tn : 每一类中允许的样本最少数目(少于该数目的聚类可能会被删除)
- te : 类内相对标准差上限(超过该上限的聚类,可能会被分裂)
- tc : 聚类中心点之间的最小距离
- nt : 每次迭代中最多可以合并的次数
- ns : 最多迭代次数
-
初始化
- 在所有样本中,随机选取nc个不重复样本作为聚类中心,之后根据距离最小法判断所有样本属于哪一种聚类
- 对第一次初始化后的聚类,检测是否符合tn条件,样本数目小于tn的类别被取消,重新根据上一步的距离最小法进行分配
-
计算分类后的聚类参数,包含以下参数:
- 聚类中心
- 各类中样本到聚类中心的平均距离
- 各个样本到其所属类别中心的总体平均距离
-
根据当前的状态选择下一步的行为,进行分裂、合并或停止
- 若迭代次数达到要求,则停止
- 若当前聚类数量小于期望数量的一般,则进行分裂检测,判断当前是否需要分裂,若需要则执行分裂行为
- 若聚类数量大于期望数量的两倍,则进行合并检测,判断是否需要合并,若需要则执行合并行为
- 若聚类数量在期望聚类数量的1/2到2倍之间,奇数次迭代则执行分裂检测,偶数次迭代则进行合并检测
关键步骤原理
- 分裂检测步骤
- 计算各个类别中,各个样本到类别中心的标准差,标准差为一矢量,求出每一类内标准差向量中的最大分量
- 若标准差向量中的最大分量大于te,同时满足以下条件之一,则对该类别进行分裂
- 聚类个数小于期望个数一半
- 聚类内部平均距离大于总体平均距离且聚类样本数量大于tn两倍
- 分裂操作:
- 在类内标准差最大的维度上分裂聚类中心点
- 利用距离最小法将当前聚类的样本点分配到两个聚类中
- 更新平均距离等聚类参数
- 合并检测步骤
- 计算聚类中心两两之间的距离,距离小于tc的聚类对,按照距离越小优先级越大的原则,进行合并
- 合并的总次数小于设定值,且一次迭代中,一个聚类只能被合并一次
- 更新聚类参数
代码实现
代码结构
- Cluster.h/cpp : 聚类结构体
- common.h.cpp : 公共函数,包含一些矢量的操作符重载,主要为了编程时候的便捷
- isodata.h/cpp : isodata聚类分析算法类
- error.h : 定义了一些出错的输出字符,为了简便,会做一些出错的检查,然后报出错误,用于提示,但是本身不会做额外错误处理
主要类实现
- Cluster
//Cluster.h
//
// Created by Jeff on 2019/1/6 0006.
//
#ifndef ISODATA_CLUSTER_H
#define ISODATA_CLUSTER_H
#include <vector>
#include <unordered_set>
using namespace std;
struct Cluster{
double innerMeanDis; // 类内平均距离
vector<double> sigma; // 每个聚类的标准差
static double allMeanDis; // 总体平均距离
vector<double> center; // 聚类中心位置的
unordered_set<unsigned> ids; // 从属于此聚类的样本的id,位于isodata的data中的
Cluster():
center{
}, innerMeanDis(0), sigma(vector<double>{
}){
}
explicit Cluster(vector<double> &c):
center(c), innerMeanDis(0), sigma(vector<double>(c.size(), 0)) {
}
void add_point(int p_index);
void clear_ids();
};
#endif //ISODATA_CLUSTER_H
//Cluster.cpp
#include "Cluster.h"
#include "common.h"
double Cluster::allMeanDis = 0;
/**
* 向聚类中添加点
* @param p_index 点的id
*/
void Cluster::add_point(int p_index) {
if (ids.find(static_cast<const unsigned int &>(p_index)) != ids.end())
{
cout << WARN_POINT_REPEAT << endl;
return;
}
ids.emplace(p_index);
}
/**
* 清除聚类中的所有点
*/
void Cluster::clear_ids() {
ids.clear();
}
- ISODATA
//
// Created by Jeff on 2019/1/2 0002.
//
#ifndef ISODATA_ISODATA_H
#define ISODATA_ISODATA_H
#include <vector>
#include <iostream>
#include <string>
#include <algorithm>
#include <unordered_set>
#include <time.h>
#include <random>
#include "error.h"
#include "Cluster.h"
#include <deque>
#include <functional>
#include "common.h"
using namespace std;
// 实现ISODATA聚类算法
class isodata {
private:
typedef function<vector<vector<double>>(void)> READFUNC;
// 直接初始化的数据
// 为了简单,不预留更改设置的接口,只在初始化时设置
unsigned _c; // 预期的聚类个数
unsigned _nc; // 初始聚类中心数目
unsigned _tn; // 每一类中允许的样本最少数目
double _te; // 类内相对标准差上限
double _tc; // 聚类中心点之间的最小距离
unsigned _nt; // 每次迭代中最多可以合并的次数
unsigned _ns; // 最多迭代次数
// 不直接在构造函数中初始化
unsigned row; // 数据的行数,也就是样本个数
unsigned col; // 数据的列数,也就是特征个数
vector<vector<double>> data; // 待分类的数据
deque<Cluster> clusters; // 聚类
READFUNC read_func; // 读取数据的函数,可以自定义
double alpha; // 分裂系数
public:
/**
* 构造函数
* @param _c 预期的聚类个数
* @param _nc 初始聚类中心数目
* @param _tn 每一类中允许的样本最少数目
* @param _te 类内相对标准差上限
* @param _tc 聚类中心点之间的最小距离
* @param _nt 每次迭代中最多可以合并的次数
* @param _ns 最多迭代次数
* @param func 读取数据的函数,无输入,返回二维double vector
*/
explicit isodata(unsigned int c, unsigned int _nc, unsigned int _tn,
double _te, double _tc, unsigned int _nt,
unsigned int _ns, READFUNC func) :
_c(c), _nc(_nc), _tn(_tn),
_te(_te), _tc(_tc), _nt(_nt),
_ns(_ns), row(0), col(0),
clusters(), read_func(std::move(func)), alpha(0.3) {
}
void run()
{
setData();
init_clusters();
for (int i = 0; i < _ns; ++i) {
re_assign();
check_tn(<