mhl 0.0.1 → 0.1.0
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.
- 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
|