File size: 17,280 Bytes
5faf2eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#!/usr/bin/env python3
"""
PPO Evaluation Script for Seriguela Block 3
Tests if PPO finetuning can find symbolic regression expressions
"""

import os
import sys
import json
import numpy as np
import torch
from pathlib import Path
from typing import Dict, List, Tuple
from datetime import datetime

# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from transformers import AutoTokenizer, AutoModelForCausalLM, StoppingCriteria, StoppingCriteriaList
from classes.expression import Expression


class ExpressionStoppingCriteria(StoppingCriteria):
    """Stop generation at natural expression boundaries."""
    def __init__(self, tokenizer, stop_sequences):
        self.tokenizer = tokenizer
        self.stop_ids = [tokenizer.encode(seq, add_special_tokens=False)
                        for seq in stop_sequences]

    def __call__(self, input_ids, scores, **kwargs):
        # Check if any stop sequence appears in generated text
        for stop_ids in self.stop_ids:
            if len(stop_ids) > 0 and len(input_ids[0]) >= len(stop_ids):
                if input_ids[0][-len(stop_ids):].tolist() == stop_ids:
                    return True
        return False

class PPOEvaluator:
    """Evaluates if PPO training works for symbolic regression"""

    def __init__(self, model_name: str, output_dir: str):
        self.model_name = model_name
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)

        # Load V2 model with optimal inference config (90% valid rate)
        print(f"Loading model: {model_name}")

        # Load base model first without adapters
        print("Loading base GPT-2 model...")
        self.model = AutoModelForCausalLM.from_pretrained(
            "gpt2",
            torch_dtype=torch.float16,
            device_map="auto"
        )

        # Configure tokenizer with special tokens
        print("Configuring tokenizer with special tokens...")
        self.tokenizer = AutoTokenizer.from_pretrained("gpt2")
        self.tokenizer.add_special_tokens({
            "additional_special_tokens": ["<|startofex|>", "<|endofex|>"]
        })

        # Resize embeddings to match tokenizer
        print(f"Resizing embeddings from {self.model.get_input_embeddings().weight.shape[0]} to {len(self.tokenizer)}...")
        self.model.resize_token_embeddings(len(self.tokenizer))

        # Now load the V2 adapter weights
        print(f"Loading V2 adapter from {model_name}...")
        try:
            from peft import PeftModel
            self.model = PeftModel.from_pretrained(self.model, model_name)
            print("V2 adapter loaded successfully (LoRA weights)")
            print("Merging adapter into base model...")
            self.model = self.model.merge_and_unload()
            print("Adapter merged successfully")
        except Exception as e:
            print(f"Warning: Could not load as PEFT model: {e}")
            print("Attempting to load as full model...")
            # If not a PEFT model, load full weights
            self.model = AutoModelForCausalLM.from_pretrained(
                model_name,
                torch_dtype=torch.float16,
                device_map="auto"
            )

        self.model.eval()

        # V2 optimal generation config (from FINAL_RESULTS)
        self.generation_config = {
            "temperature": 0.7,
            "top_k": 0,
            "top_p": 0.8,
            "repetition_penalty": 1.0,
            "max_new_tokens": 128,
            "do_sample": True,
            "pad_token_id": self.tokenizer.eos_token_id,
        }

        print(f"Model loaded. Using optimal V2 configuration.")

    def create_synthetic_dataset(self, formula: str, n_samples: int = 100) -> Tuple[np.ndarray, np.ndarray]:
        """Create synthetic dataset from a known formula"""
        print(f"Creating dataset for formula: {formula}")

        # Generate random input data
        X = np.random.uniform(-2, 2, (n_samples, 2))

        # Evaluate true formula
        try:
            expr = Expression(formula, is_prefix=False)
            y = expr.evaluate(X)
            return X, y
        except Exception as e:
            print(f"Error creating dataset: {e}")
            raise

    def test_baseline_generation(self, n_samples: int = 10) -> Dict:
        """Test baseline: V2 generates valid expressions but not fitted to data"""
        print("\n" + "="*60)
        print("BASELINE TEST: V2 Generation Without PPO")
        print("="*60)

        # Create test dataset (simple formula)
        X, y = self.create_synthetic_dataset("x_1 * x_2", n_samples=50)

        results = {
            "test": "baseline_generation",
            "timestamp": datetime.now().isoformat(),
            "generations": [],
            "summary": {}
        }

        prompt = """vars: x_1, x_2
oper: *, +, -, sin, cos
cons: C
expr:"""

        inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)

        # Create stopping criteria for <|endofex|>
        stopping_criteria = StoppingCriteriaList([
            ExpressionStoppingCriteria(self.tokenizer, ["<|endofex|>", "\n\nvars:"])
        ])

        valid_count = 0
        r2_scores = []

        print(f"\nGenerating {n_samples} expressions...")
        for i in range(n_samples):
            output = self.model.generate(
                **inputs,
                **self.generation_config,
                stopping_criteria=stopping_criteria
            )
            text = self.tokenizer.decode(output[0], skip_special_tokens=False)

            # Extract expression
            if "expr:" in text:
                expr_str = text.split("expr:")[-1].strip()
                expr_str = expr_str.split("<|endofex|>")[0].strip()
            else:
                expr_str = text

            # Debug: Show first few generations
            if i < 3:
                print(f"\n  DEBUG Sample {i+1}:")
                print(f"    Raw output: {text[:200]}")
                print(f"    Extracted: {expr_str[:100]}")

            # Validate and compute R²
            is_valid = False
            r2 = -1.0

            try:
                expr = Expression(expr_str, is_prefix=False)
                # Check if expression can be evaluated on dataset
                if expr.is_valid_on_dataset(X):
                    is_valid = True
                    valid_count += 1

                    # Fit constants and compute R²
                    try:
                        r2 = expr.fit_constants(X, y)
                        if np.isfinite(r2):
                            r2_scores.append(r2)
                        else:
                            r2 = -1.0
                    except:
                        r2 = -1.0
            except:
                pass

            results["generations"].append({
                "index": i + 1,
                "expression": expr_str,
                "valid": is_valid,
                "r2_score": float(r2) if r2 != -1.0 else None
            })

            if (i + 1) % 5 == 0:
                print(f"Generated {i + 1}/{n_samples} - Valid: {valid_count}, Avg R²: {np.mean(r2_scores) if r2_scores else 'N/A'}")

        # Summary
        results["summary"] = {
            "total_generations": n_samples,
            "valid_count": valid_count,
            "valid_rate": valid_count / n_samples,
            "r2_scores": r2_scores,
            "mean_r2": float(np.mean(r2_scores)) if r2_scores else None,
            "max_r2": float(np.max(r2_scores)) if r2_scores else None,
            "conclusion": "Baseline generates valid expressions but R² is low (not fitted to target)"
        }

        print("\n" + "-"*60)
        print(f"BASELINE RESULTS:")
        print(f"  Valid Rate: {results['summary']['valid_rate']:.1%} ({valid_count}/{n_samples})")
        print(f"  Mean R²: {results['summary']['mean_r2']:.4f}" if r2_scores else "  Mean R²: N/A")
        print(f"  Max R²: {results['summary']['max_r2']:.4f}" if r2_scores else "  Max R²: N/A")
        print(f"  Interpretation: V2 generates valid expressions (good!), but doesn't fit target data (expected without PPO)")
        print("-"*60)

        # Save results
        output_file = self.output_dir / "baseline_results.json"
        with open(output_file, 'w') as f:
            json.dump(results, f, indent=2)
        print(f"\nResults saved to: {output_file}")

        return results

    def test_ppo_simulation(self, target_formula: str = "x_1 * x_2", n_iterations: int = 10) -> Dict:
        """Simulate PPO: Generate expressions and check if best reward improves"""
        print("\n" + "="*60)
        print("PPO SIMULATION TEST: Check if Reward Can Improve")
        print("="*60)
        print(f"Target formula: {target_formula}")
        print("Note: This simulates PPO by generating multiple expressions")
        print("      and tracking best R² score. Real PPO would optimize")
        print("      the model to generate better expressions over time.")

        # Create target dataset
        X, y = self.create_synthetic_dataset(target_formula, n_samples=100)

        prompt = """vars: x_1, x_2
oper: *, +, -, sin, cos
cons: C
expr:"""

        inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)

        # Create stopping criteria
        stopping_criteria = StoppingCriteriaList([
            ExpressionStoppingCriteria(self.tokenizer, ["<|endofex|>", "\n\nvars:"])
        ])

        results = {
            "test": "ppo_simulation",
            "timestamp": datetime.now().isoformat(),
            "target_formula": target_formula,
            "iterations": [],
            "summary": {}
        }

        print(f"\nGenerating {n_iterations} expressions and tracking best R²...")

        best_r2 = -np.inf
        best_expr = None
        r2_history = []
        valid_count = 0

        for i in range(n_iterations):
            output = self.model.generate(
                **inputs,
                **self.generation_config,
                stopping_criteria=stopping_criteria
            )
            text = self.tokenizer.decode(output[0], skip_special_tokens=False)

            # Extract expression
            if "expr:" in text:
                expr_str = text.split("expr:")[-1].strip()
                expr_str = expr_str.split("<|endofex|>")[0].strip()
            else:
                expr_str = text

            # Compute reward (R²)
            is_valid = False
            r2 = -1.0

            try:
                expr = Expression(expr_str, is_prefix=False)
                if expr.is_valid_on_dataset(X):
                    is_valid = True
                    valid_count += 1
                    r2 = expr.fit_constants(X, y)

                    if np.isfinite(r2):
                        r2_history.append(r2)
                        if r2 > best_r2:
                            best_r2 = r2
                            best_expr = expr_str
                    else:
                        r2 = -1.0
            except:
                pass

            results["iterations"].append({
                "iteration": i + 1,
                "expression": expr_str,
                "valid": is_valid,
                "r2": float(r2) if np.isfinite(r2) else None,
                "is_best": (r2 == best_r2) if np.isfinite(r2) else False
            })

            if (i + 1) % 5 == 0:
                print(f"Iteration {i + 1}/{n_iterations} - Valid: {valid_count}, Best R²: {best_r2:.4f}")

        # Summary
        results["summary"] = {
            "total_iterations": n_iterations,
            "valid_count": valid_count,
            "valid_rate": valid_count / n_iterations,
            "best_r2": float(best_r2) if np.isfinite(best_r2) else None,
            "best_expression": best_expr,
            "r2_history": [float(r) for r in r2_history],
            "mean_r2": float(np.mean(r2_history)) if r2_history else None,
            "conclusion": self._analyze_ppo_simulation(best_r2, r2_history)
        }

        print("\n" + "-"*60)
        print("PPO SIMULATION RESULTS:")
        print(f"  Valid expressions: {valid_count}/{n_iterations}")
        print(f"  Best R²: {best_r2:.4f}" if np.isfinite(best_r2) else "  Best R²: N/A")
        print(f"  Mean R²: {results['summary']['mean_r2']:.4f}" if r2_history else "  Mean R²: N/A")
        print(f"  Best expression: {best_expr}")
        print(f"\n  Interpretation:")
        print(f"    - Baseline (Test 1) shows random expressions have low R² (~0.2)")
        print(f"    - PPO should improve this by learning to generate fitted expressions")
        print(f"    - Best R² of {best_r2:.4f} shows what's possible with current model")
        if best_r2 >= 0.9:
            print(f"    ✅ Model CAN find high-quality solutions (R² >= 0.9)")
        elif best_r2 >= 0.5:
            print(f"    ⚠️  Model can find partial solutions (R² >= 0.5)")
        else:
            print(f"    ❌ Model struggles to find good solutions (R² < 0.5)")
        print("-"*60)

        # Save results
        output_file = self.output_dir / "ppo_simulation_results.json"
        with open(output_file, 'w') as f:
            json.dump(results, f, indent=2)
        print(f"\nResults saved to: {output_file}")

        return results

    def _analyze_ppo_simulation(self, best_r2: float, r2_history: List[float]) -> str:
        """Analyze PPO simulation results"""
        if not r2_history:
            return "❌ No valid expressions generated"

        if best_r2 >= 0.9:
            return f"✅ EXCELLENT: Found high-quality solution (R² = {best_r2:.4f}). PPO training should work well."
        elif best_r2 >= 0.5:
            return f"⚠️  MODERATE: Found partial solution (R² = {best_r2:.4f}). PPO may help but needs tuning."
        else:
            return f"❌ POOR: Best solution is weak (R² = {best_r2:.4f}). PPO will struggle with current model."

    def _analyze_ppo_results(self, training_results: Dict) -> str:
        """Analyze PPO training results and provide conclusion"""
        if "epoch_rewards" not in training_results:
            return "Unable to analyze: No reward history found"

        rewards = training_results["epoch_rewards"]
        initial = rewards[0]
        final = rewards[-1]
        best = max(rewards)
        improvement = final - initial

        if best >= 0.9:
            return f"✅ EXCELLENT: Found high-quality solution (R² = {best:.4f})"
        elif improvement > 0.2:
            return f"✅ GOOD: Significant improvement ({improvement:+.4f}), PPO is working"
        elif improvement > 0.05:
            return f"⚠️  MODERATE: Some improvement ({improvement:+.4f}), may need more epochs"
        elif improvement > 0:
            return f"⚠️  WEAK: Minimal improvement ({improvement:+.4f}), check hyperparameters"
        else:
            return f"❌ POOR: No improvement or decline ({improvement:+.4f}), PPO not working properly"


