ReMe/benchmark/longmemeval/compute_stats.py
Zhouwk eff323105f
halumem和longmemeval的Benchmark评估代码 (#124)
* feat(benchmark): 添加 LongMemEval 评估功能和内存检索器

- 实现了 LongMemEval 数据集的评估管道
- 添加了 PersonalLongmemevalRetriever 和 PersonalLongmemevalSummarizer
- 创建了详细的统计分析工具 compute_stats.py
- 实现了完整的答案判断和准确性计算功能
- 集成了 ReMe 内存操作和查询功能
- 添加了性能指标和时间统计功能

* feat(benchmark): 添加内存准确性和完整性评估功能

- 实现了 evaluation_for_memory_accuracy 函数用于评估提取内存的准确性
- 实现了 evaluation_for_memory_integrity 函数用于评估内存完整性
- 创建了 MemoryIntegrityEvaluator 类来评估内存点覆盖情况
- 创建了 MemoryAccuracyEvaluator 类来评估提取内存的准确性
- 添加了 compute_memory_integrity_metrics 和 compute_memory_accuracy_metrics 统计函数
- 在 MetricsAggregator 中集成内存完整性和准确性指标计算
- 更新了命令行参数默认值:top_k 改为 10,batch_size 改为 16
- 重构了个人记忆汇总器中的工具循环逻辑
- 更新了评估提示词模板以支持内存质量评估
- 添加了新的检索配置和模型设置

* feat(memory): 添加个人记忆摘要器配置文件

- 新增 personal_halumem_summarizer_adddraft.yaml 配置文件
- 新增 personal_halumem_summarizer_original_backup.yaml 备份配置文件
- 实现记忆架构师系统提示和用户消息模板
- 实现个人资料代理系统提示和用户消息模板
- 支持生物特征和行为模式记忆存储
- 实现记忆去重和合并功能
- 支持用户个人资料的动态更新和删除操作

* feat(memory): 添加个人记忆摘要器配置文件

- 新增 personal_halumem_summarizer_adddraft.yaml 配置文件
- 新增 personal_halumem_summarizer_original_backup.yaml 备份配置文件
- 实现记忆架构师系统提示和用户消息模板
- 实现个人资料代理系统提示和用户消息模板
- 支持生物特征和行为模式记忆存储
- 实现记忆去重和合并功能
- 支持用户个人资料的动态更新和删除操作

* docs(readme): 添加 ReMe Memory Agent 详细介绍文档

- 创建英文版 README.md 包含核心概念、架构设计和使用指南
- 创建中文版 README_ZH.md 提供完整的本地化文档
- 介绍 Agent 驱动的记忆管理理念和层次化检索机制
- 详述项目架构包括 ReMeSummarizer 和 ReMeRetriever 组件
- 提供快速开始示例和程序化内存操作方法
- 展示 LoCoMo、LongMemEval、HaluMem 基准测试结果
- 包含完整的项目结构说明和配置要求

* chore(config): 移除配置文件中的API密钥

- 从配置文件中删除FLOW_LLM_API_KEY环境变量设置
- 移除相关的API密钥配置项
- 更新配置文档以反映新的安全实践
- 确保敏感信息不再硬编码在配置文件中
- 添加注释说明如何通过环境变量方式配置API密钥

* fix(benchmark): 修复模型调用和配置参数问题

- 修正了reme.get_llm方法的参数传递,移除冗余的name参数
- 添加了qwen3-max模型的配置支持
- 调整了默认并发数从16降至4以提高稳定性
- 修改算法版本默认值从longmemeval和v1统一为default
- 减少每类样本数量默认值从16至2以优化测试效率

* feat(benchmark): 添加记忆准确性和完整性评估功能

- 修改了 simple_request_for_json 调用以支持模型名称参数
- 新增 evaluation_for_memory_accuracy 函数用于评估记忆准确性
- 新增 evaluation_for_memory_integrity 函数用于评估记忆完整性
- 将默认模型名称从 qwen3-max 更改为 None
- 更新提取记忆逻辑以过滤 time_int 和 when_to_use 字段
- 新增 MemoryIntegrityEvaluator 类用于评估记忆完整性
- 新增 MemoryAccuracyEvaluator 类用于评估记忆准确性
- 添加 compute_memory_integrity_metrics 方法计算记忆完整性指标
- 添加 compute_memory_accuracy_metrics 方法计算记忆准确性指标
- 配置多种新 LLM 模型包括 qwen-plus-t、qwen-max-t、gpt-4o-mini 等
- 初始化完整性评估器和准确性评估器实例
- 在会话数据中添加记忆完整性和准确性评估结果
- 收集记忆完整性记录和准确性记录用于统计
- 在最终结果中包含记忆完整性和准确性指标
- 更新摘要打印方法显示记忆完整性和准确性统计信息
- 更新默认评估模型为 gpt-4o-mini-2024-07-18

* docs(readme): 删除 ReMe Memory Agent 的中英文文档

- 移除英文版 README.md 中关于 ReMe Memory Agent 的详细介绍
- 删除中文版 README_ZH.md 中关于 ReMe Memory Agent 的完整文档
- 清理了包括架构图、功能特性、快速开始和实验数据在内的所有文档内容
2026-03-02 18:58:37 +08:00

346 lines
9.9 KiB
Python

"""
LongMemEval Evaluation Statistics Analyzer
Computes detailed statistics from evaluation results including:
- Overall accuracy
- Accuracy by question type
- Timing statistics (summary, retrieval)
- Memory extraction statistics
Usage:
python bench/longmemeval/compute_stats.py \
--results_dir bench/longmemeval/bench_results/longmemeval_reme
"""
import argparse
import json
from collections import defaultdict
from pathlib import Path
from typing import Any
def load_results(results_dir: str) -> list[dict]:
"""Load all question result files from the directory.
Args:
results_dir: Path to the results directory
Returns:
List of result dictionaries
"""
results_path = Path(results_dir)
results = []
# Load individual question files
question_files = sorted(results_path.glob("question_*.json"))
for file_path in question_files:
try:
with open(file_path, "r", encoding="utf-8") as f:
result = json.load(f)
results.append(result)
except Exception as e:
print(f"⚠️ Error loading {file_path}: {e}")
return results
def compute_accuracy_stats(results: list[dict]) -> dict[str, Any]:
"""Compute overall and per-type accuracy statistics.
Args:
results: List of result dictionaries
Returns:
Dictionary with accuracy statistics
"""
total = len(results)
correct = 0
incorrect = 0
error = 0
# Per question type statistics
type_stats = defaultdict(lambda: {"total": 0, "correct": 0, "incorrect": 0, "error": 0})
for r in results:
qtype = r.get("question_type", "unknown")
judgment = r.get("judgment", {})
is_correct = judgment.get("is_correct")
type_stats[qtype]["total"] += 1
if is_correct is True:
correct += 1
type_stats[qtype]["correct"] += 1
elif is_correct is False:
incorrect += 1
type_stats[qtype]["incorrect"] += 1
else:
error += 1
type_stats[qtype]["error"] += 1
# Compute accuracies
overall = {
"total": total,
"correct": correct,
"incorrect": incorrect,
"error": error,
"accuracy": correct / total if total > 0 else 0,
"accuracy_valid": correct / (correct + incorrect) if (correct + incorrect) > 0 else 0,
}
by_type = {}
for qtype, stats in type_stats.items():
valid = stats["correct"] + stats["incorrect"]
by_type[qtype] = {
**stats,
"accuracy": stats["correct"] / stats["total"] if stats["total"] > 0 else 0,
"accuracy_valid": stats["correct"] / valid if valid > 0 else 0,
}
return {
"overall": overall,
"by_question_type": by_type,
}
def compute_timing_stats(results: list[dict]) -> dict[str, Any]:
"""Compute timing statistics.
Args:
results: List of result dictionaries
Returns:
Dictionary with timing statistics
"""
summary_times = []
retrieve_times = []
for r in results:
summary_ms = r.get("summary_duration_ms", 0)
retrieve_ms = r.get("retrieve_duration_ms", 0)
if summary_ms > 0:
summary_times.append(summary_ms)
if retrieve_ms > 0:
retrieve_times.append(retrieve_ms)
def compute_stats(times: list[float]) -> dict:
if not times:
return {"count": 0, "total_ms": 0, "avg_ms": 0, "min_ms": 0, "max_ms": 0}
return {
"count": len(times),
"total_ms": sum(times),
"total_min": sum(times) / 1000 / 60,
"avg_ms": sum(times) / len(times),
"min_ms": min(times),
"max_ms": max(times),
}
return {
"summary": compute_stats(summary_times),
"retrieve": compute_stats(retrieve_times),
"total_time_min": (sum(summary_times) + sum(retrieve_times)) / 1000 / 60,
}
def compute_memory_stats(results: list[dict]) -> dict[str, Any]:
"""Compute memory extraction statistics.
Args:
results: List of result dictionaries
Returns:
Dictionary with memory statistics
"""
memory_counts = []
session_counts = []
for r in results:
memories = r.get("extracted_memories", [])
num_sessions = r.get("num_sessions", 0)
memory_counts.append(len(memories))
session_counts.append(num_sessions)
def compute_stats(counts: list[int]) -> dict:
if not counts:
return {"count": 0, "total": 0, "avg": 0, "min": 0, "max": 0}
return {
"count": len(counts),
"total": sum(counts),
"avg": sum(counts) / len(counts),
"min": min(counts),
"max": max(counts),
}
return {
"memories_per_question": compute_stats(memory_counts),
"sessions_per_question": compute_stats(session_counts),
}
def print_report(
accuracy_stats: dict,
timing_stats: dict,
memory_stats: dict,
results_dir: str,
):
"""Print formatted statistics report.
Args:
accuracy_stats: Accuracy statistics
timing_stats: Timing statistics
memory_stats: Memory statistics
results_dir: Path to results directory
"""
print("\n" + "=" * 80)
print("LONGMEMEVAL EVALUATION STATISTICS")
print(f"Results Directory: {results_dir}")
print("=" * 80)
# Overall accuracy
overall = accuracy_stats["overall"]
print("\n📊 Overall Accuracy:")
print(f" Total Questions: {overall['total']}")
print(f" ✅ Correct: {overall['correct']} ({100 * overall['accuracy']:.2f}%)")
print(
f" ❌ Incorrect: {overall['incorrect']} "
f"({100 * overall['incorrect'] / overall['total'] if overall['total'] > 0 else 0:.2f}%)",
)
if overall["error"] > 0:
print(f" ⚠️ Error: {overall['error']} ({100 * overall['error'] / overall['total']:.2f}%)")
print(f" Accuracy (valid): {100 * overall['accuracy_valid']:.2f}%")
# Accuracy by question type
print("\n📊 Accuracy by Question Type:")
print("-" * 60)
print(f"{'Question Type':<30} {'Correct':<10} {'Total':<10} {'Accuracy':<10}")
print("-" * 60)
by_type = accuracy_stats["by_question_type"]
for qtype in sorted(by_type.keys()):
stats = by_type[qtype]
print(f"{qtype:<30} {stats['correct']:<10} {stats['total']:<10} {100 * stats['accuracy']:.2f}%")
print("-" * 60)
# Timing statistics
print("\n⏱️ Timing Statistics:")
summary = timing_stats["summary"]
retrieve = timing_stats["retrieve"]
print(" Memory Summarization:")
print(f" Total Time: {summary['total_min']:.2f} min")
print(f" Avg per Q: {summary['avg_ms']:.0f} ms")
print(f" Min/Max: {summary['min_ms']:.0f} / {summary['max_ms']:.0f} ms")
print(" Memory Retrieval:")
print(f" Total Time: {retrieve['total_min']:.2f} min")
print(f" Avg per Q: {retrieve['avg_ms']:.0f} ms")
print(f" Min/Max: {retrieve['min_ms']:.0f} / {retrieve['max_ms']:.0f} ms")
print(f" Total Time: {timing_stats['total_time_min']:.2f} min")
# Memory statistics
print("\n📝 Memory Statistics:")
mem = memory_stats["memories_per_question"]
sess = memory_stats["sessions_per_question"]
print(" Extracted Memories per Question:")
print(f" Total: {mem['total']}")
print(f" Average: {mem['avg']:.1f}")
print(f" Min/Max: {mem['min']} / {mem['max']}")
print(" Sessions per Question:")
print(f" Average: {sess['avg']:.1f}")
print(f" Min/Max: {sess['min']} / {sess['max']}")
print("\n" + "=" * 80)
def save_statistics(
accuracy_stats: dict,
timing_stats: dict,
memory_stats: dict,
output_file: str,
):
"""Save statistics to JSON file.
Args:
accuracy_stats: Accuracy statistics
timing_stats: Timing statistics
memory_stats: Memory statistics
output_file: Path to output file
"""
stats = {
"accuracy": accuracy_stats,
"timing": timing_stats,
"memory": memory_stats,
}
with open(output_file, "w", encoding="utf-8") as f:
json.dump(stats, f, indent=4, ensure_ascii=False)
print(f"\n📁 Statistics saved to: {output_file}")
def main(results_dir: str, output_file: str = None):
"""Main function to compute and display statistics.
Args:
results_dir: Path to results directory
output_file: Optional path to save statistics JSON
"""
print(f"\nLoading results from: {results_dir}")
results = load_results(results_dir)
if not results:
print("❌ No results found!")
return
print(f"Loaded {len(results)} question results")
# Compute statistics
accuracy_stats = compute_accuracy_stats(results)
timing_stats = compute_timing_stats(results)
memory_stats = compute_memory_stats(results)
# Print report
print_report(accuracy_stats, timing_stats, memory_stats, results_dir)
# Save to file if specified
if output_file:
save_statistics(accuracy_stats, timing_stats, memory_stats, output_file)
else:
# Default output file in results directory
default_output = Path(results_dir) / "statistics.json"
save_statistics(accuracy_stats, timing_stats, memory_stats, str(default_output))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Compute statistics from LongMemEval evaluation results",
)
parser.add_argument(
"--results_dir",
type=str,
default="bench_results/longmemeval_reme",
help="Path to results directory containing question_*.json files",
)
parser.add_argument(
"--output_file",
type=str,
default=None,
help="Path to save statistics JSON (default: <results_dir>/statistics.json)",
)
args = parser.parse_args()
main(
results_dir=args.results_dir,
output_file=args.output_file,
)