跳到主要内容

半监督学习

半监督学习(Semi-Supervised Learning)是介于监督学习和无监督学习之间的一种学习方式。它利用少量的标注数据和大量的未标注数据进行模型训练,在实际应用中非常有价值,因为标注数据往往昂贵且耗时,而未标注数据则相对容易获取。

为什么需要半监督学习?

标注数据的困境

在实际项目中,获取大量高质量标注数据面临诸多挑战:

成本高昂:医学影像诊断需要专业医生标注,法律文档分类需要律师审核,这些都需要大量的人力成本。

时间消耗:标注过程本身耗时,特别是对于复杂任务(如语义分割、实体识别),一个样本可能需要数分钟甚至更长。

专家稀缺:某些领域需要专业知识才能准确标注,而具备这些知识的专家往往有限。

未标注数据的价值

未标注数据虽然不包含目标标签,但仍然蕴含着数据分布的信息。半监督学习的核心假设是:

平滑假设(Smoothness Assumption):如果两个样本在高密度区域中相近,它们的标签很可能相同。

聚类假设(Cluster Assumption):数据倾向于形成离散的簇,同一簇中的样本更可能属于同一类别。

流形假设(Manifold Assumption):高维数据往往聚集在低维流形附近,我们可以利用流形结构来推断标签。

半监督学习的适用场景

  • 标注数据有限(几十到几千个样本)
  • 未标注数据充足
  • 数据分布满足上述假设

sklearn 中的半监督学习方法

sklearn 提供了多种半监督学习算法,主要分为以下几类:

方法类型基本思想
SelfTrainingClassifier自训练用已标注数据训练初始模型,预测未标注数据,将高置信度预测加入训练集
LabelPropagation图方法基于图的结构传播标签,相似样本共享标签
LabelSpreading图方法LabelPropagation 的变体,使用正则化更稳健
IterativeClassifier迭代方法结合特征提取和分类的迭代过程

自训练(Self-Training)

自训练是最简单的半监督学习方法,其核心思想是"自我学习":用已有标注数据训练模型,预测未标注数据,将置信度高的预测结果加入训练集,重复迭代。

算法流程

  1. 用已标注数据训练初始分类器
  2. 用分类器预测未标注数据
  3. 选择置信度高于阈值的样本,将其预测标签作为"伪标签"
  4. 将这些样本加入训练集
  5. 重复步骤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)

标签传播是一种基于图的半监督学习方法。它将数据点看作图的节点,节点之间的边权重由样本相似度决定,然后通过图结构传播标签信息。

算法原理

构建相似度图:将每个样本看作图的节点,节点之间的边权重为:

wij=exp(xixj22σ2)w_{ij} = \exp\left(-\frac{\|x_i - x_j\|^2}{2\sigma^2}\right)

这是一个高斯核函数,样本越相似,边权重越大。

标签传播:每个节点将自己的标签信息按边的权重传播给邻居节点,迭代进行直到收敛。

数学上,标签传播可以表示为:

Y(t+1)=αSY(t)+(1α)Y(0)Y^{(t+1)} = \alpha S Y^{(t)} + (1-\alpha) Y^{(0)}

其中 SS 是归一化的相似度矩阵,Y(0)Y^{(0)} 是初始标签矩阵,α\alpha 是传播系数。

基本用法

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'
gammaRBF 核的参数20
n_neighborsKNN 核的邻居数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 因子,确保初始标签不会被完全覆盖。

Y(t+1)=αSY(t)+(1α)Y(0)Y^{(t+1)} = \alpha S Y^{(t)} + (1-\alpha) Y^{(0)}

这里的 (1α)Y(0)(1-\alpha)Y^{(0)} 确保原始标签始终有一定权重,防止传播偏离太远。

基本用法

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

特性LabelPropagationLabelSpreading
正则化
对噪声敏感度较高较低
计算效率稍快稍慢
适用场景数据质量高数据有噪声

选择建议

  • 数据干净、噪声少 → 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个)
  • 数据分布不满足平滑假设
  • 初始标签错误率高
  • 数据维度极高且稀疏

与其他方法的对比

方法适用场景优势劣势
半监督学习有少量标注数据利用未标注数据对初始标签敏感
迁移学习有相关领域数据跨领域知识迁移需要预训练模型
主动学习可选择性标注高效利用标注预算需要人工介入
监督学习标注数据充足简单直接需要大量标注

小结

半监督学习是解决标注数据稀缺问题的有效方法:

  1. 自训练:最简单的方法,用高置信度预测扩充训练集
  2. 标签传播:基于图结构传播标签,适合平滑数据
  3. 标签扩散:标签传播的正则化版本,更稳健
  4. 参数选择:阈值、核函数、alpha参数需要根据数据特点调整
  5. 应用场景:标注成本高、未标注数据充足的场景

半监督学习的关键是利用数据的内在结构。在使用前,需要确认数据满足平滑假设或聚类假设,否则效果可能不佳。