标注一致性讨论两个人是否按同一套规则判断。规则确定之后,还有另一个问题:候选池有一万条数据,预算只够标注一百条,应该先看哪一条?

主动学习让当前模型参与选择下一批需要人工标注的数据。目标是提高标签使用效率,但它不承诺“挑最难的样本一定更好”。下面先定义查询分数,再用一个能够完整枚举的阈值实验说明:选样只看当前模型与未标注特征,真实标签只能通过本轮查询取得。

先把训练池和评测集分开

本文采用 pool-based 设置:有标签集 L、未标注池 U,以及总标注预算 B。每轮训练或更新模型,从 U 选一条或一批,交给标注者,再将结果加入 L、从 U 移除。池式设置与不确定性查询的定义可对照 Settles 的原始综述第 2.3、3.1 节。

集合 允许的用途 容易发生的错误
初始有标签集 L 训练初始模型 只有一个类别,却认为模型覆盖了全部类别
未标注池 U 根据特征与模型输出选择查询 排序时偷看模拟数据的真实标签
独立开发集 比较预先约定的查询方案、确定停止规则 每轮随意挑最有利指标
最终测试集 最终一次报告目标分布上的效果 把测试样本送去训练或反复据此选策略

主动选出的训练集通常不是从目标总体独立同分布抽取的。查询集上的准确率不能直接当作线上准确率;查询集上的标注一致性也应说明抽样方式。保留一部分随机审计样本,能帮助观察被当前模型忽略的区域,但不自动消除选择偏差。

三种分数各自在比较什么

对一个 K 类样本,设模型输出概率 p_1,…,p_K,排序后前两大概率为 p_(1)、p_(2)。常见规则是:

1
2
3
4
least confidence:u_LC = 1 − max_k p_k        取最大
margin: m = p_(1) − p_(2) 取最小
entropy: H = −Σ p_k log₂(p_k) 取最大
约定 0 log₂ 0 = 0

LC 只看最可能类别,margin 看两个主要竞争者,熵使用整个分布。三者都依赖模型给出的概率;高熵本身不等于“标完就能显著改善模型”。

三分类输出 LC margin 熵 bits(约)
A=(0.50,0.49,0.01) 0.50 0.01 1.071
B=(0.50,0.25,0.25) 0.50 0.25 1.500
C=(0.80,0.10,0.10) 0.20 0.70 0.922

LC 无法区分 A、B;margin 偏好 A;熵偏好 B。不能把三分类下不同规则的排序视为同一回事。上述公式对应综述中的查询规则,本表是本站构造的算例。

二分类为什么会得到相同排序

令正类概率为 p,负类为 1−p。LC=min(p,1−p),margin=|2p−1|。二元熵为:

1
2
H(p) = −p log₂p − (1−p)log₂(1−p)
H'(p) = log₂((1−p)/p),0 < p < 1

H 在 (0,0.5) 递增、(0.5,1) 递减,并关于 0.5 对称。因此三种规则都偏好接近 0.5 的概率,除并列处理外排序一致。确定性并列规则仍应固定,否则重跑可能选到不同 ID。

这里的结论只比较同一组二分类概率,不能推出不同模型、不同校准方法也给出相同选样。

八个等先验阈值经查询 x 等于 4、6、5,候选模型数从八缩小到四、二、一;独立评测集位于查询环之外
原创版本空间图。卡片数字表示剩余候选阈值的数量,不代表神经网络参数量或真实业务样本数。查看完整图

八个阈值怎样只用三个标签区分

为了精确核验,用有限假设集合代替神经网络。候选阈值 t 为 0.5、1.5、…、7.5,共八个;模型 h_t(x)=1[x≥t]。候选查询位置为整数 1 到 7,隐藏真值设 t*=5.5。假定标签无噪声,真实规则包含在候选模型中,初始八个阈值先验等权。

版本空间 V 是与已查询标签全部一致的阈值集合。对新位置 x,以候选模型投票得到预测概率:

