Skip to content

Commit 81befa6

Browse files
committed
tutorial; weighted choice
1 parent c638164 commit 81befa6

File tree

3 files changed

+167
-88
lines changed

3 files changed

+167
-88
lines changed

Genetic/README.markdown

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,51 @@ The above is used to generate a completely new generation based on the current g
188188

189189
## Putting it all together -- Running the Genetic Algorithm
190190

191-
We now have all the methods we need to kick off the process. What is missing a `main()` function that loops though each generation of the GA.
191+
We now have all the functions we need to kick off the algorthim. Let's start from the beginning, first we need a random population to serve as a starting point. We will also initialize a fittest variable to hold the fittest individual, we will initialize it with the first individual of our random population.
192+
193+
```swift
194+
var population:[[UInt8]] = randomPopulation(from: lex, populationSize: POP_SIZE, dnaSize: DNA_SIZE)
195+
var fittest = population[0]
196+
```
197+
198+
Now for the meat, the remainder of the code will take place in the generation loop, running once for every generation:
199+
200+
```swift
201+
for generation in 0...GENERATIONS {
202+
// run
203+
}
204+
```
205+
206+
Now, for each individual in the population, we need to calculate its fitness and weighted value. For weighted choice we store the fitness as a percent `1 / fitness`.
207+
208+
```swift
209+
var weightedPopulation = [(item:[UInt8], weight:Double)]()
210+
211+
for individual in population {
212+
let fitnessValue = calculateFitness(dna: individual, optimal: OPTIMAL)
213+
let pair = ( individual, fitnessValue == 0 ? 1.0 : 1.0/Double( fitnessValue ) )
214+
weightedPopulation.append(pair)
215+
}
216+
```
217+
218+
To understand weighted choice, let walk though a smaller example, let's say we have the following population, where 0 is the best fitness:
219+
220+
```txt
221+
1: 10
222+
2: 5
223+
3: 4
224+
4: 7
225+
5: 11
226+
```
227+
228+
Now here is the weight of each:
229+
230+
```txt
231+
1:
232+
2:
233+
3:
234+
4:
235+
5:
236+
237+
total =
238+
```

Genetic/gen.playground/Contents.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,55 @@ func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[
102102
[UInt8](dna2.prefix(upTo: dna2Index1) + dna1.suffix(from: dna1Index1))
103103
)
104104
}
105+
106+
func main() {
107+
108+
// generate the starting random population
109+
var population:[[UInt8]] = randomPopulation(from: lex, populationSize: POP_SIZE, dnaSize: DNA_SIZE)
110+
// print("population: \(population), dnaSize: \(DNA_SIZE) ")
111+
var fittest = [UInt8]()
112+
113+
for generation in 0...GENERATIONS {
114+
print("Generation \(generation) with random sample: \(String(bytes: population[0], encoding:.ascii)!)")
115+
116+
var weightedPopulation = [(item:[UInt8], weight:Double)]()
117+
118+
// calulcated the fitness of each individual in the population
119+
// and add it to the weight population (weighted = 1.0/fitness)
120+
for individual in population {
121+
let fitnessValue = calculateFitness(dna: individual, optimal: OPTIMAL)
122+
123+
let pair = ( individual, fitnessValue == 0 ? 1.0 : 1.0/Double( fitnessValue ) )
124+
125+
weightedPopulation.append(pair)
126+
}
127+
128+
population = []
129+
130+
// create a new generation using the individuals in the origional population
131+
for _ in 0...POP_SIZE/2 {
132+
let ind1 = weightedChoice(items: weightedPopulation)
133+
let ind2 = weightedChoice(items: weightedPopulation)
134+
135+
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
136+
137+
// append to the population and mutate
138+
population.append(mutate(lexicon: lex, dna: offspring.dna1, mutationChance: MUTATION_CHANCE))
139+
population.append(mutate(lexicon: lex, dna: offspring.dna2, mutationChance: MUTATION_CHANCE))
140+
}
141+
142+
fittest = population[0]
143+
var minFitness = calculateFitness(dna: fittest, optimal: OPTIMAL)
144+
145+
// parse the population for the fittest string
146+
for indv in population {lex
147+
let indvFitness = calculateFitness(dna: indv, optimal: OPTIMAL)
148+
if indvFitness < minFitness {
149+
fittest = indv
150+
minFitness = indvFitness
151+
}
152+
}
153+
if minFitness == 0 { break; }
154+
}
155+
print("fittest string: \(String(bytes: fittest, encoding: .ascii)!)")
156+
}

Genetic/gen.swift

Lines changed: 67 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,56 @@
1-
/*
2-
base .. to be refactored
3-
*/
1+
//: Playground - noun: a place where people can play
42

53
import Foundation
64

7-
// HELPERS
8-
/*
9-
String extension to convert a string to ascii value
10-
*/
115
extension String {
126
var asciiArray: [UInt8] {
13-
return unicodeScalars.filter{$0.isASCII}.map{UInt8($0.value)}
7+
return [UInt8](self.utf8)
148
}
159
}
1610

17-
let lex = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".map { }
1811

