td-builder commited on
Commit
494bef0
·
verified ·
1 Parent(s): c98555a

Upload 149 files

Browse files
Files changed (1) hide show
  1. hugging/td_fuse/selfimprove.py +716 -639
hugging/td_fuse/selfimprove.py CHANGED
@@ -1,534 +1,629 @@
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
27
  import time
28
  import json
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.
81
- """
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
 
 
513
  from peft import LoraConfig, get_peft_model, TaskType
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:
@@ -542,13 +637,13 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
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),
@@ -572,212 +667,194 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
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
 
692
  start = time.time()
693
  print("=" * 60)
694
- print(f"TD SELF-IMPROVEMENT — CYCLE {cycle_num}")
 
695
  print(f"Model: {cfg.model_path}")
 
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,
712
- device_map="auto", trust_remote_code=True
713
- )
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__":
 
1
  """
2
+ TD Self-Improvement v4 — Full Weight Intelligence + Targeted LoRA
3
+
4
+ NOT just fixing merge damage. Analyzes EVERYTHING about every layer:
5
+
6
+ 1. STANDALONE HEALTH — No reference needed:
7
+ - Distribution quality (kurtosis, skew — are weights well-shaped?)
8
+ - Spectral efficiency (effective rank vs max rank — is capacity wasted?)
9
+ - Conditioning (condition number — are gradients flowing well?)
10
+ - Sparsity patterns (dead neurons, over-sparse layers)
11
+ - Layer coherence (sudden norm jumps between adjacent layers)
12
+ - Weight magnitude balance (are norms proportional to layer depth?)
13
+
14
+ 2. REFERENCE COMPARISON — What changed from the merge:
15
+ - Norm drift, variance shift, distribution shape change
16
+ - This is just ONE signal among many
17
+
18
+ 3. IMPROVEMENT SCORING — Every layer gets a score:
19
+ - How much COULD this layer be improved?
20
+ - Combines standalone health + reference damage
21
+ - Ranks ALL layers, targets the ones with most room to grow
22
+
23
+ 4. LoRA IMPROVEMENT — Train the layers with most potential:
24
+ - Diverse data: text, math, code, reasoning, conversation
25
+ - Targets layers ranked by improvement potential
26
+
27
+ From test_18: LoRA/DoRA with layers_to_transform, layers 16-28 for reasoning.
28
+ But we don't limit — we go wherever the analysis says.
29
  """
30
 
31
  import torch
32
  import time
33
  import json
34
  import math
 
35
  import gc
 
36
  import shutil
37
+ import numpy as np
38
  from pathlib import Path
39
+ from typing import List, Dict, Tuple, Optional
40
+ from dataclasses import dataclass, field
41
 
42
 
43
  @dataclass
44
  class SelfImproveConfig:
45
+ """Configuration for full self-improvement."""
46
  model_path: str = "td_fuse_outputs/reasoning_healed"
47
+ reference_model: str = "Qwen/Qwen3-VL-8B-Instruct"
48
  output_dir: str = "td_fuse_outputs/self_improve"
 
 
 
 
 
 
49
 
50
+ # How many layers to target (by improvement potential)
51
+ top_n_layers: int = 16 # Target more layers — we're improving, not just fixing
52
+ min_improvement_score: float = 0.05 # Floor to be worth targeting
 
 
 
53
 
54
  # Training
55
+ lora_r: int = 32
56
  lora_alpha: int = 64
57
  train_epochs: int = 3
58
  train_batch: int = 2
59
+ train_grad_accum: int = 8
60
+ learning_rate: float = 1.5e-5
61
  save_steps: int = 50
62
+ num_train_samples: int = 3000 # More diverse data for general improvement
 
63
 
64
+ # Weights for improvement scoring (standalone health vs reference damage)
65
+ w_health: float = 0.55 # Standalone health issues matter most
66
+ w_damage: float = 0.25 # Merge damage
67
+ w_coherence: float = 0.20 # Layer-to-layer coherence
68
 
69
 
70
  # ============================================================
71
+ # STEP 1: DEEP WEIGHT ANALYSIS — Every layer, every metric
72
  # ============================================================
73
 
74
+ def analyze_layer(name: str, tensor: torch.Tensor) -> Dict:
75
+ """Full health analysis for a single weight tensor."""
76
+ t = tensor.float()
77
+
78
+ stats = {
79
+ "name": name,
80
+ "shape": list(tensor.shape),
81
+ "params": tensor.numel(),
82
+ "dtype": str(tensor.dtype),
83
+ # Basic stats
84
+ "l2_norm": t.norm(2).item(),
85
+ "frobenius_norm": t.norm('fro').item() if t.dim() >= 2 else t.norm(2).item(),
86
+ "mean": t.mean().item(),
87
+ "std": t.std().item(),
88
+ "min": t.min().item(),
89
+ "max": t.max().item(),
90
+ "abs_mean": t.abs().mean().item(),
91
+ # Distribution shape
92
+ "sparsity": (t.abs() < 1e-6).float().mean().item(),
93
+ "kurtosis": ((t - t.mean()) / max(t.std().item(), 1e-8)).pow(4).mean().item() - 3,
94
+ "skewness": ((t - t.mean()) / max(t.std().item(), 1e-8)).pow(3).mean().item(),
95
+ # Dead/saturated neurons
96
+ "near_zero_pct": (t.abs() < 1e-5).float().mean().item(),
97
+ "extreme_pct": (t.abs() > t.abs().mean() * 5).float().mean().item(),
98
+ }
99
+
100
+ # For 2D matrices: spectral analysis
101
+ if t.dim() == 2 and min(t.shape) >= 2:
102
+ try:
103
+ rows = min(512, t.shape[0])
104
+ cols = min(512, t.shape[1])
105
+ sample = t[:rows, :cols]
106
+
107
+ svd = torch.linalg.svdvals(sample)
108
+ stats["sv_max"] = svd[0].item()
109
+ stats["sv_min"] = svd[-1].item()
110
+ stats["sv_ratio"] = svd[0].item() / max(svd[-1].item(), 1e-10)
111
+
112
+ # Effective rank — how much capacity is actually used
113
+ sv_norm = svd / svd.sum()
114
+ entropy = -(sv_norm * torch.log(sv_norm + 1e-10)).sum()
115
+ stats["sv_effective_rank"] = torch.exp(entropy).item()
116
+ stats["sv_max_rank"] = float(min(sample.shape))
117
+ stats["sv_rank_utilization"] = stats["sv_effective_rank"] / stats["sv_max_rank"]
118
+
119
+ # Top singular value concentration — is too much info in few dimensions?
120
+ top5_energy = (svd[:5] ** 2).sum() / (svd ** 2).sum()
121
+ stats["sv_top5_energy"] = top5_energy.item()
122
+
123
+ # Stable rank (Frobenius^2 / spectral^2)
124
+ stats["stable_rank"] = (sample.norm('fro').item() ** 2) / max(svd[0].item() ** 2, 1e-10)
125
+
126
+ except Exception:
127
+ stats["sv_ratio"] = -1
128
+ stats["sv_rank_utilization"] = -1
129
+
130
+ return stats
131
+
132
 
133
+ def analyze_model_weights(model_path: str, label: str = "model") -> Dict:
134
+ """Load and analyze every language layer in the model."""
135
+ print(f"\n{'='*50}")
136
+ print(f"ANALYZING: {label}")
137
+ print(f"Path: {model_path}")
138
+ print(f"{'='*50}")
139
 
140
+ from safetensors.torch import load_file
141
+ import glob
142
+
143
+ st_files = sorted(glob.glob(f"{model_path}/*.safetensors"))
144
+ if not st_files:
145
+ raise FileNotFoundError(f"No safetensors files in {model_path}")
146
+
147
+ all_stats = {}
148
+ total_params = 0
149
+
150
+ for st_file in st_files:
151
+ print(f" Loading {Path(st_file).name}...")
152
+ state_dict = load_file(st_file, device="cpu")
153
+
154
+ for name, tensor in state_dict.items():
155
+ # Skip vision encoder — language model only
156
+ if any(skip in name for skip in ["visual.", "merger."]):
157
+ continue
158
+
159
+ stats = analyze_layer(name, tensor)
160
+ all_stats[name] = stats
161
+ total_params += stats["params"]
162
+
163
+ del state_dict
164
+ gc.collect()
165
+
166
+ print(f" Analyzed {len(all_stats)} tensors ({total_params/1e9:.2f}B params)")
167
+ return {"label": label, "layers": all_stats, "total_params": total_params}
168
 
169
 
170
  # ============================================================
171
+ # STEP 2: STANDALONE HEALTH SCORING — No reference needed
172
  # ============================================================
173
 
174
+ def compute_health_scores(analysis: Dict) -> Dict[str, Dict]:
175
  """
