mhl 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +8 -0
- data/lib/mhl.rb +2 -0
- data/lib/mhl/charged_swarm.rb +85 -0
- data/lib/mhl/generic_particle.rb +52 -0
- data/lib/mhl/generic_swarm.rb +41 -0
- data/lib/mhl/genetic_algorithm_solver.rb +64 -10
- data/lib/mhl/integer_genotype_space.rb +39 -11
- data/lib/mhl/multiswarm_qpso_solver.rb +140 -0
- data/lib/mhl/particle.rb +33 -0
- data/lib/mhl/particle_swarm_optimization_solver.rb +67 -69
- data/lib/mhl/pso_swarm.rb +52 -0
- data/lib/mhl/qpso_swarm.rb +45 -0
- data/lib/mhl/quantum_particle.rb +40 -0
- data/lib/mhl/quantum_particle_swarm_optimization_solver.rb +113 -0
- data/lib/mhl/version.rb +1 -1
- data/mhl.gemspec +2 -2
- data/test/mhl/genetic_algorithm_solver_test.rb +52 -0
- data/test/mhl/integer_genotype_space_test.rb +77 -0
- data/test/mhl/multiswarm_qpso_solver_test.rb +18 -0
- data/{spec/mhl/particle_swarm_optimization_solver_spec.rb → test/mhl/particle_swarm_optimization_solver_test.rb} +3 -2
- data/test/mhl/quantum_particle_swarm_optimization_solver_test.rb +16 -0
- data/test/test_helper.rb +6 -0
- metadata +49 -34
- data/spec/mhl/genetic_algorithm_spec.rb +0 -53
- data/spec/spec_helper.rb +0 -19
data/lib/mhl/particle.rb
ADDED
@@ -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 '
|
2
|
-
require '
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
79
|
+
@logger.info("PSO - Starting generation #{gen}") if @logger
|
53
80
|
|
54
|
-
#
|
55
|
-
|
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
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
#
|
69
|
-
|
94
|
+
# wait for all the threads to terminate
|
95
|
+
latch.wait
|
70
96
|
|
71
|
-
#
|
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 =
|
105
|
+
overall_best = swarm_attractor
|
74
106
|
else
|
75
|
-
overall_best = [ overall_best,
|
107
|
+
overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
|
76
108
|
end
|
77
109
|
|
78
110
|
# mutate swarm
|
79
|
-
|
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
|
-
|
116
|
-
|
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
|