完成实验的过程学习下聚类分析算法
内容图片如无法查看请前往原站点访问:http://taoblog421.cn/posts/27782ca8/
参考文章:https://developer.ibm.com/zh/articles/ba-1607-clustering-algorithm/
1、分类和聚类
监督学习与非监督学习
生活例子:
高中或者初中阶段,同学们三五成群,比如看小说的几个人一伙,玩魔方的几个人一伙,上网的几个人一伙…,这个过程老师一般是不知道的,也就是没有老师进行监督,称为非监督学习,聚类算法是非监督学习的算法。
高三的时候,有些老师会采取什么学习小组政策,擅长不同学科的同学组成互帮互助学习小组,这个过程是老师监督下完成的,称为监督学习,分类算法是监督学习的算法
数据分类是根据已有的分类标准对数据划分为不同的类别,类别数和分类的标准都是已知的。而聚类则相反,需要根据数据提取属性,按照属性进行分组,每个组之间的数据是相似的,显然组内的相似性越高,组间的相异性越高,聚类的效果越好
2、K均值算法
2.1 基础知识
k均值算法为聚类算法中最基础也最重要的算法,对绝大多数数据有效,但是不能处理异型簇
- 随机的取 k 个点作为 k 个初始质心;
- 计算其他点到这个 k 个质心的距离;
- 如果某个点 p 离第 n 个质心的距离更近,则该点属于 cluster n,并对其打标签,标注 point p.label=n,其中 n<=k;
- 计算同一 cluster 中,也就是相同 label 的点向量的平均值,作为新的质心;
- 迭代至所有质心都不变化为止,即算法结束。
K值的估计
对于k的值必须提前知道,这也是kmeans算法的一个缺点,对于k值有多种估计方法,这里使用平均直径法来估计
就是首先视所有的点为一个大的整体 cluster,计算所有点之间距离的平均值作为该 cluster 的平均直径。选择初始质心的时候,先选择最远的两个点,接下来从这最两个点开始,与这最两个点距离都很远的点(远的程度为,该点到之前选择的最远的两个点的距离都大于整体 cluster 的平均直径)可视为新发现的质心,否则不视之为质心。这样就可以得到质心的数量
2.2 代码实现
测试数据 kmeans.txt
1,1
2,1
1,2
2,2
6,1
6,2
7,1
7,2
1,5
1,6
2,5
2,6
6,5
6,6
7,5
7,6
代码实现(java):
文本读取工具类:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 读取.txt文件工具类
* @author: zyb
* @date: 2020/10/13 15:47
*/
public class FileRead {
public static void main(String[] args) {
String filePath = "文件所在路径/文件名";
List<String> list = FileRead.read(filePath);
for (String s : list) {
System.out.println(s);
}
}
public static List<String> read(String filePath){
FileInputStream inputStream = null;
BufferedReader bufferedReader = null;
List<String> data = new ArrayList<String>();
try {
inputStream = new FileInputStream(filePath);
//设置inputStreamReader的构造方法并创建对象设置编码方式为gbk(编码格式可自行切换)
bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"gbk"));
String str = null;
while((str = bufferedReader.readLine()) != null)
{
data.add(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
}
Kmeans.java:
import java.util.*;
/**
* @Author liaotao
* @Date 2020/12/29 16:28
* 实现kmeans算法
*/
public class Kmeans {
// 节点列表
private static List<Node> nodeList;
// 质心
private static Map<Integer,Node> centroid;
public static void main(String[] args) {
nodeList = getNodeListByPath("g:/kmeans.txt");
centroid = computeK(nodeList);
doIteration();
printResult();
}
/**
* 输出聚类结果
*/
public static void printResult() {
for (Node node : nodeList) {
System.out.println(Arrays.toString(node.getAttributes()) + "belongs to cluster " + node.getLabel());
}
}
/**
* 关键代码,迭代进行聚类
*/
public static void doIteration() {
while (true) {
// 1.计算各个节点到质心的距离,最近的就属于哪个簇
for (Node node : nodeList) {
// 暂时存储一个节点与所有质心的距离,方便取最小值
Map<Double,Integer> distance = new HashMap<>();
for (Map.Entry<Integer, Node> entry : centroid.entrySet()) {
distance.put(getDistance(node,entry.getValue()), entry.getKey());
}
//找最小值
double min = 0;
for (Double value : distance.keySet()) {
if (min < value) {
min = value;
}
}
node.setLabel(distance.get(min));
}
// 2.计算同一 cluster 中,也就是相同 label 的点向量的平均值,作为新的质心;
// 保留旧的质心用于比较
Map<Integer,Node> oldCentroid = centroid;
// 清空旧的质心
centroid = new HashMap<>();
int count = 1;
List<Integer> labelList = new ArrayList<>();
List<CentroidSupport> centroidSupportList = new ArrayList<>();
for (Node node : nodeList) {
if (! labelList.contains(node.getLabel())) {
CentroidSupport centroidSupport = new CentroidSupport(node.getLabel());
labelList.add(node.getLabel());
centroidSupport.getNodeList().add(node);
centroidSupportList.add(centroidSupport);
} else {
for (CentroidSupport centroidSupport : centroidSupportList) {
if (centroidSupport.getLabel() == node.getLabel()) {
centroidSupport.getNodeList().add(node);
}
}
}
}
for (CentroidSupport centroidSupport : centroidSupportList) {
Node avg = CentroidSupport.getAvg(centroidSupport.getNodeList());
centroid.put(count,avg);
count ++;
}
// 判断质心是否变化,若无变化则退出循环
boolean falg = false;
for (Map.Entry<Integer, Node> entry : centroid.entrySet()) {
Node node1 = centroid.get(entry.getKey());
Node node2 = oldCentroid.get(entry.getKey());
if (node1.getLabel() == node2.getLabel() && node1.getAttributes() != node2.getAttributes()) {
falg = true;
}
}
if (falg) {
break;
}
}
}
/**
* 根据文件路径获取节点列表
* @return
*/
public static List<Node> getNodeListByPath(String path) {
List<Node> result = new ArrayList<>();
List<String> list = FileRead.read(path);
for (String s : list) {
List<String> list1 = Arrays.asList(s.split(","));
Node node = new Node();
double[] attArr = new double[6];
for (int i = 0; i < list1.size(); i++) {
attArr[i] = Double.parseDouble(list1.get(i));
}
node.setAttributes(attArr);
result.add(node);
}
return result;
}
/**
* 计算k值和初始质心
* @param nodeList
* @return
*/
public static Map<Integer,Node> computeK(List<Node> nodeList) {
// 计算所有点之间距离的平均值
// 辅助计算节点列表
List<NodeSupport> nodeSupportList = new ArrayList<>();
double distanceSum = 0;
double distanceAvg;
int count = 0;
//最终返回结果map
Map<Integer,Node> resultMap = new HashMap<>();
for (int i = 0; i < nodeList.size(); i++) {
for (int j = i+1; j < nodeList.size(); j++) {
distanceSum += getDistance(nodeList.get(i),nodeList.get(j));
// 填充nodeSupportList
nodeSupportList.add(new NodeSupport(nodeList.get(i),nodeList.get(j),getDistance(nodeList.get(i),nodeList.get(j))));
count ++;
}
}
distanceAvg = distanceSum/count;
//还要用到计数器
count = 3;
// 选择初始质心的时候,先选择最远的两个点
// 这时想到应该封装一个类,类属性有节点1,节点2,他们之间的距离 方便后续操作
//得到距离最远的两个点
NodeSupport max = Collections.max(nodeSupportList, (n1, n2) -> (int) (n1.getDistance() - n2.getDistance()));
resultMap.put(1,max.getNode1());
resultMap.put(2,max.getNode2());
// 接下来从这最两个点开始,与这最两个点距离都大于平均距离的点可视为新发现的质心,否则不视之为质心
for (Node node : nodeList) {
if (getDistance(node,max.getNode1()) > distanceAvg && getDistance(node,max.getNode2()) > distanceAvg) {
//新的质心
resultMap.put(count,node);
count ++;
}
}
return resultMap;
}
/**
* 计算两个结点之间得到欧式距离的平方(为了避免比较时出现奇怪的问题,直接用平方来比较)
* @param n1 节点1
* @param n2 节点2
* @return 欧氏距离的平方
*/
public static double getDistance(Node n1,Node n2) {
double distance = 0;
for (int i = 0; i < n1.getAttributes().length; i++) {
distance += (n1.getAttributes()[i] - n2.getAttributes()[i]) * (n1.getAttributes()[i] - n2.getAttributes()[i]);
}
return distance;
}
}
/**
* 定义分类的节点
*/
class Node {
private int label; //label(标签)用来记录这个点属于哪个cluster(簇)
private double[] attributes = new double[6]; //存放属性,使用数组是可以存放多维的属性
public int getLabel() {
return label;
}
public void setLabel(int label) {
this.label = label;
}
public double[] getAttributes() {
return attributes;
}
public void setAttributes(double[] attributes) {
this.attributes = attributes;
}
@Override
public String toString() {
return "Node{" +
"label=" + label +
", attributes=" + Arrays.toString(attributes) +
'}';
}
}
/**
* 辅助完成功能的类
*/
class NodeSupport{
private Node node1;
private Node node2;
private double distance;
public NodeSupport(Node node1, Node node2, double distance) {
this.node1 = node1;
this.node2 = node2;
this.distance = distance;
}
public Node getNode1() {
return node1;
}
public void setNode1(Node node1) {
this.node1 = node1;
}
public Node getNode2() {
return node2;
}
public void setNode2(Node node2) {
this.node2 = node2;
}
public double getDistance() {
return distance;
}
public void setDistance(double distance) {
this.distance = distance;
}
@Override
public String toString() {
return "NodeSupport{" +
"node1=" + node1 +
", node2=" + node2 +
", distance=" + distance +
'}';
}
}
/**
* 辅助完成质点划分
*/
class CentroidSupport{
private Integer label;
private List<Node> nodeList = new ArrayList<>();
/**
* 根据节点列表算出平均值
* @param list
* @return
*/
public static Node getAvg(List<Node> list) {
Node sum = new Node();
for (Node node : list) {
for (int i = 0; i < node.getAttributes().length; i++) {
sum.getAttributes()[i] += node.getAttributes()[i];
}
}
for (int i = 0; i < sum.getAttributes().length; i++) {
sum.getAttributes()[i] /= list.size();
}
return sum;
}
public Integer getLabel() {
return label;
}
public void setLabel(Integer label) {
this.label = label;
}
public List<Node> getNodeList() {
return nodeList;
}
public void setNodeList(List<Node> nodeList) {
this.nodeList = nodeList;
}
public CentroidSupport(Integer label) {
this.label = label;
}
@Override
public String toString() {
return "CentroidSupport{" +
"label=" + label +
", nodeList=" + nodeList +
'}';
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fi17BdY4-1609473021134)(https://i.loli.net/2020/12/29/1uRi9t7BlemLwQT.png)]
这里默认数据最多是六维,设计的时候不应该使用数组存储,发现已经来不及改,若需要增加多维数据修改Node类即可
3、层次聚类算法
3.1 基础知识
凝聚式层次聚类,就是在初始阶段将每一个点都视为一个簇,之后每一次合并两个最接近的簇,当然对于接近程度的定义则需要指定簇的邻近准则。
簇的邻近准则:
min 、max、组平均。下面这张图可以很好的理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jgQxNHR7-1609473021138)(https://i.loli.net/2020/12/29/jeldwvnT7KsQDL8.png)]
各种距离:
该部分参考:
https://blog.csdn.net/sinat_30353259/article/details/80885702
欧式距离:
初中高中数学经常使用的两点之间的距离公式
曼哈顿距离:
名字可以猜测个大概,从曼哈顿街区一个十字路口到另一个路口的距离肯定不是直线距离,而是实际驾驶的距离,除非暴君mk2直接飞过去…
切比雪夫距离:
之前好像学过个切比雪夫不等式,不知道有啥关系没有…
国际象棋里国王的走法每次只能向各个方向走一步,从点(x1,y1)走到(x2,y2)的最少步数为切比雪夫距离,这个距离为 max(|x1 - x2|,|y1 -y2|)
汉明距离:
两个字符串对应字符不一样的个数
1011101 与 1001001 之间的汉明距离是 2
2143896 与 2233796 之间的汉明距离是 3
“toned” 与 “roses” 之间的汉明距离是 3
余弦距离:
1 - 两个向量角的余弦值(梦回高中,余弦定理 ??),这个余弦值又叫做余弦相似度
闵氏距离:
不是一种距离, 而是一组距离的定义, 是对多个距离度量公式的概括性的表述。
两个n维变量a(x11,x12,…,x1n)与b(x21,x22,…,x2n)间的闵可夫斯基距离定义为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZesCzFVw-1609473021140)(https://i.loli.net/2020/12/30/pSQmR7batZou4vA.png)]
其中p是一个变参数:
当p=1时, 就是曼哈顿距离;
当p=2时, 就是欧氏距离;
当p→∞时, 就是切比雪夫距离。
根据p的不同, 闵氏距离可以表示某一类/种的距离。
绝对距离:
百度百科:平面直角坐标系中两点的横坐标的差的绝对值与纵坐标的差的绝对值的和叫做这两点的绝对距离
d = |x1 - x2| + |y1 + y2|
…(这些应该够用了)
3.2 代码实现
选取学习老师安排的实验:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CQRXIVGR-1609473021143)(https://i.loli.net/2020/12/30/HDZx5t6bTwlpeBO.png)]
临近准则就选最大和最小显然比较简单,距离选择欧氏距离和切比雪夫距离(本来选的平方欧式距离,结果linkage这个函数的参数里没有平方欧式距离)
这样就有四种组合,先用spss软件操作一波,在这两个地方分布选择临近准则和距离衡量
(四种组合就不用了,应该是用到就行,不用覆盖每个组合,两种就行这样)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CNpilRPT-1609473021145)(https://i.loli.net/2020/12/30/BsM14qGviOyIDdR.png)]
好家伙,一点直接报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2ZeAt7Q-1609473021146)(https://i.loli.net/2020/12/30/63IXEzOGDjhsq12.png)]
后来试了几次因为勾选了这个东西,这样结果就没有树状图
网上查了是安装目录的问题,需要将spss安装在默认目录下,无奈卸载重装(时间实在有限,期末将至,暂且认为这样可以解决)
结果:
-
最小距离,欧式距离
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4euOOA95-1609473021148)(https://i.loli.net/2020/12/30/ReGQ1w2yEtg7voz.png)] -
最大距离,切比雪夫距离
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ha4qT1AJ-1609473021148)(https://i.loli.net/2020/12/30/aYS3xN4X2he8dwC.png)]
貌似都差不多,根据不错
既然要求了python就只好先用了
原来python直接调库就行,都不用实现算法,还是有点爽
代码来源:本校大佬博客
import pandas as pd
import scipy.cluster.hierarchy as sch
from sklearn.cluster import AgglomerativeClustering
from sklearn.preprocessing import MinMaxScaler
from matplotlib import pyplot as plt
data = pd.read_csv("test3-1.csv",encoding="gbk") #读入数据
#清除‘酒’这列数据
data = data.drop(['酒'], axis=1)
df = MinMaxScaler().fit_transform(data)
# 建立模型
model = AgglomerativeClustering(n_clusters=3)
model.fit(df)
data['类别标签'] = model.labels_
print(data.head())
# 画图
#single为最近邻点算法,euclidean为欧式距离
ss = sch.linkage(df,method='single', metric='euclidean')
sch.dendrogram(ss)
plt.show()
# 画图
ss = sch.linkage(df,method='complete', metric='chebychev')
sch.dendrogram(ss)
plt.show()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KF07q5nI-1609473021149)(https://i.loli.net/2020/12/30/M1noeHRkzWSiKF9.jpg)]
少库什么的,安装起来又贼慢,要不就先到此为止了,运行结果应该是没有问题。
还是再搞搞,先换个国内得到镜像:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
再全部安装一遍库
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yi5vJH4-1609473021150)(https://i.loli.net/2020/12/30/fDkha24dYy7rnZv.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rG708ngj-1609473021151)(https://i.loli.net/2020/12/30/Q6qXSY4tlfZ29BD.png)]