# PhotoFilter Ranker (v4) > 从一堆旅行照片里挑出最值得留下的那几张。**全程在你自己电脑上跑,不上传照片,不花钱。** --- ## 这是解决什么问题的 旅行回来 500 张照片,你想挑 20 张发朋友圈。 最烦的不是修图,是**选**:同一个瞬间连拍了 15 张,看起来几乎一样; 大部分照片技术上都没毛病,但只有少数几张真的好看;一张张放大对比要花掉一整个晚上。 市面上的「AI 选片」大多在做另一件事 —— **排除坏片**(糊的、闭眼的、过曝的)。 但你的问题不是坏片太多,是**好片太多,得从好里挑更好**。 --- ## 我们的方案(以及为什么是这个方案) 最自然的想法是:把照片发给能看图的 AI 大模型,让它打分,取前 20。 **我们试过了,花了 997 次付费调用,结果等于掷硬币。** 原因很实在:让 AI 在 0–100 的标尺上给照片打绝对分,它每次调用都在重新想象这把尺子。 同一张照片评两次,分数能差出 7.28 分 —— 而 309 张真实旅行照片彼此的差距只有 6.72 分。 **尺子的抖动比要测的差异还大。**(完整诊断见 [v3 版本记录](../docs/versions/V3-agent-rubric.md)) ### 我们试了 7 个不用大模型的方案,赢的那个最朴素 | 方案 | AUC(0.5 = 掷硬币) | 说明 | |---|---:|---| | liqe | 0.449 | 通用画质模型 | | clipiqa+ | 0.473 | 通用画质模型 | | nima(AVA 美学) | 0.481 | 通用美学模型 | | **付费大模型六维打分** | **0.497** | ← 997 次调用 | | topiq_iaa | 0.512 | 通用美学模型 | | 全局清晰度 | 0.517 | 整张图锐不锐 | | musiq-ava | 0.548 | 通用美学模型 | | laion_aes | 0.583 | CLIP 向量 + 陌生人审美打分头 | | topiq_nr-face | 0.606 | 专门评人脸的模型 | | **Apple Vision 人脸质量** | **0.723** | ← **最终采用,macOS 自带,免费** | **赢的是 macOS 系统自带的那个,不是任何一个下载来的模型,更不是付费大模型。** ### 为什么这么朴素的东西反而最好 因为**它被训练来回答的,正好是我们在问的那个问题**。 `VNDetectFaceCaptureQualityRequest` 是苹果为「实况照片选关键帧」「连拍选最佳」 做的模型 —— 它学的是「同一张脸的多次拍摄里,哪一次拍得更好」。 而一次旅行的 309 张照片,问的正是「这一组差不多的人像里哪张最好」。 其他模型学的是「任意图片好不好看」(风景、艺术、食物都在训练集里), 那是另一个问题。 三个佐证: - **它只看脸。** 全局清晰度和人脸质量的相关系数只有 **−0.318** —— 一张图可以整体很锐但脸是糊的,而人挑照片时看的是脸。 - **它天生是比较用的。** 苹果自己说这个分适合比较同一主体的多次拍摄, 不适合当跨场景的绝对分。我们要做的正是同一批内部排序。 - **信噪比 21.2**(照片间 σ=13.94,重复扫描 σ=0.66)。 对照 v3 的付费打分:6.72 / 7.28 = **0.92**。差了 23 倍。 **结论:任务足够窄的时候,任务对口的小模型 > 通用大模型。** ### 再加一道免费的闭眼门 苹果 Vision 给眼睛轮廓坐标,算个高宽比就能判断睁没睁(阈值 0.22,实测标定)。 不加这道门时,选出的 20 张里有 **6 张是闭眼的**(全池基准率 8.2%), 而用户自己挑的 20 张里闭眼 **0 张**。加上之后: ``` 不加门 交付 3/20 p=0.1299 ⚠ 与运气区分不开 加门 交付 4/20 p=0.0316 ✅ 统计显著 ``` **从「跟运气区分不开」跨到「统计显著」,代价是零。** ## 它具体怎么工作 ``` 你的照片文件夹 │ ├─ 1. 扫描 + 降采样 原图 40MP 太大,缩到 1024px 存一份缓存(309 张约 60 秒) │ ├─ 2. 算 CLIP 向量 每张变成 768 个数字(0.084 秒/张,全池 26 秒) │ ├─ 3. 打分 │ ├─ 标注 <5 张 → 冷启动:人脸质量 或 laion_aes(按人脸检出率自动选) AUC ~0.61 │ ├─ 5–11 张 → 两者线性过渡(样本太少时探针方差大) │ ├─ ≥12 张 → 你自己的口味模型 AUC ~0.67–0.71 │ └─ 护栏:标注太集中 → 退回冷启动并警告(见下) │ ├─ 4. 场景分组 用 CLIP 向量的余弦相似度找连拍/近重复(免费,向量已经有了) │ └─ 5. 按分数取前 N 每个场景组最多取 2 张,避免 20 张里 15 张是同一个瞬间 ``` **整条链路是确定性的**:同样的照片、同样的标注,永远给同样的结果。 这一点比 AUC 数字更重要 —— 因为可复现,才谈得上「改进」。 --- ## 上手 ### 装环境 需要 Python 3.9+,macOS / Linux 都行(Mac 上会自动用 GPU 加速)。 ```bash cd ranker python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt ``` 第一次运行会自动下载模型(CLIP 约 900MB,其余约 400MB),之后就不用再下了。 ### 最简单的用法:直接挑 20 张 ```bash python -m photofilter_rank.cli pick ~/Pictures/冰岛旅行 --target 20 ``` 输出: ``` 候选 309 张 · 指纹 ca88cc18 · device=mps 缓存完成 309 张,58s embedding 完成 (309, 768),26s 模式 cold(0 张标注)· 309 张候选 · 41 个场景组 · 315.0s 选出 20 张: 1. DSCF9127.JPG +2.31 2. DSCF8993.JPG +2.04 ... ``` ### 关于「告诉它你喜欢什么」—— 实测无效,默认关闭 这本来是 v4 的核心卖点:从你标过的照片里学你的口味。做完之后按 **产品真正交付的前 20 张**去测,结论是没有收益: | 方案 | 交付前 20 命中 | AUC | |---|---:|---:| | 忽略标注(默认) | **1.83 ± 0.97** | 0.714 | | 融合个人口味探针 | 1.57 ± 0.80 | **0.754** | 30 次随机划分里,融合只赢 7 次、打平 13 次、落后 10 次。 **AUC 更高但交付没变好** —— 和之前融合两个通用美学指标时是同一个模式: 全局排序改善了,最上面那一小截被稀释了。 所以工具仍然接受 `--labels`,但**默认不用它排序**,并会明确告诉你这一点: ``` ⚠ 收到 10 张标注,但**没有用于排序**。实测个人口味探针在交付的前 20 张上 没有可测收益(融合 1.57 vs 冷启动 1.83,30 次划分里只赢 7 次),所以默认关闭。 ``` 想强行启用,在 `config.py` 里设 `use_probe=True`。 代码和测量都留着 —— 这个负面结果本身是产出。 > 如果你真要启用:标注必须**分散在整批照片里**。实测连着标旅程前半段的 10 张, > AUC 只有 0.439 —— 比不标(0.72)差,比随机(0.5)还差, > 因为图像向量强烈编码场景内容,模型会学成「像那个地方的照片」而不是「好照片」。 > 工具会自动检测并拒绝过于集中的标注。 ### 想知道它到底准不准 如果你手上有一份「标准答案」(自己认真挑出来的精选),可以直接测: ```bash python -m photofilter_rank.cli eval ~/Pictures/冰岛旅行 \ --gold 我的精选.txt --target 20 ``` ``` === 评测 · cold 模式 · 冷启动指标 vision_face === ── 实际交付的名单(产品给用户的就是这个)── 交付命中 4/20 共交付 20 张 随机期望 1.29 超几何 p 值 0.0316 ✅ 显著 ── 排序本身(不含同组限流)── AUC 0.723 (0.5 = 掷硬币) 头部提升 2.83x (K=10/20/30/40/50 平均,相对随机) 候选 309 张 · 付费调用 0 次 ``` **注意报告分成两半**:产品交付的是过了同组限流的名单,不是按分数的前 K。 只报后者会高估 —— 我们真的踩过(报 4/20 而实际交付 3/20)。 这五个数字的含义见 [怎么判断一个选片器行不行](../docs/MEASUREMENT.md)。 特别注意「头部提升」—— 只看 AUC 会选错方案,我们真的踩过这个坑。 ### 想知道该标几张 ```bash python -m photofilter_rank.cli curve ~/Pictures/冰岛旅行 --gold 我的精选.txt ``` ``` 协议 production(复刻产品真实条件) 标注数 探针 AUC 冷启动 胜过冷启动 -------------------------------------------------- 3 0.555±0.063 0.606 27% 5 0.610±0.064 0.613 52% 10 0.673±0.076 0.605 77% ✅ 15 0.713±0.090 0.582 78% ✅ ``` `--protocol research` 会给出更好看的数字,但那是乐观上界 —— 它假装「你没标到的好照片」不存在于训练集里,而产品里它们就在那儿当负样本。 --- ## 常用参数 | 参数 | 默认 | 说明 | |---|---|---| | `--target` | 20 | 要挑几张 | | `--labels` | 无 | 你喜欢的照片清单,每行一个文件名 | | `--exclude` | 无 | 要跳过的子目录(相对路径),做评测时用来排除答案目录 | | `--device` | auto | `mps`(Mac GPU)/ `cuda` / `cpu` | | `--cold` | auto | 冷启动指标:`auto` / `face` / `laion_aes` / `blend`(blend 只为复现「融合更差」) | | `--cache` | `~/.cache/photofilter-rank` | 缓存位置 | | `--json` | 无 | 把完整结果(含全部分数、分组)写到文件 | 更细的阈值在 [`photofilter_rank/config.py`](photofilter_rank/config.py) 里,都有注释说明为什么是这个值。 --- ## 关于你的照片 - **原图只读。** 全程只读取,不移动、不删除、不改名、不写回。 - **不上传。** v4 的排序链路完全在本机,没有任何网络请求。 - **缓存在你自己电脑上**(`~/.cache/photofilter-rank`),可以随时删。 - 仓库里不含任何照片或缓存。 --- ## 现在还做不到什么 诚实的清单: - **只在一个用户、一个文件夹、20 张标注上验证过。** AUC 的 ±0.07~0.10 是几百次随机划分的 标准差,方向可信,绝对值别当准。换一批照片、换一个人,数字会变。 - **「不融合」和「集中度阈值 1.05」都是看着同一份答案定的**,有过拟合风险 —— 前者有跨 5 个 K 值的一致性支撑,后者有 146 个划分的支撑,但仍然只有一个数据集。 - **集中度指标假设编码器的基线余弦是个正数**(CLIP 是 0.798)。换编码器要重新校准; 基线太低时代码会拒绝判断而不是给假警报。 - **只测过人像。** 风景用的是同一套流程,但没有人工答案来验证。 - **场景分组的松紧没法验证。** 排序有人工精选可以对,但「哪几张算同一个瞬间」 没有答案集,所以阈值只能靠观察定。实测一组连号照片会被分成 4 组 —— 同组上限确实生效(20 张分布在 14 组、每组最多 2 张), 但你可能觉得其中某几张本来就该算一组。用 `--target` 调整或直接看 `--json` 里的分组。 - **没有「否决」环节。** 闭眼、糊脸、人被切一半这些,本地向量判断不了, 需要视觉模型补一刀。这部分设计好了但还没实现,见 [v4 版本记录](../docs/versions/V4-local-first.md)。 - **第一次跑要下载约 1.3GB 模型**,之后才快。 --- ## 跟前三个版本什么关系 这是第四版。前三版的思路和结果都记在 [`docs/versions/`](../docs/versions/) 里, 包括第三版为什么花了 997 次调用最后等于掷硬币 —— 那次失败是这一版存在的全部理由。