algorithm-genetic 0.0.1
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 +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}")
|