Files

189 lines
6.5 KiB
Python
Raw Permalink Normal View History

2026-07-10 17:59:59 +08:00
#!/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()