多标签与多输出学习
传统的机器学习任务通常只预测单个目标:一个类别标签或一个连续值。但在实际应用中,经常需要同时预测多个相关目标。例如,预测一篇文章可能同时属于多个主题,或者预测一个地区的人口和GDP。sklearn 提供了专门处理这类多学习问题的模块。
问题类型概述
多学习问题根据目标数量和目标基数可分为四类:
| 问题类型 | 目标数量 | 目标基数 | 示例 |
|---|---|---|---|
| 多分类(Multiclass) | 1 | >2 | 识别水果是苹果、橙子还是梨 |
| 多标签分类(Multilabel) | >1 | 2(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 个类别,训练 个二分类器:
- 分类器 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 | 大多数分类器原生支持 |
| 多标签分类 | MultiOutputClassifier、ClassifierChain | 样本可属于多个类别 |
| 多输出回归 | 原生支持或 MultiOutputRegressor | 同时预测多个连续值 |
选择建议:
- 多分类:直接使用原生支持多分类的模型,如逻辑回归、随机森林
- 多标签分类:
- 标签独立:使用
MultiOutputClassifier - 标签相关:使用
ClassifierChain
- 标签独立:使用
- 多输出回归:
- 优先使用原生支持的模型(随机森林、线性回归等)
- 目标相关时考虑
RegressorChain
注意事项:
- 多标签分类的评估指标与单标签不同,推荐使用汉明损失和 F1 分数
- 链式方法的效果取决于链顺序,建议尝试多个随机顺序
- 并行计算(
n_jobs=-1)可以加速多输出模型的训练