跳到主要内容

多标签与多输出学习

传统的机器学习任务通常只预测单个目标:一个类别标签或一个连续值。但在实际应用中,经常需要同时预测多个相关目标。例如,预测一篇文章可能同时属于多个主题,或者预测一个地区的人口和GDP。sklearn 提供了专门处理这类多学习问题的模块。

问题类型概述

多学习问题根据目标数量和目标基数可分为四类:

问题类型目标数量目标基数示例
多分类(Multiclass)1>2识别水果是苹果、橙子还是梨
多标签分类(Multilabel)>12(0或1)文章同时属于"科技"和"创业"
多分类-多输出>1>2预测水果类型和颜色
多输出回归>1连续值预测房价和租金

目标基数指每个目标变量可能的取值数量。多标签分类中,每个标签只有"属于"或"不属于"两种可能,所以基数是2。

多分类问题

多分类是指有三个或更多类别的分类任务,每个样本只能属于一个类别。好消息是:sklearn 所有分类器都原生支持多分类,无需使用元估计器,除非你想尝试不同的多分类策略。

一对多策略(One-vs-Rest)

一对多策略为每个类别训练一个二分类器,将该类别与其他所有类别区分开。这是最常用的多分类策略。

工作原理

假设有 K 个类别,训练 K 个二分类器:

  • 分类器 1:类别 1 vs 其他
  • 分类器 2:类别 2 vs 其他
  • ...
  • 分类器 K:类别 K vs 其他

预测时,选择输出置信度最高的类别。

from sklearn import datasets
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import LinearSVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# 加载数据
X, y = datasets.load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 一对多策略
ovr = OneVsRestClassifier(LinearSVC(random_state=42))
ovr.fit(X_train, y_train)

print(f"分类器数量: {len(ovr.estimators_)}") # 3个(3个类别)
print(f"\n测试集准确率: {ovr.score(X_test, y_test):.2%}")

# 每个分类器的系数
for i, clf in enumerate(ovr.estimators_):
print(f"\n分类器 {i} (类别 {i} vs 其他) 系数:")
print(clf.coef_)

优点

  • 只需要 K 个分类器,计算效率高
  • 每个分类器对应一个类别,可解释性强
  • 支持概率输出

缺点

  • 每个分类器都需要处理全部训练数据
  • 类别不平衡时可能影响性能

一对一策略(One-vs-One)

一对一策略为每对类别训练一个二分类器,通过投票决定最终类别。

工作原理

对于 K 个类别,训练 K(K1)/2K(K-1)/2 个二分类器:

  • 分类器 1:类别 1 vs 类别 2
  • 分类器 2:类别 1 vs 类别 3
  • ...

预测时,每个分类器投票,得票最多的类别胜出。

from sklearn.multiclass import OneVsOneClassifier

# 一对一策略
ovo = OneVsOneClassifier(LinearSVC(random_state=42))
ovo.fit(X_train, y_train)

print(f"分类器数量: {len(ovo.estimators_)}") # 3个类别 -> 3个分类器
print(f"测试集准确率: {ovo.score(X_test, y_test):.2%}")

优点

  • 每个分类器只使用两个类别的数据,训练更快
  • 对于核方法等不随样本数扩展良好的算法更有利
  • 在某些数据集上准确率更高

缺点

  • 分类器数量随类别数平方增长
  • 不支持概率输出

策略选择

场景推荐策略
默认选择一对多
核方法(SVM)一对一
类别数较多一对多
需要概率输出一对多
样本量大、类别少一对一可能更快

原生支持多分类的分类器

以下分类器原生支持多分类,不需要元估计器:

# 原生多分类支持
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB

# 这些模型可以直接处理多分类
lr = LogisticRegression(multi_class='multinomial') # 多项式逻辑回归
rf = RandomForestClassifier()
knn = KNeighborsClassifier()
nb = GaussianNB()

多标签分类

多标签分类中,每个样本可以同时属于多个类别。例如:

  • 一篇文章可能同时是"科技"、"创业"、"AI"
  • 一部电影可能同时是"动作"、"科幻"、"冒险"
  • 一张图片可能同时包含"人"、"车"、"建筑"

目标格式

多标签目标是一个二元矩阵,每行代表一个样本,每列代表一个标签:

import numpy as np
from sklearn.preprocessing import MultiLabelBinarizer

# 原始标签列表
y_raw = [['科技', '创业'], ['娱乐', '电影'], ['科技', 'AI', '创业']]

# 转换为二元矩阵
mlb = MultiLabelBinarizer()
y = mlb.fit_transform(y_raw)

print("标签名称:", mlb.classes_)
print("二元矩阵:\n", y)
# [[1 0 1 0] 科技、创业
# [0 0 0 1] 电影(假设classes_顺序)
# [1 1 1 0]] 科技、AI、创业

MultiOutputClassifier

MultiOutputClassifier 为每个标签独立训练一个分类器:

