algorithm-genetic 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +674 -0
- data/README.md +29 -0
- data/Rakefile +7 -0
- data/algorithm-genetic.gemspec +25 -0
- data/lib/algorithm/genetic.rb +10 -0
- data/lib/algorithm/genetic/crossover/order.rb +22 -0
- data/lib/algorithm/genetic/crossover/point.rb +13 -0
- data/lib/algorithm/genetic/evaluator.rb +24 -0
- data/lib/algorithm/genetic/gene.rb +63 -0
- data/lib/algorithm/genetic/mutation/shift.rb +16 -0
- data/lib/algorithm/genetic/mutation/swap.rb +11 -0
- data/lib/algorithm/genetic/population.rb +94 -0
- data/lib/algorithm/genetic/selection/elite.rb +8 -0
- data/lib/algorithm/genetic/version.rb +5 -0
- data/sample/i_love_ruby.rb +55 -0
- data/sample/tsp.rb +94 -0
- data/spec/algorithm/genetic_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +122 -0
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Algorithm::Genetic
|
2
|
+
|
3
|
+
A Generic Algorithm Library for Ruby Language
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'algorithm-genetic'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install algorithm-genetic
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/tdtds/algorithm-genetic/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'algorithm/genetic/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "algorithm-genetic"
|
8
|
+
spec.version = Algorithm::Genetic::VERSION
|
9
|
+
spec.authors = ["TADA Tadashi"]
|
10
|
+
spec.email = ["t@tdtds.jp"]
|
11
|
+
spec.summary = %q{A Generic Algorithm Library for Ruby Language}
|
12
|
+
spec.description = %q{A Generic Algorithm Library for Ruby Language}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "GPL"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "pry"
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#
|
2
|
+
# Crossover::Order: order crossover
|
3
|
+
#
|
4
|
+
module Algorithm::Genetic::Crossover
|
5
|
+
module Order
|
6
|
+
def crossover(parent1, parent2)
|
7
|
+
cut_point = rand(parent1.code.length)
|
8
|
+
child1 = mate(parent1.code, parent2.code, cut_point)
|
9
|
+
child2 = mate(parent2.code, parent1.code, cut_point)
|
10
|
+
return child1, child2
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def mate(p1, p2, point)
|
15
|
+
child = p1[0, point]
|
16
|
+
p2.each do |g|
|
17
|
+
child << g unless child.index(g)
|
18
|
+
end
|
19
|
+
return child
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#
|
2
|
+
# Crossover::Point: (one) point crossover
|
3
|
+
#
|
4
|
+
module Algorithm::Genetic::Crossover
|
5
|
+
module Point
|
6
|
+
def crossover(parent1, parent2, cut_num)
|
7
|
+
pivot = (parent1.code.length / 2.0).round
|
8
|
+
child1 = parent1.code[0, pivot] + parent2.code[pivot, pivot]
|
9
|
+
child2 = parent2.code[0, pivot] + parent1.code[pivot, pivot]
|
10
|
+
return child1, child2
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module Algorithm
|
3
|
+
module Genetic
|
4
|
+
#
|
5
|
+
# = evaluate fitness of a gene
|
6
|
+
#
|
7
|
+
class Evaluator
|
8
|
+
# evaluate fitness, implement by yourself
|
9
|
+
#
|
10
|
+
# gene :: a instance of Gene class
|
11
|
+
def fitness(gene)
|
12
|
+
raise NotImplementedError.new("implement 'fitness' method by yourself.")
|
13
|
+
end
|
14
|
+
|
15
|
+
# judgment of termination
|
16
|
+
#
|
17
|
+
# gene :: a instance of Gene class
|
18
|
+
def terminated?(gene)
|
19
|
+
raise NotImplementedError.new("implement 'terminated?' method by yourself.")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Algorithm
|
2
|
+
module Genetic
|
3
|
+
#
|
4
|
+
# = A gene class
|
5
|
+
#
|
6
|
+
class Gene
|
7
|
+
attr_reader :code, :fitness
|
8
|
+
|
9
|
+
# constructor of Gene
|
10
|
+
#
|
11
|
+
# code :: initial code as Array
|
12
|
+
# evaluator :: a Evaluator instance or a Proc instance returns Evaluator instance
|
13
|
+
# opts :: hash of options
|
14
|
+
#
|
15
|
+
# options:
|
16
|
+
# :crossover :: an array of module name including crossover method and params
|
17
|
+
# :mutation :: an array of module name including mutate method and params
|
18
|
+
# :mutation_chance :: mutation chance (float of 0 to 1)
|
19
|
+
def initialize(code, evaluator, opts = {})
|
20
|
+
@code, @evaluator_org, @opts = code, evaluator, opts
|
21
|
+
if @evaluator_org.respond_to?(:fitness) # Evaluator instance
|
22
|
+
@evaluator = @evaluator_org
|
23
|
+
else # Proc instance
|
24
|
+
@evaluator = @evaluator_org.call
|
25
|
+
end
|
26
|
+
@fitness = @evaluator.fitness(self)
|
27
|
+
|
28
|
+
if opts[:crossover]
|
29
|
+
@crossover_params = opts[:crossover].dup
|
30
|
+
crossover_module = @crossover_params.shift.to_s.capitalize
|
31
|
+
self.extend(Algorithm::Genetic::Crossover.const_get(crossover_module))
|
32
|
+
end
|
33
|
+
if opts[:mutation]
|
34
|
+
@mutation_params = opts[:mutation].dup
|
35
|
+
mutation_module = @mutation_params.shift.to_s.capitalize
|
36
|
+
self.extend(Algorithm::Genetic::Mutation.const_get(mutation_module))
|
37
|
+
end
|
38
|
+
@mutation_chance = opts[:mutation_chance] || 0.5
|
39
|
+
end
|
40
|
+
|
41
|
+
# crossover with a partner, returning a couple of children
|
42
|
+
#
|
43
|
+
# partner :: a partner's gene
|
44
|
+
def crossover_with(partner)
|
45
|
+
code1, code2 = crossover(self, partner, *@crossover_params)
|
46
|
+
return Gene.new(code1, @evaluator_org, @opts), Gene.new(code2, @evaluator_org, @opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
# mutate the code
|
50
|
+
def mutate!
|
51
|
+
return if rand > @mutation_chance
|
52
|
+
@code = mutate(@code, *@mutation_params)
|
53
|
+
@fitness = @evaluator.fitness(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# judgement termination
|
57
|
+
def terminated?
|
58
|
+
@evaluator.terminated?(self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
module Algorithm::Genetic::Mutation
|
3
|
+
module Shift
|
4
|
+
def mutate(code)
|
5
|
+
index = (rand * code.length).to_i
|
6
|
+
direction = rand <= 0.5 ? -1 : 1
|
7
|
+
begin
|
8
|
+
new_char = (code[index].ord + direction).chr
|
9
|
+
rescue
|
10
|
+
return(code)
|
11
|
+
end
|
12
|
+
code[index] = new_char
|
13
|
+
return code
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Algorithm
|
2
|
+
module Genetic
|
3
|
+
#
|
4
|
+
# = terminated exception
|
5
|
+
#
|
6
|
+
class Terminated < StandardError
|
7
|
+
attr_reader :gene
|
8
|
+
|
9
|
+
def initialize(gene)
|
10
|
+
@gene = gene
|
11
|
+
super('terminated.')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# = population management class
|
17
|
+
#
|
18
|
+
class Population
|
19
|
+
include Enumerable
|
20
|
+
attr_reader :generation
|
21
|
+
|
22
|
+
# constructor of population
|
23
|
+
#
|
24
|
+
# code_length :: size of code
|
25
|
+
# population_size :: size of population
|
26
|
+
# evaluator :: an Evaluator instance or Proc instance returns Evaluator instance
|
27
|
+
# opts :: hash of options
|
28
|
+
#
|
29
|
+
# options:
|
30
|
+
# :selection :: an array of module name including select method and params
|
31
|
+
# :crossover :: an array of module name including crossover method and params
|
32
|
+
# :mutation :: an array of module name including mutate method and params
|
33
|
+
# :mutation_chance :: mutation chance (float of 0 to 1)
|
34
|
+
#
|
35
|
+
# need block for generate an initial (random) code of a gene
|
36
|
+
def initialize(population_size, evaluator, opts = {})
|
37
|
+
@evaluator = evaluator
|
38
|
+
@members = Array.new(population_size){
|
39
|
+
Algorithm::Genetic::Gene.new(yield, evaluator, opts)
|
40
|
+
}
|
41
|
+
@generation = 0
|
42
|
+
|
43
|
+
if opts[:selection]
|
44
|
+
@selection_params = opts[:selection].dup
|
45
|
+
selection_module = @selection_params.shift.to_s.capitalize
|
46
|
+
self.extend(Algorithm::Genetic::Selection.const_get(selection_module))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# increment the generation: senection, crossover and mutation
|
51
|
+
def generate
|
52
|
+
@generation += 1
|
53
|
+
select!
|
54
|
+
crossover
|
55
|
+
mutate
|
56
|
+
sort!
|
57
|
+
end
|
58
|
+
|
59
|
+
# iterate each member
|
60
|
+
def each
|
61
|
+
return @members.each unless block_given?
|
62
|
+
@members.each{|m| yield m }
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def sort!
|
67
|
+
@members.sort! do |a, b|
|
68
|
+
b.fitness <=> a.fitness
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def select!
|
73
|
+
@members = select(@members, *@selection_params) do |a, b|
|
74
|
+
b.fitness <=> a.fitness
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def crossover
|
79
|
+
@members += @members[0].crossover_with(@members[1])
|
80
|
+
end
|
81
|
+
|
82
|
+
def mutate
|
83
|
+
@members.each do |m|
|
84
|
+
m.mutate!
|
85
|
+
if m.terminated?
|
86
|
+
sort!
|
87
|
+
raise Terminated.new(m)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Algorithm::Genetic sample: say 'I Love Ruby'
|
4
|
+
#
|
5
|
+
require 'algorithm/genetic'
|
6
|
+
require 'algorithm/genetic/selection/elite'
|
7
|
+
require 'algorithm/genetic/crossover/point'
|
8
|
+
require 'algorithm/genetic/mutation/shift'
|
9
|
+
|
10
|
+
class StringEvaluator < Algorithm::Genetic::Evaluator
|
11
|
+
def initialize(goal)
|
12
|
+
@goal = goal
|
13
|
+
end
|
14
|
+
|
15
|
+
def fitness(gene)
|
16
|
+
total = 0
|
17
|
+
gene.code.each_with_index do |c, i|
|
18
|
+
total -= (c.ord - @goal[i].ord) ** 2
|
19
|
+
end
|
20
|
+
return total
|
21
|
+
end
|
22
|
+
|
23
|
+
def terminated?(gene)
|
24
|
+
gene.code == @goal
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def show(population)
|
29
|
+
puts "Generation: #{population.generation}"
|
30
|
+
population.each do |gene|
|
31
|
+
puts " #{gene.code.join.inspect} (#{gene.fitness})"
|
32
|
+
end
|
33
|
+
puts
|
34
|
+
end
|
35
|
+
|
36
|
+
goal = 'I love Ruby'.split(//)
|
37
|
+
size = 10
|
38
|
+
evaluator = StringEvaluator.new(goal)
|
39
|
+
population = Algorithm::Genetic::Population.new(
|
40
|
+
size, evaluator,
|
41
|
+
selection: [:elite, size - 2],
|
42
|
+
crossover: [:point, 1],
|
43
|
+
mutation: [:shift]
|
44
|
+
) do
|
45
|
+
(' ' * goal.length).each_byte.map{|c| (rand * 255).to_i.chr}
|
46
|
+
end
|
47
|
+
show(population)
|
48
|
+
begin
|
49
|
+
loop do
|
50
|
+
population.generate
|
51
|
+
show(population)
|
52
|
+
end
|
53
|
+
rescue Algorithm::Genetic::Terminated => e
|
54
|
+
puts "Got '#{e.gene.code.join}' at generation #{population.generation}."
|
55
|
+
end
|
data/sample/tsp.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Algorithm::Genetic sample: traveling salesman problem
|
4
|
+
#
|
5
|
+
require 'algorithm/genetic'
|
6
|
+
require 'algorithm/genetic/selection/elite'
|
7
|
+
require 'algorithm/genetic/crossover/order'
|
8
|
+
require 'algorithm/genetic/mutation/swap'
|
9
|
+
|
10
|
+
class City
|
11
|
+
attr_reader :name, :x, :y
|
12
|
+
|
13
|
+
def initialize(name, x, y)
|
14
|
+
@name, @x, @y = name, x, y
|
15
|
+
end
|
16
|
+
|
17
|
+
def distance_with(city)
|
18
|
+
Math.sqrt((@x - city.x) ** 2 + (@y - city.y) ** 2)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class RouteEvaluator < Algorithm::Genetic::Evaluator
|
23
|
+
def initialize(start)
|
24
|
+
@start = start
|
25
|
+
end
|
26
|
+
|
27
|
+
def fitness(gene)
|
28
|
+
total = 0.0
|
29
|
+
prev = @start
|
30
|
+
gene.code.each do |city|
|
31
|
+
total += prev.distance_with(city)
|
32
|
+
prev = city
|
33
|
+
end
|
34
|
+
total += prev.distance_with(@start)
|
35
|
+
return -total
|
36
|
+
end
|
37
|
+
|
38
|
+
def terminated?(gene)
|
39
|
+
# cannot know termination by gene.
|
40
|
+
# stop yourself by limitation of generation count
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def distance_char(city1, city2)
|
46
|
+
'_' * (city1.distance_with(city2) / 20)
|
47
|
+
end
|
48
|
+
|
49
|
+
def show(population, start)
|
50
|
+
puts "Generation: #{population.generation}"
|
51
|
+
population.each do |cities|
|
52
|
+
printf('%7.2f %s', -cities.fitness, start.name)
|
53
|
+
prev = start
|
54
|
+
cities.code.map do |city|
|
55
|
+
print distance_char(prev, city)
|
56
|
+
print city.name
|
57
|
+
prev = city
|
58
|
+
end
|
59
|
+
print distance_char(prev, start)
|
60
|
+
puts start.name
|
61
|
+
end
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# create all cities
|
67
|
+
#
|
68
|
+
name = '@' # previous of 'A'
|
69
|
+
cities = Array.new(20).map do
|
70
|
+
City.new(name.succ!.dup, 0.0 + rand(100), 0.0 + rand(100))
|
71
|
+
end
|
72
|
+
start = cities.shift
|
73
|
+
|
74
|
+
size = 10
|
75
|
+
evaluator = RouteEvaluator.new(start)
|
76
|
+
population = Algorithm::Genetic::Population.new(
|
77
|
+
size, evaluator,
|
78
|
+
selection: [:elite, size - 2],
|
79
|
+
crossover: [:order],
|
80
|
+
mutation: [:swap],
|
81
|
+
mutation_chance: 0.3
|
82
|
+
) do
|
83
|
+
cities.shuffle
|
84
|
+
end
|
85
|
+
|
86
|
+
show(population, start)
|
87
|
+
print "press any key to start."
|
88
|
+
gets
|
89
|
+
1000.times do
|
90
|
+
population.generate
|
91
|
+
show(population, start)
|
92
|
+
end
|
93
|
+
printf('distance: %1.2f ', -population.first.fitness)
|
94
|
+
puts("#{start.name}-#{population.first.code.map{|city| city.name}.join('-')}-#{start.name}")
|