Spaces:
Sleeping
Sleeping
| """ | |
| train.py β Fine-tune MARBERT on labeled ABSA reviews. | |
| This is the main training loop. It: | |
| 1. Loads and preprocesses the training + validation data. | |
| 2. Applies aggressive Arabic-only filtering. | |
| 3. Builds the MARBERT model with 9 per-aspect softmax heads. | |
| 4. Trains with AdamW optimizer + linear warmup schedule. | |
| 5. Uses weighted CrossEntropy per aspect to handle class imbalance. | |
| 6. Monitors validation Micro-F1 and saves the best checkpoint. | |
| 7. Stops early if validation F1 doesn't improve for N epochs. | |
| Run: | |
| python -m src.train | |
| """ | |
| import os | |
| import time | |
| import json | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import DataLoader | |
| from torch.optim import AdamW | |
| from transformers import get_linear_schedule_with_warmup | |
| from tqdm import tqdm | |
| from src.config import ( | |
| DEVICE, BATCH_SIZE, NUM_EPOCHS, LEARNING_RATE, WEIGHT_DECAY, | |
| WARMUP_RATIO, EARLY_STOPPING_PATIENCE, MODEL_DIR, | |
| TRAIN_FILE, VAL_FILE, NUM_ASPECTS, | |
| ) | |
| from src.preprocess import load_labeled_data, compute_class_weights | |
| from src.dataset import ABSADataset | |
| from src.model import ABSAModel | |
| from src.evaluate import predict_all, compute_micro_f1 | |
| def train_one_epoch(model, dataloader, optimizer, scheduler, class_weights, | |
| device): | |
| """Train for a single epoch, return average loss.""" | |
| model.train() | |
| total_loss = 0.0 | |
| num_batches = 0 | |
| for batch in tqdm(dataloader, desc=" Training", leave=False): | |
| input_ids = batch["input_ids"].to(device) | |
| attn_mask = batch["attention_mask"].to(device) | |
| labels = batch["labels"].to(device) | |
| optimizer.zero_grad() | |
| output = model(input_ids, attn_mask, labels=labels, | |
| class_weights=class_weights) | |
| loss = output["loss"] | |
| loss.backward() | |
| # Gradient clipping prevents exploding gradients | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) | |
| optimizer.step() | |
| scheduler.step() | |
| total_loss += loss.item() | |
| num_batches += 1 | |
| return total_loss / num_batches | |
| def train(train_df=None, val_df=None, model=None, extra_tag="", | |
| num_epochs=NUM_EPOCHS, use_early_stopping=True): | |
| """ | |
| Full training procedure. | |
| Parameters | |
| ---------- | |
| train_df, val_df : pd.DataFrame or None | |
| If None, loads from the default paths. | |
| model : ABSAModel or None | |
| If None, creates a fresh model from MARBERT. | |
| extra_tag : str | |
| Optional suffix for saved model filename. | |
| num_epochs : int | |
| Maximum epochs to train. | |
| use_early_stopping : bool | |
| Whether to use early stopping. Disable for Stage 3 (no val set). | |
| Returns | |
| ------- | |
| model : ABSAModel | |
| The best model (loaded from checkpoint). | |
| best_f1 : float | |
| The best validation F1 achieved (or last training F1). | |
| """ | |
| device = DEVICE | |
| print(f"\n{'='*60}") | |
| print(f" ABSA Training Pipeline") | |
| print(f" Device: {device}") | |
| print(f"{'='*60}\n") | |
| # ββ Load data ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if train_df is None: | |
| print("Loading training data...") | |
| train_df = load_labeled_data(TRAIN_FILE) | |
| if val_df is None and use_early_stopping: | |
| print("Loading validation data...") | |
| val_df = load_labeled_data(VAL_FILE) | |
| print(f" Train samples: {len(train_df)}") | |
| if val_df is not None: | |
| print(f" Val samples: {len(val_df)}") | |
| # ββ Compute class weights ββββββββββββββββββββββββββββββββββββββββ | |
| weights = compute_class_weights(train_df) | |
| class_weights = torch.tensor(weights).to(device) | |
| print(f" Class weights computed (shape: {weights.shape})") | |
| # ββ DataLoaders ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| train_dataset = ABSADataset(train_df) | |
| train_loader = DataLoader( | |
| train_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=False | |
| ) | |
| val_loader = None | |
| if val_df is not None: | |
| val_dataset = ABSADataset(val_df) | |
| val_loader = DataLoader( | |
| val_dataset, batch_size=BATCH_SIZE, shuffle=False | |
| ) | |
| # ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if model is None: | |
| print("Initialising MARBERT model...") | |
| model = ABSAModel() | |
| model = model.to(device) | |
| # ββ Optimizer & Scheduler ββββββββββββββββββββββββββββββββββββββββ | |
| optimizer = AdamW( | |
| model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY | |
| ) | |
| total_steps = len(train_loader) * num_epochs | |
| warmup_steps = int(total_steps * WARMUP_RATIO) | |
| scheduler = get_linear_schedule_with_warmup( | |
| optimizer, | |
| num_warmup_steps=warmup_steps, | |
| num_training_steps=total_steps, | |
| ) | |
| # ββ Training loop ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| os.makedirs(MODEL_DIR, exist_ok=True) | |
| save_path = os.path.join(MODEL_DIR, f"best_model{extra_tag}.pt") | |
| best_f1 = 0.0 | |
| patience_counter = 0 | |
| for epoch in range(1, num_epochs + 1): | |
| start = time.time() | |
| print(f"\nEpoch {epoch}/{num_epochs}") | |
| # Train | |
| avg_loss = train_one_epoch( | |
| model, train_loader, optimizer, scheduler, class_weights, device | |
| ) | |
| elapsed = time.time() - start | |
| if val_loader is not None and use_early_stopping: | |
| # Validate | |
| preds, probs, labels, _ = predict_all(model, val_loader, device) | |
| val_f1 = compute_micro_f1(labels, preds) | |
| print(f" Loss: {avg_loss:.4f} | Val F1: {val_f1:.4f} | " | |
| f"Time: {elapsed:.0f}s") | |
| # Checkpoint | |
| if val_f1 > best_f1: | |
| best_f1 = val_f1 | |
| patience_counter = 0 | |
| torch.save(model.state_dict(), save_path) | |
| print(f" β New best model saved (F1={best_f1:.4f})") | |
| else: | |
| patience_counter += 1 | |
| print(f" β No improvement ({patience_counter}/" | |
| f"{EARLY_STOPPING_PATIENCE})") | |
| if patience_counter >= EARLY_STOPPING_PATIENCE: | |
| print(f"\nEarly stopping triggered after {epoch} epochs.") | |
| break | |
| else: | |
| # No validation set β save every epoch | |
| print(f" Loss: {avg_loss:.4f} | Time: {elapsed:.0f}s") | |
| torch.save(model.state_dict(), save_path) | |
| best_f1 = avg_loss # track loss for reporting | |
| # ββ Load best model ββββββββββββββββββββββββββββββββββββββββββββββ | |
| if val_loader is not None and use_early_stopping: | |
| print(f"\nLoading best model (F1={best_f1:.4f})...") | |
| model.load_state_dict(torch.load(save_path, map_location=device)) | |
| # Final validation | |
| preds, probs, labels, _ = predict_all(model, val_loader, device) | |
| final_f1 = compute_micro_f1(labels, preds) | |
| else: | |
| final_f1 = best_f1 | |
| print(f"\n{'='*60}") | |
| print(f" Training complete!") | |
| print(f" Best Micro-F1: {final_f1:.4f}") | |
| print(f" Model saved to: {save_path}") | |
| print(f"{'='*60}\n") | |
| # Save metadata alongside model | |
| meta = {"best_f1": float(final_f1) if isinstance(final_f1, (float, np.floating)) else 0.0} | |
| with open(os.path.join(MODEL_DIR, "training_meta.json"), "w") as f: | |
| json.dump(meta, f, indent=2) | |
| return model, final_f1 | |
| if __name__ == "__main__": | |
| train() | |