半监督学习
半监督学习(Semi-Supervised Learning)是介于监督学习和无监督学习之间的一种学习方式。它利用少量的标注数据和大量的未标注数据进行模型训练,在实际应用中非常有价值,因为标注数据往往昂贵且耗时,而未标注数据则相对容易获取。
为什么需要半监督学习?
标注数据的困境
在实际项目中,获取大量高质量标注数据面临诸多挑战:
成本高昂:医学影像诊断需要专业医生标注,法律文档分类需要律师审核,这些都需要大量的人力成本。
时间消耗:标注过程本身耗时,特别是对于复杂任务(如语义分割、实体识别),一个样本可能需要数分钟甚至更长。
专家稀缺:某些领域需要专业知识才能准确标注,而具备这些知识的专家往往有限。
未标注数据的价值
未标注数据虽然不包含目标标签,但仍然蕴含着数据分布的信息。半监督学习的核心假设是:
平滑假设(Smoothness Assumption):如果两个样本在高密度区域中相近,它们的标签很可能相同。
聚类假设(Cluster Assumption):数据倾向于形成离散的簇,同一簇中的样本更可能属于同一类别。
流形假设(Manifold Assumption):高维数据往往聚集在低维流形附近,我们可以利用流形结构来推断标签。
半监督学习的适用场景
- 标注数据有限(几十到几千个样本)
- 未标注数据充足
- 数据分布满足上述假设
sklearn 中的半监督学习方法
sklearn 提供了多种半监督学习算法,主要分为以下几类:
| 方法 | 类型 | 基本思想 |
|---|---|---|
| SelfTrainingClassifier | 自训练 | 用已标注数据训练初始模型,预测未标注数据,将高置信度预测加入训练集 |
| LabelPropagation | 图方法 | 基于图的结构传播标签,相似样本共享标签 |
| LabelSpreading | 图方法 | LabelPropagation 的变体,使用正则化更稳健 |
| IterativeClassifier | 迭代方法 | 结合特征提取和分类的迭代过程 |
自训练(Self-Training)
自训练是最简单的半监督学习方法,其核心思想是"自我学习":用已有标注数据训练模型,预测未标注数据,将置信度高的预测结果加入训练集,重复迭代。
算法流程
- 用已标注数据训练初始分类器
- 用分类器预测未标注数据
- 选择置信度高于阈值的样本,将其预测标签作为"伪标签"
- 将这些样本加入训练集
- 重复步骤1-4直到收敛或达到最大迭代次数
基本用法
from sklearn.semi_supervised import SelfTrainingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import numpy as np
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 模拟半监督场景:只保留10%的标签
rng = np.random.RandomState(42)
random_unlabeled_points = rng.rand(len(y)) < 0.9 # 90%无标签
y_semi = np.copy(y)
y_semi[random_unlabeled_points] = -1 # -1表示无标签
print(f"有标签样本数: {np.sum(y_semi != -1)}")
print(f"无标签样本数: {np.sum(y_semi == -1)}")
# 创建自训练分类器
base_classifier = RandomForestClassifier(n_estimators=100, random_state=42)
self_training = SelfTrainingClassifier(
base_classifier,
threshold=0.9, # 置信度阈值
max_iter=10 # 最大迭代次数
)
# 训练(包含有标签和无标签数据)
self_training.fit(X, y_semi)
# 查看最终使用的标签
print(f"\n迭代次数: {self_training.n_iter_}")
print(f"最终有标签样本数: {self_training.labeled_iter_.size}")
# 预测
y_pred = self_training.predict(X)
accuracy = (y_pred == y).mean()
print(f"准确率: {accuracy:.2%}")
重要参数
| 参数 | 说明 | 默认值 |
|---|---|---|
base_estimator | 基分类器,需要有 predict_proba 方法 | - |
threshold | 置信度阈值,高于此值的预测会被采纳 | 0.75 |
max_iter | 最大迭代次数 | 10 |
k_best | 每次迭代添加的最佳样本数,与 threshold 二选一 | 10 |
阈值选择:
阈值的选择很关键。阈值太高,可能永远无法添加足够的伪标签样本;阈值太低,可能引入错误标签,导致"确认偏差"(Confirmation Bias)。
from sklearn.model_selection import cross_val_score
# 尝试不同阈值
thresholds = [0.7, 0.8, 0.9, 0.95]
results = {}
for thresh in thresholds:
st = SelfTrainingClassifier(
RandomForestClassifier(n_estimators=50, random_state=42),
threshold=thresh,
max_iter=20
)
st.fit(X, y_semi)
# 在完整标签上评估
y_pred = st.predict(X)
results[thresh] = (y_pred == y).mean()
print("不同阈值的准确率:")
for thresh, acc in results.items():
print(f" threshold={thresh}: {acc:.2%}")
自训练的风险
确认偏差:如果初始模型对某些样本给出错误但高置信度的预测,这些错误会在迭代中被放大。模型会越来越"自信"地做出错误预测。
缓解方法:
- 使用较高的阈值
- 使用
k_best参数限制每次添加的样本数 - 结合集成方法,多个分类器投票决定伪标签
标签传播(Label Propagation)
标签传播是一种基于图的半监督学习方法。它将数据点看作图的节点,节点之间的边权重由样本相似度决定,然后通过图结构传播标签信息。
算法原理
构建相似度图:将每个样本看作图的节点,节点之间的边权重为:
这是一个高斯核函数,样本越相似,边权重越大。
标签传播:每个节点将自己的标签信息按边的权重传播给邻居节点,迭代进行直到收敛。
数学上,标签传播可以表示为:
其中 是归一化的相似度矩阵, 是初始标签矩阵, 是传播系数。
基本用法
from sklearn.semi_supervised import LabelPropagation
from sklearn.datasets import load_iris
import numpy as np
# 准备数据(同上)
iris = load_iris()
X, y = iris.data, iris.target
rng = np.random.RandomState(42)
random_unlabeled_points = rng.rand(len(y)) < 0.9
y_semi = np.copy(y)
y_semi[random_unlabeled_points] = -1
# 创建标签传播模型
label_propagation = LabelPropagation(
kernel='rbf', # 核函数:'knn' 或 'rbf'
gamma=20, # RBF核的参数
max_iter=1000 # 最大迭代次数
)
# 训练
label_propagation.fit(X, y_semi)
# 查看传播后的标签
y_transduced = label_propagation.transduction_
print(f"传播后的标签分布: {np.bincount(y_transduced)}")
# 预测
y_pred = label_propagation.predict(X)
accuracy = (y_pred == y).mean()
print(f"准确率: {accuracy:.2%}")
重要参数
| 参数 | 说明 | 默认值 |
|---|---|---|
kernel | 核函数类型:'knn' 或 'rbf' | 'rbf' |
gamma | RBF 核的参数 | 20 |
n_neighbors | KNN 核的邻居数 | 7 |
max_iter | 最大迭代次数 | 1000 |
tol | 收敛阈值 | 1e-3 |
核函数选择:
'rbf':适用于连续特征,需要调整gamma参数'knn':适用于任意距离度量,需要调整n_neighbors参数
# 使用KNN核
lp_knn = LabelPropagation(kernel='knn', n_neighbors=5)
lp_knn.fit(X, y_semi)
y_pred_knn = lp_knn.predict(X)
# 使用RBF核
lp_rbf = LabelPropagation(kernel='rbf', gamma=10)
lp_rbf.fit(X, y_semi)
y_pred_rbf = lp_rbf.predict(X)
print(f"KNN核准确率: {(y_pred_knn == y).mean():.2%}")
print(f"RBF核准确率: {(y_pred_rbf == y).mean():.2%}")
标签扩散(Label Spreading)
标签扩散是标签传播的变体,主要区别在于添加了正则化项,使得算法对噪声更加稳健。
与标签传播的区别
标签传播:直接使用相似度矩阵进行传播,可能对噪声敏感。
标签扩散:使用归一化的拉普拉斯矩阵,并添加 clamping 因子,确保初始标签不会被完全覆盖。
这里的 确保原始标签始终有一定权重,防止传播偏离太远。
基本用法
from sklearn.semi_supervised import LabelSpreading
# 创建标签扩散模型
label_spreading = LabelSpreading(
kernel='rbf',
gamma=20,
alpha=0.2 # clamping因子,越小对原始标签依赖越大
)
# 训练
label_spreading.fit(X, y_semi)
# 预测
y_pred = label_spreading.predict(X)
accuracy = (y_pred == y).mean()
print(f"准确率: {accuracy:.2%}")
alpha 参数的影响
alpha 参数控制传播过程中对原始标签的保持程度:
alpha接近 1:更依赖传播过程,可能传播错误标签alpha接近 0:更依赖原始标签,传播效果减弱
alphas = [0.1, 0.3, 0.5, 0.7, 0.9]
for alpha in alphas:
ls = LabelSpreading(kernel='rbf', gamma=20, alpha=alpha)
ls.fit(X, y_semi)
y_pred = ls.predict(X)
print(f"alpha={alpha}: 准确率 {(y_pred == y).mean():.2%}")
LabelPropagation vs LabelSpreading
| 特性 | LabelPropagation | LabelSpreading |
|---|---|---|
| 正则化 | 无 | 有 |
| 对噪声敏感度 | 较高 | 较低 |
| 计算效率 | 稍快 | 稍慢 |
| 适用场景 | 数据质量高 | 数据有噪声 |
选择建议:
- 数据干净、噪声少 →
LabelPropagation - 数据有噪声、标签可能有错 →
LabelSpreading - 不确定时,两者都尝试比较
实际应用示例
文本分类示例
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.semi_supervised import SelfTrainingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
import numpy as np
# 加载数据
categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics', 'sci.med']
newsgroups_train = fetch_20newsgroups(subset='train', categories=categories)
newsgroups_test = fetch_20newsgroups(subset='test', categories=categories)
# 特征提取
vectorizer = TfidfVectorizer(max_features=5000)
X_train = vectorizer.fit_transform(newsgroups_train.data)
X_test = vectorizer.transform(newsgroups_test.data)
y_train = newsgroups_train.target
y_test = newsgroups_test.target
# 模拟少量标注数据
rng = np.random.RandomState(42)
n_labeled = 50 # 只使用50个标注样本
# 创建半监督标签
y_semi = np.full(len(y_train), -1)
labeled_indices = rng.choice(len(y_train), n_labeled, replace=False)
y_semi[labeled_indices] = y_train[labeled_indices]
print(f"有标签样本: {n_labeled}")
print(f"无标签样本: {len(y_train) - n_labeled}")
# 对比实验
# 1. 仅使用有标签数据
clf_supervised = LogisticRegression(max_iter=1000, random_state=42)
clf_supervised.fit(X_train[labeled_indices], y_train[labeled_indices])
y_pred_supervised = clf_supervised.predict(X_test)
# 2. 使用半监督学习
clf_semi = SelfTrainingClassifier(
LogisticRegression(max_iter=1000, random_state=42),
threshold=0.9,
max_iter=10
)
clf_semi.fit(X_train, y_semi)
y_pred_semi = clf_semi.predict(X_test)
print("\n=== 仅使用有标签数据 ===")
print(classification_report(y_test, y_pred_supervised, target_names=categories))
print("=== 半监督学习 ===")
print(classification_report(y_test, y_pred_semi, target_names=categories))
可视化标签传播效果
from sklearn.semi_supervised import LabelSpreading
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt
# 生成月牙形数据
X, y = make_moons(n_samples=200, noise=0.1, random_state=42)
# 模拟少量标签
rng = np.random.RandomState(42)
y_semi = np.full(len(y), -1)
labeled_indices = rng.choice(len(y), 10, replace=False)
y_semi[labeled_indices] = y[labeled_indices]
# 标签传播
ls = LabelSpreading(kernel='knn', n_neighbors=10, alpha=0.2)
ls.fit(X, y_semi)
# 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 原始数据
axes[0].scatter(X[:, 0], X[:, 1], c='gray', s=30)
axes[0].scatter(X[labeled_indices, 0], X[labeled_indices, 1],
c=y[labeled_indices], cmap='coolwarm', s=100, edgecolor='black')
axes[0].set_title('初始标签(灰色为无标签)')
# 传播结果
y_pred = ls.transduction_
axes[1].scatter(X[:, 0], X[:, 1], c=y_pred, cmap='coolwarm', s=30)
axes[1].set_title('标签传播结果')
# 真实标签
axes[2].scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', s=30)
axes[2].set_title('真实标签')
plt.tight_layout()
plt.show()
最佳实践
数据预处理
半监督学习对数据预处理要求较高:
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
# 创建包含预处理的半监督管道
pipeline = Pipeline([
('scaler', StandardScaler()),
('classifier', LabelSpreading(kernel='rbf', gamma=10))
])
# 注意:StandardScaler会使用所有数据(包括无标签)计算均值和标准差
# 这在半监督学习中是合理的
pipeline.fit(X, y_semi)
验证策略
由于无标签数据没有真实标签,验证策略需要特别注意:
from sklearn.model_selection import train_test_split
# 方案1:保留部分有标签数据作为验证集
X_labeled = X[y_semi != -1]
y_labeled = y_semi[y_semi != -1]
X_train_labeled, X_val, y_train_labeled, y_val = train_test_split(
X_labeled, y_labeled, test_size=0.3, random_state=42
)
# 方案2:使用交叉验证评估有标签部分
from sklearn.model_selection import cross_val_score
scores = cross_val_score(
LabelSpreading(kernel='rbf'),
X_labeled, y_labeled, cv=5
)
print(f"交叉验证分数: {scores.mean():.3f}")
标签质量检查
在使用半监督学习前,检查有标签数据的质量:
# 检查标签分布
import pandas as pd
labeled_mask = y_semi != -1
label_dist = pd.Series(y_semi[labeled_mask]).value_counts()
print("有标签数据的类别分布:")
print(label_dist)
# 检查标签是否平衡
if label_dist.std() / label_dist.mean() > 0.5:
print("警告:标签分布不平衡,可能影响传播效果")
何时使用半监督学习
适合的场景
- 标注成本极高(医学影像、法律文档)
- 有大量未标注数据
- 数据分布相对平滑,满足聚类假设
- 初始标注数据质量高
不适合的场景
- 标注数据极其稀少(<10个)
- 数据分布不满足平滑假设
- 初始标签错误率高
- 数据维度极高且稀疏
与其他方法的对比
| 方法 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 半监督学习 | 有少量标注数据 | 利用未标注数据 | 对初始标签敏感 |
| 迁移学习 | 有相关领域数据 | 跨领域知识迁移 | 需要预训练模型 |
| 主动学习 | 可选择性标注 | 高效利用标注预算 | 需要人工介入 |
| 监督学习 | 标注数据充足 | 简单直接 | 需要大量标注 |
小结
半监督学习是解决标注数据稀缺问题的有效方法:
- 自训练:最简单的方法,用高置信度预测扩充训练集
- 标签传播:基于图结构传播标签,适合平滑数据
- 标签扩散:标签传播的正则化版本,更稳健
- 参数选择:阈值、核函数、alpha参数需要根据数据特点调整
- 应用场景:标注成本高、未标注数据充足的场景
半监督学习的关键是利用数据的内在结构。在使用前,需要确认数据满足平滑假设或聚类假设,否则效果可能不佳。