Commit 62327872 62327872aa8745dcb17156501138e261ddc4017d by cnb.bofCdSsphPA

Make segmentation strategy benchmarks comparable under fixed query budgets

Clarify that the pipeline already mixes random sampling with librosa-guided candidate selection, while keeping heavier structural segmentation as a later optimization path.

Constraint: Must avoid staging local datasets and transient smoke artifacts
Rejected: Full librosa.segment.* default rollout | Too CPU-heavy and too distribution-shaping for current smoke/training stage
Confidence: high
Scope-risk: narrow
Directive: Keep future segmentation comparisons capped by equal query budgets when reporting quality deltas
Tested: py_compile for evaluate/external_adapters/ab_smoke_segmentation; evaluate.py --max-queries 5; ab_smoke_segmentation end-to-end smoke with max_test_queries=5
Not-tested: Multi-strategy medium-size capped A/B benchmark on larger real FMA subset
1 parent f04a314e
#!/usr/bin/env python3
import argparse
import json
import random
from pathlib import Path
import numpy as np
......@@ -28,6 +29,8 @@ def main():
parser.add_argument("--chroma-weight", type=float, default=0.25)
parser.add_argument("--ecapa-weight", type=float, default=0.5)
parser.add_argument("--melody-weight", type=float, default=0.25)
parser.add_argument("--max-queries", type=int, default=None)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
data_dir = Path(args.data)
......@@ -57,6 +60,9 @@ def main():
queries = [x for x in items if x.get("type") != "reference"]
if not queries:
raise SystemExit("No segment queries found for evaluation")
if args.max_queries is not None and args.max_queries > 0 and len(queries) > args.max_queries:
rng = random.Random(args.seed)
queries = rng.sample(queries, args.max_queries)
top1 = 0
topk = 0
......
......@@ -71,8 +71,10 @@ def main():
parser.add_argument("--batch-size", type=int, default=2)
parser.add_argument("--device", default="cpu")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--max-test-queries", type=int, default=None)
parser.add_argument("--strategies", nargs="*", default=DEFAULT_STRATEGIES)
parser.add_argument("--output-json", default=None)
parser.add_argument("--resume", action="store_true")
args = parser.parse_args()
repo = Path(__file__).resolve().parents[1]
......@@ -80,9 +82,20 @@ def main():
work_root = (repo / args.work_root).resolve()
subset_dir = work_root / "subset_audio"
subset_info = prepare_subset(input_dir, subset_dir, args.subset_size)
progress_path = work_root / "progress.json"
cached_results = {}
if args.resume and progress_path.exists():
try:
payload = json.loads(progress_path.read_text())
cached_results = {item["strategy"]: item for item in payload.get("strategies", [])}
except Exception:
cached_results = {}
results = []
for strategy in args.strategies:
if strategy in cached_results:
results.append(cached_results[strategy])
continue
smoke_root = work_root / strategy
if smoke_root.exists():
shutil.rmtree(smoke_root)
......@@ -110,6 +123,7 @@ def main():
str(args.batch_size),
"--device",
args.device,
*([] if args.max_test_queries is None else ["--max-test-queries", str(args.max_test_queries)]),
"--seed",
str(args.seed),
]
......@@ -130,6 +144,17 @@ def main():
"report_dir": summary["report_dir"],
"sample_failures": eval_report.get("sample_failures", [])[:3],
})
progress_payload = {
"dataset": args.dataset,
"subset": subset_info,
"query_duration": args.query_duration,
"query_stride": args.query_stride,
"train_epochs": args.train_epochs,
"batch_size": args.batch_size,
"device": args.device,
"strategies": results,
}
progress_path.write_text(json.dumps(progress_payload, ensure_ascii=False, indent=2))
results.sort(key=lambda x: (x["top1"], x["topk"], x["num_queries"]), reverse=True)
report = {
......@@ -140,6 +165,7 @@ def main():
"train_epochs": args.train_epochs,
"batch_size": args.batch_size,
"device": args.device,
"max_test_queries": args.max_test_queries,
"strategies": results,
"winner": results[0] if results else None,
}
......
......@@ -373,6 +373,7 @@ def smoke_local_dataset(
segment_strategy: str,
silence_top_db: int,
index_checkpoint_every_refs: int,
max_test_queries: int | None,
seed: int,
train_epochs: int,
batch_size: int,
......@@ -449,6 +450,8 @@ def smoke_local_dataset(
"--device", resolved_device,
"--fast-eval",
"--output-json", str(eval_json),
"--seed", str(seed),
*([] if max_test_queries is None else ["--max-queries", str(max_test_queries)]),
], check=True)
config = build_smoke_config_summary(
......@@ -467,6 +470,7 @@ def smoke_local_dataset(
config["run"]["index_checkpoint_every_refs"] = index_checkpoint_every_refs
config["run"]["index_resume_enabled"] = True
config["run"]["train_segment_strategy"] = segment_strategy
config["run"]["max_test_queries"] = max_test_queries
report_dir.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(config, indent=2))
......@@ -552,6 +556,7 @@ def main():
p.add_argument("--segment-strategy", choices=["random", "silence_aware", "high_energy", "onset_aware", "beat_aware", "repeated_section_aware", "hybrid"], default="random")
p.add_argument("--silence-top-db", type=int, default=30)
p.add_argument("--index-checkpoint-every-refs", type=int, default=100)
p.add_argument("--max-test-queries", type=int, default=None)
p.add_argument("--seed", type=int, default=42)
p.add_argument("--train-epochs", type=int, default=1)
p.add_argument("--batch-size", type=int, default=2)
......@@ -612,6 +617,7 @@ def main():
segment_strategy=args.segment_strategy,
silence_top_db=args.silence_top_db,
index_checkpoint_every_refs=args.index_checkpoint_every_refs,
max_test_queries=args.max_test_queries,
seed=args.seed,
train_epochs=args.train_epochs,
batch_size=args.batch_size,
......
......@@ -2,6 +2,50 @@
## 2026-06-02
### Stage: 为切片策略评测补齐公平 query cap,并澄清 librosa 分段现状
完成项:
- 修改 `acr-engine/evaluate.py`
- 新增 `--max-queries`
- 新增 `--seed`
- 允许评测前对 query 集进行可复现随机抽样
- 修改 `acr-engine/src/data/external_adapters.py`
- `smoke-local` 新增 `--max-test-queries`
- 自动透传到 `evaluate.py --max-queries`
- smoke 配置摘要同步记录 cap 信息
- 修改 `acr-engine/scripts/ab_smoke_segmentation.py`
- 新增 `--max-test-queries`
- 可在策略 A/B smoke 中统一限制 query 预算
- 更新文档:
- [open-dataset-workflow.md](./open-dataset-workflow.md)
- [dataset-spec.md](./dataset-spec.md)
- [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
- 文档中额外澄清:
- 当前切片**不是只有 random**
- 已经接入 `librosa.effects.split / onset_detect / beat_track / chroma_cqt`
- 但尚未把更重的 `librosa.segment.*` 结构分段作为默认主流程
验证结果:
- 语法检查:
- `/usr/local/miniconda3/bin/python -m py_compile evaluate.py src/data/external_adapters.py scripts/ab_smoke_segmentation.py`
- 单点评测验证:
- `evaluate.py --max-queries 5 --seed 123`
- 输出 `num_queries = 5`
- `top1 = 1.0`
- `topk = 1.0`
- 端到端 smoke 验证:
- `scripts/ab_smoke_segmentation.py --strategies hybrid --max-test-queries 5`
- 最终报告:
- `max_test_queries = 5`
- `num_queries = 5`
- `top1 = 1.0`
- `topk = 1.0`
结论:
- 现在策略 A/B 不再只能比较“谁生成的 query 更多”
- 已经可以在**统一 query 成本预算**下比较不同切片策略
- 当前项目也已明确进入“random + librosa 音乐感知候选”的混合切片阶段,而不是纯随机切片阶段
### Stage: 为内部素材 query 自动补 duration / offset 规则
完成项:
......
......@@ -89,16 +89,16 @@ flowchart TD
| 场景 | 当前实现 | 是否重叠 | 代码位置 |
|---|---|---:|---|
| 训练 `SongPairDataset` | 每次采样随机取一个 5s clip | 否,**不是固定滑窗** | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
| 训练 `SongPairDataset` | 每次采样`segment_strategy` 选 1 个 5s clip;默认可随机,也可走音乐感知候选 | 否,**不是固定滑窗全集展开** | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
| 检索 / embedding / 建索引 | `window_sec=5.0`, `stride_sec=2.5` | 是,**50% overlap** | [acr-engine/src/utils/audio.py](../acr-engine/src/utils/audio.py), [acr-engine/src/engines/ecapa_embedder.py](../acr-engine/src/engines/ecapa_embedder.py) |
| `audio-dir-to-splits` 默认 | 每首歌只生成 1 个随机 query | 否 | [acr-engine/src/data/manifest_tools.py](../acr-engine/src/data/manifest_tools.py) |
| `audio-dir-to-splits` 默认 | 每首歌生成 query;可随机,也可按音乐感知策略产出 | 否 | [acr-engine/src/data/manifest_tools.py](../acr-engine/src/data/manifest_tools.py) |
| `audio-dir-to-splits --query-stride 4.0` 例 | 对单首歌生成多个滑窗 query | 是,可配置 | [acr-engine/src/data/manifest_tools.py](../acr-engine/src/data/manifest_tools.py) |
### 直接回答你的问题
- **有重叠窗口,但只在检索/索引链路里有。**
- **当前训练主链路没有对 3 分钟 mp3 预展开成“全量重叠切片集”**,而是每次 batch 动态随机裁一个 5s 片段
- **当前外部数据集 manifest 生成器默认仍是一首歌 1 个随机 query,但现在已经支持通过 `--query-stride` 开启多 query / overlap query 生成。**
- **有重叠窗口,主要在检索/索引链路;训练端不是全量滑窗展开。**
- **当前训练主链路不是“只会随机切”**,而是每次 batch 动态选 1 个 5s 片段;候选可以来自 `random / silence_aware / high_energy / onset_aware / beat_aware / repeated_section_aware / hybrid`
- **当前外部数据集 manifest 生成器也不再只有随机 query**,可通过 `--query-strategy` 走音乐感知切法,也可通过 `--query-stride` 开启多 query / overlap query 生成。**
---
......@@ -108,12 +108,37 @@ flowchart TD
|---|---|---|
| 训练随机裁剪 | 节省存储,不必预生成几万切片 | 同一 epoch 暴露到的时间区域有限 |
| 检索重叠滑窗 | 更接近真实 ACR reference coverage | 索引体积更大 |
| 外部数据一首歌一个 query | smoke 更轻、更快验证 | 训练/评测覆盖不充分 |
| 音乐感知候选切片 | 更容易打到主段、起音、拍点、非静音区 | CPU 分析成本更高 |
| 外部数据少量 query smoke | smoke 更轻、更快验证 | 训练/评测覆盖不充分 |
推荐理解方式:
- **训练端**更像“随机数据增强采样器”
- **检索端**更像“为了召回覆盖做滑窗索引”
### 5.3 我们到底有没有用 librosa 的分段逻辑
有,而且已经进入主链路,但不是“把整套结构分段 API 全盘替代随机采样”。
当前已用到的 `librosa` 音乐感知逻辑:
| 逻辑 | 当前用途 | 代码位置 |
|---|---|---|
| `librosa.effects.split` | `silence_aware`,避开静音区 | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
| `librosa.onset.onset_detect` | `onset_aware`,优先起音附近 | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
| `librosa.beat.beat_track` | `beat_aware`,优先规则拍点 | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
| `librosa.feature.chroma_cqt` | `repeated_section_aware`,近似找重复主段 / hook | [acr-engine/src/data/dataset.py](../acr-engine/src/data/dataset.py) |
**没有**直接上整套更重的 `librosa.segment.*` 结构分段主流程,原因主要是:
1. **训练 query 的真实来源并不总对齐段落边界**,完全结构分段会把训练分布拉得过“整齐”;
2. **CPU 成本更高**,对 FMA / MTG 这类大目录 smoke 和批量 manifest 生成不够轻;
3. **当前阶段先追求稳健可复现**,优先落地静音、起音、拍点、重复段这几类收益更直接的候选策略。
所以现在的设计不是“没考虑 librosa 分段”,而是:
- **已经用了 librosa 的轻量高收益部分**
- **保留 random 作为泛化增强**
- **把更重的结构分段留作后续增强,而不是一上来替代全部采样逻辑**
---
## 6. 当前训练信号与 hard-case 规则
......
......@@ -118,6 +118,32 @@ flowchart LR
- 最后按 `num_queries`
这样在 top1/top5 持平时,会优先保留**覆盖 query 更多**的策略,而不是误把 query 更少的策略排到第一。
如果你要做**更公平**的策略比较,建议再加 `--max-test-queries`,让每个策略在同样的 query 预算下评测:
```bash
/usr/local/miniconda3/bin/python acr-engine/scripts/ab_smoke_segmentation.py \
--dataset fma \
--input-dir acr-engine/data/raw/fma_small_audio \
--work-root /tmp/ab_smoke_seg_cap \
--subset-size 6 \
--query-duration 8 \
--train-epochs 1 \
--batch-size 2 \
--device cpu \
--strategies hybrid \
--max-test-queries 5 \
--output-json /tmp/ab_smoke_seg_cap/report.json
```
已验证:
- 最终报告会显式记录 `max_test_queries`
- `evaluate.py` 会按 `--seed` 复现抽样
- 端到端 smoke 报告中的 `num_queries` 已成功收敛到 `5`
这一步的意义是:
- 之前的 A/B 排名更偏“覆盖能力”
- 加上 cap 后,可以更公平地比较“同等 query 成本下的识别质量”
/usr/local/miniconda3/bin/python evaluate.py --data data/external_ingested/fma/manifests --model data/models_fma_smoke/best_model.pt --index-prefix data/index_fma_smoke/reference --split test --device cpu --fast-eval --output-json reports/fma-smoke/eval.json
/usr/local/miniconda3/bin/python scripts/generate_artifacts.py --eval-json reports/fma-smoke/eval.json --config-json reports/fma-smoke/config.json --output-dir reports/fma-smoke --model-version fma-smoke --data-version fma_local
```
......
......@@ -347,7 +347,7 @@ flowchart TD
## 11.5 切片策略:不要只用随机切
当前项目现在已经支持 4 类切片思路,但职责不同:
当前项目现在已经支持类切片思路,但职责不同:
| 策略 | 适用位置 | 作用 | 是否已接入 |
|---|---|---|---|
......@@ -358,7 +358,7 @@ flowchart TD
| `onset_aware` | 训练 query / 外部 query 生成 | 优先靠近起音事件,减少截到拖尾/空拍 | 是 |
| `beat_aware` | 训练 query / 外部 query 生成 | 优先靠近节拍点,适合强节奏流行/电子/舞曲等 | 是 |
| `repeated_section_aware` | 训练 query / 外部 query 生成 | 优先抽取与其它窗口最相似的重复主段,近似副歌/重复 hook | 是 |
| `hybrid` | 训练 query / 外部 query 生成 | 混合 silence-aware + random,兼顾稳定性与泛化 | 是 |
| `hybrid` | 训练 query / 外部 query 生成 | 混合 repeated-section / beat / energy / onset / silence / random | 是 |
推荐理解:
......@@ -367,7 +367,29 @@ flowchart TD
2. **reference 建库不是随机切**
建库仍然是固定滑窗
3. **外部数据 query 生成也不是只能随机切**
现在可选 `--query-strategy silence_aware`
现在可选 `--query-strategy random|silence_aware|high_energy|onset_aware|beat_aware|repeated_section_aware|hybrid`
### 11.6 为什么没有直接全量切到 `librosa.segment.*`
这不是没考虑,而是当前做了更保守的工程取舍:
- 已经接入 `librosa.effects.split / onset_detect / beat_track / chroma_cqt`
- 先把非静音、起音、拍点、重复段这些高收益候选打通
- 暂时没有把更重的结构分段作为默认主流程
原因:
1. **ACR 查询不总是结构化片段**
用户截到的可能是副歌,也可能是过门、录屏残片、短视频二创片段。
2. **重结构分段更耗 CPU**
对 FMA 这类真实开放集批量 prepare/smoke 不够轻。
3. **训练仍需要随机性**
纯结构分段会降低截取点分布的多样性。
当前更合理的策略是:
- `hybrid` 作为默认训练切片推荐
- `beat_aware / repeated_section_aware` 作为偏音乐主段的强化选项
- `random` 保留为泛化基线
为什么不直接完全依赖音乐结构分段?
......