3个不稳定版本

0.1.1 2023年3月6日
0.1.0 2023年3月2日
0.0.0 2023年2月17日

#1094 in 算法

MIT许可证

86KB
1.5K SLoC

EvioLite

Published on crates.io! Read the docs on docs.rs!

EvioLite是一套用于在Rust中使用进化算法的工具和算法。它以性能为导向,采用最小复制的风格编写,并使用rayon来并行化计算量最大的部分。它还包括一个用于替换rand's thread_rng的即插即用组件,该组件可以完全重复并可以从环境变量中初始化。这意味着如果您得到一个满意的运行结果,您可以与您的程序一起分享该种子,其他人将保证得到与您相同的结果。

入门指南

将以下内容添加到您的Cargo.toml

eviolite = "0.1"

然后继续阅读简单的示例,或者如果您不喜欢教程,请查看文档

示例

我们将一步一步地查看一个完整的程序,该程序使用遗传算法找到近似正弦函数的多项式。这个程序相当短,但很好地展示了使用EvioLite的一般工作流程。

首先,我们将从EvioLite导入所有需要的项

use eviolite::prelude::*;

然后导入一些其他有用的东西

use ndarray::Array1;
use ndarray_rand::RandomExt;
use rand::distributions::Uniform;
use std::f64::consts::FRAC_PI_2;

定义我们的解决方案类型

#[derive(Clone)]
struct Polynomial(Array1<f64>);

此数组的元素将代表多项式abcd的系数。现在让我们定义一个方法来评估给定x的多项式。

impl Polynomial {
    fn apply(&self, x: f64) -> f64 {
        self.0[0]
        + self.0[1] * x
        + self.0[2] * x.powi(2)
        + self.0[3] * x.powi(3)
    }
}

接下来,我们将创建一个测试点的数组。由于评估我们的近似在每一个点上都需要很长时间,所以我们将使用100个在0和π/2之间的均匀分布的点。我们将使用lazy_static包装它,以便它可以在全局范围内访问。

lazy_static::lazy_static! {
    static ref TEST_POINTS: Array1<f64> = Array1::range(0., FRAC_PI_2, FRAC_PI_2 / 100.);
}

实现Solution

现在我们准备好实现Solution特质了

impl Solution for Polynomial {

首先,我们定义我们的适应度值类型。目前,我们将保持简单,并说一个好的近似值可以用一个单一的f64表示。

type Fitness = f64;

接下来,我们需要定义四个遗传算法原语:评估交叉变异

Eviolite需要能够创建一个随机生成的个体群体,作为算法的起点。我们将使用来自random_using的方便方法ndarray-rand,使用Eviolite的可重复RNG生成四个介于0和1之间的随机f64数。这些数字将代表我们多项式a + bx + cx² + dx³中的系数abcd

fn generate() -> Polynomial {
        Polynomial(Array1::random_using(4, Uniform::new_inclusive(0.0, 1.0), &mut thread_rng()))
}

评估

我们需要告诉算法给定多项式在近似sin方面有多好。我们将通过为每个测试点x评估我们的多项式,并取该值与sin(x)之间的绝对差异,然后计算所有这些差异的平均值,以获得所有测试点的平均误差。

fn evaluate(&self) -> f64 {
        -TEST_POINTS.mapv(
            |x| (self.apply(x) - x.sin()).abs()
        ).mean().unwrap()
}

遗传算法假设更高的适应度值是更好的,所以我们前面加了一个负号。

活动:考虑其他我们可以用来衡量这些近似适应度的方式!

交叉

如果你要将遗传算法与现实的选育过程进行比较,这部分就是繁殖的部分。我们需要混合两个多项式的系数,以便它们都从对方那里获取一些信息,同时保留一些自己的信息。我们将使用简单的单点交叉,这有一个内置方法。

fn crossover(a: &mut Self, b: &mut Self) {
        crossover::one_point(&mut a.0, &mut b.0);
}

变异

就像在现实生活中一样,这是“新想法”产生的地方。我们需要随机地稍微修改一个多项式。一种方法是向系数添加一些高斯噪声。幸运的是,这也有一个内置方法。我们将给每个系数一个50%的变异机会,并将标准差为0.1的噪声添加到被选中的变异系数中。

fn mutate(&mut self) {
        mutation::gaussian(&mut self.0, 0.5, 0.1);
}

就是这样!我们已经实现了Solution,这意味着Eviolite拥有了进化多项式以近似sin(x)所需的一切。现在我们可以编写我们的main()函数,并实际得到一个结果。

运行进化

让我们创建我们的 Evolution 实例。我们将使用 (μ + λ) 算法,并使用一些相对随意选择的参数。我们将传递一个 Tournament 选择器,该选择器将反复随机选择10个多项式并挑选出最好的。我们还将创建一个大小为1的 BestN 优胜者殿堂,它会自动追踪迄今为止找到的最佳多项式。最后,我们将在每25,000代后完全删除种群并生成一个全新的种群,以作为算法卡住的保险措施。在 main()

let evo: Evolution<Polynomial, _, _, ()> = Evolution::with_resets(
    alg::MuPlusLambda::new(
        // population size (μ)
        1000,
        // offspring size (λ)
        1000,
        // crossover chance (cxpb)
        0.5,
        // mutation chance (mutpb)
        0.2,
        // selection operator
        select::Tournament::new(10)
    ),

    hof::BestN::new(1),

    25000
);

活动:尝试调整这些参数,看看它们如何影响进化!

现在我们只需运行它!我们将让进化运行,直到找到所有测试点平均误差小于0.001的多项式。我们还将计时以供参考

let start = std::time::Instant::now();

let log = evo.run_until(
    |gen| -gen.hall_of_fame[0].evaluate() < 0.001
);

let time = start.elapsed();

.run_until() 接收有关运行当前状态的闭包信息,并决定是否停止它,所以我们只需检查我们看到的最佳解决方案的平均误差是否小于0.001。

现在,让我们从运行日志中提取我们新获得的小于0.001平均误差的多项式,并将其打印到控制台

let (best, _) = log.hall_of_fame[0].clone().into_inner();

println!("found in {:.3} secs: sin(x) ≈ {:.3} + {:.3}x + {:.3}x² + {:.3}",
    time.as_secs_f64(), best.0[0], best.0[1], best.0[2], best.0[3]
);

这就结束了!查看 examples/approx_sin.rs 获取完整代码。

当你编译并运行此示例(在发布构建上 - 遗传算法非常计算密集!)后,过了一会儿,你应该得到一个类似这样的输出

found in 3.982 secs: sin(x)0.001 + 1.017x + -0.059+ -0.118

活动:将环境变量 EVIOLITE_SEED 设置为 1175913497836025702 并再次运行程序,以获得与上面完全相同的输出!(当然,不包括运行时间。)

sin(x) 的泰勒级数展开开始于 x - (/ 3!) + ...,所以检查系数大致匹配0,1,0和-1/6是一个很好的稳定性检查。

依赖关系

~1.8–2.5MB
~47K SLoC