intent-classifier / src /evaluation /evaluate_all.py
Prince012's picture
feat: initial deployment
72e2b6e
Raw
History Blame Contribute Delete
14.8 kB
import time
from pathlib import Path
import matplotlib.pyplot as plt
import mlflow
import numpy as np
import pandas as pd
import torch
from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
from torch.utils.data import DataLoader
from src.data.loader import load_splits
from src.data.preprocessor import preprocess
from src.features.tfidf import load_vectorizer, transform
from src.models.classical import LogisticRegressionModel, SVMModel
from src.models.neural import IntentDatasetNN, LSTMModel, TextCNN, Vocabulary
from src.models.transformer import IntentDatasetHF, TransformerModel
from src.utils.config import load_config
from src.utils.mlflow_utils import get_or_create_experiment
from src.utils.settings import settings
VECTORIZER_PATH = "artifacts/vectorizers/tfidf.pkl"
VOCAB_PATH = "artifacts/models/vocab.pkl"
LOGREG_PATH = "artifacts/models/logreg.pkl"
SVM_PATH = "artifacts/models/svm.pkl"
TEXTCNN_PATH = "artifacts/models/textcnn.pt"
RNN_PATH = "artifacts/models/rnn.pt"
LSTM_PATH = "artifacts/models/lstm.pt"
DISTILBERT_DIR = "artifacts/models/distilbert"
MAX_LENGTH_NN = 32
MAX_LENGTH_HF = 128
REPORT_DIR = Path("artifacts/evaluation")
def load_data():
splits = load_splits("data/raw")
processed, label_map = preprocess(splits)
id_to_label = {v: k for k, v in label_map.items()}
label_names = [id_to_label[i] for i in range(len(label_map))]
return processed, label_map, label_names
def compute_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
from sklearn.metrics import accuracy_score, f1_score
return {
"accuracy": round(accuracy_score(y_true, y_pred), 4),
"macro_f1": round(f1_score(y_true, y_pred, average="macro", zero_division=0), 4),
"weighted_f1": round(f1_score(y_true, y_pred, average="weighted", zero_division=0), 4),
}
def measure_latency(predict_fn, input_data, n_runs: int = 50) -> dict:
latencies = []
for _ in range(n_runs):
start = time.perf_counter()
predict_fn(input_data)
latencies.append((time.perf_counter() - start) * 1000)
latencies = np.array(latencies)
return {
"latency_mean_ms": round(float(np.mean(latencies)), 3),
"latency_p50_ms": round(float(np.percentile(latencies, 50)), 3),
"latency_p95_ms": round(float(np.percentile(latencies, 95)), 3),
"latency_p99_ms": round(float(np.percentile(latencies, 99)), 3),
}
def eval_logreg(processed: dict, label_names: list[str]) -> tuple[dict, np.ndarray]:
print(" evaluating logistic regression...")
vectorizer = load_vectorizer(VECTORIZER_PATH)
X_test = transform(vectorizer, processed["test"]["text"].tolist())
y_test = processed["test"]["label_id"].values
model = LogisticRegressionModel()
model.load(LOGREG_PATH)
y_pred = model.predict(X_test)
metrics = compute_metrics(y_test, y_pred)
latency = measure_latency(model.predict, X_test[:100])
return {**metrics, **latency}, y_pred
def eval_svm(processed: dict, label_names: list[str]) -> tuple[dict, np.ndarray]:
print(" evaluating svm...")
vectorizer = load_vectorizer(VECTORIZER_PATH)
X_test = transform(vectorizer, processed["test"]["text"].tolist())
y_test = processed["test"]["label_id"].values
model = SVMModel()
model.load(SVM_PATH)
y_pred = model.predict(X_test)
metrics = compute_metrics(y_test, y_pred)
latency = measure_latency(model.predict, X_test[:100])
return {**metrics, **latency}, y_pred
def eval_neural(
model_type: str,
model_path: str,
processed: dict,
label_map: dict,
) -> tuple[dict, np.ndarray]:
print(f" evaluating {model_type}...")
vocab = Vocabulary.load(VOCAB_PATH)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
num_classes = len(label_map)
neural_config = load_config("neural")
model_cfg = neural_config["model"][model_type]
if model_type == "textcnn":
model = TextCNN(
vocab_size=len(vocab),
embedding_dim=model_cfg["embedding_dim"],
num_filters=model_cfg["num_filters"],
kernel_sizes=model_cfg["kernel_sizes"],
num_classes=num_classes,
dropout=model_cfg["dropout"],
)
elif model_type == "rnn":
from src.models.neural import RNNModel
model = RNNModel(
vocab_size=len(vocab),
embedding_dim=model_cfg["embedding_dim"],
hidden_dim=model_cfg["hidden_dim"],
num_layers=model_cfg["num_layers"],
num_classes=num_classes,
dropout=model_cfg["dropout"],
)
elif model_type == "lstm":
model = LSTMModel(
vocab_size=len(vocab),
embedding_dim=model_cfg["embedding_dim"],
hidden_dim=model_cfg["hidden_dim"],
num_layers=model_cfg["num_layers"],
num_classes=num_classes,
dropout=model_cfg["dropout"],
)
model.load(model_path)
model.to(device)
model.eval()
dataset = IntentDatasetNN(
processed["test"]["text"].tolist(),
processed["test"]["label_id"].tolist(),
vocab,
MAX_LENGTH_NN,
)
loader = DataLoader(dataset, batch_size=64, shuffle=False)
all_preds = []
y_test = processed["test"]["label_id"].values
with torch.no_grad():
for inputs, labels in loader:
inputs = inputs.to(device)
logits = model(inputs)
preds = logits.argmax(dim=1).cpu().numpy()
all_preds.extend(preds)
y_pred = np.array(all_preds)
metrics = compute_metrics(y_test, y_pred)
def predict_fn(loader):
with torch.no_grad():
for inputs, _ in loader:
model(inputs.to(device))
latency = measure_latency(predict_fn, loader, n_runs=20)
return {**metrics, **latency}, y_pred
def eval_distilbert(processed: dict, label_map: dict) -> tuple[dict, np.ndarray]:
print(" evaluating distilbert...")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transformer = TransformerModel(
model_name=DISTILBERT_DIR,
num_labels=len(label_map),
)
transformer.model.to(device)
transformer.model.eval()
dataset = IntentDatasetHF(
processed["test"]["text"].tolist(),
processed["test"]["label_id"].tolist(),
transformer.tokenizer,
MAX_LENGTH_HF,
)
loader = DataLoader(dataset, batch_size=32, shuffle=False)
all_preds = []
y_test = processed["test"]["label_id"].values
with torch.no_grad():
for batch in loader:
input_ids = batch["input_ids"].to(device)
attention_mask = batch["attention_mask"].to(device)
outputs = transformer.model(input_ids=input_ids, attention_mask=attention_mask)
preds = outputs.logits.argmax(dim=1).cpu().numpy()
all_preds.extend(preds)
y_pred = np.array(all_preds)
metrics = compute_metrics(y_test, y_pred)
single_text = processed["test"]["text"].iloc[:1].tolist()
single_dataset = IntentDatasetHF(
single_text,
[0],
transformer.tokenizer,
MAX_LENGTH_HF,
)
single_loader = DataLoader(single_dataset, batch_size=1)
def predict_fn(loader):
with torch.no_grad():
for batch in loader:
transformer.model(
input_ids=batch["input_ids"].to(device),
attention_mask=batch["attention_mask"].to(device),
)
latency = measure_latency(predict_fn, single_loader, n_runs=50)
return {**metrics, **latency}, y_pred
def plot_comparison(results: dict, save_path: str) -> None:
df = pd.DataFrame(results).T.reset_index()
df.columns = ["model"] + list(df.columns[1:])
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
metrics = ["accuracy", "macro_f1", "weighted_f1"]
titles = ["Accuracy", "Macro F1", "Weighted F1"]
colors = ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B2", "#937860"]
for ax, metric, title in zip(axes, metrics, titles):
bars = ax.bar(df["model"], df[metric].astype(float), color=colors)
ax.set_title(title, fontsize=14)
ax.set_ylim(0, 1.1)
ax.set_xticks(range(len(df["model"])))
ax.set_xticklabels(df["model"], rotation=30, ha="right")
for bar, val in zip(bars, df[metric].astype(float)):
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + 0.01,
f"{val:.3f}",
ha="center",
va="bottom",
fontsize=9,
)
plt.suptitle("Model Comparison — CLINC150", fontsize=16, y=1.02)
plt.tight_layout()
plt.savefig(save_path, dpi=100, bbox_inches="tight")
plt.close()
print(f" saved: {save_path}")
def plot_latency(results: dict, save_path: str) -> None:
models = list(results.keys())
p50 = [results[m]["latency_p50_ms"] for m in models]
p95 = [results[m]["latency_p95_ms"] for m in models]
x = np.arange(len(models))
width = 0.35
fig, ax = plt.subplots(figsize=(12, 6))
ax.bar(x - width / 2, p50, width, label="P50", color="#4C72B0")
ax.bar(x + width / 2, p95, width, label="P95", color="#DD8452")
ax.set_yscale("log")
ax.set_ylabel("Latency (ms) — log scale")
ax.set_title("Inference Latency Comparison")
ax.set_xticks(x)
ax.set_xticklabels(models, rotation=30, ha="right")
ax.legend()
plt.tight_layout()
plt.savefig(save_path, dpi=100, bbox_inches="tight")
plt.close()
print(f" saved: {save_path}")
def get_top_confused_pairs(
y_true: np.ndarray,
y_pred: np.ndarray,
label_names: list[str],
top_n: int = 20,
) -> pd.DataFrame:
cm = confusion_matrix(y_true, y_pred)
np.fill_diagonal(cm, 0)
pairs = []
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
if cm[i, j] > 0:
pairs.append(
{
"true_label": label_names[i],
"predicted_label": label_names[j],
"count": int(cm[i, j]),
}
)
df = pd.DataFrame(pairs).sort_values("count", ascending=False).head(top_n)
return df.reset_index(drop=True)
def plot_top_confused_pairs(df: pd.DataFrame, model_name: str, save_path: str) -> None:
fig, ax = plt.subplots(figsize=(10, max(6, len(df) * 0.35)))
labels = [f"{row.true_label} -> {row.predicted_label}" for row in df.itertuples()]
ax.barh(labels, df["count"], color="#C44E52")
ax.invert_yaxis()
ax.set_xlabel("Misclassification Count")
ax.set_title(f"Top Confused Pairs — {model_name}")
plt.tight_layout()
plt.savefig(save_path, dpi=100, bbox_inches="tight")
plt.close()
print(f" saved: {save_path}")
def plot_oos_binary_confusion(
y_true: np.ndarray,
y_pred: np.ndarray,
label_map: dict,
model_name: str,
save_path: str,
) -> None:
oos_id = label_map.get("oos")
y_true_binary = (y_true == oos_id).astype(int)
y_pred_binary = (y_pred == oos_id).astype(int)
cm = confusion_matrix(y_true_binary, y_pred_binary)
fig, ax = plt.subplots(figsize=(5, 5))
disp = ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=["in-scope", "oos"],
)
disp.plot(ax=ax, colorbar=False, cmap="Blues")
ax.set_title(f"OOS Detection — {model_name}")
plt.tight_layout()
plt.savefig(save_path, dpi=100, bbox_inches="tight")
plt.close()
print(f" saved: {save_path}")
def main():
REPORT_DIR.mkdir(parents=True, exist_ok=True)
print("loading data...")
processed, label_map, label_names = load_data()
y_test = processed["test"]["label_id"].values
print("\nevaluating all models...")
results = {}
predictions = {}
results["logreg"], predictions["logreg"] = eval_logreg(processed, label_names)
results["svm"], predictions["svm"] = eval_svm(processed, label_names)
results["textcnn"], predictions["textcnn"] = eval_neural("textcnn", TEXTCNN_PATH, processed, label_map)
results["rnn"], predictions["rnn"] = eval_neural("rnn", RNN_PATH, processed, label_map)
results["lstm"], predictions["lstm"] = eval_neural("lstm", LSTM_PATH, processed, label_map)
results["distilbert"], predictions["distilbert"] = eval_distilbert(processed, label_map)
print("\ngenerating plots...")
plot_comparison(results, str(REPORT_DIR / "model_comparison.png"))
plot_latency(results, str(REPORT_DIR / "latency_comparison.png"))
for model_name, y_pred in predictions.items():
confused_df = get_top_confused_pairs(y_test, y_pred, label_names, top_n=20)
confused_csv_path = REPORT_DIR / f"{model_name}_top_confused_pairs.csv"
confused_df.to_csv(confused_csv_path, index=False)
print(f" saved: {confused_csv_path}")
plot_top_confused_pairs(
confused_df,
model_name,
str(REPORT_DIR / f"{model_name}_top_confused_pairs.png"),
)
plot_oos_binary_confusion(
y_test,
y_pred,
label_map,
model_name,
str(REPORT_DIR / f"{model_name}_oos_binary.png"),
)
print("\nlogging to MLflow...")
mlflow.set_tracking_uri(settings.mlflow_tracking_uri)
experiment_id = get_or_create_experiment("intent-classifier")
with mlflow.start_run(experiment_id=experiment_id, run_name="unified-evaluation"):
for model_name, metrics in results.items():
for metric_name, value in metrics.items():
mlflow.log_metric(f"{model_name}.{metric_name}", value)
mlflow.log_artifact(str(REPORT_DIR / "model_comparison.png"))
mlflow.log_artifact(str(REPORT_DIR / "latency_comparison.png"))
print("\nfinal results:")
print(f"{'model':<14} {'accuracy':<12} {'macro_f1':<12} {'weighted_f1':<14} {'p50_ms':<12} {'p95_ms'}")
print("-" * 78)
for model_name, metrics in results.items():
print(
f"{model_name:<14} "
f"{metrics['accuracy']:<12} "
f"{metrics['macro_f1']:<12} "
f"{metrics['weighted_f1']:<14} "
f"{metrics['latency_p50_ms']:<12} "
f"{metrics['latency_p95_ms']}"
)
if __name__ == "__main__":
main()