Skip to content

Commit de935b7

Browse files
authored
Updates README.markdown
* now using new `random` API from the Swift collection protocols, instead of creating a new function that uses `arc4random` * updates code to Swift 4.2 * fixes some inconsistencies between terms begin used - such as `MAX_GENERATIONS` and `GENERATIONS`
1 parent a0cfcee commit de935b7

File tree

1 file changed

+23
-37
lines changed

1 file changed

+23
-37
lines changed

Genetic/README.markdown

+23-37
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
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), crossover and mutation. To understand more, let's walk through these processes in terms of biology:
66

77
### 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)
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.
99
1010
In other words, survival of the fittest. Organisms that survive in their environment tend to reproduce more. With GAs we generate a fitness model that will rank individuals and give them a better chance for reproduction.
1111

@@ -60,35 +60,23 @@ extension String {
6060
let OPTIMAL:[UInt8] = "Hello, World".unicodeArray
6161
let DNA_SIZE = OPTIMAL.count
6262
let POP_SIZE = 50
63-
let MAX_GENERATIONS = 5000
63+
let GENERATIONS = 5000
6464
let MUTATION_CHANCE = 100
6565
```
6666

67-
The last piece we need for set up is a function to give us a random unicode value from our lexicon:
68-
69-
```swift
70-
func randomChar(from lexicon: [UInt8]) -> UInt8 {
71-
let len = UInt32(lexicon.count-1)
72-
let rand = Int(arc4random_uniform(len))
73-
return lexicon[rand]
74-
}
75-
```
76-
77-
**Note**: `arc4random_uniform` is strictly used in this example. It would be fun to play around with some of the [randomization in GameKit](https://developer.apple.com/library/content/documentation/General/Conceptual/GameplayKit_Guide/RandomSources.html)
78-
7967
### Population Zero
8068

8169
Before selecting, crossover and mutation, we need a population to start with. Now that we have the universe defined we can write that function:
8270

8371
```swift
8472
func randomPopulation(from lexicon: [UInt8], populationSize: Int, dnaSize: Int) -> [[UInt8]] {
85-
73+
guard lexicon.count > 1 else { return [] }
8674
var pop = [[UInt8]]()
8775

8876
(0..<populationSize).forEach { _ in
8977
var dna = [UInt8]()
9078
(0..<dnaSize).forEach { _ in
91-
let char = randomChar(from: lexicon)
79+
let char = lexicon.randomElement()! // guaranteed to be non-nil by initial guard statement
9280
dna.append(char)
9381
}
9482
pop.append(dna)
@@ -102,10 +90,11 @@ Before selecting, crossover and mutation, we need a population to start with. No
10290
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 unicode values:
10391

10492
```swift
105-
func calculateFitness(dna:[UInt8], optimal:[UInt8]) -> Int {
93+
func calculateFitness(dna: [UInt8], optimal: [UInt8]) -> Int {
94+
guard dna.count == optimal.count else { return -1 }
10695
var fitness = 0
107-
(0...dna.count-1).forEach { c in
108-
fitness += abs(Int(dna[c]) - Int(optimal[c]))
96+
for index in dna.indices {
97+
fitness += abs(Int(dna[index]) - Int(optimal[index]))
10998
}
11099
return fitness
111100
}
@@ -121,13 +110,11 @@ Let's take a second and ask why on this one. Why would you not always want to se
121110

122111
With all that, here is our weight choice function:
123112

124-
```swift
125-
func weightedChoice(items:[(dna:[UInt8], weight:Double)]) -> (dna:[UInt8], weight:Double) {
126-
127-
let total = items.reduce(0.0) { return $0 + $1.weight}
128-
129-
var n = Double(arc4random_uniform(UInt32(total * 1000000.0))) / 1000000.0
130-
113+
func weightedChoice(items: [(dna: [UInt8], weight: Double)]) -> (dna: [UInt8], weight: Double) {
114+
115+
let total = items.reduce(0) { $0 + $1.weight }
116+
var n = Double.random(in: 0..<(total * 1000000)) / 1000000.0
117+
131118
for item in items {
132119
if n < item.weight {
133120
return item
@@ -136,25 +123,24 @@ func weightedChoice(items:[(dna:[UInt8], weight:Double)]) -> (dna:[UInt8], weigh
136123
}
137124
return items[1]
138125
}
139-
```
140126

141-
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.
127+
128+
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. `Double.random` only uses integers so this is required to convert to a precise Double, it's not perfect, but enough for our example.
142129

143130
## Mutation
144131

145132
The all powerful mutation, the thing that introduces otherwise non existent fitness variance. It can either hurt of improve a individuals fitness but over time it will cause evolution towards more fit populations. Imagine if our initial random population was missing the charachter `H`, in that case we need to rely on mutation to introduce that character into the population in order to achieve the optimal solution.
146133

147134
```swift
148-
func mutate(lexicon: [UInt8], dna:[UInt8], mutationChance:Int) -> [UInt8] {
135+
func mutate(lexicon: [UInt8], dna: [UInt8], mutationChance: Int) -> [UInt8] {
149136
var outputDna = dna
150-
151137
(0..<dna.count).forEach { i in
152-
let rand = Int(arc4random_uniform(UInt32(mutationChance)))
138+
let rand = Int.random(in: 0..<mutationChance)
153139
if rand == 1 {
154-
outputDna[i] = randomChar(from: lexicon)
140+
outputDna[i] = lexicon.randomElement()!
155141
}
156142
}
157-
143+
158144
return outputDna
159145
}
160146
```
@@ -168,12 +154,12 @@ This allows for a population to explore all the possibilities of it's building b
168154
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.
169155

170156
```swift
171-
func crossover(dna1:[UInt8], dna2:[UInt8], dnaSize:Int) -> [UInt8] {
172-
let pos = Int(arc4random_uniform(UInt32(dnaSize-1)))
173-
157+
func crossover(dna1: [UInt8], dna2: [UInt8], dnaSize: Int) -> [UInt8] {
158+
let pos = Int.random(in: 0..<dnaSize)
159+
174160
let dna1Index1 = dna1.index(dna1.startIndex, offsetBy: pos)
175161
let dna2Index1 = dna2.index(dna2.startIndex, offsetBy: pos)
176-
162+
177163
return [UInt8](dna1.prefix(upTo: dna1Index1) + dna2.suffix(from: dna2Index1))
178164
}
179165
```

0 commit comments

Comments
 (0)