# 怎么判断一个选片器到底行不行 > 这一篇写给完全没有算法背景的读者。它是整个项目里最重要的一篇 —— > 因为第三版失败的根本原因,就是从头到尾没有做这件事。 ## 为什么「看起来挺准的」不能作数 假设有 309 张照片,你自己精心挑了 20 张最喜欢的当答案。 现在一个 AI 挑了 20 张,跟你的答案对上了 3 张。 3/20 听起来还行?**闭着眼睛乱挑,期望也能对上 1.3 张。** 因为你挑的 20 张占了全池的 6.5%,AI 随便挑 20 张,按比例本来就会撞上 1.3 张。 从 1.3 到 3 的这点提升,是真本事还是运气?必须用数字回答,不能靠感觉。 --- ## 三个必须报的数字 ### 1. AUC —— 排序能力有多强 **一句话定义**:随机拿一张「你喜欢的」和一张「你没选的」,AI 给前者打分更高的概率。 ``` AUC = 0.5 完全等于掷硬币,AI 一点用没有 AUC = 0.6 有一点信号,但很弱 AUC = 0.7 明显有用 AUC = 0.9 很强 AUC = 1.0 完美,你喜欢的全部排在前面 ``` **为什么不用「准确率」?** 因为 309 张里只有 20 张是你喜欢的。 一个啥也不干、把所有照片都判为「不喜欢」的模型,准确率是 93.5%。 这个数字毫无意义。AUC 不受这种正负比例失衡的影响。 **AUC 比「前 20 命中几张」更稳定**,因为命中数只看前 20 名这条线,波动很大 (20 张金标时标准差约 1.07,所以 4 张和 2 张只差 1.9σ,不是决定性差异)。 **但 AUC 不能单独作数 —— 它和产品要交付的东西不是一回事。** 我们在这个项目里实测到一个反直觉的现象:把两个都有效的模型加权融合, **AUC 变好了,前 20 反而变差,而且跨 K=10/20/30/40/50 一致:** | | AUC | 头部平均提升(相对随机) | |---|---:|---:| | laion_aes 单独 | 0.583 | 2.47x | | topiq_nr-face 单独 | 0.606 | **2.51x** | | 两者秩融合 | **0.624** | 2.01x ← AUC 更高,头部更差 | 原因是两个模型**互补**而非一致:它们各自在前 20 里命中 4 张金标,但只重叠 2 张。 平均两个互补的排序器,会让各自笃定的头部被对方的「无所谓」拉下来 —— 中间那一大段排序变好了,最上面那一小截被稀释了。 **教训:产品交付什么,就得测什么。** 交付「前 20 张」,就不能只看 AUC。 ### 2. 超几何 p 值 —— 这个成绩会不会只是运气 p 值回答:**如果 AI 完全是瞎猜,它能碰巧考出这个成绩(或更好)的概率是多少?** ``` p = 0.13 有 13% 的概率是运气 → 不能下结论 p = 0.03 只有 3% 的概率是运气 → 可以认为真的有效果 ``` 习惯上以 0.05 为界。309 张挑 20 张的具体门槛: | 命中 | p 值 | 判定 | |---:|---:|---| | 1 | 0.75 | 就是运气 | | 3 | 0.13 | 区分不开 | | **4** | **0.03** | ✅ 过线 | | 8 | 0.00001 | 明显有效 | **所以「3/20」和「4/20」不是差一张的区别,是「不能下结论」和「可以下结论」的区别。** ### 3. 头部提升(lift@K)—— 比单个命中数稳的头部指标 单看「前 20 命中 4 张」波动太大。把 K 取 10/20/30/40/50 各算一次 「命中数 ÷ 随机期望」再平均,得到一个稳得多的头部指标: ``` 1.0x = 等于随机 2.5x = 头部的金标密度是随机的 2.5 倍 ``` 这个数既反映产品真正交付的东西(前 K 张),又不像单个 K 那样容易被抽样噪声骗。 ### 4. 噪声底 —— 你的尺子本身晃不晃 这是最容易被忽略、也是第三版栽的地方。 做法很简单:**把同一张照片重新评一次分,看两次差多少。** 重复几十张,算标准差。 第三版实测: ``` 309 张不同照片的分数标准差 6.72 ← 照片之间的真实差异 同一张照片重评的分数标准差 7.28 ← 尺子自己的抖动 ``` **尺子的抖动比它要测的差异还大。** 这个时候,无论后面接多精巧的排序算法、 多严格的审计流程,输出都是随机的。 > 打个比方:你用一把每次读数上下浮动 7 厘米的尺子,去量一群身高相差 6 厘米的人, > 然后花很大力气设计了一套「双人复核 + 交叉验证 + 独立审计」的流程来排名。 > 流程再严谨,排出来的名次也是随机的 —— 问题在尺子,不在流程。 **任何打分系统上线前,第一件事就是测自己的噪声底。** ### 补记:这个案例本身有混淆(2026-09 修正) 上面那两个 σ 是真实测量,没有问题。**有问题的是当时对它的归因。** 当时的结论写成「打分这种输出形式不可靠」,据此把排序整个挪回本机。 后来复查发现:那次实测发给模型的是 512px 整幅图,而这批照片在这个尺寸下 **人脸中位数只有 45 像素**(25 分位 34px,最小 21px)—— 100% 低于本项目自己后来在 `sent_image_check.py` 里定的 96px 门槛。 让模型在 45 像素的脸上判断「眼神有没有神」,重评方差大是必然的。 那测到的是**「看不见时的猜测方差」**,不是「打分机制的固有噪声」。 **所以正确的教训不是「不要用打分」,而是:** > 测到一个部件表现差时,先确认**它拿到的输入是否足以完成任务**, > 再去归因到它的机制。否则你会把「没给够信息」错记成「这个方法不行」, > 然后基于这个错误结论重构整个系统。 这个混淆在项目里留了三个月,写进了 agent 人设、README 和三份 profile, 每次运行都讲给用户听。**发现它的不是复盘,是用户的一句反问。** 本地模型是确定性的,噪声底为 0,这本身就是选择本地方案的一个理由。 --- ## 怎么做出一个能用的测试集 这一步做错,上面三个数字全都作废。 1. **答案必须是你自己挑的**,不能是任何模型挑的。 2. **答案必须留在候选池里。** 如果你把 20 张精选照片移到了另一个文件夹, AI 面对的池子里根本没有正确答案,重合率必然是 0,而且你会误以为是 AI 不行。 > 这个坑我们真的踩过:一轮 289 张的运行跑完后才发现答案照片当时已经被移走了, > 整轮数据只能当过程证据,重合率作废。 3. **答案不能被 AI 看到。** 如果答案子目录还在照片文件夹里, 必须在**枚举、指纹、匿名 ID 生成之前**就排除掉 —— 排除晚一步都可能泄漏。 4. **测完不能拿答案回去调参。** 看着答案调参数直到重合率变高,那叫过拟合, 换一批照片就原形毕露。要调参就必须留出一部分答案不看。 --- ## 学习曲线:需要多少张标注 如果方案是「从用户自己的标注里学口味」,还要回答一个产品问题: **要用户标几张才够?** 做法:随机取 m 张答案当训练,剩下的答案 + 没见过的负样本当测试,重复几百次取平均。 **关键:正样本和负样本都要留出。** 只留出正样本(负样本全在训练里见过)会让 AUC 虚高 —— 这是我们第一版探针犯的错, 修正后差别不大,但协议必须是对的 —— 而且这只是两个协议问题里较小的那个,见下文「坑二」。 本项目实测(v4): | 标注数 | 留出 AUC | 冷启动对照 | 胜过冷启动 | 说明 | |---:|---:|---:|---:|---| | 3 | 0.555 | 0.606 | 27% | 太少,反而更差 | | 5 | 0.610 | 0.613 | 52% | 打平 | | 10 | 0.673 | 0.605 | 77% | 明显有用 | | 15 | 0.713 | 0.582 | 78% | 还在涨 | 这条曲线直接决定产品形态:**10 张就够用,而且用户浏览照片时顺手就标完了, 不需要单独的引导流程;少于 8 张则应该自动退回不用标注的方案。** **还有一件比「标几张」更重要的事:标注怎么选。** 同样标 10 张 —— 分散在整批照片里是 0.670,连着标旅程前半段是 0.439(比不标还差,比随机还差)。 因为图像向量强烈编码「场景内容」,只标一个地点,模型学到的是 「像那个地方的照片」而不是「好照片」。任何「从用户标注里学」的方案都必须测这一项。 --- --- ## 最容易骗到自己的两个坑(我们两个都踩了) ### 坑一:把训练数据算进成绩 如果方案是「从用户标注里学」,那**被标注的照片必须从评测里剔除**。 我们第一版 `eval` 没剔除,加上「用户标过的直接置顶」这条产品逻辑, 结果是:给它 10 张答案当标注,它报出 **AUC 0.759、前 20 命中 10/20** —— 而那 10 张正是喂进去的答案。这个数字完全是自欺欺人。 剔除之后,同一次运行的真实成绩是 AUC 0.519。 ### 坑二:训练协议和产品设定不一样 这个更隐蔽,而且我们直到把工具跑起来才发现。 做学习曲线时,标准做法是「随机取 m 张答案训练,剩下的答案留出来测」。 但这里有个陷阱:**留出的那些答案,在训练时应该算什么?** - **研究协议**:把它们完全排除在训练之外。测的是「CLIP 特征能不能分开这个人的口味」。 - **产品现实**:用户只标了 10 张,另外 10 张好照片就躺在「未标注」里, 被当成负样本喂进去 —— 模型被明确训练成「这些不喜欢」。 两个协议差多少? ``` 研究协议(留出的金标不参与训练) AUC 0.723 产品设定(未标注的金标当负样本) AUC 0.673 ← 差 0.05 ``` 0.05 看着不大,但它会被别的错误放大。我们最初把迭代步数设成 3000, 研究协议下几乎看不出问题(模型看不到「被当成负样本的好照片」,过拟合的代价被藏起来了), 产品协议下直接掉到 0.52。 **研究协议测出来的是乐观上界,不是产品能交付的东西。** 10 个正样本对 768 个维度,模型有足够容量去记住那 10 张、 把其他所有照片(包括同样好的)一起压下去。 **教训:评测协议必须复刻产品的真实数据条件。** 少一个负样本、多一次数据泄漏,结论就完全不同。 --- ## 一份合格的评测报告长什么样 ``` === 评测 · cold 模式 · 冷启动指标 face === AUC 0.606 (0.5 = 掷硬币) 前 20 命中 4/20 随机期望 1.29 超几何 p 值 0.0316 ✅ 显著 头部提升 2.51x (K=10/20/30/40/50 平均,相对随机) 候选 309 张 · 耗时 315s · 付费调用 0 次 ``` 四行,缺一不可:**排序能力、命中、是不是运气、代价。** `ranker` 直接内置了这个命令: ```bash photofilter-rank eval <照片文件夹> --gold 你的答案.txt --target 20 ```