ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

主动学习+半监督学习:用最少标注训练高精度模型

主动学习+半监督学习:用最少标注训练高精度模型 简介压缩包围绕主动学习与半监督学习提供MATLAB算法例程面向需要处理标记数据稀缺问题的机器学习学习者与研究人员。包内包含1个fenleng.m源代码文件整体仅9KB轻量简洁适合单独研读与二次修改。文件内容可能融合不确定性采样、多样性查询、自训练、协同训练等经典策略并涵盖特征提取、降维、交叉验证等数据预处理与评估步骤有助于理解从数据准备到模型性能评估的完整闭环。在文本分类、图像识别等应用场景中这类方法尤其能利用大量未标注数据提升模型表现实际动手运行代码可直观体会主动学习如何智能挑选待标注样本、半监督学习如何借助无标注数据增强泛化能力同时积累MATLAB机器学习项目的调试与优化经验。目前已有195人学习下载适合入门巩固与算法对比研究的学习者选用。1. 主动学习和半监督学习的算法例程能让你用最少的标注拿到能上线的模型手里只有几百条标注数据模型却要达到几千条标注才能跑出的效果这是算法落地时最常见的尴尬。主动学习和半监督学习是解决这个问题的两条腿主动学习让模型自己从无标注池里挑出最该被标注的样本把人工标注的每一分钱花在刀刃上半监督学习则让模型从海量无标注数据里自己找规律不花一分钱就能白嫖一部分精度。把这两套思路揉进同一套例程就能形成一条靠谱的落地路径先小额标注冷启动再迭代拉高召回。这套算法例程正是干这件事的里面一般会覆盖不确定性采样、委员会投票、伪标签、一致性正则这些主流方法。适合谁适合正在做数据标注预算控制、或者想把模型精度再往上顶一截的算法工程师和数据团队。下面我按实际落地顺序把这套东西拆开讲。2. 主动学习核心采样策略先搞清楚让模型“开口要”哪批数据主动学习不是简单地把模型不确定的样本拿出去标注就完事了关键在“不确定”怎么定义。同样是挑500条数据交给标注员策略选错模型学完可能纹丝不动策略选对精度能直接拉出三五个点。先讲三个最容易复现、也是例程里最常见的不确定性采样实现。2.1 最小置信度、间隔采样和熵三个不确定性指标怎么选最基础的采样逻辑是让模型对无标注池里的每一条样本都做一次预测然后按“模型对自己预测的信心”排序把最没把握的挑出来。实际例程里最常出现的就是下面这段代码它把三种不确定性指标统一在一个函数里import torch def uncertainty_sampling(model, unlabeled_loader, strategyleast_confidence, top_k500, devicecuda): 主动学习: 不确定性采样 strategy: - least_confidence: 最小置信度, 只用最大类概率 - margin: 间隔采样, 看top1与top2概率差 - entropy: 熵, 考虑整个概率分布的混乱程度 model.to(device).eval() scores, indices [], [] with torch.no_grad(): for inputs, idx in unlabeled_loader: inputs inputs.to(device) logits model(inputs) probs torch.softmax(logits, dim1) if strategy least_confidence: conf, _ torch.max(probs, dim1) score 1.0 - conf # 最大类概率越接近1, 说明模型越有把握, 得分越低 elif strategy margin: sorted_probs, _ torch.sort(probs, dim1, descendingTrue) score sorted_probs[:, 0] - sorted_probs[:, 1] # top1和top2概率差越小, 说明模型越犹豫 elif strategy entropy: logp torch.log(probs.clamp(min1e-9)) score -(probs * logp).sum(dim1) # 熵越大, 分布越平均, 越不确定 scores.extend(score.tolist()) indices.extend(idx.tolist()) # 分数最高(最不确定)的top_k个样本优先送标注 selected sorted(zip(indices, scores), keylambda x: x[1], reverseTrue)[:top_k] return [sid for sid, _ in selected]这段代码本身不复杂但有几个参数必须调对。top_k是一次迭代交给标注员的样本数量不是越大越好——如果一次性塞给标注员两千条模型还没学到东西下一轮采样还是靠老模型效率会打折扣。常见做法是每次迭代选总池子的5%~10%让“训练—采样—标注”尽量同步推进。三个策略各有偏向。least_confidence对二分类最直观但它只看最大概率当类别数很多时容易把那些“所有类别概率都差不多”的样本和“某个类别概率略高”的样本混为一谈margin会好一些它关心的是模型在“最有可能的两个答案”之间的纠结程度多分类任务里比最小置信度稳定entropy则把整个概率分布都纳入计算理论上信息量最大但计算量也稍高而且对概率校准比较敏感。实际跑例程时我的习惯是先在这三个策略上各做一轮小规模对比看看哪个在验证集上表现更好。不要想当然觉得 entropy 一定最好——在我经手的几个任务里类别数在 5 以下的分类任务margin 反而经常胜出因为 entropy 会被那些“模型其实已经分得差不多”的长尾噪声样本干扰。2.2 委员会投票和 Core-Set当不确定性采样不够用时上分布覆盖不确定性采样有一个绕不开的死穴它只看模型对单条样本的“犹豫程度”完全无视样本之间的相似性。一批数据里如果有一千条几乎重复的相似样本模型会把这一千条全部选出来因为它们的预测都很犹豫但标注员标完这一千条学到的信息量可能顶不上五十条互不相似的样本。解决这个问题有两条路一条是委员会投票一条是 Core-Set这两种方法在完整例程里通常也会配上。委员会投票Query by Committee的思路是训练多个模型不同随机种子、不同网络结构让它们对同一条无标注样本投票分歧最大的样本优先标注。代码实现也不难def qbc_sampling(models, unlabeled_loader, top_k500, devicecuda): 主动学习: 委员会投票采样 用多个模型的预测分歧度衡量样本价值 all_probs [] for model in models: model.to(device).eval() preds [] with torch.no_grad(): for inputs, _ in unlabeled_loader: inputs inputs.to(device) probs torch.softmax(model(inputs), dim1) preds.append(probs.cpu()) all_probs.append(torch.cat(preds, dim0)) # 对多个模型的预测取平均, 再算熵 avg_probs torch.stack(all_probs).mean(dim0) logp torch.log(avg_probs.clamp(min1e-9)) vote_entropy -(avg_probs * logp).sum(-1) _, top_indices vote_entropy.topk(top_k) return top_indices.tolist()委员会投票的代价是训练多个模型计算成本直接翻倍但如果你的场景里标注成本远大于算力成本商业项目里这几乎总是成立的那这笔账很划算。另一个常见选择是 Core-Set它不关心模型有多纠结而是直接对无标注池做特征聚类然后从每个簇里均匀抽样本确保每次选出的样本尽可能覆盖整个数据分布。Core-Set 的实现比委员会投票繁琐常用做法是把模型倒数第二层的特征向量抽出来用 k-means 聚类后按簇分配采样名额。特征提取那一步要注意用训练集的统计量做归一化否则分布偏移会把聚类结果带偏。2.3 采样策略选型的三个经验判断例程里堆了这么多策略到底该用哪个我先说结论标注预算极低只有几百条时先用随机采样跑两轮热热场看模型在验证集上的基线预算在几千条以内、类别均衡优先试 margin 和 entropy数据分布本身很不均衡、长尾严重要把 Core-Set 或 BADGE 这类覆盖式采样作为主力否则模型会把长尾类别的样本全部忽略掉。另外有一个比较反直觉的点很多例程里不确定性采样会搭配“先预训练一小段”再做采样。直接把一个没训过的模型拿去采样它输出的概率分布几乎是均匀的选出来的只是“随机样本”没有意义。所以采样前一定让模型先在有标注的小集合上过几个 epoch把骨架搭起来。3. 半监督学习主体框架从伪标签到一致性正则主动学习解决的是“挑哪些样本让人来标”半监督学习解决的是“剩下的海量无标注数据怎么自己用起来”。这一块常见的实现路径有两条一条是伪标签Pseudo-Labeling简单直接另一条是一致性正则Consistency Regularization也即 Mean Teacher 这类方法效果更稳但成本也更高。例程里一般会把两条路径都跑通让你自己比较。3.1 伪标签与置信度阈值最简单的半监督但别把阈值设得太低伪标签的做法不需要额外的网络结构先用有标注数据训练一个模型然后拿这个模型去预测无标注数据把预测结果当成伪标签混入训练集再继续训练。核心参数只有一个置信度阈值。下面这段代码就是常见的伪标签生成逻辑torch.no_grad() def generate_pseudo_labels(model, unlabeled_loader, threshold0.8, max_numNone, devicecuda): 半监督: 伪标签生成 只保留模型置信度高于threshold的样本, 避免噪声注入 max_num: 每轮最多采纳的伪标签数量, 防止某类样本过多导致类别失衡 model.to(device).eval() pseudo_x, pseudo_y [], [] for inputs, _ in unlabeled_loader: inputs inputs.to(device) logits model(inputs) probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) mask conf threshold pseudo_x.append(inputs[mask].cpu()) pseudo_y.append(pred[mask].cpu()) pseudo_x torch.cat(pseudo_x, dim0) pseudo_y torch.cat(pseudo_y, dim0) if max_num is not None and pseudo_x.size(0) max_num: perm torch.randperm(pseudo_x.size(0))[:max_num] pseudo_x, pseudo_y pseudo_x[perm], pseudo_y[perm] return pseudo_x, pseudo_y阈值怎么设是这门手艺里最核心的玄学。设 0.95几乎不会混入噪声但能用的无标注样本太少半监督形同虚设设 0.6样本量是上来了但模型会把大量错标签当标准答案学进去越训越偏。我一般从 0.8 起步每个迭代轮次记录一下“当前阈值下能筛出多少无标注样本”如果发现筛出的数量断崖式下跌就往下调 0.05而不是硬撑着。另一个值得注意的点是阈值不一定要全局统一。类别不均衡的数据集上头部类别哪怕阈值设低也有足够多的伪标签尾部类别则可能一条都筛不出来。调整方式是按类各自算阈值——头部类用严格阈值尾部类放宽保证每个类都能补充到一些伪标签跟标注预算分配的原理是一样的。3.2 Mean Teacher 一致性正则EMA 教师模型为什么比硬伪标签稳伪标签的问题是“错”得比较硬——模型一旦在某个类别上有偏置伪标签会把这个偏置放大成更严重的偏置。一致性正则的思路换了一个角度它不看模型输出的绝对值而是看“同一个样本在不同扰动下的输出是否一致”。如果模型对一张图片做轻微裁剪、加噪声之后预测结果还是一样的说明模型在这个样本上学到了稳定的特征如果两次预测分歧很大说明这个样本还没被学好。Mean Teacher 是这类方法里最经典的代表一个学生模型正常用梯度更新另一个教师模型的参数不通过梯度更新而是用学生参数的指数移动平均EMA来滑动更新然后用教师模型的预测作为学生模型在无标注数据上的学习目标。核心训练步骤可以写成下面这样def mean_teacher_train_step(student, teacher, optimizer, labeled_batch, unlabeled_pair, alpha0.999, consistency_weight1.0, devicecuda): Mean Teacher 单步训练 labeled_batch: (有标注图像, 标签) unlabeled_pair: (无标注图像_增强1, 无标注图像_增强2) x_l, y_l labeled_batch x_ul1, x_ul2 unlabeled_pair student.train() s_logits student(x_l.to(device)) ce_loss torch.nn.functional.cross_entropy(s_logits, y_l.to(device)) # 教师模型输出两次增强的预测均值, 作为无标注数据的学习目标 with torch.no_grad(): t_logits1 teacher(x_ul1.to(device)) t_logits2 teacher(x_ul2.to(device)) t_target torch.softmax((t_logits1 t_logits2) / 2, dim1) # 学生对其中一种增强的预测, 要与教师目标对齐 s_ul_logits student(x_ul2.to(device)) s_ul_probs torch.softmax(s_ul_logits, dim1) consistency_loss torch.mean((s_ul_probs - t_target) ** 2) loss ce_loss consistency_weight * consistency_loss optimizer.zero_grad() loss.backward() optimizer.step() # EMA 更新教师模型 with torch.no_grad(): for t_param, s_param in zip(teacher.parameters(), student.parameters()): t_param.data.mul_(alpha).add_(s_param.data, alpha1 - alpha)这段代码里有三个关键参数。alpha是 EMA 衰减系数控制教师模型跟随学生模型的速度——设成 0.99 教师跟得快学生一崩教师也跟着崩设成 0.999 教师更稳但适应新数据慢。通常从 0.999 起步比较安全。consistency_weight是一致性损失的权重设太大梯度会爆炸设太小半监督学习形同虚设稳妥的做法是从 1.0 开始观察训练损失的走势再微调。教师模型为什么要对两次增强取平均——这是对预测方差的主动降噪只拿一次预测当目标目标本身噪声太大学生容易被带跑。跑 Mean Teacher 时最容易翻车的一个点学生模型和教师模型的初始化必须一致如果教师模型随机初始化而学生模型先训练了几个 epoch前几轮训练的一致性损失会非常大把有监督损失完全淹没。正确做法是先把学生模型在有标注数据上热身几个 epoch然后把学生权重直接复制给教师模型再开始联合训练。3.3 伪标签和一致性正则怎么选成本、效果、稳定性三个维度就落地而言伪标签最大的优势是省事不需要改模型结构一个函数就能塞进现有训练流程缺点是噪声敏感阈值调不好效果波动很大。一致性正则对增强策略有要求至少得有两套不同的数据增强流水线实现复杂度高一截但胜在处理分布偏移和噪声时更稳。一个实用的分工方式数据量足够、标注质量也还行的时候用伪标签做第一轮粗筛快速把无标注数据里最有价值的部分挖掘出来当伪标签带来的收益开始下降时再切换到 Mean Teacher 做精细打磨。例程里通常把这两套方法放在不同目录下但真正落地时它们不是二选一而是先后衔接的关系。4. 把主动学习和半监督学习串成完整 Pipeline迭代训练的每一步都是关键分开看主动学习和半监督学习都能跑但例程真正的价值在于把两者放到同一个迭代循环里每一轮先半监督扩增再主动采样再标注再训练。这里面的执行顺序至关重要顺序反了效果可能还不如只做一边。4.1 迭代式主动半监督学习框架的代码骨架下面这段代码是例程里常见的主循环骨架把前面几节的采样函数和半监督训练函数全部串起来def active_semisupervised_loop(model, train_labeled, unlabeled_pool, valid_set, train_fn, sample_fn, budget1000, rounds6, batch_size500, seed0): 主动学习 半监督学习 联合迭代主循环 train_fn: 接受(labeled_set, pseudo_source, warm_start_model), 返回训练好的模型 sample_fn: 接受(model, unlabeled_pool, top_k), 返回需要人工标注的样本编号 random.seed(seed) torch.manual_seed(seed) # 冷启动: 先只用有标注数据把模型训到能采样的程度 model train_fn(train_labeled, pseudo_sourceNone, warm_start_modelmodel) for r in range(rounds): # 1. 半监督扩增: 从无标注池里生成伪标签/一致性目标, 参与本轮训练 model train_fn(train_labeled, pseudo_sourceunlabeled_pool, warm_start_modelmodel) # 2. 评估当前模型在固定验证集上的表现, 记录每轮曲线 val_acc evaluate(model, valid_set) print(f[Round {r}] valid_acc{val_acc:.4f}) # 3. 主动采样: 模型挑出最该标注的样本 new_ids sample_fn(model, unlabeled_pool, top_kbatch_size) # 4. 模拟人工标注: 实际工程里这里会调用标注平台接口 new_annotations annotate(new_ids) # 5. 合并标注数据, 从无标注池中移除已标注样本 train_labeled merge_labeled(train_labeled, new_ids, new_annotations) unlabeled_pool remove_indices(unlabeled_pool, new_ids) # 预算控制: 达到总预算立刻停 if len(train_labeled) budget: break return model这段主循环里有一个细节值得强调train_fn在半监督阶段接收的是整个unlabeled_pool但实际工程里不能直接把几十万条无标注数据全部塞进训练循环。常见做法是每次从池子里随机抽一个子集比如两万条作为当轮的半监督训练集采样时再在全体池子上做预测。否则每轮迭代都全量过一遍模型时间成本会随轮次线性增长。4.2 先半监督还是先主动采样顺序决定了每轮标注的质量很多第一次跑这套例程的人会犯一个错误第一轮冷启动之后紧接着就做主动采样然后才做半监督训练。这个顺序不对。原因不难理解采样策略依赖模型对无标注数据的判断模型越准确采出的样本越有价值。如果第一轮冷启动结束后先采样此时模型的精度可能只有六成选出来的“最难样本”里混着大量模型压根没见过的分布外数据正确的做法是先跑一轮半监督训练让模型在无标注数据上见多识广一点再让它去采样。半监督训练相当于让模型预习了一遍考纲主动采样相当于让模型划重点划重点的前提是先读过书。4.3 冷启动的数据量怎么定“冷启动”用多少有标注数据直接决定后续迭代能不能起来。太少模型连基本的类别边界都分不清伪标签大量出错主动学习采出来的也会是噪声太多那就不需要半监督和主动学习来省预算了。经验值是有标注数据先覆盖全部类别、每类至少 50 条总样本量在模型输入维度的 20 倍以上对于图像分类任务500~1000 条通常是安全的起点。如果连这个量都凑不齐建议先别急着上这套框架而是先去收集一小批能覆盖全类别的种子标注。4.4 迭代轮数与批次大小的权衡迭代轮数和每轮采样数batch_size是一对需要平衡的参数。每轮采太少模型学得慢多轮迭代的时间成本高每轮采太多标注人员一次性承压而且中间几轮的模型提升不明显。我习惯把总预算的 10% 放在第一轮冷启动剩下 90% 分成 6~8 轮每轮采样量递减——前几轮多采快速建立模型骨架后几轮少采只挑那些真正让模型困惑的硬骨头。这样做还有一个好处后几轮模型已经比较强采样准确性高即便每轮只采一百条对精度的提升也比第一轮采一千条更大。5. 避坑与排查跑例程最容易翻车的五个位置这套例程我在本地和服务器上跑过很多遍也在不同数据集上调过参数。下面这几个坑基本每次换新数据集都会遇到列出来供你排查时对照。5.1 采样总是选中同一批“顽固样本”效果原地踏步现象连续迭代好几轮每轮选出的新标注样本和上一轮有七八成重叠标注员反馈“这批和上一批怎么长得一模一样”验证集精度也不涨了。原因纯不确定性采样只追求“模型最不确定”完全没考虑样本之间的相似性。如果无标注池里有大量高度相似的样本模型对它们的预测同样犹豫于是一轮接一轮地把相似样本全部选出来标注完成的信息增益极低。解决给采样函数加上多样性约束。最简单的方式是先在无标注池上对特征向量做聚类然后每个簇按比例分配采样名额而不是全局取 top_k如果想更精细可以试试 BADGE 这类兼顾不确定性和多样性的方法。在某些例程里还会对每轮采样结果做去重——对选出的样本两两算特征余弦相似度相似度超过 0.95 的保留一条其余替换成池子里其他样本。5.2 伪标签阈值设得很高半监督却毫无收益现象把伪标签置信度阈值从 0.8 一路提到 0.95训练 loss 正常下降但验证集精度和没做半监督时完全一样白跑了一轮。原因阈值太高导致筛出来的无标注样本太少尤其是类别不均衡时尾部类别可能一条都筛不出来半监督对整体分布的覆盖几乎为零。解决把阈值从 0.7~0.8 开始设每轮打印“本轮采纳伪标签数量 / 无标注池总量”的比例。如果比例低于 5%说明阈值过于严格往下调 0.05 再试。另一个技巧是分两个阈值头部类别用 0.9尾部类别用 0.7可以通过统计无标注池里模型预测各类别的数量占比来自动确定。5.3 Mean Teacher 训练几轮后 loss 直接 nan现象学生模型和教师模型在开头的十来个 step 里 loss 还正常后面某一步 loss 突然变成 nan整个训练崩掉。原因consistency_weight设置过大一致性损失的梯度把参数推到数值溢出的区间另一种可能是 EMA 更新时alpha设得太小比如 0.9教师模型跟随学生模型太快把学生模型的尖峰噪声也继承了导致两边的预测同时发散。解决先固定alpha0.999把consistency_weight降到 0.1确认 loss 能稳定下降后再把权重往上提同时检查一致性损失的计算是不是用了平方误差且没有对教师输出做 detach——如果没有 detach梯度会同时穿过学生模型和教师模型几乎没有不崩的。含NaN的 step 保存下来的模型权重没法继续训所以要在训练脚本里加一个梯度检查一旦出现 nan 就停止当前 step 并重新加载上一个 checkpoint。这就是你给自己的后悔药。5.4 训练集指标很高验证集疯狂掉点现象主动学习选出的样本在训练集上精度逼近 98%但一上验证集立刻掉回 80%怎么看都像是过拟合。原因主动学习是一个“带偏采样”的过程它优先选模型认为难的样本而这些难样本往往来自数据分布里本来就稀疏的区域。如果把这些样本大量灌进训练集训练数据分布就和真实分布脱节了模型在真实分布上的泛化自然变差。解决把每轮的采样预算拆成两部分——70% 用主动学习策略挑难样本30% 从无标注池里完全随机抽。随机样本用来锚定真实分布保证模型不过度偏向难样本。另一个办法是在验证集构造时留一块“随机验证集”从无标注池里随机抽 500 条固定不变地作为评测基准而不是用训练集里按类分层抽样的那套。5.5 无标注池太大内存和显存直接爆掉现象按例程原样把整个无标注池加载进内存做特征提取或采样机器直接 OOM卡死重启。原因无标注池可能是几十万条样本全部做前向推理并把特征张量保留在内存里单机根本扛不住。解决不要一次性全量过。常见做法是分 batch 前向推理把每条的 logits 或特征向量降维后存成.npy落盘采样时只读取这些特征不再过模型。如果连特征存储都嫌大可以先用随机子集粗筛出大概两万条候选再做精细采样——主动学习本身是迭代式的每次只追求“比随机好一截”不需要在全部池子上做全局最优。6. 验证采样策略是否有效的小技巧拿到新数据集先跑对比脚本每次拿到新数据集别急着全量跑迭代循环。我习惯先做一个“小规模策略对比”固定一小批标注种子选三四种采样策略跑两三轮迭代只看验证集上谁涨得最多。这一步能在半天内筛掉一批不靠谱的策略省下的时间远大于投入。def benchmark_strategies(train_labeled, unlabeled_pool, valid_set, strategies(random, least_confidence, margin, entropy), seed0): 小规模采样策略横向对比 固定种子和初始标注集, 只改变采样策略, 观察验证集精度 results {} for strategy in strategies: random.seed(seed) torch.manual_seed(seed) np.random.seed(seed) model init_model() cur_train copy.deepcopy(train_labeled) cur_pool copy.deepcopy(unlabeled_pool) # 每轮只采 50 条, 跑 4 轮, 成本可控 for _ in range(4): model train_model(model, cur_train, pseudo_sourcecur_pool, epochs20) val_acc evaluate(model, valid_set) selected_ids sample_by_strategy(model, cur_pool, strategy, top_k50) cur_train merge(cur_train, selected_ids, get_annotations(selected_ids)) cur_pool remove_samples(cur_pool, selected_ids) results[strategy] val_acc return results跑这个脚本时有几个细节直接决定对比结果可信不可信。固定随机种子是第一优先级训练数据顺序、数据增强、模型初始化全部要固定否则策略之间的差异会被随机性淹没。验证集不能太小最少 300 条否则精度波动一两个点你根本分不清是策略差异还是噪声。每轮评估的模型要用“当前迭代结束后的模型”而不是冷启动模型否则对比的是策略的采样质量而不是策略在迭代中的整体收益。还有一个我踩过坑的地方对比脚本里如果用了伪标签或半监督务必确保各策略跑的是完全相同的半监督配置。有一回我在对比 margin 和 entropy 时margin 的配置里多了max_num500的伪标签上限entropy 没加结果跑了四轮后 entropy 精度反而低了两个点还以为是 entropy 不行查了半天才发现是伪标签数量不一致导致的。例程里的函数参数最好做成显式传入不要依赖函数内默认值。另外每次迭代后把选出的样本 ID 和上一轮做一次重叠率检查。重叠率超过 60% 就说明采样策略在“炒冷饭”该加多样性约束了。这个检查脚本大概十行就能写完但在实际项目里救过我很多次——数据分布一偏模型对同一批样本的“不确定性”会反复被激活没有这层检查标注预算就在重复劳动里悄悄烧光了。我希望这篇笔记能让你跑例程时少走点弯路也希望你手里的标注预算能真正花在刀刃上。本文还有配套的精品资源点击获取
返回列表