Spaces:
Sleeping
Sleeping
Update training/train_agent.py
Browse files- training/train_agent.py +116 -42
training/train_agent.py
CHANGED
|
@@ -126,7 +126,7 @@ def parse_action(text: str) -> dict | None:
|
|
| 126 |
return None
|
| 127 |
|
| 128 |
|
| 129 |
-
|
| 130 |
def compute_reward(action: dict, scenario: dict) -> tuple:
|
| 131 |
"""
|
| 132 |
Compute reward LOCALLY β no HTTP, no shared state, deterministic.
|
|
@@ -142,6 +142,28 @@ def compute_reward(action: dict, scenario: dict) -> tuple:
|
|
| 142 |
if action_type == "create_index":
|
| 143 |
result = sim.apply_action("create_index", payload)
|
| 144 |
delta = result.get("delta", 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
elif action_type == "rewrite_query":
|
| 146 |
result = sim.apply_action("rewrite_query", payload)
|
| 147 |
delta = result.get("delta", 0.0)
|
|
@@ -156,10 +178,12 @@ def compute_reward(action: dict, scenario: dict) -> tuple:
|
|
| 156 |
elif action_type == "submit_report":
|
| 157 |
delta = max(0, sim.get_performance_score() - baseline)
|
| 158 |
|
| 159 |
-
final
|
| 160 |
-
|
|
|
|
|
|
|
| 161 |
max_possible = max(1.0, 100.0 - baseline)
|
| 162 |
-
ratio
|
| 163 |
|
| 164 |
# Step reward
|
| 165 |
step_r = {
|
|
@@ -175,24 +199,28 @@ def compute_reward(action: dict, scenario: dict) -> tuple:
|
|
| 175 |
# Delta reward β key signal
|
| 176 |
delta_r = min(0.65, ratio * 0.65)
|
| 177 |
|
| 178 |
-
# Milestone bonus
|
| 179 |
-
milestone
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
if ratio >= 0.75:
|
| 182 |
-
milestone = 0.40
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
# Wrong index penalty
|
| 192 |
-
wrong_pen = -0.15 if (action_type == "create_index" and delta <= 0.0) else 0.0
|
| 193 |
|
| 194 |
total = max(0.001, min(0.999, step_r + delta_r + milestone + wrong_pen))
|
| 195 |
-
|
|
|
|
| 196 |
|
| 197 |
return total, improvement, milestone, desc
|
| 198 |
|
|
@@ -270,7 +298,14 @@ def build_dataset() -> Dataset:
|
|
| 270 |
|
| 271 |
|
| 272 |
# ββ Generate loss + reward plots ββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
def generate_plots(trainer):
|
|
|
|
| 274 |
import matplotlib
|
| 275 |
matplotlib.use("Agg")
|
| 276 |
import matplotlib.pyplot as plt
|
|
@@ -280,40 +315,77 @@ def generate_plots(trainer):
|
|
| 280 |
print("β οΈ No logs to plot")
|
| 281 |
return
|
| 282 |
|
| 283 |
-
steps = [l.get("step", i)
|
| 284 |
-
losses = [l.get("loss",
|
| 285 |
rewards = [l.get("reward", 0) for l in logs]
|
| 286 |
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
fontsize=13, fontweight="bold")
|
| 290 |
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
ax1.set_xlabel("Training Step")
|
| 293 |
ax1.set_ylabel("Loss")
|
| 294 |
-
ax1.set_title("Training Loss
|
|
|
|
| 295 |
ax1.grid(True, alpha=0.3)
|
|
|
|
|
|
|
| 296 |
if losses:
|
| 297 |
-
ax1.annotate(f"Start: {losses[0]:.
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
ax2.set_xlabel("Training Step")
|
| 304 |
-
ax2.set_ylabel("Reward")
|
| 305 |
-
ax2.set_title("Reward During Training
|
| 306 |
ax2.grid(True, alpha=0.3)
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
plt.savefig("loss_curve.png", dpi=150, bbox_inches="tight")
|
| 310 |
print("β
loss_curve.png saved")
|
| 311 |
if losses:
|
| 312 |
-
print(f" Loss: {losses[0]:.
|
| 313 |
if rewards:
|
| 314 |
valid = [r for r in rewards if r > 0]
|
| 315 |
if valid:
|
| 316 |
-
print(f" Reward: {valid[0]:.
|
| 317 |
|
| 318 |
|
| 319 |
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -369,10 +441,12 @@ def train():
|
|
| 369 |
|
| 370 |
print(f"ποΈ GRPO training β {MAX_STEPS} steps")
|
| 371 |
print("Expected rewards:")
|
| 372 |
-
print(" inspect_query
|
| 373 |
-
print(" create_index (
|
| 374 |
-
print(" create_index (
|
| 375 |
-
print("
|
|
|
|
|
|
|
| 376 |
|
| 377 |
trainer.train()
|
| 378 |
print("\nβ
Training complete!")
|
|
@@ -394,4 +468,4 @@ def train():
|
|
| 394 |
|
| 395 |
|
| 396 |
if __name__ == "__main__":
|
| 397 |
-
train()
|
|
|
|
| 126 |
return None
|
| 127 |
|
| 128 |
|
| 129 |
+
|
| 130 |
def compute_reward(action: dict, scenario: dict) -> tuple:
|
| 131 |
"""
|
| 132 |
Compute reward LOCALLY β no HTTP, no shared state, deterministic.
|
|
|
|
| 142 |
if action_type == "create_index":
|
| 143 |
result = sim.apply_action("create_index", payload)
|
| 144 |
delta = result.get("delta", 0.0)
|
| 145 |
+
|
| 146 |
+
# ββ Hint-match fallback ββββββββββββββββββββββββββββββββββββββββββββ
|
| 147 |
+
# When the simulator returns delta=0 (wrong/partial columns), check
|
| 148 |
+
# the scenario's missing_index_hints and simulate a proportional delta.
|
| 149 |
+
# This gives GRPO a gradient signal early in training before the model
|
| 150 |
+
# has learned the exact right column names.
|
| 151 |
+
if delta <= 0.0:
|
| 152 |
+
hints = scenario.get("missing_index_hints", [])
|
| 153 |
+
p_table = payload.get("table", "")
|
| 154 |
+
p_cols = set(str(c).lower() for c in payload.get("columns", []))
|
| 155 |
+
best_match = 0.0
|
| 156 |
+
for hint in hints:
|
| 157 |
+
h_table = hint.get("table", "")
|
| 158 |
+
h_cols = set(str(c).lower() for c in hint.get("columns", []))
|
| 159 |
+
if not h_cols:
|
| 160 |
+
continue
|
| 161 |
+
table_ok = 1.0 if h_table == p_table else 0.3
|
| 162 |
+
col_overlap = len(p_cols & h_cols) / max(len(h_cols), 1)
|
| 163 |
+
best_match = max(best_match, table_ok * 0.4 + col_overlap * 0.6)
|
| 164 |
+
if best_match > 0:
|
| 165 |
+
max_gap = max(1.0, 100.0 - baseline)
|
| 166 |
+
delta = best_match * max_gap * 0.65 # up to 65% of gap on perfect match β reaches 25% milestone
|
| 167 |
elif action_type == "rewrite_query":
|
| 168 |
result = sim.apply_action("rewrite_query", payload)
|
| 169 |
delta = result.get("delta", 0.0)
|
|
|
|
| 178 |
elif action_type == "submit_report":
|
| 179 |
delta = max(0, sim.get_performance_score() - baseline)
|
| 180 |
|
| 181 |
+
final = sim.get_performance_score()
|
| 182 |
+
sim_improve = max(0.0, final - baseline)
|
| 183 |
+
# Use hint-simulated delta if simulator returned 0 (wrong columns early in training)
|
| 184 |
+
improvement = max(sim_improve, delta)
|
| 185 |
max_possible = max(1.0, 100.0 - baseline)
|
| 186 |
+
ratio = improvement / max_possible
|
| 187 |
|
| 188 |
# Step reward
|
| 189 |
step_r = {
|
|
|
|
| 199 |
# Delta reward β key signal
|
| 200 |
delta_r = min(0.65, ratio * 0.65)
|
| 201 |
|
| 202 |
+
# Milestone bonus β cumulative, all thresholds crossed shown + rewarded
|
| 203 |
+
milestone = 0.0
|
| 204 |
+
milestone_parts = []
|
| 205 |
+
if ratio >= 0.25:
|
| 206 |
+
milestone += 0.15
|
| 207 |
+
milestone_parts.append("25%")
|
| 208 |
+
if ratio >= 0.50:
|
| 209 |
+
milestone += 0.25
|
| 210 |
+
milestone_parts.append("50%")
|
| 211 |
if ratio >= 0.75:
|
| 212 |
+
milestone += 0.40
|
| 213 |
+
milestone_parts.append("75%")
|
| 214 |
+
milestone_str = ("π― " + "+".join(milestone_parts) + " milestone!") if milestone_parts else ""
|
| 215 |
+
|
| 216 |
+
# Wrong index penalty reduced: -0.05 instead of -0.15
|
| 217 |
+
# Old value exactly cancelled step_r (0.15 - 0.15 = 0 β clamped to 0.001)
|
| 218 |
+
# Now wrong create_index still scores ~0.10 instead of 0.001
|
| 219 |
+
wrong_pen = -0.05 if (action_type == "create_index" and delta <= 0.0) else 0.0
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
total = max(0.001, min(0.999, step_r + delta_r + milestone + wrong_pen))
|
| 222 |
+
src = "sim" if sim_improve > 0 else ("hint" if delta > 0 else "none")
|
| 223 |
+
desc = f"+{improvement:.1f}pts delta={delta:.1f}[{src}] {milestone_str}"
|
| 224 |
|
| 225 |
return total, improvement, milestone, desc
|
| 226 |
|
|
|
|
| 298 |
|
| 299 |
|
| 300 |
# ββ Generate loss + reward plots ββββββββββββββββββββββββββββββ
|
| 301 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 302 |
+
# REPLACE ONLY THIS FUNCTION in your existing train_agent.py
|
| 303 |
+
# Find: def generate_plots(trainer):
|
| 304 |
+
# Replace the entire function with this:
|
| 305 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 306 |
+
|
| 307 |
def generate_plots(trainer):
|
| 308 |
+
import json, numpy as np
|
| 309 |
import matplotlib
|
| 310 |
matplotlib.use("Agg")
|
| 311 |
import matplotlib.pyplot as plt
|
|
|
|
| 315 |
print("β οΈ No logs to plot")
|
| 316 |
return
|
| 317 |
|
| 318 |
+
steps = [l.get("step", i) for i, l in enumerate(logs)]
|
| 319 |
+
losses = [l.get("loss", 0) for l in logs]
|
| 320 |
rewards = [l.get("reward", 0) for l in logs]
|
| 321 |
|
| 322 |
+
# Save logs for generate_plots.py
|
| 323 |
+
import os, json
|
| 324 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 325 |
+
with open(f"{OUTPUT_DIR}/training_logs.json", "w") as f:
|
| 326 |
+
json.dump(trainer.state.log_history, f)
|
| 327 |
+
print(f"β
Logs saved to {OUTPUT_DIR}/training_logs.json")
|
| 328 |
+
|
| 329 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
|
| 330 |
+
fig.suptitle("GRPO Training β SQL Database Engineer Agent\n"
|
| 331 |
+
"Qwen2.5-1.5B fine-tuned with Unsloth + TRL",
|
| 332 |
fontsize=13, fontweight="bold")
|
| 333 |
|
| 334 |
+
# ββ LEFT: Loss with smoothing ββββββββββββββββββββββββββββββ
|
| 335 |
+
ax1.plot(steps, losses, "b-", lw=1.0, alpha=0.35, label="Raw loss")
|
| 336 |
+
if len(losses) >= 10:
|
| 337 |
+
smooth = np.convolve(losses, np.ones(10)/10, mode="valid")
|
| 338 |
+
ax1.plot(steps[9:], smooth, "b-", lw=2.5, label="10-step avg")
|
| 339 |
ax1.set_xlabel("Training Step")
|
| 340 |
ax1.set_ylabel("Loss")
|
| 341 |
+
ax1.set_title("Training Loss β = model learning")
|
| 342 |
+
ax1.ticklabel_format(style="sci", axis="y", scilimits=(0,0))
|
| 343 |
ax1.grid(True, alpha=0.3)
|
| 344 |
+
ax1.legend(fontsize=9)
|
| 345 |
+
# FIX 1: scientific notation annotations
|
| 346 |
if losses:
|
| 347 |
+
ax1.annotate(f"Start: {losses[0]:.2e}",
|
| 348 |
+
xy=(steps[0], losses[0]),
|
| 349 |
+
xytext=(steps[0]+3, max(losses)*0.85),
|
| 350 |
+
fontsize=8, color="red",
|
| 351 |
+
arrowprops=dict(arrowstyle="->", color="red", lw=1))
|
| 352 |
+
ax1.annotate(f"End: {losses[-1]:.2e}",
|
| 353 |
+
xy=(steps[-1], losses[-1]),
|
| 354 |
+
xytext=(steps[-1]-12, max(losses)*0.65),
|
| 355 |
+
fontsize=8, color="green",
|
| 356 |
+
arrowprops=dict(arrowstyle="->", color="green", lw=1))
|
| 357 |
+
|
| 358 |
+
# ββ RIGHT: Reward with smoothing βββββββββββββββββββββββββββ
|
| 359 |
+
ax2.plot(steps, rewards, "g-", lw=1.0, alpha=0.35, label="Raw reward")
|
| 360 |
+
if len(rewards) >= 10:
|
| 361 |
+
smooth_r = np.convolve(rewards, np.ones(10)/10, mode="valid")
|
| 362 |
+
ax2.plot(steps[9:], smooth_r, "g-", lw=2.5, label="10-step avg")
|
| 363 |
ax2.set_xlabel("Training Step")
|
| 364 |
+
ax2.set_ylabel("Avg Reward")
|
| 365 |
+
ax2.set_title("Reward During Training β = improving")
|
| 366 |
ax2.grid(True, alpha=0.3)
|
| 367 |
+
ax2.legend(fontsize=9)
|
| 368 |
+
|
| 369 |
+
# Bottom summary
|
| 370 |
+
if losses and rewards:
|
| 371 |
+
start_r = rewards[0]
|
| 372 |
+
end_r = rewards[-1]
|
| 373 |
+
pct = ((end_r-start_r)/max(abs(start_r),1e-9))*100
|
| 374 |
+
fig.text(0.5, 0.01,
|
| 375 |
+
f"Loss: {losses[0]:.2e} β {losses[-1]:.2e} | "
|
| 376 |
+
f"Reward: {start_r:.3f} β {end_r:.3f} ({'+'if pct>=0 else ''}{pct:.0f}%)",
|
| 377 |
+
ha="center", fontsize=10,
|
| 378 |
+
bbox=dict(boxstyle="round", facecolor="lightyellow", alpha=0.8))
|
| 379 |
+
|
| 380 |
+
plt.tight_layout(rect=[0, 0.07, 1, 1])
|
| 381 |
plt.savefig("loss_curve.png", dpi=150, bbox_inches="tight")
|
| 382 |
print("β
loss_curve.png saved")
|
| 383 |
if losses:
|
| 384 |
+
print(f" Loss: {losses[0]:.2e} β {losses[-1]:.2e}")
|
| 385 |
if rewards:
|
| 386 |
valid = [r for r in rewards if r > 0]
|
| 387 |
if valid:
|
| 388 |
+
print(f" Reward: {valid[0]:.3f} β {valid[-1]:.3f}")
|
| 389 |
|
| 390 |
|
| 391 |
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 441 |
|
| 442 |
print(f"ποΈ GRPO training β {MAX_STEPS} steps")
|
| 443 |
print("Expected rewards:")
|
| 444 |
+
print(" inspect_query / analyze_indexes: ~0.10")
|
| 445 |
+
print(" create_index (no table/col match): ~0.10 (was 0.001)")
|
| 446 |
+
print(" create_index (partial hint match): ~0.20-0.45")
|
| 447 |
+
print(" create_index (perfect hint match): ~0.55-0.80")
|
| 448 |
+
print(" create_index (simulator confirms): ~0.75-0.99")
|
| 449 |
+
print(" Milestones: 25%=+0.15 50%=+0.25 75%=+0.40 (cumulative)\n")
|
| 450 |
|
| 451 |
trainer.train()
|
| 452 |
print("\nβ
Training complete!")
|
|
|
|
| 468 |
|
| 469 |
|
| 470 |
if __name__ == "__main__":
|
| 471 |
+
train()
|