主动学习与半监督学习:节省标注预算的算法例程解析
简介面向机器学习学习者的 MATLAB 算法例程包聚焦主动学习与半监督学习两类策略解决标签样本稀缺时如何高效利用未标注数据的问题尤其适合正在研究文本分类、图像识别等应用的读者。压缩包体积仅 9KB包含 1 个 m 格式脚本内容紧凑但覆盖了从数据预处理、特征处理到模型评估的多个环节实现思路可分别对应主动学习中的不确定性采样、多样性采样以及半监督学习中的自训练、协同训练、低密度分离等经典算法。目前已有 195 人学习浏览。通过阅读并运行这段代码能够直观对比不同查询策略和半监督假设在少量标签数据下的表现理解算法迭代细节积累 MATLAB 工程化落地的实践经验为后续在真实项目中引入主动学习或半监督方案提供可复用的参考。1. 主动学习与半监督学习算法例程到底能帮你省多少标注预算做机器学习落地的团队大概率都卡在同一个地方手里攒了几万条无标注数据标注预算却只够覆盖一两个百分点。这时候模型选型反而不是第一问题数据策略才是。主动学习是让模型自己挑“最有价值”的样本给人标半监督学习则是把剩余的无标注数据直接拉进训练流程两者都是冲着省标注预算去的。这个标题里的算法例程包就是把这两类方法按策略、训练、评估三个环节打包成可直接跑的代码适合正在搭数据飞轮、或者刚切换到这个方向的算法工程师照着改。往下读之前先记住一个反直觉的结论这两套方法一起用往往比单独用任何一套省得更多。2. 先理清两个范式标注预算、数据分布与选型判断动手跑代码之前得先把两个范式各自解决什么问题、为什么常被放在同一个例程包里讲清楚。很多人在半监督和主动学习之间二选一其实它们是同一道题的两个半场。2.1 主动学习让模型告诉你下一批标什么主动学习的核心假设是给模型看它最“拿不准”的样本比随机挑样本标注带来的收益大得多。训练流程是把未标注数据池丢给当前模型用某个采样策略打分排序挑出 top-k 个样本送人工标注标完并入训练集再迭代。常见策略有三类不确定性采样最低置信度、边际分数、熵、委员会投票QBC多个模型分歧越大越值得标、预期模型变化EMOC标了之后梯度更新幅度预估。这里最容易被忽略的是冷启动第一轮模型还没训出来采样策略无从谈起所以通常要先随机标一批种子集把模型预热到能输出靠谱预测。种子集规模一般取总标注预算的 10%~20%太小模型连类别分布都学不稳太大又浪费预算。我一般会先用随机采样跑一个 baseline再拿主动学习跟它对比否则很难判断增益到底来自策略还是来自模型结构本身的提升。2.2 半监督学习让无标注数据进场训练半监督学习的思路是另一条路标注数据少没关系无标注数据量大那就想办法让它们也参与训练。主流方法可以分成三拨伪标签 / self-training、一致性正则化、图方法标签传播。伪标签最直观——模型对无标注数据生成预测超过置信度阈值的当成标签一起反传一致性正则化是 FixMatch、MixMatch 那条线对无标注样本做弱增强和强增强要求两种预测尽可能一致图方法则把样本看成节点让标签沿着相似度边扩散。选择依据主要看数据分布。伪标签适合类别分布相对均衡、模型校准较好的任务一致性正则化对多分类和小数据集更稳因为它不像伪标签那样直接把预测当成硬标签而是用软约束逐步拉近分布。实际例程包里通常会同时给伪标签和 FixMatch 两个版本毕竟单个数据集的翻车率不低。2.3 选型判断什么场景先上主动学习、什么场景先上半监督两条路不是互斥的而是互补。我常用的判断标准是标注成本贵不贵、反馈够不够快。如果单个样本标注成本很高比如医疗影像要专家花几分钟优先主动学习让每一块钱都花在最难分类的样本上如果标注成本低但数据量大比如文本分类打标一个人一天能标几千条优先半监督让无标注数据先把模型底子撑起来再用主动学习补边界样本。顺序也值得讲究。常见做法是先随机标一批种子集跑一轮半监督训练再用主动学习从未标注池里挑下一批补充。这样标注预算、模型精度和未标注池的利用率三条线同时动而不是等到主动学习把预算花完才想起来半监督。例程包里两个模块分开排布恰恰是为了让你按这个组合流程来串。3. 从 zip 到首次出训练曲线解压、环境与跑通最小例程拿到 zip 的第一件事不是看代码是先把环境立起来。这节按我实际跑这类例程包的顺序来写每一步的东西你可以直接抄。3.1 解压前先检查环境Python版本与依赖清单主动学习和半监督例程基本都跑在 Python 3.8~3.11 之间依赖不外乎 torch / torchvision、scikit-learn、numpy、tqdm再加上一个配置文件解析库yaml 或 json。先确认机器上有 conda 或 venv再动 zip。# 解压同时保留目录结构-o 覆盖同名文件防止重复解压 unzip active_semi.zip -d active_semi_project cd active_semi_project # 创建独立虚拟环境避免污染系统 Python python3 -m venv venv source venv/bin/activate # 安装依赖如果 requirements.txt 里版本较旧建议先用 pip index 确认兼容性 pip install -r requirements.txt逻辑说明第一条命令解压到指定目录-d后面是目标路径不写会原地摊开目录里如果还有同名文件夹就乱套。第二条命令创建虚拟环境跑机器学习项目几乎是必须动作因为 torch 和 sklearn 的依赖经常互相打架。第三条命令装依赖如果机器上已经装了 CUDA 版的 torch可以先把 requirements 里 torch 那行注释掉避免 pip 顺手把那行替换成 CPU 版。参数说明unzip的-o表示覆盖不加的话解压中途遇到同名文件会停下来问你脚本里如果有中英文文件名混排解压后乱码可以参考第 5 章的排查记录。venv目录不要提交到 git运行例程时也要记得先激活。3.2 目录结构先找到入口文件再动手这类例程包一般会按模块拆目录常见布局是active/放主动学习策略semi/放半监督训练脚本data/放数据加载器和预处理逻辑configs/放超参配置根目录放一两个汇总入口脚本。打开目录先找两个东西README.md和带example或demo字样的脚本。# 查看目录结构限制层级为 2 层避免输出太长 find . -maxdepth 2 -type f | sort # 快速浏览 README确认作者写的启动命令 head -80 README.md逻辑说明find加上-maxdepth 2是为了只扫两层目录把入口文件、配置文件这些一眼定位出来head -80看 README 开头的启动说明比直接翻代码快得多。我见过不少人在这一步栽跟头——源码里写了多个入口脚本不知道跑哪个最后只能逐个试。参数说明find的-maxdepth是层级上限写大了输出会爆head默认显示 10 行给到 80 行是因为 README 通常把环境安装和启动命令放在前两节。如果你看到的目录结构跟这里不一样不用慌找train、active_learn、pseudo这类语义词基本就是主入口。3.3 跑第一个最小示例固定随机种子是关键环境就绪、入口找到之后先用默认配置跑通一遍。这时不要贪多改参数目标是看训练曲线能出来。常见入口脚本长这样# 随机种子写死保证每次运行结果可复现 python example_active.py --dataset cifar10 --strategy margin --seed 42 --budget 2000逻辑说明example_active.py一般会依次执行“随机标种子集→主动学习迭代→半监督增强训练→输出学习曲线”这一套流程。--budget 2000表示总预算就 2000 条标注跑一轮约 5~10 次迭代。脚本跑完会在runs/目录下生成精度曲线图和一个记录每次迭代精度的 CSV。参数说明--strategy margin选择边际采样策略比least_confident更平衡是我跑第一遍的首选--seed 42这个参数不是随便写写主动学习和半监督都涉及多次随机抽样不固定种子的话两次跑出来的曲线差异会大到让你怀疑代码有 bug实际排错时也必须有固定的对照基线。4. 主动学习例程用不确定性采样维护标注队列这一章是主动学习模块的核心实现重点看策略函数怎么写、标注队列怎么维护、以及跟人工标注对接时哪些参数决定效率。4.1 三种不确定性采样策略的实现主动学习的策略函数输入是“模型预测概率”输出是每个未标注样本的得分按得分排序挑 top-k。以下实现兼容 sklearn 风格的predict_proba输出也适合改成 PyTorch 的 logits 输出。import numpy as np def score_samples(probs, strategymargin): probs: shape (n_samples, n_classes) 的行归一化概率 返回: score (n_samples,)数值越大 代表越值得标注 if strategy least_confident: # 1 - 最大概率越小说明模型越没底 score 1.0 - np.max(probs, axis1) elif strategy margin: # 最大概率与次大概率之差差越小说明决策边界越模糊 sorted_probs np.sort(probs, axis1)[:, ::-1] score 1.0 - (sorted_probs[:, 0] - sorted_probs[:, 1]) elif strategy entropy: # 信息熵分布越均匀越值得标 score -np.sum(probs * np.log(probs 1e-12), axis1) else: raise ValueError(funknown strategy: {strategy}) return score def select_top_k(probs, k, exclude_idx): scores score_samples(probs, strategymargin) scores[exclude_idx] -np.inf return np.argsort(scores)[::-1][:k]逻辑说明三种策略的得分方向我故意统一成“越大越值得标”。least_confident只看最大概率对多分类问题容易忽略次大概率的信息margin取前两个概率之差能更好捕捉边界样本entropy对分布整体形状敏感理论上最全面实际跑起来也最容易受类别不平衡干扰。select_top_k里把已标注索引的分数设成负无穷是极容易被漏掉的一步——如果不排除前几轮选出的样本会高度重复。参数说明exclude_idx必须包含两类索引已经人工标注过的样本、以及评估集/测试集样本。很多例程会在初始化时把train_idx和labeled_idx拆开把这个参数接成二者的并集即可。1e-12是防log(0)的平滑项太小会除零太大则会压低熵分数不需要经常调。4.2 标注队列去重、缓存与批次策略策略函数选出的只是索引真正的工程难点在标注队列。上一轮挑出的样本还在人工标注中下一轮迭代已经启动了这时队列如果处理不好要么重复送标要么标注结果和模型版本错位。class AnnotationQueue: def __init__(self, pool_idx, batch_size32): self.pool set(pool_idx.tolist()) # 未标注池 self.pending {} # idx - 送标时间戳 self.annotated set() # 已确认标注 def propose(self, top_k_idx): # 只在未标注池里选跳过已送标和已标注 valid [i for i in top_k_idx if i in self.pool and i not in self.annotated and i not in self.pending] batch valid[:self.batch_size] for i in batch: self.pending[i] time.time() return batch def confirm(self, idx_list): # 人工标注返回后把索引从 pending 移到 annotated for i in idx_list: self.annotated.add(i) self.pending.pop(i, None) self.pool.discard(i)逻辑说明这段代码把“选出来”和“标完了”分离。propose阶段只负责送标样本进入pending但还没从pool里删掉confirm阶段人工标注结果回来后才正式记为已标注。这解决了主动学习落地时的典型错位标注工人还没标完下一轮模型已经把同一个样本又选出来了。配合 4.1 的exclude_idx能避免重复标注。参数说明batch_size不是越大越好。一轮送标太多模型版本的更新周期会拉长太少则人工标注流程频繁被打断。我一般设成人工标注团队半天到一天的产能比如单日能标 300 条batch_size就取 150 左右保证一天至少两轮模型更新。Pool 和 pending 都放在内存即可数据量大时改成 SQLite 落盘。4.3 与真实标注流程对接的四个参数跑通例程是一回事接到自己业务里是另一回事。几个关键参数需要单独调cold_start_size冷启动随机标注数。设为总预算的 10%~20%太小模型连类别先验都学不出来。iterations主动学习迭代轮数。轮数太多边际增益递减明显一般 10~20 轮就够。batch_ratio每轮送标量与当前已标注量的比例。0.3 附近比较稳加太快会让后续轮次可挑样本太少。scoring_batch_size对未标注池打分的批大小。几万样本时无所谓到百万级就要分批否则显存炸掉。参数之间是联动的我最常看到的新手误操作是把iterations设很大、cold_start_size设很小结果前几轮选出的全是噪声后面无论怎么迭代都拉不回来。正确做法是先定预算再从预算倒推cold_start_size和iterations最后用一份小型验证集看曲线有没有稳定上升。5. 避坑与常见问题排查四个翻车现场的处理记录跑这类例程包翻车不是意外是常态。以下四条踩坑记录按“现象 → 原因 → 解决”写每一条我都在真实数据上见过。5.1 环境与数据相关的两个坑现象一解压后脚本文件出现乱码尤其是 README 和数据文件里的中文名。原因zip 包在 Windows 上用默认编码压的Linux 系统unzip默认按 UTF-8 解析中文文件名和注释就对不上。解决unzip -O gbk指定编码解压或者干脆用 Python 的zipfile脚本把文件名重新编码一遍。顺带一提遇到压缩包被加密又没有密码工具时也别急着放弃zip2john那套是暴力破解配合合法场景下清理自己忘记密码的备份还好使业务流程里建议直接找打包人。现象二依赖装完一跑就报CUDA out of memory或者torch has no attribute xxx。原因requirements.txt里的 torch 版本经常和机器上的 CUDA 版本不匹配或者 CPU 机器误装了 CUDA 版 torch显存自然不够。解决先nvidia-smi看 CUDA 版本再按官方矩阵选对应 torch没有 GPU 时直接用--index-url https://download.pytorch.org/whl/cpu装 CPU 版主动学习和半监督在小数据上 CPU 也能练只是慢一些。在看到报错之前先跑一段torch.cuda.is_available()做体检能省下大量无效排错时间。5.2 方法与调参相关的两个坑现象三半监督训练 5 个 epoch 后损失变成 NaN或者精度跌回随机水平。原因伪标签的置信度阈值设太低。我把阈值调到 0.9 试过模型对无标注数据“过度自信”输出一堆错标签自信地崩掉调低了又等于把噪声当监督信号。解决伪标签阈值从 0.95 起配合无标注数据 batch 的权重系数保 证加入训练后 loss 曲线先降后稳。FixMatch 等一致性正则方法对mu无标注 batch 与有标注 batch 的倍数很敏感取 3~5 之间常见调参时每次只改一个变量同时记录 loss 和伪标签接受率。现象四主动学习选了 5 轮精度曲线基本不涨甚至往下掉。原因类不平衡场景下不确定性采样疯狂挑选“难例”——少数类样本和高噪声样本全被选出来标注工人标到手软模型却越练越偏。解决分类任务里把算法换成先按熵分层抽样再在每层内部取 top 样本或者直接对少数类多分配预算比例。另外如果已标注池涨了 10 倍精度纹丝不动先检查是不是每轮都在选特征空间里几乎重叠的样本——此时要用基于密度的采样代替纯不确定性采样强制多样性。坑是曲线不涨时九成情况不是代码有 bug而是你的数据本身存在大量标注噪声不先做一致性清洗换什么策略都白搭。6. 半监督例程收尾伪标签迭代训练与效果验证技巧最后一节落地到半监督模块的具体代码和验证方法。跑之前确认你已经读完第 5 章不然很容易踩进同一个坑里出不来。6.1 伪标签迭代训练的最小框架先看核心训练循环代码兼容 PyTorch 2.x 和轻量封装。# 伪标签训练核心代码 for epoch in range(max_epoch): model.train() for labeled_x, labeled_y in labeled_loader: # 有标注数据走正常监督 loss sup_loss ce_loss(model(labeled_x), labeled_y) # 取无标注数据批次走弱增强得到伪标签 unlabeled_x next(unlabeled_iter) with torch.no_grad(): probs torch.softmax(model(unlabeled_x), dim1) confidence, pseudo_label probs.max(dim1) mask confidence threshold if mask.sum() 0: unsup_loss ce_loss(model(unlabeled_x)[mask], pseudo_label[mask]) else: unsup_loss torch.tensor(0.0) loss sup_loss lambda_u * unsup_loss loss.backward() optimizer.step()逻辑说明这段代码的关键在于“先算置信度、再决定是否参与训练”。confidence threshold的 mask 过滤掉了低置信度样本避免伪标签噪声直接污染监督信号。有标注数据和无标注数据在同一个 batch 内交替出现用next(unlabeled_iter)的方式让两个迭代器平行推进可以利用 DataLoader 的 shuffle 和采样器保证无标注数据不会连续重复。参数说明threshold是半监督例程最重要的超参0.95 起步许多数据集上 0.9 也可以接受但你需要在验证集上观察接受率和精度曲线的关系。lambda_u控制无标注 loss 的权重太大会让模型只学伪标签、忽略真实标注太小等于没引入无标注数据一般从 0.1 开始观察训练稳定性再微调。max_epoch设在 60~100 之间配合早停就够了伪标签方法在 epoch 数过多时容易过拟合无标注数据。6.2 效果验证学习曲线的 AUC 与消融对照验证半监督和主动学习组合效果我最在意的不是“最终精度多少”而是“达到同样精度花了多少标注”。因此建议把每轮迭代的标注量和验证精度记下来画一条学习曲线并计算曲线下面积。面积越小说明模型用更少的标注达到了同样的精度水平。具体做法是固定种子集和总预算分别跑三组——只用有标注数据、加了主动学习不加半监督、主动学习加半监督。三组放在同一张坐标图里对比。如果加了半监督反而比纯主动学习差九成是伪标签阈值和lambda_u没配对先降阈值、降权重别急着换模型。验证集尽量保持和真实分布一致不要用训练分布里采出来的部分替代否则曲线会虚高。最后说个我做这类例程的习惯每次调完参数第一件事不是看精度而是看打印出来的“每轮标注量、伪标签接受率、训练总时长”三行日志。这三行能直接暴露策略是否在真正节省标注。盲目调模型结构往往调了半天省下的时间还不如多标 200 条样本。这套组合方法也一样先跑出对的曲线再谈优化。希望帮到你。本文还有配套的精品资源点击获取