#强化学习 #遗传 #进化 #学习 #人工 #搜索 #进化

radiate

并行遗传编程引擎,能够进化监督学习、无监督学习和通用强化学习问题的解决方案

26 个稳定版本

1.1.59 2020年11月2日
1.1.58 2020年8月30日
1.1.56 2020年7月20日
1.1.54 2020年4月9日
1.0.3 2019年11月22日

#446 in 算法


2 crates 中使用

MIT 许可证

150KB
2.5K SLoC

辐射

Build Status Crates.io

来自进化辐射。

进化辐射是指具有共同祖先的物种数量迅速增加,以生态和形态多样性为特征 - Pascal Neige。

辐射是一个并行遗传编程引擎,能够进化许多问题的解决方案以及训练学习算法。通过将进化过程与被进化的对象分离,用户可以进化任何定义的结构。该算法通过物种形成进行进化,使得结构可以在自己的生态位内进行优化。

辐射向用户公开了三个必须实现的特征(下面是完整的简单实现)

  1. 基因组
    基因组包装要进化的结构,并要求用户实现两个必要的函数和一个可选的函数。距离和交叉必须实现,但基础是可选的(取决于用户如何选择填充种群)。
  2. 环境
    环境代表基因组的进化环境,这意味着它可以包含种群进化的简单统计信息,或交叉和距离的参数。内部它被包装在一个可变的线程安全指针中,所以它不是用于每个基因组的共享,而是一旦种群存在就只存在一次。环境不需要实现其一个函数,然而根据使用情况,环境公开了一个名为 reset 的函数,目的是“重置”环境。
  3. 问题
    问题是给基因组提供适应度分数的东西。它需要两个实现函数:empty 和 solve。empty 是必需的,应该返回一个基本问题(想想 new())。solve 接受一个基因组并返回该基因组的适应度分数,因此在这里发生对当前基因组状态的评估。

radiate 还自带一个已构建的模型和一个可在 crates.io 上使用的模型。这些模型分别是 radiate_matrix_tree 和 NEAT,radiate_matrix_tree 可在 crates.io 上找到,而 NEAT 则预包装在 radiate 中。

NEAT

也称为增强拓扑神经进化,是由 Kenneth O. Stanley 在这篇论文中描述的算法。这个 NEAT 实现还包括一个反向传播函数,它的工作方式与传统神经网络类似,将输入误差反向传播通过网络以调整权重。与进化引擎结合使用,这可以产生非常出色且快速的结果。NEAT 允许用户定义网络的构建方式,无论是以传统的神经网络方式堆叠相邻层,还是使用论文中解释的进化拓扑。这意味着 NEAT 可以通过正向传播和反向传播或它们的任何组合在进化意义上使用。/examples 目录中有这两种方法的示例。更多关于 NEAT 的信息可在 radiate/src/models/ 中找到

radiate 还支持离线训练,您可以在一台机器上设置要解决的问题,然后通过 Radiate Web 从另一台机器发送参数。

设置

假设所有特征都已实现,则设置种群相当简单。种群是一个高级抽象,用于跟踪进化过程中使用的变量,但在一个时代内不需要 - 例如问题、解决方案、屏幕打印等。新种群最初用默认设置填充。

pub fn new() -> Self {   
    Population {
        // define the number of members to participate in evolution and be injected into the current generation
        size: 100,
        // determine if the species should be aiming for a specific number of species by adjusting the distance threshold
        dynamic_distance: false,
        // debug_progress is only used to print out some information from each generation
        // to the console during training to get a glimpse of what is going on
        debug_progress: false,
        // create a new config to help the speciation of the population
        config: Config::new(),
        // create a new empty generation to be passed down through the population 
        curr_gen: Generation::<T, E>::new(),
        // keep track of fitness score stagnation through the population
        stagnation: Stagnant::new(0, Vec::new()),
        // Arc<Problem> so the problem can be sent between threads safely without duplicating the problem, 
        // if the problem gets duplicated every time, a supervised learning problem with a lot of data could take up a large amount of memory
        solve: Arc::new(P::empty()),
        // create new solver settings which will hold the specific settings for the defined solver 
        // that will allow the structure to evolve through generations
        environment: Arc::new(RwLock::new(E::default())),
        // determine which genomes will live on and pass down to the next generation
        survivor_criteria: SurvivalCriteria::Fittest,
        // determine how to pick parents to reproduce
        parental_criteria: ParentalCriteria::BiasedRandom
    }
}

示例

这是一个快速且简单的示例,展示了如何实现所有必要的特征,并运行遗传引擎生成字符串打印 "hello world!"。在 radiate/src/models/ 中有如何运行 Neat 神经网络的示例。要运行此示例

git clone https://github.com/pkalivas/radiate.git
cd radiate
cargo build --verbose && cargo run --bin helloworld

