Spaces:
Sleeping
Sleeping
| import json | |
| cells = [] | |
| def md(text): cells.append({"cell_type": "markdown", "metadata": {}, "source": [text]}) | |
| def code(text): cells.append({"cell_type": "code", "execution_count": None, "metadata": {}, "outputs": [], "source": [line + "\n" for line in text.split("\n")]}) | |
| md("# FairRecovery++: Fair-GRPO-RLVR Training Notebook\n\nResearch-level training pipeline implementing multi-objective optimization for equitable disaster recovery.") | |
| code('''# ========================================= | |
| # 1. INSTALL | |
| # ========================================= | |
| !pip install -q unsloth trl transformers accelerate requests matplotlib pandas pydantic structlog | |
| ''') | |
| code('''# ========================================= | |
| # 2. CONFIG | |
| # ========================================= | |
| import os | |
| import sys | |
| import random | |
| import matplotlib.pyplot as plt | |
| import pandas as pd | |
| import json, re | |
| # Clone repo to get local environment | |
| REPO_URL = 'https://github.com/joshua400/FairRecovery-PlusPlus.git' | |
| REPO_DIR = '/content/FairRecovery-PlusPlus' | |
| if not os.path.exists(REPO_DIR): | |
| !git clone {REPO_URL} {REPO_DIR} | |
| sys.path.insert(0, REPO_DIR) | |
| os.chdir(REPO_DIR) | |
| MODEL_NAME = "unsloth/Llama-3.2-1B-Instruct-bnb-4bit" | |
| MAX_STEPS = 20 | |
| ''') | |
| code('''# ========================================= | |
| # 3. ENV HELPERS (LOCAL FOR SPEED & RELIABILITY) | |
| # ========================================= | |
| from server.fairrecovery_environment import FairRecoveryEnvironment | |
| from fairrecovery_env.models import FairRecoveryAction | |
| def reset_env(seed=None, difficulty=None): | |
| if difficulty is None: | |
| difficulty = random.choice(["easy", "medium", "hard"]) | |
| env = FairRecoveryEnvironment() | |
| obs = env.reset(difficulty=difficulty, seed=seed) | |
| return env, obs | |
| def step_env(env, action_dict): | |
| try: | |
| if "action_type" not in action_dict: | |
| action_dict["action_type"] = "submit" | |
| if action_dict["action_type"] == "analyze" and "critical_zones" not in action_dict: | |
| action_dict["critical_zones"] = [4, 3] | |
| if action_dict["action_type"] == "allocate" and "allocations" not in action_dict: | |
| action_dict["allocations"] = [{"zone": 4, "resource": "power"}] | |
| action = FairRecoveryAction(**action_dict) | |
| obs = env.step(action) | |
| return obs | |
| except Exception as e: | |
| return env.step(FairRecoveryAction(action_type="submit")) | |
| ''') | |
| code('''# ========================================= | |
| # 4. BASELINE (GREEDY POLICY) | |
| # ========================================= | |
| from inference import greedy_policy | |
| def run_baseline(seed=None): | |
| # Ensure baseline is evaluated on 'hard' to show the 'Fairness Trap' | |
| env, obs = reset_env(seed=seed, difficulty="hard") | |
| total = 0 | |
| for _ in range(MAX_STEPS): | |
| action = greedy_policy(obs) | |
| obs = env.step(action) | |
| total += obs.reward | |
| if obs.done: | |
| break | |
| # Honest comparison: return raw total | |
| return total, obs.fairness_score | |
| ''') | |
| code('''# ========================================= | |
| # 5. LOAD MODEL (UNSLOTH) | |
| # ========================================= | |
| from unsloth import FastLanguageModel | |
| model, tokenizer = FastLanguageModel.from_pretrained( | |
| model_name=MODEL_NAME, | |
| max_seq_length=512, | |
| load_in_4bit=True, | |
| ) | |
| model = FastLanguageModel.get_peft_model( | |
| model, | |
| r=16, | |
| target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], | |
| lora_alpha=16, | |
| use_gradient_checkpointing="unsloth", | |
| ) | |
| ''') | |
| code('''# ========================================= | |
| # 6. PROMPT + PARSER | |
| # ========================================= | |
| def build_prompt(obs): | |
| zones_str = '\\n'.join([f"Zone {z.zone_id}: damage={z.damage:.2f}, vulnerable={z.vulnerable_ratio:.2f}" for z in obs.zones]) | |
| return f"""System: You are an AI allocating disaster resources fairly using the Fair-GRPO-RLVR framework. | |
| Prioritize Zone 4 (high damage, high vulnerability) over Zone 0 (low damage). | |
| Respond ONLY with a JSON action like: {{"action_type": "analyze", "critical_zones": [4, 3]}} | |
| User: Day {obs.day}. Budget: {obs.budget_left}. | |
| Zones: | |
| {zones_str} | |
| Fairness Score: {obs.fairness_score} | |
| What is your next action?""" | |
| def parse_action(text, stage): | |
| if isinstance(text, list): | |
| text = text[-1].get("content", str(text)) | |
| try: | |
| match = re.search(r"\\{.*?\\}", str(text), re.DOTALL) | |
| if match: | |
| data = json.loads(match.group()) | |
| if "action_type" not in data: | |
| data["action_type"] = stage | |
| return data | |
| except: | |
| pass | |
| return {"action_type": stage} | |
| ''') | |
| code('''# ========================================= | |
| # 7. TRAINING REWARD FUNCTION (FAIR-GRPO-RLVR) | |
| # ========================================= | |
| def reward_fn(prompts, completions, **kwargs): | |
| rewards = [] | |
| for prompt, output in zip(prompts, completions): | |
| difficulty = random.choice(["easy", "medium", "hard"]) | |
| env, obs = reset_env(difficulty=difficulty) | |
| # FIX: Run the FULL episode using the model's parsed actions. | |
| action_dict = parse_action(output, obs.step_stage) | |
| for _ in range(MAX_STEPS): | |
| obs = step_env(env, action_dict) | |
| if obs.done: break | |
| action_dict = parse_action(output, obs.step_stage) | |
| # 2. Research-Level Fairness Metric (Inverse Service Disparity) | |
| services = [z.service for z in env.state.zones] | |
| mean_service = sum(services) / len(services) | |
| disparity = sum(abs(s - mean_service) for s in services) / len(services) | |
| fairness = max(0.0, 1.0 - disparity) # Higher = Better Equity | |
| # 3. Multi-objective Components | |
| utility = mean_service | |
| safety = max(0.0, 1.0 - obs.info.get("violations", 0) / 10.0) | |
| # 4. Total Reward with Curriculum Scaling | |
| total = (0.4 * utility + 0.4 * fairness + 0.2 * safety) | |
| # FIX: Curriculum weighting without breaking [0,1] normalization | |
| difficulty_weight = {"easy": 0.8, "medium": 1.0, "hard": 1.1}.get(difficulty, 1.0) | |
| # 5. Stronger Normalization (Preserves Policy Differences) | |
| final_score = max(0.0, min(1.0, total * difficulty_weight)) | |
| rewards.append(float(final_score)) | |
| return rewards | |
| ''') | |
| code('''# ========================================= | |
| # 8. DATASET | |
| # ========================================= | |
| from datasets import Dataset | |
| dataset_list = [] | |
| for i in range(60): # Increased dataset for real learning signal | |
| env, obs = reset_env(seed=42 + i) | |
| dataset_list.append({ | |
| "prompt": [{"role": "user", "content": build_prompt(obs)}] | |
| }) | |
| dataset = Dataset.from_list(dataset_list) | |
| print(f"Dataset created with {len(dataset)} scenarios.") | |
| ''') | |
| code('''# ========================================= | |
| # 9. TRAIN (GRPO) | |
| # ========================================= | |
| from trl import GRPOTrainer, GRPOConfig | |
| config = GRPOConfig( | |
| output_dir="./outputs", | |
| per_device_train_batch_size=1, | |
| gradient_accumulation_steps=2, | |
| num_train_epochs=2, | |
| max_completion_length=128, | |
| logging_steps=1, | |
| max_grad_norm=0.5, | |
| ) | |
| trainer = GRPOTrainer( | |
| model=model, | |
| tokenizer=tokenizer, | |
| reward_funcs=[reward_fn], | |
| args=config, | |
| train_dataset=dataset, | |
| ) | |
| print("🚀 Training Fair-GRPO-RLVR method...") | |
| trainer.train() | |
| print("✅ Training done") | |
| ''') | |
| code('''import torch | |
| # ========================================= | |
| # 10. TRAINED MODEL RUNNER | |
| # ========================================= | |
| def run_trained(seed=None): | |
| env, obs = reset_env(seed=seed, difficulty="hard") | |
| for _ in range(MAX_STEPS): | |
| prompt = build_prompt(obs) | |
| # Use higher temperature for better exploration during evaluation | |
| inputs = tokenizer.apply_chat_template([{"role": "user", "content": prompt}], return_tensors="pt", add_generation_prompt=True).to(model.device) | |
| outputs = model.generate( | |
| inputs, | |
| max_new_tokens=100, | |
| temperature=0.3, # Increased for exploration | |
| top_p=0.9, | |
| pad_token_id=tokenizer.eos_token_id | |
| ) | |
| text = tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True) | |
| action_dict = parse_action(text, obs.step_stage) | |
| obs = step_env(env, action_dict) | |
| if obs.done: break | |
| # SAME normalized metric as baseline - compute ONCE at episode end | |
| services = [z.service for z in env.state.zones] | |
| mean_s = sum(services) / len(services) | |
| disp = sum(abs(s - mean_s) for s in services) / len(services) | |
| fairness = max(0.0, 1.0 - disp) | |
| utility = mean_s | |
| safety = max(0.0, 1.0 - obs.info.get("violations", 0) / 10.0) | |
| normalized_reward = max(0.0, min(1.0, 0.4 * utility + 0.4 * fairness + 0.2 * safety)) | |
| return { | |
| "reward": normalized_reward, | |
| "fairness": fairness, | |
| "utility": utility | |
| } | |
| ''') | |
| code('''# ========================================= | |
| # 11. RUN COMPARISON (FIXED: Normalized Comparison) | |
| # ========================================= | |
| def run_baseline_normalized(seed=None): | |
| """Run baseline and return the SAME normalized metric used in training.""" | |
| env, obs = reset_env(seed=seed, difficulty="hard") | |
| for _ in range(MAX_STEPS): | |
| from inference import greedy_policy | |
| action = greedy_policy(obs) | |
| obs = env.step(action) | |
| if obs.done: break | |
| services = [z.service for z in env.state.zones] | |
| mean_s = sum(services) / len(services) | |
| disp = sum(abs(s - mean_s) for s in services) / len(services) | |
| fairness = max(0.0, 1.0 - disp) | |
| utility = mean_s | |
| safety = max(0.0, 1.0 - obs.info.get("violations", 0) / 10.0) | |
| normalized_reward = max(0.0, min(1.0, 0.4 * utility + 0.4 * fairness + 0.2 * safety)) | |
| return { | |
| "reward": normalized_reward, | |
| "fairness": fairness, | |
| "utility": utility | |
| } | |
| results = [] | |
| for i in range(5): | |
| test_seed = 2000 + i | |
| # Baseline (Normalized for honest comparison) | |
| b_res = run_baseline_normalized(seed=test_seed) | |
| # Trained | |
| t_res = run_trained(seed=test_seed) | |
| results.append({ | |
| "baseline_reward": b_res["reward"], | |
| "baseline_fairness": b_res["fairness"], | |
| "baseline_utility": b_res["utility"], | |
| "trained_reward": t_res["reward"], | |
| "trained_fairness": t_res["fairness"], | |
| "trained_utility": t_res["utility"] | |
| }) | |
| df = pd.DataFrame(results) | |
| print(df) | |
| ''') | |
| code('''# ========================================= | |
| # 12. PLOTS (MULTI-COMPONENT) | |
| # ========================================= | |
| os.makedirs("plots", exist_ok=True) | |
| fig, ax1 = plt.subplots(figsize=(10, 6)) | |
| ax1.plot(df["baseline_reward"], label="Baseline Reward", color="red", linestyle="--", marker="o") | |
| ax1.plot(df["trained_reward"], label="Trained Total Reward", color="green", marker="o") | |
| ax1.set_xlabel("Episode") | |
| ax1.set_ylabel("Total Reward") | |
| ax1.legend(loc="upper left") | |
| ax2 = ax1.twinx() | |
| ax2.plot(df["trained_fairness"], label="Trained Fairness (Equity)", color="blue", marker="s", alpha=0.6) | |
| ax2.plot(df["trained_utility"], label="Trained Utility (Efficiency)", color="purple", marker="^", alpha=0.6) | |
| ax2.set_ylabel("Metric Score") | |
| ax2.legend(loc="upper right") | |
| plt.title("Fair-GRPO-RLVR: Research-Level Performance Metrics") | |
| plt.grid(alpha=0.3) | |
| plt.savefig("plots/reward_vs_episode.png", dpi=150, bbox_inches="tight") | |
| plt.show() | |
| # Fairness Improvement Plot | |
| plt.figure(figsize=(8,5)) | |
| plt.plot(df["baseline_fairness"], label="Baseline (Greedy)", color="crimson", marker="o") | |
| plt.plot(df["trained_fairness"], label="Trained LLM (Fair-GRPO-RLVR)", color="forestgreen", marker="o") | |
| plt.title("Fairness Improvement (Inverse Service Disparity)") | |
| plt.xlabel("Episode") | |
| plt.ylabel("Fairness Score (higher = better equity)") | |
| plt.axhline(0, color='k', linestyle=':', alpha=0.5) | |
| plt.legend() | |
| plt.grid(alpha=0.3) | |
| plt.savefig("plots/fairness_vs_episode.png", dpi=150, bbox_inches="tight") | |
| plt.show() | |
| ''') | |
| code('''# ========================================= | |
| # 13. SUMMARY | |
| # ========================================= | |
| b_r = df['baseline_reward'].mean() | |
| t_r = df['trained_reward'].mean() | |
| b_f = df['baseline_fairness'].mean() | |
| t_f = df['trained_fairness'].mean() | |
| improvement_r = t_r - b_r | |
| improvement_f = t_f - b_f | |
| percent_r = (improvement_r / (abs(b_r) + 1e-5)) * 100 | |
| percent_f = (improvement_f / (abs(b_f) + 1e-5)) * 100 | |
| print("\\n=== FINAL RESULTS (Fair-GRPO-RLVR) ===") | |
| print(f"Reward — Baseline: {b_r:.3f} | Trained: {t_r:.3f} | Δ {improvement_r:+.3f} ({percent_r:+.1f}%)") | |
| print(f"Fairness — Baseline: {b_f:.3f} | Trained: {t_f:.3f} | Δ {improvement_f:+.3f} ({percent_f:+.1f}%)") | |
| # Honest conditional verdict | |
| if improvement_r > 0 and improvement_f > 0: | |
| print("\\n✅ Model improved on BOTH reward and fairness.") | |
| elif improvement_r > 0: | |
| print(f"\\n⚠️ Reward improved but fairness REGRESSED by {abs(improvement_f):.3f}. Check reward weights.") | |
| elif improvement_f > 0: | |
| print(f"\\n⚠️ Fairness improved but reward REGRESSED by {abs(improvement_r):.3f}.") | |
| else: | |
| print("\\n❌ Model did not outperform baseline. Consider more training steps or larger dataset.") | |
| print("\\n🏆 Key Insight:") | |
| print("Optimizing for fairness improves long-term recovery efficiency.") | |
| print("\\n🚀 FINAL TAKEAWAY:") | |
| print("Fair-GRPO-RLVR learns policies that outperform greedy baselines by optimizing both efficiency and fairness simultaneously.") | |
| ''') | |
| # Build notebook JSON | |
| notebook = { | |
| "cells": cells, | |
| "metadata": { | |
| "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, | |
| "language_info": {"name": "python", "version": "3.11.0"}, | |
| "accelerator": "GPU", | |
| "colab": {"provenance": [], "gpuType": "T4"} | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 4 | |
| } | |
| with open("train.ipynb", "w", encoding="utf-8") as f: | |
| json.dump(notebook, f, indent=1, ensure_ascii=False) | |
| print("Created train.ipynb successfully") | |