Paddle-NEAT——飞桨进化神经网络组件
目录
写在前面:
最近自己写了个把 neat-python 和 paddlepaddle 深度学习框架相结合的套件,取名叫 Paddle-NEAT。链接会在下面的安装方式处给出。
该套件涉及到了机器学习的另一个方向——遗传算法与进化策略。但本文的重点是介绍其中的一个分支——NEAT(NeuroEvolution of Augmenting Topologies)
遗传算法与进化策略可以跳脱出梯度的限制(因为不以梯度作为更新的依据),在高并行的计算条件下,说不定会有超越深度学习的效果。
噢对了,现在已经有将遗传算法与 NAS 相结合的文章了——Genetic CNN。
注意:本文的代码环境基于 Jupyter Notebook
NEAT 简介
遗传算法是个通用的框架,因此需要根据具体的问题来定义遗传算法
整体框架可分为三部分:交叉、变异与适应度
我们的 NEAT 将神经元及其连接定义成基因组
基因组的表示
从上图可以看出,节点变量分为节点变量与连接变量两种。其中节点变量包括了节点属性等变量,连接变量则包括了输入节点、输出节点、权重与连接是否可行等属性。
通过将节点与连接各列成一张链表,网络计算时查询即可得到输出。
基因组的变异
同样的,对应节点与连接两种基因组组成,变异类型也有结点变异和链接变异两种
节点变异
在连接列表中随机选择一个连接,然后中间加入一个节点
连接变异
随机选择两个不同的节点,增加一段连接
基因组的交叉
我们的基因组如何交叉呢?
首先,我们要把父母双方的基因先按顺序铺开对齐,如果是双方都有的连接id(称为 innovation),那就随机选择一个;
如果有不匹配的基因,那么继承具有更好 fitness 的一方;
如果双方的 fitness 相同,那就随便选了。
最后稍微介绍一下 NEAT 的两种改进形式:
HyperNEAT
HyperNEAT是对NEAT的扩展,它用一个单独的网络(称为CPPN,用于合成图案生成网络)间接地编码网络(称为衬底)的权重
Adaptive HyperNEAT
Adaptive HyperNEAT是HyperNEAT的一个扩展,它间接地对初始权值和权值的更新规则进行编码,以便在网络的“生命周期”内进行一些学习
运行Paddle-NEAT
安装
!pip install git+https://github.com/AgentMaker/Paddle-NEAT.git
!pip install neat-python
本套件底层仍然是 neat-python,但某些算子可以借由 paddle2.0 实现加速
网络配置与节点信息参照下面的 neat-python 中的介绍
neat-python 链接:
https://github.com/CodeReclaimers/neat-python
继续来立我们的棍子吧
%%writefile cart_pole.py
import os
import click
import gym
import neat
from paddle_neat.multi_env_eval import MultiEnvEvaluator
from paddle_neat.neat_reporter import LogReporter
from paddle_neat.recurrent_net import RecurrentNet
max_env_steps = 200
# 创建环境
def make_env():
return gym.make("CartPole-v0")
# 创建网络
def make_net(genome, config, bs):
return RecurrentNet.create(genome, config, bs)
# 定义输出层
def activate_net(net, states):
outputs = net.activate(states).numpy()
return outputs[:, 0] > 0.5
@click.command()
@click.option("--n_generations", type=int, default=100)
def run(n_generations):
config_path = os.path.join(os.path.dirname(__file__), "neat_cart_pole.cfg")
config = neat.Config(
neat.DefaultGenome,
neat.DefaultReproduction,
neat.DefaultSpeciesSet,
neat.DefaultStagnation,
config_path,
)
evaluator = MultiEnvEvaluator(
make_net, activate_net, make_env=make_env, max_env_steps=max_env_steps
)
def eval_genomes(genomes, config):
for _, genome in genomes:
genome.fitness = evaluator.eval_genome(genome, config)
pop = neat.Population(config)
stats = neat.StatisticsReporter()
pop.add_reporter(stats)
reporter = neat.StdOutReporter(True)
pop.add_reporter(reporter)
logger = LogReporter("neat.log", evaluator.eval_genome)
pop.add_reporter(logger)
pop.run(eval_genomes, n_generations)
if __name__ == "__main__":
run()
运行一下
!python cart_pole.py
当然走一下迷宫也是可以滴
%%writefile maze.py
import multiprocessing
import os
import click
import neat
import paddle
import numpy as np
import sys
sys.path.append("..")
from paddle_neat import t_maze
from paddle_neat.activations import tanh_activation
from paddle_neat.adaptive_linear_net import AdaptiveLinearNet
from paddle_neat.multi_env_eval import MultiEnvEvaluator
from paddle_neat.neat_reporter import LogReporter
batch_size = 4
DEBUG = True
def make_net(genome, config, _batch_size):
input_coords = [[-1.0, 0.0], [0.0, 0.0], [1.0, 0.0], [0.0, -1.0]]
output_coords = [[-1.0, 0.0], [0.0, 0.0], [1.0, 0.0]]
return AdaptiveLinearNet.create(
genome,
config,
input_coords=input_coords,
output_coords=output_coords,
weight_threshold=0.4,
batch_size=batch_size,
activation=tanh_activation,
output_activation=tanh_activation,
)
def activate_net(net, states, debug=False, step_num=0):
if debug and step_num == 1:
print("\n" + "=" * 20 + " DEBUG " + "=" * 20)
print(net.delta_w_node)
print("W init: ", net.input_to_output.numpy()[0])
outputs = net.activate(states).numpy()
if debug and (step_num - 1) % 100 == 0:
print("\nStep {}".format(step_num - 1))
print("Outputs: ", outputs[0])
print("Delta W: ", net.delta_w.numpy()[0])
print("W: ", net.input_to_output.numpy()[0])
return np.argmax(outputs, axis=1)
@click.command()
@click.option("--n_generations", type=int, default=10000)
@click.option("--n_processes", type=int, default=1)
def run(n_generations, n_processes):
config_path = os.path.join(os.path.dirname(__file__), "neat_maze.cfg")
config = neat.Config(
neat.DefaultGenome,
neat.DefaultReproduction,
neat.DefaultSpeciesSet,
neat.DefaultStagnation,
config_path,
)
envs = [t_maze.TMazeEnv(init_reward_side=i, n_trials=100) for i in [1, 0, 1, 0]]
evaluator = MultiEnvEvaluator(
make_net, activate_net, envs=envs, batch_size=batch_size, max_env_steps=1000
)
if n_processes > 1:
pool = multiprocessing.Pool(processes=n_processes)
def eval_genomes(genomes, config):
fitnesses = pool.starmap(
evaluator.eval_genome, ((genome, config) for _, genome in genomes)
)
for (_, genome), fitness in zip(genomes, fitnesses):
genome.fitness = fitness
else:
def eval_genomes(genomes, config):
for i, (_, genome) in enumerate(genomes):
try:
genome.fitness = evaluator.eval_genome(
genome, config, debug=DEBUG and i % 100 == 0
)
except Exception as e:
print(genome)
raise e
pop = neat.Population(config)
stats = neat.StatisticsReporter()
pop.add_reporter(stats)
reporter = neat.StdOutReporter(True)
pop.add_reporter(reporter)
logger = LogReporter("log.json", evaluator.eval_genome)
pop.add_reporter(logger)
winner = pop.run(eval_genomes, n_generations)
print(winner)
final_performance = evaluator.eval_genome(winner, config)
print("Final performance: {}".format(final_performance))
generations = reporter.generation + 1
return generations
if __name__ == "__main__":
run()
运行一下
!python maze.py