Spaces:
Sleeping
Sleeping
Commit ·
5f54992
1
Parent(s): ef79a5f
feat: baseline eval and GRPO training script
Browse files- eval/baseline_eval.py +61 -0
- eval/baseline_results.json +108 -0
- training/train_grpo.py +97 -0
eval/baseline_eval.py
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from environment.env import ContextCorruptionEnv
|
| 6 |
+
from environment.actions import ContextCorruptionAction, ActionType
|
| 7 |
+
|
| 8 |
+
NUM_EPISODES = 100
|
| 9 |
+
RESULTS_PATH = Path(__file__).parent / "baseline_results.json"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def run_baseline():
|
| 13 |
+
env = ContextCorruptionEnv()
|
| 14 |
+
rewards = []
|
| 15 |
+
|
| 16 |
+
for ep in range(NUM_EPISODES):
|
| 17 |
+
obs = env.reset()
|
| 18 |
+
done = False
|
| 19 |
+
|
| 20 |
+
while not done:
|
| 21 |
+
# Randomly flag 0-4 docs
|
| 22 |
+
if random.random() < 0.4 and obs.budget_remaining > 1:
|
| 23 |
+
doc_id = random.randint(0, len(obs.documents) - 1)
|
| 24 |
+
action = ContextCorruptionAction(
|
| 25 |
+
action_type=ActionType.flag_suspicious,
|
| 26 |
+
doc_id=doc_id,
|
| 27 |
+
)
|
| 28 |
+
else:
|
| 29 |
+
action = ContextCorruptionAction(
|
| 30 |
+
action_type=ActionType.submit_answer,
|
| 31 |
+
answer="unknown",
|
| 32 |
+
confidence=0.5,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
obs = env.step(action)
|
| 36 |
+
done = obs.episode_done
|
| 37 |
+
|
| 38 |
+
rewards.append(obs.reward)
|
| 39 |
+
if (ep + 1) % 10 == 0:
|
| 40 |
+
print(f"Episode {ep + 1}/{NUM_EPISODES} | reward: {obs.reward:.4f} | avg so far: {sum(rewards)/len(rewards):.4f}")
|
| 41 |
+
|
| 42 |
+
avg = round(sum(rewards) / len(rewards), 4)
|
| 43 |
+
minimum = round(min(rewards), 4)
|
| 44 |
+
maximum = round(max(rewards), 4)
|
| 45 |
+
|
| 46 |
+
results = {
|
| 47 |
+
"num_episodes": NUM_EPISODES,
|
| 48 |
+
"avg_reward": avg,
|
| 49 |
+
"min_reward": minimum,
|
| 50 |
+
"max_reward": maximum,
|
| 51 |
+
"all_rewards": rewards,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
RESULTS_PATH.write_text(json.dumps(results, indent=2))
|
| 55 |
+
print(f"\nBaseline results — avg: {avg} | min: {minimum} | max: {maximum}")
|
| 56 |
+
print(f"Saved to {RESULTS_PATH}")
|
| 57 |
+
return results
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
run_baseline()
|
eval/baseline_results.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"num_episodes": 100,
|
| 3 |
+
"avg_reward": 0.1314,
|
| 4 |
+
"min_reward": -0.0792,
|
| 5 |
+
"max_reward": 0.3292,
|
| 6 |
+
"all_rewards": [
|
| 7 |
+
0.1458,
|
| 8 |
+
0.1458,
|
| 9 |
+
0.1458,
|
| 10 |
+
0.1458,
|
| 11 |
+
0.1458,
|
| 12 |
+
0.0417,
|
| 13 |
+
0.0417,
|
| 14 |
+
0.1875,
|
| 15 |
+
0.1458,
|
| 16 |
+
0.2917,
|
| 17 |
+
0.1458,
|
| 18 |
+
0.1458,
|
| 19 |
+
0.0417,
|
| 20 |
+
0.1458,
|
| 21 |
+
0.1458,
|
| 22 |
+
0.1458,
|
| 23 |
+
0.1458,
|
| 24 |
+
0.0417,
|
| 25 |
+
0.0417,
|
| 26 |
+
-0.0792,
|
| 27 |
+
0.1458,
|
| 28 |
+
0.1458,
|
| 29 |
+
0.0417,
|
| 30 |
+
0.1458,
|
| 31 |
+
0.1458,
|
| 32 |
+
0.1458,
|
| 33 |
+
0.1458,
|
| 34 |
+
0.2917,
|
| 35 |
+
0.1458,
|
| 36 |
+
0.0417,
|
| 37 |
+
0.1458,
|
| 38 |
+
0.2917,
|
| 39 |
+
0.1458,
|
| 40 |
+
0.1458,
|
| 41 |
+
0.0417,
|
| 42 |
+
0.1458,
|
| 43 |
+
0.1458,
|
| 44 |
+
0.3292,
|
| 45 |
+
0.0417,
|
| 46 |
+
0.0417,
|
| 47 |
+
0.0833,
|
| 48 |
+
0.1458,
|
| 49 |
+
0.1458,
|
| 50 |
+
0.1458,
|
| 51 |
+
0.1458,
|
| 52 |
+
0.0417,
|
| 53 |
+
0.1458,
|
| 54 |
+
0.1125,
|
| 55 |
+
0.0417,
|
| 56 |
+
0.2417,
|
| 57 |
+
0.1458,
|
| 58 |
+
0.1458,
|
| 59 |
+
0.1458,
|
| 60 |
+
0.1458,
|
| 61 |
+
0.2417,
|
| 62 |
+
0.1458,
|
| 63 |
+
0.0417,
|
| 64 |
+
0.1458,
|
| 65 |
+
0.1458,
|
| 66 |
+
0.1458,
|
| 67 |
+
0.1458,
|
| 68 |
+
0.1458,
|
| 69 |
+
0.1458,
|
| 70 |
+
0.1458,
|
| 71 |
+
-0.0667,
|
| 72 |
+
0.1458,
|
| 73 |
+
0.1458,
|
| 74 |
+
0.1125,
|
| 75 |
+
0.0417,
|
| 76 |
+
0.1125,
|
| 77 |
+
0.0417,
|
| 78 |
+
0.1458,
|
| 79 |
+
0.1458,
|
| 80 |
+
0.1458,
|
| 81 |
+
0.1458,
|
| 82 |
+
0.2167,
|
| 83 |
+
0.1458,
|
| 84 |
+
0.1125,
|
| 85 |
+
0.1458,
|
| 86 |
+
0.1458,
|
| 87 |
+
0.1458,
|
| 88 |
+
0.0417,
|
| 89 |
+
0.1292,
|
| 90 |
+
0.1458,
|
| 91 |
+
0.1458,
|
| 92 |
+
0.1458,
|
| 93 |
+
0.1458,
|
| 94 |
+
0.1458,
|
| 95 |
+
0.1458,
|
| 96 |
+
0.2333,
|
| 97 |
+
0.2167,
|
| 98 |
+
0.1458,
|
| 99 |
+
0.0417,
|
| 100 |
+
0.0417,
|
| 101 |
+
0.1458,
|
| 102 |
+
0.1458,
|
| 103 |
+
0.1458,
|
| 104 |
+
0.1458,
|
| 105 |
+
0.1458,
|
| 106 |
+
0.1458
|
| 107 |
+
]
|
| 108 |
+
}
|
training/train_grpo.py
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import wandb
|
| 3 |
+
|
| 4 |
+
# Fill in after HF Space is live
|
| 5 |
+
ENV_URL = os.getenv("ENV_URL", "http://localhost:8000")
|
| 6 |
+
|
| 7 |
+
SYSTEM_PROMPT = """You are an epistemic agent. You will be given a question and a set of documents.
|
| 8 |
+
Your goal is to identify the correct answer and flag any documents that contain corrupted or false information.
|
| 9 |
+
|
| 10 |
+
You have three tools:
|
| 11 |
+
- read_doc(doc_id): Spend 1 budget unit to focus on a specific document (content already visible).
|
| 12 |
+
- flag_suspicious(doc_id): Mark a document as potentially corrupted.
|
| 13 |
+
- submit_answer(answer, confidence): End the episode with your final answer and a confidence score (0.0–1.0).
|
| 14 |
+
|
| 15 |
+
Strategy:
|
| 16 |
+
- Cross-reference claims across documents. Corrupted documents will contradict the majority or your parametric knowledge.
|
| 17 |
+
- When documents conflict, trust your parametric knowledge and flag the outlier.
|
| 18 |
+
- Use your budget wisely — you have 12 actions total. Flag only when confident, as false positives are penalised.
|
| 19 |
+
- Submit as soon as you are confident; unused budget gives a small efficiency bonus."""
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def build_prompt(obs_dict: dict) -> str:
|
| 23 |
+
docs_text = "\n\n".join(
|
| 24 |
+
f"[Doc {d['id']}] {d['title']}\n{d['content']}"
|
| 25 |
+
for d in obs_dict["documents"]
|
| 26 |
+
)
|
| 27 |
+
flagged = obs_dict.get("flagged_ids", [])
|
| 28 |
+
return (
|
| 29 |
+
f"Question: {obs_dict['question']}\n\n"
|
| 30 |
+
f"Documents:\n{docs_text}\n\n"
|
| 31 |
+
f"Flagged so far: {flagged}\n"
|
| 32 |
+
f"Budget remaining: {obs_dict['budget_remaining']}\n"
|
| 33 |
+
f"Turn: {obs_dict['turn']}\n\n"
|
| 34 |
+
"What is your next action? Respond with a JSON action object:\n"
|
| 35 |
+
'{"action_type": "submit_answer", "answer": "...", "confidence": 0.9}\n'
|
| 36 |
+
"or\n"
|
| 37 |
+
'{"action_type": "flag_suspicious", "doc_id": 2}'
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def main():
|
| 42 |
+
from unsloth import FastLanguageModel
|
| 43 |
+
from trl import GRPOTrainer, GRPOConfig
|
| 44 |
+
|
| 45 |
+
wandb.init(project="context-corruption-env", name="qwen-1.5b-grpo-run1")
|
| 46 |
+
|
| 47 |
+
# Load model with Unsloth
|
| 48 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 49 |
+
model_name="unsloth/Qwen2-1.5B-Instruct",
|
| 50 |
+
max_seq_length=2048,
|
| 51 |
+
load_in_4bit=True,
|
| 52 |
+
)
|
| 53 |
+
model = FastLanguageModel.get_peft_model(
|
| 54 |
+
model,
|
| 55 |
+
r=16,
|
| 56 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
|
| 57 |
+
lora_dropout=0.0,
|
| 58 |
+
use_gradient_checkpointing="unsloth",
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Environment factory for GRPO — one env instance per parallel generation
|
| 62 |
+
from environment.env import ContextCorruptionEnv
|
| 63 |
+
|
| 64 |
+
def env_factory():
|
| 65 |
+
return ContextCorruptionEnv()
|
| 66 |
+
|
| 67 |
+
config = GRPOConfig(
|
| 68 |
+
num_train_epochs=3,
|
| 69 |
+
per_device_train_batch_size=4,
|
| 70 |
+
gradient_accumulation_steps=4,
|
| 71 |
+
learning_rate=5e-5,
|
| 72 |
+
max_completion_length=512,
|
| 73 |
+
num_generations=8,
|
| 74 |
+
report_to="wandb",
|
| 75 |
+
logging_steps=10,
|
| 76 |
+
save_steps=50,
|
| 77 |
+
output_dir="checkpoints/grpo-qwen-1.5b",
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
trainer = GRPOTrainer(
|
| 81 |
+
model=model,
|
| 82 |
+
args=config,
|
| 83 |
+
processing_class=tokenizer,
|
| 84 |
+
environment_factory=env_factory,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
trainer.train()
|
| 88 |
+
wandb.finish()
|
| 89 |
+
|
| 90 |
+
# Save model
|
| 91 |
+
model.save_pretrained("checkpoints/grpo-qwen-1.5b-final")
|
| 92 |
+
tokenizer.save_pretrained("checkpoints/grpo-qwen-1.5b-final")
|
| 93 |
+
print("Training complete. Model saved to checkpoints/grpo-qwen-1.5b-final")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
main()
|