polystep 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polystep/__init__.py +176 -0
- polystep/_compiled.py +334 -0
- polystep/_step_blockwise.py +994 -0
- polystep/_step_momentum.py +128 -0
- polystep/_step_monolithic.py +1070 -0
- polystep/adaptive_subspace.py +762 -0
- polystep/api.py +307 -0
- polystep/ask_tell.py +164 -0
- polystep/benchmarks/__init__.py +62 -0
- polystep/benchmarks/baselines.py +604 -0
- polystep/benchmarks/rl/__init__.py +13 -0
- polystep/benchmarks/rl/cartpole.py +260 -0
- polystep/benchmarks/rl/gym_evaluator.py +304 -0
- polystep/benchmarks/rl/metrics.py +53 -0
- polystep/benchmarks/rl/policies.py +133 -0
- polystep/benchmarks/utils.py +1262 -0
- polystep/blockwise.py +549 -0
- polystep/cma.py +400 -0
- polystep/cma_subspace.py +404 -0
- polystep/cost_nn.py +633 -0
- polystep/costs.py +86 -0
- polystep/dynamics.py +138 -0
- polystep/epsilon.py +217 -0
- polystep/geometry.py +410 -0
- polystep/hybrid_subspace.py +1335 -0
- polystep/layers/__init__.py +16 -0
- polystep/layers/attention.py +243 -0
- polystep/layers/rnn.py +276 -0
- polystep/objectives/__init__.py +25 -0
- polystep/objectives/base.py +77 -0
- polystep/objectives/synthetic.py +232 -0
- polystep/optimizer.py +1405 -0
- polystep/projection/__init__.py +5 -0
- polystep/projection/sparse.py +291 -0
- polystep/py.typed +0 -0
- polystep/quadratic_model.py +285 -0
- polystep/solver.py +654 -0
- polystep/solvers/__init__.py +34 -0
- polystep/solvers/_prelude.py +129 -0
- polystep/solvers/base.py +77 -0
- polystep/solvers/greedy.py +159 -0
- polystep/solvers/kl_softmax.py +200 -0
- polystep/solvers/sinkhorn.py +522 -0
- polystep/solvers/softmax.py +151 -0
- polystep/solvers/tempered_softmax.py +97 -0
- polystep/subspace.py +694 -0
- polystep/transform.py +365 -0
- polystep-0.7.0.dist-info/METADATA +285 -0
- polystep-0.7.0.dist-info/RECORD +52 -0
- polystep-0.7.0.dist-info/WHEEL +5 -0
- polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
- polystep-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""EvoTorch CMA-ES and Nevergrad baseline wrappers for benchmarks.
|
|
2
|
+
|
|
3
|
+
Shared implementations used across all benchmark comparison scripts.
|
|
4
|
+
|
|
5
|
+
EvoTorch CMA-ES:
|
|
6
|
+
- Uses separable=True for models >10K params (O(n) vs O(n^2) memory)
|
|
7
|
+
- GPU-accelerated when available
|
|
8
|
+
- Reports function_evals = generations * popsize
|
|
9
|
+
|
|
10
|
+
Nevergrad ES:
|
|
11
|
+
- Uses OnePlusOne (1+1)-ES optimizer
|
|
12
|
+
- Simple and effective for neural network optimization
|
|
13
|
+
- Reports function_evals = budget
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from polystep.benchmarks.baselines import train_cmaes, train_nevergrad
|
|
17
|
+
|
|
18
|
+
# CMA-ES training
|
|
19
|
+
result = train_cmaes(model, train_data, train_labels, test_data, test_labels)
|
|
20
|
+
|
|
21
|
+
# Nevergrad training
|
|
22
|
+
result = train_nevergrad(model, train_data, train_labels, test_data, test_labels)
|
|
23
|
+
|
|
24
|
+
Installation:
|
|
25
|
+
pip install evotorch nevergrad
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import time
|
|
31
|
+
from typing import Callable, List, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import torch
|
|
35
|
+
import torch.nn as nn
|
|
36
|
+
from torch import Tensor
|
|
37
|
+
|
|
38
|
+
from .utils import BenchmarkResult
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Check for optional dependencies
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
_HAS_EVOTORCH = False
|
|
46
|
+
_EVOTORCH_ERROR = None
|
|
47
|
+
try:
|
|
48
|
+
from evotorch import Problem
|
|
49
|
+
from evotorch.algorithms import CMAES
|
|
50
|
+
|
|
51
|
+
_HAS_EVOTORCH = True
|
|
52
|
+
except ImportError as e:
|
|
53
|
+
_EVOTORCH_ERROR = str(e)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_HAS_NEVERGRAD = False
|
|
57
|
+
_NEVERGRAD_ERROR = None
|
|
58
|
+
try:
|
|
59
|
+
import nevergrad as ng
|
|
60
|
+
|
|
61
|
+
_HAS_NEVERGRAD = True
|
|
62
|
+
except ImportError as e:
|
|
63
|
+
_NEVERGRAD_ERROR = str(e)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def has_evotorch() -> bool:
|
|
67
|
+
"""Check if EvoTorch is available."""
|
|
68
|
+
return _HAS_EVOTORCH
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def has_nevergrad() -> bool:
|
|
72
|
+
"""Check if Nevergrad is available."""
|
|
73
|
+
return _HAS_NEVERGRAD
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# EvoTorch CMA-ES Implementation
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
if _HAS_EVOTORCH:
|
|
81
|
+
|
|
82
|
+
class NNOptimizationProblem(Problem):
|
|
83
|
+
"""EvoTorch Problem wrapper for neural network parameter optimization.
|
|
84
|
+
|
|
85
|
+
This wraps a PyTorch model for use with EvoTorch's CMA-ES optimizer.
|
|
86
|
+
The fitness function evaluates classification accuracy on a batch of data.
|
|
87
|
+
|
|
88
|
+
Adapted from CMA-ES baseline.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
model: PyTorch model to optimize.
|
|
92
|
+
train_data: Training data tensor, shape (N, ...).
|
|
93
|
+
train_labels: Training labels tensor, shape (N,).
|
|
94
|
+
batch_size: Batch size for fitness evaluation.
|
|
95
|
+
device: Device for computation ('cpu' or 'cuda').
|
|
96
|
+
fitness_fn: Optional custom fitness function. If None, uses accuracy.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> model = MNISTNet()
|
|
100
|
+
>>> problem = NNOptimizationProblem(model, X_train, y_train)
|
|
101
|
+
>>> searcher = CMAES(problem, popsize=16, stdev_init=0.5)
|
|
102
|
+
>>> searcher.step()
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
model: nn.Module,
|
|
108
|
+
train_data: Tensor,
|
|
109
|
+
train_labels: Tensor,
|
|
110
|
+
batch_size: int = 512,
|
|
111
|
+
device: str = "cpu",
|
|
112
|
+
fitness_fn: Optional[Callable[[nn.Module, Tensor, Tensor], float]] = None,
|
|
113
|
+
):
|
|
114
|
+
self._model = model
|
|
115
|
+
self._param_count = sum(p.numel() for p in model.parameters())
|
|
116
|
+
self._param_shapes = [p.shape for p in model.parameters()]
|
|
117
|
+
|
|
118
|
+
# Store training data on device
|
|
119
|
+
self._train_data = train_data.to(device)
|
|
120
|
+
self._train_labels = train_labels.to(device)
|
|
121
|
+
self._batch_size = batch_size
|
|
122
|
+
self._fitness_fn = fitness_fn
|
|
123
|
+
self._device_str = device
|
|
124
|
+
|
|
125
|
+
super().__init__(
|
|
126
|
+
objective_sense="max", # Maximize accuracy
|
|
127
|
+
solution_length=self._param_count,
|
|
128
|
+
dtype=torch.float32,
|
|
129
|
+
device=device,
|
|
130
|
+
initial_bounds=(-1.0, 1.0),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _load_weights(self, model: nn.Module, solution: Tensor) -> None:
|
|
134
|
+
"""Load flattened solution vector into model parameters."""
|
|
135
|
+
offset = 0
|
|
136
|
+
with torch.no_grad():
|
|
137
|
+
for p in model.parameters():
|
|
138
|
+
numel = p.numel()
|
|
139
|
+
p.data.copy_(solution[offset : offset + numel].view(p.shape))
|
|
140
|
+
offset += numel
|
|
141
|
+
|
|
142
|
+
def _evaluate(self, solution) -> None:
|
|
143
|
+
"""Evaluate a single solution (parameter vector).
|
|
144
|
+
|
|
145
|
+
EvoTorch calls this with a Solution object. We must SET the
|
|
146
|
+
evaluation on the solution, not return a value.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
solution: EvoTorch Solution object containing parameter values.
|
|
150
|
+
"""
|
|
151
|
+
values = solution.values
|
|
152
|
+
device = values.device
|
|
153
|
+
|
|
154
|
+
batch_size = min(self._batch_size, len(self._train_data))
|
|
155
|
+
|
|
156
|
+
# Sample a random batch for fitness evaluation
|
|
157
|
+
indices = torch.randperm(len(self._train_data), device=device)[:batch_size]
|
|
158
|
+
batch_data = self._train_data[indices]
|
|
159
|
+
batch_labels = self._train_labels[indices]
|
|
160
|
+
|
|
161
|
+
# Create model on correct device for evaluation
|
|
162
|
+
# Clone model architecture (create fresh instance)
|
|
163
|
+
model = type(self._model)(*self._get_model_init_args()).to(device)
|
|
164
|
+
model.eval()
|
|
165
|
+
|
|
166
|
+
self._load_weights(model, values)
|
|
167
|
+
|
|
168
|
+
if self._fitness_fn is not None:
|
|
169
|
+
fitness = self._fitness_fn(model, batch_data, batch_labels)
|
|
170
|
+
else:
|
|
171
|
+
# Default: accuracy
|
|
172
|
+
with torch.no_grad():
|
|
173
|
+
outputs = model(batch_data)
|
|
174
|
+
preds = outputs.argmax(dim=-1)
|
|
175
|
+
fitness = (preds == batch_labels).float().mean().item()
|
|
176
|
+
|
|
177
|
+
solution.set_evaluation(fitness)
|
|
178
|
+
|
|
179
|
+
def _get_model_init_args(self):
|
|
180
|
+
"""Get initialization arguments for model cloning.
|
|
181
|
+
|
|
182
|
+
Override this for models with custom initialization.
|
|
183
|
+
Returns empty tuple for default (no-arg) constructors.
|
|
184
|
+
"""
|
|
185
|
+
return ()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def train_cmaes(
|
|
189
|
+
model: nn.Module,
|
|
190
|
+
train_data: Tensor,
|
|
191
|
+
train_labels: Tensor,
|
|
192
|
+
test_data: Tensor,
|
|
193
|
+
test_labels: Tensor,
|
|
194
|
+
generations: int = 200,
|
|
195
|
+
popsize: int = 16,
|
|
196
|
+
stdev_init: float = 0.5,
|
|
197
|
+
batch_size: int = 512,
|
|
198
|
+
device: str = "cuda",
|
|
199
|
+
log_interval: int = 10,
|
|
200
|
+
verbose: bool = True,
|
|
201
|
+
model_init_args: tuple = (),
|
|
202
|
+
) -> BenchmarkResult:
|
|
203
|
+
"""Train model using EvoTorch CMA-ES.
|
|
204
|
+
|
|
205
|
+
Uses separable=True for models >10K params (O(n) memory instead of O(n^2)).
|
|
206
|
+
Reports function_evals = generations * popsize.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
model: PyTorch model to optimize.
|
|
210
|
+
train_data: Training data tensor, shape (N, ...).
|
|
211
|
+
train_labels: Training labels tensor, shape (N,).
|
|
212
|
+
test_data: Test data tensor, shape (M, ...).
|
|
213
|
+
test_labels: Test labels tensor, shape (M,).
|
|
214
|
+
generations: Number of CMA-ES generations.
|
|
215
|
+
popsize: Population size.
|
|
216
|
+
stdev_init: Initial standard deviation.
|
|
217
|
+
batch_size: Batch size for fitness evaluation.
|
|
218
|
+
device: Device for computation.
|
|
219
|
+
log_interval: Print stats every N generations.
|
|
220
|
+
verbose: Print progress.
|
|
221
|
+
model_init_args: Arguments for model constructor (for cloning).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
BenchmarkResult with training metrics.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
ImportError: If EvoTorch is not installed.
|
|
228
|
+
"""
|
|
229
|
+
if not _HAS_EVOTORCH:
|
|
230
|
+
raise ImportError(
|
|
231
|
+
f"EvoTorch is required for CMA-ES baseline. Install with: pip install evotorch\n"
|
|
232
|
+
f"Import error: {_EVOTORCH_ERROR}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Move model to device
|
|
236
|
+
model = model.to(device)
|
|
237
|
+
param_count = sum(p.numel() for p in model.parameters())
|
|
238
|
+
|
|
239
|
+
# Reset GPU memory stats
|
|
240
|
+
if device == "cuda" and torch.cuda.is_available():
|
|
241
|
+
torch.cuda.reset_peak_memory_stats()
|
|
242
|
+
|
|
243
|
+
# Create Problem with model cloning support
|
|
244
|
+
class ModelProblem(NNOptimizationProblem):
|
|
245
|
+
def _get_model_init_args(self):
|
|
246
|
+
return model_init_args
|
|
247
|
+
|
|
248
|
+
problem = ModelProblem(
|
|
249
|
+
model=model,
|
|
250
|
+
train_data=train_data,
|
|
251
|
+
train_labels=train_labels,
|
|
252
|
+
batch_size=batch_size,
|
|
253
|
+
device=device,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Use separable CMA-ES for large models (O(n) vs O(n^2) memory)
|
|
257
|
+
use_separable = param_count > 10000
|
|
258
|
+
if verbose and use_separable:
|
|
259
|
+
print(f" Using separable (diagonal) CMA-ES for {param_count:,} params")
|
|
260
|
+
|
|
261
|
+
searcher = CMAES(
|
|
262
|
+
problem,
|
|
263
|
+
popsize=popsize,
|
|
264
|
+
stdev_init=stdev_init,
|
|
265
|
+
separable=use_separable,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Convert test data to tensors on device
|
|
269
|
+
test_data_t = test_data.to(device)
|
|
270
|
+
test_labels_t = test_labels.to(device)
|
|
271
|
+
|
|
272
|
+
# Training loop
|
|
273
|
+
epoch_logs = []
|
|
274
|
+
best_test_acc = 0.0
|
|
275
|
+
best_solution = None
|
|
276
|
+
start_time = time.time()
|
|
277
|
+
|
|
278
|
+
# Build the eval model once and reuse across calls; rebuilding it on
|
|
279
|
+
# every log_interval was a hot-path allocation under the original
|
|
280
|
+
# benchmark configuration (generations=200, log_interval=10 ⇒ 21 rebuilds).
|
|
281
|
+
eval_model = type(model)(*model_init_args).to(device)
|
|
282
|
+
eval_model.eval()
|
|
283
|
+
|
|
284
|
+
def evaluate_on_test(solution) -> float:
|
|
285
|
+
"""Evaluate ``solution`` on the full test set."""
|
|
286
|
+
if solution is None:
|
|
287
|
+
return 0.0
|
|
288
|
+
values = solution.values if hasattr(solution, "values") else solution
|
|
289
|
+
|
|
290
|
+
# Load weights into the reused model.
|
|
291
|
+
offset = 0
|
|
292
|
+
with torch.no_grad():
|
|
293
|
+
for p in eval_model.parameters():
|
|
294
|
+
numel = p.numel()
|
|
295
|
+
p.data.copy_(values[offset : offset + numel].view(p.shape))
|
|
296
|
+
offset += numel
|
|
297
|
+
|
|
298
|
+
# Evaluate in batches
|
|
299
|
+
correct = 0
|
|
300
|
+
total = 0
|
|
301
|
+
eval_batch_size = 512
|
|
302
|
+
|
|
303
|
+
with torch.no_grad():
|
|
304
|
+
for i in range(0, len(test_data_t), eval_batch_size):
|
|
305
|
+
batch_data = test_data_t[i : i + eval_batch_size]
|
|
306
|
+
batch_labels = test_labels_t[i : i + eval_batch_size]
|
|
307
|
+
outputs = eval_model(batch_data)
|
|
308
|
+
preds = outputs.argmax(dim=-1)
|
|
309
|
+
correct += (preds == batch_labels).sum().item()
|
|
310
|
+
total += len(batch_labels)
|
|
311
|
+
|
|
312
|
+
return correct / total if total > 0 else 0.0
|
|
313
|
+
|
|
314
|
+
for gen in range(generations):
|
|
315
|
+
searcher.step()
|
|
316
|
+
|
|
317
|
+
# Get stats from status
|
|
318
|
+
status = searcher.status
|
|
319
|
+
pop_best_fitness = float(status.get("pop_best_eval", 0.0))
|
|
320
|
+
mean_fitness = float(status.get("mean_eval", 0.0))
|
|
321
|
+
sigma = float(status.get("stepsize", stdev_init))
|
|
322
|
+
|
|
323
|
+
# Evaluate on test set periodically
|
|
324
|
+
if (gen + 1) % log_interval == 0 or gen == generations - 1:
|
|
325
|
+
pop_best_sol = status.get("pop_best", None)
|
|
326
|
+
if pop_best_sol is not None:
|
|
327
|
+
test_acc = evaluate_on_test(pop_best_sol)
|
|
328
|
+
if test_acc > best_test_acc:
|
|
329
|
+
best_test_acc = test_acc
|
|
330
|
+
best_solution = pop_best_sol.clone() if hasattr(pop_best_sol, "clone") else pop_best_sol
|
|
331
|
+
else:
|
|
332
|
+
test_acc = 0.0
|
|
333
|
+
|
|
334
|
+
epoch_logs.append(
|
|
335
|
+
{
|
|
336
|
+
"epoch": gen + 1,
|
|
337
|
+
"generation": gen + 1,
|
|
338
|
+
"accuracy": test_acc,
|
|
339
|
+
"test_accuracy": test_acc,
|
|
340
|
+
"pop_best_fitness": pop_best_fitness,
|
|
341
|
+
"mean_fitness": mean_fitness,
|
|
342
|
+
"loss": -pop_best_fitness,
|
|
343
|
+
"sigma": sigma,
|
|
344
|
+
"time": time.time() - start_time,
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if verbose:
|
|
349
|
+
print(
|
|
350
|
+
f" Gen {gen + 1:4d} | best_fit={pop_best_fitness:.4f} | "
|
|
351
|
+
f"mean_fit={mean_fitness:.4f} | sigma={sigma:.6f} | "
|
|
352
|
+
f"test_acc={test_acc * 100:.1f}%"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
elapsed = time.time() - start_time
|
|
356
|
+
total_evals = generations * popsize
|
|
357
|
+
|
|
358
|
+
# Final evaluation
|
|
359
|
+
final_test_acc = evaluate_on_test(best_solution) if best_solution is not None else best_test_acc
|
|
360
|
+
|
|
361
|
+
# Get memory stats
|
|
362
|
+
peak_memory = 0.0
|
|
363
|
+
if device == "cuda" and torch.cuda.is_available():
|
|
364
|
+
peak_memory = torch.cuda.max_memory_allocated() / 1024 / 1024
|
|
365
|
+
|
|
366
|
+
return BenchmarkResult(
|
|
367
|
+
optimizer="cmaes",
|
|
368
|
+
seed=0, # CMA-ES uses internal randomness
|
|
369
|
+
final_accuracy=final_test_acc,
|
|
370
|
+
best_accuracy=best_test_acc,
|
|
371
|
+
final_loss=None, # CMA-ES maximizes accuracy, no loss
|
|
372
|
+
wall_time_seconds=elapsed,
|
|
373
|
+
peak_gpu_memory_mb=peak_memory,
|
|
374
|
+
total_steps=generations,
|
|
375
|
+
function_evals=total_evals,
|
|
376
|
+
convergence_epoch=None,
|
|
377
|
+
epoch_logs=epoch_logs,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Nevergrad ES Implementation
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def train_nevergrad(
|
|
387
|
+
model: nn.Module,
|
|
388
|
+
train_data: Tensor,
|
|
389
|
+
train_labels: Tensor,
|
|
390
|
+
test_data: Tensor,
|
|
391
|
+
test_labels: Tensor,
|
|
392
|
+
budget: int = 10000,
|
|
393
|
+
batch_size: int = 512,
|
|
394
|
+
device: str = "cuda",
|
|
395
|
+
log_interval: int = 100,
|
|
396
|
+
verbose: bool = True,
|
|
397
|
+
model_init_args: tuple = (),
|
|
398
|
+
) -> BenchmarkResult:
|
|
399
|
+
"""Train model using Nevergrad OnePlusOne ES.
|
|
400
|
+
|
|
401
|
+
Nevergrad minimizes, so we negate accuracy for the fitness function.
|
|
402
|
+
Reports function_evals = budget.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
model: PyTorch model to optimize.
|
|
406
|
+
train_data: Training data tensor, shape (N, ...).
|
|
407
|
+
train_labels: Training labels tensor, shape (N,).
|
|
408
|
+
test_data: Test data tensor, shape (M, ...).
|
|
409
|
+
test_labels: Test labels tensor, shape (M,).
|
|
410
|
+
budget: Total function evaluations (budget).
|
|
411
|
+
batch_size: Batch size for fitness evaluation.
|
|
412
|
+
device: Device for computation.
|
|
413
|
+
log_interval: Print stats every N evaluations.
|
|
414
|
+
verbose: Print progress.
|
|
415
|
+
model_init_args: Arguments for model constructor (for cloning).
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
BenchmarkResult with training metrics.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
ImportError: If Nevergrad is not installed.
|
|
422
|
+
"""
|
|
423
|
+
if not _HAS_NEVERGRAD:
|
|
424
|
+
raise ImportError(
|
|
425
|
+
f"Nevergrad is required for ES baseline. Install with: pip install nevergrad\n"
|
|
426
|
+
f"Import error: {_NEVERGRAD_ERROR}"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Move model to device
|
|
430
|
+
model = model.to(device)
|
|
431
|
+
param_count = sum(p.numel() for p in model.parameters())
|
|
432
|
+
|
|
433
|
+
# Reset GPU memory stats
|
|
434
|
+
if device == "cuda" and torch.cuda.is_available():
|
|
435
|
+
torch.cuda.reset_peak_memory_stats()
|
|
436
|
+
|
|
437
|
+
# Move data to device
|
|
438
|
+
train_data_t = train_data.to(device)
|
|
439
|
+
train_labels_t = train_labels.to(device)
|
|
440
|
+
test_data_t = test_data.to(device)
|
|
441
|
+
test_labels_t = test_labels.to(device)
|
|
442
|
+
|
|
443
|
+
# Create Nevergrad parametrization
|
|
444
|
+
# OnePlusOne works well for neural network optimization
|
|
445
|
+
parametrization = ng.p.Array(shape=(param_count,), lower=-2.0, upper=2.0)
|
|
446
|
+
optimizer = ng.optimizers.OnePlusOne(
|
|
447
|
+
parametrization=parametrization,
|
|
448
|
+
budget=budget,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def load_params(params: np.ndarray) -> None:
|
|
452
|
+
"""Load flattened numpy array into model parameters."""
|
|
453
|
+
offset = 0
|
|
454
|
+
with torch.no_grad():
|
|
455
|
+
for p in model.parameters():
|
|
456
|
+
numel = p.numel()
|
|
457
|
+
p.data.copy_(torch.from_numpy(params[offset : offset + numel]).view(p.shape).to(device))
|
|
458
|
+
offset += numel
|
|
459
|
+
|
|
460
|
+
def fitness(params: np.ndarray) -> float:
|
|
461
|
+
"""Fitness function: negative accuracy (Nevergrad minimizes)."""
|
|
462
|
+
load_params(params)
|
|
463
|
+
|
|
464
|
+
# Sample random batch
|
|
465
|
+
batch_size_actual = min(batch_size, len(train_data_t))
|
|
466
|
+
indices = torch.randperm(len(train_data_t), device=device)[:batch_size_actual]
|
|
467
|
+
batch_data = train_data_t[indices]
|
|
468
|
+
batch_labels = train_labels_t[indices]
|
|
469
|
+
|
|
470
|
+
model.eval()
|
|
471
|
+
with torch.no_grad():
|
|
472
|
+
outputs = model(batch_data)
|
|
473
|
+
preds = outputs.argmax(dim=-1)
|
|
474
|
+
accuracy = (preds == batch_labels).float().mean().item()
|
|
475
|
+
|
|
476
|
+
return -accuracy # Negate for minimization
|
|
477
|
+
|
|
478
|
+
def evaluate_on_test(params: np.ndarray) -> float:
|
|
479
|
+
"""Evaluate on full test set."""
|
|
480
|
+
load_params(params)
|
|
481
|
+
model.eval()
|
|
482
|
+
|
|
483
|
+
correct = 0
|
|
484
|
+
total = 0
|
|
485
|
+
eval_batch_size = 512
|
|
486
|
+
|
|
487
|
+
with torch.no_grad():
|
|
488
|
+
for i in range(0, len(test_data_t), eval_batch_size):
|
|
489
|
+
batch_data = test_data_t[i : i + eval_batch_size]
|
|
490
|
+
batch_labels = test_labels_t[i : i + eval_batch_size]
|
|
491
|
+
outputs = model(batch_data)
|
|
492
|
+
preds = outputs.argmax(dim=-1)
|
|
493
|
+
correct += (preds == batch_labels).sum().item()
|
|
494
|
+
total += len(batch_labels)
|
|
495
|
+
|
|
496
|
+
return correct / total if total > 0 else 0.0
|
|
497
|
+
|
|
498
|
+
# Optimization loop
|
|
499
|
+
epoch_logs = []
|
|
500
|
+
best_test_acc = 0.0
|
|
501
|
+
eval_count = 0
|
|
502
|
+
start_time = time.time()
|
|
503
|
+
|
|
504
|
+
if verbose:
|
|
505
|
+
print(f" Nevergrad OnePlusOne: budget={budget}, params={param_count:,}")
|
|
506
|
+
|
|
507
|
+
# Use tell/ask interface for finer control
|
|
508
|
+
while eval_count < budget:
|
|
509
|
+
# Ask for candidate
|
|
510
|
+
candidate = optimizer.ask()
|
|
511
|
+
params = candidate.value
|
|
512
|
+
|
|
513
|
+
# Evaluate fitness
|
|
514
|
+
loss = fitness(params)
|
|
515
|
+
eval_count += 1
|
|
516
|
+
|
|
517
|
+
# Tell optimizer the result
|
|
518
|
+
optimizer.tell(candidate, loss)
|
|
519
|
+
|
|
520
|
+
# Periodic logging and test evaluation
|
|
521
|
+
if eval_count % log_interval == 0 or eval_count == budget:
|
|
522
|
+
test_acc = evaluate_on_test(params)
|
|
523
|
+
if test_acc > best_test_acc:
|
|
524
|
+
best_test_acc = test_acc
|
|
525
|
+
|
|
526
|
+
epoch_logs.append(
|
|
527
|
+
{
|
|
528
|
+
"eval_count": eval_count,
|
|
529
|
+
"fitness": -loss, # Convert back to accuracy
|
|
530
|
+
"test_accuracy": test_acc,
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
if verbose:
|
|
535
|
+
print(f" Eval {eval_count:5d}/{budget} | fitness={-loss:.4f} | test_acc={test_acc * 100:.1f}%")
|
|
536
|
+
|
|
537
|
+
elapsed = time.time() - start_time
|
|
538
|
+
|
|
539
|
+
# Get best recommendation
|
|
540
|
+
recommendation = optimizer.recommend()
|
|
541
|
+
final_params = recommendation.value
|
|
542
|
+
final_test_acc = evaluate_on_test(final_params)
|
|
543
|
+
if final_test_acc > best_test_acc:
|
|
544
|
+
best_test_acc = final_test_acc
|
|
545
|
+
|
|
546
|
+
# Get memory stats
|
|
547
|
+
peak_memory = 0.0
|
|
548
|
+
if device == "cuda" and torch.cuda.is_available():
|
|
549
|
+
peak_memory = torch.cuda.max_memory_allocated() / 1024 / 1024
|
|
550
|
+
|
|
551
|
+
return BenchmarkResult(
|
|
552
|
+
optimizer="nevergrad",
|
|
553
|
+
seed=0, # Nevergrad uses internal randomness
|
|
554
|
+
final_accuracy=final_test_acc,
|
|
555
|
+
best_accuracy=best_test_acc,
|
|
556
|
+
final_loss=None, # ES optimizes accuracy, no loss
|
|
557
|
+
wall_time_seconds=elapsed,
|
|
558
|
+
peak_gpu_memory_mb=peak_memory,
|
|
559
|
+
total_steps=budget,
|
|
560
|
+
function_evals=budget,
|
|
561
|
+
convergence_epoch=None,
|
|
562
|
+
epoch_logs=epoch_logs,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
# Utility functions for benchmark scripts
|
|
568
|
+
# ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def check_gradient_free_deps() -> Tuple[bool, bool, str]:
|
|
572
|
+
"""Check which gradient-free dependencies are available.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Tuple of (has_evotorch, has_nevergrad, message).
|
|
576
|
+
"""
|
|
577
|
+
messages = []
|
|
578
|
+
if not _HAS_EVOTORCH:
|
|
579
|
+
messages.append(f"EvoTorch not installed: {_EVOTORCH_ERROR}")
|
|
580
|
+
if not _HAS_NEVERGRAD:
|
|
581
|
+
messages.append(f"Nevergrad not installed: {_NEVERGRAD_ERROR}")
|
|
582
|
+
|
|
583
|
+
message = "\n".join(messages) if messages else "All gradient-free dependencies available"
|
|
584
|
+
return _HAS_EVOTORCH, _HAS_NEVERGRAD, message
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def get_available_optimizers(include_gradient_free: bool = True) -> List[str]:
|
|
588
|
+
"""Get list of available optimizers.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
include_gradient_free: Whether to include CMA-ES and Nevergrad.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
List of available optimizer names.
|
|
595
|
+
"""
|
|
596
|
+
optimizers = ["sgd", "adam", "adamw", "polystep"]
|
|
597
|
+
|
|
598
|
+
if include_gradient_free:
|
|
599
|
+
if _HAS_EVOTORCH:
|
|
600
|
+
optimizers.append("cmaes")
|
|
601
|
+
if _HAS_NEVERGRAD:
|
|
602
|
+
optimizers.append("nevergrad")
|
|
603
|
+
|
|
604
|
+
return optimizers
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Reinforcement-learning benchmark helpers for PolyStep experiments."""
|
|
2
|
+
|
|
3
|
+
from .cartpole import CartPoleEvaluator
|
|
4
|
+
from .metrics import build_rl_metrics, normalize_score
|
|
5
|
+
from .policies import DiscreteMLPPolicy, stack_module_params
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"build_rl_metrics",
|
|
9
|
+
"normalize_score",
|
|
10
|
+
"CartPoleEvaluator",
|
|
11
|
+
"DiscreteMLPPolicy",
|
|
12
|
+
"stack_module_params",
|
|
13
|
+
]
|