本文记录了几种主流的聚类算法的评价指标。主要参考文献:《机器学习》-周志华。
其中,我们重点关注聚类精度( AC )这种评价指标的原理及实现。
大体上,聚类算法的评价指标分为两种,
0) 外部评价指标
1) 内部评价指标
外部评价指标是在真实标签已知的情况下,衡量聚类结果与真实标签之间的吻合程度。常用的有以下几个:
0)Jaccard Coefficient ( JC );
1)Fowlkes and Mallows Index ( FMI );
2)Rand Index ( RI );
3) Purity ;
4)Accuracy ( AC );
5)Normalized Mutual Information ( NMI );
内部评价指标是在不能获得真实标签的情况下,衡量聚类结果本身的好坏情况(比如簇的内聚性,簇间独立性)。常用的有两个:
6)Davies-Bouldin Index ( DBI );
7)Dunn Index ( DI );
下面分别介绍:
假设数据集 D={x1,…,xn} ,假设聚类得出的标签为 p=[p1,…,pn] ,真实的标签为 r=[r1,…,rn] ,将样本两两配对考虑,定义
SS={(xi,xj)|pi=pj,ri=rj,i<j} ,
SD={(xi,xj)|pi=pj,ri≠rj,i<j} ,
DS={(xi,xj)|pi≠pj,ri=rj,i<j} ,
DD={(xi,xj)|pi≠pj,ri≠rj,i<j} ,
其中,SS包含了那些预测为相同簇并且真实标签也一致的样本对,
SD包含了那些预测为相同簇但是真实标签不一致的样本对,
DS包含了那些预测为不同簇但是真实标签一致的样本对,
DD包含了那些预测为不同簇并且真实标签也不一致的样本对。
易知,每个样本对出现并只能出现在上述某一个集合中。
基于上述式子,可导出以下外部指标:
0) JC
1) FMI
2) RI
假设通过聚类给出的簇划分为 C={Ci}ki=1 ,真实簇划分为 C′={C′i}si=1 ,我们构建一个矩阵 W={wij=|Ci∩C′j|}k×s , W 存储了每一个预测簇和真实簇之间的相同样本数量。
3) Purity
顾名思义, Purity 指的是纯度,该指标可通过如下优化问题获得:
4) AC
AC 是目前最流行的聚类评价指标。在很多文献里面,都将 AC 作为聚类结果的评价指标。 AC 定义如下:
import java.util.Arrays;
import org.ujmp.core.Matrix;
import org.ujmp.core.calculation.Calculation.Ret;
/**
* The Hungary method solving allocating problem.
* @author Yanxue
*
*/
public class Hungary {
Matrix graph;
int n, m;
//int minMatchValue;
Matrix mapMatrix;
int[] mapIndices;
public static final int MAX_ITE_NUM = 1000;
public Hungary(Matrix pGraph) {
graph = pGraph.plus(Ret.NEW, false, 0);
n = (int) pGraph.getRowCount();
m = (int) pGraph.getColumnCount();
if (n != m) {
graphSqureChange();
}
}
private void graphSqureChange() {
if (n < m) {
graph = graph.appendVertically(Ret.LINK,
Matrix.Factory.zeros(m - n, m));
} else {
graph = graph.appendHorizontally(Ret.LINK,
Matrix.Factory.zeros(n, n - m));
}
n = (int) graph.getRowCount();
m = n;
}
public void findMinMatch() {
// Compute C'
Matrix rowMinValue = graph.min(Ret.NEW, 1);
Matrix tC = Matrix.Factory.emptyMatrix();
for (int i = 0; i < n; i++) {
tC = tC.appendVertically(Ret.LINK, graph.selectRows(Ret.LINK, i)
.minus(rowMinValue.getAsInt(i, 0)));
}
Matrix columnMinValue = tC.min(Ret.NEW, 0);
Matrix _tC = Matrix.Factory.emptyMatrix();
for (int i = 0; i < m; i++) {
_tC = _tC.appendHorizontally(
Ret.LINK,
tC.selectColumns(Ret.LINK, i).minus(
columnMinValue.getAsInt(0, i)));
}
//System.out.println("C(1) computed");
Matrix tMapMatrix = constructMapAndUpdate(_tC)[0];
int tCount = 0;
while (!isOptimal(tMapMatrix) && tCount++ < MAX_ITE_NUM) {
Matrix[] tMatrix = constructMapAndUpdate(_tC);
tMapMatrix = tMatrix[0];
_tC = tMatrix[1];
}
mapMatrix = tMapMatrix;
mapIndices = new int[n];
Arrays.fill(mapIndices, -1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if(mapMatrix.getAsInt(i, j) == 1) {
mapIndices[i] = j;
break;
}
}
}
}
private Matrix[] constructMapAndUpdate(Matrix c) {
Matrix tMap = Matrix.Factory.zeros(n, m);
Matrix updateC = c.plus(Ret.NEW, false, 0);
int[][] rowZeroIndices = getRowZeroIndices(c);
int[] indexSequence = findMinToMaxRowZeroCountIndexSequence(rowZeroIndices);
boolean[] rowComputed = new boolean[n];
boolean[] columnComputed = new boolean[m];
for (int i = 0; i < n; i++) {
int currentRow = indexSequence[i];
for (int j = 0; j < rowZeroIndices[currentRow].length; j++) {
if (!columnComputed[rowZeroIndices[currentRow][j]]) {
tMap.setAsInt(1, currentRow, rowZeroIndices[currentRow][j]);
columnComputed[rowZeroIndices[currentRow][j]] = true;
// 1) Flag for having bracket.
rowComputed[currentRow] = true;
break;
}
}
}
//System.out.println("C(1)\r\n" + tMap);
if (isOptimal(tMap)) {
return new Matrix[] { tMap, updateC };
}
// C' --> C''
boolean[] rowFlag = new boolean[n];
// 1)
for (int i = 0; i < n; i++) {
rowFlag[i] = !rowComputed[i];
}
//System.out.println("C(1): " + Arrays.toString(rowFlag));
boolean[] columnFlag = new boolean[m];
boolean[] _rowFlag = new boolean[n];
boolean[] _columnFlag = new boolean[m];
while (!Arrays.equals(_rowFlag, rowFlag)
|| !Arrays.equals(_columnFlag, columnFlag)) {
_rowFlag = rowFlag;
_columnFlag = columnFlag;
// 2) Flag column indices for all the zero elements in those
// bracket-flaged row.
for (int i = 0; i < n; i++) {
// flaged row
if (rowFlag[i]) {
for (int j = 0; j < rowZeroIndices[i].length; j++) {
columnFlag[rowZeroIndices[i][j]] = true;
}
}
}
//System.out.println("C(1)" + Arrays.toString(columnFlag));
// 3) Flag row indices for those bracket-flaged elements in flaged
// columns.
for (int i = 0; i < m; i++) {
if (columnFlag[i]) {
for (int j = 0; j < n; j++) {
if (tMap.getAsInt(j, i) == 1) {
rowFlag[j] = true;
break;
}
}
}
}
}
// 5) Find minimum element in those locations uncovered by lines.
int tMinValue = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
// skip row Lines
if (!rowFlag[i]) {
continue;
}
for (int j = 0; j < m; j++) {
if (!columnFlag[j]) {
if (c.getAsInt(i, j) < tMinValue) {
tMinValue = c.getAsInt(i, j);
}
}
}
}
// 6) Minus the minimum value for those flaged rows.
for (int i = 0; i < n; i++) {
if (rowFlag[i]) {
for (int j = 0; j < m; j++) {
updateC.setAsInt(updateC.getAsInt(i, j) - tMinValue, i, j);
}
}
}
// 6) Plus the minimum value for those flaged columns.
for (int i = 0; i < m; i++) {
if (columnFlag[i]) {
for (int j = 0; j < n; j++) {
updateC.setAsInt(updateC.getAsInt(j, i) + tMinValue, j, i);
}
}
}
return new Matrix[] { tMap, updateC };
}
private int[] findMinToMaxRowZeroCountIndexSequence(int[][] rowZeroIndices) {
int[] tSequence = new int[n];
int tIndex = 0;
boolean[] rowComputed = new boolean[n];
while (tIndex < n) {
int minZeroCountIndex = 0;
int minZeroCount = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
if (rowComputed[i]) {
continue;
}
if (rowZeroIndices[i].length < minZeroCount) {
minZeroCount = rowZeroIndices[i].length;
minZeroCountIndex = i;
}
}
tSequence[tIndex++] = minZeroCountIndex;
rowComputed[minZeroCountIndex] = true;
}
return tSequence;
}
private int[][] getRowZeroIndices(Matrix c) {
int[][] tRowZeroIndices = new int[n][];
int[] tRowZeroCounts = new int[n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (c.getAsInt(i, j) == 0) {
tRowZeroCounts[i]++;
}
}
}
for (int i = 0; i < n; i++) {
tRowZeroIndices[i] = new int[tRowZeroCounts[i]];
tRowZeroCounts[i] = 0;
for (int j = 0; j < m; j++) {
if (c.getAsInt(i, j) == 0) {
tRowZeroIndices[i][tRowZeroCounts[i]++] = j;
}
}
}
return tRowZeroIndices;
}
/**
* Judge if the map matrix is optimal.
*
* @param mapC
* @return
*/
private boolean isOptimal(Matrix mapC) {
return mapC.sum(Ret.NEW, Matrix.ALL, false).getAsInt(0, 0) == n;
}
public int[] getMapIndices() {
return mapIndices;
}
/**
Testing method.
**/
public static void main(String[] args) {
int[][] m = null;
m = new int[][]{
{ 12, 7, 9, 7, 9 },
{ 8, 9, 6, 6, 6 },
{ 7, 17, 12, 14, 9 },
{ 15, 14, 6, 6, 10 },
{ 4, 10, 7, 10, 9 }
};
m = new int[][]{
{2, 15, 13, 4},
{10, 4, 14, 15},
{9, 14, 16, 13},
{7, 8, 11, 9},
};
Matrix mMatrix = Matrix.Factory.zeros(m.length, m[0].length);
for (int i = 0; i < m.length; i++) {
for (int j = 0; j < m[i].length; j++) {
mMatrix.setAsInt(m[i][j], i, j);
}
}
Hungary h = new Hungary(mMatrix);
h.findMinMatch();
System.out.println(h.mapMatrix);
System.out.println(Arrays.toString(h.mapIndices));
}
}
在使用这个算法的时候,需要注意以下2点:
1. UJMP三方库是必不可少的,这里面涉及到矩阵运算,下载链接https://ujmp.org/;
2. 这个算法解决的是极小化的指派问题,如需计算极大化问题的最优解( AC 就是极大化问题),需要将 W 转化为
W′={w′ij}k×s,w′ij=max(W)−wij , max(W) 是矩阵 W 中的最大值。这样转化之后的极小化问题的最优解等于原问题的最优解。
计算 AC 的时候,只需要拿到这个匹配, W 矩阵中对应的数相加,再除以样本总数,就可以了。
关于这个算法还有Matlab实现,可参见
http://www.cad.zju.edu.cn/home/dengcai/Data/code/hungarian.m
5) NMI
NMI 为归一化的互信息,给定两个随机变量 P 和 Q , P,Q 之间的NMI由下式给出:
我们再谈一谈两个内部评价指标,内部的评价指标并没有利用到真实的标签,或者说,内部的评价指标反应了预测簇本身的内聚性,或者反应了簇间的独立性。考虑聚类结果的簇划分 C={Ci}ki=1 ,定义
6) DBI
注意, DBI 反应了簇间的独立性与簇的内聚性,越小越好。
7) DI