CH 23 / SOURCE EXAMPLE
threshold_labels.py
ch23 时长预估建模/code/ch01_threshold_labels/threshold_labels.py
这个示例讲什么
│ └── threshold_labels.py 固定阈值 / 分桶阈值 / 校准阈值 / 分位阈值 / 多阈值联合
阈值化时长 Label:EVTR / LVR / SVR / FPR 的构造与 Duration Bias(第23章 §23.1) 正文的核心链路: 播放时长 → Duration-aware 阈值 → 二值化 Label → 分类 → P(W > T(D)) 三个要点,本脚本逐一验证: 1) 固定绝对阈值 T 会让短视频天然占优(Duration Bias):同样 T=10s, 12 秒视频只要看 10 秒就为正,120 秒视频看 10 秒可能只是刚开始; 这会让模型把「物理时长短」学成「用户兴趣强」。 2) Duration 分桶经验阈值(正文表 23.2)能显著拉平各 Duration 分桶的正样本率。 3) 用分位数阈值 T_k = Q_α(W | D ∈ B_k) 可以精确把每个桶的正样本率钉在 1−α, 代价是阈值随数据漂移。 另外核对正文 §2
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
bucket_index· 第 38 行返回每条样本所属的分桶编号;超出最后一个桶的归入最后一桶。bucket_threshold· 第 44 行按分桶取阈值:which=2 为 EVTR,3 为 LVTR。rate_by_bucket· 第 51 行main· 第 61 行
1"""2阈值化时长 Label:EVTR / LVR / SVR / FPR 的构造与 Duration Bias(第23章 §23.1)34正文的核心链路:5 播放时长 → Duration-aware 阈值 → 二值化 Label → 分类 → P(W > T(D))67三个要点,本脚本逐一验证:81) 固定绝对阈值 T 会让短视频天然占优(Duration Bias):同样 T=10s,9 12 秒视频只要看 10 秒就为正,120 秒视频看 10 秒可能只是刚开始;10 这会让模型把「物理时长短」学成「用户兴趣强」。112) Duration 分桶经验阈值(正文表 23.2)能显著拉平各 Duration 分桶的正样本率。123) 用分位数阈值 T_k = Q_α(W | D ∈ B_k) 可以精确把每个桶的正样本率钉在 1−α,13 代价是阈值随数据漂移。1415另外核对正文 §23.1 的四个工业 Label 定义(EVR / LVR / SVR / FPR)。1617运行:python3 ch01_threshold_labels/threshold_labels.py18"""1920from __future__ import annotations2122import pathlib23import sys2425import numpy as np2627sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))2829from common import make_watch_data, table # noqa: E4023031SEED = 20263233# 正文表 23.2 的 Duration 分桶经验阈值(秒)34BUCKETS = [(0, 30, 5, 15), (30, 60, 10, 30), (60, 120, 20, 60),35 (120, 300, 30, 90), (300, 600, 40, 120)]363738def bucket_index(d: np.ndarray) -> np.ndarray:39 """返回每条样本所属的分桶编号;超出最后一个桶的归入最后一桶。"""40 edges = np.array([b[1] for b in BUCKETS], dtype=np.float32)41 return np.searchsorted(edges, d, side="right").clip(0, len(BUCKETS) - 1)424344def bucket_threshold(d: np.ndarray, which: int = 2) -> np.ndarray:45 """按分桶取阈值:which=2 为 EVTR,3 为 LVTR。"""46 k = bucket_index(d)47 thr = np.array([b[which] for b in BUCKETS], dtype=np.float32)48 return thr[k]495051def rate_by_bucket(d: np.ndarray, y: np.ndarray, k: np.ndarray) -> list[list]:52 rows = []53 for i, (lo, hi, t_e, t_l) in enumerate(BUCKETS):54 m = k == i55 rows.append([f"[{lo},{hi})", int(m.sum()),56 f"{y[m].mean():.3f}" if m.any() else "—",57 f"{t_e}/{t_l}"])58 return rows596061def main() -> None:62 print("阈值化时长 Label:EVTR / LVR / SVR / FPR 与 Duration Bias(§23.1)\n")63 data = make_watch_data(n=20000, dim=16, seed=SEED)64 d, w = data["d"], data["w"]65 k = bucket_index(d)6667 # ---------------- 1) 固定阈值的 Duration Bias ----------------68 print("① 固定绝对阈值 T=10s:各 Duration 分桶的正样本率")69 print(" (正文命题:相同观看秒数对不同物理时长的内容意义不同)")70 y_fix = (w > 10).astype(np.float32)71 rows = rate_by_bucket(d, y_fix, k)72 table(["Duration 分桶", "样本数", "P(W>10s)", "本桶 EVTR/LVTR 阈值"], rows)73 short = rows[0][2]74 long_ = rows[-1][2]75 print(f"\n 最短桶正样本率 {short},最长桶只有 {long_}——差 "76 f"{float(short) - float(long_):.2f}。")77 print(" 这个差不是用户对短视频更感兴趣,而是短视频更容易达到 10 秒。")78 print(" 模型若直接学这个 Label,会把 duration 当成强特征,形成 Duration Bias。\n")7980 # ---------------- 2) 分桶经验阈值 ----------------81 print("② Duration 分桶经验阈值(正文表 23.2):同样看各桶正样本率")82 y_evtr = (w > bucket_threshold(d, 2)).astype(np.float32)83 rows = rate_by_bucket(d, y_evtr, k)84 table(["Duration 分桶", "样本数", "P(W>T_EVTR(D))", "本桶 EVTR/LVTR 阈值"], rows)85 rates = np.array([float(r[2]) for r in rows])86 print(f"\n 分桶后正样本率落在 {rates.min():.3f}~{rates.max():.3f},极差 "87 f"{rates.max() - rates.min():.3f}")88 print(f" ——比固定阈值的 {float(short) - float(long_):.3f} 还大。这不奇怪:")89 print(" 表 23.2 里最短桶用 5 秒阈值(比 10 秒更松),最长桶用 40 秒(更严),")90 print(" 两端同时朝同一方向推,偏差自然被放大。也就是说,「分桶」本身不消除")91 print(" Duration Bias,只有「校准过的分桶阈值」才会。\n")9293 print(" 按数据校准:为每个桶搜索一个阈值,使该桶正样本率都等于固定阈值下的整体")94 target = float(y_fix.mean())95 calib = []96 for i in range(len(BUCKETS)):97 m = k == i98 if not m.any():99 continue100 cand = np.linspace(0.0, float(np.quantile(w[m], 0.99)), 400)101 err = [abs(float((w[m] > t).mean()) - target) for t in cand]102 t_best = float(cand[int(np.argmin(err))])103 calib.append((i, t_best, float((w[m] > t_best).mean())))104 rows = [[f"[{BUCKETS[i][0]},{BUCKETS[i][1]})", f"{BUCKETS[i][2]}",105 f"{t:.1f}", f"{r:.3f}"] for i, t, r in calib]106 table(["Duration 分桶", "表 23.2 的 EVTR 阈值", "校准后阈值", "校准后正样本率"], rows)107 gap = max(r for _, _, r in calib) - min(r for _, _, r in calib)108 print(f"\n 校准后各桶都落在 {target:.3f} 附近,极差 {gap:.3f}。")109 print(" 这才是分桶阈值的正确用法:阈值不是拍脑袋定的经验值,而是对着业务目标")110 print(" (各桶「有效观看」的比例一致)反解出来的。表 23.2 给的是一组起点,")111 print(" 真实系统每季度都要用最新数据重新解一次。\n")112113 # ---------------- 3) 分位数阈值 ----------------114 print("③ 用分位数阈值 T_k = Q_α(W | D∈B_k) 精确控制各桶正样本率")115 rows = []116 for alpha in (0.5, 0.6, 0.7):117 ok = 0118 for i in range(len(BUCKETS)):119 m = k == i120 if not m.any():121 continue122 t = np.quantile(w[m], alpha)123 r = float((w[m] > t).mean())124 ok += int(abs(r - (1 - alpha)) < 0.02)125 rows.append([f"{alpha:.1f}", f"{1 - alpha:.1f}", f"{ok}/{len(BUCKETS)}",126 "阈值由数据决定,需随分布漂移重估"])127 table(["α(分位)", "期望正样本率", "达标桶数", "备注"], rows)128 print("\n 分位阈值的优点是「每个桶的正样本率被钉死」,模型不会因为某桶")129 print(" 天然正样本多而偏向它;代价是阈值完全跟随数据,一旦观看分布漂移,")130 print(" 旧阈值立即失效。工业上通常按季度/半年重新拟合一次。\n")131132 # ---------------- 4) 正文的四个工业 Label ----------------133 print("④ 正文 §23.4 的四个工业 Label 定义(EVR/LVR/SVR/FPR)在本数据上的比例")134 lvr_thr = np.where(d <= 3, 10.0,135 np.where(d <= 40, 0.8 * d, 50.0))136 labels = {137 "EVR(playing_time ≥ 12s)": (w >= 12),138 "LVR(分段阈值)": (w > lvr_thr),139 "SVR(0 < playing_time ≤ 3s)": ((w > 0) & (w <= 3)),140 "FPR(duration>3s 且看完)": ((d > 3) & (w >= d)),141 }142 rows = []143 for name, y in labels.items():144 yf = y.astype(np.float32)145 rows.append([name, f"{yf.mean():.3f}",146 f"{yf[d < 30].mean():.3f}", f"{yf[d >= 120].mean():.3f}"])147 table(["Label", "整体正样本率", "短视频(<30s)", "长视频(≥120s)"], rows)148 gap_evr = labels["EVR(playing_time ≥ 12s)"][d < 30].mean() \149 - labels["EVR(playing_time ≥ 12s)"][d >= 120].mean()150 gap_lvr = labels["LVR(分段阈值)"][d < 30].mean() \151 - labels["LVR(分段阈值)"][d >= 120].mean()152 print(f"\n EVR 的短/长差距 {gap_evr:+.3f},LVR 是 {gap_lvr:+.3f}——")153 print(" 分段阈值没有把差距压小,反而略大。看阈值本身:短视频的中位时长约 21 秒,")154 print(" LVR 阈值 0.8d ≈ 17 秒,比 EVR 的 12 秒严 5 秒;长视频中位约 154 秒,")155 print(" 但 LVR 在 d>40 后封顶在 50 秒,比 12 秒严 38 秒。")156 print(" 两边都变严,但长桶严得多,于是长桶的正样本率掉得更多,差距被放大。")157 print(" 这说明分段阈值好不好,取决于阈值函数和真实观看分布是否匹配,")158 print(" 而不是「分了桶就一定更好」——这一段用的是正文 §23.1 的示意口径,")159 print(" 真实系统里这组参数要按 ② 的方法对着业务目标校准。")160 print(" SVR(≤3 秒)在长视频上反而更高(0.415 vs 0.392):本数据里短视频的")161 print(" 观看时长受物理时长约束,不容易落进 3 秒以内,长视频则有一批「划走」。")162 print(" 想用它当辅助信号,必须先按 Duration 校正,否则它衡量的其实是 Duration。")163 print(" FPR 是完播率口径,短桶确实更高,正文提醒的 Metric Hacking 正是指")164 print(" 只优化这一条时,模型会系统性地把长视频排在后面。\n")165166 # ---------------- 5) 多阈值联合建模 ----------------167 print("⑤ 多阈值联合建模:同一批样本上的 4 个嵌套 Label")168 thresholds = [3.0, 10.0, 30.0, 60.0]169 Y = np.stack([(w > t).astype(np.float32) for t in thresholds], axis=1)170 nested = np.all(np.diff(Y, axis=1) <= 0) # 是否处处单调不增171 print(f" 阈值 {thresholds} → Label 矩阵形状 {Y.shape}")172 print(f" 正样本率:{[f'{Y[:, j].mean():.3f}' for j in range(Y.shape[1])]}")173 print(f" 单调(y_1 ≥ y_2 ≥ y_3 ≥ y_4)的样本占比:{nested.mean():.1%}")174 print(" 这组 Label 合起来就是生存函数 S(t)=P(W>t) 在 4 个点的采样,")175 print(" 正文说的「多阈值逐步逼近完整时长分布」在数据上就是逐点变稀疏的 0/1 矩阵。\n")176177 print("⑥ 一句话:时长 Label 的起点不是模型,而是「什么算有效观看」。")178 print(" 固定阈值简单但带 Duration Bias,分桶阈值校准了难度,")179 print(" 分位阈值把正样本率钉死但依赖数据分布——三者是一条连续谱。")180181182if __name__ == "__main__":183 main()如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。