justhum4n's picture
Fix Vercel/Netlify deployment (CPU PyTorch + vercel.json)
a79dc56
Raw
History Blame Contribute Delete
5.12 kB
"""
evaluate.py — Compute metrics on the validation set.
The competition uses Micro-F1 as the primary metric. Here we also
compute per-aspect breakdowns so we can see exactly where the model
is strong or weak.
Adapted for the per-aspect softmax architecture (9 heads × 4 classes).
"""
import json
import numpy as np
import torch
from torch.utils.data import DataLoader
from sklearn.metrics import f1_score
from tqdm import tqdm
from src.config import (
DEVICE, BATCH_SIZE, ASPECTS, NUM_ASPECTS,
IDX_TO_SENTIMENT, IDX_TO_ASPECT,
VAL_FILE, MODEL_DIR,
)
from src.preprocess import load_labeled_data, decode_labels
from src.dataset import ABSADataset
from src.model import ABSAModel
def predict_all(model, dataloader, device):
"""
Run the model on a dataloader and collect all predictions.
Returns
-------
all_preds : np.ndarray (N, 9) — predicted class per aspect
all_probs : list of np.ndarray — raw softmax probs per aspect
all_labels : np.ndarray (N, 9) — ground truth class per aspect
all_ids : list — review IDs
"""
model.eval()
all_preds = []
all_labels = []
all_ids = []
all_aspect_probs = [[] for _ in range(NUM_ASPECTS)]
with torch.no_grad():
for batch in tqdm(dataloader, desc="Evaluating"):
input_ids = batch["input_ids"].to(device)
attn_mask = batch["attention_mask"].to(device)
labels = batch["labels"] # (batch, 9) int64
output = model(input_ids, attn_mask)
probs_list = output["probs"] # list of 9 tensors, each (batch, 4)
batch_preds = []
for i, probs in enumerate(probs_list):
p = probs.cpu().numpy() # (batch, 4)
all_aspect_probs[i].append(p)
pred = np.argmax(p, axis=-1) # (batch,)
batch_preds.append(pred)
# Stack into (batch, 9)
batch_preds = np.stack(batch_preds, axis=1)
all_preds.append(batch_preds)
all_labels.append(labels.numpy())
all_ids.extend(batch["review_id"].tolist())
all_preds = np.concatenate(all_preds, axis=0)
all_labels = np.concatenate(all_labels, axis=0)
# Concatenate per-aspect probs
for i in range(NUM_ASPECTS):
all_aspect_probs[i] = np.concatenate(all_aspect_probs[i], axis=0)
return all_preds, all_aspect_probs, all_labels, all_ids
def compute_micro_f1(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
Compute Micro-F1 in the ABSA sense:
An (aspect, sentiment) pair is a "positive example" if the aspect
is mentioned (class != 0). A correct prediction must match BOTH
the aspect being mentioned AND the correct sentiment.
We convert back to 27-dim binary vectors (the original competition
format) and compute micro-F1 on those.
"""
# Build 27-dim binary vectors for true and pred
num_labels = NUM_ASPECTS * 3 # 9 aspects × 3 sentiments (excl. absent)
y_true_bin = np.zeros((len(y_true), num_labels), dtype=int)
y_pred_bin = np.zeros((len(y_pred), num_labels), dtype=int)
for i in range(len(y_true)):
for j in range(NUM_ASPECTS):
true_class = y_true[i, j]
pred_class = y_pred[i, j]
# class 0 = not_mentioned, 1 = positive, 2 = negative, 3 = neutral
if true_class > 0:
idx = j * 3 + (true_class - 1)
y_true_bin[i, idx] = 1
if pred_class > 0:
idx = j * 3 + (pred_class - 1)
y_pred_bin[i, idx] = 1
return f1_score(y_true_bin.flatten(), y_pred_bin.flatten(), average="binary")
def evaluate_model(model=None, val_df=None, device=DEVICE, verbose=True):
"""
Full evaluation pipeline. If model/data not provided, loads from disk.
"""
import os
if val_df is None:
val_df = load_labeled_data(VAL_FILE)
if model is None:
model = ABSAModel()
weights_path = os.path.join(MODEL_DIR, "best_model.pt")
model.load_state_dict(torch.load(weights_path, map_location=device))
model = model.to(device)
val_dataset = ABSADataset(val_df)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)
preds, probs, labels, ids = predict_all(model, val_loader, device)
f1 = compute_micro_f1(labels, preds)
if verbose:
print(f"\n{'='*60}")
print(f" Micro-F1 (ABSA): {f1:.4f}")
print(f"{'='*60}\n")
# Per-aspect breakdown
for i, aspect in enumerate(ASPECTS):
true_mentioned = (labels[:, i] > 0).sum()
pred_mentioned = (preds[:, i] > 0).sum()
correct = ((labels[:, i] > 0) & (preds[:, i] == labels[:, i])).sum()
if true_mentioned > 0:
acc = correct / true_mentioned
print(f" {aspect:20s}: mentioned={true_mentioned:4d}, "
f"pred={pred_mentioned:4d}, correct={correct:4d}, "
f"acc={acc:.3f}")
return f1, preds, probs, labels
if __name__ == "__main__":
evaluate_model()