19-
/*
20-
helper function to return a random character string
21-
*/
22-
func randomChar() -> UInt8 {
12+
let lex: [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
2313

24-
let letters : [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
25-
let len = UInt32(letters.count-1)
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
2616

27-
let rand = Int(arc4random_uniform(len))
28-
return letters[rand]
29-
}
17+
// The length of the string in our population. Organisms need to be similar
18+
let DNA_SIZE = OPTIMAL.count
3019

31-
// END HELPERS
20+
// size of each generation
21+
let POP_SIZE = 200
3222

33-
let OPTIMAL:[UInt8] = "Hello, World".asciiArray
34-
let DNA_SIZE = OPTIMAL.count
35-
let POP_SIZE = 50
23+
// max number of generations, script will stop when it reach 5000 if the optimal value is not found
3624
let GENERATIONS = 5000
25+
26+
// The chance in which a random nucleotide can mutate (1/n)
3727
let MUTATION_CHANCE = 100
3828

39-
/*
40-
calculated the fitness based on approximate string matching
41-
compares each character ascii value difference and adds that to a total fitness
42-
optimal string comparsion = 0
43-
*/
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+
4454
func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
4555

4656
var fitness = 0
@@ -50,26 +60,37 @@ func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
5060
return fitness
5161
}
5262

53-
/*
54-
randomly mutate the string
55-
*/
56-
func mutate(dna:[UInt8], mutationChance:Int, dnaSize:Int) -> [UInt8] {
63+
calculateFitness(dna: "Gello, 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(lexicon: [UInt8], dna:[UInt8], mutationChance:Int) -> [UInt8] {
5781
var outputDna = dna
5882

59-
for i in 0..<dnaSize {
83+
for i in 0..<dna.count {
6084
let rand = Int(arc4random_uniform(UInt32(mutationChance)))
6185
if rand == 1 {
62-
outputDna[i] = randomChar()
86+
outputDna[i] = randomChar(from: lexicon)
6387
}
6488
}
6589

6690
return outputDna
6791
}
6892

69-
/*
70-
combine two parents to create an offspring
71-
parent = xy & yx, offspring = xx, yy
72-
*/
93+
7394
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[UInt8]) {
7495
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
7596

@@ -82,59 +103,15 @@ func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[
82103
)
83104
}
84105

85-
86-
/*
87-
returns a random population, used to start the evolution
88-
*/
89-
func randomPopulation(populationSize: Int, dnaSize: Int) -> [[UInt8]] {
90-
91-
let letters : [UInt8] = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".asciiArray
92-
let len = UInt32(letters.count)
93-
94-
var pop = [[UInt8]]()
95-
96-
for _ in 0..<populationSize {
97-
var dna = [UInt8]()
98-
for _ in 0..<dnaSize {
99-
let rand = arc4random_uniform(len)
100-
let nextChar = letters[Int(rand)]
101-
dna.append(nextChar)
102-
}
103-
pop.append(dna)
104-
}
105-
return pop
106-
}
107-
108-
109-
/*
110-
function to return random canidate of a population randomally, but weight on fitness.
111-
*/
112-
func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], weight:Double) {
113-
var weightTotal = 0.0
114-
for itemTuple in items {
115-
weightTotal += itemTuple.weight;
116-
}
117-
118-
var n = Double(arc4random_uniform(UInt32(weightTotal * 1000000.0))) / 1000000.0
119-
120-
for itemTuple in items {
121-
if n < itemTuple.weight {
122-
return itemTuple
123-
}
124-
n = n - itemTuple.weight
125-
}
126-
return items[1]
127-
}
128-
129106
func main() {
130107

131108
// generate the starting random population
132-
var population:[[UInt8]] = randomPopulation(populationSize: POP_SIZE, dnaSize: DNA_SIZE)
109+
var population:[[UInt8]] = randomPopulation(from: lex, populationSize: POP_SIZE, dnaSize: DNA_SIZE)
133110
// print("population: \(population), dnaSize: \(DNA_SIZE) ")
134111
var fittest = [UInt8]()
135112

136113
for generation in 0...GENERATIONS {
137-
print("Generation \(generation) with random sample: \(String(bytes: population[0], encoding:.ascii)!)")
114+
// print("Generation \(generation) with random sample: \(String(bytes: population[0], encoding:.ascii)!)")
138115

139116
var weightedPopulation = [(item:[UInt8], weight:Double)]()
140117

@@ -143,7 +120,7 @@ func main() {
143120
for individual in population {
144121
let fitnessValue = calculateFitness(dna: individual, optimal: OPTIMAL)
145122

146-
let pair = ( individual, fitnessValue == 0 ? 1.0 : 1.0/Double( fitnessValue ) )
123+
let pair = ( individual, fitnessValue == 0 ? 1.0 : Double(100/POP_SIZE)/Double( fitnessValue ) )
147124

148125
weightedPopulation.append(pair)
149126
}
@@ -158,22 +135,25 @@ func main() {
158135
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
159136

160137
// append to the population and mutate
161-
population.append(mutate(dna: offspring.dna1, mutationChance: MUTATION_CHANCE, dnaSize: DNA_SIZE))
162-
population.append(mutate(dna: offspring.dna2, mutationChance: MUTATION_CHANCE, dnaSize: DNA_SIZE))
138+
population.append(mutate(lexicon: lex, dna: offspring.dna1, mutationChance: MUTATION_CHANCE))
139+
population.append(mutate(lexicon: lex, dna: offspring.dna2, mutationChance: MUTATION_CHANCE))
163140
}
164141

165142
fittest = population[0]
166143
var minFitness = calculateFitness(dna: fittest, optimal: OPTIMAL)
167144

168145
// parse the population for the fittest string
169-
for indv in population {
146+
for indv in population {lex
170147
let indvFitness = calculateFitness(dna: indv, optimal: OPTIMAL)
171148
if indvFitness < minFitness {
172149
fittest = indv
173150
minFitness = indvFitness
174151
}
175152
}
176153
if minFitness == 0 { break; }
154+
if generation % 1000 == 0 {
155+
print("\(generation): \(String(bytes: fittest, encoding: .utf8)!)")
156+
}
177157
}
178158
print("fittest string: \(String(bytes: fittest, encoding: .ascii)!)")
179159
}

0 commit comments

Comments
 (0)