引言
对数回归(LogisticRegression)、支持向量机(SVM)等机器学习算法可以对二元数据集进行分类,但是无法处理超过 2 个目标类标签的多类分类任务。对于多类分类或多标签分类任务,我们需要使用某些技巧或者其他机器学习算法来训练数据集。例如OVO、OVR、ECOC、Softmax等。本文重点介绍ECOC算法。
算法介绍
纠错输出代码 (ECOC) 是一种强大的技术,用于机器学习和数据分类任务。它们通过将多类问题转换为一系列二元分类问题,作为一种增强多类分类器性能的方法。在处理传统分类器可能难以实现高精度的复杂数据集时,这种方法特别有用。通过使用唯一的二进制代码对每个类进行编码,ECOC 允许更稳健的决策过程,从而有效减少分类错误。
ECOC 的基本原理是将二进制代码分配给多类分类问题中的每个类。例如,如果有三个类,ECOC 可能会将代码 00、01 和 10 分别分配给每个类。在训练阶段,训练多个二元分类器,每个分类器负责区分特定的类子集。这种策略不仅提高了整体分类准确性,而且还提供了一种纠错机制,因为系统可以根据编码输出识别和纠正误分类。
使用纠错输出代码的主要优势之一是它们能够处理嘈杂的数据。在实际应用程序中,数据集通常包含错误或异常值,这些错误或异常值可能会对分类器的性能产生不利影响。ECOC 通过利用分配给每个类的二进制代码中的冗余来缓解此问题。如果分类器对实例进行了错误分类,ECOC 框架仍然可以通过分析其他分类器的输出及其相应的代码来恢复正确的分类,从而提高系统的噪声弹性。
ECOC 的另一个重要方面是它的可扩展性。随着类数量的增加,由于决策边界的复杂性,传统的多类分类器可能会变得不那么有效。ECOC 通过将问题分解为可管理的二元分类任务来应对这一挑战,从而实现更高效的训练和推理过程。这种可扩展性使 ECOC 成为图像识别和自然语言处理等大规模应用的有吸引力的选择。
尽管有其优势,但实施纠错输出代码确实带来了挑战。需要训练多个二元分类器可能会导致计算成本增加和训练时间延长,尤其是对于大型数据集。此外,二进制代码矩阵的设计需要仔细调整以确保最佳性能。然而,随着计算能力和算法效率的进步,这些挑战变得越来越可控。
Sklearn的multiclass系列库能够支持OvO、OvR和ECOC,在 ECOC 中,“通过距离度量解码最终类别” 是预测阶段的核心步骤。为了更直观地理解这一过程,我们通过一个完整的例子逐步说明其原理和计算细节。
1. 编码矩阵(Code Matrix)的结构
假设我们有一个4 类别的分类任务,并使用 3 个二分类器(编码长度为 3,code_size=3),编码矩阵可能设计如下:
类别 | 分类器1 | 分类器2 | 分类器3 |
类别0 | +1 | +1 | +1 |
类别1 | +1 | -1 | -1 |
类别2 | -1 | +1 | -1 |
类别3 | -1 | -1 | +1 |
- 每一行 对应一个类别的编码(如类别0的编码是 [+1, +1, +1])。
- 每一列对应一个二分类器的训练任务(如分类器1的任务是判断样本是否属于“类别0或1” vs “类别2或3”)。
2. 预测阶段的核心步骤
当输入一个测试样本时,ECOC 的预测过程如下:
步骤1:获取所有二分类器的预测结果
假设测试样本经过 3 个二分类器的预测结果为:
- 分类器1:+1
- 分类器2:-1
- 分类器3:+1
则测试样本的编码向量为 [+1, -1, +1]。
步骤2:计算测试编码与每个类别的编码距离
使用 汉明距离(Hamming Distance)或 欧氏距离(Euclidean Distance) 比较测试编码与编码矩阵中每一行的差异。
以汉明距离为例(统计不一致的位数):
- 类别0:[+1, +1, +1] vs [+1, -1, +1] → 不一致的位数 = 1(仅第2位不同) → 距离=1
- 类别1:[+1, -1, -1] vs [+1, -1, +1] → 不一致的位数 = 1(第3位不同) → 距离=1
- 类别2:[-1, +1, -1] vs [+1, -1, +1] → 不一致的位数 = 3(全不同) → 距离=3
- 类别3:[-1, -1, +1] vs [+1, -1, +1] → 不一致的位数 = 1(第1位不同) → 距离=1
步骤3:选择距离最小的类别作为预测结果
- 类别0、类别1、类别3 的距离均为 1,类别2 的距离为 3。
- 此时可能出现 平局(如多个类别的距离相同),可通过随机选择或更精细的距离度量(如欧氏距离)解决。
- 最终预测结果可能是类别0、类别1或类别3中的任意一个(实际应用中可通过调整编码矩阵或分类器设计避免平局)。
3. 关键细节解析
(1) 为什么用距离度量?
容错性:即使某些二分类器预测错误,最终的编码距离可能仍然更接近真实类别。
示例:假设真实类别为0(编码 [+1, +1, +1]),但分类器2预测错误(本应为 +1,错误输出 -1)。此时测试编码为 [+1, -1, +1],与类别0的汉明距离为1,仍可能正确分类。
(2) 汉明距离 vs 欧氏距离
距离类型 | 计算方式 | 适用场景 |
汉明距离 | 统计不一致的位数(0-1差异) | 适用于二进制编码(如 +1/-1) |
欧氏距离 | 计算向量间的几何距离 | 适用于连续值编码(概率输出) |
(3) 三进制编码(含 0 的编码)
如果编码矩阵包含 0(表示某些二分类器忽略该类别),计算距离时通常 忽略 0对应的位。
示例:
- 类别编码为 [+1, 0, -1],测试编码为 [+1, -1, -1]。
- 计算距离时,仅比较第1位和第3位(第2位被忽略),汉明距离为 0(一致)。
4. 实际应用中的改进策略
1. 优化编码矩阵:
- 设计编码矩阵时,确保不同类别的编码行之间的汉明距离最大化(提高容错能力)。
- 例如,使用随机生成 + 筛选,或基于纠错码理论(如 Reed-Solomon 码)生成。
2. 动态权重调整:
- 为每个二分类器赋予不同的权重(如根据分类器的准确率调整),加权计算距离。
3. 概率解码:
- 如果基分类器输出概率(而非硬标签),可用概率值加权距离计算(如用概率代替 +1/-1)。
实例演示
作者先用sklearn库中的make_classification生成一个多类数据集(10个feature,4个类别),代码如下:
import numpy as np
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
#生成一个分类的数据集
X, y = make_classification(
n_samples=2000, # 样本数量
n_features=10, # 特征数量
n_informative=6, # 用于构建模型的有用特征数量
n_redundant=1, # 冗余特征数量,这些特征是信息特征的随机组合
n_repeated=1, # 重复特征数量
n_classes=4, # 类别数量
weights=[0.4, 0.3, 0.2, 0.1], # 每个类别的样本比例,均衡分布
flip_y=0.1, # 样本的噪声程度,值越小噪声越小
class_sep=1.5, # 类别之间的分离程度,值越大类别越容易区分
random_state=42
)
# 数据标准化
scaler = StandardScaler()
X = scaler.fit_transform(X)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=10)
然后用调优后的随机森林作为基本模型,把随机森林模型进行调优(超参优化),调优后的随机森林模型用结合ECOC算法进行多类划分,这个时候OutputCodeClassifier函数的code_rate参数配置为0.5,这个模型的平均得分是0.83.
from sklearn.multiclass import OutputCodeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RepeatedStratifiedKFold,cross_validate,GridSearchCV
cv = RepeatedStratifiedKFold(n_splits=3,n_repeats=5,random_state=0)
#m随机森林的max_depth参数作为调优超参
param_grid = {"max_depth":[3,5,7,9,11,13]}
#选择log_loss作为随机森林模型分支测量的标准
RandomForest = RandomForestClassifier(criterion='log_loss',min_samples_split=10,min_samples_leaf=5)
#GridSearchCV函数完成超参优化
model_optimized = GridSearchCV(RandomForest, param_grid=param_grid, cv=3)
#用OutputCodeClassifier函数完成多类编码
clf = OutputCodeClassifier(model_optimized,code_size = 0.5,random_state=10)
#用cross_validate函数完成交叉验证
cv_results_RF = cross_validate(model_optimized, X, y, cv=cv, n_jobs=2)
cv_results_RF['test_score'].mean()
笔者开始好奇 OutputCodeClassifier函数的code_rate参数配置对模型会有什么影响,用如下代码做了测试,发现code_rate=1的时候,模型得分是最低的:
code_size = [0.5,1,1.5,2,3,4,5,20]
for cs in code_size:
clf = OutputCodeClassifier(model_optimized,code_size = cs,random_state=10)
cv_results_RF = cross_validate(clf, X, y, cv=2, n_jobs=2)
print("code_size=%.1f,average test_score:%.2f"%(cs,cv_results_RF['test_score'].mean()))
运行结果如下:
code_size=0.5,average test_score:0.82
code_size=1.0,average test_score:0.77
code_size=1.5,average test_score:0.80
code_size=2.0,average test_score:0.82
code_size=3.0,average test_score:0.81
code_size=4.0,average test_score:0.81
code_size=5.0,average test_score:0.81
笔者怀疑是随机森林模型的问题,然后又用logisticregression模型运行,得到一样的规律:code_size=0.5的时候还比较好,code_size=1开始恶化,code_size>=2后基本平稳。因为OutputCodeClassifier函数用参数random_state随机生成代表N个类别的伪随机序列,序列长度用code_size控制,比如演示的这个数据集有4个类别,code_size=0.5,那么代表每个类别的随机序列长度是4*0.5=2,如果code_size=1,序列长度是4*1=4。所以笔者怀疑是4个序列的汉明距离性能不好导致的模型性能下降。
clf = OutputCodeClassifier(model_optimized,code_size = 1,random_state=10)
用如下代码导出code_size = 1,random_state=10的4个序列组
clf = OutputCodeClassifier(model_optimized,code_size = 1,random_state=10)
clf.fit(X_train,y_train)
print(clf.code_book_)
笔者分别计算3组序列的汉明距离,第一组是random_state=10,产生的随机序列,第二组是每个分类器置2个1,第三组是每个分类器置1个1,笔者理解第三组分类序列相当于独热码编码,算法效果相当于OvR算法。
计算这个序列的汉明距离:
from itertools import combinations
# 示例输入(4个4bit序列)
sequences0 = [
'1011',
'0001',
'0011',
'0111'
]
sequences1 = [
'1001',
'1100',
'0110',
'0011'
]
sequences2 = [
'1000',
'0100',
'0010',
'0001'
]
# 汉明距离计算函数
def hamming_distance(s1, s2):
if len(s1) != len(s2):
raise ValueError("序列长度必须相同")
return sum(c1 != c2 for c1, c2 in zip(s1, s2))
# 输出格式化为矩阵
print("\nSeq0汉明距离矩阵:")
n = len(sequences0)
matrix = [[0]*n for _ in range(n)]
for i in range(n):
for j in range(n):
matrix[i][j] = hamming_distance(sequences0[i], sequences0[j])
for row in matrix:
print(' '.join(f'{num:2}' for num in row))
print("\nSeq1汉明距离矩阵:")
n = len(sequences1)
matrix = [[0]*n for _ in range(n)]
for i in range(n):
for j in range(n):
matrix[i][j] = hamming_distance(sequences1[i], sequences1[j])
for row in matrix:
print(' '.join(f'{num:2}' for num in row))
print("\nSeq2汉明距离矩阵:")
n = len(sequences2)
matrix = [[0]*n for _ in range(n)]
for i in range(n):
for j in range(n):
matrix[i][j] = hamming_distance(sequences2[i], sequences2[j])
for row in matrix:
print(' '.join(f'{num:2}' for num in row))
计算结果:
Seq0汉明距离矩阵:
0 2 1 2
2 0 1 2
1 1 0 1
2 2 1 0
Seq1汉明距离矩阵:
0 2 4 2
2 0 2 4
4 2 0 2
2 4 2 0
Seq2汉明距离矩阵:
0 2 2 2
2 0 2 2
2 2 0 2
2 2 2 0
从三组类别序列的汉明距离矩阵计算结果看,OutputCodeClassifier随机产生的序列码并非最优的。当序列长度随着code_size的增大,平均汉明距离加大,模型性能也随之提升,但是模型性能也不会随code_size持续增长,从上面的4类预测结果看,当code_size>2,模型得分基本趋于稳定,模型最大性能会受到模型算法/参数和数据集的影响。
总结
ECOC 的“距离度量解码”本质是 通过比较测试样本的二分类器输出与编码矩阵的每一行,找到最接近的类别编码。这种方法的优势在于:
- 冗余编码允许部分分类器预测错误,但仍能通过整体距离纠正结果。
- 灵活支持不同编码设计和距离度量方法,适用于复杂分类任务。
如果数据集的类别比较少,用OutputCodeClassifier随机生成的类别序列性能不是最佳的汉明距离序列组,建议加大code_size的配置。