check_cross.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. # -*- coding: utf-8 -*-
  2. """
  3. 交叉验证效果核对 — 用来源截图独立重读当"真值",逐条评判交叉验证的每个决定
  4. =============================================================================
  5. 用法: python check_cross.py # 核对 logs/cross_validate.jsonl 里全部记录
  6. python check_cross.py --max 20 # 只核对最近20条有分歧的记录(PP-OCR较慢)
  7. 评判口径(真值 = PP-OCR+几何规则在来源截图上枚举出的完整卡片):
  8. 分歧候选卡(仲裁对象):
  9. 保留+图上真有完整卡 → 正确保留 保留+图上没有 → 错误保留(脏数据)
  10. 丢弃+图上真没有 → 正确丢弃(残卡) 丢弃+图上有 → 误杀(漏卡)
  11. 错误率对比(同一屏):
  12. 单次识别错误数(run1: 多余+漏卡) vs 交叉最终错误数 → 错误是否减少
  13. """
  14. import json
  15. import sys
  16. from pathlib import Path
  17. ROOT = Path(__file__).parent
  18. sys.path.insert(0, str(ROOT))
  19. from audit_ocr import enumerate_cards, price_num, shop_match
  20. from ai_helper_vision1 import VisionParser1
  21. LOG = ROOT / "logs" / "cross_validate.jsonl"
  22. def title_ov(a, b, min_len=6):
  23. a, b = str(a or ""), str(b or "")
  24. best, prev = 0, [0] * (len(b) + 1)
  25. for ca in a:
  26. cur = [0]
  27. for j, cb in enumerate(b, 1):
  28. cur.append(prev[j - 1] + 1 if ca == cb else 0)
  29. best = max(best, cur[-1])
  30. prev = cur
  31. return best >= min_len
  32. def cand_matches_truth(cand, truth_cards):
  33. """分歧候选卡是否与图上某张完整卡配得上(价格+店名+标题)"""
  34. pn = price_num(cand.get("price"))
  35. if not pn:
  36. return False
  37. for t in truth_cards:
  38. if t["price"] == pn and shop_match(cand.get("shop"), t["shop"]) \
  39. and title_ov(cand.get("title"), t["title"]):
  40. return True
  41. return False
  42. def card_errors(cards, truth_cards):
  43. """(多余数, 漏卡数): cards vs 图上真值"""
  44. extras = sum(0 if cand_matches_truth(c, truth_cards) else 1 for c in cards)
  45. used = set()
  46. misses = 0
  47. for t in truth_cards:
  48. hit = False
  49. for i, c in enumerate(cards):
  50. if i in used:
  51. continue
  52. if price_num(c.get("price")) == t["price"] and shop_match(c.get("shop"), t["shop"]) \
  53. and title_ov(c.get("title"), t["title"]):
  54. hit = True
  55. used.add(i)
  56. break
  57. if not hit:
  58. misses += 1
  59. return extras, misses
  60. def main():
  61. max_n = 0
  62. if "--max" in sys.argv:
  63. try:
  64. max_n = int(sys.argv[sys.argv.index("--max") + 1])
  65. except (IndexError, ValueError):
  66. pass
  67. if not LOG.exists():
  68. print("没有日志: logs/cross_validate.jsonl (交叉版跑过采集后生成)")
  69. sys.exit(1)
  70. recs = [json.loads(ln) for ln in LOG.read_text(encoding="utf-8").splitlines() if ln.strip()]
  71. disputed = [r for r in recs if (r.get("a_only") or r.get("b_only")) and r.get("image")]
  72. if max_n:
  73. disputed = disputed[-max_n:]
  74. print(f"日志共 {len(recs)} 条, 有分歧需核对的 {len(disputed)} 条\n")
  75. if not disputed:
  76. print("抽样核对的记录里没有分歧卡 —— 交叉版与单次版无差异")
  77. return
  78. parser = VisionParser1()
  79. truth_cache = {}
  80. def truth_of(image):
  81. if image not in truth_cache:
  82. try:
  83. truth_cache[image] = enumerate_cards(parser._ppocr_blocks(image, 0))
  84. except Exception as e:
  85. print(f" [真值枚举失败] {image}: {e}")
  86. truth_cache[image] = None
  87. return truth_cache[image]
  88. # ── 仲裁决定逐条评判 ──
  89. kept_ok = kept_bad = drop_ok = drop_bad = 0
  90. bad_detail = []
  91. for r in disputed:
  92. truth = truth_of(r["image"])
  93. if truth is None:
  94. continue
  95. cands = r.get("a_only", []) + r.get("b_only", [])
  96. if not cands:
  97. continue
  98. for c in cands:
  99. exists = cand_matches_truth(c, truth)
  100. was_kept = any(price_num(k.get("price")) == price_num(c.get("price"))
  101. and shop_match(k.get("shop"), c.get("shop"))
  102. and title_ov(k.get("title"), c.get("title"))
  103. for k in r.get("kept", []))
  104. if was_kept:
  105. if exists:
  106. kept_ok += 1
  107. else:
  108. kept_bad += 1
  109. bad_detail.append(f" [错误保留→脏数据] ¥{c.get('price')} {c.get('shop')} {str(c.get('title'))[:24]} @ {Path(r['image']).name}")
  110. else:
  111. if exists:
  112. drop_bad += 1
  113. bad_detail.append(f" [误杀→漏卡] ¥{c.get('price')} {c.get('shop')} {str(c.get('title'))[:24]} @ {Path(r['image']).name}")
  114. else:
  115. drop_ok += 1
  116. total_decisions = kept_ok + kept_bad + drop_ok + drop_bad
  117. print("=" * 78)
  118. print(f"口径一 仲裁决定正确率(共 {total_decisions} 个分歧决定)")
  119. print(f" 正确保留 {kept_ok} 错误保留 {kept_bad} 正确丢弃 {drop_ok} 误杀 {drop_bad}")
  120. if total_decisions:
  121. print(f" → 仲裁决定正确率 {(kept_ok + drop_ok) / total_decisions * 100:.1f}%")
  122. for d in bad_detail:
  123. print(d)
  124. # ── 错误率对比: 单次(run1) vs 交叉(final) ──
  125. n = 0
  126. err_run1 = err_final = 0
  127. extra1 = miss1 = extraf = missf = 0
  128. for r in recs:
  129. if r.get("status") != "ok" or not r.get("image") or not r.get("final_cards") and not r.get("run1"):
  130. continue
  131. truth = truth_of(r["image"])
  132. if truth is None:
  133. continue
  134. n += 1
  135. e1x, e1m = card_errors(r.get("run1", []), truth)
  136. efx, efm = card_errors(r.get("final_cards", []), truth)
  137. extra1 += e1x; miss1 += e1m; extraf += efx; missf += efm
  138. err_run1 += e1x + e1m
  139. err_final += efx + efm
  140. print("=" * 78)
  141. print(f"口径二 错误率对比({n} 屏有真值)")
  142. print(f" 单次识别(run1) 错误总数: 多余{extra1} + 漏卡{miss1} = {err_run1} ({err_run1/n:.2f}/屏)")
  143. print(f" 交叉最终错误总数: 多余{extraf} + 漏卡{missf} = {err_final} ({err_final/n:.2f}/屏)")
  144. if err_run1:
  145. print(f" → 错误减少 {(1 - err_final / err_run1) * 100:.0f}%")
  146. print("=" * 78)
  147. if __name__ == "__main__":
  148. main()