1
2
p(y=1 | x,V) = |{t ∈ V : x ≥ t}| / |V|
V_new = {t ∈ V : 1[x≥t] = 查询得到的 y}

该概率是本教学模型在均匀后验下的模型平均,不是已校准的真实业务概率。第一轮 x=1、2、3、4 的正类概率依次为 1/8、2/8、3/8、4/8;熵依次约为 0.544、0.811、0.954、1.000,故先查 x=4。

轮次查询 x查询前 p(1)返回 y保留阈值数量
初始———0.5,…,7.58
141/204.5,5.5,6.5,7.54
261/214.5,5.52
351/205.51

表格较宽时可以横向滚动;不要把查询前概率误写成看到标签后的概率。每次只调用一次 oracle,已有标签不重复付费。

熵何时恰好等于期望信息收益

在这个有限、无噪声、等权版本空间中,设 |V|=M,查询把它分成大小 n_0、n_1 的两组,p_y=n_y/M。结果为 y 后仍是组内均匀后验,因此:

1
2
3
E[log₂ |V_new|] = Σ_y p_y log₂ n_y
= log₂ M + Σ_y p_y log₂ p_y
log₂ M − E[log₂ |V_new|] = H(p)

空分支概率为零,其项按极限取零。此时最大预测熵确实最大化一步的期望版本空间对数缩减。离开这些假设,预测熵不再必然等于参数信息增益:例如标签本来就随机,模型即使已经知道这种随机性,输出仍可能接近 0.5。

区分八个等概率阈值需要至少 log₂8=3 个二元答案的平均信息量;本例可以每次等分,八种隐藏真值都恰好查询三次。这个结论来自有限模型的结构,不能外推成“深度模型用三条标签即可训练”。

完整标准库实验

下面既实现主动查询,也实现无放回随机查询基线。随机方案仍会查询无信息的位置;这是本次比较明确规定的基线,不是所有被动学习策略的上界。停止标准是唯一识别候选阈值,不是准确率达到某个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import itertools
import json
import math
import random
from fractions import Fraction


def numbers(values, name):
result = tuple(values)
if not result or any(type(v) not in (int, float) or
not math.isfinite(v) for v in result):
raise ValueError(name + " must be nonempty finite numbers")
if len(set(result)) != len(result):
raise ValueError(name + " must be unique")
return tuple(sorted(result))


def entropy(p):
if type(p) not in (int, float) or not math.isfinite(p) or not 0 <= p <= 1:
raise ValueError("invalid probability")
return -sum(q * math.log2(q) for q in (p, 1-p) if q)


def select(versions, pool):
# This function has no access to the oracle or hidden labels.
scored = [(entropy(sum(x >= t for t in versions) / len(versions)), x)
for x in pool]
if not scored:
return None
score, x = min(scored, key=lambda pair: (-pair[0], pair[1]))
return x if score > 0 else None


def run(thresholds, pool, oracle, strategy="entropy", seed=0, budget=None):
versions = list(numbers(thresholds, "thresholds"))
remaining = list(numbers(pool, "pool"))
if strategy not in ("entropy", "random") or type(seed) is not int:
raise ValueError("invalid strategy or seed")
if budget is None:
budget = len(remaining)
if type(budget) is not int or budget < 0:
raise ValueError("invalid budget")
rng = random.Random(seed)
trace = []
for _ in range(min(budget, len(remaining))):
if len(versions) == 1:
break
x = select(versions, remaining) if strategy == "entropy" else rng.choice(remaining)
if x is None:
break # Different thresholds may agree everywhere in this pool.
p = sum(x >= t for t in versions) / len(versions)
y = oracle(x)
if type(y) is not int or y not in (0, 1):
raise ValueError("oracle must return integer 0 or 1")
kept = [t for t in versions if int(x >= t) == y]
if not kept:
raise ValueError("labels contradict the hypothesis class")
trace.append({"x": x, "p": p, "y": y,
"before": len(versions), "after": len(kept)})
versions = kept
remaining.remove(x)
return {"queries": len(trace), "versions": versions, "trace": trace}


