1. 程式人生 > >NEAT(基於NEAT-Python模組)實現監督學習和強化學習

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 1Input 2Output
000
011
101
110

適應度函式

適應度值通常為float型別。在本例中,基於基因組genome建立了一個feed-forward前向神經網路,對於表中的每一個例子,提供輸入並且計算網路的輸出。每個基因組genome的誤差為

,其中e(i)為期望輸出(expected),a(i)為實際輸出(actual)。fitness值越高越準確,若fitness=1表示準確輸出。

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造成的無限迴圈問題。