153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
from typing import Tuple
|
|
from random import gauss, choice, random
|
|
|
|
|
|
def clip(x, min_val, max_val):
|
|
return min(max(x, min_val), max_val)
|
|
|
|
|
|
class NodeGene:
|
|
|
|
def __init__(self, key: int, config, init_val=True):
|
|
self.key = key
|
|
self.config = config
|
|
|
|
if init_val:
|
|
self.init_value()
|
|
else:
|
|
self.bias = 0
|
|
self.response = 1
|
|
self.act = 0
|
|
self.agg = 0
|
|
|
|
def __repr__(self):
|
|
return f'node({self.key}, bias: {self.bias:.3f}, ' \
|
|
f'response: {self.response:.3f}, act: {self.act}, agg: {self.agg})'
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, NodeGene):
|
|
return False
|
|
return self.key == other.key and \
|
|
self.bias == other.bias and \
|
|
self.response == other.response and \
|
|
self.act == other.act and \
|
|
self.agg == other.agg
|
|
|
|
def copy(self):
|
|
new_gene = self.__class__(self.key, config=self.config, init_val=False)
|
|
new_gene.bias = self.bias # numpy array is mutable, so we need to copy it
|
|
new_gene.response = self.response
|
|
new_gene.act = self.act
|
|
new_gene.agg = self.agg
|
|
return new_gene
|
|
|
|
def init_value(self):
|
|
c = self.config.gene
|
|
self.bias = gauss(c.bias.init_mean, c.bias.init_stdev)
|
|
self.response = gauss(c.response.init_mean, c.response.init_stdev)
|
|
self.act = choice(c.activation.options)
|
|
self.agg = choice(c.aggregation.options)
|
|
|
|
self.bias = clip(self.bias, c.bias.min_value, c.bias.max_value)
|
|
self.response = clip(self.response, c.response.min_value, c.response.max_value)
|
|
|
|
def distance(self, other):
|
|
s = abs(self.bias - other.bias) + abs(self.response - other.response)
|
|
if self.act != other.act:
|
|
s += 1
|
|
if self.agg != other.agg:
|
|
s += 1
|
|
return s
|
|
|
|
def mutate(self):
|
|
self.bias = mutate_float(self.bias, self.config.gene.bias)
|
|
self.response = mutate_float(self.response, self.config.gene.response)
|
|
self.act = mutate_string(self.act, self.config.gene.activation)
|
|
self.agg = mutate_string(self.agg, self.config.gene.aggregation)
|
|
|
|
@classmethod
|
|
def crossover(cls, g1, g2):
|
|
assert g1.key == g2.key
|
|
c = cls(g1.key, g1.config, init_val=False)
|
|
c.bias = g1.bias if random() > 0.5 else g2.bias
|
|
c.response = g1.response if random() > 0.5 else g2.response
|
|
c.act = g1.act if random() > 0.5 else g2.act
|
|
c.agg = g1.agg if random() > 0.5 else g2.agg
|
|
return c
|
|
|
|
|
|
class ConnectionGene:
|
|
def __init__(self, key: Tuple[int, int], config, init_val=True):
|
|
self.key = key
|
|
self.config = config
|
|
self.enabled = True
|
|
if init_val:
|
|
self.init_value()
|
|
else:
|
|
self.weight = 1
|
|
|
|
def __repr__(self):
|
|
return f'connection({self.key}, {self.weight:.3f}, {self.enabled})'
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, ConnectionGene):
|
|
return False
|
|
return self.key == other.key and \
|
|
self.weight == other.weight and \
|
|
self.enabled == other.enabled
|
|
|
|
def copy(self):
|
|
new_gene = self.__class__(self.key, self.config, init_val=False)
|
|
new_gene.weight = self.weight
|
|
new_gene.enabled = self.enabled
|
|
return new_gene
|
|
|
|
def init_value(self):
|
|
c = self.config.gene
|
|
self.weight = gauss(c.weight.init_mean, c.weight.init_stdev)
|
|
self.weight = clip(self.weight, c.weight.min_value, c.weight.max_value)
|
|
|
|
def distance(self, other):
|
|
s = abs(self.weight - other.weight)
|
|
if self.enabled != other.enabled:
|
|
s += 1
|
|
return s
|
|
|
|
def mutate(self):
|
|
self.weight = mutate_float(self.weight, self.config.gene.weight)
|
|
if random() < self.config.gene.enabled.mutate_rate:
|
|
self.enabled = not self.enabled
|
|
|
|
@classmethod
|
|
def crossover(cls, g1, g2):
|
|
assert g1.key == g2.key
|
|
c = cls(g1.key, g1.config, init_val=False)
|
|
c.weight = g1.weight if random() > 0.5 else g2.weight
|
|
c.enabled = g1.enabled if random() > 0.5 else g2.enabled
|
|
return c
|
|
|
|
|
|
# HAHA, exactly the bug is here!!
|
|
# After I fixed it, the result is much better!!
|
|
def mutate_float(v, vc):
|
|
"""vc -> value config"""
|
|
r = random()
|
|
if r < vc.mutate_rate:
|
|
v += gauss(0, vc.mutate_power)
|
|
v = clip(v, vc.min_value, vc.max_value)
|
|
# Previous, seems like a huge bug
|
|
# if r < vc.mutate_rate + vc.replace_rate:
|
|
# Now:
|
|
elif r < vc.mutate_rate + vc.replace_rate:
|
|
v = gauss(vc.init_mean, vc.init_stdev)
|
|
v = clip(v, vc.min_value, vc.max_value)
|
|
return v
|
|
|
|
|
|
def mutate_string(v, vc):
|
|
"""vc -> value config"""
|
|
r = random()
|
|
if r < vc.mutate_rate:
|
|
v = choice(vc.options)
|
|
return v
|