一、介绍
聚类是拥有相同属性的对象或记录的集合,属于无监督学习,K-Means 聚类算法是其中较为简单的聚类算法之一,具有易理解,运算速度快的特点。
1.1 内容
通过本次课程我们将使用 C++ 语言实现一个完整的面向对象的可并行K-Means算法。这里我们一起围绕着算法需求实现各种类,最终打造出一个健壮的程序。所以为了更好地完成这个实验,需要你有 C++ 语言基础,会安装一些常用库,喜欢或愿意学习面向对象的编程思维。
1.2 实验知识点
- C++ 语言语法
- K-Means 算法思路与实现
- 并行计算思路与实现
- boost 库的常用技巧(Smart Pointers,Variant,tokenizer)
1.3 环境
- boost 1.68.0
- mpich 3.2.1
1.4 适合人群
本课程适合有 C++ 语言基础,对聚类算法感兴趣并希望在动手能力上得到提升的同学。
1.5 代码获取
下载本次实验的代码,由于文件较多,我们主要参照下载的代码进行讲解。
$ cd /home/shiyanlou
$ wget http://labfile.oss.aliyuncs.com/courses/1193/MPIK-Means.zip
$ unzip MPIK-Means.zip
1.6 效果图
完成时间显示:
- 单进程
- 四进程
输出结果文件:
1.7 项目结构与框架
项目的整个文件目录:
├── clusters
│ ├── distance.hpp
│ └── record.hpp
├── datasets
│ ├── attrinfo.hpp
│ ├── dataset.hpp
│ └── dcattrinfo.hpp
├── mainalgorithm
│ ├── mpikmean.hpp
│ └── mpikmeanmain.cpp
└── utilities
├── datasetreader.hpp
├── exceptions.hpp
├── null.hpp
└── types.hpp
这里简单介绍一下功能模块,在具体实践每一个类的时候会有详细UML图或流程图。
主要分为4个模块:实用工具类,数据集类,聚集类,算法类。
-
实用工具类:定义各种需要的数据类型;常用的异常处理;文件读取。
-
数据集类:将文件中的数据通过智能指针建立一个统一数据类,拥有丰富的属性和操作。
-
聚集类:在数据类基础上实现中心簇。
-
算法类:完成对聚集类的初始化,通过算法进行更新迭代,最终实现数据集的聚类并输出聚类结果。
二、实验原理
这一章我们将配置好实验环境并介绍一些基础知识。
2.1 依赖库安装
安装boost
和mpich2
,安装时间可能会有点久,安装完毕后记得保存实验环境!
mpich2下载(由于配置和编译需要很长的时间,所以我们提供了编译好的安装包,下载然后安装就可以了):
$ cd /home/shiyanlou
$ wget http://labfile.oss.aliyuncs.com/courses/1193/mpich-3.2.1.zip
$ unzip mpich-3.2.1.zip
安装:
$ cd mpich-3.2.1
$ sudo make install
boost下载:
$ wget http://labfile.oss.aliyuncs.com/courses/1193/boost_1_68_0.tar.gz
解压
$ tar xvfz boost_1_68_0.tar.gz
$ cd boost_1_68_0
编译:
$ sh bootstrap.sh
修改 project-config.jam 文件,在第22行后添加一句: using mpi ; 如下图所示,注意添加位置,以及 mpi 和 ; 之间需要一个空格。
安装(此处安装时间比较长约20分钟左右,请耐心等待!):
$ sudo ./bjam --with-program_options --with-mpi install
mpi 支持的进程数和计算机的配置有关,通过cat /proc/cpuinfo |grep "processor"|wc -l
命令,可以查看得知实验楼最多支持4个进程。
检验 boost 是否安装成功,可以检测一下,这里我们测试开启3个进程,运行/home/shiyanlou/MPIK-Means/test/mpitest.cpp
:
$ cd /home/shiyanlou/MPIK-Means/test
$ mpic++ -o mpitest mpitest.cpp -L/usr/local/lib -lboost_mpi -lboost_serialization
$ mpirun -np 3 ./mpitest
若结果如下,有三个Process
则证明安装成功!
Process 1: a msg from master
Process 2: a msg from master
Process 2:
Process 1:
Process 0: zero one two
Process 0: zero one two
Process 1: zero one two
Process 2: zero one two
2.2 boost的小技巧
2.2.1 Smart Pointers
在 Boost 中,智能指针是存储指向动态分配对象的指针的对象。智能指针非常有用,因为它们确保正确销毁动态分配的对象,即使在异常情况下也是如此。事实上,智能指针被视为拥有指向的对象,因此负责在不再需要时删除对象。Boost 智能指针库提供了六个智能指针类模板。表给出了这些类模板的描述。本实验中将大量使用智能指针。
类 | 描述 |
---|---|
scoped_ptr | 单个对象的简单唯一所有权,不可复制 |
scoped_array | 数组的简单唯一所有权,不可复制 |
shared_ptr | 对象所有权在多个指针之间共享 |
shared_array | 多个指针共享的数组所有权 |
weak_ptr | shared_ptr 拥有的对象的非拥有观察者 |
intrusive_ptr | 具有嵌入引用计数的对象的共享所有权 |
表1 智能指针类型简介
2.2.2 Variant versus Any
Boost
variant
类模板是一个安全通用的联合容器,和std::vector
不同,std::vector
是储存单个类型的多个值,variant
可以储存多个类型的单个值,本实验中将使用variant
储存双精度和整数类型来表示不同类型的数据。
与 variant 一样,Boost any 是另一个异构容器。虽然 Boost any 有许多与 Boost variant 相同的功能。
根据 Boost 库文档,Boost variant 比 Boost any 具有以下优势:
-
variant 保证其内容的类型是用户指定的有限类型集之一。
-
variant 提供对其内容的编译时检查访问。
-
variant 通过提供有效的、基于堆栈的存储方案,可以避免动态分配的开销。
同样 Boost any 也有一些优势:
-
any 几乎允许任何类型的内容。
-
很少使用模板元编程技术。
-
any 对交换操作提供安全的不抛出异常保证。
2.2.3 Tokenizer
Tokenizer 提供了一种灵活而简单的方法通过分割符(如:",")将一个完整的string分隔开。
字符串为:”A flexible,easy tokenizer“
如果通过","分割,则结果为:
[A flexible] [ easy tokenizer]
以" " 为分隔符,分割结果为:
[A] [flexible,] [easy] [tokenizer]
三、数据集的构建
接下来将具体实践各个类,会给出每一个类的声明并解释其成员函数和数据成员以及相关联类之间的继承关系和逻辑关系。涉及到重要的成员函数的实现会给出其定义代码,一些普通的成员函数的源码可以到下载的源文件中查看,里面也会有详细的注解。
数据对于一个聚类算法来说非常重要,在这里我们将一个数据集描述为一个记录(record),一个记录由一些属性(Attribute)表征。因此自然而然将依次建立 attributes,records,最后是数据集 datasets。
在此之前我们需要了解一下我们在聚类中实际接触到的数据类型。 这里有一个示例心脏数据集
//heart.data
70.0,1.0,4.0,130.0,322.0,0.0,2.0,109.0,0.0,2.4,2.0,3.0,3.0,2
67.0,0.0,3.0,115.0,564.0,0.0,2.0,160.0,0.0,1.6,2.0,0.0,7.0,1
57.0,1.0,2.0,124.0,261.0,0.0,0.0,141.0,0.0,0.3,1.0,0.0,7.0,2
64.0,1.0,4.0,128.0,263.0,0.0,0.0,105.0,1.0,0.2,2.0,1.0,7.0,1
74.0,0.0,2.0,120.0,269.0,0.0,2.0,121.0,1.0,0.2,1.0,1.0,3.0,1
65.0,1.0,4.0,120.0,177.0,0.0,0.0,140.0,0.0,0.4,1.0,0.0,7.0,1
......
包含13个属性,age,sex,chest pain type(4 values),resting blood pressure......
为了更好地表述不同数据相同属性的差异,我们需要对这些数据进行离散/连续处理,即对于有些数据我们认为它是连续的如:年龄;有些是离散的如:性别。这样我们建立一个描述数据类型的文件:
//heart.names
schema file for heart.dat
///: schema
1, Continuous
2, Discrete
3, Discrete
4, Continuous
5, Continuous
6, Discrete
7, Discrete
8, Continuous
9, Discrete
10, Continuous
11, Discrete
12, Continuous
13, Discrete
14, Class
3.1 AttrValue类
AttrValue 类有一个私有变量,有两个友元函数,一个公有成员函数。 _value
是一个 variant 类型变量,它可以存储一个双精度或无符号整形的数据,分类数据用无符号整形数据表示。
AttrValue类自身无法存储或获取数据。它的两个友元函数可以获取和修改数据_value
。
//MPIK-Means:datasets/attrinfo.hpp
class AttrValue
{
public:
friend class DAttrInfo;//友元函数可以访问_value
friend class CAttrInfo;//友元函数可以访问_value
typedef boost::variant<Real,Size> value_type;//可存储双精度和无符号整形数据
AttrValue();
private:
value_type _value;
};
inline AttrValue::AttrValue(): _value(Null<Size>()) {
}//构造函数,将_value初始化为Null<Size>(定义在utillities/null.hpp中)
3.2 AttrInfo类
AttrInfo 是一个基类,包括了许多虚函数和纯虚函数。这些函数都将在它的派生类中具体实现,基类中仅进行声明和简单定义。