一.总结
在网上找了一份基于MPI实现的 k-means 聚类算法C代码,对其进行分析和学习,以巩固MPI和k-means算法相关知识
二.学习笔记
1.并行思路和算法流程
采用主从模式,主节点负责数据的划分与分配以及初始质心的生成,其他从节点完成本地数据的计算,并将结果返回给主节点,主节点更新质心再次广播给从节点,如此反复迭代
2.输入和输出文件样例
(1)输入文件zoo.data
(2)输出文件clusters-mpi.txt
3.代码解析
//kmeans算法mpi实现
#include <mpi.h>
#include <time.h>
#include <iostream>
#include <fstream>
#include <cstring>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <math.h>
using namespace std;
const int K=7; //聚类的数目
const int D=16;//数据维度(不包括名称和类型)
const int epoch=10000;//迭代轮数
unordered_map<int,string> idx2name;//建立动物名字到整型的映射
//自定义结构体,一个animal结构体就是一个数据点(16维)
struct animal{
int name;//在idx2name中的索引,可根据索引值找到名字
int type;//动物所属的簇(0~K-1)
int characters[D];//动物的16个特征
};
//从zoo.data中读取数据
int loadData(string filename,animal* &data){ //?
ifstream infile; //文件读操作
infile.open(filename);
if(!infile) cout<<"failed to open file "+filename+" !\n";
string str;
int dataNum=0;
vector<animal> tmp; //声明一个元素为结构体animal的向量
while(infile>>str){ //按空格读取,遇到空白符结束
animal curline; //参照zoo.data格式可知一次循环读取一行
int i=0;
//变量name保存动物名字
string name="";
while(str[i]!=',')name+=str[i++];
i++;
//确定名字到整数索引的映射,即初始化idx2name数组
idx2name[dataNum]=name;
//保存名字的索引到结构体中,dataNum记录了数据点总数
curline.name=dataNum++;
//保存特征
for(int j=0;j<D;j++){
curline.characters[j]=str[i]-'0'; //字符转换成整型
i+=2; //跳过逗号
}
//保存所属类型
int type=str[i]-'0';
curline.type=type;
tmp.push_back(curline);
}
data=new animal[dataNum];
for(int i=0;i<dataNum;i++){
data[i]=tmp[i];
}
return dataNum; //返回值为数据点总数
}
//求欧式距离
double distance(int charc[],double center_charc[]){
double dis=0.0;
for(int i=0;i<D;i++){
dis+=(charc[i]*1.0-center_charc[i])*(charc[i]*1.0-center_charc[i]);
}
return sqrt(dis);
}
//聚类
void cluster(animal* &data,int dataSize,double data_center[][D],
double new_data_center[][D],int cnt[]){
/* 输入data存储了所有数据点,其每一个元素是一个animal结构体
输入dataSize是数据点总数
输入data_center是一个K*D二维数组,记录了当前的K个质心的D维数据
输出new_data_center也是一个K*D二维数组,记录了本轮迭代结束后K个质心的D维数据
输出cnt数组记录了本轮迭代后每个质心包含的数据点个数 */
/* 遍历每一个数据点(具体为animal结构体) */
for(int i=0;i<dataSize;i++){
double min_dis=10000.0;
int clusterId=-1;
/* 对于当前数据点,遍历K个质心,找到距离其最近的一个质心,
并记录最短距离和质心编号(0-6) */
for(int j=0;j<K;j++){
double cur_dis=distance(data[i].characters,data_center[j]);
if(cur_dis<min_dis){
min_dis=cur_dis;
clusterId=j;
}
}
/* 将当前遍历到的数据点的信息累加到其所在簇的信息中,
便于后续计算新的聚类中心 */
for(int j=0;j<D;j++){
new_data_center[clusterId][j]+=data[i].characters[j];
}
cnt[clusterId]++;//记录该簇包含的数据点个数
data[i].type=clusterId;//更新当前数据点所属类别
}
}
int main(int argc,char *argv[]){
int rank,size;
int dataNum;//每个进程处理的数据点数
animal* data;//保存数据
double cluster_center[K][D];//数据聚类中心点
memset(cluster_center,0,sizeof(cluster_center));//先全部初始化为0
double local_cluster_center[K][D];//每次聚类得到的新聚类中心
MPI_Status status;
clock_t startTime,endTime;
startTime = clock();
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);//rank记录了当前进程的编号
MPI_Comm_size(MPI_COMM_WORLD,&size);//size记录了总的进程数
//进程0(主进程)读取数据,同时告知每个进程它需要处理的数据量
if(rank==0){
//读取数据到data中,同时记录数据点总数到dataNum中
dataNum=loadData("zoo.data",data);
//为每一个从进程平均分配处理的数据,并发送给它们
for(int i=1;i<size;i++){
int nums=dataNum/(size-1);
int start=(i-1)*nums;
int end=i*nums;
if(i==size-1)end=dataNum;
int sendNum=end-start;
//先把需要处理的数据量发送给各个从进程
MPI_Send(&sendNum,1,MPI_INT,i,99,MPI_COMM_WORLD);
}
}
//如果是从进程,就接收主进程送来的数据量
else{
MPI_Recv(&dataNum,1,MPI_INT,0,99,MPI_COMM_WORLD,&status);
}
MPI_Barrier(MPI_COMM_WORLD); //同步一下(暂时
不太理解)
if(rank==0){
//分发数据,以字节的类型发送,一次send将所有数据发送给接收方
for(int i=1;i<size;i++){
int nums=dataNum/(size-1);
int start=(i-1)*nums;
int end=i*nums;
if(i==size-1)end=dataNum;
//把需要处理的数据发送给各个从进程
MPI_Send((void*)(data+start),sizeof(animal)*(end-start),MPI_BYTE,i,99,MPI_COMM_WORLD);
}
//如果是从进程,就接收主进程送来的数据
}
else{
data=new animal[dataNum];
//为什么是dataNum而不是nums呢?
MPI_Recv(data,sizeof(animal)*dataNum,MPI_BYTE,0,99,MPI_COMM_WORLD,&status);
}
MPI_Barrier(MPI_COMM_WORLD); //同步一下
//进程0产生随机中心点
if(rank==0){
srand((unsigned int)(time(NULL)));
unordered_set<int> vis;
int i=0;
while(i<K){
int idx=rand()%dataNum;
//该数据没被选择过
//count返回vis中值为idx的元素个数
if(vis.count(idx)==0){
//将生成的质心信息存储到cluster_center中
for(int j=0;j<D;j++)
cluster_center[i][j]=data[idx].characters[j];
//vis存储了随机生成的K个质心在data中的索引
vis.insert(idx);
i++;
}
}
}
//广播数据中心,也即将初始质心的信息广播给所有进程(包括0号进程本身)
MPI_Bcast(cluster_center,K*D,MPI_DOUBLE,0,MPI_COMM_WORLD);
//开始做聚类
int local_cnt[K],total_cnt[K];
//每一次for循环进行一轮迭代
for(int round=0;round<epoch;round++){
//先把两个数组填充为全0
memset(local_cluster_center,0,sizeof(local_cluster_center));
memset(local_cnt,0,sizeof(local_cnt));
//各个从进程进行聚类
if(rank){
cluster(data,dataNum,cluster_center,local_cluster_center,local_cnt);
}
//规约前先将容器清零
memset(cluster_center,0,sizeof(cluster_center));
memset(total_cnt,0,sizeof(total_cnt));
//各个从进程将local_cluster_center规约到进程0以便计算新的聚类中心 MPI_Reduce(local_cluster_center,cluster_center,D*K,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD);
//各个从进程将local_cnt规约到进程0以便计算新的聚类中心
MPI_Reduce(local_cnt,total_cnt,K,MPI_INT,MPI_SUM,0,MPI_COMM_WORLD);
MPI_Barrier(MPI_COMM_WORLD); //同步一下
if(rank==0){
//计算新的聚类中心
for(int i=0;i<K;i++){
for(int j=0;j<D;j++){
if(total_cnt[i]!=0)//total_cnt[i]代表第i个质心代表的簇内的数据点数
cluster_center[i][j]/=total_cnt[i];
}
}
}
//广播新的中心
MPI_Bcast(cluster_center,K*D,MPI_DOUBLE,0,MPI_COMM_WORLD);
}
//收集数据,打印结果
if(rank){
//只需要一次if,dataNum就有了?
int buf[dataNum*2];
for(int i=0;i<dataNum;i++){
buf[i*2]=data[i].name;
buf[i*2+1]=data[i].type;
}
MPI_Send(buf,dataNum*2,MPI_INT,0,99,MPI_COMM_WORLD);
//各个从进程将name和type信息发送给主进程
}else{
int buf[dataNum*2];
for(int i=1;i<size;i++){
int nums=dataNum/(size-1);
int start=(i-1)*nums;
int end=i*nums;
if(i==size-1)end=dataNum;
int sendNum=end-start;
MPI_Recv(&buf[start*2],sendNum*2,MPI_INT,i,99,MPI_COMM_WORLD,&status);
//主进程接收信息
}
vector<string> clusters[K];
for(int i=0;i<dataNum;i++){
clusters[buf[i*2+1]].push_back(idx2name[buf[i*2]]);
}
//clusters[i]存储了第i个质心包含的数据点的名称(字符串)
string filename="clusters-mpi.txt";
ofstream out(filename);
for(int i=0;i<K;i++){
out<<"cluster-"<<i<<":"<<endl;
int cnts=1;
for(string name:clusters[i]){ //?
if(cnts%6==0) //每行6个
out<<name<<endl;
else out<<name<<" ,";
cnts++;
}
out<<endl<<endl;
}
out.close();
}
delete []data;
MPI_Barrier(MPI_COMM_WORLD);
MPI_Finalize();
endTime = clock();
cout <<rank<< " : The run time is: " <<(double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
return 0;
}
三.工作记录
暂无
注:代码来自http://t.csdn.cn/36Phy