""" Evaluation Script for Translation Quality Compute BLEU score and other metrics using sacreBleu """ import torch from torch.utils.data import DataLoader from typing import List, Dict import time from tqdm import tqdm from collections import Counter import math import sys import os from pathlib import Path from sacrebleu import corpus_bleu # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from models_best import BestTransformer, TransformerConfig from utils.data_processing import DataProcessor, collate_fn from config import Config def compute_bleu_with_tokens(references: List[List[int]], hypotheses: List[List[int]], processor: DataProcessor, max_n: int = 4) -> Dict[str, float]: """ Compute BLEU score using sacreBleu Args: references: List of reference token sequences hypotheses: List of hypothesis token sequences processor: DataProcessor for decoding tokens to text max_n: Maximum n-gram order (default 4 for BLEU-4) Returns: Dictionary with BLEU scores """ assert len(references) == len(hypotheses), "References and hypotheses must have same length" # Convert token IDs to text reference_texts = [] hypothesis_texts = [] for ref_tokens, hyp_tokens in zip(references, hypotheses): # Decode to text ref_text = processor.decode_sentence(ref_tokens, skip_special_tokens=True) hyp_text = processor.decode_sentence(hyp_tokens, skip_special_tokens=True) reference_texts.append(ref_text) hypothesis_texts.append(hyp_text) # sacreBleu expects references as list of lists (for multiple references per example) refs = [reference_texts] # Wrap in list for sacreBleu format # Calculate BLEU scores for different n-grams using sacreBleu results = {} for n in range(1, max_n + 1): bleu = corpus_bleu( hypothesis_texts, refs, max_ngram_order=n, smooth_method='exp', lowercase=False, tokenize='13a' # Standard international tokenization ) results[f'bleu-{n}'] = bleu.score return results class Evaluator: """ Evaluate translation model """ def __init__(self, model: BestTransformer, test_loader: DataLoader, processor: DataProcessor): """ Args: model: Trained model test_loader: Test dataloader processor: DataProcessor for decoding """ self.model = model self.test_loader = test_loader self.processor = processor self.device = model.device @torch.no_grad() def evaluate(self, use_beam: bool = True, beam_size: int = 5) -> Dict: """ Evaluate model on test set Args: use_beam: Use beam search (slower but better) beam_size: Beam size Returns: Dictionary with evaluation metrics """ self.model.eval() references = [] hypotheses = [] print(f"Evaluating on test set...") print(f" Method: {'Beam Search' if use_beam else 'Greedy Search'}") if use_beam: print(f" Beam size: {beam_size}") start_time = time.time() for batch in tqdm(self.test_loader, desc="Translating"): src = batch['src'].to(self.device) # [batch_size, src_len] tgt = batch['tgt'].to(self.device) # [batch_size, tgt_len] # Get reference (remove BOS and EOS) for t in tgt: ref_tokens = t.tolist() # Remove padding ref_tokens = [tok for tok in ref_tokens if tok != 0] # Remove BOS (2) and EOS (3) ref_tokens = [tok for tok in ref_tokens if tok not in [2, 3]] references.append(ref_tokens) # Translate if use_beam: # Translate each in batch for i in range(src.size(0)): translation = self.model.translate_beam( src[i:i+1], max_len=self.model.config.max_len, beam_size=beam_size, length_penalty=0.6 ) # Remove special tokens hyp_tokens = [tok for tok in translation.tolist() if tok not in [0, 2, 3]] hypotheses.append(hyp_tokens) else: # Greedy search for i in range(src.size(0)): translation = self.model.translate_greedy( src[i:i+1], max_len=self.model.config.max_len ) hyp_tokens = [tok for tok in translation.tolist() if tok not in [0, 2, 3]] hypotheses.append(hyp_tokens) eval_time = time.time() - start_time # Compute BLEU using the BLEU library print("\nComputing BLEU scores...") bleu_results = compute_bleu_with_tokens(references, hypotheses, self.processor) # Add timing info bleu_results['eval_time'] = eval_time bleu_results['sentences_per_sec'] = len(references) / eval_time # Print results print("\n" + "=" * 60) print("Evaluation Results:") print("=" * 60) print(f"BLEU-1: {bleu_results['bleu-1']:.2f}") print(f"BLEU-2: {bleu_results['bleu-2']:.2f}") print(f"BLEU-3: {bleu_results['bleu-3']:.2f}") print(f"BLEU-4: {bleu_results['bleu-4']:.2f}") print(f"Eval time: {eval_time:.1f}s") print(f"Speed: {bleu_results['sentences_per_sec']:.1f} sent/s") print("=" * 60) return bleu_results def show_examples(self, processor: DataProcessor, num_examples: int = 5): """Show translation examples""" self.model.eval() print("\n" + "=" * 60) print(f"Translation Examples:") print("=" * 60) count = 0 for batch in self.test_loader: if count >= num_examples: break src = batch['src'][0:1].to(self.device) # Take first sample tgt = batch['tgt'][0:1].to(self.device) # Translate translation = self.model.translate_beam( src, max_len=self.model.config.max_len, beam_size=4, length_penalty=0.6 ) # Decode to text src_text = processor.decode_sentence(src[0].tolist(), skip_special_tokens=True) ref_text = processor.decode_sentence(tgt[0].tolist(), skip_special_tokens=True) pred_text = processor.decode_sentence(translation.tolist(), skip_special_tokens=True) print(f"\nExample {count+1}:") print(f" Source: {src_text}") print(f" Reference: {ref_text}") print(f" Translation: {pred_text}") count += 1 def main(): """Main evaluation function - Evaluate on TEST SET""" # ========== Configuration ========== # Get project root directory (parent of trainer/) PROJECT_ROOT = Path(__file__).resolve().parent.parent # CHANGE THIS to your trained model checkpoint (relative to project root) CHECKPOINT_PATH = PROJECT_ROOT / "checkpoints" / "best_model_en2vi" / "best_model.pt" TOKENIZER_DIR = PROJECT_ROOT / "SentencePiece-from-scratch" / "tokenizer_models" print("=" * 60) print("EVALUATING MODEL ON TEST SET (EN → VI)") print("=" * 60) # Check if checkpoint exists if not Path(CHECKPOINT_PATH).exists(): print(f"\nāŒ Error: Checkpoint not found at {CHECKPOINT_PATH}") print("\nAvailable checkpoints:") checkpoints_dir = Path(CHECKPOINT_PATH).parent.parent for model_dir in checkpoints_dir.glob('*/'): if model_dir.is_dir(): print(f" - {model_dir.name}/") for ckpt in model_dir.glob('*.pt'): print(f" {ckpt.name}") return # Load checkpoint print(f"\nLoading checkpoint: {CHECKPOINT_PATH}") checkpoint = torch.load(CHECKPOINT_PATH, map_location='cpu', weights_only=False) config = checkpoint['config'] config.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Model configuration:") print(f" d_model: {config.d_model}") print(f" layers: {config.n_encoder_layers}") print(f" heads: {config.n_heads}") print(f" Trained epochs: {checkpoint['epoch']}") print(f" Best val loss: {checkpoint['best_val_loss']:.4f}") # Initialize data processor print("\nInitializing data processor...") processor = DataProcessor(Config) processor.load_tokenizer(TOKENIZER_DIR) # Prepare datasets (load test set) print("\nPreparing test dataset...") datasets = processor.prepare_datasets() test_dataset = datasets['test'] test_loader = DataLoader( test_dataset, batch_size=32, # Can use larger batch for evaluation shuffle=False, collate_fn=lambda b: collate_fn(b, processor.pad_idx), num_workers=0 ) print(f" Test samples: {len(test_dataset):,}") # Create model print("\nCreating model...") model = BestTransformer( src_vocab_size=processor.vocab_size, tgt_vocab_size=processor.vocab_size, config=config ) model.load_state_dict(checkpoint['model_state_dict']) model.to(config.device) model.eval() print(f" Model loaded to {config.device}") print(f" Parameters: {model.count_parameters():,}") # Create evaluator with processor evaluator = Evaluator(model, test_loader, processor) # Show examples evaluator.show_examples(processor, num_examples=5) # Evaluate with beam search print("\n\n" + "=" * 60) print("EVALUATING WITH BEAM SEARCH") print("=" * 60) bleu_beam = evaluator.evaluate(use_beam=True, beam_size=4) # Evaluate with greedy search (faster) print("\n\n" + "=" * 60) print("EVALUATING WITH GREEDY SEARCH") print("=" * 60) bleu_greedy = evaluator.evaluate(use_beam=False) # Compare print("\n" + "=" * 60) print("COMPARISON") print("=" * 60) print(f"Beam Search BLEU-4: {bleu_beam['bleu-4']:.2f}") print(f"Greedy Search BLEU-4: {bleu_greedy['bleu-4']:.2f}") print(f"Improvement: +{bleu_beam['bleu-4'] - bleu_greedy['bleu-4']:.2f}") print("=" * 60) # Save results results_file = Path(CHECKPOINT_PATH).parent / 'test_results.txt' with open(results_file, 'w', encoding='utf-8') as f: f.write("TEST SET EVALUATION RESULTS\n") f.write("=" * 60 + "\n") f.write(f"Checkpoint: {CHECKPOINT_PATH}\n") f.write(f"Test samples: {len(test_dataset):,}\n\n") f.write("Beam Search (beam_size=4):\n") f.write(f" BLEU-1: {bleu_beam['bleu-1']:.2f}\n") f.write(f" BLEU-2: {bleu_beam['bleu-2']:.2f}\n") f.write(f" BLEU-3: {bleu_beam['bleu-3']:.2f}\n") f.write(f" BLEU-4: {bleu_beam['bleu-4']:.2f}\n\n") f.write("Greedy Search:\n") f.write(f" BLEU-1: {bleu_greedy['bleu-1']:.2f}\n") f.write(f" BLEU-2: {bleu_greedy['bleu-2']:.2f}\n") f.write(f" BLEU-3: {bleu_greedy['bleu-3']:.2f}\n") f.write(f" BLEU-4: {bleu_greedy['bleu-4']:.2f}\n") print(f"\nāœ“ Results saved to: {results_file}") print("\n" + "=" * 60) print("EVALUATION COMPLETE") print("=" * 60) if __name__ == '__main__': main()