Files
PlantAI-rag/evaluation/compare_extraction.py
2026-07-10 17:59:59 +08:00

189 lines
6.5 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
对比Gold Standard和自动提取结果
计算Precision, Recall, F1
"""
import json
import argparse
from typing import Dict, List, Tuple, Set
from dataclasses import dataclass
@dataclass
class ComparisonResult:
"""对比结果"""
true_positives: int
false_positives: int
false_negatives: int
precision: float
recall: float
f1: float
def normalize_entity(entity: Dict) -> str:
"""标准化实体用于对比(忽略id,比较type+name+species)"""
entity_type = (entity.get("type") or "").lower()
name = (entity.get("name") or entity.get("id") or "").lower().strip()
species = (entity.get("species") or "").lower().strip()
# For genes/proteins, species is important
if entity_type in ["gene", "protein"]:
return f"{entity_type}:{name}:{species}"
else:
return f"{entity_type}:{name}"
def normalize_relation(relation: Dict, entities: List[Dict]) -> str:
"""标准化关系用于对比(直接使用ID,不转换名称)"""
# 支持两种字段名: "type" 或 "relation"
rel_type = relation.get("type") or relation.get("relation", "")
rel_type = rel_type.lower()
source = relation.get("source", "").lower()
target = relation.get("target", "").lower()
# 直接使用ID,不转换名称(避免名称不一致问题)
return f"{source}--{rel_type}--{target}"
def compare_entities(gold: List[Dict], pred: List[Dict]) -> ComparisonResult:
"""对比实体"""
gold_set = {normalize_entity(e) for e in gold}
pred_set = {normalize_entity(e) for e in pred}
tp = len(gold_set & pred_set)
fp = len(pred_set - gold_set)
fn = len(gold_set - pred_set)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
return ComparisonResult(tp, fp, fn, precision, recall, f1)
def compare_relations(gold: List[Dict], gold_entities: List[Dict],
pred: List[Dict], pred_entities: List[Dict]) -> ComparisonResult:
"""对比关系"""
gold_set = {normalize_relation(r, gold_entities) for r in gold}
pred_set = {normalize_relation(r, pred_entities) for r in pred}
tp = len(gold_set & pred_set)
fp = len(pred_set - gold_set)
fn = len(gold_set - pred_set)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
return ComparisonResult(tp, fp, fn, precision, recall, f1)
def print_comparison(name: str, result: ComparisonResult, gold: List, pred: List):
"""打印对比结果"""
print(f"\n{'='*60}")
print(f"{name} Comparison")
print(f"{'='*60}")
print(f"Gold Standard: {len(gold)} items")
print(f"Predicted: {len(pred)} items")
print(f"-"*60)
print(f"True Positives: {result.true_positives}")
print(f"False Positives: {result.false_positives}")
print(f"False Negatives: {result.false_negatives}")
print(f"-"*60)
print(f"Precision: {result.precision:.3f}")
print(f"Recall: {result.recall:.3f}")
print(f"F1 Score: {result.f1:.3f}")
def print_detailed_comparison(gold: List[Dict], pred: List[Dict], item_type: str = "entity"):
"""打印详细对比(缺失和多余的项)"""
if item_type == "entity":
gold_set = {normalize_entity(e) for e in gold}
pred_set = {normalize_entity(e) for e in pred}
# Create reverse lookup
gold_map = {normalize_entity(e): e for e in gold}
pred_map = {normalize_entity(e): e for e in pred}
else:
# For relations, we need entity lists
return # Skip for now
missing = gold_set - pred_set
extra = pred_set - pred_set
if missing:
print(f"\n⚠️ Missing {item_type}s (False Negatives):")
for item in sorted(missing)[:10]: # Show first 10
print(f" - {item}")
if len(missing) > 10:
print(f" ... and {len(missing) - 10} more")
if extra:
print(f"\n⚠️ Extra {item_type}s (False Positives):")
for item in sorted(extra)[:10]:
print(f" - {item}")
if len(extra) > 10:
print(f" ... and {len(extra) - 10} more")
def main():
parser = argparse.ArgumentParser(description="Compare Gold Standard with automatic extraction")
parser.add_argument("--gold", required=True, help="Gold Standard JSON file")
parser.add_argument("--pred", required=True, help="Predicted/Auto extraction JSON file")
parser.add_argument("--detailed", action="store_true", help="Show detailed comparison")
args = parser.parse_args()
# Load files
print(f"Loading Gold Standard: {args.gold}")
with open(args.gold, 'r', encoding='utf-8') as f:
gold_data = json.load(f)
print(f"Loading Prediction: {args.pred}")
with open(args.pred, 'r', encoding='utf-8') as f:
pred_data = json.load(f)
# Extract entities and relations
gold_entities = gold_data.get("entities", [])
gold_relations = gold_data.get("relations", [])
pred_entities = pred_data.get("entities", [])
pred_relations = pred_data.get("relations", [])
# Compare entities
entity_result = compare_entities(gold_entities, pred_entities)
print_comparison("Entity", entity_result, gold_entities, pred_entities)
if args.detailed:
print_detailed_comparison(gold_entities, pred_entities, "entity")
# Compare relations
relation_result = compare_relations(gold_relations, gold_entities,
pred_relations, pred_entities)
print_comparison("Relation", relation_result, gold_relations, pred_relations)
# Overall summary
print(f"\n{'='*60}")
print("Summary")
print(f"{'='*60}")
print(f"Entity F1: {entity_result.f1:.3f}")
print(f"Relation F1: {relation_result.f1:.3f}")
avg_f1 = (entity_result.f1 + relation_result.f1) / 2
print(f"Average F1: {avg_f1:.3f}")
# Quality assessment
if avg_f1 >= 0.8:
print("\n✅ Excellent extraction quality")
elif avg_f1 >= 0.6:
print("\n⚠️ Good extraction quality, some improvements needed")
elif avg_f1 >= 0.4:
print("\n⚠️ Fair extraction quality, significant improvements needed")
else:
print("\n❌ Poor extraction quality, major rework needed")
if __name__ == "__main__":
main()