312 lines
11 KiB
Python
312 lines
11 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
"""
|
|||
|
|
评估脚本:计算自动提取 vs Gold Standard的准确率
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from typing import List, Dict, Tuple
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class EvaluationMetrics:
|
|||
|
|
"""评估指标"""
|
|||
|
|
precision: float
|
|||
|
|
recall: float
|
|||
|
|
f1: float
|
|||
|
|
tp: int # True Positive
|
|||
|
|
fp: int # False Positive
|
|||
|
|
fn: int # False Negative
|
|||
|
|
|
|||
|
|
def __str__(self):
|
|||
|
|
return (
|
|||
|
|
f"Precision: {self.precision:.3f}\n"
|
|||
|
|
f"Recall: {self.recall:.3f}\n"
|
|||
|
|
f"F1-Score: {self.f1:.3f}\n"
|
|||
|
|
f"TP: {self.tp}, FP: {self.fp}, FN: {self.fn}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Evaluator:
|
|||
|
|
"""评估器"""
|
|||
|
|
|
|||
|
|
def __init__(self, gold_path: str, pred_path: str):
|
|||
|
|
"""
|
|||
|
|
Args:
|
|||
|
|
gold_path: Gold Standard文件路径
|
|||
|
|
pred_path: 自动提取结果文件路径
|
|||
|
|
"""
|
|||
|
|
with open(gold_path, 'r', encoding='utf-8') as f:
|
|||
|
|
self.gold = json.load(f)
|
|||
|
|
with open(pred_path, 'r', encoding='utf-8') as f:
|
|||
|
|
self.pred = json.load(f)
|
|||
|
|
|
|||
|
|
self.errors = {
|
|||
|
|
"entity": {"fp": [], "fn": [], "type_mismatch": []},
|
|||
|
|
"relation": {"fp": [], "fn": [], "type_mismatch": []}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _normalize_name(self, name: str) -> str:
|
|||
|
|
"""标准化名称用于比较"""
|
|||
|
|
return name.lower().strip().replace(" ", "_")
|
|||
|
|
|
|||
|
|
def match_entity(self, gold_ent: Dict, pred_ent: Dict) -> Tuple[bool, str]:
|
|||
|
|
"""
|
|||
|
|
判断两个实体是否匹配
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(是否匹配, 匹配类型)
|
|||
|
|
匹配类型: "exact"(完全匹配), "name"(名称匹配), "id"(ID匹配), "none"(不匹配)
|
|||
|
|
"""
|
|||
|
|
# 标准1: ID完全匹配(最严格)
|
|||
|
|
if gold_ent.get("id") and pred_ent.get("id"):
|
|||
|
|
if gold_ent["id"] == pred_ent["id"]:
|
|||
|
|
return True, "id"
|
|||
|
|
|
|||
|
|
# 标准2: 名称+类型匹配
|
|||
|
|
gold_name = self._normalize_name(gold_ent.get("name", ""))
|
|||
|
|
pred_name = self._normalize_name(pred_ent.get("name", ""))
|
|||
|
|
|
|||
|
|
if gold_name == pred_name:
|
|||
|
|
if gold_ent.get("type") == pred_ent.get("type"):
|
|||
|
|
return True, "exact"
|
|||
|
|
else:
|
|||
|
|
return False, "type_mismatch"
|
|||
|
|
|
|||
|
|
# 标准3: 名称包含关系(宽松)
|
|||
|
|
if gold_name in pred_name or pred_name in gold_name:
|
|||
|
|
if gold_ent.get("type") == pred_ent.get("type"):
|
|||
|
|
return True, "name"
|
|||
|
|
|
|||
|
|
return False, "none"
|
|||
|
|
|
|||
|
|
def match_relation(self, gold_rel: Dict, pred_rel: Dict) -> Tuple[bool, str]:
|
|||
|
|
"""
|
|||
|
|
判断两个关系是否匹配
|
|||
|
|
|
|||
|
|
关系匹配需要: source匹配 + target匹配 + type相同
|
|||
|
|
"""
|
|||
|
|
# 检查source是否匹配
|
|||
|
|
source_match = self._match_relation_endpoint(
|
|||
|
|
gold_rel.get("source", ""),
|
|||
|
|
pred_rel.get("source", "")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 检查target是否匹配
|
|||
|
|
target_match = self._match_relation_endpoint(
|
|||
|
|
gold_rel.get("target", ""),
|
|||
|
|
pred_rel.get("target", "")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 检查type是否匹配
|
|||
|
|
type_match = gold_rel.get("type") == pred_rel.get("type")
|
|||
|
|
|
|||
|
|
if source_match and target_match and type_match:
|
|||
|
|
return True, "exact"
|
|||
|
|
elif source_match and target_match:
|
|||
|
|
return False, "type_mismatch"
|
|||
|
|
else:
|
|||
|
|
return False, "none"
|
|||
|
|
|
|||
|
|
def _match_relation_endpoint(self, gold_id: str, pred_id: str) -> bool:
|
|||
|
|
"""匹配关系的端点(source或target)"""
|
|||
|
|
# 直接匹配
|
|||
|
|
if gold_id == pred_id:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 名称标准化后匹配
|
|||
|
|
return self._normalize_name(gold_id) == self._normalize_name(pred_id)
|
|||
|
|
|
|||
|
|
def evaluate_entities(self) -> EvaluationMetrics:
|
|||
|
|
"""评估实体提取"""
|
|||
|
|
gold_ents = self.gold.get("entities", [])
|
|||
|
|
pred_ents = self.pred.get("entities", [])
|
|||
|
|
|
|||
|
|
tp = fp = fn = 0
|
|||
|
|
|
|||
|
|
# 计算TP和FP
|
|||
|
|
matched_gold_indices = set()
|
|||
|
|
for pred_ent in pred_ents:
|
|||
|
|
matched = False
|
|||
|
|
for i, gold_ent in enumerate(gold_ents):
|
|||
|
|
is_match, match_type = self.match_entity(gold_ent, pred_ent)
|
|||
|
|
if is_match:
|
|||
|
|
tp += 1
|
|||
|
|
matched_gold_indices.add(i)
|
|||
|
|
matched = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not matched:
|
|||
|
|
fp += 1
|
|||
|
|
self.errors["entity"]["fp"].append({
|
|||
|
|
"pred": pred_ent,
|
|||
|
|
"reason": "未找到匹配的gold实体"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 计算FN
|
|||
|
|
for i, gold_ent in enumerate(gold_ents):
|
|||
|
|
if i not in matched_gold_indices:
|
|||
|
|
fn += 1
|
|||
|
|
self.errors["entity"]["fn"].append({
|
|||
|
|
"gold": gold_ent,
|
|||
|
|
"reason": "未提取到"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 计算指标
|
|||
|
|
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 EvaluationMetrics(precision, recall, f1, tp, fp, fn)
|
|||
|
|
|
|||
|
|
def evaluate_relations(self) -> EvaluationMetrics:
|
|||
|
|
"""评估关系提取"""
|
|||
|
|
gold_rels = self.gold.get("relations", [])
|
|||
|
|
pred_rels = self.pred.get("relations", [])
|
|||
|
|
|
|||
|
|
tp = fp = fn = 0
|
|||
|
|
|
|||
|
|
# 计算TP和FP
|
|||
|
|
matched_gold_indices = set()
|
|||
|
|
for pred_rel in pred_rels:
|
|||
|
|
matched = False
|
|||
|
|
for i, gold_rel in enumerate(gold_rels):
|
|||
|
|
is_match, match_type = self.match_relation(gold_rel, pred_rel)
|
|||
|
|
if is_match:
|
|||
|
|
tp += 1
|
|||
|
|
matched_gold_indices.add(i)
|
|||
|
|
matched = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not matched:
|
|||
|
|
fp += 1
|
|||
|
|
self.errors["relation"]["fp"].append({
|
|||
|
|
"pred": pred_rel,
|
|||
|
|
"reason": "未找到匹配的gold关系"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 计算FN
|
|||
|
|
for i, gold_rel in enumerate(gold_rels):
|
|||
|
|
if i not in matched_gold_indices:
|
|||
|
|
fn += 1
|
|||
|
|
self.errors["relation"]["fn"].append({
|
|||
|
|
"gold": gold_rel,
|
|||
|
|
"reason": "未提取到"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 计算指标
|
|||
|
|
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 EvaluationMetrics(precision, recall, f1, tp, fp, fn)
|
|||
|
|
|
|||
|
|
def print_detailed_errors(self, max_errors: int = 5):
|
|||
|
|
"""打印详细错误分析"""
|
|||
|
|
print("\n" + "="*60)
|
|||
|
|
print("详细错误分析")
|
|||
|
|
print("="*60)
|
|||
|
|
|
|||
|
|
# 实体错误
|
|||
|
|
print("\n【实体错误】")
|
|||
|
|
|
|||
|
|
if self.errors["entity"]["fp"]:
|
|||
|
|
print(f"\n假阳性 (FP): {len(self.errors['entity']['fp'])}个")
|
|||
|
|
print("提取了但不应该存在的实体:")
|
|||
|
|
for i, error in enumerate(self.errors["entity"]["fp"][:max_errors]):
|
|||
|
|
pred = error["pred"]
|
|||
|
|
print(f" {i+1}. {pred.get('name')} ({pred.get('type')})")
|
|||
|
|
print(f" 原因: {error['reason']}")
|
|||
|
|
|
|||
|
|
if self.errors["entity"]["fn"]:
|
|||
|
|
print(f"\n假阴性 (FN): {len(self.errors['entity']['fn'])}个")
|
|||
|
|
print("应该提取但未提取的实体:")
|
|||
|
|
for i, error in enumerate(self.errors["entity"]["fn"][:max_errors]):
|
|||
|
|
gold = error["gold"]
|
|||
|
|
print(f" {i+1}. {gold.get('name')} ({gold.get('type')})")
|
|||
|
|
print(f" ID: {gold.get('id')}")
|
|||
|
|
|
|||
|
|
# 关系错误
|
|||
|
|
print("\n【关系错误】")
|
|||
|
|
|
|||
|
|
if self.errors["relation"]["fp"]:
|
|||
|
|
print(f"\n假阳性 (FP): {len(self.errors['relation']['fp'])}个")
|
|||
|
|
print("提取了但不应该存在的关系:")
|
|||
|
|
for i, error in enumerate(self.errors["relation"]["fp"][:max_errors]):
|
|||
|
|
pred = error["pred"]
|
|||
|
|
print(f" {i+1}. {pred.get('source')} --[{pred.get('type')}]--> {pred.get('target')}")
|
|||
|
|
print(f" 原因: {error['reason']}")
|
|||
|
|
|
|||
|
|
if self.errors["relation"]["fn"]:
|
|||
|
|
print(f"\n假阴性 (FN): {len(self.errors['relation']['fn'])}个")
|
|||
|
|
print("应该提取但未提取的关系:")
|
|||
|
|
for i, error in enumerate(self.errors["relation"]["fn"][:max_errors]):
|
|||
|
|
gold = error["gold"]
|
|||
|
|
print(f" {i+1}. {gold.get('source')} --[{gold.get('type')}]--> {gold.get('target')}")
|
|||
|
|
|
|||
|
|
def generate_report(self, output_path: str = None):
|
|||
|
|
"""生成完整评估报告"""
|
|||
|
|
lines = []
|
|||
|
|
lines.append("="*60)
|
|||
|
|
lines.append("GeneFamily-RAG 提取质量评估报告")
|
|||
|
|
lines.append("="*60)
|
|||
|
|
lines.append(f"Gold Standard: {self.gold.get('doc_id', 'unknown')}")
|
|||
|
|
lines.append(f"Prediction: {self.pred.get('doc_id', 'unknown')}")
|
|||
|
|
lines.append("")
|
|||
|
|
|
|||
|
|
# 实体评估
|
|||
|
|
ent_metrics = self.evaluate_entities()
|
|||
|
|
lines.append("【实体提取】")
|
|||
|
|
lines.append(str(ent_metrics))
|
|||
|
|
lines.append("")
|
|||
|
|
|
|||
|
|
# 关系评估
|
|||
|
|
rel_metrics = self.evaluate_relations()
|
|||
|
|
lines.append("【关系提取】")
|
|||
|
|
lines.append(str(rel_metrics))
|
|||
|
|
lines.append("")
|
|||
|
|
|
|||
|
|
# 综合评分
|
|||
|
|
overall_f1 = (ent_metrics.f1 + rel_metrics.f1) / 2
|
|||
|
|
lines.append("="*60)
|
|||
|
|
lines.append(f"综合F1-Score: {overall_f1:.3f}")
|
|||
|
|
lines.append("="*60)
|
|||
|
|
|
|||
|
|
report = "\n".join(lines)
|
|||
|
|
|
|||
|
|
if output_path:
|
|||
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(report)
|
|||
|
|
print(f"\n报告已保存: {output_path}")
|
|||
|
|
|
|||
|
|
return report
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
parser = argparse.ArgumentParser(description='评估实体关系提取质量')
|
|||
|
|
parser.add_argument('--gold', '-g', required=True, help='Gold Standard文件路径')
|
|||
|
|
parser.add_argument('--pred', '-p', required=True, help='自动提取结果文件路径')
|
|||
|
|
parser.add_argument('--output', '-o', help='评估报告输出路径')
|
|||
|
|
parser.add_argument('--verbose', '-v', action='store_true', help='显示详细错误')
|
|||
|
|
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
# 运行评估
|
|||
|
|
evaluator = Evaluator(args.gold, args.pred)
|
|||
|
|
|
|||
|
|
# 打印报告
|
|||
|
|
report = evaluator.generate_report(args.output)
|
|||
|
|
print(report)
|
|||
|
|
|
|||
|
|
# 详细错误分析
|
|||
|
|
if args.verbose:
|
|||
|
|
evaluator.print_detailed_errors()
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|