手把手教你用Python计算聚类指标:从混淆矩阵到ARI/AMI/ACC的完整推导
手把手教你用Python计算聚类指标:从混淆矩阵到ARI/AMI/ACC的完整推导
在机器学习领域,聚类分析作为无监督学习的重要分支,其效果评估一直是研究者和实践者关注的焦点。当我们使用sklearn等工具包时,常常会调用adjusted_rand_score()或adjusted_mutual_info_score()等函数快速获得评估结果,但你是否真正理解这些指标背后的数学逻辑?本文将带你从最基础的混淆矩阵出发,逐步推导ARI、AMI和ACC三大核心聚类指标的计算过程,让你不仅知其然,更知其所以然。
1. 基础准备:理解混淆矩阵与聚类评估
聚类评估的核心在于比较算法输出与真实标签(如有)或内部结构的一致性。混淆矩阵(Contingency Table)作为基础工具,记录了聚类结果与真实类别之间的样本分布关系。让我们从一个简单的例子开始:
假设我们对9个样本进行聚类,真实类别和聚类结果如下:
| 样本编号 | 真实类别 | 聚类结果 |
|---|---|---|
| 1 | A | A |
| 2 | A | C |
| 3 | A | C |
| 4 | B | C |
| 5 | B | B |
| 6 | B | C |
| 7 | C | B |
| 8 | C | B |
| 9 | C | C |
构建混淆矩阵时,行代表真实类别,列代表聚类结果,单元格n_ij表示真实类别i被划分到聚类j的样本数:
import numpy as np confusion_matrix = np.array([ [1, 0, 2], # 真实类别A:1个在聚类A,0个在B,2个在C [0, 1, 2], # 真实类别B [0, 2, 1] # 真实类别C ])这个矩阵将成为我们计算所有指标的基础。值得注意的是,在无监督学习中,如果真实标签不可得,我们则需要依赖轮廓系数等内部评估指标。
2. 调整兰德指数(ARI)的完整推导
ARI衡量的是两个数据分布之间的一致性,修正了随机分配的影响。其核心思想是比较实际配对与期望配对的相似度。
2.1 配对统计基础
首先定义几个关键变量:
- a_i: 聚类结果中第i类的样本数(行和)
- b_j: 真实类别中第j类的样本数(列和)
- n_ij: 真实i类且聚类j类的样本数
- n: 总样本数
从我们的例子可得:
a = np.sum(confusion_matrix, axis=1) # [3, 3, 3] b = np.sum(confusion_matrix, axis=0) # [1, 3, 5] n = np.sum(a) # 92.2 组合数计算
ARI公式中的组合数计算如下:
$$ ARI = \frac{\sum_{ij} \binom{n_{ij}}{2} - [\sum_i \binom{a_i}{2} \sum_j \binom{b_j}{2}] / \binom{n}{2}}{\frac{1}{2} [\sum_i \binom{a_i}{2} + \sum_j \binom{b_j}{2}] - [\sum_i \binom{a_i}{2} \sum_j \binom{b_j}{2}] / \binom{n}{2}} $$
用Python实现组合计算:
from math import comb def compute_ari(confusion_matrix): # 计算各项组合数 sum_comb_nij = sum(comb(n_ij, 2) for row in confusion_matrix for n_ij in row) sum_comb_ai = sum(comb(a_i, 2) for a_i in np.sum(confusion_matrix, axis=1)) sum_comb_bj = sum(comb(b_j, 2) for b_j in np.sum(confusion_matrix, axis=0)) comb_n = comb(np.sum(confusion_matrix), 2) # 计算ARI分子和分母 numerator = sum_comb_nij - (sum_comb_ai * sum_comb_bj) / comb_n denominator = 0.5 * (sum_comb_ai + sum_comb_bj) - (sum_comb_ai * sum_comb_bj) / comb_n return numerator / denominator2.3 结果验证
与我们例子中的sklearn输出对比:
from sklearn.metrics import adjusted_rand_score labels_true = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'] labels_pred = ['A', 'C', 'C', 'C', 'B', 'C', 'B', 'B', 'C'] print(f"Sklearn ARI: {adjusted_rand_score(labels_true, labels_pred):.3f}") print(f"Manual ARI: {compute_ari(confusion_matrix):.3f}")输出结果均为-0.032,验证了我们的推导。
3. 调整互信息(AMI)的数学原理与实现
AMI基于信息论中的互信息概念,通过熵来衡量两个分类系统的一致性。
3.1 熵与联合分布计算
首先计算真实类别分布U和聚类结果分布V的熵:
def entropy(probs): return -np.sum(p * np.log2(p) for p in probs if p > 0) # 计算真实类别熵H(U) probs_u = np.sum(confusion_matrix, axis=1) / n h_u = entropy(probs_u) # 计算聚类结果熵H(V) probs_v = np.sum(confusion_matrix, axis=0) / n h_v = entropy(probs_v)3.2 互信息计算
互信息衡量两个分布的相互依赖程度:
$$ MI(U,V) = \sum_{i=1}^{|U|} \sum_{j=1}^{|V|} \frac{n_{ij}}{n} \log \left( \frac{n_{ij}/n}{a_i b_j / n^2} \right) $$
Python实现:
def mutual_info(confusion_matrix, n): mi = 0.0 for i in range(confusion_matrix.shape[0]): for j in range(confusion_matrix.shape[1]): n_ij = confusion_matrix[i,j] if n_ij > 0: a_i = np.sum(confusion_matrix[i,:]) b_j = np.sum(confusion_matrix[:,j]) term = (n_ij / n) * np.log2((n_ij * n) / (a_i * b_j)) mi += term return mi3.3 期望互信息与AMI计算
AMI通过调整随机期望来标准化互信息:
def expected_mi(confusion_matrix, n): # 简化计算,实际实现应考虑更精确的期望计算 a = np.sum(confusion_matrix, axis=1) b = np.sum(confusion_matrix, axis=0) term1 = np.sum(comb(a_i, 2) for a_i in a) / comb(n, 2) term2 = np.sum(comb(b_j, 2) for b_j in b) / comb(n, 2) return np.log2(n) - (1/n) - (1 - term1 - term2) def compute_ami(confusion_matrix, n): mi = mutual_info(confusion_matrix, n) emi = expected_mi(confusion_matrix, n) h_u = entropy(np.sum(confusion_matrix, axis=1) / n) h_v = entropy(np.sum(confusion_matrix, axis=0) / n) return (mi - emi) / (max(h_u, h_v) - emi)验证结果与sklearn一致:
from sklearn.metrics import adjusted_mutual_info_score print(f"Sklearn AMI: {adjusted_mutual_info_score(labels_true, labels_pred):.3f}") print(f"Manual AMI: {compute_ami(confusion_matrix, n):.3f}")4. 聚类准确率(ACC)的特殊处理
ACC看似简单,但在聚类中需要特殊处理标签对应问题。
4.1 标签对齐问题
由于聚类标签是任意的,直接计算准确率会得到错误结果。我们需要找到最优的标签映射:
from itertools import permutations from sklearn.metrics import accuracy_score def cluster_accuracy(y_true, y_pred): # 获取唯一标签 true_labels = np.unique(y_true) pred_labels = np.unique(y_pred) # 生成所有可能的映射 best_acc = 0 for mapping in permutations(true_labels, len(pred_labels)): mapped_pred = [mapping[np.where(pred_labels == p)[0][0]] for p in y_pred] acc = accuracy_score(y_true, mapped_pred) if acc > best_acc: best_acc = acc return best_acc4.2 实际应用示例
# 将字符标签转换为数字 label_map = {'A':0, 'B':1, 'C':2} y_true = [label_map[x] for x in labels_true] y_pred = [label_map[x] for x in labels_pred] print(f"Direct ACC: {accuracy_score(y_true, y_pred):.3f}") print(f"Optimal ACC: {cluster_accuracy(y_true, y_pred):.3f}")这个例子展示了为什么简单的准确率在聚类中可能产生误导,以及如何正确计算聚类准确率。
5. 指标特性与使用场景对比
不同聚类评估指标各有特点,理解它们的数学本质有助于在实际应用中选择合适的指标:
| 指标 | 取值范围 | 随机预期值 | 适用场景 | 优缺点 |
|---|---|---|---|---|
| ARI | [-1,1] | 0 | 有真实标签 | 对随机分配有修正,对称性度量 |
| AMI | [0,1] | 0 | 有真实标签 | 考虑信息熵,适合不平衡分类 |
| ACC | [0,1] | 随机概率 | 有真实标签 | 直观但需标签对齐,对不平衡敏感 |
注意:当真实标签不可得时,应考虑轮廓系数、Calinski-Harabasz指数等内部评估指标。
在实际项目中,我通常会同时计算多个指标来全面评估聚类效果。特别是在处理高维数据时,AMI往往能更敏感地捕捉到聚类质量的变化。而调试算法参数时,理解这些指标的计算过程能帮助更快定位问题所在。
