I am using NEAT-Python to mimic the course of a regular sine function based on the curve's absolute difference from 0. The configuration file has almost entirely been adopted from the basic XOR example, with the exception of the number of inputs being set to 1
. The direction of the offset is inferred from the original data right after the actual prediction step, so this is really all about predicting offsets in the range from [0, 1]
.
The fitness function and most of the remaining code have also been adopted from the help pages, which is why I am fairly confident that the code is consistent from a technical perspective. As seen from the visualization of observed vs. predicted offsets included below, the model creates quite good results in most cases. However, it fails to capture the lower and upper end of the range of values.
Any help on how to improve the algorithm's performance, particularly at the lower/upper edge, would be highly appreciated. Or are there any methodical limitations that I haven't taken into consideration so far?
config-feedforward
located in current working directory:
#--- parameters for the XOR-2 experiment ---#
[NEAT]
fitness_criterion = max
fitness_threshold = 3.9
pop_size = 150
reset_on_extinction = False
[DefaultGenome]
# node activation options
activation_default = sigmoid
activation_mutate_rate = 0.0
activation_options = sigmoid
# node aggregation options
aggregation_default = sum
aggregation_mutate_rate = 0.0
aggregation_options = sum
# node bias options
bias_init_mean = 0.0
bias_init_stdev = 1.0
bias_max_value = 30.0
bias_min_value = -30.0
bias_mutate_power = 0.5
bias_mutate_rate = 0.7
bias_replace_rate = 0.1
# genome compatibility options
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.5
# connection add/remove rates
conn_add_prob = 0.5
conn_delete_prob = 0.5
# connection enable options
enabled_default = True
enabled_mutate_rate = 0.01
feed_forward = True
initial_connection = full
# node add/remove rates
node_add_prob = 0.2
node_delete_prob = 0.2
# network parameters
num_hidden = 0
num_inputs = 1
num_outputs = 1
# node response options
response_init_mean = 1.0
response_init_stdev = 0.0
response_max_value = 30.0
response_min_value = -30.0
response_mutate_power = 0.0
response_mutate_rate = 0.0
response_replace_rate = 0.0
# connection weight options
weight_init_mean = 0.0
weight_init_stdev = 1.0
weight_max_value = 30
weight_min_value = -30
weight_mutate_power = 0.5
weight_mutate_rate = 0.8
weight_replace_rate = 0.1
[DefaultSpeciesSet]
compatibility_threshold = 3.0
[DefaultStagnation]
species_fitness_func = max
max_stagnation = 20
species_elitism = 2
[DefaultReproduction]
elitism = 2
survival_threshold = 0.2
NEAT functions:
# . fitness function ----
def eval_genomes(genomes, config):
for genome_id, genome in genomes:
genome.fitness = 4.0
net = neat.nn.FeedForwardNetwork.create(genome, config)
for xi in zip(abs(x)):
output = net.activate(xi)
genome.fitness -= abs(output[0] - xi[0]) ** 2
# . neat run ----
def run(config_file, n = None):
# load configuration
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_file)
# create the population, which is the top-level object for a NEAT run
p = neat.Population(config)
# add a stdout reporter to show progress in the terminal
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
p.add_reporter(neat.Checkpointer(5))
# run for up to n generations
winner = p.run(eval_genomes, n)
return(winner)
Code:
### ENVIRONMENT ====
### . packages ----
import os
import neat
import numpy as np
import matplotlib.pyplot as plt
import random
### . sample data ----
x = np.sin(np.arange(.01, 4000 * .01, .01))
### NEAT ALGORITHM ====
### . model evolution ----
random.seed(1899)
winner = run('config-feedforward', n = 25)
### . prediction ----
## extract winning model
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
'config-feedforward')
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
## make predictions
y = []
for xi in zip(abs(x)):
y.append(winner_net.activate(xi))
## if required, adjust signs
for i in range(len(y)):
if (x[i] < 0):
y[i] = [x * -1 for x in y[i]]
## display sample vs. predicted data
plt.scatter(range(len(x)), x, color='#3c8dbc', label = 'observed') # blue
plt.scatter(range(len(x)), y, color='#f39c12', label = 'predicted') # orange
plt.hlines(0, xmin = 0, xmax = len(x), colors = 'grey', linestyles = 'dashed')
plt.xlabel("Index")
plt.ylabel("Offset")
plt.legend(bbox_to_anchor = (0., 1.02, 1., .102), loc = 10,
ncol = 2, mode = None, borderaxespad = 0.)
plt.show()
plt.clf()
答案 0 :(得分:2)
NEAT存在不同的实现方式,因此细节可能有所不同。
通常,NEAT通过包括始终处于活动状态的特殊输入神经元来处理偏差(激活后1)。我怀疑bias_max_value和bias_min_value决定了该偏置神经元和隐藏神经元之间连接的最大允许强度。在我使用的NEAT代码中,这两个参数不存在,并且偏向隐藏的连接被视为正常连接(在我们自己的范围内,从-5到5)。
如果您使用的是Sigmoid函数,则输出神经元将在0到1的范围内工作(考虑更快地激活隐藏神经元,可能是RELU)。
如果您要预测接近0或1的值,这是一个问题,因为您确实需要将神经元推到其范围的极限,并且Sigmoids渐近地(渐渐地)渐近地接近那些极限:
幸运的是,有一种非常简单的方法来查看是否存在问题:只需重新缩放输出!
out = raw_out * 1.2 - 0.1
这将使您的理论输出超出预期的输出范围(在我的示例中为-0.1到1.1),达到0和1会更容易(实际上严格来说是可能的)。