在我的电脑上(Windows 10,x64-based,i7-7700 @ 4.20GHz,32GB RAM),此操作在不到半秒钟内完成。

extern crate radiate;
extern crate rand;

use std::error::Error;
use std::time::Instant;
use std::sync::{Arc, RwLock};
use rand::Rng;
use radiate::prelude::*;

fn main() -> Result<(), Box<dyn Error>> {
    let thread_time = Instant::now();
    let (top, _) = Population::<Hello, HelloEnv, World>::new()
        .size(100)
        .populate_base()
        .dynamic_distance(true)
        .stagnation(10, vec![Genocide::KillWorst(0.9)])
        .configure(Config {
            inbreed_rate: 0.001,
            crossover_rate: 0.75,
            distance: 0.5,
            species_target: 5
        })
        .run(|model, fit, num| {
            println!("Generation: {} score: {:.3?}\t{:?}", num, fit, model.as_string());
            fit == 12.0 || num == 500
        })?;
        
    println!("\nTime in millis: {}, solution: {:?}", thread_time.elapsed().as_millis(), top.as_string());
    Ok(())
}

现在创建一个问题,它包含目标和实际评分求解器。请注意,目标数据不会为每个求解器复制。

pub struct World { target: Vec<char> }

impl World {
    pub fn new() -> Self {
        World {
            target: vec!['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!']
        }
    }
}

impl Problem<Hello> for World {

    fn empty() -> Self { World::new() }

    fn solve(&self, model: &mut Hello) -> f32 {
        let mut total = 0.0;
        for (index, letter) in self.target.iter().enumerate() {
            if letter == &model.data[index] {
                total += 1.0;
            }
        }
        total        
    }
}

现在定义一个环境来保存全局数据,用于交叉和距离,如记录/统计、交叉概率等,任何需要全局的数据都包含在这个环境中。

#[derive(Debug, Clone)]
pub struct HelloEnv {
    pub alph: Vec<char>,
}

impl HelloEnv {
    pub fn new() -> Self {
        HelloEnv {
            alph: vec!['!', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'], // now i know my abcs..
        }
    }
}

/// implement Environment and Default for the HelloEnv, Environment is there in case you want the environment to be dynamic
impl Envionment for HelloEnv {}
impl Default for HelloEnv {
    fn default() -> Self {
        Self::new()
    }
}

最后,定义一个求解器。这是进化过程中的 'Genome' 类型,并构成每一代的种群。

#[derive(Debug, Clone, PartialEq)]
pub struct Hello {
    pub data: Vec<char>
}

impl Hello {
    pub fn new(alph: &[char]) -> Self {
        let mut r = rand::thread_rng();
        Hello { 
            data: (0..12)
                .map(|_| alph[r.gen_range(0, alph.len())])
                .collect() 
        }
    }

    pub fn as_string(&self) -> String {
        self.data
            .iter()
            .map(|x| String::from(x.to_string()))
            .collect::<Vec<_>>()
            .join("")
    }
}

/// implement genome for Hello
impl Genome<Hello, HelloEnv> for Hello {

    // the first parent is always going to be the most fit parent
    fn crossover(parent_one: &Hello, parent_two: &Hello, env: &Arc<RwLock<HelloEnv>>, crossover_rate: f32) -> Option<Hello> {
        let params = env.read().unwrap();
        let mut r = rand::thread_rng();
        let mut new_data = Vec::new();
        
        if r.gen::<f32>() < crossover_rate {
            for (one, two) in parent_one.data.iter().zip(parent_two.data.iter()) {
                if one != two {
                    new_data.push(*one);
                } else {
                    new_data.push(*two);
                }
            }
        } else {
            new_data = parent_one.data.clone();
            let swap_index = r.gen_range(0, new_data.len());
            new_data[swap_index] = params.alph[r.gen_range(0, params.alph.len())];
        }
        Some(Hello { data: new_data })
    }

    fn distance(one: &Hello, two: &Hello, _: &Arc<RwLock<HelloEnv>>) -> f32 {
        let mut total = 0_f32;
        for (i, j) in one.data.iter().zip(two.data.iter()) {
            if i == j {
                total += 1_f32;
            }
        }
        one.data.len() as f32 / total
    }

