crime / eval_baseline.py
printf-sourav's picture
Upload core project files (environment, server, dashboard, training)
1704969 verified
Raw
History Blame Contribute Delete
13.5 kB
#!/usr/bin/env python3
"""Β§19 Demo evaluation: baseline vs trained model comparison.
Loads the base model (no training) and a checkpoint, runs NUM_EVAL_EPISODES
identical fixed episodes through both, then outputs an accuracy/reward
comparison table and saves full transcripts to episode_comparison.json.
Usage:
# Auto-detects latest checkpoint_ep* directory
python eval_baseline.py
# Or point at a specific checkpoint
CHECKPOINT_PATH=./checkpoint_ep300 python eval_baseline.py
# Adjust episode count
NUM_EVAL_EPISODES=20 python eval_baseline.py
"""
import copy
import json
import os
import sys
import random
import numpy as np
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
pipeline,
)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from crime_env.environment import CrimeInvestigationEnv
from crime_env.case_generator import generate_case
# ── Configuration ──────────────────────────────────────────────────────────
MODEL_NAME = os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-1.5B-Instruct")
NPC_MODEL_NAME = os.environ.get("NPC_MODEL_NAME", "Qwen/Qwen2.5-0.5B-Instruct")
CHECKPOINT_PATH = os.environ.get("CHECKPOINT_PATH", "")
NUM_EVAL_EPISODES = int(os.environ.get("NUM_EVAL_EPISODES", "10"))
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
SEED = 42
OUTPUT_FILE = "episode_comparison.json"
# ── Helpers ─────────────────────────────────────────────────────────────────
def _quant_config():
if DEVICE != "cuda":
return None
try:
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
)
except Exception:
return None
def _load_base_model():
"""Load the original pretrained model without any LoRA / fine-tuning."""
print(f" Loading base model: {MODEL_NAME}")
qc = _quant_config()
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=qc,
torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32,
device_map="auto" if DEVICE == "cuda" else None,
trust_remote_code=True,
)
model.eval()
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
return model, tokenizer
def _load_checkpoint_model(checkpoint_path: str):
"""Load a PEFT-trained checkpoint. Falls back to base model if not found."""
if not checkpoint_path or not os.path.isdir(checkpoint_path):
print(f" Checkpoint not found at '{checkpoint_path}'. Using base model as fallback.")
return _load_base_model()
print(f" Loading checkpoint: {checkpoint_path}")
try:
from peft import PeftModel
base, tokenizer = _load_base_model()
model = PeftModel.from_pretrained(base, checkpoint_path)
model.eval()
return model, tokenizer
except Exception as e:
print(f" PEFT load failed ({e}). Trying plain HF load.")
qc = _quant_config()
model = AutoModelForCausalLM.from_pretrained(
checkpoint_path,
quantization_config=qc,
torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32,
device_map="auto" if DEVICE == "cuda" else None,
trust_remote_code=True,
)
model.eval()
tokenizer = AutoTokenizer.from_pretrained(checkpoint_path, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
return model, tokenizer
def _load_npc_pipeline():
qc = _quant_config()
tok = AutoTokenizer.from_pretrained(NPC_MODEL_NAME, trust_remote_code=True)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
npc = AutoModelForCausalLM.from_pretrained(
NPC_MODEL_NAME,
quantization_config=qc,
torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32,
device_map="auto" if DEVICE == "cuda" else None,
trust_remote_code=True,
)
npc.eval()
return pipeline(
"text-generation", model=npc, tokenizer=tok,
max_new_tokens=80, do_sample=True, temperature=0.7, top_p=0.9,
)
def _make_npc_call(npc_pipe):
tok = npc_pipe.tokenizer
def llm_call(system_prompt: str, conversation_history: list) -> str:
user_prompt = "Conversation so far:\n"
for entry in conversation_history[-8:]:
user_prompt += f"{entry.get('speaker', '')}: {entry.get('content', '')[:180]}\n"
messages = [
{"role": "system", "content": system_prompt[:800]},
{"role": "user", "content": user_prompt[-1800:] + "\nRespond in 1-2 sentences."},
]
if hasattr(tok, "apply_chat_template"):
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
else:
prompt = f"System: {messages[0]['content']}\n\nUser: {messages[1]['content']}\n\nAssistant:"
try:
out = npc_pipe(prompt, return_full_text=False)
resp = out[0]["generated_text"].strip()
return resp.split("\n")[0][:300] if resp else "I have nothing to add."
except Exception:
return "I don't recall anything specific about that."
return llm_call
def _generate_action(model, tokenizer, obs: dict, max_turns: int) -> str:
"""Greedy-decode one action (no sampling variance for fair comparison)."""
history = obs.get("conversation_history", [])
prompt = (
f"You are a detective investigating a crime.\n"
f"Briefing: {obs['briefing'][:300]}\n"
f"Turn: {obs['turn']}/{max_turns}\nRecent conversation:\n"
)
for entry in history[-6:]:
prompt += f" {entry['speaker']}: {entry['content'][:100]}\n"
prompt += (
"\nChoose ONE action:\n"
"ACTION: ask_question | TARGET: Suspect_A | CONTENT: <question>\n"
"ACTION: request_evidence | ITEM: keycard_log\n"
"ACTION: accuse | TARGET: Suspect_A\n\nYour action:"
)
if hasattr(tokenizer, "apply_chat_template"):
messages = [
{"role": "system", "content": "You are a detective. Choose your next action."},
{"role": "user", "content": prompt},
]
prompt_text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
else:
prompt_text = f"Detective: {prompt}\n\nAction:"
inputs = tokenizer(prompt_text, return_tensors="pt", truncation=True, max_length=1024)
if DEVICE == "cuda":
inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
with torch.no_grad():
out = model.generate(
**inputs,
max_new_tokens=56,
do_sample=False, # greedy β€” reproducible across both runs
pad_token_id=tokenizer.pad_token_id,
)
response = tokenizer.decode(
out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
).strip()
if not response.upper().startswith("ACTION:"):
turn = obs.get("turn", 0)
force_at = max(1, int(max_turns * 0.7))
if turn >= force_at:
return "ACTION: accuse | TARGET: Suspect_A"
return "ACTION: ask_question | TARGET: Suspect_A | CONTENT: Where were you at the time of the crime?"
return response
# ── Episode runner ──────────────────────────────────────────────────────────
def run_episodes(
model,
tokenizer,
npc_call,
fixed_cases: list[dict],
label: str,
) -> list[dict]:
"""Run fixed_cases through the environment with the given model.
Returns a list of per-episode result dicts.
"""
env = CrimeInvestigationEnv(llm_call=npc_call)
results = []
for i, case in enumerate(fixed_cases):
max_turns = case.get("max_turns", 15)
env.MAX_TURNS = max_turns
obs = env.reset(case_data=copy.deepcopy(case))
done = False
turn_count = 0
while not done:
action = _generate_action(model, tokenizer, obs, max_turns=max_turns)
obs, _, done, info = env.step(action)
turn_count += 1
final_r = env.reward_calc.get_rewards()
result = "correct" if info.get("action") == "accuse" and info.get("correct") \
else ("wrong" if info.get("action") == "accuse" else "timeout")
row = {
"episode": i + 1,
"criminal": case["criminal"],
"result": result,
"correct": result == "correct",
"detective_reward": round(float(final_r.get("detective", 0.0)), 4),
"turns": turn_count,
"conversation_history": list(env.conversation_history),
}
results.append(row)
print(
f" Ep {i+1:>2}: {result:<7} | "
f"reward={row['detective_reward']:>+6.2f} | turns={turn_count}"
)
return results
# ── Main ────────────────────────────────────────────────────────────────────
def main():
print("=" * 60)
print(" AI Crime Investigation β€” Baseline vs Trained Evaluation")
print("=" * 60)
# Auto-detect latest checkpoint if not specified
checkpoint_path = CHECKPOINT_PATH
if not checkpoint_path:
candidates = sorted(
[d for d in os.listdir(".") if d.startswith("checkpoint_ep") and os.path.isdir(d)],
key=lambda x: int(x.replace("checkpoint_ep", "") or "0"),
)
checkpoint_path = candidates[-1] if candidates else ""
print(f"Checkpoint : {checkpoint_path or '(none β€” will clone base for both runs)'}")
print(f"Episodes : {NUM_EVAL_EPISODES}")
# Reproducible fixed cases β€” same seed for both conditions
random.seed(SEED)
np.random.seed(SEED)
fixed_cases = [generate_case(difficulty="hard") for _ in range(NUM_EVAL_EPISODES)]
print("\nLoading NPC model ...")
npc_pipe = _load_npc_pipeline()
npc_call = _make_npc_call(npc_pipe)
# ── Baseline run ───────────────────────────────────────────────────
print("\n[1/2] BASE MODEL (untrained) ...")
base_model, base_tok = _load_base_model()
base_results = run_episodes(base_model, base_tok, npc_call, fixed_cases, label="base")
del base_model
if DEVICE == "cuda":
torch.cuda.empty_cache()
# ── Trained run ────────────────────────────────────────────────────
print(f"\n[2/2] TRAINED MODEL ({checkpoint_path or 'base fallback'}) ...")
trained_model, trained_tok = _load_checkpoint_model(checkpoint_path)
trained_results = run_episodes(trained_model, trained_tok, npc_call, fixed_cases, label="trained")
del trained_model
if DEVICE == "cuda":
torch.cuda.empty_cache()
# ── Comparison table ───────────────────────────────────────────────
def _stats(rows):
acc = sum(r["correct"] for r in rows) / len(rows)
mean_r = float(np.mean([r["detective_reward"] for r in rows]))
avg_turns = float(np.mean([r["turns"] for r in rows]))
return acc, mean_r, avg_turns
b_acc, b_r, b_t = _stats(base_results)
t_acc, t_r, t_t = _stats(trained_results)
print("\n" + "=" * 62)
print(" EVALUATION RESULTS")
print("=" * 62)
print(f" {'Metric':<32} {'Base':>9} {'Trained':>9} {'Ξ”':>9}")
print(f" {'-'*62}")
print(f" {'Accuracy (%)':<32} {b_acc*100:>9.1f} {t_acc*100:>9.1f} {(t_acc-b_acc)*100:>+9.1f}")
print(f" {'Mean detective reward':<32} {b_r:>9.3f} {t_r:>9.3f} {t_r-b_r:>+9.3f}")
print(f" {'Avg turns to accuse':<32} {b_t:>9.1f} {t_t:>9.1f} {t_t-b_t:>+9.1f}")
print("=" * 62)
# ── Save full results ──────────────────────────────────────────────
output = {
"config": {
"model": MODEL_NAME,
"checkpoint": checkpoint_path or "base",
"num_episodes": NUM_EVAL_EPISODES,
"seed": SEED,
},
"summary": {
"base": {"accuracy": b_acc, "mean_reward": b_r, "avg_turns": b_t},
"trained": {"accuracy": t_acc, "mean_reward": t_r, "avg_turns": t_t},
},
"base_episodes": base_results,
"trained_episodes": trained_results,
}
with open(OUTPUT_FILE, "w") as f:
json.dump(output, f, indent=2)
print(f"\nFull transcripts saved β†’ {OUTPUT_FILE}")
if __name__ == "__main__":
main()