Skip to content

Commit 66f75d2

Browse files
committed
tutotial complete
1 parent 81befa6 commit 66f75d2

File tree

3 files changed

+139
-50
lines changed

3 files changed

+139
-50
lines changed

Genetic/README.markdown

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Genetic Algorthim
1+
individual# Genetic Algorthim
22

33
## What is it?
44

@@ -20,7 +20,9 @@ The randomization that allows for organisms to change over time. In GAs we build
2020
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.
2121

2222
### Resources:
23-
* [Wikipedia]()
23+
* [Genetic Algorithms in Search Optimization, and Machine Learning](https://www.amazon.com/Genetic-Algorithms-Optimization-Machine-Learning/dp/0201157675/ref=sr_1_sc_1?ie=UTF8&qid=1520628364&sr=8-1-spell&keywords=Genetic+Algortithms+in+search)
24+
* [Wikipedia](https://en.wikipedia.org/wiki/Genetic_algorithm)
25+
* [My Original Gist](https://gist.github.com/blainerothrock/efda6e12fe10792c99c990f8ff3daeba)
2426

2527

2628
## The Code
@@ -141,7 +143,7 @@ func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], wei
141143
}
142144
```
143145

144-
The above function takes a list of individuals with their calculated fitness. Then selects one at random offset by their fitness value.
146+
The above function takes a list of individuals with their calculated fitness. Then selects one at random offset by their fitness value. The horrible 1,000,000 multiplication and division is to insure precision by calculating decimals. `arc4random` only uses integers so this is required to convert to a precise Double, it's not perfect, but enough for our example.
145147

146148
## Mutation
147149

@@ -168,19 +170,16 @@ This allows for a population to explore all the possibilities of it's building b
168170

169171
## Crossover
170172

171-
Crossover, the sexy part of a GA, is how offspring are created from 2 selected individuals in the current population. This is done by splitting the parents into 2 parts, then combining 1 part from each parent to create the offspring. To promote diversity, we randomly select a index to split the parents:
173+
Crossover, the sexy part of a GA, is how offspring are created from 2 selected individuals in the current population. This is done by splitting the parents into 2 parts, then combining 1 part from each parent to create the offspring. To promote diversity, we randomly select a index to split the parents.
172174

173175
```swift
174-
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[UInt8]) {
176+
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> [UInt8] {
175177
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
176178

177179
let dna1Index1 = dna1.index(dna1.startIndex, offsetBy: pos)
178180
let dna2Index1 = dna2.index(dna2.startIndex, offsetBy: pos)
179181

180-
return (
181-
[UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1)),
182-
[UInt8](dna2.prefix(upTo: dna2Index1) + dna1.suffix(from: dna1Index1))
183-
)
182+
return [UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1))
184183
}
185184
```
186185

@@ -203,7 +202,7 @@ for generation in 0...GENERATIONS {
203202
}
204203
```
205204

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`.
205+
Now, for each individual in the population, we need to calculate its fitness and weighted value. Since 0 is the best value we will use `1/fitness` to represent the weighted value. Note this is not a percent, but just how much more likely the value is to be selected over others. If the highest number was the most fit, the weight calculation would be `fitness/totalFitness`, which would be a percent.
207206

208207
```swift
209208
var weightedPopulation = [(item:[UInt8], weight:Double)]()
@@ -215,24 +214,118 @@ for individual in population {
215214
}
216215
```
217216

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:
217+
From here we can start to build the next generation.
219218

220-
```txt
221-
1: 10
222-
2: 5
223-
3: 4
224-
4: 7
225-
5: 11
219+
```swift
220+
var nextGeneration = []
221+
```
222+
223+
The below loop is where we pull everything together. We loop for `POP_SIZE`, selecting 2 individuals by weighted choice, crossover their values to produce a offspring, then finial subject the new individual to mutation. Once completed we have a completely new generation based on the last generation.
224+
225+
```swift
226+
0...POP_SIZE).forEach { _ in
227+
let ind1 = weightedChoice(items: weightedPopulation)
228+
let ind2 = weightedChoice(items: weightedPopulation)
229+
230+
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
231+
232+
// append to the population and mutate
233+
nextGeneration.append(mutate(lexicon: lex, dna: offspring, mutationChance: MUTATION_CHANCE))
234+
}
235+
```
236+
237+
The final piece to the main loop is to select the fittest individual of a population:
238+
239+
```swift
240+
fittest = population[0]
241+
var minFitness = calculateFitness(dna: fittest, optimal: OPTIMAL)
242+
243+
for indv in population {lex
244+
let indvFitness = calculateFitness(dna: indv, optimal: OPTIMAL)
245+
if indvFitness < minFitness {
246+
fittest = indv
247+
minFitness = indvFitness
248+
}
249+
}
250+
if minFitness == 0 { break; }
251+
print("\(generation): \(String(bytes: fittest, encoding: .utf8)!)")
226252
```
227253

