mhl 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ require 'securerandom'
2
+
3
+ require 'mhl/generic_particle'
4
+
5
+ module MHL
6
+ class Particle < GenericParticle
7
+ def initialize(initial_position, initial_velocity)
8
+ super(initial_position)
9
+ @velocity = initial_velocity
10
+ end
11
+
12
+ # move particle and update attractor
13
+ def move(omega, c1, c2, swarm_attractor)
14
+ raise 'Particle attractor is nil!' if @attractor.nil?
15
+ # raise 'Swarm attractor is nil!' if swarm_attractor.nil?
16
+
17
+ # update velocity
18
+ @velocity =
19
+ # previous velocity is damped by inertia weight omega
20
+ omega * @velocity +
21
+ # "memory" component (linear attraction towards the best position
22
+ # that this particle encountered so far)
23
+ c1 * SecureRandom.random_number * (attractor[:position] - @position) +
24
+ # "social" component (linear attraction towards the best position
25
+ # that the entire swarm encountered so far)
26
+ c2 * SecureRandom.random_number * (swarm_attractor[:position] - @position)
27
+
28
+ # update position
29
+ @position = @position + @velocity
30
+ end
31
+
32
+ end
33
+ end
@@ -1,8 +1,19 @@
1
- require 'matrix'
2
- require 'securerandom'
1
+ require 'concurrent'
2
+ require 'facter'
3
+ require 'logger'
4
+
5
+ require 'mhl/pso_swarm'
6
+
3
7
 
4
8
  module MHL
5
9
 
10
+ # This solver implements the PSO with inertia weight variant algorithm.
11
+ #
12
+ # For more information, refer to equation 4 of:
13
+ # [REZAEEJORDEHI13] A. Rezaee Jordehi & J. Jasni (2013) Parameter selection
14
+ # in particle swarm optimisation: a survey, Journal of Experimental &
15
+ # Theoretical Artificial Intelligence, 25:4, pp. 527-542, DOI:
16
+ # 10.1080/0952813X.2013.782348
6
17
  class ParticleSwarmOptimizationSolver
7
18
 
8
19
  def initialize(opts={})
@@ -16,6 +27,23 @@ module MHL
16
27
 
17
28
  @start_positions = opts[:start_positions]
18
29
  @exit_condition = opts[:exit_condition]
30
+
31
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
32
+
33
+ case opts[:logger]
34
+ when :stdout
35
+ @logger = Logger.new(STDOUT)
36
+ when :stderr
37
+ @logger = Logger.new(STDERR)
38
+ else
39
+ @logger = opts[:logger]
40
+ end
41
+
42
+ @quiet = opts[:quiet]
43
+
44
+ if @logger
45
+ @logger.level = (opts[:log_level] or Logger::WARN)
46
+ end
19
47
  end
20
48
 
21
49
  # This is the method that solves the optimization problem
@@ -23,99 +51,69 @@ module MHL
23
51
  # Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
24
52
  # object) that accepts the genotype as argument (that is, the set of
25
53
  # parameters) and returns the phenotype (that is, the function result)
26
- def solve(func)
54
+ def solve(func, params={})
27
55
  # setup particles
28
56
  if @start_positions.nil?
29
- particles = Array.new(@swarm_size) do
30
- { position: Vector[*@random_position_func.call], velocity: Vector[*@random_velocity_func.call] }
31
- end
57
+ swarm = PSOSwarm.new(@swarm_size,
58
+ Array.new(@swarm_size) { Vector[*@random_position_func.call] },
59
+ Array.new(@swarm_size) { Vector[*@random_velocity_func.call] },
60
+ params)
32
61
  else
33
- particles = @start_positions.each_slice(2).map do |pos,vel|
34
- { position: Vector[*pos], velocity: Vector[*vel] }
35
- end
62
+ # we only support the definition of start positions - not velocities
63
+ swarm = PSOSwarm.new(@swarm_size,
64
+ @start_positions.map {|x| Vector[*x] },
65
+ Array.new(@swarm_size) { Vector[*@random_velocity_func.call] },
66
+ params)
67
+ # particles = @start_positions.each_slice(2).map do |pos,vel|
68
+ # { position: Vector[*pos], velocity: Vector[*vel] }
69
+ # end
36
70
  end
