| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- #!/usr/bin/env python3
- """
- Compute overlap metrics only (Recall / Precision / F1) from raw JSONL.
- Workflow:
- 1. Read data (optional label backfill from reference JSONL)
- 2. Compute overlap metrics and aggregate by tag / overall
- """
- import json
- import argparse
- import os
- from collections import defaultdict
- def load_reference_labels(reference_file):
- """Load label data from reference file, indexed by (video_id, begin, end, dataset_name, idx)."""
- print(f" Loading labels from reference file: {reference_file}")
- labels_by_key = {}
- with open(reference_file, "r", encoding="utf-8") as f:
- for line in f:
- if not line.strip():
- continue
- try:
- item = json.loads(line)
- except json.JSONDecodeError:
- continue
- vid = item.get("video_id")
- dataset = item.get("dataset_name")
- begin = item.get("begin")
- end = item.get("end")
- idx = item.get("idx")
- if not vid:
- continue
- label = item.get("label")
- if label is not None:
- labels_by_key[(vid, begin, end, dataset, idx)] = label
- print(f" Loaded {len(labels_by_key)} label entries from reference file")
- return labels_by_key
- def load_data(input_file, reference_labels=None):
- """Load data and fill missing labels by exact key when reference is provided."""
- print(f"Step 1/2: Reading data: {input_file}")
- results = []
- ref_label_used_count = 0
- with open(input_file, "r", encoding="utf-8") as f:
- for line_num, line in enumerate(f, 1):
- if not line.strip():
- continue
- try:
- item = json.loads(line)
- except json.JSONDecodeError:
- print(f" Warning: JSON parsing failed at line {line_num}")
- continue
- vid = item.get("video_id")
- dataset = item.get("dataset_name")
- if not vid:
- continue
- begin = item.get("begin")
- end = item.get("end")
- idx = item.get("idx")
- tag = item.get("tag")
- pred = item.get("pred")
- label = item.get("label")
- if label is None and reference_labels:
- ref_key = (vid, begin, end, dataset, idx)
- if ref_key in reference_labels:
- label = reference_labels[ref_key]
- ref_label_used_count += 1
- results.append(
- {
- "video_id": vid,
- "dataset_name": dataset,
- "begin": begin,
- "end": end,
- "idx": idx,
- "tag": tag,
- "pred": pred,
- "label": label,
- }
- )
- print(f" Read {len(results)} records")
- if reference_labels:
- print(f" Backfilled {ref_label_used_count} labels from reference file")
- return results
- def get_speaking_seconds_from_content(content, begin, end):
- """Extract speaking seconds from pred or label content as a set[int]."""
- speaking_seconds = set()
- if content is None:
- return speaking_seconds
- if isinstance(content, dict):
- for key in content.keys():
- try:
- speaking_seconds.add(int(key))
- except (TypeError, ValueError):
- continue
- elif isinstance(content, str) and content.strip():
- if begin is not None and end is not None:
- for second in range(begin, end + 1):
- speaking_seconds.add(second)
- return speaking_seconds
- def calculate_overlap_single(sample):
- """Compute Recall/Precision/F1 for one sample."""
- pred = sample.get("pred")
- label = sample.get("label")
- pred_seconds = get_speaking_seconds_from_content(pred, sample.get("begin"), sample.get("end"))
- label_seconds = get_speaking_seconds_from_content(label, sample.get("begin"), sample.get("end"))
- if not label_seconds:
- return None
- tp = len(pred_seconds & label_seconds)
- recall = tp / len(label_seconds) if label_seconds else 0
- precision = tp / len(pred_seconds) if pred_seconds else 0
- f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
- return {
- "video_id": sample.get("video_id"),
- "tag": sample.get("tag"),
- "begin": sample.get("begin"),
- "end": sample.get("end"),
- "idx": sample.get("idx"),
- "recall": recall,
- "precision": precision,
- "f1": f1,
- "tp": tp,
- "predict_count": len(pred_seconds),
- "label_count": len(label_seconds),
- }
- def safe_div(numerator, denominator):
- return numerator / denominator if denominator else 0
- def f1_from_pr(precision, recall):
- return 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
- def evaluate_f1(data_list):
- """Compute overlap metrics for all samples and aggregate supporting stats."""
- print("\nStep 2/2: Computing overlap metrics (Recall/Precision/F1)")
- all_metrics = []
- by_tag = defaultdict(
- lambda: {
- "recall_list": [],
- "precision_list": [],
- "f1_list": [],
- "tp_sum": 0,
- "predict_count_sum": 0,
- "label_count_sum": 0,
- "sample_count": 0,
- }
- )
- overall = {
- "tp_sum": 0,
- "predict_count_sum": 0,
- "label_count_sum": 0,
- }
- for sample in data_list:
- metric = calculate_overlap_single(sample)
- if not metric:
- continue
- all_metrics.append(metric)
- tag = metric["tag"]
- by_tag[tag]["recall_list"].append(metric["recall"])
- by_tag[tag]["precision_list"].append(metric["precision"])
- by_tag[tag]["f1_list"].append(metric["f1"])
- by_tag[tag]["tp_sum"] += metric["tp"]
- by_tag[tag]["predict_count_sum"] += metric["predict_count"]
- by_tag[tag]["label_count_sum"] += metric["label_count"]
- by_tag[tag]["sample_count"] += 1
- overall["tp_sum"] += metric["tp"]
- overall["predict_count_sum"] += metric["predict_count"]
- overall["label_count_sum"] += metric["label_count"]
- print(f" Computed metrics for {len(all_metrics)} samples")
- return all_metrics, by_tag, overall
- def main():
- parser = argparse.ArgumentParser(description="Compute overlap metrics only (Recall/Precision/F1)")
- parser.add_argument("input", help="Input raw JSONL file")
- parser.add_argument("--output", required=True, help="Output result JSON file")
- parser.add_argument("--reference", help="Reference JSONL file for label backfill")
- parser.add_argument("--verbose", action="store_true", help="Save per-sample overlap details")
- args = parser.parse_args()
- exp_name = os.path.splitext(os.path.basename(args.input))[0]
- print("=" * 80)
- print(f"Experiment name: {exp_name}")
- print("=" * 80)
- reference_labels = load_reference_labels(args.reference) if args.reference else None
- data_list = load_data(args.input, reference_labels)
- all_metrics, by_tag, overall = evaluate_f1(data_list)
- all_recall = [m["recall"] for m in all_metrics]
- all_precision = [m["precision"] for m in all_metrics]
- all_f1 = [m["f1"] for m in all_metrics]
- macro_recall = sum(all_recall) / len(all_recall) if all_recall else 0
- macro_precision = sum(all_precision) / len(all_precision) if all_precision else 0
- macro_f1 = sum(all_f1) / len(all_f1) if all_f1 else 0
- micro_recall = safe_div(overall["tp_sum"], overall["label_count_sum"])
- micro_precision = safe_div(overall["tp_sum"], overall["predict_count_sum"])
- micro_f1 = f1_from_pr(micro_precision, micro_recall)
- result = {
- "exp_name": exp_name,
- "overall": {
- "recall": macro_recall,
- "precision": macro_precision,
- "f1": macro_f1,
- "overlap_macro": {
- "sample_count": len(all_metrics),
- "recall": macro_recall,
- "precision": macro_precision,
- "f1": macro_f1,
- },
- "overlap_micro": {
- "sample_count": len(all_metrics),
- "tp": overall["tp_sum"],
- "predict_count": overall["predict_count_sum"],
- "label_count": overall["label_count_sum"],
- "recall": micro_recall,
- "precision": micro_precision,
- "f1": micro_f1,
- },
- },
- "by_tag": {},
- "details": {"overlap": all_metrics if args.verbose else []},
- }
- for tag in sorted(by_tag.keys()):
- recalls = by_tag[tag]["recall_list"]
- precisions = by_tag[tag]["precision_list"]
- f1s = by_tag[tag]["f1_list"]
- tag_macro_recall = sum(recalls) / len(recalls) if recalls else 0
- tag_macro_precision = sum(precisions) / len(precisions) if precisions else 0
- tag_macro_f1 = sum(f1s) / len(f1s) if f1s else 0
- tag_micro_recall = safe_div(by_tag[tag]["tp_sum"], by_tag[tag]["label_count_sum"])
- tag_micro_precision = safe_div(by_tag[tag]["tp_sum"], by_tag[tag]["predict_count_sum"])
- tag_micro_f1 = f1_from_pr(tag_micro_precision, tag_micro_recall)
- result["by_tag"][tag] = {
- "recall": tag_macro_recall,
- "precision": tag_macro_precision,
- "f1": tag_macro_f1,
- "overlap_macro": {
- "sample_count": by_tag[tag]["sample_count"],
- "recall": tag_macro_recall,
- "precision": tag_macro_precision,
- "f1": tag_macro_f1,
- },
- "overlap_micro": {
- "sample_count": by_tag[tag]["sample_count"],
- "tp": by_tag[tag]["tp_sum"],
- "predict_count": by_tag[tag]["predict_count_sum"],
- "label_count": by_tag[tag]["label_count_sum"],
- "recall": tag_micro_recall,
- "precision": tag_micro_precision,
- "f1": tag_micro_f1,
- },
- }
- out_dir = os.path.dirname(args.output)
- if out_dir:
- os.makedirs(out_dir, exist_ok=True)
- with open(args.output, "w", encoding="utf-8") as f:
- json.dump(result, f, ensure_ascii=False, indent=2)
- print(f" Results saved to: {args.output}")
- if __name__ == "__main__":
- main()
|