176
+ Score each layer's health based on its own stats.
177
+ Higher score = MORE room for improvement (worse health).
178
+
179
+ Checks:
180
+ - Distribution quality: Extreme kurtosis or skew = unhealthy
181
+ - Spectral efficiency: Low rank utilization = wasted capacity
182
+ - Conditioning: Very high condition number = bad gradient flow
183
+ - Dead neurons: High sparsity = parts of the layer doing nothing
184
+ - Extreme weights: Too many outliers = potential instability
185
  """
186
+ print("\n=== STANDALONE HEALTH ANALYSIS ===")
187
+
188
+ health_scores = {}
189
+
190
+ for name, stats in analysis["layers"].items():
191
+ issues = []
192
+ score = 0.0
193
+
194
+ # 1. Distribution quality — ideal kurtosis near 0 (normal), skew near 0
195
+ kurt = abs(stats["kurtosis"])
196
+ if kurt > 10:
197
+ score += 0.3
198
+ issues.append(f"extreme kurtosis ({kurt:.1f})")
199
+ elif kurt > 5:
200
+ score += 0.15
201
+ issues.append(f"high kurtosis ({kurt:.1f})")
202
+
203
+ skew = abs(stats["skewness"])
204
+ if skew > 2:
205
+ score += 0.15
206
+ issues.append(f"high skew ({skew:.2f})")
207
+
208
+ # 2. Spectral efficiency — are we using the layer's full capacity?
209
+ rank_util = stats.get("sv_rank_utilization", -1)
210
+ if rank_util > 0:
211
+ if rank_util < 0.1:
212
+ score += 0.3
213
+ issues.append(f"very low rank utilization ({rank_util:.2%})")
214
+ elif rank_util < 0.25:
215
+ score += 0.15
216
+ issues.append(f"low rank utilization ({rank_util:.2%})")
217
+
218
+ # Top-5 energy concentration — too much in few dimensions
219
+ top5 = stats.get("sv_top5_energy", 0)
220
+ if top5 > 0.9:
221
+ score += 0.2
222
+ issues.append(f"top-5 SVD concentration ({top5:.2%})")
223
+ elif top5 > 0.75:
224
+ score += 0.1
225
+
226
+ # 3. Conditioning — gradient flow health
227
+ sv_ratio = stats.get("sv_ratio", -1)
228
+ if sv_ratio > 0:
229
+ if sv_ratio > 10000:
230
+ score += 0.25
231
+ issues.append(f"extreme condition number ({sv_ratio:.0f})")
232
+ elif sv_ratio > 1000:
233
+ score += 0.1
234
+
235
+ # 4. Dead neurons — near-zero weights
236
+ dead = stats["near_zero_pct"]
237
+ if dead > 0.3:
238
+ score += 0.2
239
+ issues.append(f"high dead neuron % ({dead:.1%})")
240
+ elif dead > 0.15:
241
+ score += 0.1
242
+
243
+ # 5. Extreme weights — outlier instability
244
+ extreme = stats["extreme_pct"]
245
+ if extreme > 0.05:
246
+ score += 0.15
247
+ issues.append(f"many extreme weights ({extreme:.1%})")
248
+
249
+ # 6. Std too low (collapsed) or too high (exploded)
250
+ if stats["std"] < 1e-4:
251
+ score += 0.25
252
+ issues.append(f"near-zero variance ({stats['std']:.6f})")
253
+ elif stats["std"] > 1.0:
254
+ score += 0.15
255
+ issues.append(f"high variance ({stats['std']:.4f})")
256
+
257
+ health_scores[name] = {
258
+ "health_score": min(score, 1.0), # Cap at 1.0
259
+ "issues": issues,
260
+ "stats": stats,
261
+ }
262
+
263
+ return health_scores
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
 
266
  # ============================================================
267
+ # STEP 3: REFERENCE COMPARISON — Merge damage signal
268
  # ============================================================
269
 
270
+ def compute_damage_scores(model_analysis: Dict, ref_analysis: Dict) -> Dict[str, float]:
271
+ """Compare each layer to reference. Returns damage score per layer name."""
272
+ print("\n=== REFERENCE COMPARISON (merge damage) ===")
273
+
274
+ damages = {}
275
+ model_layers = model_analysis["layers"]
276
+ ref_layers = ref_analysis["layers"]
277
+ matched = 0
278
+
279
+ for name, m_stats in model_layers.items():
280
+ ref_key = None
281
+ for c in [name, name.replace("model.", "", 1), f"model.{name}"]:
282
+ if c in ref_layers:
283
+ ref_key = c
284
+ break
285
 
286
+ if ref_key is None:
287
+ continue
288
 
289
+ r_stats = ref_layers[ref_key]
290
+ matched += 1
291
+
292
+ norm_diff = abs(m_stats["l2_norm"] - r_stats["l2_norm"]) / max(r_stats["l2_norm"], 1e-8)
293
+ std_diff = abs(m_stats["std"] - r_stats["std"]) / max(r_stats["std"], 1e-8)
294
+ kurt_diff = abs(m_stats["kurtosis"] - r_stats["kurtosis"]) / max(abs(r_stats["kurtosis"]) + 1, 1e-8)
295
+ sparsity_diff = abs(m_stats["sparsity"] - r_stats["sparsity"])
296
+
297
+ sv_ratio_diff = 0
298
+ if "sv_ratio" in m_stats and "sv_ratio" in r_stats:
299
+ if m_stats["sv_ratio"] > 0 and r_stats["sv_ratio"] > 0:
300
+ sv_ratio_diff = abs(
301
+ math.log(m_stats["sv_ratio"] + 1) - math.log(r_stats["sv_ratio"] + 1)
302
+ ) / max(math.log(r_stats["sv_ratio"] + 1), 1e-8)
303
+
304
+ combined = (0.30 * norm_diff + 0.25 * std_diff + 0.20 * kurt_diff +
305
+ 0.15 * sv_ratio_diff + 0.10 * sparsity_diff)
306
+ damages[name] = combined
307
+
308
+ print(f" Matched {matched} layers between model and reference")
309
+ return damages
310
+
311
+
312
+ # ============================================================
313
+ # STEP 4: LAYER COHERENCE — Adjacent layer smoothness
314
+ # ============================================================
315
+
316
+ def compute_coherence_scores(analysis: Dict) -> Dict[int, float]:
317
  """
