3个不稳定版本
0.1.1 | 2023年3月6日 |
---|---|
0.1.0 | 2023年3月2日 |
0.0.0 | 2023年2月17日 |
#1094 in 算法
86KB
1.5K SLoC
EvioLite
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>);
此数组的元素将代表多项式a
、b
、c
和d
的系数。现在让我们定义一个方法来评估给定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³
中的系数a
、b
、c
和d
。
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}x³",
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.059x² + -0.118x³
活动:将环境变量
EVIOLITE_SEED
设置为1175913497836025702
并再次运行程序,以获得与上面完全相同的输出!(当然,不包括运行时间。)
sin(x)
的泰勒级数展开开始于 x - (x³ / 3!) + ...
,所以检查系数大致匹配0,1,0和-1/6是一个很好的稳定性检查。
依赖关系
~1.8–2.5MB
~47K SLoC