NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python)
NEAT(NeuroEvolution of Augmenting Topologies, 增强拓扑的神经进化)属于神经进化算法家族,能够对神经网络的拓扑结构和连接权重进行进化。其用于进化复杂人工神经网络(Artificial neural networks, ANN),旨在通过在进化过程中逐步完善ANN的结构来减少参数搜索空间的维数。进化过程始于一群简单的基因组,并随着世代的发展逐渐增加其复杂性
NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python)
NEAT算法详解
NEAT算法概述
NEAT(NeuroEvolution of Augmenting Topologies, 增强拓扑的神经进化, NEAT)属于神经进化算法家族,能够对神经网络的拓扑结构和连接权重进行进化。其用于进化复杂人工神经网络(Artificial neural networks, ANN),旨在通过在进化过程中逐步完善ANN的结构来减少参数搜索空间的维数。进化过程始于一群小的、简单的基因组,并随着世代的发展逐渐增加其复杂性。
初始基因组具有非常简单的拓扑:仅表达输入
,输出
和偏置
神经元。开始时并就没有将隐藏节点引入初始基因组中,以确保对解(ANN的连接权重)的搜索在可能的最低维度参数空间中开始。在每一代中,都会可能引入其他基因,通过提供以前不存在的新维度来扩展解决方案的搜索空间。因此,进化始于在一个可以轻松优化的小空间中搜索,并在必要时增加新的维度。通过这种方法,可以逐步地逐步发现复杂的phenotype
(表型;解),这比在包含最终解的多维空间中直接启动搜索要高效得多。类似于:通过偶尔添加使表型更加复杂的新基因来完成自然进化的策略。在生物学中,这种逐步完善的过程称为物种形成(Speciation)
。
NEAT方法的主要目标是最大程度地减少基因组结构的复杂性-不仅是最终的解,而且还包括生物体的进化过程中所有中间代。因此,网络拓扑的演变通过减少搜索空间带来了显着的性能优势。例如,高维的最终解仅在进化过程结束时遇到。该算法的另一个重要特征是,引入到基因组中的每个结构都用于后代后续适应度评估,只有有用的结构才能在进化过程中生存。换句话说,基因组的结构复杂化总是带有明确的目标。
NEAT编码方案
NEAT采用的基因编码方案主要考虑:在交叉算子应用于两个亲本基因组的情况下,能够轻松匹配相应基因。NEAT基因组是编码的神经网络的连通性模式的表示,如以下NEAT基因组方案所示:
每个基因组都表示为连接基因的列表,该列表编码了神经网络节点之间的连接。另外,还有一些节点基因可以编码有关网络节点的信息,例如节点标识符,节点类型和激活函数的类型。连接基因编码网络链接的以下连接参数:
- 输入节点的标识符
- 输出节点的标识符
- 表征连接是否启用的标志位
- 一个创新编号,允许在重组过程中匹配基因
上图的底部以有向图的形式表示对应基因组所表达的网络拓扑结构。
结构突变
NEAT特有的变异运算符可以更改连接权重和网络的拓扑结构。结构突变主要有两种类型:
- 在节点之间添加新连接
- 将新节点添加到网络中
下图显示了NEAT算法的结构变异:
当将突变算子应用于NEAT基因组时,新添加的基因(连接基因或节点基因)将分配递增的创新编号。在进化过程中,种群中生物的基因组逐渐变大,并产生了大小不同的基因组。该过程导致不同的连接基因位于基因组内的相同位置,使得同源基因之间的匹配过程极为复杂。为了解决这一问题,NEAT在基因中引入了创新编号(innovation number)。当将突变算子应用于NEAT基因组时,新添加的基因(连接基因或节点基因)将分配递增的创新编号。
带有创新编号的交叉算子
在进化过程中,存在某些信息用于准确地告诉我们在拓扑多样化的种群中基因组之间匹配等位基因的匹配情况。在神经进化算法中,连接节点尽管可能具有不同的连接权重值,但具有相同历史起源的连接基因代表相同的结构。NEAT算法中基因的历史起源由递增分配的创新编号表示,这使我们能够追踪结构突变的时间顺序。
同时,在杂交过程中,后代从亲本基因组继承了带有创新编号的基因。因此,特定基因的创新编号永远不会改变,从而允许在交叉过程中匹配来自不同基因组的等位基因。能够匹配的基因具有相同的创新编号。如果创新编号不匹配,则该基因属于基因组的不相交(disjoint)
或多余(excess)
部分,具体取决于其创新编号是在另一亲本创新编号范围之内还是之外。不相交或多余的基因表示在另一个亲本的基因组中并不存在,并且在交换阶段需要特殊处理。因此,后代继承了具有相同创新编号的基因。这些是从父母之一中随机选择的。后代总是从适应度最高的父母那里继承不相交或多余的基因。此功能使NEAT算法可以使用线性基因组编码有效地执行基因重组,而无需进行复杂的拓扑分析。
下图显示了NEAT算法中的交叉(重组):
上图显示了使用NEAT算法的两个父级之间的交叉的示例。使用创新编号(连接基因细胞顶部的编号)对两个亲本的基因组进行比对。此后,当创新编号相同时,通过从任一亲本中随机选择连接基因来产生后代:创新编号为1到5的基因。最后,不相交和多余的基因是无条件地从亲本中添加的,并按创新编号排序。
物种形成(Speciation)
在进化过程中,生物可以世代相继创建各种拓扑,但是它们无法产生和维持自己的拓扑创新。较小的网络结构比较大的网络结构优化速度更快,添加新节点或新连接后,无意地减少了后代基因组的生存机会。因此,由于种群内生物体的适应度值暂时下降,新近增加的拓扑结构经历了负的进化压力。同时,从长远来看,新颖的拓扑可以引入创新,从而导致成功的解。为了解决适应度的暂时下降,在NEAT算法中引入了物种形成的概念。物种形成通过引入狭窄的生态位来限制可以交叉的生物的范围,在生态位中只有属于相同生态位的生物才相互竞争,而不是与种群中的所有生物竞争。通过划分种群来实现物种形成,从而使具有相似拓扑结构的生物属于同一物种。
物种形成算法:
NEAT方法允许创建能够解决各种控制优化问题以及其他无监督学习问题的复杂ANN。由于引入了通过逐步复杂化和物种形成来增强ANN拓扑的细节,因此这些解决方案倾向于优化训练和推理的性能。最终的ANN拓扑会不断增长,以匹配需要解决的问题,而不会引入任何过多的隐藏单元层。
NEAT算法实践——使用NEAT进行XOR问题求解器的优化
XOR问题介绍
经典的多层感知器(MLP)或人工神经网络(ANN)的拓扑中没有任何隐藏的单元,仅能正确解决线性可分离的问题。此类ANN配置无法用于模式识别以及异或问题任务。但是,对于更复杂的MLP架构,其中包括具有非线性激活函数(例如sigmoid)的隐藏单元,可以近似任何函数。因此,非线性可分离问题可用于研究神经进化过程是否可以在求解器表型的ANN中增长任意数量的隐藏单元。
XOR问题是经典计算机实验,如果不对求解器算法引入非线性操作,则无法解决。利用此问题可以证明:NEAT算法可以演化ANN的拓扑结构,从非常简单的拓扑开始,逐步增加复杂性,直到找到所有连接都合适的网络结构。XOR实验通过证明NEAT算法始终具有增加适当拓扑的能力,表明NEAT可以避免适应度值的局部最大值。
下表定义了XOR特征:
Input 1 | Input 2 | Input 3 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
XOR是一个二进制逻辑运算符,仅当两个输入中只有一个为true时才返回true。两个输入信号必须由非线性隐藏单元组合以产生正确的输出信号。对于XOR输入的组合,没有线性函数能够将它们正确地分类。
NEAT算法从初始种群开始,该种群编码一个非常简单的表型,并逐渐发展出该表型的拓扑结构,直到创建了合适的ANN。表型ANN的初始结构不包括任何隐藏的单元,而是由两个输入单元,一个输出单元和一个偏置单元组成。两个输入节点和偏置节点连接到输出节点,即初始基因型具有三个连接基因和四个节点基因。偏置单元是一种特殊类型的输入,始终将其初始化为大于0的特定值。如果我们想设置神经元单元的激活函数,那么偏置单元是必要的,它是由应用于输入和的相关激活函数计算的,如果两个输入的值都为0,则偏置为特定的非零值。
下图显示了初始和最小可能的XOR表型:
在找到最终解之前,表型的ANN变得越来越复杂。最小的解算器仅包含一个隐藏节点。
XOR问题的目标函数
在XOR实验中,种群中生物体的适应度定义为正确解与为所有四个XOR输入模式生成的输出总和之间的平方距离。计算如下:
- 针对所有四个XOR输入模式激活表型ANN。
- 从每个模式的正确答案中减去输出值,然后将结果的绝对值相加。
- 从最大适应度值(4)中减去在上一步中找到的误差值,以计算个体适应度。更高适应度值意味着更好的求解器性能。
- 然后对计算出的适应度求平方,以按比例为个体提供更高适应度,从而生成求解器ANN,为标准解提供更接近的正确解。这种方法可以增大进化的压力。
因此,目标函数可以定义如下:
f
=
(
4
−
∑
i
=
1
4
∣
y
i
−
A
N
N
(
x
1
i
,
x
2
i
)
∣
)
2
f=(4-\sum^4_{i=1}|y_i-ANN(x1_i,x2_i)|)^2
f=(4−i=1∑4∣yi−ANN(x1i,x2i)∣)2
基于NEAT-Python库的相应Python源代码如下:
# xor inputs and expected output values
xor_inputs = [(0.0,0.0),(0.0,1.0),(1.0,0.0),(1.0,1.0)]
xor_outputs = [(0.0,),(1.0,),(1.0,),(0.0,)]
def eval_fitness(net):
error_sum = 0.0
for xi,xo in zip(xor_inputs,xor_outputs):
output = net.activate(xi)
error_sum += abs(output[0]-xo[0])
# Calculate amplified fitness
fitness = (4 - error_sum) ** 2
return fitness
NOTE:
可以尝试不同的适应度得分计算方法。例如,可以实现类似于均方误差的函数,并将算法的性能与目标函数的不同实现进行比较。唯一的要求是目标函数应产生较高的适应度分数,以提供更好的求解器。
超参数
本文进行的XOR实验使用NEAT-Python库作为框架。NEAT-Python库定义了一组用于控制NEAT算法的执行和性能的超参数。配置文件以类似于Windows.INI文件的格式存储。每个部分均以方括号([section])开头,后跟以等号(=)分隔的键/值对。
超参数项详细描述参看NEAT-python官方介绍。
NEAT部分介绍
本部分包括特定于NEAT算法的参数,包括以下参数:
- fitness_criterion:根据种群中所有基因组的一组适应度值计算终止标准的函数。参数值是标准集合函数的名称,例如min,max和mean。如果总体的最小适应度或最大适应度超过给定的fitness_threshold,则最小值和最大值将用于终止进化过程。当该值设置为平均值时,种群的平均适应度用作终止标准。
- fitness_threshold:将阈值与fitness_criterion函数计算出的适应度值进行比较,以测试是否必须终止进化。
- no_fitness_termination:禁止由前面的参数定义的基于适应度的进化过程终止的标志。设置为True时,仅在评估了最大世代数之后,进化才会终止。
- pop_size:每代中生物的数量。
- reset_on_extinction:用于控制当当前世代中的所有物种由于停滞进化而灭绝时,是否应创建新的随机种群。如果为False,则在完全消失时将引发CompleteExtinctionException。
DefaultStagnation部分介绍
本部分定义了特定于物种停滞例程的参数,由DefaultStagnation类实现,包括以下参数:
- species_fitness_func:用于计算物种适应度(即计算属于特定物种的所有生物的适应度值)的函数的名称。允许的值为max,min和mean。
- max_stagnation:在超过max_stagnation世代数后,通过species_fitness_func计算出的适应度值没有显示出改善的物种被认为是停滞的,并可能灭绝。。
- species_elitism:无条件保护的物种数量,不受停滞影响。目的是防止在新物种出现之前,种群的彻底灭绝。尽管没有进一步提高适应度,但指定数量的适应度最高的物种始终在种群中生存。
DefaultReproduction部分介绍
本部分提供了由内置DefaultReproduction类实现的繁殖例程的配置,包括以下参数:
- elitism:每个物种中适应度最高的生物体的数量,无需改变即可复制到下一代。使用这个因子以保留前几代种群中发现的有益突变。
- survivor_threshold:每个物种中允许成为下一代亲本的生物的比例,即符合繁殖(交叉)的条件。通过调整该值,可以定义允许其参与繁殖过程的生物的最低适应度得分。
- min_species_size:每个物种在繁殖周期后应保留的最小生物数量。
DefaultSpeciesSet部分介绍
本部分提供了由内置DefaultSpeciesSet类实现的物种形成过程的配置,包括以下参数:
- compatibility_threshold:用于控制生物是属于同一物种(基因组距离小于该值)还是属于不同物种的阈值。较高的值表示进化过程的种群形成能力较小。
DefaultGenome部分介绍
本部分定义了用于创建和维护基因组的配置参数,由DefaultGenome类实现,包括以下参数:
- activation_default:在节点基因中使用的激活函数的名称。
- activation_mutate_rate:如果基因组支持多种激活函数(例如针对CPPN基因组),则这是该突变用支持的函数列表中的新节点替换了当前节点的激活函数的概率(请参阅activation_options )。
- activation_options:节点基因可以使用的激活函数的空格分隔列表。
- aggregation_default:默认聚合函数的名称,网络节点将使用该默认聚合函数来激活之前从其他节点接收到的任何聚合输入信号。
- aggregation_mutate_rate:如果基因组支持多个聚合函数,则此参数定义突变的概率,该概率用聚合函数列表中的新节点替换当前节点的聚合函数(请参阅aggregation_options)。
- aggregation_options:节点基因可以使用的聚合函数的空格分隔列表。支持的值为sum,min,max,mean,median和maxabs。
- compatibility_threshold:用于控制生物是属于同一物种(基因组距离小于该值)还是属于不同物种的阈值。较高的值表示进化过程的物种 形成能力较小。
- compatibility_disjoint_coefficient:在基因组距离计算过程中用来计算不相交或多余基因对计算结果的贡献程度的系数。此参数较高会放大基因组距离计算中不相交或多余的基因存在的重要性。
- compatibility_weight_coefficient:用于管理节点基因的偏置和响应属性之间的差异的基因组距离计算以及连接基因的权重属性对结果的贡献的系数。
- conn_add_prob:在现有节点基因之间引入新连接基因的突变概率。
- conn_delete_prob:从基因组中删除现有连接基因的突变概率。
- enabled_default:新创建的连接基因的enabled属性的默认值。
- enabled_mutate_rate:触发连接基因的启用属性的突变概率。
- feed_forward:控制在生成过程中生成的表型网络的类型。如果设置为True,则不允许重复连接。
- initial_connection:为新创建的基因组指定初始连接模式。允许的值包括unconnected,fs_neat_nohidden,fs_neat_hidden,full_direct,full_nodirect,partial_direct和partial_nodirect。
- node_add_prob:添加新节点基因的突变概率。
node_delete_prob:从基因组及其所有连接中删除现有节点基因的突变概率。 - num_hidden,num_inputs,num_outputs:初始种群基因组中隐藏,输入和输出节点的数量。
- single_structural_mutation:如果设置为True,则在演化过程中仅允许结构突变,即,仅允许添加或移除节点或连接。
XOR实践采用超参数
[NEAT]
fitness_criterion = max
fitness_threshold = 15.5
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_direct
# node add/remove rates
node_add_prob = 0.2
node_delete_prob = 0.2
# The network parameters
num_hidden = 0
num_inputs = 2
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.0
weight_min_value = -30.0
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
min_species_size = 4
XOR实践运行
选择NEAT-Python库作为编写代码的框架。
环境配置
依赖库包括:
- neat-python
- matplotlib
- graphviz
- python-graphviz
用于可视化的源代码visualize.py
from __future__ import print_function
import copy
import warnings
import graphviz
import matplotlib.pyplot as plt
import numpy as np
def plot_stats(statistics,ylog=False,view=False,filename='avg_fitness.svg'):
"""
plots the population's average and best fitness.
"""
if plt is None:
warnings.warn("This display is not available due to a missing optional dependency (matplotlib)")
return
generation = range(len(statistics.most_fit_genomes))
best_fitness = [c.fitness for c in statistics.most_fit_genomes]
avg_fitness = np.array(statistics.get_fitness_mean())
stdev_fitness = np.array(statistics.get_fitness_stdev())
plt.plot(generation,avg_fitness,'b-',label='average')
plt.plot(generation,avg_fitness-stdev_fitness,'g-.',label='-1 sd')
plt.plot(generation,avg_fitness+stdev_fitness,'g-',label='+1 sd')
plt.plot(generation,best_fitness,'r-',label='best')
plt.title("Population's average and best fitness")
plt.xlabel("Generation")
plt.ylabel("Fitness")
plt.grid()
plt.legend(loc="best")
if ylog:
plt.gca().set_yscale('symlog')
plt.savefig(filename)
if view:
plt.show()
plt.close()
def plot_species(statistics,view=False,filename='speciation.svg'):
"""
Visualizes speciation throughout evolution.
"""
if plt is None:
warnings.warn("This display is not available due to a missing optional dependency (matplotlib)")
return
species_sizes = statistics.get_species_sizes()
num_generations = len(species_sizes)
curves = np.array(species_sizes).T
fig,ax = plt.subplots()
ax.stackplot(range(num_generations),*curves)
plt.title("Speciation")
plt.ylabel("Size per Species")
plt.xlabel("Generations")
plt.savefig(filename)
if view:
plt.show()
plt.close()
def draw_net(config,genome,view=False,filename=None,directory=None,
node_names=None,show_disabled=True,prune_unused=False,
node_colors=None,fmt='svg'):
"""
Receives a genome and draws a neural network with arbitrary topology.
"""
# Attributes for network nodes.
if graphviz is None:
warnings.warn("This display is not available due to a missing optional dependency (graphviz")
return
if node_names is None:
node_names = {}
assert type(node_names) is dict
if node_colors is None:
node_colors = {}
assert type(node_colors) is dict
node_attrs = {
'shape': 'circle',
'fontsize': '9',
'height': '0.2',
'width': '0.2'
}
dot = graphviz.Digraph(format=fmt,node_attr=node_attrs)
inputs = set()
for k in config.genome_config.input_keys:
inputs.add(k)
name = node_names.get(k,str(k))
input_attrs = {'style':'filled','shape':'box','fillcolor':node_colors.get(k,'lightgray')}
dot.node(name,_attributes=input_attrs)
outputs = set()
for k in config.genome_config.output_keys:
outputs.add(k)
name = node_names.get(k,str(k))
node_attrs = {
'style':'filled',
'fillcolor':node_colors.get(k,'lightgray')
}
if prune_unused:
connections = set()
for cg in genome.connections.values():
if cg.enabled or show_disabled:
connections.add((cg.in_node_id,cg._out_node_id))
used_nodes = copy.copy(outputs)
pending = copy.copy(outputs)
while pending:
new_pending = set()
for a,b in connections:
for b in pending and a not in used_nodes:
new_pending.add(a)
used_nodes.add(a)
pending = new_pending
else:
used_nodes = set(genome.nodes.keys())
for n in used_nodes:
if n in inputs or n in outputs:
continue
attrs = {
'style':'filled',
'fillecolor':node_colors.get(n,'while')
}
dot.node(str(n),_attributes=attrs)
for cg in genome.connections.values():
if cg.enabled or show_disabled:
# if cg.input not in used_nodes or cg.output not in used_nodes:
# continue
input_node,output_node = cg.key
a = node_names.get(input_node,str(input_node))
b = node_names.get(output_node,str(output_node))
style = ' solid' if cg.enabled else 'dotted'
color = 'green' if cg.weight > 0 else 'red'
width = str(0.1 + abs(cg.weight / 5.0))
dot.edge(a,b,_attributes={'style':style,'color':color,'penwidth':width})
dot.render(filename,directory,view=view)
return dot
XOR实践源代码xor_experiment详解
- 首先,需要导入将在以后使用的库:
# The Python standard library import
import os
# The NEAT-Python library imports
import neat
# The helper used to visualize experiment results
import visualize
- 接下来,需要编写适应度评估代码:
# The XOR inputs and expected corresponding outputs for fitness evaluate
xor_inputs = [(0.0,0.0),(0.0,1.0),(1.0,0.0),(1.0,1.0)]
xor_outputs = [(0.0,),(1.0,),(1.0,),(0.0,)]
def eval_fitness(net):
"""
Evaluates fitness of the genome that was used to generate provided net
Arguments:
net: The feed-forward neural network generated from genome
Returns:
The fitness score - the higher score the means the better fit organism.
Maximal score: 16.0
"""
error_sum = 0.0
for xi,xo in zip(xor_inputs,xor_outputs):
output = net.activate(xi)
error_sum += abs(output[0] - xo[0])
# calculate amplified fitness
fitness = (4 - error_sum) ** 2
return fitness
- 使用适应度评估函数,编写函数来评估当前一代中的所有生物,并相应地更新每个基因组的适应度:
def eval_genomes(genomes,config):
"""
The fitness to evaluate the fitness of each genome in the genomes list.
The provided configuration is used to create feed-forward neural network from each
genome and after that created the neural neural network evaluated in its ability to
solve XOR problem.
As a result of that function execution, the fitness score of each genome updated to
the newly one.
"""
for genome_id,genome in genomes:
genome.fitness = 4.0
net = neat.nn.FeedForwardNetwork.create(genome,config)
genome.fitness = eval_fitness(net)
- run_experiment函数从配置文件加载超参数配置,并创建初始基因组种群:
# 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 stdut 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,filename_prefix='out/neat-checkpoint-'))
- 之后,通过提供eval_genome函数,运行神经进化300代,该函数用于评估每个世代群体中每个基因组的适应度评分,直到找到解或该过程达到最大世代数为止:
# Run for up to 300 generations
best_genome = p.run(eval_genomes,300)
- 当NEAT算法的执行由于成功或达到最大世代数而停止执行时,将返回最适合的基因组。可以检查该基因组是否是所求解:
# Check if the best genome is an adequate XOR solor
best_genome_fitness = eval_fitness(net)
if best_genome_fitness > config.fitness_threshold:
print("\n\nSUCCESS: The XOR problem solver found!!!")
else:
print("\n\nFAILURE: Failed to find XOR problem solver!!!")
8.最后,可以将收集到的统计数据和最适合的基因组可视化,以探索神经进化过程的结果:
# Visualize the experiment result
node_names = {-1:'A',-2:'B',0:'A XOR B'}
visualize.draw_net(config,best_genome,True,node_names=node_names,directory=output_dir)
visualize.plot_stats(stats,ylog=False,view=True,filename=os.path.join(output_dir,'avg_fitness.svg'))
visualize.plot_species(
stats,view=True,filename=os.path.join(output_dir,'speciation.svg')
)
xor_experiment.py完整代码
import os
import shutil
import neat
import visualize
# The current working directory
local_dir = os.path.dirname(__file__)
# The directory to store outputs
output_dir = os.path.join(local_dir,'out')
# The XOR inputs and expected corresponding outputs for fitness evaluate
xor_inputs = [(0.0,0.0),(0.0,1.0),(1.0,0.0),(1.0,1.0)]
xor_outputs = [(0.0,),(1.0,),(1.0,),(0.0,)]
def eval_fitness(net):
"""
Evaluates fitness of the genome that was used to generate provided net
Arguments:
net: The feed-forward neural network generated from genome
Returns:
The fitness score - the higher score the means the better fit organism.
Maximal score: 16.0
"""
error_sum = 0.0
for xi,xo in zip(xor_inputs,xor_outputs):
output = net.activate(xi)
error_sum += abs(output[0] - xo[0])
# calculate amplified fitness
fitness = (4 - error_sum) ** 2
return fitness
def eval_genomes(genomes,config):
"""
The fitness to evaluate the fitness of each genome in the genomes list.
The provided configuration is used to create feed-forward neural network from each
genome and after that created the neural neural network evaluated in its ability to
solve XOR problem.
As a result of that function execution, the fitness score of each genome updated to
the newly one.
"""
for genome_id,genome in genomes:
genome.fitness = 4.0
net = neat.nn.FeedForwardNetwork.create(genome,config)
genome.fitness = eval_fitness(net)
def run_experiment(config_file):
"""
The function to run XOR experiment against hyper-parameters
defined in the provided configuration file.
The winner genome will be rendered as a graph as well as the
important statistics of neuroevolution process execution.
Arguments:
config_file: the path to the file with experiment configuration
"""
# 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 stdut 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,filename_prefix='out/neat-checkpoint-'))
# Run for up to 300 generations
best_genome = p.run(eval_genomes,300)
# Display the best genome among generations
print('\nBest genome: {!s}'.format(best_genome))
# show output of the most fit genome against training data
print('\nOutput:')
net = neat.nn.FeedForwardNetwork.create(best_genome,config)
for xi,xo in zip(xor_inputs,xor_outputs):
output = net.activate(xi)
print("input {!r}, expected output {!r}, got {!r}".format(xi,xo,output))
# Check if the best genome is an adequate XOR solor
best_genome_fitness = eval_fitness(net)
if best_genome_fitness > config.fitness_threshold:
print("\n\nSUCCESS: The XOR problem solver found!!!")
else:
print("\n\nFAILURE: Failed to find XOR problem solver!!!")
# Visualize the experiment result
node_names = {-1:'A',-2:'B',0:'A XOR B'}
visualize.draw_net(config,best_genome,True,node_names=node_names,directory=output_dir)
visualize.plot_stats(stats,ylog=False,view=True,filename=os.path.join(output_dir,'avg_fitness.svg'))
visualize.plot_species(
stats,view=True,filename=os.path.join(output_dir,'speciation.svg')
)
def clean_output():
if os.path.isdir(output_dir):
# remove files from previous run
shutil.rmtree(output_dir)
# create the output directory
os.makedirs(output_dir,exist_ok=False)
if __name__ == '__main__':
# Determine path to configuration file.
# This path manipulation is here so that the script will run successfully regardless
# of the current working directory.
config_path = os.path.join(local_dir,'xor_config.ini')
# clean results of previous run if any or init the output directory
clean_output()
# run the experiment
run_experiment(config_path)
代码运行与分析
运行代码:
python3 xor_experiment.py
代码运行期间,显示中间结果,输出如下:
****** Running generation 242 ******
Population's average fitness: 6.36101 stdev: 2.54992
Best fitness: 13.14907 - size: (2, 5) - species 74 - id 27424
Average adjusted fitness: 0.291
Mean genetic distance 4.234, standard deviation 2.219
Population of 153 members in 22 species:
ID age size fitness adj fit stag
==== === ==== ======= ======= ====
13 188 10 9.0 0.401 175
30 163 6 9.0 0.352 11
44 125 9 9.0 0.232 1
57 85 4 9.0 0.401 14
61 83 11 9.0 0.245 0
63 72 8 9.0 0.343 8
67 54 12 8.9 0.185 0
68 51 9 9.0 0.366 3
69 47 6 4.1 0.062 1
70 47 5 9.0 0.315 0
71 46 6 9.0 0.278 3
72 41 3 9.0 0.444 4
73 38 6 9.0 0.249 0
74 36 8 13.1 0.403 1
75 31 9 9.0 0.315 3
76 30 9 9.0 0.441 0
77 27 8 9.0 0.379 0
78 12 7 9.0 0.403 0
79 3 4 7.8 0.191 2
80 3 8 4.0 0.057 2
81 2 2 4.0 0.057 1
82 0 3 -- -- 0
Total extinctions: 0
Generation time: 0.101 sec (0.089 average)
第208代的总体平均适应度(6.36101)与配置文件中设置的完成标准(fitness_threshold = 15.5)相比要低得多。但是,有一些潜在的冠军物种(ID:74,适应度: 13.14907)正在通过进化来达到目标适应度阈值,这编码了一个由2个节点和5个连接(size为(2,5))组成的ANN表型。
物种包括153个个体,分为22个物种,具有以下特征:
- id是物种标识符。
- age是物种的年龄,即从物种创建到现在的世代数。
- size是属于该物种的个体数量。
- fitness是根据其个体计算出来的物种适应度得分(实验采用max值)。
- adj fit(适应度调整)是指已针对整个种群的适应度得分进行了调整的特定物种的适应度。
- stag是特定物种的停滞年龄,是自该物种上次适应度提高以来的世代数。
当通过NEAT算法找到合适的XOR求解器时,将显示以下输出:
****** Running generation 270 ******
Population's average fitness: 6.20279 stdev: 2.81250
Best fitness: 15.75097 - size: (2, 5) - species 73 - id 30706
Best individual in generation 270 meets fitness threshold - complexity: (2, 5)
Best genome: Key: 30706
Fitness: 15.750968156147557
Nodes:
0 DefaultNodeGene(key=0, bias=-0.7847091156509401, response=1.0, activation=sigmoid, aggregation=sum)
2413 DefaultNodeGene(key=2413, bias=-2.1180165773346697, response=1.0, activation=sigmoid, aggregation=sum)
Connections:
DefaultConnectionGene(key=(-2, 0), weight=-3.0021580093741296, enabled=True)
DefaultConnectionGene(key=(-2, 2413), weight=4.160190460681551, enabled=True)
DefaultConnectionGene(key=(-1, 0), weight=1.669978800496584, enabled=True)
DefaultConnectionGene(key=(-1, 2413), weight=-5.6712360597088916, enabled=True)
DefaultConnectionGene(key=(2413, 0), weight=8.950256543847727, enabled=True)
Output:
input (0.0, 0.0), expected output (0.0,), got [0.019408982960993575]
input (0.0, 1.0), expected output (1.0,), got [0.9999999999938545]
input (1.0, 0.0), expected output (1.0,), got [0.9881832299672123]
input (1.0, 1.0), expected output (0.0,), got [2.5306068238951827e-05]
SUCCESS: The XOR problem solver found!!!
DefaultNodeGene保存有bias值,激活函数的名称以及用于聚合每个节点上的输入的函数的名称。DefaultConnectionGene提供源节点和目标节点的ID,以及相关的连接权重。
Output表示接收四个输入数据对时,由物种优胜者表型的ANN产生的输出值。
Output目录还包含成功的XOR求解器的ANN拓扑图:
带有经过几代进化的适应度变化统计信息的图也被保存到Output目录:
Output目录还包含物种形成图,如下所示:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)