228-
Now here is the weight of each:
254+
Since we know the fittest string, I've added a `break` to kill the program if we find it. At the end of a loop at a print statement for the fittest string:
229255

230-
```txt
231-
1:
232-
2:
233-
3:
234-
4:
235-
5:
256+
```swift
257+
print("fittest string: \(String(bytes: fittest, encoding: .utf8)!)")
258+
```
236259

237-
total =
260+
Now we can run the program! Playgrounds are a nice place to develop, but are going to run this program **very slow**. I highly suggest running in Terminal: `swift gen.swift`. When running you should see something like this and it should not take too long to get `Hello, World`:
261+
262+
```text
263+
0: RXclh F HDko
264+
1: DkyssjgElk];
265+
2: TiM4u) DrKvZ
266+
3: Dkysu) DrKvZ
267+
4: -kysu) DrKvZ
268+
5: Tlwsu) DrKvZ
269+
6: Tlwsu) Drd}k
270+
7: Tlwsu) Drd}k
271+
8: Tlwsu) Drd}k
272+
9: Tlwsu) Drd}k
273+
10: G^csu) |zd}k
274+
11: G^csu) |zdko
275+
12: G^csu) |zdko
276+
13: Dkysu) Drd}k
277+
14: G^wsu) `rd}k
278+
15: Dkysu) `rdko
279+
16: Dkysu) `rdko
280+
17: Glwsu) `rdko
281+
18: TXysu) `rdkc
282+
19: U^wsu) `rdko
283+
20: G^wsu) `rdko
284+
21: Glysu) `rdko
285+
22: G^ysu) `rdko
286+
23: G^ysu) `ryko
287+
24: G^wsu) `rdko
288+
25: G^wsu) `rdko
289+
26: G^wsu) `rdko
290+
...
291+
1408: Hello, Wormd
292+
1409: Hello, Wormd
293+
1410: Hello, Wormd
294+
1411: Hello, Wormd
295+
1412: Hello, Wormd
296+
1413: Hello, Wormd
297+
1414: Hello, Wormd
298+
1415: Hello, Wormd
299+
1416: Hello, Wormd
300+
1417: Hello, Wormd
301+
1418: Hello, Wormd
302+
1419: Hello, Wormd
303+
1420: Hello, Wormd
304+
1421: Hello, Wormd
305+
1422: Hello, Wormd
306+
1423: Hello, Wormd
307+
1424: Hello, Wormd
308+
1425: Hello, Wormd
309+
1426: Hello, Wormd
310+
1427: Hello, Wormd
311+
1428: Hello, Wormd
312+
1429: Hello, Wormd
313+
1430: Hello, Wormd
314+
1431: Hello, Wormd
315+
1432: Hello, Wormd
316+
1433: Hello, Wormd
317+
1434: Hello, Wormd
318+
1435: Hello, Wormd
319+
fittest string: Hello, World
238320
```
321+
322+
How long it takes will vary since this is based on randomization, but it should almost always finish in under 5000 generations. Woo!
323+
324+
325+
## Now What?
326+
327+
We did it, we have a running simple genetic algorithm. Take some time a play around with the global variables, `POP_SIZE`, `OPTIMAL`, `MUTATION_CHANCE`, `GENERATIONS`. Just make sure to only add characters that are in the lexicon, but go ahead and update too!
328+
329+
For an example let's try something much longer: `Ray Wenderlich's Swift Algorithm Club Rocks`. Plug that string into `OPTIMAL` and change `GENERATIONS` to `10000`. You'll be able to see that the we are getting somewhere, but you most likely will not reach the optimal string in 10,000 generations. Since we have a larger string let's raise our mutation chance to `200` (1/2 as likely to mutate). You may not get there, but you should get a lot closer than before. With a longer string, too much mutate can make it hard for fit strings to survive. Now try either upping `POP_SIZE` or increase `GENERATIONS`. Either way you should eventually get the value, but there will be a "sweet spot" for an individual of a certain size.
330+
331+
Please submit any kind of update to this tutorial or add more examples!

Genetic/gen.playground/Contents.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
6363
calculateFitness(dna: "Gello, World".asciiArray, optimal: "Hello, World".asciiArray)
6464

6565
func weightedChoice(items:[(item:[UInt8], weight:Double)]) -> (item:[UInt8], weight:Double) {
66-
66+
6767
let total = items.reduce(0.0) { return $0 + $1.weight}
6868

6969
var n = Double(arc4random_uniform(UInt32(total * 1000000.0))) / 1000000.0
@@ -91,16 +91,13 @@ func mutate(lexicon: [UInt8], dna:[UInt8], mutationChance:Int) -> [UInt8] {
9191
}
9292

9393

94-
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[UInt8]) {
94+
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> [UInt8] {
9595
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
9696

9797
let dna1Index1 = dna1.index(dna1.startIndex, offsetBy: pos)
9898
let dna2Index1 = dna2.index(dna2.startIndex, offsetBy: pos)
9999

100-
return (
101-
[UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1)),
102-
[UInt8](dna2.prefix(upTo: dna2Index1) + dna1.suffix(from: dna1Index1))
103-
)
100+
return [UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1))
104101
}
105102

106103
func main() {
@@ -111,7 +108,7 @@ func main() {
111108
var fittest = [UInt8]()
112109

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

116113
var weightedPopulation = [(item:[UInt8], weight:Double)]()
117114

@@ -120,23 +117,22 @@ func main() {
120117
for individual in population {
121118
let fitnessValue = calculateFitness(dna: individual, optimal: OPTIMAL)
122119

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

125122
weightedPopulation.append(pair)
126123
}
127124

128125
population = []
129126

130127
// create a new generation using the individuals in the origional population
131-
for _ in 0...POP_SIZE/2 {
128+
(0...POP_SIZE).forEach { _ in
132129
let ind1 = weightedChoice(items: weightedPopulation)
133130
let ind2 = weightedChoice(items: weightedPopulation)
134131

135132
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
136133

137134
// 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))
135+
population.append(mutate(lexicon: lex, dna: offspring, mutationChance: MUTATION_CHANCE))
140136
}
141137

142138
fittest = population[0]
@@ -151,6 +147,11 @@ func main() {
151147
}
152148
}
153149
if minFitness == 0 { break; }
150+
print("\(generation): \(String(bytes: fittest, encoding: .utf8)!)")
151+
154152
}
155-
print("fittest string: \(String(bytes: fittest, encoding: .ascii)!)")
153+
print("fittest string: \(String(bytes: fittest, encoding: .utf8)!)")
156154
}
155+
156+
main()
157+

Genetic/gen.swift

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let OPTIMAL:[UInt8] = "Hello, World".asciiArray
1818
let DNA_SIZE = OPTIMAL.count
1919

2020
// size of each generation
21-
let POP_SIZE = 200
21+
let POP_SIZE = 50
2222

2323
// max number of generations, script will stop when it reach 5000 if the optimal value is not found
2424
let GENERATIONS = 5000
@@ -91,16 +91,13 @@ func mutate(lexicon: [UInt8], dna:[UInt8], mutationChance:Int) -> [UInt8] {
9191
}
9292

9393

94-
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> (dna1:[UInt8], dna2:[UInt8]) {
94+
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> [UInt8] {
9595
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
9696

9797
let dna1Index1 = dna1.index(dna1.startIndex, offsetBy: pos)
9898
let dna2Index1 = dna2.index(dna2.startIndex, offsetBy: pos)
9999

100-
return (
101-
[UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1)),
102-
[UInt8](dna2.prefix(upTo: dna2Index1) + dna1.suffix(from: dna1Index1))
103-
)
100+
return [UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1))
104101
}
105102

106103
func main() {
@@ -128,15 +125,14 @@ func main() {
128125
population = []
129126

130127
// create a new generation using the individuals in the origional population
131-
for _ in 0...POP_SIZE/2 {
128+
(0...POP_SIZE).forEach { _ in
132129
let ind1 = weightedChoice(items: weightedPopulation)
133130
let ind2 = weightedChoice(items: weightedPopulation)
134131

135132
let offspring = crossover(dna1: ind1.item, dna2: ind2.item, dnaSize: DNA_SIZE)
136133

137134
// 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))
135+
population.append(mutate(lexicon: lex, dna: offspring, mutationChance: MUTATION_CHANCE))
140136
}
141137

142138
fittest = population[0]
@@ -151,11 +147,10 @@ func main() {
151147
}
152148
}
153149
if minFitness == 0 { break; }
154-
if generation % 1000 == 0 {
155-
print("\(generation): \(String(bytes: fittest, encoding: .utf8)!)")
156-
}
150+
print("\(generation): \(String(bytes: fittest, encoding: .utf8)!)")
151+
157152
}
158-
print("fittest string: \(String(bytes: fittest, encoding: .ascii)!)")
153+
print("fittest string: \(String(bytes: fittest, encoding: .utf8)!)")
159154
}
160155

161156
main()

0 commit comments

Comments
 (0)