☰
主动学习+半监督学习:用最少标注训练高精度模型
2026/10/1 14:26:03 网站建设 项目流程

简介:压缩包围绕主动学习与半监督学习提供MATLAB算法例程,面向需要处理标记数据稀缺问题的机器学习学习者与研究人员。包内包含1个fenleng.m源代码文件,整体仅9KB,轻量简洁,适合单独研读与二次修改。文件内容可能融合不确定性采样、多样性查询、自训练、协同训练等经典策略,并涵盖特征提取、降维、交叉验证等数据预处理与评估步骤,有助于理解从数据准备到模型性能评估的完整闭环。在文本分类、图像识别等应用场景中,这类方法尤其能利用大量未标注数据提升模型表现;实际动手运行代码,可直观体会主动学习如何智能挑选待标注样本、半监督学习如何借助无标注数据增强泛化能力,同时积累MATLAB机器学习项目的调试与优化经验。目前已有195人学习下载,适合入门巩固与算法对比研究的学习者选用。

1. 主动学习和半监督学习的算法例程,能让你用最少的标注拿到能上线的模型

手里只有几百条标注数据,模型却要达到几千条标注才能跑出的效果,这是算法落地时最常见的尴尬。主动学习和半监督学习是解决这个问题的两条腿:主动学习让模型自己从无标注池里挑出最该被标注的样本,把人工标注的每一分钱花在刀刃上;半监督学习则让模型从海量无标注数据里自己找规律,不花一分钱就能白嫖一部分精度。把这两套思路揉进同一套例程,就能形成一条靠谱的落地路径:先小额标注冷启动,再迭代拉高召回。这套算法例程正是干这件事的,里面一般会覆盖不确定性采样、委员会投票、伪标签、一致性正则这些主流方法。适合谁?适合正在做数据标注预算控制、或者想把模型精度再往上顶一截的算法工程师和数据团队。下面我按实际落地顺序,把这套东西拆开讲。

2. 主动学习核心采样策略:先搞清楚让模型“开口要”哪批数据

主动学习不是简单地把模型不确定的样本拿出去标注就完事了,关键在“不确定”怎么定义。同样是挑500条数据交给标注员,策略选错,模型学完可能纹丝不动;策略选对,精度能直接拉出三五个点。先讲三个最容易复现、也是例程里最常见的不确定性采样实现。

2.1 最小置信度、间隔采样和熵:三个不确定性指标怎么选

最基础的采样逻辑是:让模型对无标注池里的每一条样本都做一次预测,然后按“模型对自己预测的信心”排序,把最没把握的挑出来。实际例程里最常出现的就是下面这段代码,它把三种不确定性指标统一在一个函数里:

import torch def uncertainty_sampling(model, unlabeled_loader, strategy="least_confidence", top_k=500, device="cuda"): """ 主动学习: 不确定性采样 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, dim=1) if strategy == "least_confidence": conf, _ = torch.max(probs, dim=1) score = 1.0 - conf # 最大类概率越接近1, 说明模型越有把握, 得分越低 elif strategy == "margin": sorted_probs, _ = torch.sort(probs, dim=1, descending=True) score = sorted_probs[:, 0] - sorted_probs[:, 1] # top1和top2概率差越小, 说明模型越犹豫 elif strategy == "entropy": logp = torch.log(probs.clamp(min=1e-9)) score = -(probs * logp).sum(dim=1) # 熵越大, 分布越平均, 越不确定 scores.extend(score.tolist()) indices.extend(idx.tolist()) # 分数最高(最不确定)的top_k个样本优先送标注 selected = sorted(zip(indices, scores), key=lambda x: x[1], reverse=True)[: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_k=500, device="cuda"): """ 主动学习: 委员会投票采样 用多个模型的预测分歧度衡量样本价值 """ 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), dim=1) preds.append(probs.cpu()) all_probs.append(torch.cat(preds, dim=0)) # 对多个模型的预测取平均, 再算熵 avg_probs = torch.stack(all_probs).mean(dim=0) logp = torch.log(avg_probs.clamp(min=1e-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, threshold=0.8, max_num=None, device="cuda"): """ 半监督: 伪标签生成 只保留模型置信度高于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, dim=1) conf, pred = torch.max(probs, dim=1) mask = conf >= threshold pseudo_x.append(inputs[mask].cpu()) pseudo_y.append(pred[mask].cpu()) pseudo_x = torch.cat(pseudo_x, dim=0) pseudo_y = torch.cat(pseudo_y, dim=0) 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, alpha=0.999, consistency_weight=1.0, device="cuda"): """ 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, dim=1) # 学生对其中一种增强的预测, 要与教师目标对齐 s_ul_logits = student(x_ul2.to(device)) s_ul_probs = torch.softmax(s_ul_logits, dim=1) 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, alpha=1 - 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, budget=1000, rounds=6, batch_size=500, seed=0): """ 主动学习 + 半监督学习 联合迭代主循环 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_source=None, warm_start_model=model) for r in range(rounds): # 1. 半监督扩增: 从无标注池里生成伪标签/一致性目标, 参与本轮训练 model = train_fn(train_labeled, pseudo_source=unlabeled_pool, warm_start_model=model) # 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_k=batch_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),教师模型跟随学生模型太快,把学生模型的尖峰噪声也继承了,导致两边的预测同时发散。

解决:先固定alpha=0.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"), seed=0): """ 小规模采样策略横向对比 固定种子和初始标注集, 只改变采样策略, 观察验证集精度 """ 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_source=cur_pool, epochs=20) val_acc = evaluate(model, valid_set) selected_ids = sample_by_strategy(model, cur_pool, strategy, top_k=50) 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_num=500的伪标签上限,entropy 没加,结果跑了四轮后 entropy 精度反而低了两个点,还以为是 entropy 不行,查了半天才发现是伪标签数量不一致导致的。例程里的函数参数最好做成显式传入,不要依赖函数内默认值。

另外,每次迭代后把选出的样本 ID 和上一轮做一次重叠率检查。重叠率超过 60% 就说明采样策略在“炒冷饭”,该加多样性约束了。这个检查脚本大概十行就能写完,但在实际项目里救过我很多次——数据分布一偏,模型对同一批样本的“不确定性”会反复被激活,没有这层检查,标注预算就在重复劳动里悄悄烧光了。我希望这篇笔记能让你跑例程时少走点弯路,也希望你手里的标注预算能真正花在刀刃上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询