justhum4n's picture
Fix Vercel/Netlify deployment (CPU PyTorch + vercel.json)
a79dc56
Raw
History Blame Contribute Delete
8.06 kB
"""
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()