f1_only.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. #!/usr/bin/env python3
  2. """
  3. Compute overlap metrics only (Recall / Precision / F1) from raw JSONL.
  4. Workflow:
  5. 1. Read data (optional label backfill from reference JSONL)
  6. 2. Compute overlap metrics and aggregate by tag / overall
  7. """
  8. import json
  9. import argparse
  10. import os
  11. from collections import defaultdict
  12. def load_reference_labels(reference_file):
  13. """Load label data from reference file, indexed by (video_id, begin, end, dataset_name, idx)."""
  14. print(f" Loading labels from reference file: {reference_file}")
  15. labels_by_key = {}
  16. with open(reference_file, "r", encoding="utf-8") as f:
  17. for line in f:
  18. if not line.strip():
  19. continue
  20. try:
  21. item = json.loads(line)
  22. except json.JSONDecodeError:
  23. continue
  24. vid = item.get("video_id")
  25. dataset = item.get("dataset_name")
  26. begin = item.get("begin")
  27. end = item.get("end")
  28. idx = item.get("idx")
  29. if not vid:
  30. continue
  31. label = item.get("label")
  32. if label is not None:
  33. labels_by_key[(vid, begin, end, dataset, idx)] = label
  34. print(f" Loaded {len(labels_by_key)} label entries from reference file")
  35. return labels_by_key
  36. def load_data(input_file, reference_labels=None):
  37. """Load data and fill missing labels by exact key when reference is provided."""
  38. print(f"Step 1/2: Reading data: {input_file}")
  39. results = []
  40. ref_label_used_count = 0
  41. with open(input_file, "r", encoding="utf-8") as f:
  42. for line_num, line in enumerate(f, 1):
  43. if not line.strip():
  44. continue
  45. try:
  46. item = json.loads(line)
  47. except json.JSONDecodeError:
  48. print(f" Warning: JSON parsing failed at line {line_num}")
  49. continue
  50. vid = item.get("video_id")
  51. dataset = item.get("dataset_name")
  52. if not vid:
  53. continue
  54. begin = item.get("begin")
  55. end = item.get("end")
  56. idx = item.get("idx")
  57. tag = item.get("tag")
  58. pred = item.get("pred")
  59. label = item.get("label")
  60. if label is None and reference_labels:
  61. ref_key = (vid, begin, end, dataset, idx)
  62. if ref_key in reference_labels:
  63. label = reference_labels[ref_key]
  64. ref_label_used_count += 1
  65. results.append(
  66. {
  67. "video_id": vid,
  68. "dataset_name": dataset,
  69. "begin": begin,
  70. "end": end,
  71. "idx": idx,
  72. "tag": tag,
  73. "pred": pred,
  74. "label": label,
  75. }
  76. )
  77. print(f" Read {len(results)} records")
  78. if reference_labels:
  79. print(f" Backfilled {ref_label_used_count} labels from reference file")
  80. return results
  81. def get_speaking_seconds_from_content(content, begin, end):
  82. """Extract speaking seconds from pred or label content as a set[int]."""
  83. speaking_seconds = set()
  84. if content is None:
  85. return speaking_seconds
  86. if isinstance(content, dict):
  87. for key in content.keys():
  88. try:
  89. speaking_seconds.add(int(key))
  90. except (TypeError, ValueError):
  91. continue
  92. elif isinstance(content, str) and content.strip():
  93. if begin is not None and end is not None:
  94. for second in range(begin, end + 1):
  95. speaking_seconds.add(second)
  96. return speaking_seconds
  97. def calculate_overlap_single(sample):
  98. """Compute Recall/Precision/F1 for one sample."""
  99. pred = sample.get("pred")
  100. label = sample.get("label")
  101. pred_seconds = get_speaking_seconds_from_content(pred, sample.get("begin"), sample.get("end"))
  102. label_seconds = get_speaking_seconds_from_content(label, sample.get("begin"), sample.get("end"))
  103. if not label_seconds:
  104. return None
  105. tp = len(pred_seconds & label_seconds)
  106. recall = tp / len(label_seconds) if label_seconds else 0
  107. precision = tp / len(pred_seconds) if pred_seconds else 0
  108. f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
  109. return {
  110. "video_id": sample.get("video_id"),
  111. "tag": sample.get("tag"),
  112. "begin": sample.get("begin"),
  113. "end": sample.get("end"),
  114. "idx": sample.get("idx"),
  115. "recall": recall,
  116. "precision": precision,
  117. "f1": f1,
  118. "tp": tp,
  119. "predict_count": len(pred_seconds),
  120. "label_count": len(label_seconds),
  121. }
  122. def safe_div(numerator, denominator):
  123. return numerator / denominator if denominator else 0
  124. def f1_from_pr(precision, recall):
  125. return 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
  126. def evaluate_f1(data_list):
  127. """Compute overlap metrics for all samples and aggregate supporting stats."""
  128. print("\nStep 2/2: Computing overlap metrics (Recall/Precision/F1)")
  129. all_metrics = []
  130. by_tag = defaultdict(
  131. lambda: {
  132. "recall_list": [],
  133. "precision_list": [],
  134. "f1_list": [],
  135. "tp_sum": 0,
  136. "predict_count_sum": 0,
  137. "label_count_sum": 0,
  138. "sample_count": 0,
  139. }
  140. )
  141. overall = {
  142. "tp_sum": 0,
  143. "predict_count_sum": 0,
  144. "label_count_sum": 0,
  145. }
  146. for sample in data_list:
  147. metric = calculate_overlap_single(sample)
  148. if not metric:
  149. continue
  150. all_metrics.append(metric)
  151. tag = metric["tag"]
  152. by_tag[tag]["recall_list"].append(metric["recall"])
  153. by_tag[tag]["precision_list"].append(metric["precision"])
  154. by_tag[tag]["f1_list"].append(metric["f1"])
  155. by_tag[tag]["tp_sum"] += metric["tp"]
  156. by_tag[tag]["predict_count_sum"] += metric["predict_count"]
  157. by_tag[tag]["label_count_sum"] += metric["label_count"]
  158. by_tag[tag]["sample_count"] += 1
  159. overall["tp_sum"] += metric["tp"]
  160. overall["predict_count_sum"] += metric["predict_count"]
  161. overall["label_count_sum"] += metric["label_count"]
  162. print(f" Computed metrics for {len(all_metrics)} samples")
  163. return all_metrics, by_tag, overall
  164. def main():
  165. parser = argparse.ArgumentParser(description="Compute overlap metrics only (Recall/Precision/F1)")
  166. parser.add_argument("input", help="Input raw JSONL file")
  167. parser.add_argument("--output", required=True, help="Output result JSON file")
  168. parser.add_argument("--reference", help="Reference JSONL file for label backfill")
  169. parser.add_argument("--verbose", action="store_true", help="Save per-sample overlap details")
  170. args = parser.parse_args()
  171. exp_name = os.path.splitext(os.path.basename(args.input))[0]
  172. print("=" * 80)
  173. print(f"Experiment name: {exp_name}")
  174. print("=" * 80)
  175. reference_labels = load_reference_labels(args.reference) if args.reference else None
  176. data_list = load_data(args.input, reference_labels)
  177. all_metrics, by_tag, overall = evaluate_f1(data_list)
  178. all_recall = [m["recall"] for m in all_metrics]
  179. all_precision = [m["precision"] for m in all_metrics]
  180. all_f1 = [m["f1"] for m in all_metrics]
  181. macro_recall = sum(all_recall) / len(all_recall) if all_recall else 0
  182. macro_precision = sum(all_precision) / len(all_precision) if all_precision else 0
  183. macro_f1 = sum(all_f1) / len(all_f1) if all_f1 else 0
  184. micro_recall = safe_div(overall["tp_sum"], overall["label_count_sum"])
  185. micro_precision = safe_div(overall["tp_sum"], overall["predict_count_sum"])
  186. micro_f1 = f1_from_pr(micro_precision, micro_recall)
  187. result = {
  188. "exp_name": exp_name,
  189. "overall": {
  190. "recall": macro_recall,
  191. "precision": macro_precision,
  192. "f1": macro_f1,
  193. "overlap_macro": {
  194. "sample_count": len(all_metrics),
  195. "recall": macro_recall,
  196. "precision": macro_precision,
  197. "f1": macro_f1,
  198. },
  199. "overlap_micro": {
  200. "sample_count": len(all_metrics),
  201. "tp": overall["tp_sum"],
  202. "predict_count": overall["predict_count_sum"],
  203. "label_count": overall["label_count_sum"],
  204. "recall": micro_recall,
  205. "precision": micro_precision,
  206. "f1": micro_f1,
  207. },
  208. },
  209. "by_tag": {},
  210. "details": {"overlap": all_metrics if args.verbose else []},
  211. }
  212. for tag in sorted(by_tag.keys()):
  213. recalls = by_tag[tag]["recall_list"]
  214. precisions = by_tag[tag]["precision_list"]
  215. f1s = by_tag[tag]["f1_list"]
  216. tag_macro_recall = sum(recalls) / len(recalls) if recalls else 0
  217. tag_macro_precision = sum(precisions) / len(precisions) if precisions else 0
  218. tag_macro_f1 = sum(f1s) / len(f1s) if f1s else 0
  219. tag_micro_recall = safe_div(by_tag[tag]["tp_sum"], by_tag[tag]["label_count_sum"])
  220. tag_micro_precision = safe_div(by_tag[tag]["tp_sum"], by_tag[tag]["predict_count_sum"])
  221. tag_micro_f1 = f1_from_pr(tag_micro_precision, tag_micro_recall)
  222. result["by_tag"][tag] = {
  223. "recall": tag_macro_recall,
  224. "precision": tag_macro_precision,
  225. "f1": tag_macro_f1,
  226. "overlap_macro": {
  227. "sample_count": by_tag[tag]["sample_count"],
  228. "recall": tag_macro_recall,
  229. "precision": tag_macro_precision,
  230. "f1": tag_macro_f1,
  231. },
  232. "overlap_micro": {
  233. "sample_count": by_tag[tag]["sample_count"],
  234. "tp": by_tag[tag]["tp_sum"],
  235. "predict_count": by_tag[tag]["predict_count_sum"],
  236. "label_count": by_tag[tag]["label_count_sum"],
  237. "recall": tag_micro_recall,
  238. "precision": tag_micro_precision,
  239. "f1": tag_micro_f1,
  240. },
  241. }
  242. out_dir = os.path.dirname(args.output)
  243. if out_dir:
  244. os.makedirs(out_dir, exist_ok=True)
  245. with open(args.output, "w", encoding="utf-8") as f:
  246. json.dump(result, f, ensure_ascii=False, indent=2)
  247. print(f" Results saved to: {args.output}")
  248. if __name__ == "__main__":
  249. main()