evolvable 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +4 -0
- data/CHANGELOG.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -2
- data/README.md +227 -290
- data/README_YARD.md +237 -0
- data/bin/console +18 -5
- data/evolvable.gemspec +2 -0
- data/examples/ascii_art.rb +62 -0
- data/examples/ascii_gene.rb +9 -0
- data/examples/hello_world.rb +91 -0
- data/examples/images/diagram.png +0 -0
- data/examples/stickman.rb +77 -0
- data/exe/hello +16 -0
- data/lib/evolvable/count_gene.rb +42 -0
- data/lib/evolvable/equalize_goal.rb +29 -0
- data/lib/evolvable/evaluation.rb +29 -6
- data/lib/evolvable/evolution.rb +38 -6
- data/lib/evolvable/gene.rb +47 -5
- data/lib/evolvable/gene_combination.rb +69 -0
- data/lib/evolvable/genome.rb +86 -0
- data/lib/evolvable/goal.rb +36 -3
- data/lib/evolvable/maximize_goal.rb +30 -0
- data/lib/evolvable/minimize_goal.rb +29 -0
- data/lib/evolvable/mutation.rb +66 -14
- data/lib/evolvable/point_crossover.rb +33 -19
- data/lib/evolvable/population.rb +171 -31
- data/lib/evolvable/rigid_count_gene.rb +17 -0
- data/lib/evolvable/search_space.rb +181 -0
- data/lib/evolvable/selection.rb +28 -1
- data/lib/evolvable/serializer.rb +21 -0
- data/lib/evolvable/uniform_crossover.rb +28 -8
- data/lib/evolvable/version.rb +1 -1
- data/lib/evolvable.rb +191 -31
- metadata +49 -10
- data/examples/evolvable_string/char_gene.rb +0 -9
- data/examples/evolvable_string.rb +0 -32
- data/lib/evolvable/gene_crossover.rb +0 -28
- data/lib/evolvable/gene_space.rb +0 -40
- data/lib/evolvable/goal/equalize.rb +0 -19
- data/lib/evolvable/goal/maximize.rb +0 -19
- data/lib/evolvable/goal/minimize.rb +0 -19
data/README.md
CHANGED
@@ -1,422 +1,359 @@
|
|
1
|
-
# Evolvable
|
1
|
+
# Evolvable 🦎
|
2
|
+
|
2
3
|
[![Gem Version](https://badge.fury.io/rb/evolvable.svg)](https://badge.fury.io/rb/evolvable) [![Maintainability](https://api.codeclimate.com/v1/badges/7faf84a6d467718b33c0/maintainability)](https://codeclimate.com/github/mattruzicka/evolvable/maintainability)
|
3
4
|
|
4
|
-
|
5
|
+
An evolutionary framework for writing programs that use operations such as selection, crossover, and mutation. Explore ideas generatively in any domain, discover novel solutions to complex problems, and build intuitions about intelligence, complexity, and the natural world.
|
6
|
+
|
7
|
+
Subscribe to the [Evolvable Newsletter](https://www.evolvable.site/newsletter) to slowly learn more, or keep reading this contextualization of the [full documentation](https://rubydoc.info/github/mattruzicka/evolvable).
|
5
8
|
|
6
|
-
[Evolutionary algorithms](https://en.wikipedia.org/wiki/Evolutionary_algorithm) build upon ideas such as natural selection, crossover, and mutation to construct relatively simple solutions to complex problems. This gem has been used to implement evolutionary behaviors for [visual, textual, and auditory experiences](https://projectpag.es/evolvable) as well as a variety of AI agents.
|
7
9
|
|
8
|
-
|
10
|
+
## Table of Contents
|
11
|
+
* [Installation](#installation)
|
12
|
+
* [Getting Started](#getting-started)
|
13
|
+
* [Concepts](#concepts)
|
14
|
+
* [Genes](#genes)
|
15
|
+
* [Populations](#populations)
|
16
|
+
* [Evaluation](#evaluation)
|
17
|
+
* [Evolution](#evolution)
|
18
|
+
* [Selection](#selection)
|
19
|
+
* [Combination](#combination)
|
20
|
+
* [Mutation](#mutation)
|
21
|
+
* [Search Space](#search-space)
|
9
22
|
|
10
|
-
### The Evolvable Abstraction
|
11
|
-
Population objects are composed of instances that include the `Evolvable` module. Instances are composed of gene objects that include the `Evolvable::Gene` module. Evaluation and evolution objects are used by population objects to evolve your instances. An evaluation object has one goal object and the evolution object is composed of selection, crossover, and mutation objects by default. All classes exposed by Evolvable are prefixed with `Evolvable::` and can be configured, inherited, removed, and extended.
|
12
23
|
|
13
24
|
## Installation
|
14
25
|
|
15
|
-
Add
|
26
|
+
Add [gem "evolvable"](https://rubygems.org/gems/evolvable) to your Gemfile and run `bundle install` or install it yourself with: `gem install evolvable`
|
16
27
|
|
17
28
|
## Getting Started
|
18
29
|
|
19
|
-
|
30
|
+
The `Evolvable` module makes it possible to implement evolutionary behaviors for
|
31
|
+
any class by defining a `.search_space` class method and `#value` instance method.
|
32
|
+
Then to evolve instances, initialize a population with `.new_population` and invoke
|
33
|
+
the `#evolve` method on the resulting population object.
|
20
34
|
|
21
|
-
|
22
|
-
2. Implement `.gene_space`, define any gene classes referenced by it, and include the `Evolvable::Gene` module for each. (See [Genes](#Genes)).
|
23
|
-
3. Implement `#value`. (See [Evaluation](#evaluation-1)).
|
24
|
-
4. Initialize a population and start evolving. (See [Populations](#Populations)).
|
35
|
+
### Implementation Steps
|
25
36
|
|
26
|
-
|
37
|
+
1. [Include the `Evolvable` module in the class you want to evolve.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable)
|
38
|
+
2. [Define `.search_space` and any gene classes that you reference.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
|
39
|
+
3. [Define `#value`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
|
40
|
+
4. [Initialize a population with `.new_population` and use `#evolve`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
|
27
41
|
|
28
|
-
If you’d like to quickly play around with an evolvable string Population object, you can do so by cloning this repo and running the command `bin/console` in this project's directory.
|
29
42
|
|
30
|
-
|
31
|
-
- [Configuration](#Configuration)
|
32
|
-
- [Genes](#Genes)
|
33
|
-
- [Populations](#Populations)
|
34
|
-
- [Evaluation](#evaluation-1)
|
35
|
-
- [Evolution](#evolution-1)
|
36
|
-
- [Selection](#selection-1)
|
37
|
-
- [Crossover](#Crossover)
|
38
|
-
- [Mutation](#mutation-1)
|
43
|
+
To demonstrate these steps, we'll look at the [Hello World](#) example program.
|
39
44
|
|
40
|
-
|
45
|
+
### Hello World
|
41
46
|
|
42
|
-
|
47
|
+
Let's build the evolvable hello world program using the above steps. It'll evolve a population of arbitrary strings to be more like a given target string. After installing this gem, run `evolvable hello` at the command line to see it in action.
|
43
48
|
|
44
|
-
|
45
|
-
class Melody
|
46
|
-
include Evolvable
|
49
|
+
Below is example output from evolving a population of randomly initialized string objects to match "Hello World!", then "Hello Evolvable World".
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
51
|
+
```
|
52
|
+
❯ Enter a string to evolve: Hello World!
|
53
|
+
|
54
|
+
pp`W^jXG'_N`% Generation 0
|
55
|
+
H-OQXZ\a~{H* Generation 1 ...
|
56
|
+
HRv9X WorlNi Generation 50 ...
|
57
|
+
HRl6W World# Generation 100 ...
|
58
|
+
Hello World! Generation 165
|
59
|
+
|
60
|
+
❯ Enter a string to evolve: Hello Evolvable World
|
61
|
+
|
62
|
+
Helgo World!b+=1}3 Generation 165
|
63
|
+
Helgo Worlv!}:c(SoV Generation 166
|
64
|
+
Helgo WorlvsC`X(Joqs Generation 167
|
65
|
+
Helgo WorlvsC`X(So1RE Generation 168 ...
|
66
|
+
Hello Evolv#"l{ Wor*5 Generation 300 ...
|
67
|
+
Hello Evolvable World Generation 388
|
57
68
|
```
|
58
69
|
|
59
|
-
|
60
|
-
|
61
|
-
### EvolvableClass.gene_space
|
62
|
-
|
63
|
-
You're expected to override this and return a gene space configuration hash or GeneSpace object. It defines the mapping for a hyperdimensional "gene space" so to speak. The above sample definition for the melody class configures each instance to have 16 note genes and 1 instrument gene.
|
64
|
-
|
65
|
-
See the section on [Genes](#genes) for more details.
|
66
|
-
|
67
|
-
### EvolvableClass.new_population(keyword_args = {})
|
68
|
-
|
69
|
-
Initializes a new population. Example: `population = Melody.new_population(size: 100)`
|
70
|
-
|
71
|
-
Accepts the same arguments as [Population.new](#evolvablepopulationnew)
|
72
|
-
|
73
|
-
### EvolvableClass.new_instance(population: nil, genes: [], population_index: nil)
|
74
|
-
|
75
|
-
Initializes a new instance. Accepts a population object, an array of gene objects, and the instance's population index. This method is useful for re-initializing instances and populations that have been saved.
|
76
|
-
|
77
|
-
_It is not recommended that you override this method_ as it is used by Evolvable internals. If you need to customize how your instances are initialized you can override either of the following two "initialize_instance" methods.
|
78
|
-
|
79
|
-
### EvolvableClass.initialize_instance
|
80
|
-
|
81
|
-
The default implementation simply delegates to `.new` and is useful for instances with custom initialize methods.
|
82
|
-
|
83
|
-
### EvolvableClass#initialize_instance
|
84
|
-
|
85
|
-
Runs after Evolvable finishes building your instance. It's useful for stuff like implementing custom gene initialization logic. For example, the Evolvable Strings web demo (coming soon) uses it to read from a "length gene" and add or remove "char genes" accordingly.
|
86
|
-
|
87
|
-
### EvolvableClass#population, #population=
|
88
|
-
|
89
|
-
The population object being used to evolve this instance.
|
90
|
-
|
91
|
-
### EvolvableClass#genes, #genes=
|
92
|
-
|
93
|
-
An array of all an instance's genes. You can find specific types of genes with the following two methods.
|
94
|
-
|
95
|
-
### EvolvableClass#find_genes(key)
|
96
|
-
|
97
|
-
Returns an array of genes that have the given key. Gene keys are defined in the [EvolvableClass.gene_space](#evolvableclassgene_space) method. In the Melody example above, the key for the note genes would be `:notes`. The following would return an array of them: `note_genes = melody.find_genes(:notes)`
|
98
|
-
|
99
|
-
### EvolvableClass#find_gene(key)
|
100
|
-
|
101
|
-
Returns the first gene with the given key. In the Melody example above, the instrument gene has the key `:instrument` so we might write something like: `instrument_gene = melody.find_gene(instrument)`
|
102
|
-
|
103
|
-
### EvolvableClass#population_index, #population_index=
|
104
|
-
|
105
|
-
Returns an instance's population index - an integer representing the order in which it was initialized in a population. It's the most basic way to distinguish instances in a population.
|
106
|
-
|
107
|
-
### EvolvableClass#value
|
108
|
-
|
109
|
-
You must implement this method. It is used when evaluating instances before undergoing evolution. The above melody example imagines that the melodies have ratings and uses them as the basis for evaluation and selection.
|
110
|
-
|
111
|
-
Technically, this method can return any object that implements Ruby's [Comparable](https://ruby-doc.org/core-2.7.1/Comparable.html). See the section on [Evaluation](#evaluation-1) for details.
|
112
|
-
|
113
|
-
### Evolvable Hooks
|
114
|
-
|
115
|
-
The following class method hooks can be overridden. The hooks run for each evolution in the following order:
|
70
|
+
### Step 1
|
116
71
|
|
117
|
-
**.
|
72
|
+
Let's begin by defining a `HelloWorld` class and have it **include the `Evolvable` module**.
|
118
73
|
|
119
|
-
|
74
|
+
```ruby
|
75
|
+
class HelloWorld
|
76
|
+
include Evolvable
|
77
|
+
end
|
78
|
+
```
|
120
79
|
|
121
|
-
|
80
|
+
### Step 2
|
122
81
|
|
123
|
-
|
82
|
+
Now we can **define the `.search_space`** class method with the types of [genes](#genes) that we want our our evolvable "hello world" instances to be able to have. We'll use `CharGene` instances to represent single characters within strings. So an instance with the string value of "Hello" would be composed of five `CharGene` instances.
|
124
83
|
|
125
84
|
```ruby
|
126
|
-
class
|
127
|
-
|
128
|
-
best_melody = population.best_instance
|
129
|
-
best_melody.play
|
130
|
-
end
|
85
|
+
class HelloWorld
|
86
|
+
include Evolvable
|
131
87
|
|
132
|
-
def
|
133
|
-
|
134
|
-
note_values = note_genes.map(&:value)
|
135
|
-
find_gene(:instrument).play(note_values)
|
88
|
+
def self.search_space
|
89
|
+
["CharGene", 1..40]
|
136
90
|
end
|
137
91
|
end
|
138
92
|
```
|
139
93
|
|
140
|
-
|
141
|
-
|
142
|
-
Instances rely on gene objects to compose behaviors. In other words, a gene can be thought of as an object that in some way affects the behavior of an instance. They are used to encapsulate a "sample space" and return a sample outcome when accessed.
|
94
|
+
The [Search Space](#search-space) can be defined in a variety of ways. The above is shorthand that's useful for when there's only one type of gene. This method can also return an array of arrays or hash.
|
143
95
|
|
144
|
-
|
96
|
+
The `1..40` specifies the range of possible genes for a particular HelloWorld instance. Evolvable translates this range or integer value into a `Evolvable::CountGene` object.
|
145
97
|
|
146
|
-
|
98
|
+
By specifying a range, an `Evolvable::CountGene` instance can change the number of genes that are present in an evovlable instance. Count genes undergo evolutionary operations like any other gene. Their effects can be seen in the letter changes from Generation 165 to 168 in the above example output.
|
147
99
|
|
148
|
-
To
|
100
|
+
To finish step 2, we'll **define the gene class** that we referenced in the above `.search_space` method. Gene classes should include the `Evolvable::Gene` module.
|
149
101
|
|
150
102
|
```ruby
|
151
|
-
class
|
103
|
+
class CharGene
|
152
104
|
include Evolvable::Gene
|
153
105
|
|
154
|
-
|
106
|
+
def self.chars
|
107
|
+
@chars ||= 32.upto(126).map(&:chr)
|
108
|
+
end
|
155
109
|
|
156
|
-
def
|
157
|
-
@
|
110
|
+
def to_s
|
111
|
+
@to_s ||= self.class.chars.sample
|
158
112
|
end
|
159
113
|
end
|
160
114
|
```
|
161
|
-
Here, the "sample space" for the NoteGene class has twelve notes, but each object will have only one note which is randomly chosen when the "value" method is invoked for the first time. _It is important that the data for a particular gene never change._ Ruby's or-equals operator `||=` is super useful for memoizing gene attributes. It is used above to randomly pick a note only once and return the same note for the lifetime of the object.
|
162
115
|
|
163
|
-
|
116
|
+
It's important that, once accessed, the data for a particular gene never change. When the `#to_s` method first runs, Ruby's `||=` operator memoizes the result of randomly picking a char, enabling this method to sample a char only once per gene.
|
164
117
|
|
165
|
-
|
166
|
-
class InstrumentGene
|
167
|
-
include Evolvable::Gene
|
118
|
+
After defining the search space, we can now initialize `HelloWorld` instances with random genes, but to actually evolve them, we need to **define the `#value` instance method**. It provides the basis for comparing different evolvable instances.
|
168
119
|
|
169
|
-
|
170
|
-
@instrument_class ||= [Guitar, Synth, Trumpet].sample
|
171
|
-
end
|
120
|
+
### Step 3
|
172
121
|
|
173
|
-
|
174
|
-
@volume ||= rand(1..100)
|
175
|
-
end
|
122
|
+
In the next step, we'll set the goal value to 0, so that evolution favors evolvable HelloWorld instances with `#value` methods that return numbers closer to 0. That means we want instances that more closely match their targets to return scores nearer to 0. As an example, if our target is "hello world", an instance that returns "jello world" would have a value of 1 and "hello world" would have a value of 0.
|
176
123
|
|
177
|
-
|
178
|
-
instrument_class.play(notes: notes, volume: volume)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
```
|
124
|
+
For a working implementation, see the `#value` method in [examples/hello_world.rb](https://github.com/mattruzicka/evolvable/blob/main/examples/hello_world.rb)
|
182
125
|
|
183
|
-
|
126
|
+
### Step 4
|
184
127
|
|
185
|
-
Now
|
128
|
+
Now it's time to **initialize a population with `.new_population`**. By default, evolvable populations seek to maximize numeric values. In this program, we always know the best possible value, so setting the goal to a concrete number makes sense. This is done by passing the evaluation params with equalize set to 0.
|
186
129
|
|
187
|
-
|
188
|
-
class Melody
|
189
|
-
include Evolvable
|
130
|
+
We'll also specify the number of instances in a population using the population's `size` parameter and change the mutation porbability from 0.03 (3%) to 0.6 (60%).
|
190
131
|
|
191
|
-
|
132
|
+
Experimentation has suggested that a large mutation probability tends to decrease the time it takes to evolve matches with short strings and has the opposite effect for long strings. This is demonstrated in the example output above by how many generations it took to go from "Hello World!" to "Hello Evolvable World". As an optimization, we could dynamically change the mutation probability using a population hook detailed below, but doing so will be left as an exercise for the reader. [Pull requests are welcome.](#contributing)
|
192
133
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
end
|
198
|
-
end
|
134
|
+
```ruby
|
135
|
+
population = HelloWorld.new_population(size: 100,
|
136
|
+
evaluation: { equalize: 0 },
|
137
|
+
mutation: { probability: 0.6 }
|
199
138
|
```
|
200
139
|
|
201
|
-
In this way, instances can express behaviors via genes and even orchestrate interactions between them. Genes can also interact with each other during an instance's initialization process via the [EvolvableClass#initialize_instance](#evolvableclassinitialize_instance-1) method
|
202
|
-
|
203
|
-
### The Evolvable::GeneSpace object
|
204
|
-
|
205
|
-
The `Evolvable::GeneSpace` object is responsible for initializing the full set of genes for a particular instance according to the configuration returned by the [EvolvableClass.gene_space](#evolvableclassgene_space) method. It is used by the `Evolvable::Population` to initialize new instances.
|
206
140
|
|
207
|
-
|
141
|
+
At this point, everything should work when we run `population.evolve`, but it'll look like nothing is happening. The next section will allow us to gain instight by hooking into the evolutionary process.
|
208
142
|
|
143
|
+
### Evolvable Population Hooks
|
209
144
|
|
210
|
-
|
211
|
-
|
212
|
-
The `Evolvable::Population` object is responsible for generating and evolving instances. It orchestrates all the other Evolvable objects to do so.
|
145
|
+
The following class methods can be implemented on your Evolvable class, e.g. HelloWorld, to hook into the Population#evolve lifecycle. This is useful for logging evolutionary progress, optimizing parameters, and creating interactions with and between evolvable instances.
|
213
146
|
|
214
|
-
|
147
|
+
1. `.before_evaluation(population)` - Runs before evaluation.
|
215
148
|
|
216
|
-
|
149
|
+
2. `.before_evolution(population)`- Runs after evaluation and before evolution.
|
217
150
|
|
218
|
-
|
151
|
+
3. `.after_evolution(population)` - Runs after evolution.
|
219
152
|
|
220
|
-
#### evolvable_class
|
221
|
-
Required. Implicitly specified when using EvolvableClass.new_population.
|
222
|
-
#### id, name
|
223
|
-
Both default to `nil`. Not used by Evolvable, but convenient when working with multiple populations.
|
224
|
-
#### size
|
225
|
-
Defaults to `40`. Specifies the number of instances in the population.
|
226
|
-
#### evolutions_count
|
227
|
-
Defaults to `0`. Useful when re-initializing a saved population with instances.
|
228
|
-
#### gene_space
|
229
|
-
Defaults to `evolvable_class.new_gene_space` which uses the [EvolvableClass.gene_space](#evolvableclassgene_space) method
|
230
|
-
#### evolution
|
231
|
-
Defaults to `Evolvable::Evolution.new`. See [evolution](#evolution-1)
|
232
|
-
#### evaluation
|
233
|
-
Defaults to `Evolvable::Evaluation.new`, with a goal of maximizing towards Float::INFINITY. See [evaluation](#evaluation-1)
|
234
|
-
#### instances
|
235
|
-
Defaults to initializing a `size` number of `evolvable_class` instances using the `gene_space` object. Any given instances are assigned, but if given less than `size`, more will be initialized.
|
236
153
|
|
237
|
-
|
154
|
+
Let's define `.before_evolution` to print the best value for each generation. We'll also define `HelloWorld#to_s`, which implicitly delegates to `CharGene#to_s` during the string interpolation that happens.
|
238
155
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
The number of evolutions to run. Expects a positive integer and Defaults to Float::INFINITY and will therefore run indefinitely unless a `goal_value` is specified.
|
243
|
-
#### goal_value
|
244
|
-
Assigns the goal object's value. Will continue running until any instance's value reaches it. See [evaluation](#evaluation-1)
|
156
|
+
```ruby
|
157
|
+
class HelloWorld
|
158
|
+
include Evolvable
|
245
159
|
|
246
|
-
|
247
|
-
|
160
|
+
def self.before_evolution(population)
|
161
|
+
best_evolvable = population.best_evolvable
|
162
|
+
evolutions_count = population.evolutions_count
|
163
|
+
puts "#{best_evolvable} - Generation #{evolutions_count}"
|
164
|
+
end
|
248
165
|
|
249
|
-
|
250
|
-
Returns true if any instance's value matches the goal value, otherwise false.
|
166
|
+
# ...
|
251
167
|
|
252
|
-
|
253
|
-
|
168
|
+
def to_s
|
169
|
+
@to_s ||= genes.join
|
170
|
+
end
|
254
171
|
|
255
|
-
|
172
|
+
# ...
|
173
|
+
end
|
174
|
+
```
|
256
175
|
|
257
|
-
|
258
|
-
An array of initialized gene objects. Defaults to `[]`
|
259
|
-
#### population_index
|
260
|
-
Defaults to `nil` and expects an integer. See (EvolvableClass#population_index)[#evolvableclasspopulation_index-population_index]
|
176
|
+
Finally we can **evolve the population with the `Evolvable::Population#evolve` instance method**.
|
261
177
|
|
178
|
+
```ruby
|
179
|
+
population.evolve
|
180
|
+
```
|
262
181
|
|
263
|
-
|
264
|
-
The [selection](#selection-1) object.
|
182
|
+
**You now know the fundamental steps to building evolvable programs of endless complexity in any domain!** 🐸 The exact implementation for the command line demo can be found in [exe/hello](https://github.com/mattruzicka/evolvable/blob/main/exe/hello) and [examples/hello_world.rb](https://github.com/mattruzicka/evolvable/blob/main/examples/hello_world.rb).
|
265
183
|
|
266
|
-
|
267
|
-
The [crossover](#crossover) object.
|
184
|
+
## Concepts
|
268
185
|
|
269
|
-
|
270
|
-
The [mutation](#mutation-1) object.
|
186
|
+
[Populations](#populations) are composed of evolvables which are composed of genes. Evolvables orchestrate behaviors by delegating to gene objects. Collections of genes are organized into genomes and constitute the [search space](#search-space). [Evaluation](#evaluation) and [evolution](#evolution) objects are used to evolve populations. By default, evolution is composed of [selection](#selection), [combination](#combination), and [mutation](#mutation).
|
271
187
|
|
272
|
-
|
273
|
-
The [evaluation](#evaluation-1)'s goal object.
|
188
|
+
The following concept map depicts how genes flow through populations.
|
274
189
|
|
275
|
-
|
190
|
+
![Concept Map](https://github.com/mattruzicka/evolvable/raw/main/examples/images/diagram.png)
|
276
191
|
|
277
|
-
|
192
|
+
Evolvable is designed with extensibility in mind. Evolvable objects such as [evaluation](#evaluation), [evolution](#evolution), [selection](#selection), [combination](#combination), and [mutation](#mutation) can be extended and swapped, potentially in ways that alter the above graph.
|
278
193
|
|
279
|
-
|
194
|
+
## Genes
|
195
|
+
For evolution to be effective, an evolvable's genes must be able to influence
|
196
|
+
its behavior. Evolvables are composed of genes that can be used to run simple
|
197
|
+
functions or orchestrate complex interactions. The level of abstraction is up
|
198
|
+
to you.
|
280
199
|
|
281
|
-
|
200
|
+
Defining gene classes requires encapsulating some "sample space" and returning
|
201
|
+
a sample outcome when a gene attribute is accessed. For evolution to proceed
|
202
|
+
in a non-random way, the same sample outcome should be returned every time
|
203
|
+
a particular gene is accessed with a particular set of parameters.
|
204
|
+
Memoization is a useful technique for doing just this. The
|
205
|
+
[memo_wise](https://github.com/panorama-ed/memo_wise) gem may be useful for
|
206
|
+
more complex memoizations.
|
282
207
|
|
283
|
-
Prioritizes instances with greater values. This is the default.
|
284
208
|
|
285
|
-
|
209
|
+
```ruby
|
210
|
+
# This gene generates a random hexidecimal color code for use by evolvables.
|
286
211
|
|
287
|
-
|
212
|
+
require 'securerandom'
|
288
213
|
|
289
|
-
|
214
|
+
class ColorGene
|
215
|
+
include Evolvable::Gene
|
290
216
|
|
291
|
-
|
217
|
+
def hex_code
|
218
|
+
@hex_code ||= SecureRandom.hex(3)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
292
222
|
|
293
|
-
### The Evolvable::Goal::Equalize object
|
294
223
|
|
295
|
-
|
224
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Gene)
|
296
225
|
|
297
|
-
|
226
|
+
## Populations
|
227
|
+
Population objects are responsible for generating and evolving instances.
|
228
|
+
They orchestrate all the other Evolvable objects to do so.
|
298
229
|
|
299
|
-
|
230
|
+
Populations can be initialized and re-initialized with a number of useful
|
231
|
+
parameters.
|
300
232
|
|
301
|
-
You can implement custom goal object like so:
|
302
233
|
|
303
234
|
```ruby
|
304
|
-
|
305
|
-
include Evolvable::Goal
|
306
|
-
|
307
|
-
def evaluate(instance)
|
308
|
-
# Required by Evolvable::Evaluation in order to sort instances in preparation for selection.
|
309
|
-
end
|
310
|
-
|
311
|
-
def met?(instance)
|
312
|
-
# Used by Evolvable::Population#evolve to stop evolving when the goal value has been reached.
|
313
|
-
end
|
314
|
-
end
|
235
|
+
# TODO: initialize a population with all supported parameters
|
315
236
|
```
|
316
237
|
|
317
|
-
The goal for a population can be specified via assignment - `population.goal = Evolvable::Goal::Equalize.new` - or by passing an evaluation object when [initializing a population](#evolvablepopulationnew).
|
318
238
|
|
319
|
-
|
239
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
|
320
240
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
241
|
+
## Evaluation
|
242
|
+
For selection to be effective in the context of evolution, there needs to be
|
243
|
+
a way to compare evolvables. In the genetic algorithm, this is often
|
244
|
+
referred to as the "fitness function".
|
245
|
+
|
246
|
+
The `Evolvable::Evaluation` object expects evolvable instances to define a `#value` method that
|
247
|
+
returns some numeric value. Values are used to evaluate instances relative to each
|
248
|
+
other and with regards to some goal. Out of the box, the goal can be set
|
249
|
+
to maximize, minimize, or equalize numeric values.
|
325
250
|
|
326
|
-
or more succinctly like this:
|
327
251
|
|
328
252
|
```ruby
|
329
|
-
|
330
|
-
Evolvable::Evaluation.new(maximize: 50) # Sets goal value to 50
|
331
|
-
Evolvable::Evaluation.new(:minimize) # Uses default goal value of -Float::INFINITY
|
332
|
-
Evolvable::Evaluation.new(minimize: 100) # Sets goal value to 100
|
333
|
-
Evolvable::Evaluation.new(:equalize) # Uses default goal value of 0
|
334
|
-
Evolvable::Evaluation.new(equalize: 1000) # Sets goal value to 1000
|
253
|
+
# TODO: Show how to add/change population's evaluation object
|
335
254
|
|
255
|
+
# The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
|
256
|
+
population.evolve(goal_value: 1000)
|
336
257
|
```
|
337
258
|
|
338
|
-
## Evolution
|
339
259
|
|
340
|
-
|
260
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
|
341
261
|
|
342
|
-
|
262
|
+
## Evolution
|
263
|
+
After a population's instances are evaluated, they undergo evolution.
|
264
|
+
The default evolution object is composed of selection,
|
265
|
+
crossover, and mutation objects and applies them as operations to
|
266
|
+
a population's evolvables in that order.
|
343
267
|
|
344
|
-
### Evolvable::Evolution.new
|
345
268
|
|
346
|
-
|
269
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evolution)
|
347
270
|
|
348
|
-
|
271
|
+
## Selection
|
272
|
+
The selection object assumes that a population's evolvables have already
|
273
|
+
been sorted by the evaluation object. It selects "parent" evolvables to
|
274
|
+
undergo combination and thereby produce the next generation of evolvables.
|
349
275
|
|
350
|
-
|
351
|
-
The
|
352
|
-
#### crossover
|
353
|
-
The default is `GeneCrossover.new`
|
354
|
-
#### mutation
|
355
|
-
The default is `Mutation.new`
|
276
|
+
Only two evolvables are selected as parents for each generation by default.
|
277
|
+
The selection size is configurable.
|
356
278
|
|
357
|
-
## Selection
|
358
279
|
|
359
|
-
|
280
|
+
```ruby
|
281
|
+
# TODO: Show how to add/change population's selection object
|
282
|
+
```
|
360
283
|
|
361
|
-
Custom selection objects must implement the `#call` method which accepts the population as the first object.
|
362
284
|
|
363
|
-
### Evolvable::Selection.new
|
364
285
|
|
365
|
-
|
286
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Selection)
|
366
287
|
|
367
|
-
|
288
|
+
## Combination
|
289
|
+
Combination generates new evolvable instances by combining the genes of selected instances.
|
290
|
+
You can think of it as a mixing of parent genes from one generation to
|
291
|
+
produce the next generation.
|
368
292
|
|
369
|
-
|
370
|
-
The
|
293
|
+
You may choose from a selection of combination objects or implement your own.
|
294
|
+
The default combination object is `Evolvable::GeneCombination`.
|
371
295
|
|
372
|
-
## Crossover
|
373
296
|
|
374
|
-
|
297
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Combination)
|
375
298
|
|
376
|
-
|
299
|
+
## Mutation
|
300
|
+
Mutation serves the role of increasing genetic variation. When an evolvable
|
301
|
+
undergoes a mutation, one or more of its genes are replaced by newly
|
302
|
+
initialized ones. In effect, a gene mutation invokes a new random outcome
|
303
|
+
from the genetic search space.
|
377
304
|
|
378
|
-
|
305
|
+
Mutation frequency is configurable using the `probability` and `rate`
|
306
|
+
parameters.
|
379
307
|
|
380
|
-
Enables gene types to define crossover behaviors. Each gene class can implement a unique behavior for crossover by overriding the following default implementation which mirrors the behavior of `Evolvable::UniformCrossover`
|
381
308
|
|
382
309
|
```ruby
|
383
|
-
|
384
|
-
[gene_a, gene_b].sample
|
385
|
-
end
|
310
|
+
# Show how to initialize/assign population with a specific mutation object
|
386
311
|
```
|
387
312
|
|
388
|
-
### The Evolvable::UniformCrossover object
|
389
|
-
|
390
|
-
Randomly chooses a gene from one of the parents for each gene position.
|
391
313
|
|
392
|
-
|
314
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Mutation)
|
393
315
|
|
394
|
-
|
316
|
+
## Search Space
|
317
|
+
The search space encapsulates the range of possible genes
|
318
|
+
for a particular evolvable. You can think of it as the boundaries of
|
319
|
+
genetic variation. It is configured via the
|
320
|
+
[.search_space](#evolvableclasssearch_space) method that you define
|
321
|
+
on your evolvable class. It's used by populations to initialize
|
322
|
+
new evolvables.
|
395
323
|
|
396
|
-
|
324
|
+
Evolvable provides flexibility in how you define your search space.
|
325
|
+
The below example implementations for `.search_space` produce the
|
326
|
+
exact same search space for the
|
327
|
+
[Hello World](https://github.com/mattruzicka/evolvable#hello-world)
|
328
|
+
demo program. The different styles arguably vary in suitability for
|
329
|
+
different contexts, perhaps depending on how programs are loaded and
|
330
|
+
the number of different gene types.
|
397
331
|
|
398
|
-
Mutation serves the role of increasing genetic variation, especially when a population's instances are small in number and mostly homogeneous. When an instance undergoes a mutation, it means that one of its existing genes is replaced with a newly initialized gene. Using the language from the [section on genes](genes), a gene mutation invokes a new outcome from the gene's sample space.
|
399
332
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
333
|
+
```ruby
|
334
|
+
# All 9 of these example definitions are equivalent
|
335
|
+
|
336
|
+
# Hash syntax
|
337
|
+
{ chars: { type: 'CharGene', max_count: 100 } }
|
338
|
+
{ chars: { type: 'CharGene', min_count: 1, max_count: 100 } }
|
339
|
+
{ chars: { type: 'CharGene', count: 1..100 } }
|
340
|
+
|
341
|
+
# Array of arrays syntax
|
342
|
+
[[:chars, 'CharGene', 1..100]]
|
343
|
+
[['chars', 'CharGene', 1..100]]
|
344
|
+
[['CharGene', 1..100]]
|
345
|
+
|
346
|
+
# A single array works when there's only one type of gene
|
347
|
+
['CharGene', 1..100]
|
348
|
+
[:chars, 'CharGene', 1..100]
|
349
|
+
['chars', 'CharGene', 1..100]
|
350
|
+
```
|
405
351
|
|
406
|
-
#### probability
|
407
|
-
The probability that a particular instance undergoes a mutation. By default, the probability is 0.03 which translates to 3%. If initialized with a `rate`, the probability will be 1 which means all genes _can_ undergo mutation, but actual gene mutations will be subject to the given mutation rate.
|
408
|
-
#### rate
|
409
|
-
the rate at which individual genes mutate. The default rate is 0 which, when combined with a non-zero `probability` (the default), means that one gene for each instance that undergoes mutation will change. If a rate is given, but no `probability` is given, then the `probability` will bet set to 1 which always defers to the mutation rate.
|
410
352
|
|
411
|
-
|
353
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
|
412
354
|
|
413
|
-
|
355
|
+
## Contributing
|
414
356
|
|
415
|
-
|
416
|
-
Evolvable::Mutation.new # Approximately 3% of instances will receive one mutant gene
|
417
|
-
Evolvable::Mutation.new(probability: 0.5) # Approximately 50% of instances will receive one mutant gene
|
418
|
-
Evolvable::Mutation.new(rate: 0.03) # Approximately 3% of all genes in the population will mutate.
|
419
|
-
Evolvable::Mutation.new(probability: 0.3, rate: 0.03) # Approximately 30% of instances will have approximately 3% of their genes mutated.
|
420
|
-
```
|
357
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mattruzicka/evolvable.
|
421
358
|
|
422
|
-
|
359
|
+
If you're interested in contributing, but don't know where to get started, message me on twitter at [@mattruzicka](https://twitter.com/mattruzicka).
|