TEST_WORKFLOW.md 5.52 KB

歌词查重测试流程

本文档记录当前项目的 PostgreSQL-only 测试流程。当前链路不再使用 outputs/indexes/*.pkl,也不再生成 *.index.pkl 评估索引。

1. 准备数据

已有曲库:

data/library/

支持文件:

.lrc
.txt

生成的评估样本目录:

data/generated_eval/incoming/
data/generated_eval/hard_incoming/

评估结果目录:

outputs/results/

2. 初始化 PostgreSQL

创建数据库:

createdb lyric_dedup

初始化 schema:

python scripts/init_postgres.py \
  --dsn postgresql:///lyric_dedup

检查表:

psql postgresql:///lyric_dedup -c '\dt'

3. 导入曲库

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library

导入完成后检查数量:

psql postgresql:///lyric_dedup -c 'select count(*) from lyrics where deleted_at is null;'
psql postgresql:///lyric_dedup -c 'select count(*) from lyric_lines;'

导入脚本默认会 soft delete exact_hash 完全一致的重复记录,并输出:

outputs/results/postgres_exact_duplicates.csv

如果要额外查看高行级覆盖的疑似重复:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library \
  --line-duplicate-report outputs/results/postgres_line_duplicates.csv

4. 检查单个文件

python -m lyric_dedup.cli check-file \
  --dsn postgresql:///lyric_dedup \
  --file test_api/test_lyric.txt

如需启用 trigram 文本召回:

python -m lyric_dedup.cli check-file \
  --dsn postgresql:///lyric_dedup \
  --file test_api/test_lyric.txt \
  --enable-trgm \
  --trgm-threshold 0.3

5. 生成 standard 评估集

python -m lyric_dedup.cli generate-eval-set \
  --library-dir data/library \
  --lyrics-dir data/generated_eval/incoming \
  --csv data/generated_eval/eval_5000.csv \
  --size 5000 \
  --positive-ratio 0.3

standard 口径:

应去重: 30%
不应去重: 70%

样本类型:

positive_* = 应去重,全曲歌词样式变化,例如时间戳、标点、平台噪声、空行、重复副歌次数变化、附加翻译、少量错别字
negative_real_holdout_full_song = 不应去重,完整真实歌词,从评估候选里排除自身
negative_fragment = 不应去重,单曲片段
negative_shared_chorus = 不应去重,重复副歌碰撞
negative_translation_only = 不应去重,仅翻译相似
negative_same_theme_synthetic = 不应去重,同主题新歌词
edge_short_or_placeholder = 不应去重,短歌词/占位边界样本

6. 生成 hard 评估集

python -m lyric_dedup.cli generate-eval-set \
  --profile hard \
  --library-dir data/library \
  --lyrics-dir data/generated_eval/hard_incoming \
  --csv data/generated_eval/eval_hard_5000.csv \
  --size 5000 \
  --positive-ratio 0.3

hard 口径强调真实业务边界,不故意制造反常输入:

positive_realistic_variant = 应去重,同曲平台版本噪声、较完整缺段、整段翻译附加、真实录入/OCR 错
negative_near_neighbor_holdout_full_song = 不应去重,和曲库有较多行重合的真实 holdout 新歌
negative_long_fragment = 不应去重,较长但不完整的单曲片段
negative_catalog_mashup = 不应去重,多首真实歌词片段组成的串烧/混剪式输入

7. 严格评估

严格口径只把 duplicate 算作预测应去重:

python scripts/evaluate_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --csv data/generated_eval/eval_hard_5000.csv \
  --base-dir data/generated_eval \
  --out outputs/results/postgres_eval_hard_5000.csv

适合看自动拦截质量,重点关注:

precision
false_positive

8. 召回评估

召回口径把 duplicatereview 都算作抓到可疑样本:

python scripts/evaluate_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --csv data/generated_eval/eval_hard_5000.csv \
  --base-dir data/generated_eval \
  --positive-decisions duplicate,review \
  --out outputs/results/postgres_eval_hard_5000_review_positive.csv

适合看漏召风险,重点关注:

recall
false_negative

9. 查看 summary

cat outputs/results/postgres_eval_hard_5000.csv.summary.json
cat outputs/results/postgres_eval_hard_5000_review_positive.csv.summary.json

指标含义:

accuracy        总正确率
precision       预测应去重的样本里,有多少是真的应去重
recall          真实应去重的样本里,有多少被系统抓到
f1              precision 和 recall 的综合指标
true_positive   应去重且预测应去重
false_positive  不应去重但预测应去重,误杀
true_negative   不应去重且预测不应去重
false_negative  应去重但预测不应去重,漏召

10. 查看失败样本

严格口径失败样本:

python -c 'import csv; rows=csv.DictReader(open("outputs/results/postgres_eval_hard_5000.csv", encoding="utf-8")); [print(r["id"], r["expected_duplicate"], r["decision"], r["reason"], sep=" | ") for r in rows if r["correct"] == "False"]'

按样本类型统计:

python -c 'import csv,collections; meta={r["id"]:r for r in csv.DictReader(open("data/generated_eval/eval_hard_5000.csv", encoding="utf-8-sig"))}; rows=csv.DictReader(open("outputs/results/postgres_eval_hard_5000.csv", encoding="utf-8")); c=collections.Counter(meta.get(r["id"],{}).get("sample_type","") for r in rows if r["correct"]=="False"); print(c)'