注: 本文代碼及方法僅供參考,請勿直接使用
C++實現過程
算法流程
kmeans - 點作爲數據,cluster是點的聚簇
BEGIN
選出來 k 個點作爲中心點生成聚簇
循環
計算點與聚簇的距離
每個點加入到距離最近的聚簇中
更新聚簇中心點
聚簇中心點未變 或 循環次數足夠?退出
輸出聚簇
END
數據結構設計
爲了設計出更爲通用的結構,選擇採用OOP面向對象設計,結構比較複雜,尤其是距離計算,求中心這兩個函數。想要通用,那麼就不能限定距離的計算方法,同理,求中心點的方法也可能是任意的,因此需要作爲參數傳遞給算法。
結構概要
VirtualPoint - 虛擬點類(抽象類),無數據成員,定義了 == != 兩個純虛函數
Cluster - 聚簇類,數據成員: VirtualPoint的集合 和 中心點(VirtualPoint類型)
函數成員: 設置中心 更新中心 清空點...
KmeansAlg - 算法框架,run方法實現了聚類算法,提供必要參數(點之間距離計算,求平均點方法),無需重寫算法即可運行
------------------
NDimenPoint - 多維點類,繼承VirtualPoint,用來處理多維數據
首先是兩個通用類 - 虛擬點與聚簇,實際使用的時候,繼承VirtualPoint
類,實現兩個運算符之後即可(當然由於avgPoints
和calcDis
兩個函數,可能需要添加其它方法,不過這是另一回事兒了)。
class VirtualPoint {
private:
public:
VirtualPoint() {}
virtual ~VirtualPoint() {}
// 如下的 相等 判斷主要在判斷中心是否更改時用到
virtual bool operator==(const VirtualPoint &p) = 0;
virtual bool operator!=(const VirtualPoint &p) = 0;
virtual string toString() = 0;
};
typedef shared_ptr<VirtualPoint> sharedVPoint;
typedef sharedVPoint avgPointFunc(const vector<sharedVPoint> &);
// 聚簇類
class Cluster {
private:
vector<sharedVPoint> points; // 所有的點
sharedVPoint centroid; // 中心
avgPointFunc *avgPoints; // 計算所有點的中心的方法
public:
Cluster(avgPointFunc avg);
~Cluster() {}
Cluster &setCentroid(sharedVPoint p); // 設置中心
bool updateCentroid(); // 更新中心,使用 avgPoints 函數更新得到新的中心,並且返回新中心是否與舊中心不同
void clear(); // 清空點
void addPoint(sharedVPoint p); // 添加點
string toString();
// 獲取中心與所有的點,輸出時用
sharedVPoint getCentroid();
const vector<sharedVPoint> &getPoints();
};
然後是kmeans主要過程類,注意下面的run
方法爲算法框架,已經實現,因此如果要針對其他數據類型實現kmeans,無需修改該類,而是繼承VirtualPoint
然後調用該類即可。
// 計算 VirtualPoint 與 Cluster的質心 之間的距離
typedef double calcFunc(const VirtualPoint &, const Cluster &);
class KmeansAlg {
public:
KmeansAlg() {}
~KmeansAlg() {}
// 生成 k 個 位於 [0, n) 中的隨機數, n < 100000000
static vector<int> randDiffNumbers(int n, int k);
static vector<Cluster> run(vector<sharedVPoint> data, int k, calcFunc calcDis, avgPointFunc avgPoints, const int maxRuond = 2000);
};
然後是一個繼承VirtualPoint
的多維點類,能夠處理任意維度的點
class NDimenPoint : public VirtualPoint {
private:
int dimension; // 維度
vector<double> xs; // x1 x2 x3 ...
public:
NDimenPoint(const int d);
NDimenPoint(const int d, vector<double> l);
NDimenPoint(const NDimenPoint &p);
~NDimenPoint();
bool operator==(const VirtualPoint &p) override; // 重載,需要 static_cast
bool operator!=(const VirtualPoint &p) override; // 重載,需要 static_cast
void add(const NDimenPoint &p); // 主要用來計算點的平均值
NDimenPoint operator/(const int n);
double disTo(const NDimenPoint &p); // 計算到某個點的距離
string toString() override;
// 兩個靜態函數,計算點到聚簇距離 以及 計算點的中心值
static double calcDisToCluster(const VirtualPoint &p, const Cluster &c);
static sharedVPoint avgPoints(const vector<sharedVPoint> &points);
};
和多維點類一樣,對於其他非點類型的數據,通過繼承VirtualPoint
,實現必要的函數之後即可調用前述KmeansAlg
的run
方法從而實現kmeans聚類。
代碼:
kmeans_oop.h
#include <algorithm>
#include <cmath>
#include <ctime>
#include <exception>
#include <iostream>
#include <memory>
#include <random>
#include <sstream>
#include <string>
#include <vector>
using std::cerr;
using std::endl;
using std::make_shared;
using std::pow;
using std::shared_ptr;
using std::sqrt;
using std::string;
using std::stringstream;
using std::to_string;
using std::vector;
/**
* kmeans - 點作爲數據,cluster是點的聚簇
* BEGIN
* 選出來 k 個點作爲中心點生成聚簇
* 循環
* 計算點與聚簇的距離
* 每個點加入到距離最近的聚簇中
* 更新聚簇中心點
* 聚簇中心點未變?退出
* 輸出聚簇
* END
*
* 數據結構
* 點 - ==() toString()
* 聚簇 - 計算中心點()
* calcDis(point cluster)
* kmeans() -
*/
class VirtualPoint {
private:
public:
VirtualPoint() {}
virtual ~VirtualPoint() {}
virtual bool operator==(const VirtualPoint &p) = 0;
virtual bool operator!=(const VirtualPoint &p) = 0;
virtual string toString() = 0;
};
typedef shared_ptr<VirtualPoint> sharedVPoint;
typedef sharedVPoint avgPointFunc(const vector<sharedVPoint> &);
class Cluster {
private:
vector<sharedVPoint> points;
sharedVPoint centroid;
avgPointFunc *avgPoints;
public:
Cluster(avgPointFunc avg) { avgPoints = avg; }
~Cluster() {}
Cluster &setCentroid(sharedVPoint p) {
centroid = p;
points.push_back(p);
return *this;
}
bool updateCentroid() {
sharedVPoint tmpPoint = avgPoints(points);
if (tmpPoint == nullptr) return false;
bool changed;
if (tmpPoint != nullptr && centroid != nullptr)
changed = (*tmpPoint) != (*centroid);
else
changed = true;
centroid = tmpPoint;
return changed;
}
void clear() { points.clear(); }
void addPoint(sharedVPoint p) {
points.push_back(p);
}
string toString() const {
stringstream ss;
if (centroid == nullptr || points.size() == 0) return "{}";
ss << "{\"centroid\": " << centroid->toString() << ",\"points\": [";
for (int i = 0; i < points.size(); i++) {
if (i > 0) ss << ", ";
ss << points[i]->toString();
}
ss << "]}";
return ss.str();
}
sharedVPoint getCentroid() const { return centroid; }
const vector<sharedVPoint> &getPoints() { return points; }
};
// 計算 VirtualPoint 與 Cluster的質心 之間的距離
typedef double calcFunc(const VirtualPoint &, const Cluster &);
class KmeansAlg {
public:
KmeansAlg() {}
~KmeansAlg() {}
// 生成 k 個 位於 [0, n) 中的隨機數, n < 100000000
static vector<int> randDiffNumbers(int n, int k) {
const int maxn = 100000000;
vector<int> res;
if (n <= 0 || n >= maxn)
throw std::runtime_error("n is less than zero or greater than maxn(100,000,000)");
for (int i = 0; i < n; i++)
res.push_back(i);
random_shuffle(res.begin(), res.end());
res.resize(k);
return res;
}
static vector<Cluster> run(vector<sharedVPoint> data, int k, calcFunc calcDis, avgPointFunc avgPoints, const int maxRuond = 2000) {
if (k <= 1) throw std::runtime_error("k is less than 1");
vector<Cluster> clusters;
for (auto &&i : randDiffNumbers(data.size(), k))
clusters.push_back(Cluster(avgPoints).setCentroid(data[i]));
for (int round = 0; round < maxRuond; round++) {
// 清空
for (auto &&c : clusters) c.clear();
for (size_t i = 0; i < data.size(); i++) {
// 計算距離,加入到最近聚簇中
double minDis = calcDis(*(data[i]), clusters[0]);
int minIndex = 0;
for (size_t j = 1; j < clusters.size(); j++) {
double tmpDis = calcDis(*(data[i]), clusters[j]);
if (tmpDis < minDis) minDis = tmpDis, minIndex = j;
}
clusters[minIndex].addPoint(data[i]);
}
bool changed = false;
for (auto &&c : clusters) changed = changed || c.updateCentroid();
if (!changed) break;
// cerr << "debug\t\tround: " << round << " ";
// for (auto &&c : clusters)
// if (c.getPoints().size() > 0)
// cerr << c.getCentroid()->toString() << ", ";
// cerr << endl;
}
return clusters;
}
};
kmeans_h
#include "kmeans_oop.h"
using std::cin;
using std::cout;
using std::initializer_list;
using std::runtime_error;
class NDimenPoint : public VirtualPoint {
private:
int dimension;
vector<double> xs;
public:
NDimenPoint(const int d) : dimension(d) { xs.resize(d); }
NDimenPoint(const int d, vector<double> l) : dimension(d), xs(l){};
NDimenPoint(const NDimenPoint &p) : dimension(p.dimension), xs(p.xs) {}
~NDimenPoint(){};
bool operator==(const VirtualPoint &p) override {
auto pp = static_cast<const NDimenPoint &>(p);
if (dimension != pp.dimension) return false;
for (size_t i = 0; i < xs.size(); i++)
if (xs[i] != pp.xs[i]) return false;
return true;
}
bool operator!=(const VirtualPoint &p) override {
auto pp = static_cast<const NDimenPoint &>(p);
if (dimension != pp.dimension) return true;
for (size_t i = 0; i < xs.size(); i++)
if (xs[i] != pp.xs[i]) return true;
return false;
}
void add(const NDimenPoint &p) {
if (p.dimension != dimension) throw runtime_error("dimension mismatch");
for (size_t i = 0; i < xs.size(); i++)
xs[i] += p.xs[i];
}
NDimenPoint operator/(const int n) {
if (n == 0) throw std::runtime_error("divisor zero error!");
NDimenPoint res(dimension);
for (size_t i = 0; i < dimension; i++) {
res.xs[i] = xs[i] / n;
}
return res;
}
double disTo(const NDimenPoint &p) {
double tmp = 0;
for (size_t i = 0; i < dimension; i++) tmp += pow(xs[i] - p.xs[i], 2);
return sqrt(tmp);
}
string toString() override {
stringstream ss;
ss << "[";
for (size_t i = 0; i < dimension; i++) {
if (i > 0) ss << ", ";
ss << xs[i];
}
ss << "]";
return ss.str();
}
static double calcDisToCluster(const VirtualPoint &p, const Cluster &c) {
auto pp = static_cast<const NDimenPoint &>(p);
auto cp = static_cast<const NDimenPoint &>(*(c.getCentroid()));
return pp.disTo(cp);
}
static sharedVPoint avgPoints(const vector<sharedVPoint> &points) {
if (points.size() <= 0) return nullptr;
NDimenPoint resPoint(static_cast<const NDimenPoint &>(*points[0]).dimension);
for (auto &&p : points)
resPoint.add(static_cast<const NDimenPoint &>(*p));
resPoint = resPoint / points.size();
// cerr << "DEBUG\t" << resPoint.toString() << ", POINTS.SIZE " << points.size() << endl;
return make_shared<NDimenPoint>(resPoint);
};
};
vector<NDimenPoint> geneData(int num, const int dimension, double maxVal = 1000) {
std::default_random_engine generator(time(NULL));
std::uniform_real_distribution<double> distribution(0, maxVal);
vector<NDimenPoint> points;
for (size_t i = 0; i < num; i++) {
vector<double> tmpVec;
for (size_t j = 0; j < dimension; j++) tmpVec.push_back(distribution(generator));
points.push_back(NDimenPoint(dimension, tmpVec));
}
return points;
}
void output(const vector<Cluster> &clusters, const int dimension) {
cout << "{"
<< "\"dimension\":" << dimension << "," << endl
<< "\"clusters\":[";
for (int i = 0; i < clusters.size(); i++) {
if (i > 0) cout << ", ";
std::cout << clusters[i].toString() << std::endl;
}
cout << "]}" << endl;
}
void kmeans_work() {
const int maxRound = 10000;
const int pointCnt = 150;
int dimension = 1;
int k = 0;
cerr << "dimension, k: ";
cin >> dimension >> k;
vector<sharedVPoint> points;
for (auto &&p : geneData(pointCnt, dimension)) points.push_back(make_shared<NDimenPoint>(p));
auto clusters = KmeansAlg::run(points, k, NDimenPoint::calcDisToCluster, NDimenPoint::avgPoints, maxRound);
output(clusters, dimension);
}
main.cpp
int main(int argc, char const *argv[]) {
kmeans_work();
return 0;
}
Python可視化過程
原本打算使用opengl可視化,但是繪製一個三角形就需要一二百行代碼實在難以接受且低效,則選擇使用matplotlib
實現,支持二維和三維
實現過程的tips:
- matplotlib 繪製三維圖 -
plt.figure().add_subplot(111, projection='3d')
- 二維參數 -
ax.scatter(xs=xs, ys=ys, zs=zs, zdir='z', c=color, marker=marker)
- 三維參數 -
ax.scatter(x=xs, y=ys, c=color, marker=marker)
- 二維參數 -
- 散點圖scatter
- 可以在一個ax(fig.add_subplot返回值)上多次scatter
- 每次scatter的時候可以指定一個顏色’#000000’
- marker - “.”: 點, “,”:像素 , “o”: 圈, “^”: 倒三角, “+”: 加, 參考官方文檔
具體實現過程與代碼如下
# 運行kmeans算法
# 將結果(JSON化)輸出到文件中
# 使用Python讀取文件內容
# 使用pyplot可視化
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import json
import random
colors = [
"#ff0000", "#00ff00", "#0000ff", "#404040", "#ff00ff", "#00ffff", "#C0ff00", "#ffC000", "#ff00C0", "#000070", "#007000", "#700000",
]
def paint(ax, xs, ys, color, zs=None, marker='.'):
if zs != None:
ax.scatter(xs=xs, ys=ys, zs=zs, zdir='z', c=color, marker=marker)
else:
ax.scatter(x=xs, y=ys, c=color, marker=marker)
def readData():
random.shuffle(colors)
data = json.load(open("foo.json", mode="r", encoding="utf-8"))
dimension = data["dimension"]
clusters = []
clusterCnt = 0
for tmpRawCluster in data["clusters"]:
tmpCluster = {"centroid": None, "xss": [],
"color": colors[clusterCnt % 140]}
if "centroid" in tmpRawCluster:
tmpCluster["centroid"] = tmpRawCluster["centroid"]
for i in range(0, dimension):
tmpCluster["xss"].append([])
if "points" in tmpRawCluster:
for tmpRawPoint in tmpRawCluster["points"]:
for j in range(0, len(tmpRawPoint)):
tmpCluster["xss"][j].append(tmpRawPoint[j])
clusters.append(tmpCluster)
clusterCnt += 1
return {"dimension": dimension, "clusters": clusters}
def work():
data = readData()
fig = plt.figure()
if data["dimension"] == 2:
ax = fig.add_subplot(111)
for cluster in data["clusters"]:
if cluster["centroid"]:
paint(ax, cluster["xss"][0],
cluster["xss"][1], cluster["color"], marker='o')
paint(ax, [cluster["centroid"][0]], [
cluster["centroid"][1]], "#000000", marker='^')
elif data["dimension"] == 3:
ax = fig.add_subplot(111, projection='3d')
for cluster in data["clusters"]:
paint(ax, cluster["xss"][0], cluster["xss"]
[1], cluster["color"], cluster["xss"][2])
plt.show()
pass
if __name__ == "__main__":
work()
部分截圖
如下效果圖僅供參考,三角形爲聚簇中心點,後續考慮使用更優化的算法。