This repository has been archived by the owner on Dec 11, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
/
genome.go
433 lines (385 loc) · 13 KB
/
genome.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
// genome.go implementation of the genome in evolution.
//
// Copyright (C) 2017 Jin Yeom
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package neat
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"os"
"time"
)
// NodeGene is an implementation of each node in the graph representation of a
// genome. Each node consists of a node ID, its type, and the activation type.
type NodeGene struct {
ID int `json:"id"` // node ID
Type string `json:"type"` // node type
Activation *ActivationFunc `json:"activation"` // activation function
}
// NewNodeGene returns a new instance of NodeGene, given its ID, its type, and
// the activation function of this node.
func NewNodeGene(id int, ntype string, activation *ActivationFunc) *NodeGene {
return &NodeGene{id, ntype, activation}
}
// Copy returns a deep copy of this node gene.
func (n *NodeGene) Copy() *NodeGene {
return &NodeGene{n.ID, n.Type, n.Activation}
}
// String returns a string representation of the node.
func (n *NodeGene) String() string {
return fmt.Sprintf("[%s(%d, %s)]", n.Type, n.ID, n.Activation.Name)
}
// ConnGene is an implementation of a connection between two nodes in the graph
// representation of a genome. Each connection includes its input node, output
// node, connection weight, and an indication of whether this connection is
// disabled
type ConnGene struct {
From int `json:"from"` // input node
To int `json:"to"` // output node
Weight float64 `json:"weight"` // connection weight
Disabled bool `json:"disabled"` // true if disabled
}
// NewConnGene returns a new instance of ConnGene, given the input and output
// node genes. By default, the connection is enabled.
func NewConnGene(from, to int, weight float64) *ConnGene {
return &ConnGene{from, to, weight, false}
}
// Copy returns a deep copy of this connection gene.
func (c *ConnGene) Copy() *ConnGene {
return &ConnGene{
From: c.From,
To: c.To,
Weight: c.Weight,
Disabled: c.Disabled,
}
}
// String returns the string representation of this connection.
func (c *ConnGene) String() string {
connectivity := fmt.Sprintf("{%.3f}", c.Weight)
if c.Disabled {
connectivity = " / "
}
return fmt.Sprintf("[%d]-%s->[%d]", c.From, connectivity, c.To)
}
// Genome encodes the weights and topology of the output network as a collection
// of nodes and connection genes.
type Genome struct {
ID int `json:"id"` // genome ID
SpeciesID int `json:"speciesID"` // genome's species ID
NodeGenes []*NodeGene `json:"nodeGenes"` // nodes in the genome
ConnGenes []*ConnGene `json:"connGenes"` // connections in the genome
Fitness float64 `json:"fitness"` // fitness score
evaluated bool // true if already evaluated
}
// NewFCGenome returns an instance of initial Genome with fully connected input
// and output layers.
func NewFCGenome(id, numInputs, numOutputs int, initFitness float64) *Genome {
nodeGenes := make([]*NodeGene, 0, numInputs+numOutputs)
connGenes := make([]*ConnGene, 0, numInputs*numOutputs)
for i := 0; i < numInputs; i++ {
inputNode := NewNodeGene(i, "input", ActivationSet["identity"])
nodeGenes = append(nodeGenes, inputNode)
}
for i := numInputs; i < numInputs+numOutputs; i++ {
outputNode := NewNodeGene(i, "output", ActivationSet["sigmoid"])
for j := 0; j < numInputs; j++ {
c := NewConnGene(j, i, rand.NormFloat64()*6.0)
connGenes = append(connGenes, c)
}
nodeGenes = append(nodeGenes, outputNode)
}
return &Genome{
ID: id,
SpeciesID: -1,
NodeGenes: nodeGenes,
ConnGenes: connGenes,
Fitness: initFitness,
evaluated: false,
}
}
// NewGenome returns an instance of initial Genome with no initial connections.
func NewGenome(id, numInputs, numOutputs int, initFitness float64) *Genome {
return &Genome{
ID: id,
SpeciesID: -1,
NodeGenes: func() []*NodeGene {
nodeGenes := make([]*NodeGene, 0, numInputs+numOutputs)
for i := 0; i < numInputs; i++ {
inputNode := NewNodeGene(i, "input", ActivationSet["identity"])
nodeGenes = append(nodeGenes, inputNode)
}
for i := numInputs; i < numInputs+numOutputs; i++ {
outputNode := NewNodeGene(i, "output", ActivationSet["sigmoid"])
nodeGenes = append(nodeGenes, outputNode)
}
return nodeGenes
}(),
ConnGenes: make([]*ConnGene, 0),
Fitness: initFitness,
evaluated: false,
}
}
// Copy returns a deep copy of this genome.
func (g *Genome) Copy() *Genome {
return &Genome{
ID: g.ID,
SpeciesID: g.SpeciesID,
NodeGenes: func() []*NodeGene {
copies := make([]*NodeGene, len(g.NodeGenes))
for i := range copies {
copies[i] = g.NodeGenes[i].Copy()
}
return copies
}(),
ConnGenes: func() []*ConnGene {
copies := make([]*ConnGene, len(g.ConnGenes))
for i := range copies {
copies[i] = g.ConnGenes[i].Copy()
}
return copies
}(),
Fitness: g.Fitness,
evaluated: g.evaluated,
}
}
// String returns the string representation of the genome.
func (g *Genome) String() string {
str := fmt.Sprintf("Genome(%d, %.3f):\n", g.ID, g.Fitness)
str += "Nodes (\n"
for _, node := range g.NodeGenes {
str += " " + node.String() + "\n"
}
str += ")\n"
str += "Connections (\n"
for _, conn := range g.ConnGenes {
str += " " + conn.String() + "\n"
}
str += ")"
return str
}
// Evaluate takes an evaluation function and evaluates its fitness. Only perform
// the evaluation if it hasn't yet. If the lamarckian indicator is true, encode
// the phenotype neural network back into the genome.
func (g *Genome) Evaluate(evaluate EvaluationFunc) {
if g.evaluated {
return
}
nn := NewNeuralNetwork(g)
g.Fitness = evaluate(nn)
g.evaluated = true
}
// ExportJSON exports a JSON file that contains this genome's information. If
// the argument format indicator is true, the exported JSON file will be
// formatted with indentations.
func (g *Genome) ExportJSON(format bool) error {
// create a new json file
filename := fmt.Sprintf("genome_%d_%d.json", g.ID, time.Now().UnixNano())
f, err := os.Create(filename)
if err != nil {
return err
}
encoder := json.NewEncoder(f)
if format {
encoder.SetIndent("", "\t")
}
if err = encoder.Encode(g); err != nil {
return err
}
return nil
}
// MutatePerturb mutates the genome by perturbation of its weights by the
// argument rate.
func (g *Genome) MutatePerturb(rate float64) {
// perturb connection weights
for _, conn := range g.ConnGenes {
if rand.Float64() < rate {
g.evaluated = false
conn.Weight += rand.NormFloat64()
}
}
}
// MutateAddNode mutates the genome by adding a node with the argument
// activation function.
func (g *Genome) MutateAddNode(rate float64, activation *ActivationFunc) {
// add node between two connected nodes, by randomly selecting a connection;
// only applied if there are connections in the genome
if rand.Float64() < rate && len(g.ConnGenes) != 0 {
g.evaluated = false
selected := g.ConnGenes[rand.Intn(len(g.ConnGenes))]
newNode := NewNodeGene(len(g.NodeGenes), "hidden", ActivationSet["sigmoid"])
g.NodeGenes = append(g.NodeGenes, newNode)
g.ConnGenes = append(g.ConnGenes,
NewConnGene(selected.From, newNode.ID, 1.0),
NewConnGene(newNode.ID, selected.To, selected.Weight))
selected.Disabled = true
}
}
// MutateAddConn mutates the genome by adding a connection.
func (g *Genome) MutateAddConn(rate float64) {
// add connection between two disconnected nodes; only applied if the selected
// nodes are not connected yet, and the resulting connection doesn't make the
// phenotype network recurrent
if rand.Float64() < rate {
g.evaluated = false
selectedNode0 := g.NodeGenes[rand.Intn(len(g.NodeGenes))].ID
selectedNode1 := g.NodeGenes[rand.Intn(len(g.NodeGenes))].ID
for _, conn := range g.ConnGenes {
if conn.From == selectedNode0 && conn.To == selectedNode1 {
return
}
}
if g.NodeGenes[selectedNode1].Type == "input" ||
g.NodeGenes[selectedNode0].Type == "output" {
return
}
if !g.pathExists(selectedNode1, selectedNode0) {
g.ConnGenes = append(g.ConnGenes, NewConnGene(selectedNode0,
selectedNode1, rand.NormFloat64()*6.0))
}
}
}
// pathExists returns true if there is a path from the source to the
// destination. Helper method of MutateAddConn.
func (g *Genome) pathExists(src, dst int) bool {
if src == dst {
return true
}
for _, edge := range g.ConnGenes {
if edge.From == src {
if g.pathExists(edge.To, dst) {
return true
}
}
}
return false
}
// Crossover returns a new child genome by performing crossover between the two
// argument genomes.
//
// innovations is a temporary dictionary for the child genome's connection
// genes; it essentially stores all connection genes that will be contained
// in the child genome.
//
// Initially, all of one parent genome's connections are recorded to
// innovations. Then, as the other parent genome's connections are added, it
// checks if each connection already exists; if it does, swap with the other
// parent's connection by 50% chance. Otherwise, append the new connection.
func Crossover(id int, g0, g1 *Genome, initFitness float64) *Genome {
innovations := make(map[[2]int]*ConnGene)
for _, conn := range g0.ConnGenes {
innovations[[2]int{conn.From, conn.To}] = conn
}
for _, conn := range g1.ConnGenes {
innov := [2]int{conn.From, conn.To}
if innovations[innov] != nil {
if rand.Float64() < 0.5 {
innovations[innov] = conn
}
} else {
innovations[innov] = conn
}
}
// copy node genes
largerParent := g0
if len(g0.NodeGenes) < len(g1.NodeGenes) {
largerParent = g1
}
nodeGenes := make([]*NodeGene, len(largerParent.NodeGenes))
for i := range largerParent.NodeGenes {
nodeGenes[i] = largerParent.NodeGenes[i].Copy()
}
// copy connection genes
connGenes := make([]*ConnGene, 0, len(innovations))
for _, conn := range innovations {
connGenes = append(connGenes, conn.Copy())
}
return &Genome{
ID: id,
NodeGenes: nodeGenes,
ConnGenes: connGenes,
Fitness: initFitness,
}
}
// Compatibility computes the compatibility distance between two argument
// genomes.
//
// Compatibility distance of two genomes is utilized for differentiating their
// species during speciation. The distance is computed as follows:
//
// d = c0 * U + c1 * W
//
// c0, c1, are hyperparameter coefficients, and U, W are respectively number of
// unmatching genes, and the average weight differences of matching genes. This
// approach is a slightly modified version of Dr. Kenneth Stanley's original
// approach in which unmatching genes are separated into excess and disjoint
// genes.
func Compatibility(g0, g1 *Genome, c0, c1 float64) float64 {
innov0 := make(map[[2]int]*ConnGene) // innovations in g0
innov1 := make(map[[2]int]*ConnGene) // innovations in g1
for _, conn := range g0.ConnGenes {
innov0[[2]int{conn.From, conn.To}] = conn
}
for _, conn := range g1.ConnGenes {
innov1[[2]int{conn.From, conn.To}] = conn
}
matching := make(map[*ConnGene]*ConnGene) // pairs of matching genes
unmatchingCount := 0 // unmatching gene counter
// look for matching/unmatching genes from g1's innovations; if a connection
// in g0 is not one of g1's innovations, increment unmatching counter.
// Otherwise, add the connection to matching
for _, conn := range g0.ConnGenes {
innov := innov1[[2]int{conn.From, conn.To}]
if innov == nil {
unmatchingCount++
} else {
matching[innov] = conn
}
}
// repeat for g0's innovations, to count unmatching connection genes for g1.
for _, conn := range g1.ConnGenes {
if innov0[[2]int{conn.From, conn.To}] == nil {
unmatchingCount++
}
}
// compute average weight differences of matching genes
diffSum := 0.0
matchingCount := len(matching)
for conn0, conn1 := range matching {
diffSum += math.Abs(conn0.Weight - conn1.Weight)
}
avgDiff := diffSum / float64(matchingCount)
if matchingCount == 0 {
avgDiff = 0.0
}
return c0*float64(unmatchingCount) + c1*avgDiff
}
// ComparisonFunc is a type of function that returns a boolean value that
// indicates whether the first argument genome is better than the second one
// in terms of its fitness.
type ComparisonFunc func(g0, g1 *Genome) bool
// NewComparisonFunc returns a new comparison function, given an indicator of
// whether the fitness is better when minimized.
func NewComparisonFunc(minimize bool) ComparisonFunc {
if minimize {
return func(g0, g1 *Genome) bool {
return g0.Fitness < g1.Fitness
}
}
return func(g0, g1 *Genome) bool {
return g0.Fitness > g1.Fitness
}
}