Upload 149 files
Browse files- hugging/td_fuse/grpo_reason.py +4 -1
- hugging/td_fuse/selfimprove.py +557 -303
hugging/td_fuse/grpo_reason.py
CHANGED
|
@@ -268,7 +268,10 @@ def verify_answer(problem: Dict, response: str) -> float:
|
|
| 268 |
# Positive indicators anywhere in answer
|
| 269 |
yes_words = ["yes", "do breathe", "does breathe", "does need", "do need",
|
| 270 |
"correct", "true", "indeed", "absolutely", "certainly",
|
| 271 |
-
"they do", "it does", "they can", "it can"
|
|
|
|
|
|
|
|
|
|
| 272 |
# Negative indicators that would flip it
|
| 273 |
no_words = ["no,", "no.", "not ", "cannot", "can't", "doesn't", "don't",
|
| 274 |
"is not", "are not", "do not"]
|
|
|
|
| 268 |
# Positive indicators anywhere in answer
|
| 269 |
yes_words = ["yes", "do breathe", "does breathe", "does need", "do need",
|
| 270 |
"correct", "true", "indeed", "absolutely", "certainly",
|
| 271 |
+
"they do", "it does", "they can", "it can",
|
| 272 |
+
"therefore,", "therefore ", "so yes", "the answer is yes",
|
| 273 |
+
"breathe air", "have feathers", "have 4 sides",
|
| 274 |
+
"need water"]
|
| 275 |
# Negative indicators that would flip it
|
| 276 |
no_words = ["no,", "no.", "not ", "cannot", "can't", "doesn't", "don't",
|
| 277 |
"is not", "are not", "do not"]
|
hugging/td_fuse/selfimprove.py
CHANGED
|
@@ -1,27 +1,26 @@
|
|
| 1 |
"""
|
| 2 |
TD Self-Improvement Loop — the core of Time Dilation.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
Based on findings from test_1 through test_18 interviews:
|
| 6 |
-
|
| 7 |
-
THE LOOP:
|
| 8 |
1. Ask the model "what are you bad at?" → it identifies weak spots
|
| 9 |
-
2. Generate
|
| 10 |
-
3.
|
| 11 |
-
4.
|
| 12 |
-
5.
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
- Cherry_LLM perplexity filter prevents mode collapse
|
| 18 |
-
- Mix
|
| 19 |
-
-
|
| 20 |
-
|
| 21 |
-
COST SPLIT (from test_16):
|
| 22 |
-
- 70-80% inference scaling (generate many, pick best)
|
| 23 |
-
- 10-20% short GRPO training
|
| 24 |
-
- 5-10% tooling/evaluation
|
| 25 |
"""
|
| 26 |
|
| 27 |
import torch
|
|
@@ -30,44 +29,52 @@ import json
|
|
| 30 |
import math
|
| 31 |
import random
|
| 32 |
import gc
|
|
|
|
|
|
|
| 33 |
from pathlib import Path
|
| 34 |
-
from typing import Optional
|
| 35 |
-
from dataclasses import dataclass
|
| 36 |
|
| 37 |
|
| 38 |
@dataclass
|
| 39 |
class SelfImproveConfig:
|
| 40 |
"""Configuration for one self-improvement cycle."""
|
| 41 |
-
model_path: str = "td_fuse_outputs/
|
| 42 |
-
output_dir: str = "td_fuse_outputs/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
#
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
# Training
|
| 50 |
-
lora_r: int =
|
| 51 |
-
lora_alpha: int =
|
| 52 |
-
train_epochs: int =
|
| 53 |
-
train_batch: int =
|
| 54 |
-
train_grad_accum: int =
|
| 55 |
-
learning_rate: float =
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
# Data settings
|
| 58 |
-
num_reasoning_problems: int = 200 # Logic/reasoning problems to generate
|
| 59 |
-
num_math_problems: int = 200 # Math problems
|
| 60 |
-
num_code_problems: int = 100 # Code problems
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
|
| 65 |
|
| 66 |
# ============================================================
|
| 67 |
# STEP 1: DIAGNOSE — Ask the model what it's bad at
|
| 68 |
# ============================================================
|
| 69 |
|
| 70 |
-
def diagnose_weaknesses(model, tokenizer, eos_id):
|
| 71 |
"""
|
| 72 |
Ask the model to identify its own weaknesses.
|
| 73 |
All 3 AIs (ChatGPT, Grok, Gemini) confirmed models can do this.
|
|
@@ -75,244 +82,431 @@ def diagnose_weaknesses(model, tokenizer, eos_id):
|
|
| 75 |
print("\n=== STEP 1: SELF-DIAGNOSIS ===")
|
| 76 |
|
| 77 |
prompts = [
|
| 78 |
-
"What kinds of questions
|
| 79 |
-
|
| 80 |
-
"
|
|
|
|
|
|
|
|
|
|
| 81 |
]
|
| 82 |
|
| 83 |
-
|
| 84 |
-
for prompt in prompts:
|
| 85 |
-
p = f"<|im_start|>user\n{prompt}<|im_end|>\n
|
|
|
|
| 86 |
ids = tokenizer(p, return_tensors="pt").to(model.device)
|
| 87 |
out = model.generate(
|
| 88 |
-
**ids, max_new_tokens=
|
| 89 |
temperature=0.7, eos_token_id=eos_id
|
| 90 |
)
|
| 91 |
response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
# ============================================================
|
| 99 |
-
# STEP 2: GENERATE
|
| 100 |
# ============================================================
|
| 101 |
|
| 102 |
-
def
|
| 103 |
"""
|
| 104 |
-
Generate
|
| 105 |
-
|
| 106 |
"""
|
| 107 |
problems = []
|
| 108 |
|
| 109 |
-
#
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
"
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
("Which is heavier: a ton of feathers or a ton of steel?", "same", "They weigh the same — both are one ton."),
|
| 132 |
-
("Which weighs more: 1kg of cotton or 1kg of iron?", "same", "They weigh the same — both are 1 kilogram."),
|
| 133 |
-
("If you have 5 apples and take away 3, how many do YOU have?", "3", "You have 3 apples — you took them."),
|
| 134 |
-
("A farmer has 17 sheep. All but 9 die. How many are left?", "9", "9 sheep are left — 'all but 9' means 9 survive."),
|
| 135 |
-
("How many times can you subtract 5 from 25?", "1", "Once — after that it's 20, not 25."),
|
| 136 |
-
("If there are 3 apples and you take away 2, how many do you have?", "2", "You have 2 — you took them."),
|
| 137 |
-
("What has a head and a tail but no body?", "coin", "A coin has a head and a tail but no body."),
|
| 138 |
-
]
|
| 139 |
-
for q, key, full_answer in trick_qs:
|
| 140 |
-
problems.append({
|
| 141 |
-
"question": q,
|
| 142 |
-
"answer": full_answer,
|
| 143 |
-
"verify_key": key,
|
| 144 |
-
"type": "trick_question"
|
| 145 |
-
})
|
| 146 |
-
|
| 147 |
-
# Syllogism / deductive reasoning
|
| 148 |
-
syllogisms = [
|
| 149 |
-
("All mammals are warm-blooded. A whale is a mammal. Is a whale warm-blooded?", "yes"),
|
| 150 |
-
("All birds have feathers. A penguin is a bird. Does a penguin have feathers?", "yes"),
|
| 151 |
-
("No reptiles are mammals. A snake is a reptile. Is a snake a mammal?", "no"),
|
| 152 |
-
("All squares are rectangles. All rectangles have 4 sides. Do all squares have 4 sides?", "yes"),
|
| 153 |
-
("Some dogs are brown. Max is a dog. Is Max definitely brown?", "no"),
|
| 154 |
-
("All cats are animals. Some animals are pets. Are all cats pets?", "no"),
|
| 155 |
-
]
|
| 156 |
-
for q, a in syllogisms:
|
| 157 |
-
problems.append({
|
| 158 |
-
"question": q + " Explain your reasoning.",
|
| 159 |
-
"answer": a,
|
| 160 |
-
"type": "syllogism"
|
| 161 |
-
})
|
| 162 |
-
|
| 163 |
-
# Multi-step reasoning
|
| 164 |
-
multi_step = [
|
| 165 |
-
("If A is taller than B, and B is taller than C, who is the shortest?", "c"),
|
| 166 |
-
("If X is older than Y, Y is older than Z, and Z is older than W, who is the youngest?", "w"),
|
| 167 |
-
("In a race, Tom finished before Jerry but after Sam. Who won the race?", "sam"),
|
| 168 |
-
("Amy is shorter than Bob. Bob is shorter than Carol. Carol is shorter than Dave. Who is the tallest?", "dave"),
|
| 169 |
-
]
|
| 170 |
-
for q, a in multi_step:
|
| 171 |
-
problems.append({
|
| 172 |
-
"question": q,
|
| 173 |
-
"answer": a,
|
| 174 |
-
"type": "multi_step"
|
| 175 |
-
})
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
answer = a * b
|
| 197 |
problems.append({
|
| 198 |
-
"question": f"
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
})
|
| 202 |
|
| 203 |
-
elif ptype == "
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
problems.append({
|
| 218 |
-
"question": f"What
|
| 219 |
-
"answer": str(
|
| 220 |
-
"type": "math_percentage"
|
| 221 |
})
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
problems.append({
|
| 229 |
-
"question":
|
| 230 |
-
"
|
| 231 |
-
"type": "math_fraction"
|
| 232 |
})
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
return problems
|
| 235 |
|
| 236 |
|
| 237 |
# ============================================================
|
| 238 |
-
# STEP 3:
|
| 239 |
# ============================================================
|
| 240 |
|
| 241 |
-
def
|
| 242 |
-
"""
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
if "verify_key" in check_problem:
|
| 249 |
-
check_problem["answer"] = check_problem["verify_key"]
|
| 250 |
-
from .grpo_reason import verify_answer as smart_verify
|
| 251 |
-
return smart_verify(check_problem, model_answer)
|
| 252 |
|
| 253 |
|
| 254 |
-
def generate_and_score(model, tokenizer, problems, cfg, eos_id):
|
| 255 |
"""
|
| 256 |
-
|
| 257 |
-
|
| 258 |
"""
|
| 259 |
-
print(f"\n=== STEP
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
winning_pairs = [] # (question_chat, best_answer_chat) pairs for training
|
| 262 |
total_correct = 0
|
|
|
|
| 263 |
|
| 264 |
-
for i
|
| 265 |
-
|
| 266 |
-
prompt =
|
| 267 |
ids = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
|
|
|
| 271 |
for _ in range(cfg.num_candidates):
|
| 272 |
out = model.generate(
|
| 273 |
**ids, max_new_tokens=cfg.max_gen_tokens,
|
| 274 |
do_sample=True, temperature=cfg.temperature,
|
| 275 |
eos_token_id=eos_id
|
| 276 |
)
|
| 277 |
-
|
| 278 |
-
score = verify_answer(prob, answer)
|
| 279 |
-
candidates.append((answer, score))
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
# Format as chat for training
|
| 290 |
-
chat = f"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n{best[0]}<|im_end|>"
|
| 291 |
-
winning_pairs.append(chat)
|
| 292 |
|
| 293 |
-
|
| 294 |
-
pct = total_correct / (i + 1) * 100
|
| 295 |
-
print(f" [{i+1}/{len(problems)}] Correct so far: {total_correct}/{i+1} ({pct:.0f}%)")
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
|
| 304 |
# ============================================================
|
| 305 |
-
# STEP 4: TRAIN —
|
| 306 |
# ============================================================
|
| 307 |
|
| 308 |
def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
| 309 |
"""
|
| 310 |
-
Train on
|
| 311 |
-
|
| 312 |
"""
|
| 313 |
-
print(f"\n=== STEP 4: TRAIN ON WINNERS ({len(winning_pairs)}
|
| 314 |
|
| 315 |
-
if len(winning_pairs) <
|
| 316 |
print(" Too few winning pairs — skipping training")
|
| 317 |
return model
|
| 318 |
|
|
@@ -320,18 +514,22 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
|
| 320 |
from transformers import TrainingArguments, Trainer
|
| 321 |
from torch.utils.data import Dataset
|
| 322 |
|
| 323 |
-
#
|
|
|
|
|
|
|
|
|
|
| 324 |
lora_config = LoraConfig(
|
| 325 |
r=cfg.lora_r, lora_alpha=cfg.lora_alpha, lora_dropout=0.0,
|
| 326 |
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
| 327 |
"gate_proj", "up_proj", "down_proj"],
|
|
|
|
| 328 |
bias="none", task_type=TaskType.CAUSAL_LM,
|
| 329 |
)
|
| 330 |
model = get_peft_model(model, lora_config)
|
| 331 |
model.print_trainable_parameters()
|
| 332 |
|
| 333 |
-
class
|
| 334 |
-
def __init__(self, texts, tokenizer, max_len=
|
| 335 |
self.data = []
|
| 336 |
for t in texts:
|
| 337 |
e = tokenizer(t, truncation=True, max_length=max_len,
|
|
@@ -344,12 +542,13 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
|
| 344 |
def __len__(self): return len(self.data)
|
| 345 |
def __getitem__(self, i): return self.data[i]
|
| 346 |
|
| 347 |
-
dataset =
|
| 348 |
|
| 349 |
out_dir = Path(cfg.output_dir) / "train_output"
|
| 350 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 351 |
|
| 352 |
total_steps = (len(dataset) * cfg.train_epochs) // (cfg.train_batch * cfg.train_grad_accum)
|
|
|
|
| 353 |
|
| 354 |
args = TrainingArguments(
|
| 355 |
output_dir=str(out_dir),
|
|
@@ -358,10 +557,10 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
|
| 358 |
gradient_accumulation_steps=cfg.train_grad_accum,
|
| 359 |
learning_rate=cfg.learning_rate,
|
| 360 |
bf16=True,
|
| 361 |
-
logging_steps=max(1, total_steps //
|
| 362 |
save_strategy="steps",
|
| 363 |
-
save_steps=
|
| 364 |
-
save_total_limit=
|
| 365 |
warmup_ratio=0.05,
|
| 366 |
lr_scheduler_type="cosine",
|
| 367 |
optim="adamw_torch",
|
|
@@ -373,91 +572,120 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
|
| 373 |
train_dataset=dataset, args=args
|
| 374 |
)
|
| 375 |
|
| 376 |
-
print(
|
| 377 |
trainer.train()
|
| 378 |
|
| 379 |
-
# Merge LoRA FIRST, then clean checkpoints (safer)
|
| 380 |
print(" Merging LoRA...")
|
| 381 |
merged = model.merge_and_unload()
|
| 382 |
gc.collect()
|
| 383 |
|
| 384 |
-
import shutil
|
| 385 |
shutil.rmtree(str(out_dir), ignore_errors=True)
|
|
|
|
| 386 |
|
| 387 |
return merged
|
| 388 |
|
| 389 |
|
| 390 |
# ============================================================
|
| 391 |
-
# STEP 5: BENCHMARK
|
| 392 |
# ============================================================
|
| 393 |
|
| 394 |
def benchmark(model, tokenizer, eos_id):
|
| 395 |
-
"""
|
| 396 |
print("\n=== STEP 5: BENCHMARK ===")
|
| 397 |
-
|
| 398 |
results = {}
|
| 399 |
|
| 400 |
-
def ask(prompt):
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
| 402 |
ids = tokenizer(p, return_tensors="pt").to(model.device)
|
| 403 |
-
out = model.generate(**ids, max_new_tokens=
|
| 404 |
return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
|
| 405 |
|
| 406 |
-
#
|
| 407 |
-
math_tests = [("7+8", "15"), ("123+456", "579"), ("1000-387", "613"),
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
reason_tests = [
|
| 414 |
-
("If all roses are flowers and all flowers need water, do roses need water?", "yes"),
|
| 415 |
("Which is heavier: a pound of feathers or a pound of bricks?", "same"),
|
| 416 |
("If yesterday was Monday, what day is tomorrow?", "wednesday"),
|
| 417 |
("A farmer has 17 sheep. All but 9 die. How many are left?", "9"),
|
| 418 |
-
("If you
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
]
|
| 420 |
-
|
| 421 |
for q, expected in reason_tests:
|
| 422 |
-
a = ask(q)
|
| 423 |
-
correct =
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
("
|
|
|
|
|
|
|
| 434 |
]
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
# Perplexity
|
| 440 |
test_text = "The quick brown fox jumps over the lazy dog. Machine learning models can process natural language."
|
| 441 |
enc = tokenizer(test_text, return_tensors="pt").to(model.device)
|
| 442 |
with torch.no_grad():
|
| 443 |
loss = model(**enc, labels=enc.input_ids).loss.item()
|
| 444 |
-
|
| 445 |
-
results[
|
| 446 |
-
print(f" Perplexity: {ppl:.2f}")
|
| 447 |
|
| 448 |
return results
|
| 449 |
|
| 450 |
|
| 451 |
# ============================================================
|
| 452 |
-
# MAIN
|
| 453 |
# ============================================================
|
| 454 |
|
| 455 |
def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
|
| 456 |
-
"""
|
| 457 |
-
Run one complete self-improvement cycle.
|
| 458 |
-
|
| 459 |
-
Returns path to improved model.
|
| 460 |
-
"""
|
| 461 |
if cfg is None:
|
| 462 |
cfg = SelfImproveConfig()
|
| 463 |
|
|
@@ -468,9 +696,16 @@ def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
|
|
| 468 |
print(f"Started: {time.strftime('%H:%M:%S')}")
|
| 469 |
print("=" * 60)
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
# Load model
|
| 472 |
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 473 |
-
|
| 474 |
print("\nLoading model...")
|
| 475 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 476 |
cfg.model_path, dtype=torch.bfloat16,
|
|
@@ -479,52 +714,71 @@ def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
|
|
| 479 |
tokenizer = AutoTokenizer.from_pretrained(cfg.model_path, trust_remote_code=True)
|
| 480 |
eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
|
| 481 |
|
| 482 |
-
#
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
# Step 2: Generate problems
|
| 486 |
-
print("\n=== STEP 2: GENERATING PROBLEMS ===")
|
| 487 |
-
problems = []
|
| 488 |
-
problems.extend(generate_reasoning_problems())
|
| 489 |
-
print(f" Reasoning problems: {len(problems)}")
|
| 490 |
-
math_probs = generate_math_problems(cfg.num_math_problems)
|
| 491 |
-
problems.extend(math_probs)
|
| 492 |
-
print(f" Math problems: {len(math_probs)}")
|
| 493 |
-
print(f" Total: {len(problems)}")
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
-
# Step 4: Train
|
| 501 |
model = train_on_winners(model, tokenizer, winning_pairs, cfg)
|
| 502 |
|
| 503 |
# Save improved model
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
print(f"\nSaving improved model to {
|
| 507 |
-
model.save_pretrained(str(
|
| 508 |
-
tokenizer.save_pretrained(str(
|
| 509 |
-
sz = sum(f.stat().st_size for f in
|
| 510 |
-
print(f"SAVED: {
|
| 511 |
-
|
| 512 |
-
# Step 5:
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
results["cycle"] = cycle_num
|
| 518 |
-
results["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 519 |
-
results["duration_min"] = (time.time() - start) / 60
|
| 520 |
-
with open(results_file, "w") as f:
|
| 521 |
-
json.dump(results, f, indent=2)
|
| 522 |
-
|
| 523 |
elapsed = (time.time() - start) / 60
|
| 524 |
print(f"\n{'=' * 60}")
|
| 525 |
print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min")
|
| 526 |
-
print(f"
|
| 527 |
-
print(f"
|
|
|
|
| 528 |
print(f"{'=' * 60}")
|
| 529 |
|
| 530 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
TD Self-Improvement Loop — the core of Time Dilation.
|
| 3 |
|
| 4 |
+
THE LOOP (from test_1 through test_18 interviews):
|
|
|
|
|
|
|
|
|
|
| 5 |
1. Ask the model "what are you bad at?" → it identifies weak spots
|
| 6 |
+
2. Generate HARD problems targeting those weaknesses (stuff it fails 30-50%)
|
| 7 |
+
3. Generate N candidates per problem, score with verified rewards
|
| 8 |
+
4. Train on winners (STaR/GRPO approach)
|
| 9 |
+
5. Benchmark → measure improvement
|
| 10 |
+
6. Save → repeat
|
| 11 |
+
|
| 12 |
+
KEY DIFFERENCE FROM GRPO CYCLE 2:
|
| 13 |
+
- Problems are HARDER — designed so the model fails 30-50% of the time
|
| 14 |
+
- This creates real contrast for learning (correct vs wrong chains)
|
| 15 |
+
- Uses CoT (<think>...</think>) for all reasoning
|
| 16 |
+
- Saves after every major step (diagnosis, generation, training)
|
| 17 |
+
- Targets layers 16-28 specifically for reasoning improvement
|
| 18 |
+
|
| 19 |
+
PRINCIPLES (from interviews + dad's tests):
|
| 20 |
+
- Verified rewards only: math correct, logic valid, no learned reward model
|
| 21 |
- Cherry_LLM perplexity filter prevents mode collapse
|
| 22 |
+
- Mix problem types to avoid overfitting
|
| 23 |
+
- Small nudges per cycle (1-5%), compounds over time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
import torch
|
|
|
|
| 29 |
import math
|
| 30 |
import random
|
| 31 |
import gc
|
| 32 |
+
import re
|
| 33 |
+
import shutil
|
| 34 |
from pathlib import Path
|
| 35 |
+
from typing import List, Dict, Optional
|
| 36 |
+
from dataclasses import dataclass
|
| 37 |
|
| 38 |
|
| 39 |
@dataclass
|
| 40 |
class SelfImproveConfig:
|
| 41 |
"""Configuration for one self-improvement cycle."""
|
| 42 |
+
model_path: str = "td_fuse_outputs/reasoning_healed"
|
| 43 |
+
output_dir: str = "td_fuse_outputs/self_improve"
|
| 44 |
+
progress_file: str = "td_fuse_outputs/self_improve/progress.json"
|
| 45 |
+
|
| 46 |
+
# Generation
|
| 47 |
+
num_candidates: int = 8 # Generate N answers per question, pick best
|
| 48 |
+
max_gen_tokens: int = 1024 # Long — CoT needs room
|
| 49 |
+
temperature: float = 0.8 # Higher than GRPO — we want diversity + some failures
|
| 50 |
|
| 51 |
+
# Problems — more than GRPO, and harder
|
| 52 |
+
num_hard_math: int = 150 # Multi-step math (the model should fail ~30-50%)
|
| 53 |
+
num_hard_logic: int = 100 # Complex logic chains
|
| 54 |
+
num_hard_tricks: int = 50 # Tricky questions that trip up LLMs
|
| 55 |
+
num_hard_coding: int = 50 # Code reasoning problems
|
| 56 |
+
batch_save_every: int = 25 # Save progress more often
|
| 57 |
|
| 58 |
+
# Training
|
| 59 |
+
lora_r: int = 32 # Higher rank — more capacity for reasoning
|
| 60 |
+
lora_alpha: int = 64
|
| 61 |
+
train_epochs: int = 3
|
| 62 |
+
train_batch: int = 2
|
| 63 |
+
train_grad_accum: int = 8 # Effective batch = 16
|
| 64 |
+
learning_rate: float = 1e-5 # Gentle
|
| 65 |
+
save_steps: int = 50
|
| 66 |
+
target_layers: str = "16-28" # Reasoning circuit (from test_18)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
# Import verify from grpo_reason
|
| 70 |
+
from .grpo_reason import verify_answer as smart_verify
|
| 71 |
|
| 72 |
|
| 73 |
# ============================================================
|
| 74 |
# STEP 1: DIAGNOSE — Ask the model what it's bad at
|
| 75 |
# ============================================================
|
| 76 |
|
| 77 |
+
def diagnose_weaknesses(model, tokenizer, eos_id, save_path=None):
|
| 78 |
"""
|
| 79 |
Ask the model to identify its own weaknesses.
|
| 80 |
All 3 AIs (ChatGPT, Grok, Gemini) confirmed models can do this.
|
|
|
|
| 82 |
print("\n=== STEP 1: SELF-DIAGNOSIS ===")
|
| 83 |
|
| 84 |
prompts = [
|
| 85 |
+
("What kinds of questions are you worst at? Be brutally honest. "
|
| 86 |
+
"List your top 5 weaknesses with specific examples of questions you'd get wrong."),
|
| 87 |
+
("Give me 10 math or logic problems that would be genuinely hard for you. "
|
| 88 |
+
"Problems where you'd likely make mistakes. Be specific."),
|
| 89 |
+
("What types of multi-step reasoning cause you to make errors? "
|
| 90 |
+
"Give 5 specific examples where you'd probably get the wrong answer."),
|
| 91 |
]
|
| 92 |
|
| 93 |
+
all_responses = []
|
| 94 |
+
for i, prompt in enumerate(prompts):
|
| 95 |
+
p = (f"<|im_start|>user\n{prompt}<|im_end|>\n"
|
| 96 |
+
f"<|im_start|>assistant\n<think>\nLet me think carefully about my actual weaknesses.\n")
|
| 97 |
ids = tokenizer(p, return_tensors="pt").to(model.device)
|
| 98 |
out = model.generate(
|
| 99 |
+
**ids, max_new_tokens=800, do_sample=True,
|
| 100 |
temperature=0.7, eos_token_id=eos_id
|
| 101 |
)
|
| 102 |
response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
|
| 103 |
+
all_responses.append(response)
|
| 104 |
+
# Show the answer part (after </think>) if available
|
| 105 |
+
display = response.split("</think>")[-1].strip() if "</think>" in response else response
|
| 106 |
+
print(f" Diagnosis {i+1}: {display[:200]}...")
|
| 107 |
|
| 108 |
+
# Save diagnosis
|
| 109 |
+
if save_path:
|
| 110 |
+
with open(save_path, "w") as f:
|
| 111 |
+
json.dump({"diagnoses": all_responses, "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}, f, indent=2)
|
| 112 |
+
print(f" Saved diagnosis to {save_path}")
|
| 113 |
+
|
| 114 |
+
return all_responses
|
| 115 |
|
| 116 |
|
| 117 |
# ============================================================
|
| 118 |
+
# STEP 2: GENERATE HARD PROBLEMS
|
| 119 |
# ============================================================
|
| 120 |
|
| 121 |
+
def generate_hard_problems(cfg: SelfImproveConfig) -> List[Dict]:
|
| 122 |
"""
|
| 123 |
+
Generate problems that are HARD enough for the model to fail 30-50%.
|
| 124 |
+
This is the key difference from GRPO cycle 2 (which was too easy at 97%).
|
| 125 |
"""
|
| 126 |
problems = []
|
| 127 |
|
| 128 |
+
# --- HARD MATH: multi-step, carrying, fractions, negative numbers ---
|
| 129 |
+
for _ in range(cfg.num_hard_math):
|
| 130 |
+
ptype = random.choice([
|
| 131 |
+
"multi_step", "carrying", "negative", "fraction_word",
|
| 132 |
+
"percentage_chain", "rate_comparison", "sequence_hard"
|
| 133 |
+
])
|
| 134 |
+
|
| 135 |
+
if ptype == "multi_step":
|
| 136 |
+
# 3-4 step word problems
|
| 137 |
+
items = random.choice(["books", "apples", "shirts", "tickets", "cookies"])
|
| 138 |
+
start = random.randint(20, 100)
|
| 139 |
+
buy = random.randint(5, 30)
|
| 140 |
+
give = random.randint(1, min(15, start + buy - 1))
|
| 141 |
+
lose = random.randint(1, min(5, start + buy - give - 1))
|
| 142 |
+
final = start + buy - give - lose
|
| 143 |
+
problems.append({
|
| 144 |
+
"question": (f"You have {start} {items}. You buy {buy} more. "
|
| 145 |
+
f"You give {give} to a friend. You lose {lose}. "
|
| 146 |
+
f"How many {items} do you have now?"),
|
| 147 |
+
"answer": str(final), "type": "hard_math",
|
| 148 |
+
"think_hint": f"{start} + {buy} - {give} - {lose} = {final}"
|
| 149 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
+
elif ptype == "carrying":
|
| 152 |
+
# Large number arithmetic that requires carrying
|
| 153 |
+
a = random.randint(500, 9999)
|
| 154 |
+
b = random.randint(500, 9999)
|
| 155 |
+
op = random.choice(["+", "-", "*"])
|
| 156 |
+
if op == "+": answer = a + b
|
| 157 |
+
elif op == "-": answer = a - b
|
| 158 |
+
else:
|
| 159 |
+
a = random.randint(20, 200)
|
| 160 |
+
b = random.randint(20, 200)
|
| 161 |
+
answer = a * b
|
| 162 |
+
problems.append({
|
| 163 |
+
"question": f"Calculate: {a} {op} {b}",
|
| 164 |
+
"answer": str(answer), "type": "hard_math",
|
| 165 |
+
"think_hint": f"{a} {op} {b} = {answer}"
|
| 166 |
+
})
|
| 167 |
|
| 168 |
+
elif ptype == "negative":
|
| 169 |
+
a = random.randint(-50, 50)
|
| 170 |
+
b = random.randint(-50, 50)
|
| 171 |
+
op = random.choice(["+", "-", "*"])
|
| 172 |
+
if op == "+": answer = a + b
|
| 173 |
+
elif op == "-": answer = a - b
|
| 174 |
+
else: answer = a * b
|
| 175 |
+
a_str = str(a) if a >= 0 else f"({a})"
|
| 176 |
+
b_str = str(b) if b >= 0 else f"({b})"
|
| 177 |
+
problems.append({
|
| 178 |
+
"question": f"What is {a_str} {op} {b_str}?",
|
| 179 |
+
"answer": str(answer), "type": "hard_math",
|
| 180 |
+
"think_hint": f"{a} {op} {b} = {answer}"
|
| 181 |
+
})
|
| 182 |
|
| 183 |
+
elif ptype == "fraction_word":
|
| 184 |
+
total = random.choice([12, 16, 20, 24, 30, 36, 40, 48, 60])
|
| 185 |
+
frac_n = random.choice([1, 2, 3])
|
| 186 |
+
frac_d = random.choice([2, 3, 4, 5, 6])
|
| 187 |
+
while frac_n >= frac_d:
|
| 188 |
+
frac_n = random.choice([1, 2, 3])
|
| 189 |
+
first = total * frac_n // frac_d
|
| 190 |
+
remaining = total - first
|
| 191 |
+
second_frac = random.choice([2, 3, 4])
|
| 192 |
+
second = remaining // second_frac
|
| 193 |
+
final = remaining - second
|
| 194 |
+
problems.append({
|
| 195 |
+
"question": (f"A jar has {total} marbles. You take {frac_n}/{frac_d} of them. "
|
| 196 |
+
f"Then you give away {second} of what's left. How many remain in the jar?"),
|
| 197 |
+
"answer": str(final), "type": "hard_math",
|
| 198 |
+
"think_hint": (f"Take {frac_n}/{frac_d} of {total} = {first}. "
|
| 199 |
+
f"Left: {remaining}. Give {second}. Final: {final}")
|
| 200 |
+
})
|
| 201 |
|
| 202 |
+
elif ptype == "percentage_chain":
|
| 203 |
+
base = random.choice([100, 200, 400, 500, 800, 1000])
|
| 204 |
+
pct1 = random.choice([10, 20, 25, 30, 50])
|
| 205 |
+
after1 = base + base * pct1 // 100
|
| 206 |
+
pct2 = random.choice([10, 20, 25])
|
| 207 |
+
final = after1 - after1 * pct2 // 100
|
| 208 |
+
problems.append({
|
| 209 |
+
"question": (f"A stock is worth ${base}. It goes up {pct1}%, "
|
| 210 |
+
f"then drops {pct2}%. What is it worth now?"),
|
| 211 |
+
"answer": str(final), "type": "hard_math",
|
| 212 |
+
"think_hint": (f"Up {pct1}%: {base} + {base*pct1//100} = {after1}. "
|
| 213 |
+
f"Down {pct2}%: {after1} - {after1*pct2//100} = {final}")
|
| 214 |
+
})
|
| 215 |
|
| 216 |
+
elif ptype == "rate_comparison":
|
| 217 |
+
speed1 = random.randint(30, 80)
|
| 218 |
+
time1 = random.randint(2, 6)
|
| 219 |
+
speed2 = random.randint(40, 100)
|
| 220 |
+
time2 = random.randint(1, 5)
|
| 221 |
+
dist1 = speed1 * time1
|
| 222 |
+
dist2 = speed2 * time2
|
| 223 |
+
diff = abs(dist1 - dist2)
|
| 224 |
+
who = "first" if dist1 > dist2 else "second"
|
|
|
|
| 225 |
problems.append({
|
| 226 |
+
"question": (f"Car A drives {speed1} mph for {time1} hours. "
|
| 227 |
+
f"Car B drives {speed2} mph for {time2} hours. "
|
| 228 |
+
f"How much further did the car that drove more go?"),
|
| 229 |
+
"answer": str(diff), "type": "hard_math",
|
| 230 |
+
"think_hint": f"Car A: {dist1}mi. Car B: {dist2}mi. Diff: {diff}mi."
|
| 231 |
})
|
| 232 |
|
| 233 |
+
elif ptype == "sequence_hard":
|
| 234 |
+
# Non-obvious sequences
|
| 235 |
+
seq_type = random.choice(["quadratic", "fibonacci_like", "alternating"])
|
| 236 |
+
if seq_type == "quadratic":
|
| 237 |
+
a, b, c = random.randint(1, 3), random.randint(1, 5), random.randint(0, 10)
|
| 238 |
+
seq = [a*n*n + b*n + c for n in range(1, 6)]
|
| 239 |
+
next_val = a*36 + b*6 + c
|
| 240 |
+
elif seq_type == "fibonacci_like":
|
| 241 |
+
a, b = random.randint(1, 5), random.randint(1, 5)
|
| 242 |
+
seq = [a, b]
|
| 243 |
+
for _ in range(4):
|
| 244 |
+
seq.append(seq[-1] + seq[-2])
|
| 245 |
+
next_val = seq[-1] + seq[-2]
|
| 246 |
+
seq = seq[:5]
|
| 247 |
+
else: # alternating
|
| 248 |
+
start = random.randint(1, 10)
|
| 249 |
+
step1 = random.randint(2, 5)
|
| 250 |
+
step2 = random.randint(1, 3)
|
| 251 |
+
seq = [start]
|
| 252 |
+
for i in range(4):
|
| 253 |
+
if i % 2 == 0:
|
| 254 |
+
seq.append(seq[-1] + step1)
|
| 255 |
+
else:
|
| 256 |
+
seq.append(seq[-1] - step2)
|
| 257 |
+
# Next follows the pattern
|
| 258 |
+
next_val = seq[-1] + step1 if len(seq) % 2 == 1 else seq[-1] - step2
|
| 259 |
problems.append({
|
| 260 |
+
"question": f"What comes next: {', '.join(map(str, seq))}?",
|
| 261 |
+
"answer": str(next_val), "type": "hard_math",
|
|
|
|
| 262 |
})
|
| 263 |
|
| 264 |
+
# --- HARD LOGIC: longer chains, negations, misdirection ---
|
| 265 |
+
logic_templates = [
|
| 266 |
+
# 4+ entity ordering
|
| 267 |
+
("Amy is taller than Bob. Carol is shorter than Bob but taller than Dave. "
|
| 268 |
+
"Eve is taller than Amy. Who is the shortest?",
|
| 269 |
+
"dave", "Eve > Amy > Bob > Carol > Dave. Dave is shortest."),
|
| 270 |
+
("In a tournament: A beat B, C beat A, D beat C, B beat D. Who beat the most players?",
|
| 271 |
+
"1", "Each player beat exactly 1 other player (it's circular)."),
|
| 272 |
+
# Negation chains
|
| 273 |
+
("If it's not true that all cats are black, and Fluffy is a cat, "
|
| 274 |
+
"can we conclude Fluffy is not black?",
|
| 275 |
+
"no", "We can't conclude either way — some cats might be black, some not."),
|
| 276 |
+
("None of the winners cheated. Tom didn't cheat. Did Tom win?",
|
| 277 |
+
"no", "Not cheating is necessary but not sufficient for winning."),
|
| 278 |
+
# Conditional logic
|
| 279 |
+
("If it rains, the ground is wet. The ground is wet. Did it rain?",
|
| 280 |
+
"no", "The ground could be wet for other reasons (sprinkler, etc). Affirming the consequent."),
|
| 281 |
+
("If it rains, the ground is wet. It didn't rain. Is the ground dry?",
|
| 282 |
+
"no", "The ground could still be wet from other causes. Denying the antecedent."),
|
| 283 |
+
("If A then B. If B then C. A is true. Is C true?",
|
| 284 |
+
"yes", "A→B→C. A is true, so B is true, so C is true. Modus ponens chain."),
|
| 285 |
+
("If A then B. If B then C. C is false. Is A true?",
|
| 286 |
+
"no", "C is false means B must be false (contrapositive), which means A must be false."),
|
| 287 |
+
# Set theory
|
| 288 |
+
("In a class of 30 students, 18 play soccer and 15 play basketball. "
|
| 289 |
+
"At least how many play both?",
|
| 290 |
+
"3", "18 + 15 = 33. Must overlap by at least 33 - 30 = 3."),
|
| 291 |
+
("A group has 40 people. 25 speak English, 20 speak French, and 10 speak both. "
|
| 292 |
+
"How many speak neither?",
|
| 293 |
+
"5", "English or French: 25 + 20 - 10 = 35. Neither: 40 - 35 = 5."),
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
for _ in range(cfg.num_hard_logic):
|
| 297 |
+
if logic_templates:
|
| 298 |
+
q, a, hint = random.choice(logic_templates)
|
| 299 |
problems.append({
|
| 300 |
+
"question": q, "answer": a, "type": "hard_logic",
|
| 301 |
+
"think_hint": hint
|
|
|
|
| 302 |
})
|
| 303 |
|
| 304 |
+
# Also generate parameterized ordering problems (harder: 5-6 entities)
|
| 305 |
+
for _ in range(cfg.num_hard_logic // 2):
|
| 306 |
+
names = random.sample(["Amy", "Bob", "Carol", "Dave", "Eve", "Frank"], 5)
|
| 307 |
+
# Create a random ordering
|
| 308 |
+
order = list(range(5))
|
| 309 |
+
random.shuffle(order)
|
| 310 |
+
# Generate comparison clues (not all pairs — make them work it out)
|
| 311 |
+
clues = []
|
| 312 |
+
for i in range(4):
|
| 313 |
+
a_idx = order[i]
|
| 314 |
+
b_idx = order[i + 1]
|
| 315 |
+
clues.append(f"{names[a_idx]} is taller than {names[b_idx]}")
|
| 316 |
+
random.shuffle(clues)
|
| 317 |
+
tallest = names[order[0]]
|
| 318 |
+
shortest = names[order[4]]
|
| 319 |
+
q_type = random.choice(["tallest", "shortest"])
|
| 320 |
+
answer = tallest.lower() if q_type == "tallest" else shortest.lower()
|
| 321 |
+
problems.append({
|
| 322 |
+
"question": f"{'. '.join(clues)}. Who is the {q_type}?",
|
| 323 |
+
"answer": answer, "type": "hard_logic",
|
| 324 |
+
})
|
| 325 |
+
|
| 326 |
+
# --- HARD TRICKS: LLM failure modes ---
|
| 327 |
+
tricks = [
|
| 328 |
+
("How many times can you subtract 5 from 25?",
|
| 329 |
+
"once", "Only once — after that it's 20, not 25."),
|
| 330 |
+
("How many months have 28 days?",
|
| 331 |
+
"all", "All 12 months have at least 28 days."),
|
| 332 |
+
("I have 6 eggs. I broke 2, cooked 2, and ate 2. How many eggs do I have left?",
|
| 333 |
+
"4", "You broke 2 to cook them, and ate the cooked ones. Same 2 eggs through the process. 6-2=4."),
|
| 334 |
+
("A doctor gives you 3 pills and says take one every 30 minutes. How long do they last?",
|
| 335 |
+
"60", "Take first at 0 min, second at 30 min, third at 60 min. Total: 60 minutes."),
|
| 336 |
+
("How many times does the digit 9 appear between 1 and 100?",
|
| 337 |
+
"20", "9,19,29,39,49,59,69,79,89 (nine times in ones) + 90-99 (ten times in tens, plus 99 double-counts). Total: 20."),
|
| 338 |
+
("If you overtake the last person in a race, what position are you in?",
|
| 339 |
+
"impossible", "You can't overtake the last person — if you're behind them, YOU are last."),
|
| 340 |
+
("A bat and ball cost $1.10 total. The bat costs $1 more than the ball. How much is the ball?",
|
| 341 |
+
"5", "Ball = x, Bat = x+1.00. Total: 2x+1.00=1.10. x=0.05. Ball costs $0.05 or 5 cents."),
|
| 342 |
+
("If 5 machines take 5 minutes to make 5 widgets, how long for 100 machines to make 100 widgets?",
|
| 343 |
+
"5", "Each machine makes 1 widget in 5 minutes. 100 machines make 100 widgets in 5 minutes."),
|
| 344 |
+
("You're in a dark room with a candle, a wood stove, and a gas lamp. You have one match. What do you light first?",
|
| 345 |
+
"match", "You light the match first — you need it to light anything else."),
|
| 346 |
+
("Is it legal for a man to marry his widow's sister?",
|
| 347 |
+
"no", "If he has a widow, he's dead. Dead people can't marry."),
|
| 348 |
+
]
|
| 349 |
+
for _ in range(cfg.num_hard_tricks):
|
| 350 |
+
q, a, hint = random.choice(tricks)
|
| 351 |
+
problems.append({
|
| 352 |
+
"question": q, "answer": a, "type": "hard_trick",
|
| 353 |
+
"think_hint": hint
|
| 354 |
+
})
|
| 355 |
+
|
| 356 |
+
# --- CODE REASONING (no actual code execution, just logic) ---
|
| 357 |
+
code_problems = [
|
| 358 |
+
("What does this print?\nx = 5\nx = x + 3\nprint(x)", "8",
|
| 359 |
+
"x starts at 5, then x = 5+3 = 8. Prints 8."),
|
| 360 |
+
("What does this print?\nfor i in range(3): print(i)\n(List all numbers printed)", "0 1 2",
|
| 361 |
+
"range(3) gives 0, 1, 2."),
|
| 362 |
+
("What's wrong with this code?\ndef add(a, b):\n return a - b", "subtracts instead of adds",
|
| 363 |
+
"Function named 'add' but uses minus operator."),
|
| 364 |
+
("What does this evaluate to?\nTrue and False or True", "true",
|
| 365 |
+
"(True and False) = False. False or True = True."),
|
| 366 |
+
("After this code runs, what is x?\nx = [1,2,3]\ny = x\ny.append(4)\nprint(len(x))", "4",
|
| 367 |
+
"y = x makes y point to same list. Appending to y also changes x. len(x) = 4."),
|
| 368 |
+
]
|
| 369 |
+
for _ in range(cfg.num_hard_coding):
|
| 370 |
+
q, a, hint = random.choice(code_problems)
|
| 371 |
+
problems.append({
|
| 372 |
+
"question": q, "answer": a.lower(), "type": "hard_coding",
|
| 373 |
+
"think_hint": hint
|
| 374 |
+
})
|
| 375 |
+
|
| 376 |
+
random.shuffle(problems)
|
| 377 |
+
print(f" Generated {len(problems)} hard problems")
|
| 378 |
+
print(f" Math: {cfg.num_hard_math}, Logic: {cfg.num_hard_logic + cfg.num_hard_logic//2}, "
|
| 379 |
+
f"Tricks: {cfg.num_hard_tricks}, Code: {cfg.num_hard_coding}")
|
| 380 |
+
|
| 381 |
return problems
|
| 382 |
|
| 383 |
|
| 384 |
# ============================================================
|
| 385 |
+
# STEP 3: GENERATE & SCORE WITH COT
|
| 386 |
# ============================================================
|
| 387 |
|
| 388 |
+
def build_cot_prompt(question: str) -> str:
|
| 389 |
+
"""Build prompt that encourages chain-of-thought reasoning."""
|
| 390 |
+
return (
|
| 391 |
+
f"<|im_start|>user\n{question}\n"
|
| 392 |
+
f"Think step by step before giving your final answer.<|im_end|>\n"
|
| 393 |
+
f"<|im_start|>assistant\n<think>\n"
|
| 394 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
|
| 397 |
+
def generate_and_score(model, tokenizer, problems, cfg, eos_id, resume_from=0):
|
| 398 |
"""
|
| 399 |
+
Generate CoT candidates, score them, return winning pairs.
|
| 400 |
+
Saves progress every batch_save_every problems.
|
| 401 |
"""
|
| 402 |
+
print(f"\n=== STEP 3: GENERATE & SCORE ({len(problems)} problems, {cfg.num_candidates} candidates) ===")
|
| 403 |
+
gen_start = time.time()
|
| 404 |
+
|
| 405 |
+
progress_path = Path(cfg.progress_file)
|
| 406 |
+
progress_path.parent.mkdir(parents=True, exist_ok=True)
|
| 407 |
+
|
| 408 |
+
winning_pairs = []
|
| 409 |
+
losing_pairs = []
|
| 410 |
+
|
| 411 |
+
if resume_from > 0 and progress_path.exists():
|
| 412 |
+
with open(progress_path) as f:
|
| 413 |
+
saved = json.load(f)
|
| 414 |
+
winning_pairs = saved.get("winning_pairs", [])
|
| 415 |
+
losing_pairs = saved.get("losing_pairs", [])
|
| 416 |
+
print(f" Resumed from problem {resume_from}, {len(winning_pairs)} winners loaded")
|
| 417 |
|
|
|
|
| 418 |
total_correct = 0
|
| 419 |
+
total_attempted = resume_from
|
| 420 |
|
| 421 |
+
for i in range(resume_from, len(problems)):
|
| 422 |
+
prob = problems[i]
|
| 423 |
+
prompt = build_cot_prompt(prob["question"])
|
| 424 |
ids = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 425 |
|
| 426 |
+
correct_responses = []
|
| 427 |
+
wrong_responses = []
|
| 428 |
+
|
| 429 |
for _ in range(cfg.num_candidates):
|
| 430 |
out = model.generate(
|
| 431 |
**ids, max_new_tokens=cfg.max_gen_tokens,
|
| 432 |
do_sample=True, temperature=cfg.temperature,
|
| 433 |
eos_token_id=eos_id
|
| 434 |
)
|
| 435 |
+
response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
|
|
|
|
|
|
|
| 436 |
|
| 437 |
+
# Use verify_key if it exists (trick questions)
|
| 438 |
+
check_prob = dict(prob)
|
| 439 |
+
if "verify_key" in check_prob:
|
| 440 |
+
check_prob["answer"] = check_prob["verify_key"]
|
| 441 |
+
score = smart_verify(check_prob, response)
|
| 442 |
|
| 443 |
+
if score > 0:
|
| 444 |
+
correct_responses.append(response)
|
| 445 |
+
else:
|
| 446 |
+
wrong_responses.append(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
+
total_attempted += 1
|
|
|
|
|
|
|
| 449 |
|
| 450 |
+
if correct_responses:
|
| 451 |
+
total_correct += 1
|
| 452 |
+
best = min(correct_responses, key=len) # Shortest correct = most concise
|
| 453 |
+
chat = (
|
| 454 |
+
f"<|im_start|>user\n{prob['question']}\n"
|
| 455 |
+
f"Think step by step before giving your final answer.<|im_end|>\n"
|
| 456 |
+
f"<|im_start|>assistant\n{best}<|im_end|>"
|
| 457 |
+
)
|
| 458 |
+
winning_pairs.append(chat)
|
| 459 |
|
| 460 |
+
if wrong_responses:
|
| 461 |
+
worst = max(wrong_responses, key=len)
|
| 462 |
+
losing_pairs.append(
|
| 463 |
+
f"<|im_start|>user\n{prob['question']}<|im_end|>\n"
|
| 464 |
+
f"<|im_start|>assistant\n{worst}<|im_end|>"
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
# Progress
|
| 468 |
+
if (i + 1) % cfg.batch_save_every == 0 or i == len(problems) - 1:
|
| 469 |
+
pct = total_correct / max(1, total_attempted) * 100
|
| 470 |
+
elapsed = time.time() - gen_start
|
| 471 |
+
rate = (i + 1 - resume_from) / max(1, elapsed)
|
| 472 |
+
eta = (len(problems) - i - 1) / max(0.001, rate) / 60
|
| 473 |
+
print(f" [{i+1}/{len(problems)}] Correct: {total_correct}/{total_attempted} ({pct:.0f}%) "
|
| 474 |
+
f"| Winners: {len(winning_pairs)} | ETA: {eta:.0f}min")
|
| 475 |
+
|
| 476 |
+
with open(progress_path, "w") as f:
|
| 477 |
+
json.dump({
|
| 478 |
+
"winning_pairs": winning_pairs,
|
| 479 |
+
"losing_pairs": losing_pairs,
|
| 480 |
+
"problems_completed": i + 1,
|
| 481 |
+
"total_correct": total_correct,
|
| 482 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 483 |
+
}, f)
|
| 484 |
+
|
| 485 |
+
if torch.cuda.is_available():
|
| 486 |
+
torch.cuda.empty_cache()
|
| 487 |
+
|
| 488 |
+
fail_rate = 100 - (total_correct / max(1, total_attempted) * 100)
|
| 489 |
+
print(f"\n FINAL: {total_correct}/{total_attempted} correct "
|
| 490 |
+
f"({total_correct/max(1,total_attempted)*100:.0f}%)")
|
| 491 |
+
print(f" Failure rate: {fail_rate:.0f}% "
|
| 492 |
+
f"({'GOOD — enough contrast for learning' if 15 < fail_rate < 60 else 'WARNING — might be too easy or too hard'})")
|
| 493 |
+
print(f" Winners: {len(winning_pairs)} | Losers: {len(losing_pairs)}")
|
| 494 |
+
|
| 495 |
+
return winning_pairs, losing_pairs
|
| 496 |
|
| 497 |
|
| 498 |
# ============================================================
|
| 499 |
+
# STEP 4: TRAIN — SFT on winning CoT chains
|
| 500 |
# ============================================================
|
| 501 |
|
| 502 |
def train_on_winners(model, tokenizer, winning_pairs, cfg):
|
| 503 |
"""
|
| 504 |
+
Train on correct chain-of-thought responses (STaR approach).
|
| 505 |
+
Targets layers 16-28 for reasoning improvement.
|
| 506 |
"""
|
| 507 |
+
print(f"\n=== STEP 4: TRAIN ON WINNERS ({len(winning_pairs)} chains) ===")
|
| 508 |
|
| 509 |
+
if len(winning_pairs) < 20:
|
| 510 |
print(" Too few winning pairs — skipping training")
|
| 511 |
return model
|
| 512 |
|
|
|
|
| 514 |
from transformers import TrainingArguments, Trainer
|
| 515 |
from torch.utils.data import Dataset
|
| 516 |
|
| 517 |
+
# Parse target layers
|
| 518 |
+
layer_start, layer_end = map(int, cfg.target_layers.split("-"))
|
| 519 |
+
target_layers = list(range(layer_start, layer_end + 1))
|
| 520 |
+
|
| 521 |
lora_config = LoraConfig(
|
| 522 |
r=cfg.lora_r, lora_alpha=cfg.lora_alpha, lora_dropout=0.0,
|
| 523 |
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
| 524 |
"gate_proj", "up_proj", "down_proj"],
|
| 525 |
+
layers_to_transform=target_layers,
|
| 526 |
bias="none", task_type=TaskType.CAUSAL_LM,
|
| 527 |
)
|
| 528 |
model = get_peft_model(model, lora_config)
|
| 529 |
model.print_trainable_parameters()
|
| 530 |
|
| 531 |
+
class CoTDataset(Dataset):
|
| 532 |
+
def __init__(self, texts, tokenizer, max_len=1024):
|
| 533 |
self.data = []
|
| 534 |
for t in texts:
|
| 535 |
e = tokenizer(t, truncation=True, max_length=max_len,
|
|
|
|
| 542 |
def __len__(self): return len(self.data)
|
| 543 |
def __getitem__(self, i): return self.data[i]
|
| 544 |
|
| 545 |
+
dataset = CoTDataset(winning_pairs, tokenizer)
|
| 546 |
|
| 547 |
out_dir = Path(cfg.output_dir) / "train_output"
|
| 548 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 549 |
|
| 550 |
total_steps = (len(dataset) * cfg.train_epochs) // (cfg.train_batch * cfg.train_grad_accum)
|
| 551 |
+
print(f" Steps: ~{total_steps} | Target layers: {cfg.target_layers}")
|
| 552 |
|
| 553 |
args = TrainingArguments(
|
| 554 |
output_dir=str(out_dir),
|
|
|
|
| 557 |
gradient_accumulation_steps=cfg.train_grad_accum,
|
| 558 |
learning_rate=cfg.learning_rate,
|
| 559 |
bf16=True,
|
| 560 |
+
logging_steps=max(1, total_steps // 20),
|
| 561 |
save_strategy="steps",
|
| 562 |
+
save_steps=cfg.save_steps,
|
| 563 |
+
save_total_limit=2,
|
| 564 |
warmup_ratio=0.05,
|
| 565 |
lr_scheduler_type="cosine",
|
| 566 |
optim="adamw_torch",
|
|
|
|
| 572 |
train_dataset=dataset, args=args
|
| 573 |
)
|
| 574 |
|
| 575 |
+
print(" Training...")
|
| 576 |
trainer.train()
|
| 577 |
|
|
|
|
| 578 |
print(" Merging LoRA...")
|
| 579 |
merged = model.merge_and_unload()
|
| 580 |
gc.collect()
|
| 581 |
|
|
|
|
| 582 |
shutil.rmtree(str(out_dir), ignore_errors=True)
|
| 583 |
+
gc.collect()
|
| 584 |
|
| 585 |
return merged
|
| 586 |
|
| 587 |
|
| 588 |
# ============================================================
|
| 589 |
+
# STEP 5: BENCHMARK
|
| 590 |
# ============================================================
|
| 591 |
|
| 592 |
def benchmark(model, tokenizer, eos_id):
|
| 593 |
+
"""Comprehensive reasoning benchmark."""
|
| 594 |
print("\n=== STEP 5: BENCHMARK ===")
|
|
|
|
| 595 |
results = {}
|
| 596 |
|
| 597 |
+
def ask(prompt, use_cot=False):
|
| 598 |
+
if use_cot:
|
| 599 |
+
p = build_cot_prompt(prompt)
|
| 600 |
+
else:
|
| 601 |
+
p = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
|
| 602 |
ids = tokenizer(p, return_tensors="pt").to(model.device)
|
| 603 |
+
out = model.generate(**ids, max_new_tokens=500, do_sample=False, eos_token_id=eos_id)
|
| 604 |
return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
|
| 605 |
|
| 606 |
+
# Basic math
|
| 607 |
+
math_tests = [("7+8", "15"), ("123+456", "579"), ("1000-387", "613"),
|
| 608 |
+
("12*13", "156"), ("144/12", "12")]
|
| 609 |
+
mc = sum(1 for q, e in math_tests
|
| 610 |
+
if smart_verify({"answer": e}, ask(f"What is {q}? Give just the number.")) > 0)
|
| 611 |
+
results["math"] = f"{mc}/5"
|
| 612 |
+
print(f" Math: {mc}/5")
|
| 613 |
+
|
| 614 |
+
# Hard math (the stuff GRPO couldn't handle)
|
| 615 |
+
hard_math = [
|
| 616 |
+
("A stock is worth $200. It goes up 25%, then drops 20%. What is it worth?", "200"),
|
| 617 |
+
("You have 50 cookies. You eat 1/5, give 10 away, then split the rest equally among 4 friends. How many each?", "7"),
|
| 618 |
+
("Car A drives 60mph for 3 hours. Car B drives 80mph for 2 hours. How much further did Car A go?", "20"),
|
| 619 |
+
]
|
| 620 |
+
hmc = 0
|
| 621 |
+
for q, e in hard_math:
|
| 622 |
+
a = ask(q, use_cot=True)
|
| 623 |
+
correct = smart_verify({"answer": e}, a) > 0
|
| 624 |
+
hmc += correct
|
| 625 |
+
status = "OK" if correct else "FAIL"
|
| 626 |
+
answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
|
| 627 |
+
print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
|
| 628 |
+
results["hard_math"] = f"{hmc}/3"
|
| 629 |
+
print(f" Hard math: {hmc}/3")
|
| 630 |
+
|
| 631 |
+
# Reasoning with CoT
|
| 632 |
reason_tests = [
|
|
|
|
| 633 |
("Which is heavier: a pound of feathers or a pound of bricks?", "same"),
|
| 634 |
("If yesterday was Monday, what day is tomorrow?", "wednesday"),
|
| 635 |
("A farmer has 17 sheep. All but 9 die. How many are left?", "9"),
|
| 636 |
+
("If you're running and pass the person in 2nd place, what position are you in?", "2nd"),
|
| 637 |
+
("How many times can you subtract 5 from 25?", "once"),
|
| 638 |
+
("All mammals breathe air. Whales are mammals. Do whales breathe air?", "yes"),
|
| 639 |
+
("How many months have 28 days?", "all"),
|
| 640 |
+
("If A > B and B > C, who is smallest?", "c"),
|
| 641 |
+
("A bat and ball cost $1.10. The bat costs $1 more than the ball. How much is the ball?", "5"),
|
| 642 |
+
("5 machines take 5 min to make 5 widgets. How long for 100 machines to make 100 widgets?", "5"),
|
| 643 |
]
|
| 644 |
+
rc = 0
|
| 645 |
for q, expected in reason_tests:
|
| 646 |
+
a = ask(q, use_cot=True)
|
| 647 |
+
correct = smart_verify({"answer": expected}, a) > 0
|
| 648 |
+
rc += correct
|
| 649 |
+
status = "OK" if correct else "FAIL"
|
| 650 |
+
answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
|
| 651 |
+
print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
|
| 652 |
+
results["reasoning"] = f"{rc}/10"
|
| 653 |
+
print(f" Reasoning: {rc}/10")
|
| 654 |
+
|
| 655 |
+
# Logic
|
| 656 |
+
logic_tests = [
|
| 657 |
+
("If it rains, the ground is wet. The ground is wet. Did it rain?", "no"),
|
| 658 |
+
("None of the winners cheated. Tom didn't cheat. Did Tom win?", "no"),
|
| 659 |
+
("If A then B. If B then C. A is true. Is C true?", "yes"),
|
| 660 |
]
|
| 661 |
+
lc = 0
|
| 662 |
+
for q, expected in logic_tests:
|
| 663 |
+
a = ask(q, use_cot=True)
|
| 664 |
+
correct = smart_verify({"answer": expected}, a) > 0
|
| 665 |
+
lc += correct
|
| 666 |
+
status = "OK" if correct else "FAIL"
|
| 667 |
+
answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
|
| 668 |
+
print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
|
| 669 |
+
results["logic"] = f"{lc}/3"
|
| 670 |
+
print(f" Logic: {lc}/3")
|
| 671 |
|
| 672 |
# Perplexity
|
| 673 |
test_text = "The quick brown fox jumps over the lazy dog. Machine learning models can process natural language."
|
| 674 |
enc = tokenizer(test_text, return_tensors="pt").to(model.device)
|
| 675 |
with torch.no_grad():
|
| 676 |
loss = model(**enc, labels=enc.input_ids).loss.item()
|
| 677 |
+
results["perplexity"] = f"{math.exp(loss):.2f}"
|
| 678 |
+
print(f" Perplexity: {results['perplexity']}")
|
|
|
|
| 679 |
|
| 680 |
return results
|
| 681 |
|
| 682 |
|
| 683 |
# ============================================================
|
| 684 |
+
# MAIN
|
| 685 |
# ============================================================
|
| 686 |
|
| 687 |
def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
|
| 688 |
+
"""Run one complete self-improvement cycle."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
if cfg is None:
|
| 690 |
cfg = SelfImproveConfig()
|
| 691 |
|
|
|
|
| 696 |
print(f"Started: {time.strftime('%H:%M:%S')}")
|
| 697 |
print("=" * 60)
|
| 698 |
|
| 699 |
+
# Check for resume
|
| 700 |
+
progress_path = Path(cfg.progress_file)
|
| 701 |
+
resume_from = 0
|
| 702 |
+
if progress_path.exists():
|
| 703 |
+
with open(progress_path) as f:
|
| 704 |
+
saved = json.load(f)
|
| 705 |
+
resume_from = saved.get("problems_completed", 0)
|
| 706 |
+
|
| 707 |
# Load model
|
| 708 |
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
|
|
|
| 709 |
print("\nLoading model...")
|
| 710 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 711 |
cfg.model_path, dtype=torch.bfloat16,
|
|
|
|
| 714 |
tokenizer = AutoTokenizer.from_pretrained(cfg.model_path, trust_remote_code=True)
|
| 715 |
eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
|
| 716 |
|
| 717 |
+
# Pre-benchmark
|
| 718 |
+
print("\n--- PRE-TRAINING BENCHMARK ---")
|
| 719 |
+
pre_results = benchmark(model, tokenizer, eos_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
+
# Step 1: Diagnose
|
| 722 |
+
diagnosis_path = Path(cfg.output_dir) / "diagnosis.json"
|
| 723 |
+
diagnosis_path.parent.mkdir(parents=True, exist_ok=True)
|
| 724 |
+
weaknesses = diagnose_weaknesses(model, tokenizer, eos_id, save_path=str(diagnosis_path))
|
| 725 |
+
|
| 726 |
+
# Step 2: Generate hard problems
|
| 727 |
+
print("\n=== STEP 2: GENERATING HARD PROBLEMS ===")
|
| 728 |
+
random.seed(42 + cycle_num) # Different problems each cycle
|
| 729 |
+
problems = generate_hard_problems(cfg)
|
| 730 |
+
|
| 731 |
+
# Step 3: Generate & score
|
| 732 |
+
if resume_from < len(problems):
|
| 733 |
+
winning_pairs, losing_pairs = generate_and_score(
|
| 734 |
+
model, tokenizer, problems, cfg, eos_id, resume_from=resume_from
|
| 735 |
+
)
|
| 736 |
+
else:
|
| 737 |
+
with open(progress_path) as f:
|
| 738 |
+
saved = json.load(f)
|
| 739 |
+
winning_pairs = saved["winning_pairs"]
|
| 740 |
+
losing_pairs = saved.get("losing_pairs", [])
|
| 741 |
|
| 742 |
+
# Step 4: Train
|
| 743 |
model = train_on_winners(model, tokenizer, winning_pairs, cfg)
|
| 744 |
|
| 745 |
# Save improved model
|
| 746 |
+
save_dir = Path(cfg.output_dir) / f"cycle_{cycle_num}"
|
| 747 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 748 |
+
print(f"\nSaving improved model to {save_dir}...")
|
| 749 |
+
model.save_pretrained(str(save_dir), safe_serialization=True)
|
| 750 |
+
tokenizer.save_pretrained(str(save_dir))
|
| 751 |
+
sz = sum(f.stat().st_size for f in save_dir.glob("*.safetensors")) / 1e9
|
| 752 |
+
print(f"SAVED: {save_dir} ({sz:.1f} GB)")
|
| 753 |
+
|
| 754 |
+
# Step 5: Post-benchmark
|
| 755 |
+
print("\n--- POST-TRAINING BENCHMARK ---")
|
| 756 |
+
post_results = benchmark(model, tokenizer, eos_id)
|
| 757 |
+
|
| 758 |
+
# Compare & save report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
elapsed = (time.time() - start) / 60
|
| 760 |
print(f"\n{'=' * 60}")
|
| 761 |
print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min")
|
| 762 |
+
print(f"PRE: {pre_results}")
|
| 763 |
+
print(f"POST: {post_results}")
|
| 764 |
+
print(f"Model: {save_dir}")
|
| 765 |
print(f"{'=' * 60}")
|
| 766 |
|
| 767 |
+
report = {
|
| 768 |
+
"cycle": cycle_num,
|
| 769 |
+
"pre_benchmark": pre_results,
|
| 770 |
+
"post_benchmark": post_results,
|
| 771 |
+
"problems_total": len(problems),
|
| 772 |
+
"winning_pairs": len(winning_pairs),
|
| 773 |
+
"losing_pairs": len(losing_pairs),
|
| 774 |
+
"duration_min": elapsed,
|
| 775 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 776 |
+
}
|
| 777 |
+
with open(save_dir / "cycle_report.json", "w") as f:
|
| 778 |
+
json.dump(report, f, indent=2)
|
| 779 |
+
|
| 780 |
+
return str(save_dir)
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
if __name__ == "__main__":
|
| 784 |
+
run_cycle()
|