1
+ """
2
+ AdaptiveParticleSwarm(n_particles = 3,
3
+ c1 = 2.0,
4
+ c2 = 2.0,
5
+ prob_shift = 0.25,
6
+ rng = Random.GLOBAL_RNG)
7
+
8
+ Instantiate an adaptive particle swarm optimization tuning strategy. A swarm is
9
+ initiated by sampling hyperparameters with their customizable priors, and new
10
+ models are generated by referencing each member's and the swarm's best models so
11
+ far.
12
+
13
+ ### Supported Ranges and Discrete Hyperparameter Handling
14
+
15
+ See [`ParticleSwarm`](@ref) for more information about supported ranges and how
16
+ discrete hyperparameters are handled.
17
+
18
+ ### Algorithm
19
+
20
+ Hyperparameter ranges are sampled and concatenated into position vectors for
21
+ each swarm particle. Velocity is initiated to be zeros, and in each iteration,
22
+ every particle's position is updated to approach its personal best and the
23
+ swarm's best models so far with the equations:
24
+
25
+ \$ vₖ₊₁ = w⋅vₖ + c₁⋅rand()⋅(pbest - xₖ) + c₂⋅rand()⋅(gbest - xₖ)\$
26
+
27
+ \$ xₖ₊₁ = xₖ + vₖ₊₁\$
28
+
29
+ Coefficients `w`, `c1`, `c2` are adaptively adjusted at each iteration by
30
+ determining the evolutionary phase of the swarm. We calculate the evolutionary
31
+ factor by comparing the mean distance from each particle to other members of the
32
+ swarm. This factor is then used to classify whether the swarm is in exploration,
33
+ exploitation, convergence, or jumping out phase and calibrate the tuning
34
+ hyperparameters accordingly. For more information, refer to "Adaptive Particle
35
+ Swarm Optimiztion" by Zhan, Zhang, Li, and Chung. Note that we omit the elitist
36
+ learning strategy in the paper.
37
+
38
+ New models are then generated for evaluation by mutating the fields of a deep
39
+ copy of `model`. If the corresponding range has a specified `scale` function,
40
+ then the transformation is applied before the hyperparameter is returned. If
41
+ `scale` is a symbol (eg, `:log`), it is ignored.
42
+ """
43
+ mutable struct AdaptiveParticleSwarm{R<: AbstractRNG } <: AbstractParticleSwarm
44
+ n_particles:: Int
45
+ c1:: Float64
46
+ c2:: Float64
47
+ prob_shift:: Float64
48
+ rng:: R
49
+ end
50
+
51
+ # Constructor
52
+
53
+ function AdaptiveParticleSwarm (;
54
+ n_particles= 3 ,
55
+ c1= 2.0 ,
56
+ c2= 2.0 ,
57
+ prob_shift= 0.25 ,
58
+ rng:: R = Random. GLOBAL_RNG
59
+ ) where {R}
60
+ swarm = AdaptiveParticleSwarm {R} (n_particles, c1, c2, prob_shift, rng)
61
+ message = MLJTuning. clean! (swarm)
62
+ isempty (message) || @warn message
63
+ return swarm
64
+ end
65
+
66
+ # Validate tuning hyperparameters
67
+
68
+ function MLJTuning. clean! (tuning:: AdaptiveParticleSwarm )
69
+ warning = " "
70
+ if tuning. n_particles < 3
71
+ warning *= " AdaptiveParticleSwarm requires at least 3 particles. " *
72
+ " Resetting n_particles=3. "
73
+ tuning. n_particles = 3
74
+ end
75
+ c1, c2 = tuning. c1, tuning. c2
76
+ if ! (1.5 ≤ c1 ≤ 2.5 ) || ! (1.5 ≤ c2 ≤ 2.5 ) || (c1 + c2 > 4 )
77
+ c1, c2 = _clamp_coefficients (c1, c2)
78
+ warning *= " AdaptiveParticleSwarm requires 1.5 ≤ c1 ≤ 2.5, 1.5 ≤ c2 ≤ 2.5, and " *
79
+ " c1 + c2 ≤ 4. Resetting coefficients c1=$(c1) , c2=$(c2) . "
80
+ tuning. c1 = c1
81
+ tuning. c2 = c2
82
+ end
83
+ if ! (0 ≤ tuning. prob_shift < 1 )
84
+ warning *= " AdaptiveParticleSwarm requires 0 ≤ prob_shift < 1. " *
85
+ " Resetting prob_shift=0.25. "
86
+ tuning. prob_shift = 0.25
87
+ end
88
+ return warning
89
+ end
90
+
91
+ # Helper function to clamp swarm coefficients in the interval [1.5, 2.5] with a sum of less
92
+ # than or equal to 4
93
+
94
+ function _clamp_coefficients (c1, c2)
95
+ c1 = min (max (c1, 1.5 ), 2.5 )
96
+ c2 = min (max (c2, 1.5 ), 2.5 )
97
+ scale = 4. / (c1 + c2)
98
+ if scale < 1
99
+ c1 *= scale
100
+ c2 *= scale
101
+ end
102
+ return c1, c2
103
+ end
104
+
105
+ # Initial state
106
+
107
+ function MLJTuning. setup (tuning:: AdaptiveParticleSwarm , model, ranges, n, verbosity)
108
+ # state, evolutionary phase, swarm coefficients
109
+ return (initialize (ranges, tuning), nothing , tuning. c1, tuning. c2)
110
+ end
111
+
112
+ # New models
113
+
114
+ function MLJTuning. models (
115
+ tuning:: AdaptiveParticleSwarm ,
116
+ model,
117
+ history,
118
+ (state, phase, c1, c2),
119
+ n_remaining,
120
+ verbosity
121
+ )
122
+ n_particles = tuning. n_particles
123
+ if ! isnothing (history)
124
+ sig = MLJTuning. signature (history[1 ]. measure[1 ])
125
+ measurements = similar (state. pbest)
126
+ map (history[end - n_particles+ 1 : end ]) do h
127
+ measurements[h. metadata] = sig * h. measurement[1 ]
128
+ end
129
+ pbest! (state, measurements, tuning)
130
+ gbest! (state)
131
+ f = _evolutionary_factor (state. X, argmin (state. pbest))
132
+ phase = _evolutionary_phase (f, phase)
133
+ w, c1, c2 = _adapt_parameters (tuning. rng, c1, c2, f, phase)
134
+ move! (tuning. rng, state, w, c1, c2)
135
+ end
136
+ retrieve! (state, tuning)
137
+ fields = getproperty .(state. ranges, :field )
138
+ new_models = map (1 : n_particles) do i
139
+ clone = deepcopy (model)
140
+ for (field, param) in zip (fields, getindex .(state. parameters, i))
141
+ recursive_setproperty! (clone, field, param)
142
+ end
143
+ (clone, i)
144
+ end
145
+ return new_models, (state, phase, c1, c2)
146
+ end
147
+
148
+ # Helper function to calculate the evolutionary factor and phase
149
+
150
+ function _evolutionary_factor (X, gbest_i)
151
+ n_particles = size (X, 1 )
152
+ dists = zeros (n_particles, n_particles)
153
+ for i in 1 : n_particles
154
+ for j in i+ 1 : n_particles
155
+ dists[j, i] = dists[i, j] = norm (X[i, :] - X[j, :])
156
+ end
157
+ end
158
+ mean_dists = sum (dists, dims= 2 ) / (n_particles - 1 )
159
+ min_dist, max_dist = extrema (mean_dists)
160
+ gbest_dist = mean_dists[gbest_i]
161
+ f = (gbest_dist - min_dist) / max (max_dist - min_dist, sqrt (eps ()))
162
+ return f
163
+ end
164
+
165
+ function _evolutionary_phase (f, phase)
166
+ # Classify evolutionary phase
167
+ μs = [μ₁ (f), μ₂ (f), μ₃ (f), μ₄ (f)]
168
+ if phase === nothing # first iteration
169
+ phase = argmax (μs)
170
+ else
171
+ next_phase = mod1 (phase + 1 , 4 )
172
+ # switch to next phase if possible
173
+ if μs[next_phase] > 0
174
+ phase = next_phase
175
+ # stay in current phase is possible, else pick the most likely phase
176
+ elseif μs[phase] == 0
177
+ phase = argmax (μs)
178
+ end
179
+ end
180
+ return phase
181
+ end
182
+
183
+ # Helper functions to calculate probabilities of the four evolutionary states
184
+
185
+ μ₁ (f) = f ≤ 0.4 ? 0.0 :
186
+ f ≤ 0.6 ? 5 * f - 2 :
187
+ f ≤ 0.7 ? 1 :
188
+ f ≤ 0.8 ? - 10 * f + 8 :
189
+ 0.0
190
+
191
+ μ₂ (f) = f ≤ 0.2 ? 0.0 :
192
+ f ≤ 0.3 ? 10 * f - 2 :
193
+ f ≤ 0.4 ? 1.0 :
194
+ f ≤ 0.6 ? - 5 * f + 3 :
195
+ 0.0
196
+
197
+ μ₃ (f) = f ≤ 0.1 ? 1.0 :
198
+ f ≤ 0.3 ? - 5 * f + 1.5 :
199
+ 0.0
200
+
201
+ μ₄ (f) = f ≤ 0.7 ? 0.0 :
202
+ f ≤ 0.9 ? 5 * f - 3.5 :
203
+ 1.0
204
+
205
+ # Adaptive control of swarm's parameters
206
+
207
+ function _adapt_parameters (rng, c1, c2, f, phase)
208
+ w = 1.0 / (1.0 + 1.5 * exp (- 2.6 * f)) # update inertia
209
+ δ = rand (rng) * 0.05 + 0.05 # coefficient acceleration
210
+ if phase === 1 # exploration
211
+ c1 += δ
212
+ c2 -= δ
213
+ elseif phase === 2 # exploitation
214
+ δ *= 0.5
215
+ c1 += δ
216
+ c2 -= δ
217
+ elseif phase === 3 # convergence
218
+ δ *= 0.5
219
+ c1 += δ
220
+ c2 += δ
221
+ else # jumping out
222
+ c1 -= δ
223
+ c2 += δ
224
+ end
225
+ c1, c2 = _clamp_coefficients (c1, c2)
226
+ return w, c1, c2
227
+ end
0 commit comments