37
71
 
38
72
  # initialize variables
39
73
  gen = 0
40
74
  overall_best = nil
41
75
 
42
- # completely made up values
43
- alpha = 0.5
44
- beta = 0.3
45
- gamma = 0.7
46
- delta = 0.5
47
- epsilon = 0.6
48
-
49
76
  # default behavior is to loop forever
50
77
  begin
51
78
  gen += 1
52
- puts "Starting generation #{gen} at #{Time.now}"
79
+ @logger.info("PSO - Starting generation #{gen}") if @logger
53
80
 
54
- # assess height for every particle
55
- particles.each do |p|
56
- p[:task] = Concurrent::Future.new { func.call(p[:position]) }
57
- end
81
+ # create latch to control program termination
82
+ latch = Concurrent::CountDownLatch.new(@swarm_size)
58
83
 
59
- # wait for all the evaluations to end
60
- particles.each_with_index do |p,i|
61
- p[:height] = p[:task].value
62
- if p[:highest_value].nil? or p[:height] > p[:highest_value]
63
- p[:highest_value] = p[:height]
64
- p[:highest_position] = p[:position]
84
+ # assess height for every particle
85
+ swarm.each do |particle|
86
+ @pool.post do
87
+ # evaluate target function
88
+ particle.evaluate(func)
89
+ # update latch
90
+ latch.count_down
65
91
  end
66
92
  end
67
93
 
68
- # find highest particle
69
- highest_particle = particles.max_by {|x| x[:height] }
94
+ # wait for all the threads to terminate
95
+ latch.wait
70
96
 
71
- # calculate overall best
97
+ # get swarm attractor (the highest particle)
98
+ swarm_attractor = swarm.update_attractor
99
+
100
+ # print results
101
+ puts "> gen #{gen}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
102
+
103
+ # calculate overall best (that plays the role of swarm attractor)
72
104
  if overall_best.nil?
73
- overall_best = highest_particle
105
+ overall_best = swarm_attractor
74
106
  else
75
- overall_best = [ overall_best, highest_particle ].max_by {|x| x[:height] }
107
+ overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
76
108
  end
77
109
 
78
110
  # mutate swarm
79
- particles.each do |p|
80
- # randomly sample particles and use them as informants
81
- informants = random_portion(particles)
82
-
83
- # make sure that p is included among the informants
84
- informants << p unless informants.include? p
85
-
86
- # get fittest informant
87
- fittest_informant = informants.max_by {|x| x[:height] }
88
-
89
- # update velocity
90
- p[:velocity] =
91
- alpha * p[:velocity] +
92
- beta * (p[:highest_position] - p[:position]) +
93
- gamma * (fittest_informant[:highest_position] - p[:position]) +
94
- delta * (overall_best[:highest_position] - p[:position])
95
-
96
- # update position
97
- p[:position] = p[:position] + epsilon * p[:velocity]
98
- end
111
+ swarm.mutate
99
112
 
100
113
  end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
101
- end
102
-
103
- private
104
-
105
- def random_portion(array, ratio=0.1)
106
- # get size of random array to return
107
- size = (ratio * array.size).ceil
108
-
109
- (1..size).inject([]) do |acc,i|
110
- # randomly sample a new element
111
- begin
112
- new_element = array[SecureRandom.random_number(array.size)]
113
- end while acc.include? new_element
114
114
 
115
- # insert element in the accumulator
116
- acc << new_element
117
- end
118
- end
115
+ overall_best
116
+ end
119
117
 
120
118
  end
121
119
 
