evograd-diff 0.1.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.
- evograd/__init__.py +67 -0
- evograd/algorithms/__init__.py +138 -0
- evograd/algorithms/cmaes.py +1365 -0
- evograd/algorithms/de.py +895 -0
- evograd/algorithms/ga.py +532 -0
- evograd/algorithms/pso.py +648 -0
- evograd/algorithms/shade.py +1165 -0
- evograd/benchmarks/functions/__init__.py +229 -0
- evograd/benchmarks/functions/base.py +217 -0
- evograd/benchmarks/functions/cec2017/__init__.py +250 -0
- evograd/benchmarks/functions/cec2017/basic.py +413 -0
- evograd/benchmarks/functions/cec2017/composition.py +580 -0
- evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- evograd/benchmarks/functions/cec2017/data.py +350 -0
- evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
- evograd/benchmarks/functions/cec2017/simple.py +326 -0
- evograd/benchmarks/functions/classical.py +649 -0
- evograd/benchmarks/functions/smoothed_funnel.py +476 -0
- evograd/benchmarks/functions/transforms.py +463 -0
- evograd/benchmarks/run_benchmark_functions.py +1208 -0
- evograd/core/__init__.py +73 -0
- evograd/core/algorithm.py +778 -0
- evograd/core/maximize.py +269 -0
- evograd/core/minimize.py +740 -0
- evograd/core/problem.py +444 -0
- evograd/core/result.py +571 -0
- evograd/core/termination.py +602 -0
- evograd/operators/__init__.py +178 -0
- evograd/operators/crossover.py +1117 -0
- evograd/operators/mutation.py +1098 -0
- evograd/operators/relaxations.py +175 -0
- evograd/operators/repair.py +601 -0
- evograd/operators/sampling.py +577 -0
- evograd/operators/selection.py +981 -0
- evograd/operators/survival.py +1000 -0
- evograd/tests/__init__.py +11 -0
- evograd/tests/run_all.py +78 -0
- evograd/tests/test_core.py +528 -0
- evograd/tests/test_ga.py +572 -0
- evograd/tests/test_operators.py +662 -0
- evograd/tests/test_per_individual.py +326 -0
- evograd/tests/test_utils.py +328 -0
- evograd/utils/__init__.py +97 -0
- evograd/utils/callbacks.py +926 -0
- evograd/utils/device.py +502 -0
- evograd/utils/duplicates.py +421 -0
- evograd_diff-0.1.0.dist-info/METADATA +439 -0
- evograd_diff-0.1.0.dist-info/RECORD +50 -0
- evograd_diff-0.1.0.dist-info/WHEEL +4 -0
- evograd_diff-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Particle Swarm Optimisation (PSO) implementation for EvoGrad.
|
|
3
|
+
|
|
4
|
+
This module provides a fully differentiable Particle Swarm Optimisation
|
|
5
|
+
algorithm that supports both classical and gradient-enabled optimisation modes.
|
|
6
|
+
|
|
7
|
+
PSO evolves a swarm of particles through:
|
|
8
|
+
1. Velocity update: Combine inertia, cognitive, and social components
|
|
9
|
+
2. Position update: Move particles according to velocity
|
|
10
|
+
3. Personal best update: Track each particle's best position
|
|
11
|
+
4. Global best update: Track the swarm's best position
|
|
12
|
+
|
|
13
|
+
Modes:
|
|
14
|
+
- adaptive=False, differentiable=False: Classical PSO
|
|
15
|
+
- adaptive=True, differentiable=False: Hyperparameters (inertia, c1, c2)
|
|
16
|
+
are learnable via backpropagation
|
|
17
|
+
- adaptive=False, differentiable=True: Particle positions are learnable
|
|
18
|
+
via backpropagation
|
|
19
|
+
- adaptive=True, differentiable=True: Both hyperparameters and positions
|
|
20
|
+
are learnable
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> from evograd.algorithms import PSO
|
|
24
|
+
>>> from evograd.core import Problem, minimize
|
|
25
|
+
>>>
|
|
26
|
+
>>> problem = Problem(
|
|
27
|
+
... objective=lambda x: (x**2).sum(dim=-1),
|
|
28
|
+
... n_var=30,
|
|
29
|
+
... xl=-100.0,
|
|
30
|
+
... xu=100.0,
|
|
31
|
+
... )
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Classical PSO
|
|
34
|
+
>>> pso = PSO(pop_size=100, inertia=0.7, c1=1.5, c2=1.5)
|
|
35
|
+
>>> result = minimize(problem, pso, max_evals=10000)
|
|
36
|
+
>>>
|
|
37
|
+
>>> # Adaptive PSO with learnable hyperparameters
|
|
38
|
+
>>> pso = PSO(pop_size=100, adaptive=True)
|
|
39
|
+
>>> result = minimize(problem, pso, max_evals=10000)
|
|
40
|
+
|
|
41
|
+
Reference:
|
|
42
|
+
Kennedy, J. & Eberhart, R. (1995). Particle Swarm Optimization.
|
|
43
|
+
Proceedings of ICNN'95.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
49
|
+
|
|
50
|
+
import torch
|
|
51
|
+
import torch.nn as nn
|
|
52
|
+
from torch import Tensor
|
|
53
|
+
|
|
54
|
+
from evograd.core.algorithm import Algorithm
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from evograd.core.problem import Problem
|
|
58
|
+
|
|
59
|
+
__all__ = ["PSO", "pso_default", "pso_constriction"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PSO(Algorithm):
|
|
63
|
+
"""
|
|
64
|
+
Particle Swarm Optimisation (PSO) for continuous optimisation.
|
|
65
|
+
|
|
66
|
+
PSO simulates a swarm of particles moving through the search space,
|
|
67
|
+
influenced by their own best known position and the swarm's best
|
|
68
|
+
known position.
|
|
69
|
+
|
|
70
|
+
The velocity update equation is:
|
|
71
|
+
v = w*v + c1*r1*(p_best - x) + c2*r2*(g_best - x)
|
|
72
|
+
|
|
73
|
+
where:
|
|
74
|
+
- w: inertia weight (controls momentum)
|
|
75
|
+
- c1: cognitive coefficient (attraction to personal best)
|
|
76
|
+
- c2: social coefficient (attraction to global best)
|
|
77
|
+
- r1, r2: random vectors in [0, 1]
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
pop_size: Swarm size (number of particles).
|
|
81
|
+
inertia: Inertia weight w. Default: 0.7.
|
|
82
|
+
c1: Cognitive coefficient. Default: 1.5.
|
|
83
|
+
c2: Social coefficient. Default: 1.5.
|
|
84
|
+
v_max_ratio: Maximum velocity as ratio of search space range.
|
|
85
|
+
Default: 0.2 (20% of range).
|
|
86
|
+
sampling: Operator for initial population generation.
|
|
87
|
+
repair: Repair operator for constraint handling.
|
|
88
|
+
adaptive: If True, hyperparameters (inertia, c1, c2) are
|
|
89
|
+
learnable via backpropagation.
|
|
90
|
+
differentiable: If True, particle positions are learnable
|
|
91
|
+
via backpropagation.
|
|
92
|
+
per_particle_coeffs: If True and adaptive=True, each particle
|
|
93
|
+
has its own inertia, c1, c2 values. Default: False.
|
|
94
|
+
dtype: Tensor dtype.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
inertia: Current inertia weight.
|
|
98
|
+
c1: Current cognitive coefficient.
|
|
99
|
+
c2: Current social coefficient.
|
|
100
|
+
velocity: Current velocity vectors [pop_size, n_var].
|
|
101
|
+
p_best: Personal best positions [pop_size, n_var].
|
|
102
|
+
p_best_fitness: Personal best fitness values [pop_size].
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> # Classical PSO
|
|
106
|
+
>>> pso = PSO(pop_size=50, inertia=0.7, c1=1.5, c2=1.5)
|
|
107
|
+
>>>
|
|
108
|
+
>>> # Adaptive PSO with learnable coefficients
|
|
109
|
+
>>> pso = PSO(pop_size=50, adaptive=True)
|
|
110
|
+
>>>
|
|
111
|
+
>>> # Differentiable particle positions
|
|
112
|
+
>>> pso = PSO(pop_size=50, differentiable=True)
|
|
113
|
+
>>>
|
|
114
|
+
>>> # Fully differentiable
|
|
115
|
+
>>> pso = PSO(pop_size=50, adaptive=True, differentiable=True)
|
|
116
|
+
>>>
|
|
117
|
+
>>> # Per-particle adaptive coefficients
|
|
118
|
+
>>> pso = PSO(pop_size=50, adaptive=True, per_particle_coeffs=True)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
pop_size: int = 100,
|
|
124
|
+
w: float = 0.7,
|
|
125
|
+
c1: float = 1.5,
|
|
126
|
+
c2: float = 1.5,
|
|
127
|
+
v_max_ratio: float = 0.2,
|
|
128
|
+
sampling: Optional[nn.Module] = None,
|
|
129
|
+
repair: Optional[nn.Module] = None,
|
|
130
|
+
adaptive: bool = False,
|
|
131
|
+
differentiable: bool = False,
|
|
132
|
+
per_particle_coeffs: bool = False,
|
|
133
|
+
dtype: torch.dtype = torch.float32,
|
|
134
|
+
) -> None:
|
|
135
|
+
self.adaptive = adaptive
|
|
136
|
+
self.per_particle_coeffs = per_particle_coeffs
|
|
137
|
+
self._init_inertia = w
|
|
138
|
+
self._init_c1 = c1
|
|
139
|
+
self._init_c2 = c2
|
|
140
|
+
self._v_max_ratio = v_max_ratio
|
|
141
|
+
|
|
142
|
+
# PSO doesn't use standard EA operators (selection, crossover, mutation)
|
|
143
|
+
super().__init__(
|
|
144
|
+
pop_size=pop_size,
|
|
145
|
+
sampling=sampling,
|
|
146
|
+
selection=None,
|
|
147
|
+
crossover=None,
|
|
148
|
+
mutation=None,
|
|
149
|
+
survival=None,
|
|
150
|
+
repair=repair,
|
|
151
|
+
eliminate_duplicates=False,
|
|
152
|
+
n_offsprings=pop_size,
|
|
153
|
+
differentiable=differentiable,
|
|
154
|
+
adaptive=adaptive,
|
|
155
|
+
dtype=dtype,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# =========================================================================
|
|
159
|
+
# Setup
|
|
160
|
+
# =========================================================================
|
|
161
|
+
|
|
162
|
+
def _setup(self) -> None:
|
|
163
|
+
"""PSO-specific setup after initialization."""
|
|
164
|
+
n_var = self.problem.n_var
|
|
165
|
+
N = self.pop_size
|
|
166
|
+
|
|
167
|
+
# Compute velocity bounds
|
|
168
|
+
search_range = self.xu - self.xl
|
|
169
|
+
v_max = self._v_max_ratio * search_range
|
|
170
|
+
v_min = -v_max
|
|
171
|
+
|
|
172
|
+
# Register velocity bounds
|
|
173
|
+
self.register_buffer("_v_max", v_max)
|
|
174
|
+
self.register_buffer("_v_min", v_min)
|
|
175
|
+
|
|
176
|
+
# Initialize velocities to zero
|
|
177
|
+
self.register_buffer(
|
|
178
|
+
"_velocity",
|
|
179
|
+
torch.zeros(N, n_var, device=self.device, dtype=self.dtype)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Initialize personal bests
|
|
183
|
+
self.register_buffer(
|
|
184
|
+
"_p_best",
|
|
185
|
+
self._population.clone().detach()
|
|
186
|
+
)
|
|
187
|
+
self.register_buffer(
|
|
188
|
+
"_p_best_fitness",
|
|
189
|
+
self.state.fitness.clone().detach()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Setup hyperparameters
|
|
193
|
+
self._setup_hyperparameters(N, n_var)
|
|
194
|
+
|
|
195
|
+
def _setup_hyperparameters(self, N: int, n_var: int) -> None:
|
|
196
|
+
"""Setup inertia, c1, c2 as learnable or fixed parameters."""
|
|
197
|
+
if self.adaptive:
|
|
198
|
+
if self.per_particle_coeffs:
|
|
199
|
+
# Per-particle coefficients [N, 1] for broadcasting
|
|
200
|
+
self._inertia = nn.Parameter(
|
|
201
|
+
torch.full((N, 1), self._init_inertia,
|
|
202
|
+
device=self.device, dtype=self.dtype)
|
|
203
|
+
)
|
|
204
|
+
self._c1 = nn.Parameter(
|
|
205
|
+
torch.full((N, 1), self._init_c1,
|
|
206
|
+
device=self.device, dtype=self.dtype)
|
|
207
|
+
)
|
|
208
|
+
self._c2 = nn.Parameter(
|
|
209
|
+
torch.full((N, 1), self._init_c2,
|
|
210
|
+
device=self.device, dtype=self.dtype)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Scalar coefficients (shared by all particles)
|
|
214
|
+
self._inertia = nn.Parameter(
|
|
215
|
+
torch.tensor(self._init_inertia,
|
|
216
|
+
device=self.device, dtype=self.dtype)
|
|
217
|
+
)
|
|
218
|
+
self._c1 = nn.Parameter(
|
|
219
|
+
torch.tensor(self._init_c1,
|
|
220
|
+
device=self.device, dtype=self.dtype)
|
|
221
|
+
)
|
|
222
|
+
self._c2 = nn.Parameter(
|
|
223
|
+
torch.tensor(self._init_c2,
|
|
224
|
+
device=self.device, dtype=self.dtype)
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
# Fixed coefficients (buffers)
|
|
228
|
+
self.register_buffer(
|
|
229
|
+
"_inertia",
|
|
230
|
+
torch.tensor(self._init_inertia, device=self.device, dtype=self.dtype)
|
|
231
|
+
)
|
|
232
|
+
self.register_buffer(
|
|
233
|
+
"_c1",
|
|
234
|
+
torch.tensor(self._init_c1, device=self.device, dtype=self.dtype)
|
|
235
|
+
)
|
|
236
|
+
self.register_buffer(
|
|
237
|
+
"_c2",
|
|
238
|
+
torch.tensor(self._init_c2, device=self.device, dtype=self.dtype)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# =========================================================================
|
|
242
|
+
# Properties
|
|
243
|
+
# =========================================================================
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def population(self) -> Tensor:
|
|
247
|
+
"""Current particle positions."""
|
|
248
|
+
return self._population
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def fitness(self) -> Tensor:
|
|
252
|
+
"""Current fitness values."""
|
|
253
|
+
return self.state.fitness
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def velocity(self) -> Tensor:
|
|
257
|
+
"""Current velocity vectors."""
|
|
258
|
+
return self._velocity
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def p_best(self) -> Tensor:
|
|
262
|
+
"""Personal best positions."""
|
|
263
|
+
return self._p_best
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def p_best_fitness(self) -> Tensor:
|
|
267
|
+
"""Personal best fitness values."""
|
|
268
|
+
return self._p_best_fitness
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def inertia(self) -> Tensor:
|
|
272
|
+
"""Current inertia weight."""
|
|
273
|
+
return self._inertia
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def c1(self) -> Tensor:
|
|
277
|
+
"""Current cognitive coefficient."""
|
|
278
|
+
return self._c1
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def c2(self) -> Tensor:
|
|
282
|
+
"""Current social coefficient."""
|
|
283
|
+
return self._c2
|
|
284
|
+
|
|
285
|
+
# =========================================================================
|
|
286
|
+
# Core PSO Methods
|
|
287
|
+
# =========================================================================
|
|
288
|
+
|
|
289
|
+
def _update_velocity(self) -> Tensor:
|
|
290
|
+
"""
|
|
291
|
+
Compute new velocities using the PSO velocity update equation.
|
|
292
|
+
|
|
293
|
+
v_new = w*v + c1*r1*(p_best - x) + c2*r2*(g_best - x)
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
New velocity vectors [pop_size, n_var].
|
|
297
|
+
"""
|
|
298
|
+
N, D = self.pop_size, self.n_var
|
|
299
|
+
|
|
300
|
+
# Random vectors
|
|
301
|
+
r1 = torch.rand(N, D, device=self.device, dtype=self.dtype)
|
|
302
|
+
r2 = torch.rand(N, D, device=self.device, dtype=self.dtype)
|
|
303
|
+
|
|
304
|
+
# Get global best
|
|
305
|
+
g_best = self.state.best_solution
|
|
306
|
+
if g_best is None:
|
|
307
|
+
best_idx = torch.argmin(self._p_best_fitness)
|
|
308
|
+
g_best = self._p_best[best_idx]
|
|
309
|
+
|
|
310
|
+
# Velocity components
|
|
311
|
+
inertia_term = self.inertia * self._velocity
|
|
312
|
+
cognitive_term = self.c1 * r1 * (self._p_best - self.population)
|
|
313
|
+
social_term = self.c2 * r2 * (g_best.unsqueeze(0) - self.population)
|
|
314
|
+
|
|
315
|
+
# New velocity
|
|
316
|
+
v_new = inertia_term + cognitive_term + social_term
|
|
317
|
+
|
|
318
|
+
# Clamp velocity
|
|
319
|
+
v_new = torch.clamp(v_new, self._v_min, self._v_max)
|
|
320
|
+
|
|
321
|
+
return v_new
|
|
322
|
+
|
|
323
|
+
def _update_position(self, velocity: Tensor) -> Tensor:
|
|
324
|
+
"""
|
|
325
|
+
Update particle positions based on velocity.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
velocity: Velocity vectors [pop_size, n_var].
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
New positions [pop_size, n_var].
|
|
332
|
+
"""
|
|
333
|
+
new_pos = self.population + velocity
|
|
334
|
+
return new_pos
|
|
335
|
+
|
|
336
|
+
def _reflect_bounds(self, position: Tensor, velocity: Tensor) -> tuple:
|
|
337
|
+
"""
|
|
338
|
+
Handle boundary violations by reflection.
|
|
339
|
+
|
|
340
|
+
When a particle hits a boundary, it bounces back and its
|
|
341
|
+
velocity component is reversed.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
position: Particle positions [pop_size, n_var].
|
|
345
|
+
velocity: Particle velocities [pop_size, n_var].
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Tuple of (repaired_position, repaired_velocity).
|
|
349
|
+
"""
|
|
350
|
+
# Detect violations
|
|
351
|
+
below = position < self.xl
|
|
352
|
+
above = position > self.xu
|
|
353
|
+
|
|
354
|
+
# Reflect positions
|
|
355
|
+
pos_repaired = position.clone()
|
|
356
|
+
pos_repaired = torch.where(below, 2 * self.xl - position, pos_repaired)
|
|
357
|
+
pos_repaired = torch.where(above, 2 * self.xu - position, pos_repaired)
|
|
358
|
+
|
|
359
|
+
# Clamp to ensure within bounds (in case of large violations)
|
|
360
|
+
pos_repaired = torch.clamp(pos_repaired, self.xl, self.xu)
|
|
361
|
+
|
|
362
|
+
# Reverse velocity at boundaries
|
|
363
|
+
vel_repaired = velocity.clone()
|
|
364
|
+
vel_repaired = torch.where(below | above, -velocity, vel_repaired)
|
|
365
|
+
|
|
366
|
+
return pos_repaired, vel_repaired
|
|
367
|
+
|
|
368
|
+
def _update_personal_best(
|
|
369
|
+
self,
|
|
370
|
+
new_pos: Tensor,
|
|
371
|
+
new_fitness: Tensor,
|
|
372
|
+
) -> tuple:
|
|
373
|
+
"""
|
|
374
|
+
Update personal best positions where improved.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
new_pos: New particle positions [pop_size, n_var].
|
|
378
|
+
new_fitness: Fitness at new positions [pop_size].
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Tuple of (new_p_best, new_p_best_fitness).
|
|
382
|
+
"""
|
|
383
|
+
improved = new_fitness < self._p_best_fitness
|
|
384
|
+
|
|
385
|
+
new_p_best = torch.where(
|
|
386
|
+
improved.unsqueeze(-1),
|
|
387
|
+
new_pos,
|
|
388
|
+
self._p_best
|
|
389
|
+
)
|
|
390
|
+
new_p_best_fitness = torch.where(
|
|
391
|
+
improved,
|
|
392
|
+
new_fitness,
|
|
393
|
+
self._p_best_fitness
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return new_p_best, new_p_best_fitness
|
|
397
|
+
|
|
398
|
+
def _infill(self) -> Tensor:
|
|
399
|
+
"""
|
|
400
|
+
Generate new particle positions through velocity update.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
New positions [pop_size, n_var].
|
|
404
|
+
"""
|
|
405
|
+
# 1. Update velocities
|
|
406
|
+
new_velocity = self._update_velocity()
|
|
407
|
+
|
|
408
|
+
# 2. Update positions
|
|
409
|
+
new_pos = self._update_position(new_velocity)
|
|
410
|
+
|
|
411
|
+
# 3. Handle boundary violations
|
|
412
|
+
if self.repair is not None:
|
|
413
|
+
new_pos = self.repair(new_pos, self.xl, self.xu)
|
|
414
|
+
# Recompute velocity to match repaired position
|
|
415
|
+
new_velocity = new_pos - self.population
|
|
416
|
+
else:
|
|
417
|
+
new_pos, new_velocity = self._reflect_bounds(new_pos, new_velocity)
|
|
418
|
+
|
|
419
|
+
# Store velocity for state update
|
|
420
|
+
self._pending_velocity = new_velocity
|
|
421
|
+
|
|
422
|
+
return new_pos
|
|
423
|
+
|
|
424
|
+
def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Update swarm state with new positions.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
offspring: New particle positions [pop_size, n_var].
|
|
430
|
+
offspring_fitness: Fitness at new positions [pop_size].
|
|
431
|
+
"""
|
|
432
|
+
# Update personal bests
|
|
433
|
+
new_p_best, new_p_best_fitness = self._update_personal_best(
|
|
434
|
+
offspring, offspring_fitness
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Update state
|
|
438
|
+
self._update_state(
|
|
439
|
+
new_pos=offspring,
|
|
440
|
+
new_fitness=offspring_fitness,
|
|
441
|
+
new_velocity=self._pending_velocity,
|
|
442
|
+
new_p_best=new_p_best,
|
|
443
|
+
new_p_best_fitness=new_p_best_fitness,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Update global best
|
|
447
|
+
self.state.update_best(self.population, self.state.fitness)
|
|
448
|
+
|
|
449
|
+
# Cleanup
|
|
450
|
+
if hasattr(self, '_pending_velocity'):
|
|
451
|
+
del self._pending_velocity
|
|
452
|
+
|
|
453
|
+
def _update_state(
|
|
454
|
+
self,
|
|
455
|
+
new_pos: Tensor,
|
|
456
|
+
new_fitness: Tensor,
|
|
457
|
+
new_velocity: Tensor,
|
|
458
|
+
new_p_best: Tensor,
|
|
459
|
+
new_p_best_fitness: Tensor,
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Update all PSO state tensors.
|
|
462
|
+
|
|
463
|
+
In differentiable mode the SGD optimiser has already nudged
|
|
464
|
+
``self._population`` by a gradient step (P₀ → P₀ − lr·∇).
|
|
465
|
+
The PSO velocity update produced ``new_pos = P₀ + v``.
|
|
466
|
+
We combine both forces so that the final position is
|
|
467
|
+
``P₀ + v − lr·∇``, preserving the gradient correction that
|
|
468
|
+
would otherwise be overwritten by ``copy_(new_pos)``.
|
|
469
|
+
"""
|
|
470
|
+
with torch.no_grad():
|
|
471
|
+
if self.differentiable and isinstance(self._population, nn.Parameter):
|
|
472
|
+
# Recover pre-velocity positions and extract the gradient delta
|
|
473
|
+
old_pos = new_pos - new_velocity # P₀
|
|
474
|
+
grad_delta = self._population.data - old_pos # −lr·∇
|
|
475
|
+
combined = new_pos + grad_delta # P₀ + v − lr·∇
|
|
476
|
+
combined = torch.clamp(combined, self.xl, self.xu)
|
|
477
|
+
self._population.copy_(combined)
|
|
478
|
+
else:
|
|
479
|
+
self._population.copy_(new_pos)
|
|
480
|
+
self._velocity.copy_(new_velocity)
|
|
481
|
+
self._p_best.copy_(new_p_best)
|
|
482
|
+
self._p_best_fitness.copy_(new_p_best_fitness)
|
|
483
|
+
|
|
484
|
+
self.state.fitness = new_fitness
|
|
485
|
+
self.state.population = self._population
|
|
486
|
+
|
|
487
|
+
# =========================================================================
|
|
488
|
+
# Hyperparameter Management
|
|
489
|
+
# =========================================================================
|
|
490
|
+
|
|
491
|
+
@torch.no_grad()
|
|
492
|
+
def _clamp_hyperparams(self) -> None:
|
|
493
|
+
"""Clamp learnable hyperparameters to valid ranges."""
|
|
494
|
+
if self.adaptive:
|
|
495
|
+
# Inertia in [0, 1.5]
|
|
496
|
+
self._inertia.clamp_(min=0.0, max=1.5)
|
|
497
|
+
# c1, c2 in [0, 4]
|
|
498
|
+
self._c1.clamp_(min=0.0, max=4.0)
|
|
499
|
+
self._c2.clamp_(min=0.0, max=4.0)
|
|
500
|
+
|
|
501
|
+
def update_state(self) -> None:
|
|
502
|
+
"""Commit pending changes and clamp hyperparameters."""
|
|
503
|
+
super().update_state()
|
|
504
|
+
self._clamp_hyperparams()
|
|
505
|
+
|
|
506
|
+
def _get_hyperparams(self) -> Dict[str, Any]:
|
|
507
|
+
"""Return current hyperparameter values."""
|
|
508
|
+
def _to_float(x: Tensor) -> float:
|
|
509
|
+
if x.numel() == 1:
|
|
510
|
+
return float(x.item())
|
|
511
|
+
return float(x.mean().item())
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
'pop_size': self.pop_size,
|
|
515
|
+
'inertia': _to_float(self.inertia),
|
|
516
|
+
'c1': _to_float(self.c1),
|
|
517
|
+
'c2': _to_float(self.c2),
|
|
518
|
+
'v_max_ratio': self._v_max_ratio,
|
|
519
|
+
'adaptive': self.adaptive,
|
|
520
|
+
'differentiable': self.differentiable,
|
|
521
|
+
'per_particle_coeffs': self.per_particle_coeffs,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# =========================================================================
|
|
525
|
+
# String Representation
|
|
526
|
+
# =========================================================================
|
|
527
|
+
|
|
528
|
+
def __repr__(self) -> str:
|
|
529
|
+
def _fmt(x: Tensor) -> str:
|
|
530
|
+
if x.numel() == 1:
|
|
531
|
+
return f"{float(x.item()):.3f}"
|
|
532
|
+
return f"{float(x.mean().item()):.3f}"
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
f"PSO(pop_size={self.pop_size}, "
|
|
536
|
+
f"w={_fmt(self.inertia)}, "
|
|
537
|
+
f"c1={_fmt(self.c1)}, "
|
|
538
|
+
f"c2={_fmt(self.c2)}, "
|
|
539
|
+
f"adaptive={self.adaptive}, "
|
|
540
|
+
f"differentiable={self.differentiable})"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# =============================================================================
|
|
545
|
+
# Convenience Factory Functions
|
|
546
|
+
# =============================================================================
|
|
547
|
+
|
|
548
|
+
def pso_default(
|
|
549
|
+
pop_size: int = 100,
|
|
550
|
+
adaptive: bool = False,
|
|
551
|
+
differentiable: bool = False,
|
|
552
|
+
**kwargs,
|
|
553
|
+
) -> PSO:
|
|
554
|
+
"""
|
|
555
|
+
Create PSO with default settings.
|
|
556
|
+
|
|
557
|
+
Uses standard coefficients:
|
|
558
|
+
- inertia = 0.7
|
|
559
|
+
- c1 = c2 = 1.5
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
pop_size: Swarm size.
|
|
563
|
+
adaptive: If True, hyperparameters are learnable.
|
|
564
|
+
differentiable: If True, positions are learnable.
|
|
565
|
+
**kwargs: Additional arguments passed to PSO.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Configured PSO instance.
|
|
569
|
+
"""
|
|
570
|
+
return PSO(
|
|
571
|
+
pop_size=pop_size,
|
|
572
|
+
w=0.7,
|
|
573
|
+
c1=1.5,
|
|
574
|
+
c2=1.5,
|
|
575
|
+
adaptive=adaptive,
|
|
576
|
+
differentiable=differentiable,
|
|
577
|
+
**kwargs,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def pso_constriction(
|
|
582
|
+
pop_size: int = 100,
|
|
583
|
+
adaptive: bool = False,
|
|
584
|
+
differentiable: bool = False,
|
|
585
|
+
**kwargs,
|
|
586
|
+
) -> PSO:
|
|
587
|
+
"""
|
|
588
|
+
Create PSO with constriction coefficients.
|
|
589
|
+
|
|
590
|
+
Uses Clerc's constriction factor approach:
|
|
591
|
+
- inertia = 0.7298
|
|
592
|
+
- c1 = c2 = 1.4962
|
|
593
|
+
|
|
594
|
+
This configuration provides guaranteed convergence.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
pop_size: Swarm size.
|
|
598
|
+
adaptive: If True, hyperparameters are learnable.
|
|
599
|
+
differentiable: If True, positions are learnable.
|
|
600
|
+
**kwargs: Additional arguments passed to PSO.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Configured PSO instance.
|
|
604
|
+
|
|
605
|
+
Reference:
|
|
606
|
+
Clerc, M. & Kennedy, J. (2002). The particle swarm - explosion,
|
|
607
|
+
stability, and convergence in a multidimensional complex space.
|
|
608
|
+
"""
|
|
609
|
+
# Constriction coefficients
|
|
610
|
+
phi = 4.1
|
|
611
|
+
chi = 2.0 / abs(2.0 - phi - (phi**2 - 4*phi)**0.5)
|
|
612
|
+
|
|
613
|
+
return PSO(
|
|
614
|
+
pop_size=pop_size,
|
|
615
|
+
w=chi, # ~0.7298
|
|
616
|
+
c1=chi * 2.05, # ~1.4962
|
|
617
|
+
c2=chi * 2.05, # ~1.4962
|
|
618
|
+
adaptive=adaptive,
|
|
619
|
+
differentiable=differentiable,
|
|
620
|
+
**kwargs,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def pso_adaptive(
|
|
625
|
+
pop_size: int = 100,
|
|
626
|
+
per_particle: bool = False,
|
|
627
|
+
differentiable: bool = False,
|
|
628
|
+
**kwargs,
|
|
629
|
+
) -> PSO:
|
|
630
|
+
"""
|
|
631
|
+
Create PSO with adaptive (learnable) hyperparameters.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
pop_size: Swarm size.
|
|
635
|
+
per_particle: If True, each particle has its own coefficients.
|
|
636
|
+
differentiable: If True, positions are also learnable.
|
|
637
|
+
**kwargs: Additional arguments passed to PSO.
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
Configured PSO instance with adaptive=True.
|
|
641
|
+
"""
|
|
642
|
+
return PSO(
|
|
643
|
+
pop_size=pop_size,
|
|
644
|
+
adaptive=True,
|
|
645
|
+
differentiable=differentiable,
|
|
646
|
+
per_particle_coeffs=per_particle,
|
|
647
|
+
**kwargs,
|
|
648
|
+
)
|