zhan1206
fix: audit v2 P1 fixes - D8 Ollama SBLA warning, D13 metrics deprecation
939a2f5
Raw
History Blame Contribute Delete
11.7 kB
"""
模型评估指标
提供各种评估指标来计算模型性能:
- 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
@dataclass
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()
@torch.no_grad()
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
@torch.no_grad()
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)
@torch.no_grad()
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]
@torch.no_grad()
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)")