NEAT(基於NEAT-Python模組)實現監督學習和強化學習
NEAT (NeuroEvolution of Augmenting Topologies) 是一種遺傳演算法,能夠對神經網路的引數和形態進行進化。
NEAT(NeuroEvolution of Augmenting Topologies)是一種建立人工神經網路的進化演算法。想要詳細瞭解該演算法,可以閱讀Stanley’s paper在他的網站(http://www.cs.ucf.edu/~kstanley/#publications)。
如果只想瞭解演算法的要點,只需要讀幾篇早期的NEAT論文是一個好的建議。大多數比較短並且很好的解釋了概念。最初的NEAT論文(http://nn.cs.utexas.edu/downloads/papers/stanley.cec02.pdf)只有6頁長度,而且Section 2是一個高水平的overview。
在當前的NEAT-Python實現上,個體基因組種群被保留下來。每一個基因組包括2個基因集合去描述怎樣去建立一個人工神經網路。
1.結點基因,唯一地指向單個神經元。
2.連線基因,唯一地指向神經元之間的單個連線。
為了進化解決問題的方法,使用者必須提供一個fitness function計算實數值表明個體基因組的質量:擁有更好能力者有更高的分數。演算法通過使用者指定的進化次數進行進化,每一代個體產生於上一代最優個體之間的繁殖以及變異。
繁殖和變異操作可能在基因組上新增結點 and/or 連線,從而該演算法所得的基因組可能會變得複雜。當預先設定的迭代次數到達或者至少有一個個體的fitness(fitness criterion function=max)超過了使用者指定的fitness threshold,演算法終止。
一個難點是crossover的實現-怎樣實現兩個不同結構網路的crossover?NEAT用identifying number(new and higher序號產生於每個附加的結點)追蹤結點的起源,來源於相同祖先(homologous)的結點進行crossover,連線的結點具有相同的祖先的連線進行匹配。
另一個潛在的難點是結構變異,與連線的weights變異相比,新增node或者connection對於未來是有前景的,但是在短期有可能是破壞性的(直到被破壞性小的變異所調整)。NEAT將genomes分為多個物種來處理上述問題,genome之間相似性越高,genomic distance越近,genome相似的群體為一個物種,物種內部(而不是物種間)存在激烈的競爭。genomic distance怎樣測量?非同源結點和連線的數量(非同源結點被稱為disjoint或excess,取決於id的範圍是在另一個父代的範圍內還是範圍外)
關鍵步驟:
(1)使用創新ID(innovation ID)對神經網路的結點和連線進行直接編碼(direct coding)
(2)根據innovation ID進行交叉配對(crossover)
(3)對神經元(node)和神經連結(link)進行基因突變(mutation)
(4)儘量保留生物多樣性(speciation),因為效能較差的網路可能會突變為效能好的網路
(5)初始化為只有input和output相連的神經網路,從最簡單的網路開始進化。
神經網路在NEAT的表現形式:
Node genes是結點型別,輸入結點、隱藏結點還是輸出結點。
Connect genes是連結,儲存該連結是哪個點到哪個點、權重weight、是否使用(Enable or Disable)還有創新號Innov ID。
神經網路的變異
如上圖所示,2→5連結為disable,因為2→5之間變異出結點4,所以現在是通過2→4→5相連。
分為結點變異和連結變異兩種,如下圖所示:
神經網路配對
主要根據父代和母代的基因根據Innov id進行兩兩對齊,雙方都有的innovation就隨機選擇一個,如果有不匹配的基因,那麼繼承具有更好fitness的parent;如果Parent1和Parent2 fitness相同,則隨機選擇。
例項:使用NEAT進化具有xor功能的神經網路(監督學習)
進化具有如下xor函式的神經網路:
Input 1 | Input 2 | Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
適應度函式
適應度值通常為float型別。在本例中,基於基因組genome建立了一個feed-forward前向神經網路,對於表中的每一個例子,提供輸入並且計算網路的輸出。每個基因組genome的誤差為
neat-python模組使用eval_genomes計算適應度,函式需要兩個引數:genomes列表(現有的種群)和啟用的配置檔案。
執行NEAT主框架
實現fitness函式之後,需要使用模板檔案去實現以下步驟:
1.建立neat.config.Config物件,以配置檔案為引數(配置檔案引數描述)
2.使用Config物件建立neat.population.Population物件
3.呼叫Population物件的run方法,傳入fitness function和最大迭代次數
完成上述之後,NEAT會執行直到迭代次數或者有一個genome達到fitness的閾值。
獲得執行結果
當run方法return之後,可以通過Population物件的statistics成員(neat.statistics.StatisticsReporter
物件)獲得在執行過程中的最優genome(s)。在本例中,通過pop.statistics.best_genome()獲得最優winner genome。
通過statistics物件可以獲得其他資訊,比如每代的fitness平均值和標準差,最優的n個genomes等
視覺化
使用visualize module視覺化每代最佳和平均適應度,物種的變化以及一個genome的網路結構,使用視覺化功能需要安裝graphviz和python-graphviz,conda命令安裝如下(在Windows上):
conda install -c conda-forge graphviz
conda install -c conda-forge python-graphviz
在Ubuntu上:
sudo apt-get install graphviz
xor神經網路的配置檔案地址:https://github.com/CodeReclaimers/neat-python/blob/master/examples/xor/config-feedforward配置檔案解析(不能直接使用,因為添加了註釋,影響了編碼,要用從github上地址下載):
[NEAT]
fitness_criterion = max # genome fitness集合的最大準則
fitness_threshold = 3.9 # fitness閾值
pop_size = 150 # 種群數量
# if True,所有species由於stagnation become extinct時,重新生成一個random種群
# if False,CompleteExtinctionException異常會被丟擲
reset_on_extinction = False
[DefaultGenome]
# node activation options
activation_default = sigmoid
activation_mutate_rate = 0.0 # activation函式變異概率,可能變異為activation_options中的函式
activation_options = sigmoid # activation_function列表
# node aggregation options
aggregation_default = sum # 即w0*v0+w1*v1+...+wn*vn,sum就是求和
aggregation_mutate_rate = 0.0
aggregation_options = sum
# node bias options
bias_init_mean = 0.0 # 正態分佈的mean
bias_init_stdev = 1.0 # 正態分佈的stdev
bias_max_value = 30.0 # bias最大值
bias_min_value = -30.0 # bias最小值
bias_mutate_power = 0.5 # 以0為中心的正態分佈的標準差來得到bias的變異值
bias_mutate_rate = 0.7 # bias加上一個random值的變異概率
bias_replace_rate = 0.1 # bias用一個random值替換的變異概率
# genome compatibility options
compatibility_disjoint_coefficient = 1.0 # disjoint和excess基因數量在計算genomic distance的係數
compatibility_weight_coefficient = 0.5 # 基因組平均weight差值在計算genomic distance的係數
# connection add/remove rates
conn_add_prob = 0.5 # 在現存node之間新增connection的變異概率
conn_delete_prob = 0.5 # 刪除現存connection之間的變異概率
# connection enable options
enabled_default = True # 新建立的connection的enable是True還是False
enabled_mutate_rate = 0.01 # enabled狀態變為disabled概率
feed_forward = True # True表示不存在recurrent連線
initial_connection = full #
# node add/remove rates
node_add_prob = 0.2 # 新增新結點的變異概率
node_delete_prob = 0.2 # 刪除新結點的變異概率
# network parameters
num_hidden = 0
num_inputs = 2
num_outputs = 1
# node response options # 同bias 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 # 同bias 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 # genomic distance小於此距離被認為是同一物種
[DefaultStagnation]
species_fitness_func = max # 計算種群適應度的函式為種群中某個fitness最大的個體
max_stagnation = 20 # 超過此次數,該種群被視為stagnant並且移除
species_elitism = 2 # 保護該數量的fitness最大的種群不受max_stagnation的影響
[DefaultReproduction]
elitism = 2 # 每個種群的elitism個最優個體被保留到下一代
survival_threshold = 0.2 # 每一代每個species允許繁殖的概率
import os
import neat
import visualize
# 2-input XOR inputs and expected outputs.
# 神經網路的輸出值一般為float
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_genomes(genomes, config):
for genome_id, genome in genomes:
genome.fitness = 4.0
# 建立genome對應的net
net = neat.nn.FeedForwardNetwork.create(genome, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = net.activate(xi)
genome.fitness -= (output[0] - xo[0]) ** 2
def run(config_file):
# Load configuration.
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_file)
# 根據配置檔案建立種群
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)
# 每5次迭代生成一個checkpoint
p.add_reporter(neat.Checkpointer(5))
# Run for up to 300 generations.
winner = p.run(eval_genomes, 300)
# Display the winning genome.
print('\nBest genome:\n{!s}'.format(winner))
# 展示最優net的輸出結果對比訓練資料
print('\nOutput:')
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = winner_net.activate(xi)
# 使用驚歎號!後接a 、r、 s,宣告 是使用何種模式, acsii模式、引用__repr__ 或 __str__
print("input {!r}, expected output {!r}, got {!r}".format(xi, xo, output))
# 用於展示net時輸入節點和輸出節點的編號處理
# input node從-1,-2,-3...編號,output node從0,1,2...編號
node_names = {-1: 'A', -2: 'B', 0: 'A XOR B'}
# 繪製net
visualize.draw_net(config, winner, view=True, node_names=node_names)
# 繪製最優和平均適應度,ylog表示y軸使用symlog(symmetric log)刻度
visualize.plot_stats(stats, ylog=False, view=True)
# 視覺化種群變化
visualize.plot_species(stats, view=True)
# 使用restore_checkpoint方法使得種群p恢復到的checkpoint-4時的狀態,返回population
p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-4')
p.run(eval_genomes, 10)
if __name__ == '__main__':
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, 'config-feedforward')
run(config_path)
在net中,如果是實線,表示為Enable,若為虛線,則為Disable;紅線表示權重weight<=0,綠色表示weight>0,線的粗細和大小有關。
例項:利用NEAT實現深度強化學習網路
借用CartPole例項。
程式當中,is_training= True時,為進化神經網路;當為False時,可以調出最後的checkpoint復現進化的最終狀態。
import os
import neat
import visualize
import numpy as np
import gym
env = gym.make('CartPole-v0').unwrapped
max_episode = 10
winner_max_episode = 10
episode_step = 50 # 控制每輪episode的最多步數
is_training = False # True為進行進化,False為輸出最優網路
checkpoint = 9 # 最終狀態
# 只要杆滿足條件不落下,reward=1.0
# 評估fitness就看每輪episode的總reward
# 根據木桶效應,選擇最小reward的episode為fitness值
def eval_genomes(genomes, config):
for genome_id, genome in genomes:
net = neat.nn.FeedForwardNetwork.create(genome, config)
episode_reward = []
for episode in range(max_episode):
accumulative_reward = 0
observation = env.reset()
for step in range(episode_step):
action = np.argmax(net.activate(observation))
observation_, reward, done, _ = env.step(action)
accumulative_reward += reward
observation = observation_
if done:
break
episode_reward.append(accumulative_reward)
genome.fitness = np.min(episode_reward)/episode_step
def run(config_file):
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_file)
p = neat.Population(config)
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
p.add_reporter(neat.Checkpointer(5))
p.run(eval_genomes, 10)
visualize.plot_stats(stats, ylog=False, view=True)
visualize.plot_species(stats, view=True)
def evoluation():
p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-%d' % checkpoint)
winner = p.run(eval_genomes, 1)
net = neat.nn.FeedForwardNetwork.create(winner, p.config)
for episode in range(winner_max_episode):
s = env.reset()
for step in range(100):
env.render()
a = np.argmax(net.activate(s))
s, r, done, _ = env.step(a)
if done:
break
node_names = {-1: 'x', -2: 'x_dot', -3: 'theta', -4: 'theta_dot',
0: 'action1', 1: 'action2'}
visualize.draw_net(p.config, winner, view=True, node_names=node_names)
if __name__ == '__main__':
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, 'config-feedforward')
if is_training is True:
run(config_path)
else:
evoluation()
選擇進化出的某種神經網路如下:
改進為recurrent link與node
方法:將config配置檔案中的的feed_forward=True改為False,所有原來的 net = neat.nn.FeedForwardNetwork 改成 neat.nn.RecurrentNetwork。可以使得神經網路結構產生迴圈連結和結點,使得網路具有記憶功能,神經網路的形式結構就能變化的多種多樣.
比如上述帶有迴圈連結和結點的神經網路,和RNN不同的是RNN會通過hidden state來傳遞記憶;而NEAT RecurrentNetwork是通過延遲重新整理的形式,在某一次更新時,出了輸入結點In(0,1,2,3)採用新feed的值,其他所有結點都採用上一次更新時的結點值進行計算,比如act1計算的是上次更新結點2的值而不是本次更新結點2的值,這樣可以避免前向feed造成的無限迴圈問題。