from sklearn.datasets import make_multilabel_classification
from sklearn.multioutput import MultiOutputClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# 生成多标签数据
X, y = make_multilabel_classification(
n_samples=200,
n_features=20,
n_classes=3, # 3个可能的标签
n_labels=2, # 每个样本平均有2个标签
random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 多标签分类
clf = MultiOutputClassifier(
RandomForestClassifier(n_estimators=100, random_state=42),
n_jobs=-1 # 并行训练
)
clf.fit(X_train, y_train)

# 预测
y_pred = clf.predict(X_test)

print("预测结果(前5个样本):\n", y_pred[:5])
print("\n各标签准确率:")
for i in range(y.shape[1]):
acc = (y_pred[:, i] == y_test[:, i]).mean()
print(f" 标签 {i}: {acc:.2%}")

分类器链(ClassifierChain)

分类器链利用标签之间的相关性,将前一个分类器的预测结果作为后续分类器的特征。

为什么需要分类器链?

标签之间往往存在相关性。例如:

  • 如果文章是"AI",更可能是"科技"
  • 如果电影是"动作",更可能是"冒险"

分类器链可以捕获这种相关性,提升性能。

from sklearn.multioutput import ClassifierChain
from sklearn.linear_model import LogisticRegression

# 分类器链
chain = ClassifierChain(
LogisticRegression(max_iter=500),
order='random', # 随机顺序
random_state=42
)
chain.fit(X_train, y_train)

y_pred_chain = chain.predict(X_test)

print("链顺序:", chain.order_)
print("各分类器特征数:", [clf.n_features_in_ for clf in chain.estimators_])
# 可以看到后续分类器特征数增加(加入了前面分类器的预测)

链顺序的影响

链中第一个分类器没有其他标签信息,最后一个分类器拥有所有其他标签的预测。顺序不同,结果可能不同。

# 尝试多个随机链并平均
from sklearn.base import clone

n_chains = 5
chains = [
ClassifierChain(
LogisticRegression(max_iter=500),
order='random',
random_state=i
)
for i in range(n_chains)
]

# 训练所有链
for chain in chains:
chain.fit(X_train, y_train)

# 平均预测概率
y_probs = np.array([chain.predict_proba(X_test) for chain in chains])
y_prob_avg = y_probs.mean(axis=0)
y_pred_avg = (y_prob_avg > 0.5).astype(int)

print("集成预测结果:\n", y_pred_avg[:5])

多标签分类评估

from sklearn.metrics import (
accuracy_score,
hamming_loss,
f1_score,
precision_score,
recall_score
)

# 准确率(所有标签都正确才算正确)
acc = accuracy_score(y_test, y_pred)
print(f"准确率(严格): {acc:.2%}")

# 汉明损失(错误标签的比例)
hamming = hamming_loss(y_test, y_pred)
print(f"汉明损失: {hamming:.3f}")

# F1 分数
f1_micro = f1_score(y_test, y_pred, average='micro')
f1_macro = f1_score(y_test, y_pred, average='macro')
print(f"F1 (micro): {f1_micro:.3f}")
print(f"F1 (macro): {f1_macro:.3f}")

# 精确率和召回率
prec = precision_score(y_test, y_pred, average='micro')
rec = recall_score(y_test, y_pred, average='micro')
print(f"精确率: {prec:.3f}")
print(f"召回率: {rec:.3f}")

指标解释

  • 准确率:最严格,要求每个样本的所有标签都预测正确
  • 汉明损失:衡量单个标签的错误率,更适合多标签场景
  • F1 分数:平衡精确率和召回率
    • micro:全局计算,类别不平衡时偏向多数类
    • macro:各类别平均,每个类别权重相同

多输出回归

多输出回归同时预测多个连续目标变量。例如:

  • 预测房屋的价格和租金
  • 预测产品的销量和利润
  • 预测地区的温度和湿度

原生支持多输出的回归器

以下回归器原生支持多输出:

from sklearn.linear_model import LinearRegression, Ridge
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor

# 这些模型原生支持多输出
# 使用方法与单输出完全相同

MultiOutputRegressor

对于不支持多输出的回归器,使用 MultiOutputRegressor

from sklearn.datasets import make_regression
from sklearn.multioutput import MultiOutputRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score

# 生成多输出回归数据
X, y = make_regression(
n_samples=200,
n_features=20,
n_targets=3, # 3个输出目标
random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 多输出回归
mor = MultiOutputRegressor(
GradientBoostingRegressor(random_state=42),
n_jobs=-1
)
mor.fit(X_train, y_train)

y_pred = mor.predict(X_test)

# 各输出的评估
for i in range(y.shape[1]):
mse = mean_squared_error(y_test[:, i], y_pred[:, i])
r2 = r2_score(y_test[:, i], y_pred[:, i])
print(f"目标 {i}: MSE={mse:.2f}, R²={r2:.3f}")

回归器链

与分类器链类似,RegressorChain 利用目标之间的相关性:

from sklearn.multioutput import RegressorChain
from sklearn.linear_model import Ridge

# 回归器链
chain = RegressorChain(
Ridge(alpha=1.0),
order=[2, 0, 1] # 自定义顺序
)
chain.fit(X_train, y_train)

y_pred_chain = chain.predict(X_test)

print("链顺序:", chain.order_)

# 评估
r2 = r2_score(y_test, y_pred_chain, multioutput='uniform_average')
print(f"平均 R²: {r2:.3f}")

多输出回归评估

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

# 整体指标
mse = mean_squared_error(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"整体 MSE: {mse:.2f}")
print(f"整体 MAE: {mae:.2f}")
print(f"整体 R²: {r2:.3f}")

# 各输出单独评估
r2_per_output = r2_score(y_test, y_pred, multioutput='raw_values')
print(f"\n各输出 R²: {r2_per_output}")

# 各输出单独评估
r2_per_output = r2_score(y_test, y_pred, multioutput='raw_values')
print(f"\n各输出 R²: {r2_per_output}")

完整示例

多标签文本分类

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.multioutput import MultiOutputClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report
import numpy as np

# 模拟多标签数据(将原始单标签转为多标签)
categories = ['alt.atheism', 'comp.graphics', 'sci.med', 'soc.religion.christian']
train = fetch_20newsgroups(subset='train', categories=categories, random_state=42)
test = fetch_20newsgroups(subset='test', categories=categories, random_state=42)

# 将单标签转换为模拟的多标签
def to_multilabel(y, n_labels=2, random_state=42):
"""将单标签转换为多标签(模拟数据)"""
np.random.seed(random_state)
n_samples = len(y)
n_classes = len(np.unique(y))
y_multi = np.zeros((n_samples, n_classes), dtype=int)

for i, label in enumerate(y):
y_multi[i, label] = 1
# 随机添加额外标签
extra_labels = np.random.choice(
[j for j in range(n_classes) if j != label],
size=min(n_labels - 1, n_classes - 1),
replace=False
)
y_multi[i, extra_labels] = 1

return y_multi

y_train_multi = to_multilabel(train.target)
y_test_multi = to_multilabel(test.target)

# 创建管道
pipeline = Pipeline([
('tfidf', TfidfVectorizer(max_features=5000)),
('clf', MultiOutputClassifier(LogisticRegression(max_iter=1000, random_state=42)))
])

# 训练
pipeline.fit(train.data, y_train_multi)

# 预测
y_pred = pipeline.predict(test.data)

# 评估
print("多标签分类报告:")
print(classification_report(y_test_multi, y_pred, target_names=categories))

# 示例预测
print("\n示例预测:")
for i in range(3):
predicted_labels = [categories[j] for j, v in enumerate(y_pred[i]) if v == 1]
print(f"文本 {i+1}: {predicted_labels}")

多输出房价预测

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.multioutput import MultiOutputRegressor
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np

# 加载数据
housing = fetch_california_housing()
X = housing.data
y = housing.target

# 创建多个相关目标(模拟:房价 + 房租)
np.random.seed(42)
y_rent = y * 0.004 + np.random.normal(0, 0.1, len(y)) # 模拟房租数据
Y = np.column_stack([y, y_rent])

# 划分数据
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)

# 原生多输出模型
rf = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
rf.fit(X_train, Y_train)
Y_pred_rf = rf.predict(X_test)

print("随机森林(原生多输出):")
print(f" 目标1 R²: {r2_score(Y_test[:, 0], Y_pred_rf[:, 0]):.3f}")
print(f" 目标2 R²: {r2_score(Y_test[:, 1], Y_pred_rf[:, 1]):.3f}")

# 使用回归器链
from sklearn.multioutput import RegressorChain
from sklearn.linear_model import Ridge

chain = RegressorChain(Ridge(alpha=1.0))
chain.fit(X_train, Y_train)
Y_pred_chain = chain.predict(X_test)

print("\n回归器链:")
print(f" 目标1 R²: {r2_score(Y_test[:, 0], Y_pred_chain[:, 0]):.3f}")
print(f" 目标2 R²: {r2_score(Y_test[:, 1], Y_pred_chain[:, 1]):.3f}")

小结

多学习问题扩展了传统单目标学习的能力:

问题类型核心工具关键特点
多分类原生支持或 OneVsRestClassifier大多数分类器原生支持
多标签分类MultiOutputClassifierClassifierChain样本可属于多个类别
多输出回归原生支持或 MultiOutputRegressor同时预测多个连续值

选择建议

  1. 多分类:直接使用原生支持多分类的模型,如逻辑回归、随机森林
  2. 多标签分类
    • 标签独立:使用 MultiOutputClassifier
    • 标签相关:使用 ClassifierChain
  3. 多输出回归
    • 优先使用原生支持的模型(随机森林、线性回归等)
    • 目标相关时考虑 RegressorChain

注意事项

  • 多标签分类的评估指标与单标签不同,推荐使用汉明损失和 F1 分数
  • 链式方法的效果取决于链顺序,建议尝试多个随机顺序
  • 并行计算(n_jobs=-1)可以加速多输出模型的训练