318
+ Check layer-to-layer coherence.
319
+ Big jumps in norms/variance between adjacent layers = poor coherence.
320
+ Returns coherence issue score per layer number.
321
  """
322
+ print("\n=== LAYER COHERENCE ANALYSIS ===")
323
+
324
+ # Group stats by layer number
325
+ layer_norms = {}
326
+ layer_stds = {}
327
+
328
+ for name, stats in analysis["layers"].items():
329
+ parts = name.split(".")
330
+ layer_num = None
331
+ for j, part in enumerate(parts):
332
+ if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit():
333
+ layer_num = int(parts[j+1])
334
+ break
335
+
336
+ if layer_num is not None:
337
+ if layer_num not in layer_norms:
338
+ layer_norms[layer_num] = []
339
+ layer_stds[layer_num] = []
340
+ layer_norms[layer_num].append(stats["l2_norm"])
341
+ layer_stds[layer_num].append(stats["std"])
342
+
343
+ # Average norm per layer
344
+ avg_norms = {l: sum(ns)/len(ns) for l, ns in layer_norms.items()}
345
+ avg_stds = {l: sum(ss)/len(ss) for l, ss in layer_stds.items()}
346
+
347
+ sorted_layers = sorted(avg_norms.keys())
348
+ coherence_issues = {}
349
+
350
+ for i in range(len(sorted_layers)):
351
+ l = sorted_layers[i]
352
+ score = 0.0
353
+
354
+ # Check jump from previous layer
355
+ if i > 0:
356
+ prev = sorted_layers[i-1]
357
+ norm_jump = abs(avg_norms[l] - avg_norms[prev]) / max(avg_norms[prev], 1e-8)
358
+ std_jump = abs(avg_stds[l] - avg_stds[prev]) / max(avg_stds[prev], 1e-8)
359
+ if norm_jump > 0.5:
360
+ score += norm_jump * 0.5
361
+ if std_jump > 0.5:
362
+ score += std_jump * 0.3
363
+
364
+ # Check jump to next layer
365
+ if i < len(sorted_layers) - 1:
366
+ nxt = sorted_layers[i+1]
367
+ norm_jump = abs(avg_norms[l] - avg_norms[nxt]) / max(avg_norms[nxt], 1e-8)
368
+ if norm_jump > 0.5:
369
+ score += norm_jump * 0.3
370
+
371
+ coherence_issues[l] = min(score, 1.0)
372
+
373
+ return coherence_issues
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
 
376
  # ============================================================
377
+ # STEP 5: COMBINE — Rank layers by improvement potential
378
  # ============================================================
379
 
380
+ def rank_layers_for_improvement(
381
+ health_scores: Dict[str, Dict],
382
+ damage_scores: Dict[str, float],
383
+ coherence_scores: Dict[int, float],
384
+ cfg: SelfImproveConfig
385
+ ) -> Tuple[List[int], Dict]:
386
  """