    fn base(env: &mut HelloEnv) -> Hello {
        Hello::new(&env.alph)
    }
}

在命令行运行时,结果看起来像这样

Generation: 100 score: 8.000    "!eulozworlde"
Generation: 101 score: 8.000    "!eulozworlde"
Generation: 102 score: 8.000    "!eulozworlde"
Generation: 103 score: 8.000    "!eulozworlde"
Generation: 104 score: 8.000    "!eulozworlde"
Generation: 105 score: 9.000    "heulozworlde"
Generation: 106 score: 9.000    "heulozworlde"
Generation: 107 score: 9.000    "heulozworlde"
Generation: 108 score: 9.000    "heulozworlde"
Generation: 109 score: 9.000    "heulozworlde"
Generation: 110 score: 9.000    "heulozworlde"
Generation: 111 score: 9.000    "heulozworlde"
Generation: 112 score: 10.000   "heulo worlde"
Generation: 113 score: 10.000   "heulo worlde"
Generation: 114 score: 10.000   "heulo worlde"
Generation: 115 score: 10.000   "heulo worlde"
Generation: 116 score: 10.000   "heulo worlde"
Generation: 117 score: 10.000   "heulo worlde"
Generation: 118 score: 10.000   "heulo worlde"
Generation: 119 score: 10.000   "heulo worlde"
Generation: 120 score: 11.000   "hello worlde"
Generation: 121 score: 11.000   "hello worlde"
Generation: 122 score: 11.000   "hello worlde"
Generation: 123 score: 11.000   "hello worlde"
Generation: 124 score: 11.000   "hello worlde"
Generation: 125 score: 11.000   "hello worlde"
Generation: 126 score: 12.000   "hello world!"

Time in millis: 349, solution: "hello world!"

目前有四个示例,只需运行 "cargo run --bin (所需的示例名称)" 即可运行任何示例

  1. xor-neat
  2. xor-neat-backprop
  3. lstm-neat
  4. helloworld

创建一个种群

根据用户用例,种群中的初始一代可以通过四种不同的方式创建。示例展示了不同的使用方法。

  1. populate_gen - 给种群提供一个已构建的 Generation 结构。
  2. populate_base - 从 Genome 的基函数创建一代 Genome。
  3. populate_vec - 从 vec 中获取并填充生成器中的 Genome。
  4. populate_clone - 给定单个 Genome,克隆它 size 次,并从克隆创建一代。

物种形成

由于引擎旨在通过物种形成进化基因组,因此 Config 结构旨在包含用于种群物种形成的参数,调整这些参数将改变基因组在种群中的划分方式,从而通过交叉和突变推动新基因组的发现。

种族灭绝

在进化过程中,种群或特定物种可能会在问题空间中的某个点上停滞不前或陷入僵局。为了打破这种状态,population 允许用户定义停滞代数的数量,直到发生“大屠杀”。这些“大屠杀”选项可以在 genocide.rs 中找到,它们是清理种群以给基因组提供呼吸和进化新路径的机会的简单方法。

pub enum Genocide {
    KeepTop(usize),
    KillWorst(f32),
    KillRandom(f32),
    KillOldestSpecies(usize)
}

这无疑是算法中可以改进的领域。

版本

1.5.57 - 重大改进了 Dense/DensePool 层。改进之前,基准测试需要大约 1.5 分钟才能运行。改进后,大约需要 1.5 秒即可完成。

1.1.55 - 从 evtree 中移除了不安全代码,并修复了内存泄漏。重构 Evtree 以使其泛型化,将神经网络逻辑移至单独部分。清理了 send/sync 实现。

1.1.52 - 为 NEAT 添加了循环神经元。注意 - 这也仅适用于进化(我将专注于实现循环神经元和 GRU 层的反向传播 - 我目前正在从事一些需要循环进化神经元的其他项目)。这可以在 NeatEnvironment 设置中进行配置,其中可以添加循环神经元的 % 改变。0.0 表示不添加循环神经元,而 1.0 表示每个新神经元都是循环的。示例在 radiate/src/models/ 中。

1.1.5 - 添加了对 GRU 层(门控循环单元)的最小支持。GRU 仅适用于进化,目前不适用于反向传播。如果在反向传播期间使用具有 GRU 层的网络,程序将崩溃!如果需要用于反向传播目的的内存单元,请现在使用 LSTM 选项。还清理了一些代码并优化了某些点以使运行更快。Evtree 已移动到其自己的单独 crate,名为 radiate_matrix_tree,可在 crates.io 上找到。

1.1.3 - 添加了对 Radiate Web 的支持,因此可以在不同的机器上训练 Radiate。

1.1.2 - 对于 NEAT 的正向和反向传递,LSTM 层的门控传播现在并行运行,将训练时间减半。将说明文改为发动机的完整实现,这有助于设置一切。在 radiate/src/models/ 中添加了另一个说明文文件,其中提供了设置 NEAT 神经网络的示例。

1.1.1 - 修复了 NEAT 中的愚蠢错误,该错误导致反向传播中出现错误。

1.0.9 - 自 2020 年 1 月 10 日起,所有版本 1.0.9 以后的版本都需要 nightly 工具链 通过 serde 集成添加了对 NEAT 模型的序列化和反序列化 - 目前序列化 trait 对象需要 nightly crate。

依赖关系

~2.8–4MB
~83K SLoC