Spaces:
Running
Running
| """ | |
| 模型评估指标 | |
| 提供各种评估指标来计算模型性能: | |
| - Perplexity (困惑度) | |
| - BLEU score | |
| - ROUGE score | |
| - Accuracy | |
| - Loss | |
| """ | |
| import math | |
| import sys | |
| import warnings | |
| from typing import List, Dict, Optional | |
| from dataclasses import dataclass | |
| import torch | |
| import torch.nn.functional as F | |
| class EvaluationMetrics: | |
| """评估结果容器""" | |
| perplexity: float = 0.0 | |
| loss: float = 0.0 | |
| accuracy: float = 0.0 | |
| bleu: float = 0.0 | |
| rouge1: float = 0.0 | |
| rouge2: float = 0.0 | |
| rougeL: float = 0.0 | |
| def __str__(self) -> str: | |
| lines = ["[Evaluation Metrics]"] | |
| lines.append(f" Perplexity: {self.perplexity:.4f}") | |
| lines.append(f" Loss: {self.loss:.4f}") | |
| lines.append(f" Accuracy: {self.accuracy:.4f}") | |
| lines.append(f" BLEU: {self.bleu:.4f}") | |
| lines.append(f" ROUGE-1: {self.rouge1:.4f}") | |
| lines.append(f" ROUGE-2: {self.rouge2:.4f}") | |
| lines.append(f" ROUGE-L: {self.rougeL:.4f}") | |
| return "\n".join(lines) | |
| class ModelEvaluator: | |
| """模型评估器""" | |
| def __init__( | |
| self, | |
| model: torch.nn.Module, | |
| tokenizer = None, | |
| device: str = "cpu", | |
| ): | |
| """ | |
| 初始化评估器 | |
| 参数: | |
| model: 要评估的模型 | |
| tokenizer: tokenizer(可选) | |
| device: 设备(cpu/cuda) | |
| """ | |
| self.model = model | |
| self.tokenizer = tokenizer | |
| self.device = device | |
| self.model.to(device) | |
| self.model.eval() | |
| def compute_perplexity( | |
| self, | |
| text: str, | |
| ) -> float: | |
| """ | |
| 计算文本的困惑度(Perplexity) | |
| 困惑度是语言模型的基本评估指标, | |
| 表示模型对文本的预测能力(越低越好)。 | |
| 参数: | |
| text: 输入文本 | |
| 返回: | |
| 困惑度值 | |
| """ | |
| if self.tokenizer is None: | |
| # Fallback: 使用 UTF-8 字节编码 | |
| input_ids = torch.tensor([list(text.encode('utf-8'))], dtype=torch.long).to(self.device) | |
| else: | |
| input_ids = torch.tensor([self.tokenizer.encode(text)]).to(self.device) | |
| # 前向传播 | |
| outputs = self.model( | |
| input_ids=input_ids[:, :-1], | |
| labels=input_ids[:, 1:], | |
| ) | |
| # 计算困惑度 | |
| loss = outputs.loss if hasattr(outputs, 'loss') else outputs["loss"] | |
| # loss is already mean-reduced over tokens by model, so PPL = exp(loss) | |
| perplexity = torch.exp(loss).item() | |
| return perplexity | |
| def compute_loss( | |
| self, | |
| texts: List[str], | |
| max_length: int = 512, | |
| ) -> float: | |
| """ | |
| 计算一批文本的平均 loss | |
| 参数: | |
| texts: 文本列表 | |
| max_length: 最大长度 | |
| 返回: | |
| 平均 loss | |
| """ | |
| total_loss = 0.0 | |
| count = 0 | |
| for text in texts: | |
| if self.tokenizer is None: | |
| input_ids = torch.tensor([list(text.encode('utf-8'))[:max_length]], dtype=torch.long).to(self.device) | |
| else: | |
| encoded = self.tokenizer.encode(text, max_length=max_length, truncation=True) | |
| input_ids = torch.tensor([encoded]).to(self.device) | |
| if input_ids.shape[1] < 2: | |
| continue # 跳过太短的文本 | |
| outputs = self.model( | |
| input_ids=input_ids[:, :-1], | |
| labels=input_ids[:, 1:], | |
| ) | |
| loss = outputs["loss"] if isinstance(outputs, dict) else outputs.loss | |
| total_loss += loss.item() | |
| count += 1 | |
| return total_loss / max(count, 1) | |
| def compute_accuracy( | |
| self, | |
| texts: List[str], | |
| max_length: int = 512, | |
| ) -> float: | |
| """ | |
| 计算下一个 token 预测准确率 | |
| 参数: | |
| texts: 文本列表 | |
| max_length: 最大长度 | |
| 返回: | |
| 准确率(0-1) | |
| """ | |
| correct = 0 | |
| total = 0 | |
| for text in texts: | |
| if self.tokenizer is None: | |
| input_ids = torch.tensor([list(text.encode('utf-8'))[:max_length]], dtype=torch.long).to(self.device) | |
| else: | |
| encoded = self.tokenizer.encode(text, max_length=max_length, truncation=True) | |
| input_ids = torch.tensor([encoded]).to(self.device) | |
| if input_ids.shape[1] < 2: | |
| continue | |
| outputs = self.model( | |
| input_ids=input_ids[:, :-1], | |
| ) | |
| logits = outputs["logits"] if isinstance(outputs, dict) else outputs.logits | |
| predictions = logits.argmax(dim=-1) | |
| targets = input_ids[:, 1:] | |
| # 只计算有效位置 | |
| correct += (predictions == targets).sum().item() | |
| total += targets.numel() | |
| return correct / max(total, 1) | |
| def compute_bleu( | |
| self, | |
| predictions: List[str], | |
| references: List[str], | |
| ) -> float: | |
| """ | |
| 计算 BLEU score(简化版) | |
| .. deprecated:: | |
| 请使用 evaluation/bleu_rouge_meteor.py:compute_bleu() 代替。 | |
| 该实现缺少 BLEU-4 brevity penalty 和 n-gram clipping。 | |
| 参数: | |
| predictions: 预测文本列表 | |
| references: 参考文本列表 | |
| 返回: | |
| BLEU score(0-1) | |
| """ | |
| if len(predictions) != len(references): | |
| raise ValueError("predictions 和 references 长度必须相同") | |
| total_bleu = 0.0 | |
| for pred, ref in zip(predictions, references): | |
| # 简化的 BLEU:计算 1-gram 和 2-gram 重合度 | |
| pred_tokens = pred.split() | |
| ref_tokens = ref.split() | |
| if len(pred_tokens) == 0 or len(ref_tokens) == 0: | |
| continue | |
| # 1-gram 重合 | |
| pred_unigram_set = set(pred_tokens) | |
| ref_unigram_set = set(ref_tokens) | |
| unigram_overlap = len(pred_unigram_set & ref_unigram_set) / max(len(pred_unigram_set), 1) | |
| # 2-gram 重合 | |
| pred_bigrams = set(zip(pred_tokens[:-1], pred_tokens[1:])) | |
| ref_bigrams = set(zip(ref_tokens[:-1], ref_tokens[1:])) | |
| if len(pred_bigrams) > 0 and len(ref_bigrams) > 0: | |
| bigram_overlap = len(pred_bigrams & ref_bigrams) / max(len(pred_bigrams), 1) | |
| else: | |
| bigram_overlap = 0.0 | |
| # BLEU 简化公式:0.5 * unigram + 0.5 * bigram | |
| bleu = 0.5 * unigram_overlap + 0.5 * bigram_overlap | |
| total_bleu += bleu | |
| return total_bleu / max(len(predictions), 1) | |
| def compute_rouge( | |
| self, | |
| predictions: List[str], | |
| references: List[str], | |
| ) -> Dict[str, float]: | |
| """ | |
| 计算 ROUGE score(简化版) | |
| .. deprecated:: | |
| 请使用 evaluation/bleu_rouge_meteor.py:compute_rouge() 代替。 | |
| 该实现缺少标准 ROUGE-L 的 LCS 精确计算。 | |
| 参数: | |
| predictions: 预测文本列表 | |
| references: 参考文本列表 | |
| 返回: | |
| ROUGE-1, ROUGE-2, ROUGE-L 的字典 | |
| """ | |
| rouge1_scores = [] | |
| rouge2_scores = [] | |
| rougeL_scores = [] | |
| for pred, ref in zip(predictions, references): | |
| pred_tokens = pred.split() | |
| ref_tokens = ref.split() | |
| if len(pred_tokens) == 0 or len(ref_tokens) == 0: | |
| rouge1_scores.append(0.0) | |
| rouge2_scores.append(0.0) | |
| rougeL_scores.append(0.0) | |
| continue | |
| # ROUGE-1: unigram 重合率 | |
| pred_unigram_set = set(pred_tokens) | |
| ref_unigram_set = set(ref_tokens) | |
| overlap_1 = len(pred_unigram_set & ref_unigram_set) | |
| rouge1 = overlap_1 / max(len(ref_unigram_set), 1) | |
| rouge1_scores.append(rouge1) | |
| # ROUGE-2: bigram 重合率 | |
| pred_bigrams = set(zip(pred_tokens[:-1], pred_tokens[1:])) | |
| ref_bigrams = set(zip(ref_tokens[:-1], ref_tokens[1:])) | |
| if len(ref_bigrams) > 0: | |
| overlap_2 = len(pred_bigrams & ref_bigrams) | |
| rouge2 = overlap_2 / max(len(ref_bigrams), 1) | |
| else: | |
| rouge2 = 0.0 | |
| rouge2_scores.append(rouge2) | |
| # ROUGE-L: 最长公共子序列(简化版:用编辑距离近似) | |
| lcs_length = self._approximate_lcs(pred_tokens, ref_tokens) | |
| rougeL = lcs_length / max(len(ref_tokens), 1) | |
| rougeL_scores.append(rougeL) | |
| return { | |
| "rouge1": sum(rouge1_scores) / max(len(rouge1_scores), 1), | |
| "rouge2": sum(rouge2_scores) / max(len(rouge2_scores), 1), | |
| "rougeL": sum(rougeL_scores) / max(len(rougeL_scores), 1), | |
| } | |
| def _approximate_lcs(self, seq1: List[str], seq2: List[str]) -> int: | |
| """近似计算最长公共子序列长度""" | |
| # 简化版:使用动态规划 | |
| m, n = len(seq1), len(seq2) | |
| dp = [[0] * (n + 1) for _ in range(m + 1)] | |
| for i in range(1, m + 1): | |
| for j in range(1, n + 1): | |
| if seq1[i-1] == seq2[j-1]: | |
| dp[i][j] = dp[i-1][j-1] + 1 | |
| else: | |
| dp[i][j] = max(dp[i-1][j], dp[i][j-1]) | |
| return dp[m][n] | |
| def evaluate( | |
| self, | |
| texts: List[str], | |
| max_length: int = 512, | |
| ) -> EvaluationMetrics: | |
| """ | |
| 完整评估:计算所有指标 | |
| 参数: | |
| texts: 文本列表 | |
| max_length: 最大长度 | |
| 返回: | |
| EvaluationMetrics 对象 | |
| """ | |
| metrics = EvaluationMetrics() | |
| # 1. Perplexity(使用第一个文本) | |
| if len(texts) > 0: | |
| metrics.perplexity = self.compute_perplexity(texts[0]) | |
| # 2. Loss | |
| metrics.loss = self.compute_loss(texts, max_length) | |
| # 3. Accuracy | |
| metrics.accuracy = self.compute_accuracy(texts, max_length) | |
| return metrics | |
| def evaluate_model( | |
| model: torch.nn.Module, | |
| texts: List[str], | |
| tokenizer = None, | |
| device: str = "cpu", | |
| ) -> EvaluationMetrics: | |
| """ | |
| 便捷函数:评估模型 | |
| 参数: | |
| model: 要评估的模型 | |
| texts: 评估文本 | |
| tokenizer: tokenizer(可选) | |
| device: 设备 | |
| 返回: | |
| 评估指标 | |
| """ | |
| evaluator = ModelEvaluator(model, tokenizer, device) | |
| return evaluator.evaluate(texts) | |
| if __name__ == "__main__": | |
| print("[Evaluation] 模型评估指标模块") | |
| print() | |
| print("功能:") | |
| print(" - Perplexity(困惑度)") | |
| print(" - Loss") | |
| print(" - Accuracy") | |
| print(" - BLEU score") | |
| print(" - ROUGE score") | |
| print() | |
| print("用法:") | |
| print(" from evaluation.metrics import ModelEvaluator") | |
| print(" evaluator = ModelEvaluator(model, tokenizer)") | |
| print(" metrics = evaluator.evaluate(texts)") | |
| print(" print(metrics)") | |