387
+ Combine all signals into a single improvement potential score per layer.
388
+ Returns sorted list of layer numbers to target + full report.
389
  """
390
+ print("\n=== RANKING LAYERS BY IMPROVEMENT POTENTIAL ===")
391
+
392
+ # Aggregate per transformer layer number
393
+ layer_health = {} # layer_num -> list of health scores
394
+ layer_damage = {} # layer_num -> list of damage scores
395
+
396
+ for name, h in health_scores.items():
397
+ parts = name.split(".")
398
+ layer_num = None
399
+ for j, part in enumerate(parts):
400
+ if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit():
401
+ layer_num = int(parts[j+1])
402
+ break
403
+
404
+ if layer_num is not None:
405
+ if layer_num not in layer_health:
406
+ layer_health[layer_num] = []
407
+ layer_damage[layer_num] = []
408
+ layer_health[layer_num].append(h["health_score"])
409
+ if name in damage_scores:
410
+ layer_damage[layer_num].append(damage_scores[name])
411
+
412
+ # Combined score per layer
413
+ layer_scores = {}
414
+ layer_details = {}
415
+
416
+ for layer_num in layer_health:
417
+ avg_health = sum(layer_health[layer_num]) / len(layer_health[layer_num])
418
+ avg_damage = (sum(layer_damage[layer_num]) / len(layer_damage[layer_num])
419
+ if layer_damage.get(layer_num) else 0.0)
420
+ coherence = coherence_scores.get(layer_num, 0.0)
421
+
422
+ combined = (cfg.w_health * avg_health +
423
+ cfg.w_damage * avg_damage +
424
+ cfg.w_coherence * coherence)
425
+
426
+ layer_scores[layer_num] = combined
427
+ layer_details[layer_num] = {
428
+ "health": avg_health,
429
+ "damage": avg_damage,
430
+ "coherence": coherence,
431
+ "combined": combined,
432
+ }
433
+
434
+ # Sort by improvement potential (highest first)
435
+ sorted_layers = sorted(layer_scores.items(), key=lambda x: x[1], reverse=True)
436
+
437
+ # Print ranking
438
+ print(f"\n {'Layer':>7} {'Combined':>9} {'Health':>8} {'Damage':>8} {'Coherence':>10} {'Status'}")
439
+ print(f" {'-'*65}")
440
+ for layer_num, score in sorted_layers:
441
+ d = layer_details[layer_num]
442
+ bar = "█" * int(score * 50)
443
+ target = " ← TARGET" if score >= cfg.min_improvement_score else ""
444
+ print(f" Layer {layer_num:>2} {score:>9.4f} {d['health']:>8.4f} {d['damage']:>8.4f} {d['coherence']:>10.4f} {bar}{target}")
445
+
446
+ # Pick top N layers above threshold
447
+ target_layers = []
448
+ for layer_num, score in sorted_layers:
449
+ if score >= cfg.min_improvement_score and len(target_layers) < cfg.top_n_layers:
450
+ target_layers.append(layer_num)
451
+
452
+ if not target_layers:
453
+ target_layers = [l for l, _ in sorted_layers[:cfg.top_n_layers]]
454
+ print(f"\n No layers above threshold {cfg.min_improvement_score}, using top {cfg.top_n_layers}")
455
+
456
+ target_layers.sort()
457
+ print(f"\n TARGET LAYERS FOR IMPROVEMENT: {target_layers}")
458
+
459
+ # Collect top issues across all targeted layers
460
+ all_issues = []
461
+ for name, h in health_scores.items():
462
+ parts = name.split(".")
463
+ layer_num = None
464
+ for j, part in enumerate(parts):
465
+ if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit():
466
+ layer_num = int(parts[j+1])
467
+ break
468
+ if layer_num in target_layers and h["issues"]:
469
+ for issue in h["issues"]:
470
+ all_issues.append(f" Layer {layer_num} {name.split('.')[-2]}: {issue}")
471
+
472
+ if all_issues:
473
+ print(f"\n Key issues found in target layers:")
474
+ for issue in all_issues[:30]:
475
+ print(f" {issue}")
476
+
477
+ return target_layers, layer_details
478
+
479
 
480
+ # ============================================================
481
+ # STEP 6: LoRA IMPROVEMENT — Train targeted layers
482
+ # ============================================================
483
+
484
+ def improve_with_lora(model_path: str, target_layers: List[int], cfg: SelfImproveConfig):
485
+ """
486
+ Apply LoRA training to layers with highest improvement potential.
487
+ Uses diverse high-quality data covering text, reasoning, code, and conversation.
488
+ """
489
+ print(f"\n{'='*50}")
490
+ print(f"LORA IMPROVEMENT — Targeting {len(target_layers)} layers: {target_layers}")
491
+ print(f"{'='*50}")
492
 
493
+ from transformers import AutoModelForImageTextToText, AutoTokenizer, TrainingArguments, Trainer
494
  from peft import LoraConfig, get_peft_model, TaskType
495
+ from datasets import load_dataset
496
  from torch.utils.data import Dataset
497
 
498
+ # Load model
499
+ print(" Loading model...")
500
+ model = AutoModelForImageTextToText.from_pretrained(
501
+ model_path, dtype=torch.bfloat16,
502
+ device_map="auto", trust_remote_code=True
503
+ )
504
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
505
 
506
+ # LoRA on target layers
507
  lora_config = LoraConfig(
508
+ r=cfg.lora_r,
509
+ lora_alpha=cfg.lora_alpha,
510
+ lora_dropout=0.0,
511
  target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
512
  "gate_proj", "up_proj", "down_proj"],
513
  layers_to_transform=target_layers,
514
+ bias="none",
515
+ task_type=TaskType.CAUSAL_LM,
516
  )
517
  model = get_peft_model(model, lora_config)
518
  model.print_trainable_parameters()
519
 
520
+ # Load DIVERSE training data — 5 categories for general improvement
521
+ print("\n Loading diverse training data...")
522
+ train_texts = []
523
+ per_category = cfg.num_train_samples // 5 # Equal split across 5 types
524
+
525
+ # Category 1: General text (language fluency)
526
+ try:
527
+ ds = load_dataset("neuralmagic/LLM_compression_calibration", split="train")
528
+ count = 0
529
+ for row in ds:
530
+ if count >= per_category:
531
+ break
532
+ text = row.get("text", "")
533
+ if len(text) > 100:
534
+ chat = (f"<|im_start|>user\nContinue this text naturally.<|im_end|>\n"
535
+ f"<|im_start|>assistant\n{text[:800]}<|im_end|>")
536
+ train_texts.append(chat)
537
+ count += 1
538
+ print(f" General text: {count}")
539
+ except Exception as e:
540
+ print(f" WARNING: General text failed: {e}")
541
+
542
+ # Category 2: Math reasoning (step-by-step)
543
+ try:
544
+ ds = load_dataset("openai/gsm8k", "main", split="train")
545
+ count = 0
546
+ for row in ds:
547
+ if count >= per_category:
548
+ break
549
+ q, a = row.get("question", ""), row.get("answer", "")
550
+ if q and a:
551
+ chat = (f"<|im_start|>user\n{q}\nThink step by step.<|im_end|>\n"
552
+ f"<|im_start|>assistant\n<think>\n{a}\n</think><|im_end|>")
553
+ train_texts.append(chat)
554
+ count += 1
555
+ print(f" Math reasoning: {count}")
556
+ except Exception as e:
557
+ print(f" WARNING: Math data failed: {e}")
558
+
559
+ # Category 3: Code generation
560
+ try:
561
+ ds = load_dataset("sahil2801/CodeAlpaca-20k", split="train")
562
+ count = 0
563
+ for row in ds:
564
+ if count >= per_category:
565
+ break
566
+ p, c = row.get("prompt", ""), row.get("completion", "")
567
+ if p and c:
568
+ chat = (f"<|im_start|>user\n{p}<|im_end|>\n"
569
+ f"<|im_start|>assistant\n{c}<|im_end|>")
570
+ train_texts.append(chat)
571
+ count += 1
572
+ print(f" Code: {count}")
573
+ except Exception as e:
574
+ print(f" WARNING: Code data failed: {e}")
575
+
576
+ # Category 4: Instruction following (diverse tasks)
577
+ try:
578
+ ds = load_dataset("yahma/alpaca-cleaned", split="train")
579
+ count = 0
580
+ for row in ds:
581
+ if count >= per_category:
582
+ break
583
+ inst = row.get("instruction", "")
584
+ inp = row.get("input", "")
585
+ out = row.get("output", "")
586
+ if inst and out:
587
+ prompt = f"{inst}\n{inp}" if inp else inst
588
+ chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n"
589
+ f"<|im_start|>assistant\n{out}<|im_end|>")
590
+ train_texts.append(chat)
591
+ count += 1
592
+ print(f" Instructions: {count}")
593
+ except Exception as e:
594
+ print(f" WARNING: Instruction data failed: {e}")
595
+
596
+ # Category 5: Reasoning / logic
597
+ try:
598
+ ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test")
599
+ count = 0
600
+ for row in ds:
601
+ if count >= per_category:
602
+ break
603
+ q = row.get("question", "")
604
+ a = row.get("answer", "")
605
+ if q and a:
606
+ chat = (f"<|im_start|>user\n{q}\nThink step by step before answering.<|im_end|>\n"
607
+ f"<|im_start|>assistant\n<think>\nLet me think through this carefully.\n"
608
+ f"The answer is {a}.\n</think>\nThe answer is {a}.<|im_end|>")
609
+ train_texts.append(chat)
610
+ count += 1
611
+ print(f" Reasoning/logic: {count}")
612
+ except Exception as e:
613
+ print(f" WARNING: Reasoning data failed: {e}")
614
+
615
+ print(f"\n Total training samples: {len(train_texts)}")
616
+
617
+ if len(train_texts) < 100:
618
+ print(" ERROR: Not enough training data!")
619
+ return model_path
620
+
621
+ # Shuffle for variety
622
+ import random
623
+ random.shuffle(train_texts)
624
+
625
+ # Tokenize
626
+ class ImproveDataset(Dataset):
627
  def __init__(self, texts, tokenizer, max_len=1024):
628
  self.data = []
629
  for t in texts:
 
637
  def __len__(self): return len(self.data)
638
  def __getitem__(self, i): return self.data[i]
639
 
640
+ print(" Tokenizing...")
641
+ dataset = ImproveDataset(train_texts, tokenizer)
642
 
643
  out_dir = Path(cfg.output_dir) / "train_output"
644
  out_dir.mkdir(parents=True, exist_ok=True)
645
 
646
  total_steps = (len(dataset) * cfg.train_epochs) // (cfg.train_batch * cfg.train_grad_accum)
 
647
 
648
  args = TrainingArguments(
649
  output_dir=str(out_dir),
 
667
  train_dataset=dataset, args=args
668
  )
669
 
670
+ print(f"\n Training: ~{total_steps} steps on {len(target_layers)} layers")
671
+ print(f" Data: {len(train_texts)} samples × {cfg.train_epochs} epochs")
672
+ print(f" LoRA: r={cfg.lora_r}, alpha={cfg.lora_alpha}")
673
  trainer.train()
674
 
675
+ # Merge LoRA back into weights
676
+ print("\n Merging LoRA adapters into model...")
677
  merged = model.merge_and_unload()
 
678
 
679
+ # Save
680
+ save_dir = Path(cfg.output_dir) / "improved"
681
+ save_dir.mkdir(parents=True, exist_ok=True)
682
+ print(f" Saving improved model to {save_dir}...")
683
+ merged.save_pretrained(str(save_dir), safe_serialization=True)
684
+ tokenizer.save_pretrained(str(save_dir))
685
+ sz = sum(f.stat().st_size for f in save_dir.glob("*.safetensors")) / 1e9
686
+ print(f" SAVED: {save_dir} ({sz:.1f} GB)")
687
+
688
+ # Cleanup training artifacts
689
  shutil.rmtree(str(out_dir), ignore_errors=True)
690
+ del merged, model
691
  gc.collect()
692
+ if torch.cuda.is_available():
693
+ torch.cuda.empty_cache()
694
 
695
+ return str(save_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
 
698
  # ============================================================
699
+ # MAIN — Full self-improvement cycle
700
  # ============================================================
701
 
702
  def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
703
+ """
704
+ Full self-improvement cycle:
705
+ 1. Analyze own weights (standalone health)
706
+ 2. Download + analyze reference (merge damage)
707
+ 3. Check layer coherence
708
+ 4. Combine all signals → rank layers by improvement potential
709
+ 5. LoRA improve on top layers
710
+ 6. Re-analyze to confirm improvement
711
+ """
712
  if cfg is None:
713
  cfg = SelfImproveConfig()
714
 
715
  start = time.time()
716
  print("=" * 60)
717
+ print(f"TD SELF-IMPROVEMENT v4 — FULL WEIGHT INTELLIGENCE")
718
+ print(f"Cycle: {cycle_num}")
719
  print(f"Model: {cfg.model_path}")
720
+ print(f"Approach: Analyze EVERYTHING, improve what has most potential")
721
  print(f"Started: {time.strftime('%H:%M:%S')}")
722
  print("=" * 60)
723
 
724
+ # ---- STEP 1: Analyze our model ----
725
+ print(f"\n{'='*40}")
726
+ print("STEP 1/6: ANALYZE OUR MODEL")
727
+ print(f"{'='*40}")
728
+ model_analysis = analyze_model_weights(cfg.model_path, "TD merged+healed")
729
+
730
+ # ---- STEP 2: Standalone health scoring ----
731
+ print(f"\n{'='*40}")
732
+ print("STEP 2/6: STANDALONE HEALTH SCORING")
733
+ print(f"{'='*40}")
734
+ health_scores = compute_health_scores(model_analysis)
735
+
736
+ # Print health summary
737
+ unhealthy_count = sum(1 for h in health_scores.values() if h["health_score"] > 0.2)
738
+ total = len(health_scores)
739
+ print(f"\n Health summary: {unhealthy_count}/{total} tensors have notable issues")
740
+
741
+ # ---- STEP 3: Reference comparison (merge damage) ----
742
+ print(f"\n{'='*40}")
743
+ print("STEP 3/6: REFERENCE COMPARISON")
744
+ print(f"{'='*40}")
745
+
746
+ ref_path = Path(cfg.output_dir) / "reference_weights"
747
+ if not ref_path.exists() or not list(ref_path.glob("*.safetensors")):
748
+ print(f" Downloading reference: {cfg.reference_model}...")
749
+ from huggingface_hub import snapshot_download
750
+ snapshot_download(
751
+ cfg.reference_model,
752
+ local_dir=str(ref_path),
753
+ allow_patterns=["*.safetensors", "*.json"],
754
+ ignore_patterns=["*.bin", "*.pt", "*.onnx"],
 
 
 
 
 
755
  )
 
 
 
 
 
756
 
757
+ ref_analysis = analyze_model_weights(str(ref_path), "Qwen3-VL-8B (reference)")
758
+ damage_scores = compute_damage_scores(model_analysis, ref_analysis)
759
 
760
+ # Free reference model from disk
761
+ print(f" Cleaning up reference model to save disk...")
762
+ shutil.rmtree(str(ref_path), ignore_errors=True)
763
+ gc.collect()
764
+
765
+ # ---- STEP 4: Layer coherence ----
766
+ print(f"\n{'='*40}")
767
+ print("STEP 4/6: LAYER COHERENCE")
768
+ print(f"{'='*40}")
769
+ coherence_scores = compute_coherence_scores(model_analysis)
770
+
771
+ # ---- STEP 5: Rank and improve ----
772
+ print(f"\n{'='*40}")
773
+ print("STEP 5/6: RANK LAYERS + LORA IMPROVEMENT")
774
+ print(f"{'='*40}")
775
+ target_layers, layer_details = rank_layers_for_improvement(
776
+ health_scores, damage_scores, coherence_scores, cfg
777
+ )
778
 
779
+ # Save pre-improvement report
780
+ report_dir = Path(cfg.output_dir)
781
+ report_dir.mkdir(parents=True, exist_ok=True)
782
+ pre_report = {
783
+ "cycle": cycle_num,
784
+ "model_path": cfg.model_path,
785
+ "target_layers": target_layers,
786
+ "layer_details": {str(k): v for k, v in layer_details.items()},
787
+ "config": {
788
+ "w_health": cfg.w_health,
789
+ "w_damage": cfg.w_damage,
790
+ "w_coherence": cfg.w_coherence,
791
+ "top_n_layers": cfg.top_n_layers,
792
+ "lora_r": cfg.lora_r,
793
+ },
794
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
795
+ }
796
+ with open(report_dir / "pre_improvement_report.json", "w") as f:
797
+ json.dump(pre_report, f, indent=2)
798
+
799
+ # Run LoRA improvement
800
+ improved_path = improve_with_lora(cfg.model_path, target_layers, cfg)
801
+
802
+ # ---- STEP 6: Verify improvement ----
803
+ print(f"\n{'='*40}")
804
+ print("STEP 6/6: VERIFY IMPROVEMENT")
805
+ print(f"{'='*40}")
806
+
807
+ post_analysis = analyze_model_weights(improved_path, "TD improved")
808
+ post_health = compute_health_scores(post_analysis)
809
+
810
+ # Compare health before and after
811
+ pre_avg_health = sum(h["health_score"] for h in health_scores.values()) / max(len(health_scores), 1)
812
+ post_avg_health = sum(h["health_score"] for h in post_health.values()) / max(len(post_health), 1)
813
+
814
+ # Per-layer comparison for targeted layers
815
+ print(f"\n Per-layer improvement on targeted layers:")
816
+ for layer_num in target_layers:
817
+ pre_score = layer_details.get(layer_num, {}).get("combined", 0)
818
+ # Find matching post health scores
819
+ post_layer_scores = []
820
+ for name, h in post_health.items():
821
+ parts = name.split(".")
822
+ for j, part in enumerate(parts):
823
+ if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit():
824
+ if int(parts[j+1]) == layer_num:
825
+ post_layer_scores.append(h["health_score"])
826
+ break
827
+ post_avg = sum(post_layer_scores) / max(len(post_layer_scores), 1) if post_layer_scores else 0
828
+ pre_h = layer_details.get(layer_num, {}).get("health", 0)
829
+ delta = pre_h - post_avg
830
+ symbol = "+" if delta > 0 else "-" if delta < 0 else "="
831
+ print(f" Layer {layer_num:>2}: health {pre_h:.4f} → {post_avg:.4f} ({symbol}{abs(delta):.4f})")
832
 
 
833
  elapsed = (time.time() - start) / 60
834
+ print(f"\n{'='*60}")
835
  print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min")
836
+ print(f"{'='*60}")
837
+ print(f" Target layers: {target_layers}")
838
+ print(f" Average health score BEFORE: {pre_avg_health:.6f}")
839
+ print(f" Average health score AFTER: {post_avg_health:.6f}")
840
+ improvement = (pre_avg_health - post_avg_health) / max(pre_avg_health, 1e-8) * 100
841
+ print(f" Health improvement: {improvement:.1f}%")
842
+ print(f" Improved model: {improved_path}")
843
+ print(f"{'='*60}")
844
+
845
+ # Final report
846
+ final_report = {
847
+ **pre_report,
848
+ "pre_avg_health": pre_avg_health,
849
+ "post_avg_health": post_avg_health,
850
+ "health_improvement_pct": improvement,
851
  "duration_min": elapsed,
852
+ "improved_path": improved_path,
853
  }
854
+ with open(Path(improved_path) / "cycle_report.json", "w") as f:
855
+ json.dump(final_report, f, indent=2)
856
 
857
+ return improved_path
858
 
859
 
860
  if __name__ == "__main__":