genetic_algorithms 0.0.2 → 0.0.3
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.
- data/README.md +86 -12
- data/lib/genetic_algorithms/engine.rb +2 -4
- data/lib/genetic_algorithms/version.rb +1 -1
- data/lib/genetic_algorithms.rb +0 -6
- data/spec/engine_spec.rb +28 -8
- metadata +2 -4
- data/lib/genetic_algorithms/fitness_functions/all_off_sample.rb +0 -19
- data/lib/genetic_algorithms/fitness_functions/alternating_on_off_sample.rb +0 -20
data/README.md
CHANGED
@@ -6,31 +6,105 @@ This project contains my work in progress on Genetic Algorithms. The basic premi
|
|
6
6
|
|
7
7
|
A chromosome is a solution to a problem, and represented as a bit string. It is intentionally general in order to lend itself to multiple domains. The only thing linking it to a particular problem set is the fitness function. A fitness function evaluates a chromosome, that is, the effectiveness of a proposed solution.
|
8
8
|
|
9
|
-
##
|
9
|
+
## The Engine
|
10
10
|
|
11
|
-
In the GeneticAlgorithms module, the Engine class encapsulates the entire evolution process.
|
11
|
+
In the GeneticAlgorithms module, the Engine class encapsulates the entire evolution process. When you construct one, by default it gives you a population of size 10, a chromosome length of 10, and 5 generations of evolution. You'll notice that modifying these values can have a tremendous effect on the algorithm itself, and that is one of the interesting characteristics of Genetic Algorithms. In the future I will be opening up more parameters as well; currently many of them are tightly coupled to their respective classes.
|
12
|
+
|
13
|
+
## Knapsack, An Example Use Case
|
14
|
+
|
15
|
+
I stumbled on an interesting use case and decided to implement it for demonstrational purposes. Let's say we have a knapsack, and furthermore, let's say it can only hold a finite number of items. Assuming we know how much space each item takes, and how much total space the knapsack has, then how can we most efficiently utilize the knapsack? That is, we want to put as many items in it as possible with the least amount of free space left over. Enter genetic algorithms.
|
16
|
+
|
17
|
+
### Encoding
|
18
|
+
|
19
|
+
First of all, we need a way to express solutions to this problem as a chromosome. Since genetic_algorithms uses binary encoding, that's how we're going to approach it. Let's decide that there are only 4 items that we're considering for this exclusive knapsack. The items are a hammer, coin, pen, and wallet. The chromosome will then have 4 bits. An "ON" bit indicates that the item is in the knapsack, and an "OFF" means it is not. So, if we see something like "1010" that means a hammer(left one) and pen(right one) are in the knapsack, but the coin(left zero) and wallet(right zero) are not.
|
20
|
+
|
21
|
+
### The Fitness Function
|
22
|
+
|
23
|
+
From a high level perspective, the fitness function should return how efficient the knapsack is being utilized. The implementation is covered below.
|
24
|
+
|
25
|
+
Currently, genetic_algorithms is expecting better solutions to return greater numbers than worse solutions. This implies that the highest number returned from a fitness function is the best solution.
|
26
|
+
|
27
|
+
### Finally...some code!
|
28
|
+
|
29
|
+
Here is the Knapsack class:
|
12
30
|
|
13
31
|
```ruby
|
14
|
-
|
32
|
+
class Knapsack
|
33
|
+
CAPACITY, BEST_SCORE = 24, 1000
|
34
|
+
|
35
|
+
def initialize chromosome
|
36
|
+
@contents = decode chromosome
|
37
|
+
end
|
38
|
+
|
39
|
+
def utilization
|
40
|
+
space_used = @contents.inject(0) do |accum, item|
|
41
|
+
accum += item.space_needed
|
42
|
+
end
|
43
|
+
|
44
|
+
(space_used.to_f / CAPACITY * 1000).round
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def decode chromosome
|
50
|
+
all_items = Item.create_items({ hammer: 17, coin: 4, pen: 9, wallet: 11 })
|
51
|
+
index = -1
|
52
|
+
|
53
|
+
filtered_items = all_items.select do |item|
|
54
|
+
index += 1
|
55
|
+
chromosome[index] == "1"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
15
59
|
```
|
16
|
-
|
60
|
+
|
61
|
+
And the Item class:
|
17
62
|
|
18
63
|
```ruby
|
19
|
-
|
64
|
+
class Item
|
65
|
+
def self.create_items item_hash
|
66
|
+
item_hash.inject(Array.new) do |items, (name, space_needed)|
|
67
|
+
items << Item.new(name, space_needed)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize name, space_needed
|
72
|
+
@name = name
|
73
|
+
@space_needed = space_needed
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :name, :space_needed
|
77
|
+
end
|
20
78
|
```
|
21
79
|
|
22
|
-
|
80
|
+
### Enter genetic_algorithms
|
23
81
|
|
24
82
|
```ruby
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
83
|
+
require 'genetic_algorithms'
|
84
|
+
include GeneticAlgorithms
|
85
|
+
|
86
|
+
Engine.new(10,4).start(Knapsack::BEST_SCORE) do |chromosome|
|
87
|
+
score = Knapsack.new(chromosome).utilization
|
88
|
+
score = Knapsack::BEST_SCORE - score if score > Knapsack::BEST_SCORE
|
89
|
+
score
|
30
90
|
end
|
31
91
|
```
|
32
92
|
|
33
|
-
|
93
|
+
Take note of the line checking whether the score is greater than the best score. It is necessary because we don't want genetic_algorithms to think that the over-utilized knapsacks are better solutions. A knapsack with too much stuff in it would otherwise have a higher score.
|
94
|
+
|
95
|
+
The result of this run will be something like:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
{"1100"=>875}
|
99
|
+
```
|
100
|
+
|
101
|
+
We see here that the best solution found is a hammer and a coin, totalling 21 out of 24 (0.875) in terms of knapsack capacity.
|
102
|
+
|
103
|
+
### Um, that's not the best solution...
|
104
|
+
|
105
|
+
You are correct! That's why I chose to show it. The best solution is actually "0111", but given the parameters that we used for the engine, it wasn't able to discover it. This illustrates an important point. Genetic Algorithms may not find the best solution, in fact, some people refer to them as "good enough" algorithms.
|
106
|
+
|
107
|
+
You will get the best solution if you run it a few times as shown, but if you tweak the engine parameters a bit, the ideal solution will be found every time.
|
34
108
|
|
35
109
|
## Logging
|
36
110
|
|
@@ -10,9 +10,7 @@ module GeneticAlgorithms
|
|
10
10
|
@population = Population.new @chromosomes
|
11
11
|
end
|
12
12
|
|
13
|
-
def start(
|
14
|
-
extend GeneticAlgorithms::FitnessFunctions.const_get(fitness_function_type)
|
15
|
-
|
13
|
+
def start(best_possible_score, &fitness_function)
|
16
14
|
highest_score, best_gen = 0, nil
|
17
15
|
|
18
16
|
(0...@num_generations).inject(@population) do |newest_population, i|
|
@@ -20,7 +18,7 @@ module GeneticAlgorithms
|
|
20
18
|
|
21
19
|
if newest_population.highest_score > highest_score
|
22
20
|
highest_score, best_gen = newest_population.highest_score, newest_population
|
23
|
-
break if highest_score ==
|
21
|
+
break if highest_score == best_possible_score
|
24
22
|
end
|
25
23
|
|
26
24
|
next_gen
|
data/lib/genetic_algorithms.rb
CHANGED
@@ -14,11 +14,5 @@ module GeneticAlgorithms
|
|
14
14
|
require 'genetic_algorithms/exceptions'
|
15
15
|
require 'genetic_algorithms/roulette_wheel'
|
16
16
|
require 'genetic_algorithms/population'
|
17
|
-
|
18
|
-
ff_path = File.join(File.expand_path("..", __FILE__), "genetic_algorithms/fitness_functions")
|
19
|
-
Dir[File.join ff_path, "**/*.rb"].each do |f|
|
20
|
-
require f
|
21
|
-
end
|
22
|
-
|
23
17
|
require 'genetic_algorithms/engine'
|
24
18
|
end
|
data/spec/engine_spec.rb
CHANGED
@@ -2,35 +2,55 @@ require 'spec_helper'
|
|
2
2
|
include GeneticAlgorithms
|
3
3
|
|
4
4
|
describe Engine do
|
5
|
-
|
6
|
-
|
5
|
+
|
7
6
|
# TODO: DRY out this code
|
8
7
|
describe "#start" do
|
9
8
|
context "AllOffSample fitness function" do
|
9
|
+
|
10
|
+
subject do
|
11
|
+
Engine.new(10,10,10).start(10) do |chromosome|
|
12
|
+
chromosome.each_char.inject(0) do |accum, char|
|
13
|
+
accum += 1 if char == Chromosome::OFF
|
14
|
+
accum
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
10
19
|
it "returns a hash" do
|
11
|
-
subject.
|
20
|
+
subject.is_a?(Hash).should == true
|
12
21
|
end
|
13
22
|
|
14
23
|
it "returns the best solution as a Chromosome" do
|
15
|
-
subject.
|
24
|
+
subject.keys.first.is_a?(Chromosome).should == true
|
16
25
|
end
|
17
26
|
|
18
27
|
it "returns the best score as a Fixnum" do
|
19
|
-
subject.
|
28
|
+
subject.values.first.is_a?(Fixnum).should == true
|
20
29
|
end
|
21
30
|
end
|
22
31
|
|
23
32
|
context "AlternatingOnOffSample fitness function" do
|
33
|
+
|
34
|
+
subject do
|
35
|
+
Engine.new(10,10,10).start(10) do |chromosome|
|
36
|
+
(0...chromosome.length).inject(0) do |accum, index|
|
37
|
+
accum += 1 if index % 2 == 0 and chromosome[index] == Chromosome::ON
|
38
|
+
accum += 1 if index % 2 == 1 and chromosome[index] == Chromosome::OFF
|
39
|
+
accum
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
24
44
|
it "returns a hash" do
|
25
|
-
subject.
|
45
|
+
subject.is_a?(Hash).should == true
|
26
46
|
end
|
27
47
|
|
28
48
|
it "returns the best solution as a Chromosome" do
|
29
|
-
subject.
|
49
|
+
subject.keys.first.is_a?(Chromosome).should == true
|
30
50
|
end
|
31
51
|
|
32
52
|
it "returns the best score as a Fixnum" do
|
33
|
-
subject.
|
53
|
+
subject.values.first.is_a?(Fixnum).should == true
|
34
54
|
end
|
35
55
|
end
|
36
56
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: genetic_algorithms
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: logging
|
@@ -76,8 +76,6 @@ files:
|
|
76
76
|
- lib/genetic_algorithms/chromosome.rb
|
77
77
|
- lib/genetic_algorithms/engine.rb
|
78
78
|
- lib/genetic_algorithms/exceptions.rb
|
79
|
-
- lib/genetic_algorithms/fitness_functions/all_off_sample.rb
|
80
|
-
- lib/genetic_algorithms/fitness_functions/alternating_on_off_sample.rb
|
81
79
|
- lib/genetic_algorithms/natural_hash.rb
|
82
80
|
- lib/genetic_algorithms/population.rb
|
83
81
|
- lib/genetic_algorithms/roulette_wheel.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module GeneticAlgorithms
|
2
|
-
module FitnessFunctions
|
3
|
-
module AllOffSample
|
4
|
-
|
5
|
-
def best_possible_score
|
6
|
-
@chromosome_length
|
7
|
-
end
|
8
|
-
|
9
|
-
def fitness_function
|
10
|
-
lambda do |chromosome|
|
11
|
-
chromosome.each_char.inject(0) do |accum, char|
|
12
|
-
accum += 1 if char == Chromosome::OFF
|
13
|
-
accum
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
module GeneticAlgorithms
|
2
|
-
module FitnessFunctions
|
3
|
-
module AlternatingOnOffSample
|
4
|
-
|
5
|
-
def best_possible_score
|
6
|
-
@chromosome_length
|
7
|
-
end
|
8
|
-
|
9
|
-
def fitness_function
|
10
|
-
lambda do |chromosome|
|
11
|
-
(0...chromosome.length).inject(0) do |accum, index|
|
12
|
-
accum += 1 if index % 2 == 0 and chromosome[index] == Chromosome::ON
|
13
|
-
accum += 1 if index % 2 == 1 and chromosome[index] == Chromosome::OFF
|
14
|
-
accum
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|