gegene 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +34 -0
- data/README.md +197 -0
- data/gegene.gemspec +25 -0
- data/lib/allele.rb +4 -3
- data/lib/chromosome.rb +39 -50
- data/lib/gegene.rb +2 -1
- data/lib/gegene/version.rb +13 -0
- data/lib/gene.rb +47 -28
- data/lib/genome.rb +26 -19
- data/lib/karyotype.rb +31 -31
- data/lib/population.rb +72 -97
- data/releases/gegene-1.0.0.gem +0 -0
- data/releases/gegene-1.1.0.gem +0 -0
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99c7470e76700e9f8111387771e1fdd0fde4c2ff
|
4
|
+
data.tar.gz: f88f83cea4ea0ae391c2e7f0d70457c1c3e99333
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6806f2aece4b0e729c76b1fbca4a28c40bd0e08eb1fee29e3194dd7f7bb5c75bc6bb8f40de46d0b634adb6ff29ab7843dcd803bdac1de5e5f173d944a293b733
|
7
|
+
data.tar.gz: dd58f6acc164a80abf371428d829e57c9096ffff152295a37c62ed1a58ec5e96831e0d893f59529e2a6329cb5d76348cc0e1a799bc0e03f68950048f1dd40ffc
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
gegene (1.2.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.2.4)
|
10
|
+
multi_json (1.8.2)
|
11
|
+
rspec (2.14.1)
|
12
|
+
rspec-core (~> 2.14.0)
|
13
|
+
rspec-expectations (~> 2.14.0)
|
14
|
+
rspec-mocks (~> 2.14.0)
|
15
|
+
rspec-core (2.14.6)
|
16
|
+
rspec-expectations (2.14.3)
|
17
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
18
|
+
rspec-mocks (2.14.4)
|
19
|
+
simplecov (0.7.1)
|
20
|
+
multi_json (~> 1.0)
|
21
|
+
simplecov-html (~> 0.7.1)
|
22
|
+
simplecov-html (0.7.1)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
gegene!
|
29
|
+
rspec
|
30
|
+
rspec-mocks
|
31
|
+
simplecov
|
32
|
+
|
33
|
+
BUNDLED WITH
|
34
|
+
1.12.5
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
gegene
|
2
|
+
======
|
3
|
+
A generic framework for genetic algorithm fast development in Ruby
|
4
|
+
|
5
|
+
Introduction
|
6
|
+
------------
|
7
|
+
|
8
|
+
>In the computer science field of artificial intelligence, a genetic algorithm (GA) is a search heuristic that mimics the process of natural selection. This heuristic (also sometimes called a metaheuristic) is routinely used to generate useful solutions to optimization and search problems.[1] Genetic algorithms belong to the larger class of evolutionary algorithms (EA), which generate solutions to optimization problems using techniques inspired by natural evolution, such as inheritance, mutation, selection, and crossover.
|
9
|
+
|
10
|
+
from http://en.wikipedia.org/wiki/Genetic_algorithm
|
11
|
+
|
12
|
+
In order to offer a fast prototyping framework for genetic algorithm, I created gegene (French shorter for Eugene).
|
13
|
+
|
14
|
+
With gegene, all you got to do in order to use genetic algorithms to solve a problem is:
|
15
|
+
|
16
|
+
1. define the genome of your solutions in terms of gene type and chromosome organisation (eq: write an array of hashes)
|
17
|
+
2. define the way you would like to evaluate each individuals (eq: write a fitness function which takes a karyotype as parameter)
|
18
|
+
3. create a population of individuals (eq: instantiates a Population object)
|
19
|
+
4. evolve ! (eq: run Population.evolve())
|
20
|
+
5. all your solutions are sorted by fitness value (fittest first) in Population.karyotypes.
|
21
|
+
|
22
|
+
Overview
|
23
|
+
--------
|
24
|
+
|
25
|
+
### Example
|
26
|
+
|
27
|
+
In the following code, we use gegene to find *a* and *b* so _a*a + b = 12_, with *a* included in [0,5], and *b* included in [-5,5]:
|
28
|
+
```Ruby
|
29
|
+
require 'gegene'
|
30
|
+
FITNESS_TARGET = 1 / 0.001
|
31
|
+
population = Population.new(
|
32
|
+
50,
|
33
|
+
[{a:Gene.Integer(0, 5)},{b:Gene.Integer(-5,5)}],
|
34
|
+
lambda {|k| 1 / (0.001 + (12-(k[:a]**2+k[:b])).abs) }
|
35
|
+
)
|
36
|
+
population.set_mutation_rate(0.5).set_fitness_target(FITNESS_TARGET).evolve(50)
|
37
|
+
bk = population.karyotypes[0]
|
38
|
+
warn "a:#{bk[:a]} b:#{bk[:b]} => a*a + b = #{bk[:a]**2+bk[:b]}"
|
39
|
+
```
|
40
|
+
|
41
|
+
The result could be :
|
42
|
+
|
43
|
+
a:4 b:-4 => a*a + b = 12
|
44
|
+
|
45
|
+
or
|
46
|
+
|
47
|
+
a:3 b:3 => a*a + b = 12
|
48
|
+
|
49
|
+
_Note: this source code is available 'in example/simple.rb'_
|
50
|
+
|
51
|
+
### Features
|
52
|
+
|
53
|
+
At this very moment, gegene features:
|
54
|
+
|
55
|
+
* Inheritance
|
56
|
+
* Mutation
|
57
|
+
* Cross over
|
58
|
+
* Non linear selection
|
59
|
+
* An optional fitness value cache, to avoid extra computing
|
60
|
+
* A pretty cool name
|
61
|
+
|
62
|
+
### What will come next ?
|
63
|
+
|
64
|
+
My next moves will be:
|
65
|
+
|
66
|
+
* your call
|
67
|
+
|
68
|
+
Tutorial
|
69
|
+
--------
|
70
|
+
|
71
|
+
*This tutorial is available in 'example/one_max.rb'*
|
72
|
+
|
73
|
+
Let's consider the one max problem (an explanation can be found here : http://tracer.lcc.uma.es/problems/onemax/onemax.html ), where the goal is maximizing the number of ones of an array of bits.
|
74
|
+
|
75
|
+
For this example, we'll use an array of 3 bits, named v1, v2 and v3.
|
76
|
+
Obviously, the solution to this problem would be:
|
77
|
+
|
78
|
+
* v1 = 1
|
79
|
+
* v2 = 1
|
80
|
+
* v3 = 1
|
81
|
+
|
82
|
+
Let's see if gegene is able to figure this out !
|
83
|
+
|
84
|
+
### Setting the genome description
|
85
|
+
|
86
|
+
First of all, you will need to add the following line to the top of your script:
|
87
|
+
```Ruby
|
88
|
+
require 'gegene'
|
89
|
+
```
|
90
|
+
|
91
|
+
If your lib path contains gegene, your script should still execute well.
|
92
|
+
|
93
|
+
Then, we have to figure out a way to describe the potential solutions to this problem.
|
94
|
+
Obviously, there is three variables to this problem (v1, v2 & v3), so we will use a genome containing 3 genes.
|
95
|
+
Each of these gene allow 0 or 1 as allele's value, so we will use an integer gene, whose range of values will be [0,1]. Do not add this source code to your script for the moment:
|
96
|
+
```Ruby
|
97
|
+
Gene.Integer(0, 1)
|
98
|
+
```
|
99
|
+
|
100
|
+
In order to maximize the diversity of combinations, well describe three chromosome descriptions, each of them containing an integer gene and named after the corresponding variable:
|
101
|
+
```Ruby
|
102
|
+
# Follow a genome description for the one max problem:
|
103
|
+
genome_description = [
|
104
|
+
{ v1: Gene.Integer(0,1) },
|
105
|
+
{ v2: Gene.Integer(0,1) },
|
106
|
+
{ v3: Gene.Integer(0,1) }
|
107
|
+
]
|
108
|
+
```
|
109
|
+
|
110
|
+
### Defining a fitness function
|
111
|
+
|
112
|
+
Next, we have to define the fitness function. This one is a pretty simple one, as we can sum the three values in order to score a karyotype:
|
113
|
+
```Ruby
|
114
|
+
# Here is the fitness function of the one max problem:
|
115
|
+
def fitness(karyotype)
|
116
|
+
karyotype[:v1] + karyotype[:v2] + karyotype[:v3]
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
_Note that each of the karyotype 's allele can be accessed through its gene name_
|
121
|
+
The parameter of the fitness function is a [karyotype]( http://en.wikipedia.org/wiki/Karyotype). It contains all the chromosomes of a specific individual of our population.
|
122
|
+
|
123
|
+
### Creating and making a population evolve
|
124
|
+
|
125
|
+
Now that we are happy with our genome and fitness function, we have to create a population:
|
126
|
+
```Ruby
|
127
|
+
# We create a population of six individuals with the previous desc & func:
|
128
|
+
population = Population.new(6, genome_description, method(:fitness))
|
129
|
+
````
|
130
|
+
|
131
|
+
We already know the result of our fitness function for the best solution, so we can set the fitness target of our population. If the fitness target is reached, the population will stop evolving. A good usage to the fitness target is setting it to an acceptable score to avoid endless processing of a population with very small enhancement of the solutions.
|
132
|
+
```Ruby
|
133
|
+
# As we known the best solution for the one max problem, we set a
|
134
|
+
# fitness target of 3 (1+1+1).
|
135
|
+
population.fitness_target = 3
|
136
|
+
````
|
137
|
+
|
138
|
+
As the population contains only 6 individuals, we set a high mutation rate in order to introduce "new blood" at each iteration:
|
139
|
+
```Ruby
|
140
|
+
# As the population is quite small, we add a little more funk to our
|
141
|
+
# evolution process by setting a 30% mutation rate
|
142
|
+
population.mutation_rate = 0.3
|
143
|
+
```
|
144
|
+
|
145
|
+
Let me introduce to you.... Evolution !
|
146
|
+
```Ruby
|
147
|
+
# Let's go for some darwinist fun !
|
148
|
+
population.evolve(10)
|
149
|
+
````
|
150
|
+
|
151
|
+
In order to examine the best solution found, you can check the array of karyotypes (sorted by fitness value) describing the current population state:
|
152
|
+
```Ruby
|
153
|
+
# population.karyotypes is sorted by fitness score, so we can assume that
|
154
|
+
# the first element is the fittest
|
155
|
+
best_karyotype = population.karyotypes[0]
|
156
|
+
|
157
|
+
puts "Best karyotype scored #{best_karyotype.fitness}:"
|
158
|
+
[:v1, :v2, :v3].each {|x| puts " #{x.to_s}:#{best_karyotype[x]}" }
|
159
|
+
````
|
160
|
+
|
161
|
+
This code should display:
|
162
|
+
|
163
|
+
Best karyotype scored 3:
|
164
|
+
v1:1
|
165
|
+
v2:1
|
166
|
+
v3:1
|
167
|
+
|
168
|
+
Congratulations for your first evolution with gegene !
|
169
|
+
|
170
|
+
More details on the genes
|
171
|
+
-------------------------
|
172
|
+
|
173
|
+
### Available Gene types
|
174
|
+
|
175
|
+
* Gene.Integer(min, max): An integer, randomly selected in the range [min, max].
|
176
|
+
* Gene.Float(min, max): A float, randomly selected in the range [min, max].
|
177
|
+
* Gene.Enum(values_array): A value from a set of values provided in an array.
|
178
|
+
|
179
|
+
### Adding new gene types
|
180
|
+
|
181
|
+
If needed, you are able to add gene type to any project.
|
182
|
+
In order to do so, you have to create a class inheriting from the Gene class.
|
183
|
+
This class *must* provide implementation for two methods:
|
184
|
+
|
185
|
+
````Ruby
|
186
|
+
def random_allele_value()
|
187
|
+
end
|
188
|
+
````
|
189
|
+
Create a random value, according to the set of rules you choosed (ex: BooleanGene should return true or false, on a random basis)
|
190
|
+
|
191
|
+
````Ruby
|
192
|
+
def mutate(previous_value)
|
193
|
+
end
|
194
|
+
````
|
195
|
+
Create a mutated value, which can (but not necessarily) depends on the previous value an allele used to carry.
|
196
|
+
|
197
|
+
An example of gene type creation is available in 'example/adding_gene_type.rb'.
|
data/gegene.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'gegene/version'
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'gegene'
|
5
|
+
s.version = Gegene::Version::STRING
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.summary = "Genetic algorithm helpers"
|
8
|
+
s.description = "Framework for genetic algorithm fast development"
|
9
|
+
s.authors = ["Alexandre Ignjatovic"]
|
10
|
+
s.email = 'alexandre.ignjatovic@gmail.com'
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.files = `git ls-files`.split($RS).reject do |file|
|
13
|
+
file =~ %r{^(?:
|
14
|
+
spec/.*
|
15
|
+
|Gemfile
|
16
|
+
|Rakefile
|
17
|
+
|\.rspec
|
18
|
+
|\.gitignore
|
19
|
+
|\.rubocop.yml
|
20
|
+
|\.rubocop_todo.yml
|
21
|
+
|.*\.eps
|
22
|
+
)$}x
|
23
|
+
end
|
24
|
+
s.homepage = 'https://github.com/bankair/gegene'
|
25
|
+
end
|
data/lib/allele.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
+
# Specific value for a specific gene
|
1
2
|
class Allele
|
2
3
|
attr_accessor :value
|
3
4
|
def initialize(gene, value)
|
4
5
|
@gene = gene
|
5
6
|
@value = value
|
6
7
|
end
|
7
|
-
|
8
|
+
|
8
9
|
def mutate
|
9
10
|
@value = @gene.mutate(@value)
|
10
11
|
end
|
11
|
-
|
12
|
+
|
12
13
|
def copy
|
13
14
|
# /!\ if @value is a ref, its underlying object won't be copied
|
14
15
|
Allele.new(@gene, @value)
|
15
16
|
end
|
16
|
-
end
|
17
|
+
end
|
data/lib/chromosome.rb
CHANGED
@@ -1,70 +1,59 @@
|
|
1
1
|
require 'gene'
|
2
2
|
require 'allele'
|
3
|
+
require 'forwardable'
|
3
4
|
|
4
|
-
# This class represent a chromosome, which contains several allele
|
5
|
-
# and is able to mutate, and to cross over.
|
5
|
+
# This class represent a chromosome, which contains several allele
|
6
|
+
# (gene's values) and is able to mutate, and to cross over.
|
6
7
|
class Chromosome
|
7
|
-
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@alleles, :size, :[], :each_with_index, :map
|
8
11
|
# Construct a chromosome from an array of alleles
|
9
12
|
def initialize(alleles)
|
10
|
-
|
11
|
-
alleles.
|
13
|
+
unless alleles.is_a?(Array)
|
14
|
+
puts alleles.inspect
|
15
|
+
raise 'this constructor expect an array of alleles as input'
|
16
|
+
end
|
12
17
|
@alleles = alleles
|
13
18
|
end
|
14
|
-
|
19
|
+
|
15
20
|
# Copy the current chromosome and all its alleles
|
16
21
|
def copy
|
17
|
-
Chromosome.new(
|
22
|
+
Chromosome.new(map(&:copy))
|
18
23
|
end
|
19
|
-
|
24
|
+
|
20
25
|
# Create a random chromosome from a description
|
21
|
-
def
|
22
|
-
|
26
|
+
def self.create_random_from(description)
|
27
|
+
new(description.map(&:create_random))
|
23
28
|
end
|
24
|
-
|
25
|
-
# Returns the allele at a specific position
|
26
|
-
def [](gene_position)
|
27
|
-
@alleles[gene_position]
|
28
|
-
end
|
29
|
-
|
30
|
-
# Number of underlying alleles
|
31
|
-
def size
|
32
|
-
@alleles.size
|
33
|
-
end
|
34
|
-
|
29
|
+
|
35
30
|
# Mutate a randomly selected allele of the current chromosome
|
36
31
|
def mutate
|
37
|
-
|
38
|
-
@alleles[allele_index].mutate()
|
32
|
+
@alleles[rand size].mutate
|
39
33
|
end
|
40
|
-
|
34
|
+
|
41
35
|
# Aggregate all alleles values
|
42
|
-
def aggregated_alleles
|
43
|
-
|
36
|
+
def aggregated_alleles
|
37
|
+
map(&:value).join(';')
|
44
38
|
end
|
45
|
-
|
39
|
+
|
46
40
|
# Cross over two chromosomes to provide a new one
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
swap_index
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
index += 1
|
65
|
-
end
|
66
|
-
return Chromosome.new(new_alleles)
|
67
|
-
end
|
41
|
+
def self.cross_over(chromo_a, chromo_b)
|
42
|
+
chromo_a, chromo_b = randomize_chromosomes(chromo_a, chromo_b)
|
43
|
+
size = chromo_a.size
|
44
|
+
return chromo_a.copy if size < 2
|
45
|
+
return new([chromo_a[0], chromo_b[1]].map!(&:copy)) if size == 2
|
46
|
+
cross_over_impl(chromo_a, chromo_b, size)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.cross_over_impl(chromosome_a, chromosome_b, size)
|
50
|
+
swap_index = rand(size - 1)
|
51
|
+
new(chromosome_a.each_with_index.map do |from_a, index|
|
52
|
+
(index <= swap_index ? from_a : chromosome_b[index]).copy
|
53
|
+
end)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.randomize_chromosomes(*chromosomes)
|
57
|
+
chromosomes.sort_by! { rand }
|
68
58
|
end
|
69
|
-
|
70
|
-
end
|
59
|
+
end
|
data/lib/gegene.rb
CHANGED
data/lib/gene.rb
CHANGED
@@ -1,17 +1,28 @@
|
|
1
|
-
|
2
|
-
class Gene
|
3
|
-
def mutate(
|
4
|
-
|
1
|
+
# A Gene can take different values, randomly mutate.
|
2
|
+
class Gene
|
3
|
+
def mutate(_allele)
|
4
|
+
raise_missing_impl :mutate
|
5
5
|
end
|
6
6
|
|
7
7
|
def random_allele_value
|
8
|
-
|
8
|
+
raise_missing_impl :random_allele_value
|
9
9
|
end
|
10
|
+
|
10
11
|
def create_random
|
11
|
-
Allele.new(self,
|
12
|
+
Allele.new(self, random_allele_value)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
MISSING_IMPL_ERR_FMT =
|
18
|
+
"the '%s' function must be overloaded in the inheriting class.".freeze
|
19
|
+
|
20
|
+
def raise_missing_impl(func)
|
21
|
+
raise MISSING_IMPL_ERR_FMT % func
|
12
22
|
end
|
13
23
|
end
|
14
24
|
|
25
|
+
# Root class for all numeric genes
|
15
26
|
class NumericGene < Gene
|
16
27
|
attr_accessor :min, :max
|
17
28
|
def initialize(min, max)
|
@@ -22,51 +33,59 @@ class NumericGene < Gene
|
|
22
33
|
|
23
34
|
def mutate(previous_value)
|
24
35
|
next_value = random_allele_value
|
25
|
-
while next_value == previous_value
|
26
|
-
next_value = random_allele_value
|
27
|
-
end
|
36
|
+
next_value = random_allele_value while next_value == previous_value
|
28
37
|
next_value
|
29
38
|
end
|
30
|
-
end
|
39
|
+
end
|
31
40
|
|
41
|
+
# Integer gene class
|
32
42
|
class IntegerGene < NumericGene
|
33
|
-
def
|
34
|
-
|
35
|
-
|
43
|
+
def random_allele_value
|
44
|
+
rand(min..max)
|
45
|
+
end
|
36
46
|
end
|
37
47
|
|
48
|
+
# Float gene class
|
38
49
|
class FloatGene < NumericGene
|
39
|
-
def initialize(min, max) super(min, max) end
|
40
|
-
|
41
50
|
def random_allele_value
|
42
|
-
rand
|
51
|
+
rand * (max - min) + min
|
43
52
|
end
|
44
53
|
end
|
45
54
|
|
55
|
+
# Enumeration gene class
|
46
56
|
class EnumGene < Gene
|
47
57
|
attr_accessor :enum_values
|
48
58
|
|
49
59
|
def initialize(enum_values)
|
50
|
-
|
51
|
-
|
60
|
+
unless enum_values.is_a? Array
|
61
|
+
raise 'EnumGene initialization require an Array'
|
62
|
+
end
|
63
|
+
raise 'EnumGene require at least two values' unless enum_values.size > 1
|
52
64
|
@enum_values = enum_values
|
53
65
|
end
|
54
|
-
|
66
|
+
|
55
67
|
def random_allele_value
|
56
68
|
@enum_values[rand @enum_values.size]
|
57
69
|
end
|
58
|
-
|
70
|
+
|
59
71
|
def mutate(previous_value)
|
60
|
-
new_value =
|
61
|
-
while new_value == previous_value
|
62
|
-
new_value = self.random_allele_value
|
63
|
-
end
|
72
|
+
new_value = random_allele_value
|
73
|
+
new_value = random_allele_value while new_value == previous_value
|
64
74
|
new_value
|
65
75
|
end
|
66
76
|
end
|
67
77
|
|
78
|
+
# Gene class build methods
|
68
79
|
class Gene
|
69
|
-
def self.Integer(min, max)
|
70
|
-
|
71
|
-
|
72
|
-
|
80
|
+
def self.Integer(min, max)
|
81
|
+
IntegerGene.new(min, max)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.Float(min, max)
|
85
|
+
FloatGene.new(min, max)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.Enum(enum_values)
|
89
|
+
EnumGene.new(enum_values)
|
90
|
+
end
|
91
|
+
end
|
data/lib/genome.rb
CHANGED
@@ -3,36 +3,43 @@ require 'Karyotype'
|
|
3
3
|
# This class stands for the genome. It is a description of all
|
4
4
|
# the gene describing a specific population.
|
5
5
|
class Genome
|
6
|
-
|
7
6
|
DEFAULT_CROSS_OVER_RATE = 0.01
|
8
|
-
|
7
|
+
attr_accessor :gene_positions
|
8
|
+
attr_reader :cross_over_rate
|
9
|
+
|
9
10
|
def initialize(genome_description)
|
10
|
-
|
11
|
-
|
12
|
-
@gene_positions = {}
|
13
|
-
genome_description.each_with_index do |chomosome_hash, chromosome_position|
|
14
|
-
gene_array = []
|
15
|
-
chomosome_hash.keys.each_with_index do |gene_name, gene_position|
|
16
|
-
@gene_positions[gene_name] = [chromosome_position, gene_position]
|
17
|
-
gene_array << chomosome_hash[gene_name]
|
18
|
-
end
|
19
|
-
@chromosomes_description << gene_array
|
11
|
+
unless genome_description.is_a? Array
|
12
|
+
raise 'Genome description MUST be an Array'
|
20
13
|
end
|
14
|
+
initialize_genes_and_chromosomes_from! genome_description
|
21
15
|
@cross_over_rate = DEFAULT_CROSS_OVER_RATE
|
22
16
|
end
|
23
|
-
|
24
|
-
attr_reader :cross_over_rate
|
17
|
+
|
25
18
|
def cross_over_rate=(rate)
|
26
|
-
raise
|
19
|
+
raise 'cross_over_rate must be included in [0,1]' unless rate.between?(0, 1)
|
27
20
|
@cross_over_rate = rate
|
28
21
|
end
|
29
|
-
|
22
|
+
|
30
23
|
def get_gene_position(gene_name)
|
31
24
|
@gene_positions[gene_name]
|
32
25
|
end
|
33
|
-
|
26
|
+
|
34
27
|
def create_random_karyotype
|
35
28
|
Karyotype.create_random_from(self, @chromosomes_description)
|
36
29
|
end
|
37
|
-
|
38
|
-
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def initialize_genes_and_chromosomes_from!(genome_description)
|
34
|
+
@chromosomes_description = []
|
35
|
+
@gene_positions = {}
|
36
|
+
genome_description.each_with_index do |chomosome_hash, chromosome_position|
|
37
|
+
gene_array = []
|
38
|
+
chomosome_hash.keys.each_with_index do |gene_name, gene_position|
|
39
|
+
@gene_positions[gene_name] = [chromosome_position, gene_position]
|
40
|
+
gene_array << chomosome_hash[gene_name]
|
41
|
+
end
|
42
|
+
@chromosomes_description << gene_array
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/karyotype.rb
CHANGED
@@ -5,34 +5,34 @@ require 'digest/md5'
|
|
5
5
|
class Karyotype
|
6
6
|
attr_accessor :chromosomes
|
7
7
|
attr_accessor :fitness
|
8
|
-
|
8
|
+
|
9
9
|
def initialize(genome, chromosomes_description)
|
10
10
|
@genome = genome
|
11
11
|
@chromosomes_description = chromosomes_description
|
12
12
|
end
|
13
13
|
private :initialize
|
14
|
-
|
14
|
+
|
15
15
|
# Copy self and all its chromosomes to a new karyotype
|
16
16
|
def copy
|
17
17
|
karyotype = Karyotype.new(@genome, @chromosomes_description)
|
18
|
-
karyotype.chromosomes =
|
18
|
+
karyotype.chromosomes = chromosomes.map(&:copy)
|
19
19
|
karyotype
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def to_s
|
23
|
-
@genome.gene_positions.keys.map{|gn| "#{gn}=>#{self[gn]}"}.join';'
|
23
|
+
@genome.gene_positions.keys.map { |gn| "#{gn}=>#{self[gn]}" }.join';'
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# Create a random karyotype from a specific genome an its associated
|
27
27
|
# chromosomes description
|
28
|
-
def
|
28
|
+
def self.create_random_from(genome, chromosomes_description)
|
29
29
|
karyotype = Karyotype.new(genome, chromosomes_description)
|
30
30
|
karyotype.chromosomes = chromosomes_description.map {|description|
|
31
31
|
Chromosome.create_random_from(description)
|
32
32
|
}
|
33
33
|
karyotype
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
# Return the allele value of a specific named gene
|
37
37
|
# We strongly recommand using symbols as gene name
|
38
38
|
def [](gene_name)
|
@@ -40,44 +40,44 @@ class Karyotype
|
|
40
40
|
chromosome_position, gene_position =
|
41
41
|
@genome.get_gene_position(gene_name)
|
42
42
|
return nil if chromosome_position.nil? || gene_position.nil?
|
43
|
-
|
43
|
+
@chromosomes[chromosome_position][gene_position].value
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Breeding function
|
47
47
|
# Create a new karyotype based on self and an other
|
48
48
|
def +(other)
|
49
49
|
child = Karyotype.new(@genome, @chromosomes_description)
|
50
50
|
child.chromosomes = []
|
51
|
-
other.chromosomes.each_with_index
|
52
|
-
|
53
|
-
|
54
|
-
if (rand(100) / 100.0) < @genome.cross_over_rate then
|
55
|
-
# Crossing over required
|
56
|
-
child_chromosome =
|
57
|
-
Chromosome.cross_over(chromosome, chromosomes[index])
|
58
|
-
else
|
59
|
-
# Standard breeding via random selection
|
60
|
-
child_chromosome = (rand(2)==0?chromosome : chromosomes[index]).copy
|
61
|
-
end
|
62
|
-
child.chromosomes.push(child_chromosome)
|
63
|
-
}
|
51
|
+
other.chromosomes.each_with_index do |chromosome, index|
|
52
|
+
child.chromosomes.push(select_chromosome(chromosome, chromosomes[index]))
|
53
|
+
end
|
64
54
|
child
|
65
55
|
end
|
66
|
-
|
56
|
+
|
67
57
|
# Aggregate all the allele into a md5 hash value
|
68
|
-
def to_md5
|
69
|
-
if
|
70
|
-
@hash_value = @chromosomes.map
|
71
|
-
chromosome.aggregated_alleles
|
72
|
-
}.join(";")
|
58
|
+
def to_md5
|
59
|
+
if @hash_value.nil?
|
60
|
+
@hash_value = @chromosomes.map(&:aggregated_alleles).join(';')
|
73
61
|
@hash_value = Digest::MD5.hexdigest(@hash_value)
|
74
62
|
end
|
75
63
|
@hash_value
|
76
64
|
end
|
77
|
-
|
65
|
+
|
78
66
|
# Randomly mutate an allele of a randomly selected chromosome
|
79
67
|
def mutate
|
80
68
|
@chromosomes[rand @chromosomes.size].mutate
|
81
69
|
self
|
82
70
|
end
|
83
|
-
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def select_chromosome(chromosome_a, chromosome_b)
|
75
|
+
if (rand(100) / 100.0) < @genome.cross_over_rate
|
76
|
+
# Crossing over required
|
77
|
+
Chromosome.cross_over(chromosome_a, chromosome_b)
|
78
|
+
else
|
79
|
+
# Standard breeding via random selection
|
80
|
+
(rand(2) == 0 ? chromosome_a : chromosome_b).copy
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/population.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'genome'
|
2
|
-
|
2
|
+
# Stand for a whole population used for an evolution experiment
|
3
3
|
class Population
|
4
4
|
DEFAULT_MUTATION_RATE = 0.01
|
5
5
|
DEFAULT_KEEP_ALIVE_RATE = 0.1
|
@@ -8,83 +8,99 @@ class Population
|
|
8
8
|
|
9
9
|
attr_accessor :fitness_target, :karyotypes, :force_fitness_recalculation
|
10
10
|
attr_reader :mutation_rate, :keep_alive_rate
|
11
|
-
|
11
|
+
|
12
12
|
# mutation rate setter function
|
13
13
|
# Accept values in the range [0,1]
|
14
14
|
def mutation_rate=(value)
|
15
|
-
|
16
|
-
@mutation_rate = value
|
15
|
+
@mutation_rate = validate!(:mutation_rate, value)
|
17
16
|
end
|
18
|
-
|
17
|
+
|
19
18
|
# keep alive rate setter function
|
20
19
|
# Accept values in the range [0,1]
|
21
20
|
def keep_alive_rate=(value)
|
22
|
-
|
23
|
-
@keep_alive_rate = value
|
24
|
-
end
|
25
|
-
|
26
|
-
|
27
|
-
# Run the fitness function for all karyotypes, and sort it by fitness
|
28
|
-
def evaluate
|
29
|
-
if @force_fitness_recalculation then
|
30
|
-
@karyotypes.each do |karyotype|
|
31
|
-
karyotype.fitness = @fitness_calculator.call(karyotype)
|
32
|
-
@fitness_hash[karyotype.to_md5()] = karyotype.fitness
|
33
|
-
end
|
34
|
-
else
|
35
|
-
@karyotypes.each do |karyotype|
|
36
|
-
if karyotype.fitness.nil? then
|
37
|
-
if @fitness_hash[karyotype.to_md5()].nil? then
|
38
|
-
karyotype.fitness = @fitness_calculator.call(karyotype)
|
39
|
-
@fitness_hash[karyotype.to_md5()] = karyotype.fitness
|
40
|
-
else
|
41
|
-
karyotype.fitness = @fitness_hash[karyotype.to_md5()]
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
@karyotypes.sort! { |x,y| y.fitness <=> x.fitness }
|
21
|
+
@keep_alive_rate = validate!(:keep_alive_rate, value)
|
47
22
|
end
|
48
23
|
|
49
|
-
private :evaluate
|
50
|
-
|
51
24
|
def initialize(size, genome, fitness_calculator)
|
52
|
-
raise
|
25
|
+
raise 'size must be strictly positive.' if size < 1
|
53
26
|
@fitness_hash = {}
|
54
27
|
@force_fitness_recalculation = DEFAULT_FORCE_FITNESS_RECALCULATION
|
55
28
|
@mutation_rate = DEFAULT_MUTATION_RATE
|
56
29
|
@keep_alive_rate = DEFAULT_KEEP_ALIVE_RATE
|
57
30
|
@genome = Genome.new(genome)
|
58
31
|
@fitness_calculator = fitness_calculator
|
59
|
-
@karyotypes = Array.new(size){ @genome.create_random_karyotype }
|
32
|
+
@karyotypes = Array.new(size) { @genome.create_random_karyotype }
|
60
33
|
evaluate
|
61
34
|
end
|
62
35
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
@mutation_rate= rate
|
67
|
-
self
|
36
|
+
%i(mutation_rate keep_alive_rate
|
37
|
+
fitness_target force_fitness_recalculation).each do |sym|
|
38
|
+
define_method("set_#{sym}") { |value| tap { |o| o.send("#{sym}=", value) } }
|
68
39
|
end
|
69
40
|
|
70
|
-
def
|
71
|
-
|
72
|
-
@keep_alive_rate = rate
|
73
|
-
self
|
41
|
+
def size
|
42
|
+
@karyotypes.size
|
74
43
|
end
|
75
44
|
|
76
|
-
|
77
|
-
|
45
|
+
# This function make ou population evolving by:
|
46
|
+
# * Selecting and breeding the fittest karyotypes
|
47
|
+
# * Running the fitness evaluation on all the newly created karyotyopes
|
48
|
+
# The selection process include three subprocesses:
|
49
|
+
# * Selecting the fittest individuals to keep alive
|
50
|
+
# * Mutating randomly (linear) selected individuals
|
51
|
+
# * Breeding randomly (fitness weighted) selected individuals
|
52
|
+
def evolve(iterations = DEFAULT_EVOLVE_ITERATIONS)
|
53
|
+
i = 1
|
54
|
+
while (i <= iterations) &&
|
55
|
+
(@fitness_target.nil? || @fitness_target > @karyotypes[0].fitness)
|
56
|
+
evolve_impl
|
57
|
+
i += 1
|
58
|
+
end
|
78
59
|
self
|
79
60
|
end
|
80
61
|
|
81
|
-
|
82
|
-
|
83
|
-
|
62
|
+
private
|
63
|
+
|
64
|
+
NO_FITNESS = :no_fitness
|
65
|
+
|
66
|
+
# Run the fitness function for all karyotypes, and sort it by fitness
|
67
|
+
def evaluate
|
68
|
+
if @force_fitness_recalculation
|
69
|
+
@karyotypes.each { |karyotype| update!(karyotype, fitness(karyotype)) }
|
70
|
+
else
|
71
|
+
@karyotypes.each do |karyotype|
|
72
|
+
update!(karyotype, cached_fitness(karyotype)) if karyotype.fitness.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@karyotypes.sort_by! { |karyotype| - karyotype.fitness }
|
84
76
|
end
|
85
77
|
|
86
|
-
def
|
87
|
-
|
78
|
+
def update!(karyotype, fitness)
|
79
|
+
karyotype.fitness = fitness
|
80
|
+
@fitness_hash[karyotype.to_md5] = karyotype.fitness
|
81
|
+
end
|
82
|
+
|
83
|
+
def fitness(karyotype)
|
84
|
+
@fitness_calculator.call(karyotype)
|
85
|
+
end
|
86
|
+
|
87
|
+
def cached_fitness(karyotype)
|
88
|
+
@fitness_hash.fetch(karyotype.to_md5) { fitness karyotype }
|
89
|
+
end
|
90
|
+
|
91
|
+
def evolve_impl
|
92
|
+
# Keeping alive a specific amount of the best karyotypes
|
93
|
+
keep_alive_count = Integer(@karyotypes.size * @keep_alive_rate)
|
94
|
+
mutation_count = Integer(@karyotypes.size * @mutation_rate)
|
95
|
+
@karyotypes = build_new_karyotypes(keep_alive_count, mutation_count)
|
96
|
+
evaluate
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_new_karyotypes(keep_alive_count, mutation_count)
|
100
|
+
remaining = @karyotypes.size - mutation_count - keep_alive_count
|
101
|
+
@karyotypes[0, keep_alive_count]
|
102
|
+
.concat(Array.new(mutation_count) { create_random_mutation })
|
103
|
+
.concat(Array.new(remaining) { random_breed })
|
88
104
|
end
|
89
105
|
|
90
106
|
def linear_random_select
|
@@ -95,60 +111,19 @@ class Population
|
|
95
111
|
linear_random_select.copy.mutate
|
96
112
|
end
|
97
113
|
|
98
|
-
private :linear_random_select, :create_random_mutation
|
99
|
-
|
100
114
|
def fitness_weighted_random_select
|
101
115
|
@karyotypes[
|
102
116
|
@karyotypes.size -
|
103
|
-
|
117
|
+
Integer(Math.sqrt(Math.sqrt(1 + rand(@karyotypes.size**4 - 1))))
|
104
118
|
]
|
105
119
|
end
|
106
|
-
|
120
|
+
|
107
121
|
def random_breed
|
108
122
|
fitness_weighted_random_select + fitness_weighted_random_select
|
109
123
|
end
|
110
124
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
# * Selecting and breeding the fittest karyotypes
|
115
|
-
# * Running the fitness evaluation on all the newly created karyotyopes
|
116
|
-
# The selection process include three subprocesses:
|
117
|
-
# * Selecting the fittest individuals to keep alive
|
118
|
-
# * Mutating randomly (linear) selected individuals
|
119
|
-
# * Breeding randomly (fitness weighted) selected individuals
|
120
|
-
def evolve(iterations = DEFAULT_EVOLVE_ITERATIONS)
|
121
|
-
i = 1
|
122
|
-
while (i <= iterations) &&
|
123
|
-
(@fitness_target.nil? || @fitness_target > @karyotypes[0].fitness) do
|
124
|
-
evolve_impl
|
125
|
-
i += 1
|
126
|
-
end
|
127
|
-
self
|
128
|
-
end
|
129
|
-
|
130
|
-
def evolve_impl
|
131
|
-
new_population = []
|
132
|
-
|
133
|
-
# Keeping alive a specific amount of the best karyotypes
|
134
|
-
keep_alive_count = Integer(@karyotypes.size * @keep_alive_rate)
|
135
|
-
if keep_alive_count > 0 then
|
136
|
-
@karyotypes[0, keep_alive_count].each {|karyotype| new_population.push(karyotype)}
|
137
|
-
end
|
138
|
-
|
139
|
-
mutation_count = Integer(@karyotypes.size * @mutation_rate)
|
140
|
-
(0..mutation_count-1).each {
|
141
|
-
new_population.push create_random_mutation
|
142
|
-
}
|
143
|
-
|
144
|
-
remaining = @karyotypes.size-mutation_count-keep_alive_count
|
145
|
-
(0..remaining-1).each {
|
146
|
-
child = random_breed
|
147
|
-
new_population.push child
|
148
|
-
}
|
149
|
-
@karyotypes = new_population
|
150
|
-
evaluate
|
125
|
+
def validate!(label, value)
|
126
|
+
raise "#{label} value must be included in [0,1]" unless value.between?(0, 1)
|
127
|
+
value
|
151
128
|
end
|
152
|
-
|
153
|
-
|
154
|
-
end
|
129
|
+
end
|
Binary file
|
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gegene
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Ignjatovic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Framework for genetic algorithm fast development
|
14
14
|
email: alexandre.ignjatovic@gmail.com
|
@@ -16,17 +16,23 @@ executables: []
|
|
16
16
|
extensions: []
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
|
+
- Gemfile.lock
|
20
|
+
- LICENSE.txt
|
21
|
+
- README.md
|
22
|
+
- example/adding_gene_type.rb
|
23
|
+
- example/one_max.rb
|
24
|
+
- example/simple.rb
|
25
|
+
- gegene.gemspec
|
19
26
|
- lib/allele.rb
|
20
27
|
- lib/chromosome.rb
|
21
28
|
- lib/gegene.rb
|
29
|
+
- lib/gegene/version.rb
|
22
30
|
- lib/gene.rb
|
23
31
|
- lib/genome.rb
|
24
32
|
- lib/karyotype.rb
|
25
33
|
- lib/population.rb
|
26
|
-
-
|
27
|
-
-
|
28
|
-
- example/simple.rb
|
29
|
-
- LICENSE.txt
|
34
|
+
- releases/gegene-1.0.0.gem
|
35
|
+
- releases/gegene-1.1.0.gem
|
30
36
|
homepage: https://github.com/bankair/gegene
|
31
37
|
licenses:
|
32
38
|
- MIT
|
@@ -47,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
53
|
version: '0'
|
48
54
|
requirements: []
|
49
55
|
rubyforge_project:
|
50
|
-
rubygems_version: 2.
|
56
|
+
rubygems_version: 2.6.5
|
51
57
|
signing_key:
|
52
58
|
specification_version: 4
|
53
59
|
summary: Genetic algorithm helpers
|