Skip to content

Commit b59cd5f

Browse files
committed
working on readme.. finally finishing this PR
1 parent 74685a5 commit b59cd5f

File tree

4 files changed

+282
-20
lines changed

4 files changed

+282
-20
lines changed

Genetic/README.markdown

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,167 @@
1-
# todo
2-
refactoring https://gist.github.com/blainerothrock/efda6e12fe10792c99c990f8ff3daeba for swift 4. Creating a tutorial and writing in more playground friendly format
1+
# Genetic Algorthim
2+
3+
## What is it?
4+
5+
A genetic algorithm (GA) is process inspired by natural selection to find high quality solutions. Most commonly used for optimization. GAs rely on the bio-inspired processes of natural selection, more specifically the process of selection (fitness), mutation and crossover. To understand more, let's walk through these process in terms of biology:
6+
7+
### Selection
8+
>**Selection**, in biology, the preferential survival and reproduction or preferential elimination of individuals with certain genotypes (genetic compositions), by means of natural or artificial controlling factors. [Britannica](britannica)
9+
10+
In other words, survival of the fittest. Organism that survive in their environment tend to reproduce more. With GAs we generate a fitness model that will rank offspring and give them a better chance for reproduction.
11+
12+
### Mutation
13+
>**Mutation**, an alteration in the genetic material (the genome) of a cell of a living organism or of a virus that is more or less permanent and that can be transmitted to the cell’s or the virus’s descendants. [Britannica](https://www.britannica.com/science/mutation-genetics)
14+
15+
The randomization that allows for organisms to change over time. In GAs we build a randomization process that will mutate offspring in a populate in order to randomly introduce fitness variance.
16+
17+
### Crossover
18+
>**Chromosomal crossover** (or crossing over) is the exchange of genetic material between homologous chromosomes that results in recombinant chromosomes during sexual reproduction [Wikipedia](https://en.wikipedia.org/wiki/Chromosomal_crossover)
19+
20+
Simply reproduction. A generation will a mixed representation of the previous generation, with offspring taking data (DNA) from both parents. GAs do this by randomly, but weightily, mating offspring to create new generations.
21+
22+
### Resources:
23+
* [Wikipedia]()
24+
25+
26+
## The Code
27+
28+
### Problem
29+
For this quick and dirty example, we are going to obtain a optimize string using a simple genetic algorithm. More specifically we are trying to take a randomly generated origin string of a fixed length and evolve it into the most optimized string of our choosing.
30+
31+
We will be creating a bio-inspired world where the absolute existence is string `Hello, World!`. Nothing in this universe is better and it's our goal to get as close to it as possible.
32+
33+
### Define the Universe
34+
35+
Before we dive into the core processes we need to set up our "universe". First let's define a lexicon, a set of everything that exists in our universe.
36+
37+
```swift
38+
let lex: [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
39+
```
40+
41+
To make things easier, we are actually going to work in ASCII values, so let's define a String extension to help with that.
42+
43+
```swift
44+
extension String {
45+
var asciiArray: [UInt8] {
46+
return [UInt8](self.utf8)
47+
}
48+
}
49+
```
50+
51+
Now, let's define a few global variables for the universe:
52+
53+
```swift
54+
// This is the end goal and what we will be using to rate fitness. In the real world this will not exist
55+
let OPTIMAL:[UInt8] = "Hello, World".asciiArray
56+
57+
// The length of the string in our population. Organisms need to be similar
58+
let DNA_SIZE = OPTIMAL.count
59+
60+
// size of each generation
61+
let POP_SIZE = 50
62+
63+
// max number of generations, script will stop when it reach 5000 if the optimal value is not found
64+
let GENERATIONS = 5000
65+
66+
// The chance in which a random nucleotide can mutate (1/n)
67+
let MUTATION_CHANCE = 100
68+
```
69+
70+
The last piece we need for set up is a function to give us a random ASCII value from our lexicon:
71+
72+
```swift
73+
func randomChar(from lexicon: [UInt8]) -> UInt8 {
74+
let len = UInt32(lexicon.count-1)
75+
let rand = Int(arc4random_uniform(len))
76+
return lexicon[rand]
77+
}
78+
```
79+
80+
### Population Zero
81+
82+
Before selecting, mutating and reproduction, we need population to start with. Now that we have the universe defined we can write that function:
83+
84+
```swift
85+
func randomPopulation(from lexicon: [UInt8], populationSize: Int, dnaSize: Int) -> [[UInt8]] {
86+
87+
let len = UInt32(lexicon.count)
88+
89+
var pop = [[UInt8]]()
90+
91+
for _ in 0..<populationSize {
92+
var dna = [UInt8]()
93+
for _ in 0..<dnaSize {
94+
let char = randomChar(from: lexicon)
95+
dna.append(char)
96+
}
97+
pop.append(dna)
98+
}
99+
return pop
100+
}
101+
```
102+
103+
### Selection
104+
105+
There are two parts to the selection process, the first is calculating the fitness, which will assign a rating to a individual. We do this by simply calculating how close the individual is to the optimal string using ASCII values:
106+
107+
```swift
108+
func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
109+
var fitness = 0
110+
for c in 0...dna.count-1 {
111+
fitness += abs(Int(dna[c]) - Int(optimal[c]))
112+
}
113+
return fitness
114+
}
115+
```
116+
117+
The above is a very simple fitness calculation, but it'll work for our example. In a real world problem, the optimal solution is unknown or impossible. [Here](https://iccl.inf.tu-dresden.de/w/images/b/b7/GA_for_TSP.pdf) is a paper about optimizing a solution for the famous [traveling salesman problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using GA. In this example the problem is unsolvable by modern computers, but you can rate a individual solution by distance traveled. The optimal fitness here is an impossible 0. The closer the solution is to 0, the better chance for survival.
118+
119+
The second part to selection is weighted choice, also called roulette wheel selection. This defines how individuals are selected for the reproduction process out of the current population. Just because you are the best choice for natural selection doesn't mean the environment will select you. The individual could fall off a cliff, get dysentery or not be able to reproduce.
120+
121+
Let's take a second and ask why on this one. Why would you not always want to select the most fit from a population? It's hard to see from this simple example, but let's think about dog breeding, because breeders remove this process and hand select dogs for the next generation. As a result you get improved desired characteristics, but the individuals will also continue to carry genetic disorders that come along with those traits. This is essentially leading the evolution down a linear path. A certain "branch" of evolution may beat out the current fittest solution at a later time.
122+
123+
ok, back to code. Here is our weighted choice function:
124+
125+
```swift
126+
func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], weight:Double) {
127+
128+
let total = items.reduce(0.0) { return $0 + $1.weight}
129+
130+
var n = Double(arc4random_uniform(UInt32(total * 1000000.0))) / 1000000.0
131+
132+
for itemTuple in items {
133+
if n < itemTuple.weight {
134+
return itemTuple
135+
}
136+
n = n - itemTuple.weight
137+
}
138+
return items[1]
139+
}
140+
```
141+
142+
The above function takes a list of individuals with their calculated fitness. Then selects one at random offset by their fitness value.
143+
144+
## Mutation
145+
146+
The all powerful mutation. The great randomization that turns bacteria into humans, just add time. So powerful yet so simple:
147+
148+
```swift
149+
func mutate(dna:[UInt8], mutationChance:Int) -> [UInt8] {
150+
var outputDna = dna
151+
152+
for i in 0..<dna.count {
153+
let rand = Int(arc4random_uniform(UInt32(mutationChance)))
154+
if rand == 1 {
155+
outputDna[i] = randomChar()
156+
}
157+
}
158+
159+
return outputDna
160+
}
161+
```
162+
163+
Takes a mutation chance and a individual and returns that individual with mutations, if any.
164+
165+
This allows for a population to explore all the possibilities of it's building blocks and randomly stumble on a better solution. If there is too much mutation, the evolution process will get nowhere. If there is too little the populations will become too similar and never be able to branch out of a defect to meet their changing environment.
166+
167+
## Crossover

Genetic/gen.playground/Contents.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//: Playground - noun: a place where people can play
2+
3+
import Foundation
4+
5+
extension String {
6+
var asciiArray: [UInt8] {
7+
return [UInt8](self.utf8)
8+
}
9+
}
10+
11+
12+
let lex: [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
13+
14+
// This is the end goal and what we will be using to rate fitness. In the real world this will not exist
15+
let OPTIMAL:[UInt8] = "Hello, World".asciiArray
16+
17+
// The length of the string in our population. Organisms need to be similar
18+
let DNA_SIZE = OPTIMAL.count
19+
20+
// size of each generation
21+
let POP_SIZE = 50
22+
23+
// max number of generations, script will stop when it reach 5000 if the optimal value is not found
24+
let GENERATIONS = 5000
25+
26+
// The chance in which a random nucleotide can mutate (1/n)
27+
let MUTATION_CHANCE = 100
28+
29+
func randomChar(from lexicon: [UInt8]) -> UInt8 {
30+
let len = UInt32(lexicon.count-1)
31+
let rand = Int(arc4random_uniform(len))
32+
return lexicon[rand]
33+
}
34+
35+
func randomPopulation(from lexicon: [UInt8], populationSize: Int, dnaSize: Int) -> [[UInt8]] {
36+
37+
let len = UInt32(lexicon.count)
38+
39+
var pop = [[UInt8]]()
40+
41+
for _ in 0..<populationSize {
42+
var dna = [UInt8]()
43+
for _ in 0..<dnaSize {
44+
let char = randomChar(from: lexicon)
45+
dna.append(char)
46+
}
47+
pop.append(dna)
48+
}
49+
return pop
50+
}
51+
52+
randomPopulation(from: lex, populationSize: POP_SIZE, dnaSize: DNA_SIZE)
53+
54+
func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
55+
56+
var fitness = 0
57+
for c in 0...dna.count-1 {
58+
fitness += abs(Int(dna[c]) - Int(optimal[c]))
59+
}
60+
return fitness
61+
}
62+
63+
calculateFitness(dna: "Hillo, World".asciiArray, optimal: "Hello, World".asciiArray)
64+
65+
func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], weight:Double) {
66+
67+
let total = items.reduce(0.0) { return $0 + $1.weight}
68+
69+
var n = Double(arc4random_uniform(UInt32(total * 1000000.0))) / 1000000.0
70+
71+
for itemTuple in items {
72+
if n < itemTuple.weight {
73+
return itemTuple
74+
}
75+
n = n - itemTuple.weight
76+
}
77+
return items[1]
78+
}
79+
80+
func mutate(dna:[UInt8], mutationChance:Int) -> [UInt8] {
81+
var outputDna = dna
82+
83+
for i in 0..<dna.count {
84+
let rand = Int(arc4random_uniform(UInt32(mutationChance)))
85+
if rand == 1 {
86+
outputDna[i] = randomChar()
87+
}
88+
}
89+
90+
return outputDna
91+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<playground version='5.0' target-platform='ios'>
3+
<timeline fileName='timeline.xctimeline'/>
4+
</playground>

Genetic/gen.swift

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ extension String {
1414
}
1515
}
1616