def main():
    print("="*60)
    print("SERIGUELA BLOCK 3: PPO EVALUATION")
    print("="*60)
    print("Objective: Test if PPO finetuning works for symbolic regression")
    print("Model: V2 (augustocsc/Se124M_700K_infix_v2)")
    print("="*60)

    # Initialize evaluator
    evaluator = PPOEvaluator(
        model_name="augustocsc/Se124M_700K_infix_v2",
        output_dir="./logs/ppo_evaluation"
    )

    # Test 1: Baseline generation
    print("\n📊 TEST 1: Baseline Generation (V2 without PPO)")
    baseline_results = evaluator.test_baseline_generation(n_samples=30)

    # Test 2: PPO simulation
    print("\n🎯 TEST 2: PPO Simulation (Check if reward CAN improve)")
    ppo_results = evaluator.test_ppo_simulation(target_formula="x_1 * x_2", n_iterations=50)

    # Final summary
    print("\n" + "="*60)
    print("EVALUATION COMPLETE")
    print("="*60)
    print("\nResults saved to: ./logs/ppo_evaluation/")
    print("\nKey Questions Answered:")
    print("1. Does V2 generate valid expressions? Check baseline_results.json")
    print(f"   Answer: {baseline_results['summary']['valid_rate']:.1%} valid rate")
    print("2. Can model find high R² expressions? Check ppo_simulation_results.json")
    best_r2 = ppo_results['summary'].get('best_r2')
    if best_r2 is None:
        best_r2 = -1.0
    if best_r2 >= 0.9:
        print(f"   Answer: YES! Best R² = {best_r2:.4f} (excellent)")
    elif best_r2 >= 0.5:
        print(f"   Answer: PARTIAL. Best R² = {best_r2:.4f} (moderate)")
    else:
        print(f"   Answer: NO. Best R² = {best_r2:.4f} (poor)")
    print("3. Would PPO training work?")
    if best_r2 >= 0.9:
        print("   Answer: YES - Model can find solutions, PPO should learn to find them consistently")
    elif best_r2 >= 0.5:
        print("   Answer: MAYBE - Model finds partial solutions, PPO may need tuning")
    else:
        print("   Answer: UNLIKELY - Model struggles to find solutions even randomly")
    print("\nNext steps:")
    print("- Review results to understand baseline performance")
    print("- If simulation shows high R², PPO training is worth trying")
    print("- If simulation shows low R², may need to retrain base model")
    print("="*60)


if __name__ == "__main__":
    main()