莫神的链接:https://mofanpy.com/tutorials/machine-learning/evolutionary-algorithm/neat-reinforcement-learning/
NEAT
简单来说, NEAT 有几个关键步骤,
- 使用
创新号码 (Innovation ID)
对神经网络的直接编码 (direct coding)
- 根据 innovation ID 进行
交叉配对 (crossover)
- 对
神经元 (node)
,神经链接 (link)
进行基因突变 (mutation)
- 尽量保留
生物多样性 (Speciation)
(有些不好的网络说不定突然变异成超厉害的) - 通过初始化只有 input 连着 output 的神经网络来尽量减小神经网络的大小 (从最小的神经网络结构开始发展)
我们再来具体看看他是怎么 搭建/交叉/变异 神经网络的. 之后的用图都是上面提到的 paper 中的.
上面的图你可以想象成就是我们如何通过 DNA (图中的 Genome) 来编译出神经网络的. Node genes
很简单就是神经网络每个节点的定义. 哪些是输入, 哪些输出, 哪些是隐藏节点. Connect. Genes
则是对于每一个节点与节点的链接是什么样的形式, 从输入节点 (In) 到输出节点 (Out), 这个链接的参数 (weight) 是多少. 输出节点的值就是 Out = In * weight
. 然后这条链接是要被使用 (Enabled) 还是不被使用 (DISAB). 最后就是这条链接专属的 创新号 (Innov)
通过上面的 Genome 我们就能搭建出那个神经网络了, 可以看出我们有一个 2-5 DISAB
的链接, 原因就是在2-5之间我们已经变异出了一个4节点. 所以2-5 是通过 4 相链接的, 这样我们就需要将原来的 2-5 链接 disable 掉.
关于变异呢. 我们可以有 节点变异
和 链接变异
, 就和上图一样, 这个简单, 大家都看得出来. 但是要提的一点是, 如果新加的节点像 6 那样, 是在原有链接上的突变节点, 那么原来的 3-5 链接就要被 disable 掉.
再来就是 crossover
了, 两个神经网络 交配 啦. 这时你就发现原来 innovation number 在这里是这么重要. 两个父母通过 innovation number 对齐, 双方都有的 innovation, 我们就随机选一个, 如果双方有个方没有的 Innovation, 我们就直接全部遗传给后代.
之所以图上还出现了 disjoint 和 excess 的基因, 是因为在后面如果要区分种群不同度, 来选择要保留的种群的时候, 我们需要通过这个来计算, 计算方式我就不细提了, 大家知道有这么一回事就行.
好了, 通过上面的方式一步步进行, 好的神经网络被保留, 坏的杀掉. 我们的神经网络就能朝着正确的方形进化啦.
安装neat-python以及graphviz
在terminal直接输入即可:
neat-python: pip install neat-python
windows 安装graphviz:[(36条消息) 解决failed to execute ‘dot’, ‘-Tsvg’], make sure the Graphviz executables are on your systems_坤斤拷的博客-CSDN博客
例子
接着我们来说说 neat-python 网页上的一个使用例子, 用 neat 来进化出一个神经网络预测 XOR 判断. 什么是 XOR 呢, 简单来说就是 OR 判断的改版.
- 输入 True, True, 输出 False
- 输入 False, True, 输出 True
- 输入 True, False, 输出 True
- 输入 False, False 输出 False
在例子当中, 我们用这样的形式来代替要学习的 input 和 output:
import os
import neat
# visualize只是有一些出图的功能
import visualize
# 2-input XOR inputs and expected outputs.
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,)]
那怎么样来评价每个个体的适应度 (fitness), 或者说他的预测得分高低呢. 我们就对每个个体评分. 如果4个 xor 判断都预测对了就得4分, 用平方差来计算越策错的. 下面的 function 中就是根据每个 genome
(DNA), 生成一个神经网络, 用这个神经网络预测, 再对这个 genome
打分, 并写入成它的 fitness
.
评估基因好不好,在于如何定义这个功能
def eval_genomes(genomes, config):
for genome_id, genome in genomes: # for each individual
genome.fitness = 4.0 # 4 xor evaluations
# 用neat生成一个神经网络定义为net
net = neat.nn.FeedForwardNetwork.create(genome, config)
for xi, xo in zip(xor_inputs, xor_outputs):
# 神经网络的输入
output = net.activate(xi)
# 输入输出之间的差距来计算fitness
genome.fitness -= (output[0] - xo[0]) ** 2
每一个 neat 的程序里有需要有这样的评分标准. 接着我们创建一个 config 的文件, 用来给定所有运行参数. 这个 config 文件要分开存储, 而且文件里要有一下几个方面的参数预设. 对于每个方面具体的预设值请参考我在 github 中的[config-forward](https://github.com/MorvanZhou/Evolutionary-Algorithm/blob/master/tutorial-contents/Using Neural Nets/NEAT/config-feedforward)这个文件. 对于每个方面的解释, 不太明白的话, 请参考这里
[NEAT]
[DefaultGenome]
[DefaultSpeciesSet]
[DefaultStagnation]
[DefaultReproduction]
现在我们就能使用这些预设的参数来生成一个 config
的值了 (上面的 eval_genomes
也用到了这个 config
).
有了这个 config
, 我们就能拿它来生成我们整个 population
, 使用这个初始的 p
来训练 300 次, 注意在 config-forward
中我们设置了一个参数 fitness_threshold = 3.9
, 就是说, 只要有任何一个 fitness 达到了 3.9 (最大4), 我们就停止迭代更新 population
. 所以有可能不到 300 次就学好了. 学好之后, 我们输出表现最好的 winner
.
最主要的过程就完啦, 简单吧. 在这个[例子脚本](https://github.com/MorvanZhou/Evolutionary-Algorithm/blob/master/tutorial-contents/Using Neural Nets/NEAT/run_xor.py)中的其他代码都是现实结果的代码, 大家随便看看就知道了.
def run(config_file):
# Load configuration.
# config_file文件中设置好了的
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(50))
# Run for up to 300 generations.
# run的是怎么样计算fitness
# 把fitness当作一个函数放到p里面去
winner = p.run(eval_genomes, 300)
# Display the winning genome.
print('\nBest genome:\n{!s}'.format(winner))
# Show output of the most fit genome against training data.
print('\nOutput:')
# 将训练过后的winner的模型再次代入,得到output
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = winner_net.activate(xi)
print("input {!r}, expected output {!r}, got {!r}".format(xi, xo, output))
node_names = {-1:'A', -2: 'B', 0:'A XOR B'}
visualize.draw_net(config, winner, True, node_names=node_names)
visualize.plot_stats(stats, ylog=False, view=True)
visualize.plot_species(stats, view=True)
p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-49')
# 再繁衍十代
p.run(eval_genomes, 10)
执行文件代码:
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.
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, 'config-feedforward')
run(config_path)
我们通过这个来输出最后的 winner
神经网络预测结果, 不出意外, 你应该预测很准. 最后通过 visualize.py
[文件的可视化功能](https://github.com/MorvanZhou/Evolutionary-Algorithm/blob/master/tutorial-contents/Using Neural Nets/NEAT/visualize.py), 我们就能生成几个图片, 使用浏览器打开 speciation.svg
看看不同种群的变化趋势, avg_fitness.svg
看看 fitness 的变化曲线, Digraph.gv.svg
看这个生成的神经网络长怎样.
关于最下面的那个神经网络图, 需要说明一下, 如果是实线, 如 B->1, B->2, 说明这个链接是 Enabled 的. 如果是虚线(点线), 如 B->A XOR B 就说明这个链接是 Disabled 的. 红色的线代表 weight <= 0, 绿色的线代表 weight > 0. 线的宽度和 weight 的大小有关.
NEAT 强化学习
我们见到了使用 NEAT 来进化出一个类似于监督学习中的神经网络, 这次我们用 NEAT 来做强化学习 (Reinforcement Learning), 这个强化学习可是没有反向传播的神经网络哦, 有的只是一个不断进化 (还可能进化到主宰人类) 的神经网络!! (哈哈, 骗你的, 因为每次提到在电脑里进化, 联想到科幻片, 我就激动!)
立杆子的机器人最后学习的效果提前看:
这个机器人的神经网络长这样:
gym 模拟环境
OpenAI gym 应该算是当下最流行的 强化学习练手模块了吧. 它有超级多的虚拟环境可以让你 plugin 你的 python 脚本.
安装 gym 的方式也很简单, 大家可以直接参考我在之前做 强化学习 Reinforcement learning 教程中的这节内容, 简单的介绍了如何安装 Gym. 如果还是遇到了问题, 这里或许能够找到答案.
CartPole 进化吧
在 neat 的 config
[文件](https://github.com/MorvanZhou/Evolutionary-Algorithm/blob/master/tutorial-contents/Using Neural Nets/NEAT_gym/config)中, 我想提到的几个地方是:
fitness_criterion = max # 按照适应度最佳的模式选个体
# 为了一直立杆子下去, 这一个封顶值设置成永远达不到,
# 具体看我在 eval_genomes 中如何计算 fitness 的
fitness_threshold = 2.
activation_default = relu # 我挑选的 激活函数
# network 输入输出个数
num_hidden = 0
num_inputs = 4
num_outputs = 2
有了这个 config
文件里面的信息, 我们就能创建网络和评估网络了. 和上次一样, 下面的功能对每一个个体生成一个神经网络, 然后把这个网络放在立杆子游戏中玩, 一个 generation 中我们对每一个 genome
的 net
测试 GENERATION_EP
这么多回合, 然后最后挑选这么多回合中总 reward
最少的那个回合当成这个 net
的 fitness
(你可以想象这是木桶效应, 整体的效应取决于最差的那个结果). 然后要注意的是, net.activate()
output 的是动作的值. 然后我们挑选一个值最大的动作.
def eval_genomes(genomes, config):
for genome_id, genome in genomes:
# 产生一个神经网络
net = neat.nn.FeedForwardNetwork.create(genome, config)
ep_r = []
# 将产生的net放到游戏当中去玩耍
# 玩十个回合,取十个回合fitness的平均数
for ep in range(GENERATION_EP): # run many episodes for the genome in case it's lucky
accumulative_r = 0. # stage longer to get a greater episode reward
# 以下是强化学习的基本步骤
# 提供一个observation
observation = env.reset()
for t in range(EP_STEP):
# output
action_values = net.activate(observation)
# 向右0.6 向左0.4 那我选择向右的
action = np.argmax(action_values)
# 更新迭代,奖励,掉,掉了就下一回合
observation_, reward, done, _ = env.step(action)
# 累积奖励
accumulative_r += reward
if done:
break
observation = observation_
ep_r.append(accumulative_r)
# 取十个回合的最小值作为fitness
genome.fitness = np.min(ep_r)/float(EP_STEP) # depends on the minimum episode reward
不知道大家看到这里有没有想过, 如果我们能并行运算该多好. 所以, 我亲测失败. 原因是, gym
+ neat
的环境不方便运行 multiprocessing
. 如果你想多线程的话, 可以考虑使用 threading
, 不过不保证效率有提高. 想知道为什么的话, 请看这里.
接下来我们就开始写 run
里面的内容了, 创建种群, 繁衍后代, 适者生存, 不适者淘汰.
def run():
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation, CONFIG)
pop = neat.Population(config)
# recode history-->记录结果
stats = neat.StatisticsReporter()
pop.add_reporter(stats)
pop.add_reporter(neat.StdOutReporter(True))
pop.add_reporter(neat.Checkpointer(5))
pop.run(eval_genomes, 10) # train 10 generations
# visualize training
visualize.plot_stats(stats, ylog=False, view=True)
visualize.plot_species(stats, view=True)
最后我们挑选一下保存的 checkpoint
文件, 展示出最强神经网络的样子吧.
def evaluation():
p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-%i' % CHECKPOINT)
winner = p.run(eval_genomes, 1) # find the winner in restored population
# show winner net
node_names = {-1: 'In0', -2: 'In1', -3: 'In3', -4: 'In4', 0: 'act1', 1: 'act2'}
visualize.draw_net(p.config, winner, True, node_names=node_names)
net = neat.nn.FeedForwardNetwork.create(winner, p.config)
while True:
s = env.reset()
while True:
env.render()
a = np.argmax(net.activate(s))
s, r, done, _ = env.step(a)
if done: break
这串代码的结果就是这节内容最上面的那个视频效果啦. winner
的神经网络进化成这样了. 不过你的生成的神经网络可能并不是长这样. 有时候还可能某个 input
都没有使用到. 就说明这个 input
的效用可能并不大.
如果是实线, 如 B->1, B->2, 说明这个链接是 Enabled 的. 如果是虚线(点线), 如 B->A XOR B 就说明这个链接是 Disabled 的. 红色的线代表 weight <= 0, 绿色的线代表 weight > 0. 线的宽度和 weight 的大小有关.