17+
let lex = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".map { }
18+
1719
/*
1820
helper function to return a random character string
1921
*/
2022
func randomChar() -> UInt8 {
21-
23+
2224
let letters : [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
2325
let len = UInt32(letters.count-1)
24-
26+
2527
let rand = Int(arc4random_uniform(len))
2628
return letters[rand]
2729
}
@@ -40,7 +42,7 @@ let MUTATION_CHANCE = 100
4042
optimal string comparsion = 0
4143
*/
4244
func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
43-
45+
4446
var fitness = 0
4547
for c in 0...dna.count-1 {
4648
fitness += abs(Int(dna[c]) - Int(optimal[c]))
@@ -53,7 +55,7 @@ func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
5355
*/
5456
func mutate(dna:[UInt8], mutationChance:Int, dnaSize:Int) -> [UInt8] {
5557
var outputDna = dna
56-
58+
5759
for i in 0..<dnaSize {
5860
let rand = Int(arc4random_uniform(UInt32(mutationChance)))
5961
if rand == 1 {
@@ -70,10 +72,10 @@ func mutate(dna:[UInt8], mutationChance:Int, dnaSize:Int) -> [UInt8] {
7072
*/
7173
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[UInt8]) {
7274
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
73-
75+
7476
let dna1Index1 = dna1.index(dna1.startIndex, offsetBy: pos)
7577
let dna2Index1 = dna2.index(dna2.startIndex, offsetBy: pos)
76-
78+
7779
return (
7880
[UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1)),
7981
[UInt8](dna2.prefix(upTo: dna2Index1) + dna1.suffix(from: dna1Index1))
@@ -85,12 +87,12 @@ func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[
8587
returns a random population, used to start the evolution
8688
*/
8789
func randomPopulation(populationSize: Int, dnaSize: Int) -> [[UInt8]] {
88-
90+
8991
let letters : [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
9092
let len = UInt32(letters.count)
9193

9294
var pop = [[UInt8]]()
93-
95+
9496
for _ in 0..<populationSize {
9597
var dna = [UInt8]()
9698
for _ in 0..<dnaSize {
@@ -112,7 +114,7 @@ func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], wei
112114
for itemTuple in items {
113115
weightTotal += itemTuple.weight;
114116
}
115-
117+
116118
var n = Double(arc4random_uniform(UInt32(weightTotal * 1000000.0))) / 1000000.0
117119

118120
for itemTuple in items {
@@ -133,36 +135,36 @@ func main() {
133135

134136
for generation in 0...GENERATIONS {
135137
print("Generation \(generation) with random sample: \(String(bytes: population[0], encoding:.ascii)!)")
136-
138+
137139
var weightedPopulation = [(item:[UInt8], weight:Double)]()
138-
140+
139141
// calulcated the fitness of each individual in the population
140142
// and add it to the weight population (weighted = 1.0/fitness)
141143
for individual in population {
142144
let fitnessValue = calculateFitness(dna: individual, optimal: OPTIMAL)
143-
145+
144146
let pair = ( individual, fitnessValue == 0 ? 1.0 : 1.0/Double( fitnessValue ) )
145-
147+
146148
weightedPopulation.append(pair)
147149
}
148-
150+
149151
population = []
150-
152+
151153
// create a new generation using the individuals in the origional population
152154
for _ in 0...POP_SIZE/2 {
153155
let ind1 = weightedChoice(items: weightedPopulation)
154156
let ind2 = weightedChoice(items: weightedPopulation)
155-
157+
156158
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
157159

158160
// append to the population and mutate
159161
population.append(mutate(dna: offspring.dna1, mutationChance: MUTATION_CHANCE, dnaSize: DNA_SIZE))
160162
population.append(mutate(dna: offspring.dna2, mutationChance: MUTATION_CHANCE, dnaSize: DNA_SIZE))
161163
}
162-
164+
163165
fittest = population[0]
164166
var minFitness = calculateFitness(dna: fittest, optimal: OPTIMAL)
165-
167+
166168
// parse the population for the fittest string
167169
for indv in population {
168170
let indvFitness = calculateFitness(dna: indv, optimal: OPTIMAL)

0 commit comments

Comments
 (0)