File size: 9,442 Bytes
534bcfc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LongLaMP 任务评估脚本
计算 BLEU, ROUGE, METEOR 等指标


# 指定输出文件
python evaluation/evaluate_longlamp.py \
    --predictions_file /opt/tiger/PPD/data/long_output/topic_writing_zero_shot/predictions_topic_writing.json \
    --task topic_writing \
    --output_file /opt/tiger/PPD/data/long_output/topic_writing_zero_shot_metrics.json

python evaluation/evaluate_longlamp.py \
    --predictions_file /opt/tiger/PPD/data/long_output/product_review_zero_shot/predictions_product_review.json \
    --task product_review \
    --output_file /opt/tiger/PPD/data/long_output/product_review_zero_shot_metrics.json

python evaluation/evaluate_longlamp.py \
    --predictions_file /opt/tiger/PPD/data/long_output/abstract_generation_zero_shot/predictions_abstract_generation.json \
    --task abstract_generation \
    --output_file /opt/tiger/PPD/data/long_output/abstract_generation_zero_shot_metrics.json

"""

import json
import argparse
import logging
from pathlib import Path
from typing import List, Dict
import evaluate


# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def postprocess_text(preds: List[str], labels: List[str]) -> tuple:
    """
    后处理文本:去除首尾空格
    
    Args:
        preds: 预测文本列表
        labels: 标签文本列表
        
    Returns:
        (处理后的预测列表, 处理后的标签列表)
    """
    preds = [pred.strip() for pred in preds]
    labels = [[label.strip()] for label in labels]  # ROUGE 需要列表格式
    return preds, labels


def compute_metrics(decoded_preds: List[str], decoded_labels: List[str]) -> Dict:
    """
    计算评估指标:BLEU, ROUGE, METEOR
    
    Args:
        decoded_preds: 预测文本列表
        decoded_labels: 标签文本列表
        
    Returns:
        包含所有指标的字典
    """
    # 后处理文本
    decoded_preds, decoded_labels = postprocess_text(decoded_preds, decoded_labels)
    
    # 加载评估指标
    logger.info("加载评估指标...")
    bleu_metric = evaluate.load("sacrebleu")
    rouge_metric = evaluate.load('rouge')
    meteor_metric = evaluate.load('meteor')
    
    # 计算 BLEU
    logger.info("计算 BLEU 分数...")
    result_bleu = bleu_metric.compute(predictions=decoded_preds, references=decoded_labels)
    
    # 计算 ROUGE
    logger.info("计算 ROUGE 分数...")
    result_rouge = rouge_metric.compute(predictions=decoded_preds, references=decoded_labels)
    
    # 计算 METEOR
    logger.info("计算 METEOR 分数...")
    result_meteor = meteor_metric.compute(predictions=decoded_preds, references=decoded_labels)
    
    # 整理结果
    result = {
        "bleu": result_bleu["score"],
        "rouge-1": result_rouge["rouge1"],
        "rouge-2": result_rouge["rouge2"],
        "rouge-L": result_rouge["rougeL"],
        "rouge-LSum": result_rouge["rougeLsum"],
        "meteor": result_meteor['meteor']
    }
    
    return result


def load_predictions(predictions_file: str) -> List[Dict]:
    """
    加载预测结果文件
    
    Args:
        predictions_file: 预测结果 JSON 文件路径
        
    Returns:
        预测结果列表
    """
    predictions_path = Path(predictions_file)
    if not predictions_path.exists():
        raise FileNotFoundError(f"预测文件不存在: {predictions_file}")
    
    with open(predictions_path, 'r', encoding='utf-8') as f:
        predictions = json.load(f)
    
    if not isinstance(predictions, list):
        raise ValueError(f"预测文件格式错误:期望列表,得到 {type(predictions)}")
    
    logger.info(f"加载了 {len(predictions)} 个预测结果")
    return predictions


def extract_predictions_and_labels(predictions: List[Dict]) -> tuple:
    """
    从预测结果中提取预测文本和标签
    
    Args:
        predictions: 预测结果列表,每个元素包含 'generated' 和 'output' 字段
        
    Returns:
        (preds, labels) 元组
    """
    preds = []
    labels = []
    skipped_count = 0
    
    for idx, pred in enumerate(predictions):
        generated = pred.get("generated", "")
        output = pred.get("output", "")
        
        # 只处理有效的预测和标签
        if generated and output:
            preds.append(generated)
            labels.append(output)
        else:
            skipped_count += 1
            sample_id = pred.get('id', f'index_{idx}')
            logger.warning(
                f"跳过无效样本: id={sample_id}, "
                f"generated={'存在' if generated else '缺失'}, "
                f"output={'存在' if output else '缺失'}"
            )
    
    if skipped_count > 0:
        logger.warning(f"跳过了 {skipped_count} 个无效样本")
    
    logger.info(f"提取了 {len(preds)} 个有效预测-标签对")
    
    if len(preds) == 0:
        raise ValueError("没有有效的预测-标签对,无法进行评估")
    
    return preds, labels


def evaluate_predictions(
    predictions_file: str,
    output_file: str = None,
    task_name: str = None
) -> Dict:
    """
    评估预测结果
    
    Args:
        predictions_file: 预测结果 JSON 文件路径
        output_file: 输出结果文件路径(可选)
        task_name: 任务名称(可选,用于输出文件名)
        
    Returns:
        评估结果字典
    """
    logger.info("=" * 50)
    logger.info("开始评估")
    logger.info("=" * 50)
    logger.info(f"预测文件: {predictions_file}")
    
    # 加载预测结果
    predictions = load_predictions(predictions_file)
    
    # 提取预测和标签
    preds, labels = extract_predictions_and_labels(predictions)
    
    # 计算指标
    logger.info("计算评估指标...")
    metrics = compute_metrics(preds, labels)
    
    # 打印结果
    logger.info("=" * 50)
    logger.info("评估结果")
    logger.info("=" * 50)
    for metric_name, metric_value in metrics.items():
        if isinstance(metric_value, dict):
            # 某些指标返回字典(如 ROUGE)
            logger.info(f"{metric_name}:")
            for k, v in metric_value.items():
                if isinstance(v, (int, float)):
                    logger.info(f"  {k}: {v:.4f}")
                else:
                    logger.info(f"  {k}: {v}")
        else:
            if isinstance(metric_value, (int, float)):
                logger.info(f"{metric_name}: {metric_value:.4f}")
            else:
                logger.info(f"{metric_name}: {metric_value}")
    
    # 保存结果
    if output_file:
        output_path = Path(output_file)
    else:
        # 如果没有指定输出文件,使用默认路径
        predictions_path = Path(predictions_file)
        if task_name:
            output_path = predictions_path.parent / f"metrics_{task_name}.json"
        else:
            output_path = predictions_path.parent / "metrics.json"
    
    output_path.parent.mkdir(parents=True, exist_ok=True)
    
    # 构建完整结果
    result = {
        "task": task_name or "unknown",
        "num_samples": len(preds),
        "metrics": metrics
    }
    
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(result, f, ensure_ascii=False, indent=2)
    
    logger.info(f"评估结果已保存到: {output_path}")
    
    return metrics


def main():
    parser = argparse.ArgumentParser(
        description="LongLaMP 任务评估脚本",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例用法:
  # 基本用法
  python evaluate_longlamp.py \\
      --predictions_file ./output/predictions_topic_writing.json \\
      --task topic_writing
  
  # 指定输出文件
  python evaluate_longlamp.py \\
      --predictions_file ./output/predictions_topic_writing.json \\
      --task topic_writing \\
      --output_file ./output/metrics.json
        """
    )
    
    parser.add_argument(
        "--predictions_file",
        type=str,
        required=True,
        help="预测结果 JSON 文件路径(推理脚本的输出文件)"
    )
    parser.add_argument(
        "--output_file",
        type=str,
        default=None,
        help="评估结果输出文件路径(可选,默认保存在预测文件同目录)"
    )
    parser.add_argument(
        "--task",
        type=str,
        default=None,
        choices=["abstract_generation", "product_review", "topic_writing"],
        help="任务名称(可选,用于输出文件名)"
    )
    
    args = parser.parse_args()
    
    try:
        # 运行评估
        metrics = evaluate_predictions(
            predictions_file=args.predictions_file,
            output_file=args.output_file,
            task_name=args.task
        )
        
        logger.info("=" * 50)
        logger.info("评估完成")
        logger.info("=" * 50)
        
        # 返回退出码
        return 0
        
    except FileNotFoundError as e:
        logger.error(f"文件未找到: {e}")
        return 1
    except ValueError as e:
        logger.error(f"数据格式错误: {e}")
        return 1
    except Exception as e:
        logger.error(f"评估过程中出错: {e}", exc_info=True)
        return 1


if __name__ == "__main__":
    exit(main())