TonalIQ-Backend / src /models /train.py
rohan21005's picture
Upload 34 files
2486f9d verified
Raw
History Blame Contribute Delete
7.42 kB
"""
backend/src/models/train.py
Training script to fine-tune Wav2Vec2 on our preprocessed speech emotion dataset.
Includes validation checkpointing and TensorBoard metric tracking.
"""
import logging
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from transformers import Wav2Vec2Processor, Wav2Vec2ForSequenceClassification
from pathlib import Path
from tqdm import tqdm
from sklearn.metrics import accuracy_score, classification_report
from src.models.config import (
BASE_DIR, MODEL_NAME, DATA_DIR, CHECKPOINT_DIR, BATCH_SIZE,
LEARNING_RATE, EPOCHS, WEIGHT_DECAY, NUM_LABELS
)
from src.models.dataset import SpeechEmotionDataset, SpeechDataCollator
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
def train_model():
# 1. Device Setup
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
logger.info(f"Using device for training: {device}")
# 2. Processor & Model Initialization
logger.info(f"Loading pre-trained Wav2Vec2 model: {MODEL_NAME}")
processor = Wav2Vec2Processor.from_pretrained(MODEL_NAME)
# Wav2Vec2 classification head mapped to our exact label count
model = Wav2Vec2ForSequenceClassification.from_pretrained(
MODEL_NAME,
num_labels=NUM_LABELS
)
# Freeze the convolutional feature encoder (crucial for transfer learning)
model.freeze_feature_encoder()
model.to(device)
# 3. Load Datasets
logger.info("Initializing DataLoaders...")
train_manifest = DATA_DIR / "processed" / "train_split.csv"
val_manifest = DATA_DIR / "processed" / "val_split.csv"
if not train_manifest.exists() or not val_manifest.exists():
logger.error("Processed train/val manifests missing. Run preprocessing pipeline first!")
return
train_dataset = SpeechEmotionDataset(train_manifest)
val_dataset = SpeechEmotionDataset(val_manifest)
collator = SpeechDataCollator(processor)
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
collate_fn=collator
)
val_loader = DataLoader(
val_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
collate_fn=collator
)
# 4. Optimization Setup
optimizer = torch.optim.AdamW(
model.parameters(),
lr=LEARNING_RATE,
weight_decay=WEIGHT_DECAY
)
# TensorBoard Writer
writer = SummaryWriter(log_dir=str(BASE_DIR / "runs" / "ser_experiment"))
best_val_loss = float("inf")
patience = 3
epochs_no_improve = 0
start_epoch = 1
best_model_path = CHECKPOINT_DIR / "best_model.pt"
if best_model_path.exists():
logger.info(f"Found checkpoint at {best_model_path}. Loading weights to resume training...")
checkpoint = torch.load(best_model_path, map_location=device, weights_only=True)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch'] + 1
best_val_loss = checkpoint.get('val_loss', float("inf"))
logger.info(f"Resuming from epoch {start_epoch} with best validation loss {best_val_loss:.4f}")
# 5. Training Loop
logger.info("Starting training loop execution...")
for epoch in range(start_epoch, EPOCHS + 1):
model.train()
train_loss = 0.0
loop = tqdm(train_loader, desc=f"Epoch {epoch}/{EPOCHS} [Train]")
for batch in loop:
optimizer.zero_grad()
# Move batch tensors to device
input_values = batch["input_values"].to(device)
labels = batch["labels"].to(device)
attention_mask = batch.get("attention_mask", None)
if attention_mask is not None:
attention_mask = attention_mask.to(device)
outputs = model(
input_values=input_values,
attention_mask=attention_mask,
labels=labels
)
loss = outputs.loss
loss.backward()
optimizer.step()
train_loss += loss.item()
loop.set_postfix(loss=loss.item())
avg_train_loss = train_loss / len(train_loader)
writer.add_scalar("Loss/Train", avg_train_loss, epoch)
# Validation Phase
model.eval()
val_loss = 0.0
all_preds = []
all_labels = []
with torch.no_grad():
for batch in val_loader:
input_values = batch["input_values"].to(device)
labels = batch["labels"].to(device)
attention_mask = batch.get("attention_mask", None)
if attention_mask is not None:
attention_mask = attention_mask.to(device)
outputs = model(
input_values=input_values,
attention_mask=attention_mask,
labels=labels
)
val_loss += outputs.loss.item()
logits = outputs.logits
preds = torch.argmax(logits, dim=-1)
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
avg_val_loss = val_loss / len(val_loader)
val_acc = accuracy_score(all_labels, all_preds)
writer.add_scalar("Loss/Validation", avg_val_loss, epoch)
writer.add_scalar("Accuracy/Validation", val_acc, epoch)
logger.info(
f"Epoch {epoch} Results - "
f"Train Loss: {avg_train_loss:.4f} | "
f"Val Loss: {avg_val_loss:.4f} | "
f"Val Acc: {val_acc:.4f}"
)
# Checkpointing
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
epochs_no_improve = 0
best_model_path = CHECKPOINT_DIR / "best_model.pt"
logger.info(f"New validation loss baseline achieved. Saving checkpoint to {best_model_path}")
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'val_loss': best_val_loss,
'val_acc': val_acc
}, best_model_path)
# Save the raw huggingface configurations together for ONNX export
model.save_pretrained(CHECKPOINT_DIR / "hf_model")
processor.save_pretrained(CHECKPOINT_DIR / "hf_model")
else:
epochs_no_improve += 1
if epochs_no_improve >= patience:
logger.info(f"Early stopping triggered after {epoch} epochs.")
break
writer.close()
logger.info("Model fine-tuning training workflow finished successfully.")
if __name__ == "__main__":
train_model()