razorshield-api / src /explanation /benchmark.py
Vedant Sanjay Jadhav
feat: complete RazorShield AI risk platform
10ec54c
Raw
History Blame Contribute Delete
15 kB
"""
benchmark.py
------------
RazorShield Zero-Shot Hugging Face SLM Benchmark Suite.
Generates 300+ deterministic evidence examples and gold expectations,
evaluates candidate models across JSON validity, schema validity, numeric grounding,
decision consistency, severity consistency, campaign consistency, signal coverage,
hallucination rate, output length, latency (Load, Avg, P50, P95, P99), and memory usage.
Outputs:
- data/explanation/evidence_dataset.jsonl
- data/explanation/benchmark_results.json
- data/explanation/benchmark_results.csv
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
import random
import time
from typing import Any
import numpy as np
import pandas as pd
import torch
from src.explanation.explainer import RazorShieldExplainer
from src.explanation.model_loader import SLMModelLoader
from src.explanation.schemas import ExplanationInput, GoldExpectation
ROOT = Path(__file__).resolve().parents[2]
DATA_DIR = ROOT / "data" / "explanation"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
LOGGER = logging.getLogger("slm-benchmark")
def generate_benchmark_dataset(num_examples: int = 300) -> list[dict[str, Any]]:
"""Generates a deterministic dataset of 300+ evidence examples with gold expectations."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
dataset_path = DATA_DIR / "evidence_dataset.jsonl"
random.seed(42)
np.random.seed(42)
categories = [
"NORMAL", "INVESTIGATE", "ALERT", "VOLUME_ONLY_SPIKE",
"AMOUNT_SHIFT", "FRAUD_DURING_CAMPAIGN", "CAMPAIGN_WITHOUT_FRAUD"
]
dataset = []
per_cat = (num_examples // len(categories)) + 1
for cat_idx, cat in enumerate(categories):
for i in range(per_cat):
m_id = f"M_{100 + ((cat_idx * per_cat + i) % 50)}"
if cat == "NORMAL":
inc_state = "NORMAL"
sev = "LOW"
score = round(random.uniform(0.01, 0.20), 4)
spike_p = round(random.uniform(0.01, 0.15), 4)
fe_ratio = round(random.uniform(0.8, 1.2), 2)
vel_ratio = round(random.uniform(0.9, 1.2), 2)
susp_win = 0
camp = False
action = "Maintain standard automated processing."
signals = [{"name": "velocity_ratio", "value": vel_ratio, "direction": "normal"}]
elif cat == "INVESTIGATE":
inc_state = "INVESTIGATE"
sev = "MEDIUM"
score = round(random.uniform(0.35, 0.55), 4)
spike_p = round(random.uniform(0.25, 0.45), 4)
fe_ratio = round(random.uniform(1.8, 2.8), 2)
vel_ratio = round(random.uniform(1.5, 2.5), 2)
susp_win = 1
camp = False
action = "Monitor merchant stream closely and apply selective verification."
signals = [
{"name": "spike_probability", "value": spike_p, "direction": "elevated"},
{"name": "fraud_excess_ratio", "value": fe_ratio, "direction": "elevated"},
]
elif cat == "ALERT":
inc_state = "ALERT"
sev = "HIGH"
score = round(random.uniform(0.68, 0.95), 4)
spike_p = round(random.uniform(0.50, 0.92), 4)
fe_ratio = round(random.uniform(3.5, 12.0), 2)
vel_ratio = round(random.uniform(2.0, 5.0), 2)
susp_win = random.randint(2, 5)
camp = False
action = "Initiate immediate merchant review and enforce step-up authentication."
signals = [
{"name": "spike_probability", "value": spike_p, "direction": "elevated"},
{"name": "fraud_excess_ratio", "value": fe_ratio, "direction": "elevated"},
{"name": "consecutive_suspicious_windows", "value": susp_win, "direction": "persistent"},
]
elif cat == "VOLUME_ONLY_SPIKE":
inc_state = "NORMAL"
sev = "LOW"
score = round(random.uniform(0.10, 0.25), 4)
spike_p = round(random.uniform(0.05, 0.20), 4)
fe_ratio = round(random.uniform(0.8, 1.2), 2)
vel_ratio = round(random.uniform(3.5, 6.0), 2)
susp_win = 0
camp = (i % 2 == 0)
action = "Normal promotional volume surge. Maintain standard processing."
signals = [{"name": "velocity_ratio", "value": vel_ratio, "direction": "normal" if camp else "elevated"}]
elif cat == "AMOUNT_SHIFT":
inc_state = "NORMAL"
sev = "LOW"
score = round(random.uniform(0.12, 0.28), 4)
spike_p = round(random.uniform(0.05, 0.22), 4)
fe_ratio = round(random.uniform(0.9, 1.3), 2)
vel_ratio = round(random.uniform(1.0, 1.5), 2)
susp_win = 0
camp = False
action = "Bulk order shift observed. No fraud excess detected."
signals = [{"name": "amount_deviation", "value": round(random.uniform(3.0, 7.0), 2), "direction": "elevated"}]
elif cat == "FRAUD_DURING_CAMPAIGN":
inc_state = "ALERT"
sev = "HIGH"
score = round(random.uniform(0.70, 0.94), 4)
spike_p = round(random.uniform(0.45, 0.88), 4)
fe_ratio = round(random.uniform(3.0, 9.0), 2)
vel_ratio = round(random.uniform(4.0, 6.5), 2)
susp_win = random.randint(2, 4)
camp = True
action = "Flash sale active with elevated fraud excess. Enforce step-up verification."
signals = [
{"name": "fraud_excess_ratio", "value": fe_ratio, "direction": "elevated"},
{"name": "velocity_ratio", "value": vel_ratio, "direction": "suppressed"},
{"name": "consecutive_suspicious_windows", "value": susp_win, "direction": "persistent"},
]
else: # CAMPAIGN_WITHOUT_FRAUD
inc_state = "NORMAL"
sev = "LOW"
score = round(random.uniform(0.08, 0.22), 4)
spike_p = round(random.uniform(0.04, 0.18), 4)
fe_ratio = round(random.uniform(0.8, 1.1), 2)
vel_ratio = round(random.uniform(4.0, 6.0), 2)
susp_win = 0
camp = True
action = "Active flash sale with normal fraud excess. Maintain standard processing."
signals = [{"name": "velocity_ratio", "value": vel_ratio, "direction": "suppressed"}]
inp = ExplanationInput(
merchant_id=m_id,
incident_state=inc_state,
severity=sev,
incident_score=score,
spike_probability=spike_p,
fraud_excess_ratio=fe_ratio,
velocity_ratio=vel_ratio,
suspicious_windows=susp_win,
total_suspicious_windows=susp_win,
campaign_active=camp,
policy_mode="BALANCED",
signals=signals,
recommended_action=action,
)
gold = GoldExpectation(
expected_incident_state=inc_state,
expected_severity=sev,
required_numeric_values=["fraud_excess_ratio", "velocity_ratio"],
required_signals=[s["name"] for s in signals],
campaign_status=camp,
allowed_actions=["monitor", "review", "verification", "processing", "maintain"],
forbidden_claims=["$50,000", "IP address", "phishing", "confirmed fraud"],
)
dataset.append({
"example_id": len(dataset) + 1,
"category": cat,
"input": inp.model_dump(),
"gold": gold.model_dump(),
})
if len(dataset) >= num_examples:
break
if len(dataset) >= num_examples:
break
with dataset_path.open("w", encoding="utf-8") as f:
for item in dataset:
f.write(json.dumps(item) + "\n")
LOGGER.info("Generated %d benchmark dataset examples to %s", len(dataset), dataset_path)
return dataset
def evaluate_candidate_model(
model_name: str,
dataset: list[dict[str, Any]],
device_str: str = "cuda" if torch.cuda.is_available() else "cpu",
) -> dict[str, Any]:
"""Evaluates a candidate SLM across all dataset examples."""
LOGGER.info("--- Benchmarking Candidate SLM: %s ---", model_name)
loader = SLMModelLoader(model_name=model_name, device=device_str, max_new_tokens=160, temperature=0.1)
t_load_start = time.perf_counter()
load_success = loader.load_model()
t_load_sec = round(time.perf_counter() - t_load_start, 2)
if not load_success:
return {
"model": model_name,
"device": device_str,
"load_success": False,
"model_load_time_sec": t_load_sec,
"overall_score": 0.0,
"note": "Model failed to load",
}
explainer = RazorShieldExplainer(model_loader=loader)
json_valid_count = 0
schema_valid_count = 0
numeric_grounded_count = 0
dec_consistent_count = 0
sev_consistent_count = 0
camp_consistent_count = 0
signal_cov_count = 0
hallucination_free_count = 0
word_counts = []
latencies_ms = []
model_outputs = []
for item in dataset:
inp_data = ExplanationInput(**item["input"])
gold = GoldExpectation(**item["gold"])
t_start = time.perf_counter()
out, val_res = explainer.generate_explanation(inp_data, expectation=gold)
t_ms = (time.perf_counter() - t_start) * 1000.0
latencies_ms.append(t_ms)
word_counts.append(val_res["word_count"])
if not val_res.get("used_fallback", True):
json_valid_count += 1
schema_valid_count += 1
if val_res["numeric_grounded"]:
numeric_grounded_count += 1
if val_res["decision_consistent"]:
dec_consistent_count += 1
if val_res["severity_consistent"]:
sev_consistent_count += 1
if val_res["campaign_consistent"]:
camp_consistent_count += 1
if not val_res["hallucination_detected"]:
hallucination_free_count += 1
# Signal coverage check
signals_in_text = 0
text_lower = f"{out.summary} {' '.join(out.key_signals)}".lower()
for s_name in gold.required_signals:
if s_name.lower().replace("_", " ") in text_lower or s_name.lower() in text_lower:
signals_in_text += 1
if not gold.required_signals or signals_in_text >= max(1, len(gold.required_signals) // 2):
signal_cov_count += 1
model_outputs.append({
"example_id": item["example_id"],
"input": inp_data.model_dump(),
"output": out.model_dump(),
"validation": val_res,
})
n_total = len(dataset)
json_validity = round(json_valid_count / n_total, 4)
schema_validity = round(schema_valid_count / n_total, 4)
numeric_grounding = round(numeric_grounded_count / n_total, 4)
decision_consistency = round(dec_consistent_count / n_total, 4)
severity_consistency = round(sev_consistent_count / n_total, 4)
campaign_consistency = round(camp_consistent_count / n_total, 4)
signal_coverage = round(signal_cov_count / n_total, 4)
hallucination_rate = round(1.0 - (hallucination_free_count / n_total), 4)
avg_words = round(float(np.mean(word_counts)), 1)
avg_lat = round(float(np.mean(latencies_ms)), 2)
p50_lat = round(float(np.median(latencies_ms)), 2)
p95_lat = round(float(np.percentile(latencies_ms, 95)), 2)
p99_lat = round(float(np.percentile(latencies_ms, 99)), 2)
# Formula specified in prompt:
# quality_score = 0.25*json_validity + 0.20*numeric_grounding + 0.20*decision_consistency + 0.10*severity_consistency + 0.10*campaign_consistency + 0.10*signal_coverage + 0.05*(1 - hallucination_rate)
quality_score = (
(0.25 * json_validity)
+ (0.20 * numeric_grounding)
+ (0.20 * decision_consistency)
+ (0.10 * severity_consistency)
+ (0.10 * campaign_consistency)
+ (0.10 * signal_coverage)
+ (0.05 * (1.0 - hallucination_rate))
)
mem_usage_mb = round(torch.cuda.memory_allocated() / (1024 * 1024), 2) if torch.cuda.is_available() else 0.0
res = {
"model": model_name,
"load_success": True,
"device": device_str,
"model_load_time_sec": t_load_sec,
"json_validity": json_validity,
"schema_validity": schema_validity,
"numeric_grounding": numeric_grounding,
"decision_consistency": decision_consistency,
"severity_consistency": severity_consistency,
"campaign_consistency": campaign_consistency,
"signal_coverage": signal_coverage,
"hallucination_rate": hallucination_rate,
"avg_words": avg_words,
"avg_latency_ms": avg_lat,
"p50_latency_ms": p50_lat,
"p95_latency_ms": p95_lat,
"p99_latency_ms": p99_lat,
"memory_usage_mb": mem_usage_mb,
"quality_score": round(quality_score, 4),
}
# Save model outputs log
out_dir = DATA_DIR / "model_outputs"
out_dir.mkdir(parents=True, exist_ok=True)
sanitized_name = model_name.replace("/", "_").replace("-", "_")
with (out_dir / f"{sanitized_name}_outputs.json").open("w", encoding="utf-8") as f:
json.dump(model_outputs, f, indent=2)
# Clean memory
del loader
del explainer
if torch.cuda.is_available():
torch.cuda.empty_cache()
return res
def run_slm_benchmark() -> dict[str, Any]:
dataset = generate_benchmark_dataset(num_examples=300)
candidate_models = [
"Qwen/Qwen2.5-0.5B-Instruct",
"Qwen/Qwen2.5-1.5B-Instruct",
"HuggingFaceTB/SmolLM2-1.7B-Instruct",
]
results = []
for model_name in candidate_models:
res = evaluate_candidate_model(model_name, dataset)
results.append(res)
# Save benchmark_results.json and benchmark_results.csv
json_path = DATA_DIR / "benchmark_results.json"
with json_path.open("w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
df_res = pd.DataFrame(results)
csv_path = DATA_DIR / "benchmark_results.csv"
df_res.to_csv(csv_path, index=False)
LOGGER.info("Benchmark complete. Results saved to %s and %s", json_path, csv_path)
return {"benchmark_results": results}
if __name__ == "__main__":
run_slm_benchmark()