1- from __future__ import division , absolute_import , print_function
1+ #!/usr/bin/python
2+ # -*- coding: utf-8 -*-
3+ ##
4+ # test_models.py: Simple models for testing inference engines.
5+ ##
6+ # © 2017 Alan Robertson (arob8086@uni.sydney.edu.au)
7+ #
8+ # This file is a part of the Qinfer project.
9+ # Licensed under the AGPL version 3.
10+ ##
11+ # This program is free software: you can redistribute it and/or modify
12+ # it under the terms of the GNU Affero General Public License as published by
13+ # the Free Software Foundation, either version 3 of the License, or
14+ # (at your option) any later version.
15+ #
16+ # This program is distributed in the hope that it will be useful,
17+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+ # GNU Affero General Public License for more details.
20+ #
21+ # You should have received a copy of the GNU Affero General Public License
22+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
23+ ##
24+
25+ ## FEATURES ##################################################################
26+
27+ from __future__ import division
28+ from __future__ import absolute_import
29+ from __future__ import print_function
30+
31+ ## IMPORTS ###################################################################
232
333import random
434import numpy as np
535from functools import partial
636from qinfer .perf_testing import perf_test_multiple , apply_serial
737from qinfer import distributions
838
39+ ## CLASSES ####################################################################
40+
941class Optimiser (object ):
1042 '''
1143 A generic optimiser class that is inherited by the other optimisation functions.
@@ -52,8 +84,11 @@ class ParticleSwarmOptimiser(Optimiser):
5284 A particle swarm optimisation based hyperheuristic
5385 :param integer n_pso_iterations:
5486 :param integer n_pso_particles:
55- :param
56- :param
87+ :param QInferDistribution initial_velocity_distribution:
88+ :param QInferDistribution initial_velocity_distribution:
89+ :param double
90+ :param double
91+ :param function
5792 '''
5893
5994 def __call__ (self ,
@@ -66,13 +101,9 @@ def __call__(self,
66101 phi_g = 0.5 ,
67102 apply = apply_serial
68103 ):
69- self ._fitness_dt = np .dtype ([
70- ('params' , np .float64 , (self ._n_free_params ,)),
71- ('velocities' , np .float64 , (self ._n_free_params ,)),
72- ('fitness' , np .float64 )])
73- self ._fitness = np .empty ([n_pso_iterations , n_pso_particles ], dtype = self ._fitness_dt )
74- local_attractors = np .empty ([n_pso_particles ], dtype = self ._fitness_dt )
75- global_attractor = np .empty ([1 ], dtype = self ._fitness_dt )
104+ self ._fitness = np .empty ([n_pso_iterations , n_pso_particles ], dtype = self .fitness_dt ())
105+ local_attractors = np .empty ([n_pso_particles ], dtype = self .fitness_dt ())
106+ global_attractor = np .empty ([1 ], dtype = self .fitness_dt ())
76107
77108 if initial_position_distribution is None :
78109 initial_position_distribution = distributions .UniformDistribution (np .array ([[ 0 , 1 ]] * self ._n_free_params ));
@@ -161,7 +192,258 @@ def update_attractors(self, particles, local_attractors, global_attractor):
161192 local_attractors [idx ] = particle
162193 global_attractor = local_attractors [np .argmin (local_attractors ["fitness" ])]
163194 return local_attractors , global_attractor
195+
196+ def fitness_dt (self ):
197+ return np .dtype ([
198+ ('params' , np .float64 , (self ._n_free_params ,)),
199+ ('velocities' , np .float64 , (self ._n_free_params ,)),
200+ ('fitness' , np .float64 )])
201+
202+
203+ class ParticleSwarmSimpleAnnealingOptimiser (ParticleSwarmOptimiser ):
204+
205+ def __call__ (self ,
206+ n_pso_iterations = 50 ,
207+ n_pso_particles = 60 ,
208+ initial_position_distribution = None ,
209+ initial_velocity_distribution = None ,
210+ omega_v = 0.35 ,
211+ phi_p = 0.25 ,
212+ phi_g = 0.5 ,
213+ temperature = 0.95 ,
214+ apply = apply_serial
215+ ):
216+ self ._fitness = np .empty ([n_pso_iterations , n_pso_particles ], dtype = self .fitness_dt ())
217+ local_attractors = np .empty ([n_pso_particles ], dtype = self .fitness_dt ())
218+ global_attractor = np .empty ([1 ], dtype = self .fitness_dt ())
219+
220+ if initial_position_distribution is None :
221+ initial_position_distribution = distributions .UniformDistribution (np .array ([[ 0 , 1 ]] * self ._n_free_params ));
222+
223+ if initial_velocity_distribution is None :
224+ initial_velocity_distribution = distributions .UniformDistribution (np .array ([[- 1 , 1 ]] * self ._n_free_params ))
225+
226+ # Initial particle positions
227+ self ._fitness [0 ]["params" ] = initial_position_distribution .sample (n_pso_particles )
228+
229+ # Apply the boundary conditions if any exist
230+ if self ._boundary_map is not None :
231+ self ._fitness [itr ]["params" ] = self ._boundary_map (self ._fitness [itr ]["params" ])
232+
233+ # Calculate the initial particle fitnesses
234+ self ._fitness [0 ]["fitness" ] = self .evaluate_fitness (self ._fitness [0 ]["params" ],
235+ apply = apply )
236+
237+ # Calculate the positions of the attractors
238+ local_attractors = self ._fitness [0 ]
239+ local_attractors , global_attractor = self .update_attractors (
240+ self ._fitness [0 ],
241+ local_attractors ,
242+ global_attractor )
243+
244+ # Initial particle velocities
245+ self ._fitness [0 ]["velocities" ] = initial_velocity_distribution .sample (n_pso_particles )
246+ self ._fitness [0 ]["velocities" ] = self .update_velocities (
247+ self ._fitness [0 ]["params" ],
248+ self ._fitness [0 ]["velocities" ],
249+ local_attractors ["params" ],
250+ global_attractor ["params" ],
251+ omega_v , phi_p , phi_g )
252+
253+ for itr in range (1 , n_pso_iterations ):
254+ #Update the particle positions
255+ self ._fitness [itr ]["params" ] = self .update_positions (
256+ self ._fitness [itr - 1 ]["params" ],
257+ self ._fitness [itr - 1 ]["velocities" ])
258+
259+ # Apply the boundary conditions if any exist
260+ if self ._boundary_map is not None :
261+ self ._fitness [itr ]["params" ] = self ._boundary_map (self ._fitness [itr ]["params" ])
262+
263+ # Recalculate the fitness function
264+ self ._fitness [itr ]["fitness" ] = self .evaluate_fitness (
265+ self ._fitness [itr ]["params" ],
266+ apply = apply )
267+
268+ # Find the new attractors
269+ local_attractors , global_attractor = self .update_attractors (
270+ self ._fitness [itr ],
271+ local_attractors ,
272+ global_attractor )
273+
274+ # Update the velocities
275+ self ._fitness [itr ]["velocities" ] = self .update_velocities (
276+ self ._fitness [itr ]["params" ],
277+ self ._fitness [itr - 1 ]["velocities" ],
278+ local_attractors ["params" ],
279+ global_attractor ["params" ],
280+ omega_v , phi_p , phi_g )
281+
282+ # Update the PSO params
283+ omega_v , phi_p , phi_g = self .update_pso_params (
284+ temperature ,
285+ omega_v ,
286+ phi_p ,
287+ phi_g )
288+
289+ return global_attractor
290+
291+ def update_pso_params (self , temperature , omega_v , phi_p , phi_g ):
292+ omega_v , phi_p , phi_g = np .multiply (temperature , [omega_v , phi_p , phi_g ])
293+ return omega_v , phi_p , phi_g
294+
295+
296+ class ParticleSwarmTemperingOptimiser (ParticleSwarmOptimiser ):
297+ '''
298+ A particle swarm optimisation based hyperheuristic
299+ :param integer n_pso_iterations:
300+ :param integer n_pso_particles:
301+ :param QInferDistribution initial_velocity_distribution:
302+ :param QInferDistribution initial_velocity_distribution:
303+ :param double
304+ :param double
305+ :param function
306+ '''
164307
308+ def __call__ (self ,
309+ n_pso_iterations = 50 ,
310+ n_pso_particles = 60 ,
311+ initial_position_distribution = None ,
312+ initial_velocity_distribution = None ,
313+ n_temper_categories = 6 ,
314+ temper_frequency = 10 ,
315+ temper_params = None ,
316+ apply = apply_serial
317+ ):
318+ self ._fitness = np .empty ([n_pso_iterations , n_pso_particles ], dtype = self .fitness_dt ())
319+ local_attractors = np .empty ([n_pso_particles ], dtype = self .fitness_dt ())
320+ global_attractor = np .empty ([1 ], dtype = self .fitness_dt ())
321+
322+ if initial_position_distribution is None :
323+ initial_position_distribution = distributions .UniformDistribution (np .array ([[ 0 , 1 ]] * self ._n_free_params ));
324+
325+ if initial_velocity_distribution is None :
326+ initial_velocity_distribution = distributions .UniformDistribution (np .array ([[- 1 , 1 ]] * self ._n_free_params ))
327+
328+ if temper_params is None :
329+ omega_v = np .random .random (n_temper_categories )
330+ phi_p = np .random .random (n_temper_categories )
331+ phi_g = np .random .random (n_temper_categories )
332+ temper_params = [np .array ((params ), dtype = self .temper_params_dt ()) for params in zip (omega_v , phi_p , phi_g )]
333+
334+ # Distribute the particles into different temper categories
335+ temper_map = self .distribute_particles (n_pso_particles , n_temper_categories )
336+
337+ # Initial particle positions
338+ self ._fitness [0 ]["params" ] = initial_position_distribution .sample (n_pso_particles )
339+
340+ # Apply the boundary conditions if any exist
341+ if self ._boundary_map is not None :
342+ self ._fitness [itr ]["params" ] = self ._boundary_map (self ._fitness [itr ]["params" ])
343+
344+ # Calculate the initial particle fitnesses
345+ self ._fitness [0 ]["fitness" ] = self .evaluate_fitness (self ._fitness [0 ]["params" ],
346+ apply = apply )
347+
348+ # Calculate the positions of the attractors
349+ local_attractors = self ._fitness [0 ]
350+ local_attractors , global_attractor = self .update_attractors (
351+ self ._fitness [0 ],
352+ local_attractors ,
353+ global_attractor )
354+
355+ # Initial particle velocities
356+ self ._fitness [0 ]["velocities" ] = initial_velocity_distribution .sample (n_pso_particles )
357+
358+ # Update the velocities using the temper map
359+ for idx , temper_category in enumerate (temper_map ):
360+ self ._fitness [0 ]["velocities" ][temper_category ] = self .update_velocities (
361+ self ._fitness [0 ]["params" ][temper_category ],
362+ self ._fitness [0 ]["velocities" ][temper_category ],
363+ local_attractors ["params" ][temper_category ],
364+ global_attractor ["params" ],
365+ temper_params [idx ]["omega_v" ],
366+ temper_params [idx ]["phi_p" ],
367+ temper_params [idx ]["phi_g" ])
368+
369+ for itr in range (1 , n_pso_iterations ):
370+ # Update the particle positions
371+ self ._fitness [itr ]["params" ] = self .update_positions (
372+ self ._fitness [itr - 1 ]["params" ],
373+ self ._fitness [itr - 1 ]["velocities" ])
374+
375+ # Apply the boundary conditions if any exist
376+ if self ._boundary_map is not None :
377+ self ._fitness [itr ]["params" ] = self ._boundary_map (self ._fitness [itr ]["params" ])
378+
379+ # Recalculate the fitness function
380+ self ._fitness [itr ]["fitness" ] = self .evaluate_fitness (
381+ self ._fitness [itr ]["params" ],
382+ apply = apply )
383+
384+ # Find the new attractors
385+ local_attractors , global_attractor = self .update_attractors (
386+ self ._fitness [itr ],
387+ local_attractors ,
388+ global_attractor )
389+
390+ # Update the velocities
391+ for idx , temper_category in enumerate (temper_map ):
392+ self ._fitness [itr ]["velocities" ][temper_category ] = self .update_velocities (
393+ self ._fitness [itr ]["params" ][temper_category ],
394+ self ._fitness [itr - 1 ]["velocities" ][temper_category ],
395+ local_attractors ["params" ][temper_category ],
396+ global_attractor ["params" ],
397+ temper_params [idx ]["omega_v" ],
398+ temper_params [idx ]["phi_p" ],
399+ temper_params [idx ]["phi_g" ])
400+
401+ # Redistribute the particles into different temper categories
402+ if itr % temper_frequency == 0 :
403+ temper_map = self .distribute_particles (n_pso_particles , n_temper_categories )
404+
405+ return global_attractor
406+
407+ def temper_params_dt (self ):
408+ return np .dtype ([
409+ ('omega_v' , np .float64 ),
410+ ('phi_p' , np .float64 ),
411+ ('phi_g' , np .float64 )])
412+
413+
414+ def distribute_particles (self , n_pso_particles , n_temper_categories ):
415+
416+ # Distribute as many particles as evenly as possible across the categories,
417+ # This ensures that there are no empty categories
418+ n_evenly_distributable = (n_pso_particles // n_temper_categories ) * n_temper_categories
419+ n_unevenly_distributable = n_pso_particles - n_evenly_distributable
420+
421+ # Generate the required indicies for the pso particles
422+ particle_indicies = range (0 , n_pso_particles )
423+
424+ # Randomise the order
425+ np .random .shuffle (particle_indicies )
426+
427+ # Reshape to a 2D array indexed on the number of tempering categories
428+ particle_map = np .reshape (
429+ particle_indicies [:n_evenly_distributable ],
430+ (n_temper_categories , n_evenly_distributable // n_temper_categories ))
431+
432+ # Transfer to the map
433+ temper_map = {}
434+ for i , index_category in enumerate (particle_map ):
435+ temper_map [i ] = index_category
436+
437+ # Transfer any excess particles that could not be evenly distributed
438+ # This is a slow operation, so for the purposes of speed the number of
439+ # temper categories should be a factor of the number of pso particles
440+ if n_unevenly_distributable != 0 :
441+ for i in range (n_evenly_distributable , n_pso_particles ):
442+ temper_map [random .randrange (0 , n_temper_categories )] = (
443+ np .append (temper_map [random .randrange (0 , n_temper_categories )], [particle_indicies [i ]]))
444+
445+ return temper_map
446+
165447class PerfTestMultipleAbstractor (object ):
166448 def __init__ (self ,
167449 param_names ,
0 commit comments