@@ -0,0 +1,52 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_swarm'
5
+ require 'mhl/particle'
6
+
7
+
8
+ module MHL
9
+ class PSOSwarm < GenericSwarmBehavior
10
+
11
+ def initialize(size, initial_positions, initial_velocities, params={})
12
+ @size = size
13
+ @particles = Array.new(@size) do |index|
14
+ Particle.new(initial_positions[index], initial_velocities[index])
15
+ end
16
+
17
+ @generation = 1
18
+
19
+ # get values for parameters C1 and C2
20
+ @c1 = (params[:c1] || DEFAULT_C1).to_f
21
+ @c2 = (params[:c1] || DEFAULT_C2).to_f
22
+
23
+ # define procedure to get dynamic value for omega
24
+ @get_omega = if params.has_key? :omega and params[:omega].respond_to? :call
25
+ params[:omega]
26
+ else
27
+ ->(gen) { (params[:omega] || DEFAULT_OMEGA).to_f }
28
+ end
29
+
30
+ if params.has_key? :constraints
31
+ puts "PSOSwarm called w/ :constraints => #{params[:constraints]}"
32
+ end
33
+
34
+ @constraints = params[:constraints]
35
+ end
36
+
37
+ def mutate(params={})
38
+ # get omega parameter
39
+ omega = @get_omega.call(@generation)
40
+
41
+ # move particles
42
+ @particles.each_with_index do |p,i|
43
+ p.move(omega, @c1, @c2, @swarm_attractor)
44
+ if @constraints
45
+ p.remain_within(@constraints)
46
+ end
47
+ end
48
+
49
+ @generation += 1
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_swarm'
5
+ require 'mhl/quantum_particle'
6
+
7
+
8
+ module MHL
9
+ class QPSOSwarm < GenericSwarmBehavior
10
+
11
+ def initialize(size, initial_positions, params={})
12
+ @size = size
13
+ @particles = Array.new(@size) do |index|
14
+ QuantumParticle.new(initial_positions[index])
15
+ end
16
+
17
+ # find problem dimension
18
+ @dimension = initial_positions[0].size
19
+
20
+ @generation = 1
21
+
22
+ # define procedure to get dynamic value for alpha
23
+ @get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
24
+ params[:alpha]
25
+ else
26
+ ->(gen) { (params[:alpha] || DEFAULT_ALPHA).to_f }
27
+ end
28
+ end
29
+
30
+ def mutate
31
+ # get alpha parameter
32
+ alpha = @get_alpha.call(@generation)
33
+
34
+ # this calculates the C_n parameter (basically, the centroid of the set
35
+ # of all the particle attractors) as defined in [SUN11], formulae 4.81
36
+ # and 4.82
37
+ c_n = @particles.inject(Vector[*[0]*@dimension]) {|s,p| s += p.attractor[:position] } / @size.to_f
38
+
39
+ @particles.each { |p| p.move(alpha, c_n, @swarm_attractor) }
40
+
41
+ @generation += 1
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,40 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_particle'
5
+
6
+ module MHL
7
+ class QuantumParticle < GenericParticle
8
+ attr_reader :position
9
+
10
+ # move particle using QPSO - Type II algorithm
11
+ def move(alpha, mean_best, swarm_attractor)
12
+ raise 'Particle attractor is nil!' if @attractor.nil?
13
+ # raise 'Swarm attractor is nil!' if swarm_attractor.nil?
14
+
15
+ dimension = @position.size
16
+
17
+ # phi represents the \phi_{i,n} parameter in [SUN11], formula 4.83
18
+ phi = Array.new(dimension) { SecureRandom.random_number }
19
+
20
+ # p_i represents the p_{i,n} parameter in [SUN11], formulae 4.82 and 4.83
21
+ p_i =
22
+ Vector[*phi.zip(@attractor[:position]).map {|phi_j,p_j| phi_j * p_j }] +
23
+ Vector[*phi.zip(swarm_attractor[:position]).map {|phi_j,g_j| (1.0 - phi_j) * g_j }]
24
+
25
+ # delta represents the displacement for the current position.
26
+ # See [SUN11], formula 4.82
27
+ delta =
28
+ @position.zip(mean_best).map {|x,y| alpha * (x-y).abs }. # \alpha * | X_{i,n} - C_n |
29
+ map {|x| x * Math.log(1.0 / SecureRandom.random_number) } # log(\frac{1}{u_{i,n+1}})
30
+
31
+ # update position
32
+ if SecureRandom.random_number < 0.5
33
+ @position = p_i + Vector[*delta]
34
+ else
35
+ @position = p_i - Vector[*delta]
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,113 @@
1
+ require 'concurrent'
2
+ require 'facter'
3
+ require 'logger'
4
+ require 'matrix'
5
+ require 'securerandom'
6
+
7
+ require 'mhl/qpso_swarm'
8
+
9
+
10
+ module MHL
11
+
12
+ # This solver implements the QPSO Type 2 algorithm.
13
+ #
14
+ # For more information, refer to equation 4.82 of:
15
+ # [SUN11] Jun Sun, Choi-Hong Lai, Xiao-Jun Wu, "Particle Swarm Optimisation:
16
+ # Classical and Quantum Perspectives", CRC Press, 2011
17
+ class QuantumPSOSolver
18
+
19
+ def initialize(opts={})
20
+ @swarm_size = opts[:swarm_size].to_i
21
+ unless @swarm_size
22
+ raise ArgumentError, 'Swarm size is a required parameter!'
23
+ end
24
+
25
+ @random_position_func = opts[:random_position_func]
26
+
27
+ @start_positions = opts[:start_positions]
28
+ @exit_condition = opts[:exit_condition]
29
+
30
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
31
+
32
+ case opts[:logger]
33
+ when :stdout
34
+ @logger = Logger.new(STDOUT)
35
+ when :stderr
36
+ @logger = Logger.new(STDERR)
37
+ else
38
+ @logger = opts[:logger]
39
+ end
40
+
41
+ @quiet = opts[:quiet]
42
+
43
+ if @logger
44
+ @logger.level = opts[:log_level] or Logger::WARN
45
+ end
46
+ end
47
+
48
+ # This is the method that solves the optimization problem
49
+ #
50
+ # Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
51
+ # object) that accepts the genotype as argument (that is, the set of
52
+ # parameters) and returns the phenotype (that is, the function result)
53
+ def solve(func, params={})
54
+ # setup swarm
55
+ if @start_positions.nil?
56
+ swarm = QPSOSwarm.new(@swarm_size,
57
+ Array.new(@swarm_size) { Vector[*@random_position_func.call] },
58
+ params)
59
+ else
60
+ raise 'Unimplemented yet!'
61
+ # particles = @start_positions.map do |pos|
62
+ # { position: Vector[*pos] }
63
+ # end
64
+ end
65
+
66
+ # initialize variables
67
+ gen = 0
68
+ overall_best = nil
69
+
70
+ # default behavior is to loop forever
71
+ begin
72
+ gen += 1
73
+ @logger.info "QPSO - Starting generation #{gen}" if @logger
74
+
75
+ # create latch to control program termination
76
+ latch = Concurrent::CountDownLatch.new(@swarm_size)
77
+
78
+ swarm.each do |particle|
79
+ @pool.post do
80
+ # evaluate target function
81
+ particle.evaluate(func)
82
+ # update latch
83
+ latch.count_down
84
+ end
85
+ end
86
+
87
+ # wait for all the evaluations to end
88
+ latch.wait
89
+
90
+ # get swarm attractor (the highest particle)
91
+ swarm_attractor = swarm.update_attractor
92
+
93
+ # print results
94
+ puts "> gen #{gen}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
95
+
96
+ # calculate overall best (that plays the role of swarm attractor)
97
+ if overall_best.nil?
98
+ overall_best = swarm_attractor
99
+ else
100
+ overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
101
+ end
102
+
103
+ # mutate swarm
104
+ swarm.mutate
105
+
106
+ end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
107
+
108
+ overall_best
109
+ end
110
+
111
+ end
112
+
113
+ end