fin09
Deploy Bilingual Summarization NLP Suite with Git LFS
3d9ba5b
Raw History Blame Contribute Delete
11.7 kB
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import argparse
import time
import math
import json
from typing import Tuple, Dict, Any, List
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, random_split
from nlp_core.tokenizer import BilingualTokenizer
from nlp_core.vocabulary import Vocabulary
from models.abstractive.seq2seq_model import Seq2SeqSummarizer
from training.dataset import SummarizationDataset, pad_collate_fn
from training.sample_data_gen import generate_synthetic_dataset
from evaluation.metrics_manager import MetricsManager
def build_vocabularies(data_path: str, lang: str = "en") -> Tuple[Vocabulary, Vocabulary]:
"""Builds source and target vocabularies from dataset file."""
tokenizer = BilingualTokenizer()
src_vocab = Vocabulary(name=f"{lang}_src_vocab")
trg_vocab = Vocabulary(name=f"{lang}_trg_vocab")
with open(data_path, "r", encoding="utf-8") as f:
data = json.load(f)
for item in data:
src_tokens = tokenizer.tokenize_words(item["article"], lang=lang, remove_stopwords=False)
trg_tokens = tokenizer.tokenize_words(item["summary"], lang=lang, remove_stopwords=False)
src_vocab.add_sentence(src_tokens)
trg_vocab.add_sentence(trg_tokens)
src_vocab.build_vocab(min_freq=1, max_size=15000)
trg_vocab.build_vocab(min_freq=1, max_size=15000)
return src_vocab, trg_vocab
def train_epoch(model, dataloader, optimizer, criterion, clip: float = 1.0, teacher_forcing_ratio: float = 0.5):
model.train()
epoch_loss = 0.0
for src, trg in dataloader:
src = src.to(model.device)
trg = trg.to(model.device)
optimizer.zero_grad()
output = model(src, trg, teacher_forcing_ratio=teacher_forcing_ratio)
# trg shape: [batch_size, trg_len]
# output shape: [batch_size, trg_len, trg_vocab_size]
trg_vocab_size = output.shape[-1]
# Flatten excluding the <sos> token at index 0
output_dim = output[:, 1:].reshape(-1, trg_vocab_size)
trg_dim = trg[:, 1:].reshape(-1)
loss = criterion(output_dim, trg_dim)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
optimizer.step()
epoch_loss += loss.item()
return epoch_loss / len(dataloader)
def evaluate_epoch(model, dataloader, criterion):
model.eval()
epoch_loss = 0.0
with torch.no_grad():
for src, trg in dataloader:
src = src.to(model.device)
trg = trg.to(model.device)
output = model(src, trg, teacher_forcing_ratio=0.0) # No teacher forcing during eval
trg_vocab_size = output.shape[-1]
output_dim = output[:, 1:].reshape(-1, trg_vocab_size)
trg_dim = trg[:, 1:].reshape(-1)
loss = criterion(output_dim, trg_dim)
epoch_loss += loss.item()
return epoch_loss / len(dataloader)
def evaluate_model_rouge_bleu(
model: Seq2SeqSummarizer,
val_dataset,
raw_data_file: str,
lang: str = "en",
num_samples: int = 15
) -> Dict[str, float]:
"""Computes empirical ROUGE and BLEU metrics over validation samples."""
tokenizer = BilingualTokenizer()
metrics_mgr = MetricsManager()
with open(raw_data_file, "r", encoding="utf-8") as f:
all_samples = json.load(f)
# Pick validation sample subset
eval_samples = all_samples[-num_samples:] if len(all_samples) >= num_samples else all_samples
rouge1_f1_list, rouge2_f1_list, rougel_f1_list = [], [], []
bleu1_list, bleu2_list, bleu_cum_list = [], [], []
for item in eval_samples:
src_tokens = tokenizer.tokenize_words(item["article"], lang=lang)
pred_tokens = model.summarize_beam(src_tokens, beam_width=3, max_len=40)
pred_text = " ".join(pred_tokens)
ref_text = item["summary"]
eval_res = metrics_mgr.evaluate_summary(
original_text=item["article"],
generated_summary=pred_text,
reference_summary=ref_text,
lang=lang
)
if "rouge" in eval_res and "bleu" in eval_res:
rouge1_f1_list.append(eval_res["rouge"]["rouge-1"]["f1"])
rouge2_f1_list.append(eval_res["rouge"]["rouge-2"]["f1"])
rougel_f1_list.append(eval_res["rouge"]["rouge-l"]["f1"])
bleu1_list.append(eval_res["bleu"]["bleu-1"])
bleu2_list.append(eval_res["bleu"]["bleu-2"])
bleu_cum_list.append(eval_res["bleu"]["bleu_cumulative"])
avg_metrics = {
"rouge_1_f1": sum(rouge1_f1_list) / len(rouge1_f1_list) if rouge1_f1_list else 0.0,
"rouge_2_f1": sum(rouge2_f1_list) / len(rouge2_f1_list) if rouge2_f1_list else 0.0,
"rouge_l_f1": sum(rougel_f1_list) / len(rougel_f1_list) if rougel_f1_list else 0.0,
"bleu_1": sum(bleu1_list) / len(bleu1_list) if bleu1_list else 0.0,
"bleu_2": sum(bleu2_list) / len(bleu2_list) if bleu2_list else 0.0,
"cumulative_bleu": sum(bleu_cum_list) / len(bleu_cum_list) if bleu_cum_list else 0.0,
}
return avg_metrics
def run_training(
lang: str = "ar",
epochs: int = 20,
batch_size: int = 8,
lr: float = 0.002,
emb_dim: int = 128,
enc_hid_dim: int = 256,
dec_hid_dim: int = 256,
device: str = "cpu"
):
if sys.stdout.encoding != 'utf-8':
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
print(f"\n========================================================")
print(f"[*] Starting Seq2Seq Training for [{lang.upper()}] (Kaggle Benchmark)")
print(f"========================================================")
# Ensure dataset exists
data_dir = "data/datasets"
data_file = os.path.join(data_dir, f"{'arabic' if lang == 'ar' else 'english'}_corpus.json")
if not os.path.exists(data_file):
print(f"Dataset not found at {data_file}. Generating synthetic dataset...")
generate_synthetic_dataset(data_dir)
# Build vocabularies
src_vocab, trg_vocab = build_vocabularies(data_file, lang=lang)
print(f"Source Vocabulary Size: {len(src_vocab)} | Target Vocabulary Size: {len(trg_vocab)}")
# Prepare Dataset & DataLoader
full_dataset = SummarizationDataset(data_file, src_vocab, trg_vocab, lang=lang)
train_size = int(0.85 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
train_loader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True,
collate_fn=lambda b: pad_collate_fn(b, pad_idx=src_vocab.PAD_IDX)
)
val_loader = DataLoader(
val_dataset,
batch_size=batch_size,
shuffle=False,
collate_fn=lambda b: pad_collate_fn(b, pad_idx=src_vocab.PAD_IDX)
)
# Initialize Model
model = Seq2SeqSummarizer(
src_vocab=src_vocab,
trg_vocab=trg_vocab,
emb_dim=emb_dim,
enc_hid_dim=enc_hid_dim,
dec_hid_dim=dec_hid_dim,
dropout=0.2,
device=device
)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-5)
criterion = nn.CrossEntropyLoss(ignore_index=trg_vocab.PAD_IDX)
best_val_loss = float("inf")
checkpoint_path = f"checkpoints/seq2seq_{'arabic' if lang == 'ar' else 'english'}.pt"
for epoch in range(1, epochs + 1):
start_time = time.time()
# Scheduled teacher forcing decay
tf_ratio = max(0.2, 0.85 - (epoch / epochs) * 0.5)
train_loss = train_epoch(model, train_loader, optimizer, criterion, teacher_forcing_ratio=tf_ratio)
val_loss = evaluate_epoch(model, val_loader, criterion)
scheduler.step()
elapsed = time.time() - start_time
print(f"Epoch {epoch:02d}/{epochs:02d} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | LR: {scheduler.get_last_lr()[0]:.6f} | Time: {elapsed:.2f}s")
if val_loss < best_val_loss:
best_val_loss = val_loss
model.save_checkpoint(checkpoint_path)
print(f" [+] Saved Best Checkpoint -> {checkpoint_path}")
print(f"\n[+] Training finished successfully for {lang.upper()}! Best Val Loss: {best_val_loss:.4f}")
# Load best checkpoint for evaluation
best_model = Seq2SeqSummarizer.load_checkpoint(checkpoint_path, device=device)
print(f"\n[*] Evaluating Quantitative Metrics (ROUGE & BLEU) on Validation Benchmark...")
eval_metrics = evaluate_model_rouge_bleu(best_model, val_dataset, data_file, lang=lang)
print(f"--------------------------------------------------------")
print(f" • ROUGE-1 F1 Score: {eval_metrics['rouge_1_f1'] * 100:.2f}%")
print(f" • ROUGE-2 F1 Score: {eval_metrics['rouge_2_f1'] * 100:.2f}%")
print(f" • ROUGE-L F1 Score: {eval_metrics['rouge_l_f1'] * 100:.2f}%")
print(f" • BLEU-1 Score: {eval_metrics['bleu_1'] * 100:.2f}%")
print(f" • BLEU-2 Score: {eval_metrics['bleu_2'] * 100:.2f}%")
print(f" • Cumulative BLEU: {eval_metrics['cumulative_bleu'] * 100:.2f}%")
print(f"--------------------------------------------------------")
# Test sample inference
tokenizer = BilingualTokenizer()
if lang == "ar":
sample_text = "تعد معالجة اللغات الطبيعية فرعا جوهريا من فروع الذكاء الاصطناعي يهدف إلى جعل الحواسيب قادرة على فهم النصوص البشرية وتوليدها بفعالية."
ref_text = "معالجة اللغات الطبيعية تمكن الحواسيب من فهم النصوص البشرية وتوليدها واستخراج المعلومات."
else:
sample_text = "Natural language processing is an essential branch of artificial intelligence enabling computing systems to comprehend and synthesize human language."
ref_text = "Natural language processing enables computers to understand, process, and generate human language seamlessly."
sample_tokens = tokenizer.tokenize_words(sample_text, lang=lang)
greedy_summary, _ = best_model.summarize_greedy(sample_tokens, max_len=30)
beam_summary = best_model.summarize_beam(sample_tokens, beam_width=3, max_len=30)
print(f"\n[Sample Test Inference]")
print(f"Input: {sample_text}")
print(f"Reference: {ref_text}")
print(f"Greedy: {' '.join(greedy_summary)}")
print(f"Beam (k=3): {' '.join(beam_summary)}\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Train Bilingual Seq2Seq Text Summarization Model on Kaggle Benchmark")
parser.add_argument("--lang", type=str, default="all", choices=["ar", "en", "all"], help="Language to train ('ar', 'en', or 'all')")
parser.add_argument("--epochs", type=int, default=20, help="Number of training epochs")
parser.add_argument("--batch_size", type=int, default=8, help="Batch size")
parser.add_argument("--lr", type=float, default=0.002, help="Learning rate")
parser.add_argument("--device", type=str, default="cpu", help="Device ('cpu' or 'cuda')")
args = parser.parse_args()
if args.lang == "all":
run_training("ar", epochs=args.epochs, batch_size=args.batch_size, lr=args.lr, device=args.device)
run_training("en", epochs=args.epochs, batch_size=args.batch_size, lr=args.lr, device=args.device)
else:
run_training(args.lang, epochs=args.epochs, batch_size=args.batch_size, lr=args.lr, device=args.device)