def exact_random_mean(thresholds, pool):
# E[T] = sum_k P(T > k); a random prefix has a uniform k-element subset.
# This exhaustive reference is for tiny synthetic problems only.
thresholds = numbers(thresholds, "thresholds")
pool = numbers(pool, "pool")
if len(pool) > 12 or len(thresholds) > 16:
raise ValueError("exact reference is limited to tiny problems")
result = Fraction(0)
for truth in thresholds:
for k in range(len(pool)):
ambiguous = 0
for subset in itertools.combinations(pool, k):
survivors = sum(all((x >= t) == (x >= truth) for x in subset)
for t in thresholds)
ambiguous += survivors > 1
result += Fraction(ambiguous, math.comb(len(pool), k) * len(thresholds))
return result


if __name__ == "__main__":
hypotheses = [i + 0.5 for i in range(8)]
pool = list(range(1, 8))
active = run(hypotheses, pool, lambda x: int(x >= 5.5))
costs = [run(hypotheses, pool, lambda x, t=t: int(x >= t))["queries"]
for t in hypotheses]
print(json.dumps({"active_truth_5_5": active, "all_truth_costs": costs,
"random_mean_exact": str(exact_random_mean(hypotheses, pool))},
ensure_ascii=False, indent=2))

实际输出的三个查询位置为 [4,6,5],八种真值的主动标签成本均为 3,随机无放回基线的精确平均成本为 5。第二个结果平均了八种等权真值和全部随机排列,不是挑一个有利随机种子。

还可独立手算随机均值:两端真值只需遇到一个关键位置,其在七个位置随机排列中的平均名次为 4;六个内部真值需要遇到相邻两个关键位置,最后一个出现的平均名次为 2×8/3=16/3。因此总平均为 (2×4+6×16/3)/8=5。这里的平均名次恒等式也由测试中的全排列枚举核验。

设候选模型数 M、池大小 U、预算 B。选样每轮扫描 U 个位置,每个位置做 M 次投票,时间上界 O(BUM),额外存储 O(U+M+B)。真实模型还需加入训练、全池推理和人工标注成本;标签省了,不代表总耗时必然省了。精确参考的枚举时间含 2^U 因子,代码限制 U≤12、M≤16,不是生产选样器。

从小实验走向实际标注要补什么

失败边界 应检查的东西
模型自信但错误 初始覆盖、独立随机审计、目标分布;熵可能根本不选这些点
标注冲突或标签噪声 硬删除可能排除真值;检查确定性位置时还可能得到空集,需噪声模型或复核机制
批量查询近重复样本 单点分数的前 B 名可能全在同一区域;批内去重、覆盖和多样性需另外设计
标注成本不同 比较相同金额或工时预算;简单“分数除以成本”也不是通用最优算法
池中无法区分剩余模型 报告尚未识别,不把预算耗尽当作模型已正确
反复用测试集决定下一轮 评测泄漏;预设开发与最终测试职责

上述实践问题可继续查阅 Settles 的实践论文。真实任务还要版本化池、模型、规则、查询 ID 和费用,并保留拒标、缺失与裁决记录。本篇没有训练神经网络,也没有给出实际任务上的标签节省比例。

自检:每轮都挑预测概率最接近 0.5 的样本,就能用查询集准确率评估线上效果吗?

不能。选样改变了数据分布,查询集可能集中在边界、噪声或重复区域。应保留独立目标分布评测;本篇熵与信息收益相等的证明还额外依赖有限、无噪声、等先验模型。

验证程序 tests/verify-active-learning-article.cjs 提取并运行全文代码,以穷举排列独立核验随机标签成本,检查 oracle 调用、预算、候选池与非法输入。接下来可对照分层指标与总体构成,再用配对 Bootstrap处理固定独立评测集上的抽样不确定性。