genetic_algorithm 0.5

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.
Files changed (4) hide show
  1. data/README.md +25 -0
  2. data/lib/genetic_algorithm.rb +325 -0
  3. data/test/test.rb +23 -0
  4. metadata +57 -0
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ Ruby Genetic Algorithm Library
2
+ ==============================
3
+
4
+ **Author: Carlos Maximiliano Giorgio Bort**
5
+ **Date: 16/03/2011**
6
+
7
+ This is a simple implementation of a genetic algorithm.
8
+
9
+ This library works with Ruby 1.9.2 or newer versions
10
+ Together with this gem you may install:
11
+
12
+ * gnuplot (if you have a mac you can do this via mac port)
13
+ * gnuolotr which is a gem that you can install simply typing into your terminal: `gem install gnuplotr`
14
+
15
+ Gnuplot and gnuplotr are used to plot the evolution of the best chromosome in the population.
16
+
17
+ Run the `test/test.rb` to test the algorithm. Otherwise you can just the run `ga_gem.rb` file.
18
+
19
+
20
+ Note: If you are using a mac, maybe you'll need to install the newer version of ruby. Try the [rvm: Ruby Version Manager](http://rvm.beginrescueend.com)!
21
+
22
+ If you find some bugs please contact me at:
23
+ maximiliano_giorgio at yahoo dot it
24
+
25
+ Have a nice day!!
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env ruby
2
+ # ga_my
3
+ #
4
+ # Created by Carlos Maximiliano Giorgio Bort on 2011-03-06.
5
+ # Copyright (c) 2011 University of Trento. All rights reserved.
6
+ #
7
+
8
+ require 'rubygems'
9
+ require 'gnuplotr'
10
+
11
+ class Vector
12
+ if RUBY_VERSION.split(".").join.to_i < 190
13
+ # Add method for division under Ruby 1.8.x
14
+ def /(other); self * (1.0 / other); end
15
+ end
16
+ # More compact inspect version
17
+ def inspect; "V[#{self.to_a * ","}]"; end
18
+ end
19
+ class Integer
20
+ def to_bin(siz) # size is the number of bits used
21
+ bin = self.to_s(2)
22
+ bin.size <= siz ? inc = (siz - bin.size) : (raise "Use more bits for a proper binary convertion, you used #{size} bits for a #{bin.size} binary number")
23
+ inc.times { bin = "0" + bin }
24
+ return bin
25
+ end
26
+ end
27
+
28
+ module GA
29
+ class Population
30
+ attr_reader :population , :nbit
31
+
32
+ # i_o is the inverval of values used for define the first population
33
+ # values for the parameters of the first population
34
+ def initialize( dim, i_o = {}, prec = 1E-3 )
35
+ raise ArgumentError, "Need an Hash instead of #{i_o.class}" unless i_o.kind_of? Hash
36
+ @population = [] # initialize the population, is an hash which keys are: :chromosome and :fitness
37
+ dim.times do # for each chromosome of the initial population do...
38
+ cr = []
39
+ # generate the gene randomly (it must lie in the domain defined by i_o)
40
+ i_o.each_value{ |v| cr << rand()*(v.max-v.min) + v.min }
41
+ @population << { :chromosome => cr }
42
+ end
43
+ @prec = prec # the precision, i.e. the number considered at the left of the comma
44
+ end
45
+
46
+ end # class Population
47
+
48
+ # Class that implements a general n-dimensional Nelder-Meade Method (NMM).
49
+ # @author Paolo Bosetti
50
+ class Optimizer
51
+ attr_reader :simplex, :status, :iteration, :sorted
52
+ def initialize(args = {})
53
+ @cfg = {
54
+ :toll => 1E-3 , # accurancy of the solution
55
+ :nbit => 10 , # number of bits used to encode the cromosomes into a binary string
56
+ :p_mutation => 0.2 , # probability of mutation
57
+ :p_crossover=> 0.8 , # probability of cross over
58
+ :i_o => {} , # inverval of values used for define the first population
59
+ :npop => 50 , # number of population to be computed
60
+ :ncr => 100 , # number of chromosomes in each population
61
+ :pconv => true ,
62
+ :nelitist => 3 , # the 'n' best chromosomes that will automatically be copied in the new population
63
+ :plotopt => {:xlabel => 'No. iteration',
64
+ :ylabel => 'Objective function value',
65
+ :yrange => [ -10 , 10 ],
66
+ :grid => "set grid"
67
+ }
68
+ }
69
+ @cfg[:plotopt][:title] = "Population size: #{@cfg[:ncr]} chromosomes"
70
+ raise "Error with the assigned mutation probability:\n it is #{@cfg[:p_mutation]} but must be 0 <= p_mutation <= 1 " unless @cfg[:p_mutation] >= 0 and @cfg[:p_mutation] <= 1
71
+ raise "Error with the assigned crossover probability:\n it is #{@cfg[:p_crossover]} but must be 0 <= p_crossover <= 1 " unless @cfg[:p_crossover] >= 0 and @cfg[:p_crossover] <= 1
72
+ @cfg.merge! args
73
+ @nbit = @cfg[:nbit] #@pop.max_bit # is the number f bits required to encode into a binary string the chromosome
74
+ @pop = Population.new( @cfg[:ncr] , @cfg[:i_o])
75
+ @population = @pop.population
76
+ @start_points = []
77
+ @status = :filling
78
+ @iteration = 0
79
+ @best = []
80
+ if @cfg[:pconv] == true # this is the plot
81
+ @gp = GNUPlotr.new
82
+ # enable command history recording
83
+ @gp.record = true
84
+ # Issue raw gnuplot commands
85
+ @gp.raw @cfg[:plotopt][:grid]
86
+ # Some magic mapping works too:
87
+ @gp.set_grid
88
+ @gp.set_title @cfg[:plotopt][:title] , :font => "Times New Roman,18"
89
+ @gp.set_xlabel @cfg[:plotopt][:xlabel], :font => "Times New Roman,18"
90
+ @gp.set_ylabel @cfg[:plotopt][:ylabel], :font => "Times New Roman,18"
91
+ @gp.set_xrange( 0 .. @cfg[:npop])
92
+ #@gp.set_yrange(@cfg[:plotopt][:yrange][0] .. @cfg[:plotopt][:yrange][1])
93
+ end # if @cfg
94
+ end
95
+
96
+ def loop( ary = nil )
97
+ raise ArgumentError, "Block needed" unless block_given?
98
+ # evaluates the cromosomes and converts them into a string of bits
99
+ @population.each do |c|
100
+ c[:fitness] = yield( c[:chromosome] ) unless c[:fitness] # evaluates the chromosome
101
+ c[:bitstring] = encode( c[:chromosome] ) unless c[:bitstring] # converts the chromosome into a string of bits
102
+ end
103
+
104
+ until converged? or @iteration > @cfg[:npop]
105
+ @population.each{ |v| v[:fitness] = 1.0/0.0 if v[:fitness].nan?}
106
+ @sorted = @population.sort!{ |x, y| x[:fitness] <=> y[:fitness] }
107
+ selected = selection( @population )
108
+ bit_selected = []
109
+ selected.each{ |v| bit_selected << v[:bitstring] }
110
+ # @population.size is used to set the number of chromosomes in the new generation
111
+ childs = evolve( bit_selected, @population.size-@cfg[:nelitist] , @cfg[:p_crossover], @cfg[:p_mutation] )
112
+ # child is converted into an array of hashes, each with keys: :chromosome, :bitstring , :fitness
113
+ @population = @sorted[ 0 .. @cfg[:nelitist]-1 ] # reset the population and then update it
114
+ childs.each do |c|
115
+ dec = decode( c[:bitstring] )
116
+ ftn = yield( dec )
117
+ ftn = 1.0/0.0 if ftn.nan?
118
+ @population << {
119
+ :bitstring => c[:bitstring],
120
+ :chromosome => dec, # converts the string of bits into an array of floats
121
+ :fitness => ftn # evaluates the array of floats
122
+ }
123
+ end # childs do
124
+ @sorted = @population.sort!{ |x, y| x[:fitness] <=> y[:fitness] }
125
+ @best << @sorted.first
126
+ puts "#{@iteration}th generation, best: #{@best.last.inspect}" ##########
127
+ puts "#{@iteration}th generation, worst: #{ @sorted.last.inspect }" ###########
128
+ "Maximum number of iteration reached: #{@cfg[:npop]}" if @iteration == @cfg[:npop]
129
+ puts "_________________________________________________________"
130
+
131
+ # these lines ar used to do a convergence plot, i.e. all the fitnesses for the current population
132
+ if @cfg[:pconv] == true
133
+
134
+ # a. initialize the data sets for the plot
135
+ @gp.new_series(:population)
136
+
137
+ # b. fill the data sets
138
+ if @iteration == 0 # initialize the matrix containing simplex data
139
+ sdata = []
140
+ it = []
141
+ end
142
+ it << @iteration # array of integers
143
+
144
+ ### these lines are usefull to plot the evolution of entire population
145
+ #f_a = []
146
+ #@population.each{ |v| f_a << v[:fitness] } # array of array of floats
147
+ #sdata << f_a
148
+ # b. fill the data sets
149
+ #it.each do |i|
150
+ # #sdata[i].each do |v|
151
+ # sdata.each do |v|
152
+ # @gp.series[:population] << [ i , v ]
153
+ # end
154
+ #end
155
+
156
+ ### these lines are used to track the best chromosome in the population
157
+ sdata << @best[-1][:fitness]
158
+ it.each do |i|
159
+ @gp.series[:population] << [ i , sdata[i] ]
160
+ end
161
+ # c. close the data sets
162
+ @gp.series[:population].close
163
+ # d. plot the data sets
164
+ if @iteration == 0
165
+ @gp.plot :population , "with points lt 9 pt 2 notitle"
166
+ @gp.plot :population , "with line lt 9 pt 2 notitle"
167
+ else
168
+ @gp.replot :population , "with points lt 9 pt 2 notitle"
169
+ @gp.replot :population , "with line lt 9 pt 2 notitle"
170
+ end
171
+ end # if @cfg
172
+ ary[ @iteration.to_s.to_sym ] = @sorted if ary.kind_of? Hash
173
+ @iteration += 1
174
+ end # until converged
175
+ return @best[-1]
176
+ end # def loop
177
+
178
+ private
179
+
180
+ # The solution converges if the fitness for the best chromosome of the latter 3 population is the same
181
+ # Input: array of hashes. Output: boolean value
182
+ def converged?
183
+ if @iteration > 1
184
+ xx = 0.0
185
+ 3.times{ |p| xx += @sorted[p][:fitness]**2 }
186
+ if xx**0.5 <= @cfg[:toll]
187
+ return true
188
+ else
189
+ return false
190
+ end # if xx
191
+ end # if @iteration
192
+ end # converged
193
+
194
+ # Input: array of bit strings. Output: array of bit strings
195
+ def evolve( selected, pop_size, p_cross, p_mut )
196
+ children = []
197
+ selected.each_with_index do |p1, i|
198
+ # crossing over
199
+ ############################## non me piase sto metodo di scelgiere moglie e marito
200
+ # 1 ::: p2 = (i.modulo(2) == 0) ? selected[i+1] : selected[i-1] # i.modulo(2) is the reminder of the division i / 2
201
+ # p2 = selected[0] if i == selected.size - 1
202
+ # 2 ::: i == selected.size-1 ? p2 = selected[0] : p2 = selected[i+1]
203
+ # 3 ::: random choise:
204
+ j = rand(selected.size)
205
+ j = rand(selected.size) while j == i
206
+ p2 = selected[j]
207
+ ch1 = {} ; ch2 = {} ; ch3 = {}
208
+ ch1[:bitstring] , ch2[:bitstring] = crossover( p1, p2, p_cross )
209
+ children.concat( [ ch1 , ch2 ] )
210
+
211
+ # mutation
212
+ #i.modulo(2) == 1 ? p3 = selected[i+1] : p3 = selected[i-1] # p3 is a chromosome not yet used
213
+ k = rand(selected.size)
214
+ k = rand(selected.size) while k == j and k == i # random choise of the chromosome that might mutate
215
+ p3 = selected[k]
216
+ ch3[:bitstring] = mutation( p3, p_mut )
217
+ children << ch3
218
+ break if children.size >= pop_size
219
+ end
220
+ return children
221
+ end
222
+
223
+ # compares two chromosomes and selects the one with the best fitting.
224
+ # the size of selected is the half of the population size
225
+ # This is a binary tournament.
226
+ # Input: array of hashes. Output: array of hashes
227
+ def selection(pop)
228
+ raise "Error in selection: input must be a Array instead of a #{pop.class}" unless pop.kind_of? Array
229
+ sel = []
230
+ pop.size.times do
231
+ i , j = rand( pop.size ) , rand( pop.size )
232
+ j = rand( pop.size ) while j == i # if unfortunatly j = i, evaluates j again
233
+ (pop[i][:fitness] < pop[j][:fitness]) ? sel << pop[i] : sel << pop[j]
234
+ end
235
+ return sel
236
+ end
237
+
238
+ # the chromosome is a string of '0' and '1', rate [0,1]. mutant is also a string
239
+ # Input: a bit string, Output: a bit string
240
+ def mutation( bitstring , rate )
241
+ raise "Error in mutation: input must be a String instead of a #{bitstring.class}" unless bitstring.kind_of? String
242
+ mutant = ""
243
+ bitstring.size.times do |i|
244
+ gene = bitstring[i]
245
+ # change the bit value only if the rand [0,1] is minor than the mutation probability
246
+ mutant << ((rand < rate) ? ((gene == '1') ? "0" : "1") : gene)
247
+ end # chromosome
248
+ return mutant
249
+ end # def mutation
250
+
251
+ # both father and mather are strings, rate [0,1] is the crossover probability
252
+ # the crossover returns two childs.
253
+ # Input: 2 bit strings + 1 float. Output: 2 bit strings
254
+ def crossover( father, mother, rate )
255
+ raise "Error in crossover: father must be a String instead of a #{father.class}" unless father.kind_of? String
256
+ raise "Error in crossover: mother must be a String instead of a #{mother.class}" unless mother.kind_of? String
257
+ # don't do the cross over if rand is maior or equal than the crossover probability
258
+ return father , mother if rand >= rate
259
+ raise "Error in crossover, father and mother must have the same dimension" unless father.size == mother.size
260
+ point = 0.0
261
+ point = rand(mother.size) while point == 0 or point == mother.size # sets the crossover point randomly
262
+ return father[0..point-1] + mother[point..(mother.size)] , mother[0..point-1] + father[point..(father.size)]
263
+ end
264
+
265
+ # this is used to convert the input values into a binary string (the chromosome)
266
+ # Input: array of floats. Output: one bit string
267
+ def encode( ary = [] )
268
+ ary_b = ""
269
+ ary.each { |v|
270
+ i_b = v.to_i.abs.to_bin( @nbit )
271
+ # how to encode the sign: the first bit is 0 if i_b is >= 0, and 1 if it's < 0
272
+ v.to_i >= 0 ? i_b[0] = "0" : i_b[0] = "1"
273
+ d_b = ( ( (v-v.to_i)/@cfg[:tol] ).to_i.abs ).to_bin( @nbit )
274
+ ary_b += i_b+d_b
275
+ }
276
+ return ary_b
277
+ end
278
+
279
+ # this is used to decode the binary chromosome string into an array of floats
280
+ # Input: one bit string. Output: array of floats
281
+ def decode( str )
282
+ ng = str.size / @nbit # is the number of genes in one chromosome
283
+ raise "Error in the chromosome decodification: you have #{ng} genes of #{@nbit} bits, and #{str.size} bits in the chromosome" unless ng * @nbit == str.size
284
+ cr = []
285
+ dots = ""
286
+ @nbit.times{ dots += "." } # generates a string with @nbit dots: "..."
287
+ dots = "(" + dots + ")" # adds the parentesys: "(...)"
288
+ rexp = Regexp.new( dots ) # converst dots into a regulare expression: /(...)/
289
+ str_a = str.split( rexp ) # splits the string: eg."000111110011" -> ["","000","","111","","110","","011"]
290
+ str_a = str_a.delete_if{ |v| v == ""} # ["000", "111", "110", "011"]
291
+ str_a.size.times do |i|
292
+ if i <= str_a.size-2
293
+ # the first bit of the element with an odd index is the sign of the floating number
294
+ str_a[ 2*i ][0..0] == "0" ? sign = "+" : sign = "-"
295
+ # this returns: [ +00.7 , +2.6 ], each element in the array is in decimal codification
296
+ cr << (sign+str_a[ 2*i ][1..-1].to_i(2).to_s+"."+str_a[2*i+1].to_i(2).to_s).to_f
297
+ end # if
298
+ return cr if cr.size == str_a.size / 2
299
+ end # str_a.size.times
300
+ end # decode
301
+ end # class Optimizer
302
+ end # module GA#
303
+
304
+
305
+ if __FILE__ == $0
306
+ # Test function
307
+ f = lambda {|p| p[0]**2 + p[1]**2 } # a trivial parabola
308
+ #f = lambda { |p| p[0] ** 2 - 4 * p[0] + p[1] ** 2 -p[1] - p[0] * p[1]}
309
+ #f = lambda { |x| 10*(x[1] - x[0]**2)** 2 + (1 - x[0])** 2 } # Rosenbroke function
310
+
311
+ # Instantiate the optimizer, with tolerance and dimension
312
+ opt = GA::Optimizer.new( :tol => 1,
313
+ :p_mutation => 0.2,
314
+ :p_crossover => 0.8,
315
+ :i_o => { :X =>[5,10] , :Y=>[-10.23,5.234] },
316
+ :npop => 50,
317
+ :ncr => 200
318
+ )
319
+ arr = {}
320
+ opt.loop(arr) {|p| f.call(p)}
321
+ p "*"*15
322
+ p "*** The last population is: ***"
323
+ sort = opt.sorted # sort is an array with the last population in ascending order, the first element is the chromosome with the lowest fitness
324
+ sort.each{|v| puts v}
325
+ end
data/test/test.rb ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # ga_my
3
+ #
4
+ # Created by Carlos Maximiliano Giorgio Bort on 2011-03-06.
5
+ # Copyright (c) 2011 University of Trento. All rights reserved.
6
+ #
7
+
8
+ require 'rubygems'
9
+ require 'gnuplotr'
10
+ require 'genetic_algorithm'
11
+ # Test function
12
+ f = lambda {|p| p[0]**2 + p[1]**2 } # a trivial parabola
13
+
14
+ # Instantiate the optimizer, with tolerance and dimension
15
+ opt = GA::Optimizer.new( :tol => 1e-3,
16
+ :p_mutation => 0.2,
17
+ :p_crossover => 0.8,
18
+ :i_o => { :X =>[3,-4, 5] , :Y=>[3,-4, 5] , :Z=>[3,-4, 5] },
19
+ :npop => 50,
20
+ :ncr => 150,
21
+ :nbit => 10
22
+ )
23
+ opt.loop {|p| f.call(p)}
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: genetic_algorithm
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: "0.5"
6
+ platform: ruby
7
+ authors:
8
+ - Carlos Maximiliano Giorgio Bort
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-06 00:00:00 Z
14
+ dependencies: []
15
+
16
+ description: Simple genetic algorithm for functions minimization.
17
+ email: maximiliano_giorgio@yahoo.it
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.md
26
+ - lib/genetic_algorithm.rb
27
+ - test/test.rb
28
+ homepage: https://github.com/maximiliano1985
29
+ licenses: []
30
+
31
+ post_install_message:
32
+ rdoc_options:
33
+ - --inline-source
34
+ - --charset=UTF-8
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ requirements: []
50
+
51
+ rubyforge_project: genetic_algorithm
52
+ rubygems_version: 1.8.5
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: Simple genetic algorithm for function minimization. Needs gnuplot and the gem gnuplotr to work properly.
56
+ test_files: []
57
+