Baby Steps into Genetic Programming
Genetic Programming 浅尝:迈出小小的一步
1 简介
尽管我在 Google AI Contest 中的最终排名令人失望 (280th), 但这是一次非常有教育意义的经历,并且完全被 Gábor Melis 使用 Common Lisp 取得的压倒性胜利 所抵消。比赛期间,让我产生兴趣的一件事是 AI Challenge 论坛上的一个帖子,该帖子关于一个使用 genetic programming 编写的 bot。Genetic programming (GP) 和 genetic algorithms 一直让我很感兴趣,而且看到 bot 的实际运行 真的激励我深入研究这个问题。Genetic programming 的灵感来自生物进化,是一种通过建立环境(针对手头的问题进行调整!)并允许计算机程序在该环境中进化到可能的解决方案来解决问题的方法。
本文展示了我使用 Common Lisp 对 GP 的初步探索,应该是一个典型的 REPL 会话示例(我的会话分为两个晚上,持续了几个小时)。代码经过审查,使其更具可读性和 Lisp 风格,但仍然非常像我最初编写的代码。从 REPL 会话中,有用的输出已被剪切并粘贴到本文中。
这篇是对 GP 的一个 非常 基本的介绍,旨在帮助对 GP 和/或 (Common) Lisp 感兴趣的人。它侧重于代码和示例,而轻理论。我的目的是让读者在阅读本文时在 REPL 上玩转这些函数。
本文中的代码应该是可移植的 Common Lisp。如果您需要了解某个函数或宏的功能,请查阅 Common Lisp HyperSpec。
1.1 Toy Project
我在网上找到了 A Field Guide to Genetic Programming,并决定复制 Bill Clementson 的 toy project,即发现计算圆面积的公式。除了教育目的之外,我们将从头开始。
1.2 Environment
许多 Common Lispers 使用 Emacs、Slime,并且通常使用 Unix 或 OS X 作为开发环境。但是,不要以为这是编写 Lisp 的唯一方法。CLiki 上的开发页面 列出了许多其他具有不同可用性状态的替代方案。
我的建议是使用您最喜欢的文本编辑器和 CLISP,因为它内置了命令行历史记录和选项卡补全功能。在编辑器中编写或粘贴代码,并在每次添加后保存文件,例如 "gp.lisp
"。然后您只需要输入:
(load "/path/to/gp.lisp")
一次,就可以使用命令行历史记录重新加载它。
2 Generating Random Code
正如您可能已经读到的,space.invaders bot 是用 PHP 编写的。我们选择的武器是 Common Lisp,首要任务是生成随机代码。
2.1 Operators
(defparameter *operators* '(+ - * /))
为了简单起见,并且已经知道我们的目标函数 (πr²),我们选择使用四个运算符:加法、减法、乘法和除法。为了(再次)简单起见,它们的元数为 2,因此它们都将接受两个参数。
OPERATORS 是变量的名称。earmuffs 约定用于表示全局变量。
2.2 RANDOM-ELT
(defun random-elt (sequence)
(let ((seq-length (length sequence)))
(when (> seq-length 0)
(elt sequence (random seq-length)))))
下一节中的 RANDOM-FORM 函数使用 RANDOM-ELT 从运算符列表中选择一个随机元素。
2.3 Generating Random Function Forms
(defun random-form (operators)
(append (list (random-elt operators))
(loop repeat 2 ; arity
collect (let ((random-nr (random 100)))
(cond ((< random-nr 50) (random-form operators))
((< random-nr 75) (random 10.0))
(t '=input=))))))
该函数的工作方式如下:它创建一个包含三个项目的列表。第一个项目是一个随机运算符,第二个和第三个项目是该运算符的参数:
(operator argument-1 argument-2)
参数可以是 0.0 到 10.0 之间的随机数(任意限制)、一个名为 =INPUT=
的变量(稍后会详细介绍)或者 这一点很重要,另一个包含三个项目的列表。例如:
(* =INPUT= 2.345)
或
(+ 6.789 (- =INPUT= 0.123))
因此,如果需要,RANDOM-FORM 函数将递归调用自身。ARGUMENT-1 和 ARGUMENT-2 的(任意)概率为:50% 的时间是另一个随机 form,25% 的时间是一个随机数,25% 的时间是 =INPUT=
变量。
上述生成随机 form 的过程类似于 Field Guide 第 2.2 章 中描述的 "grow" 方法。
2.3.1 Limiting RANDOM-FORM
(defun random-form (operators &optional (max-depth 4))
(append (list (random-elt operators))
(loop repeat 2 ; arity
collect (let ((random-nr (random 100)))
(if (> max-depth 0)
(cond ((< random-nr 50)
(random-form operators (- max-depth 1)))
((< random-nr 75) (random 10.0))
(t '=input=))
(cond ((< random-nr 50) (random 10.0))
(t '=input=)))))))
由于 RANDOM-FORM 可以递归调用自身,因此它可能会生成大量的随机代码,甚至耗尽 Lisp 实现的堆栈。因此,我们需要对 RANDOM-FORM 施加一些限制。添加 MAX-DEPTH 参数并在再次调用 RANDOM-FORM 时减去 MAX-DEPTH 可以防止发生不良情况。
2.4 =INPUT=
由于我们希望生成的代码从输入值(半径)计算圆的面积,因此我们已将生成 =INPUT=
变量的可能性添加到 RANDOM-FORM。此变量与稍后讨论的 RUN-FORM 函数的输入参数相同。
2.5 Testing RANDOM-ELT and RANDOM-FORM
将 RANDOM-ELT 和 RANDOM-FORM 粘贴到 REPL 中并多次运行后者将产生一些随机生成的代码:
CL-USER> (random-form *operators*)
(/ 2.8173013 5.378826)
CL-USER> (random-form *operators*)
(* =INPUT= =INPUT=)
CL-USER> (random-form *operators*)
(+ 7.9595613
(- (- =INPUT= (* =INPUT= 0.57189345))
(* (- (/ 6.9174767 9.723027) =INPUT=) (* =INPUT= =INPUT=))))
CL-USER> (random-form *operators*)
(- (- =INPUT= (- (/ 2.002045 (* =INPUT= 9.829036)) 4.531122)) =INPUT=)
3 Running Generated Code
(defun run-form (form input)
(funcall (eval `(lambda (=input=) ,form)) ; note: backquote!
input))
为了运行生成的 form,我们用带有 =INPUT=
参数的 LAMBDA 包装它们,并使用输入值 funcall 评估后的 lambda。以下 REPL 交互逐行显示了 RANDOM-FORM 中发生的事情:
CL-USER> (defparameter random-form (random-form *operators*))
RANDOM-FORM
CL-USER> random-form
(* 6.341989 =INPUT=)
CL-USER> `(lambda (=input=) ,random-form)
(LAMBDA (=INPUT=) (* 6.341989 =INPUT=))
CL-USER> (eval `(lambda (=input=) ,random-form))
#<FUNCTION (LAMBDA (=INPUT=)) {AF2F08D}>
CL-USER> (funcall (eval `(lambda (=input=) ,random-form)) 2)
12.683978
CL-USER> (* 6.341989 2)
12.683978
但是,生成的代码可能不合法并导致错误。由于我们将多次运行大量的生成代码,因此我们不想看到错误消息并进入调试器。为了简单起见,我们让非法 form 返回 NIL,以便我们可以稍后将其杀死。向 RUN-FORM 添加错误处理程序将执行此操作:
(defun run-form (form input)
(let ((*error-output* (make-broadcast-stream)))
(handler-case (funcall (eval `(lambda (=input=) ,form))
input)
(error () nil))))
*ERROR-OUTPUT*
被重定向,因此我们不会受到各种编译通知的困扰。在这种情况下,它们只会分散注意力。
3.1 SBCL note
SBCL 是一个非常好的开源 CL 实现,但它编译代码的速度不是很快。我们目前通过每次重新编译 form 来处理 RUN-FORM 的方式导致本文后面的 generations 的推进非常缓慢。与编译为本机代码的 SBCL 相比,编译为字节代码的 CLISP 推进 generations 的速度要快得多。
由于我们处于探索模式,我们将保持 RUN-FORM 的原样。
4 Population
(defun create-initial-population (operators &optional (size 100))
(loop repeat size
collect (random-form operators)))
如果您不熟悉 CL:LOOP 中的 COLLECT 语句在每次迭代中累积一个随机 form,并在迭代完成后将它们作为列表返回。例如:
CL-USER> (create-initial-population *operators* 10)
((* =INPUT= 6.8947406)
(* (+ =INPUT= 2.1140647) 6.7930226)
(+
(/ (+ (* =INPUT= (/ 8.296512 =INPUT=)) 1.1989254)
(- =INPUT= (/ =INPUT= (- 5.1625586 1.4763731))))
=INPUT=)
(- 1.5379852 2.1223724)
(* =INPUT= (- 3.0632048 (* (+ (- 5.8116364 =INPUT=) 0.1915878) =INPUT=)))
(/ 8.237898 =INPUT=)
(* (/ (/ (- =INPUT= =INPUT=) 0.56726336) =INPUT=) =INPUT=)
(+ (+ 7.147339 =INPUT=) =INPUT=)
(- (- =INPUT= =INPUT=) =INPUT=)
(+ (/ =INPUT= (- (* =INPUT= =INPUT=) (* (- =INPUT= =INPUT=) =INPUT=)))
6.520609))
我们创建初始 population 的方式并不理想。这是因为 RANDOM-FORM 只创建一种类型的语法树(使用前面提到的 "grow" 方法)。为了获得更多样化的初始 population,我们需要更多代码生成函数,这些函数将输出不同类型的语法树。
"full" 方法很容易添加,留给读者作为练习。有关说明,请参见 Field Guide。
5 Fitness
(defun fitness (form fitness-fn test-input)
(loop for input in test-input
for output = (run-form form input)
for target = (funcall fitness-fn input)
for difference = (when output (abs (- target output)))
for fitness = (when output (/ 1.0 (+ 1 difference)))
when (null output) do (return-from fitness nil)
collect fitness into fitness-values
finally (return (reduce #'* fitness-values))))
我们需要一种方法来测试生成的函数相对于我们期望结果的输出,并将其表示为一个数字:fitness。这通常是 0.0 到 1.0 之间的数字。越接近 1.0,fitness 越好。
我们还需要检查 RUN-FORM 的返回值是否为 NIL,在这种情况下,它执行了非法代码。在这种情况下,FITNESS 也会返回 NIL。(穷人的异常处理,但目前就足够了。)
因此,该函数需要:1) 要检查的 form,2) 要对照检查的 fitness 函数,以及 3) 测试输入。我们还想对照多个输入值进行检查。
TEST-INPUT 参数是一个输入值列表,因此最直接的方法是迭代这些值并针对它们运行 form 和 fitness 函数。
为了获得 0.0 到 1.0 之间的 fitness 值,我们取生成的 form (OUTPUT) 的输出与 fitness 函数 (TARGET) 的输出之间的绝对差。然后,我们将 1 添加到此差异并使用它来除以 1.0。为了说明:
CL-USER> (defun fit (x) (/ 1.0 (+ 1 x)))
FIT
CL-USER> (fit 0) ; no difference so the desired output
1.0
CL-USER> (fit 0.1)
0.9090909
CL-USER> (fit 5)
0.16666667
CL-USER> (fit 500)
0.001996008
我们不测试负值,因为 FITNESS 中的 DIFFERENCE 永远不会为负。
REPL 测试 FITNESS:
CL-USER> (defparameter random-form (random-form *operators*))
RANDOM-FORM
CL-USER> random-form
(/ 8.552494 =INPUT=)
CL-USER> (fitness random-form (lambda (r) (* pi r r)) '(0 1 -2))
NIL ; this is correct since we divided by zero
CL-USER> (setf random-form (random-form *operators*))
(- =INPUT= 5.246996)
CL-USER> (fitness random-form (lambda (r) (* pi r r)) '(0 1 -2))
9.168484389517398d-4
CL-USER> (setf random-form (random-form *operators*))
(* =INPUT= (+ (* =INPUT= 1.8322039) (- =INPUT= 6.812643)))
CL-USER> (fitness random-form (lambda (r) (* pi r r)) '(0 1 -2))
0.009196622001630076d0
CL-USER> (fitness '(* pi =input= =input=) (lambda (r) (* pi r r)) '(0 1 -2))
1.0d0
CL-USER> (fitness '(* (- pi 0.1) =input= =input=) (lambda (r) (* pi r r)) '(0 1 -2))
0.649350645706412d0
6 Generation Functions
现在一切就绪,可以开始考虑从初始 population 创建新的 generations。目前,我们仅支持 cross-overs 和 mutations。
6.1 Traversing Nodes
在我们进行 cross-overs 和 mutations 之前,我们需要查看三个重要的函数:N-NODES、RANDOM-NODE 和 REPLACE-NODE。它们都共享一个通用的函数,但每个函数略有不同:TRAVERSE-NODES。
这是一个基本的递归函数。它迭代 FORM 列表的每个元素,如果该元素也是一个列表,则再次调用 TRAVERSE-NODES,并将该列表元素作为参数。
(defun traverse-nodes-example (form)
(labels ((traverse-nodes (subform &optional (indent ""))
(loop for node in subform
do (format t "~D:~A ~S~%" (/ (length indent) 2) indent node)
(when (listp node)
(traverse-nodes node
(concatenate 'string indent " "))))))
(traverse-nodes form)))
TRAVERSE-NODES-EXAMPLE 遍历 FORM 的方式与 TRAVERSE-NODES 完全相同,并且对于每个节点,它都会打印其嵌套级别和节点本身:
CL-USER> (traverse-nodes-example '(a (b c) (d (e f) g) h))
0: A
0: (B C)
1: B
1: C
0: (D (E F) G)
1: D
1: (E F)
2: E
2: F
1: G
0: H
6.1.1 N-NODES
(defun n-nodes (form)
(let ((nodes 1))
(labels ((traverse-nodes (subform)
(loop for node in subform
do (incf nodes)
(when (listp node)
(traverse-nodes node)))))
(traverse-nodes form))
nodes))
RANDOM-NODE 的辅助函数。返回 FORM 中节点的数量,包括根节点。请注意," (B C)
" 以及 B 和 C 都被视为节点:
CL-USER> (n-nodes '(b c))
3
CL-USER> (n-nodes '(a (b c) (d (e f) g) h))
12
CL-USER> (n-nodes '())
1
6.1.2 Picking Random Nodes
我们希望能够从 form 中挑选一个随机节点来执行操作。RANDOM-NODE 正是执行此操作的:
(defun random-node (form)
(let* ((index 1)
(nodes-1 (- (n-nodes form) 1))
(random-node-index (+ (random nodes-1) 1)))
(labels ((traverse-nodes (subform)
(loop for node in subform
do (when (= index random-node-index)
(return-from random-node
(list :index index :node node)))
(incf index)
(when (listp node)
(traverse-nodes node)))))
(traverse-nodes form))))
它挑选一个 RANDOM-NODE-INDEX 并开始遍历 FORM 的节点。当它到达 index 时,它会以 property list 的形式返回节点和 index:
(:index random-node-index :node random-node)
不会返回 index 0 处的根节点。
6.1.3 Replacing Nodes
我们需要能够用另一个节点替换 form 中的一个节点。为了避免错误和混淆,我们将让 REPLACE-NODE 以新 form 的形式返回此结果。
(defun replace-node (form node-index new-node)
(let ((index 0))
(labels ((traverse-nodes (subform)
(loop for node in subform
do (incf index)
when (= index node-index)
collect new-node
when (and (/= index node-index)
(not (listp node)))
collect node
when (and (/= index node-index)
(listp node))
collect (traverse-nodes node))))
(traverse-nodes form))))
遍历 FORM 的节点并收集它们以作为新 form 返回。当其 INDEX 计数器等于 NODE-INDEX 时,它会收集 NEW-NODE。
该函数不会替换根节点(index 0)。
6.2 Cross-overs
有 不同类型的 cross-overs,但我们将执行最直接的:用 form B 中的一个随机节点替换 form A 中的一个随机节点。
Cross-overs 是 GP 中最常用的遗传操作,有些人甚至建议永远不要使用 mutations。它们可以与人类的生育进行比较,其中男性和女性的属性在其后代中表示。
(defun cross-over (form1 form2 &key (debug nil))
(let ((rnode1 (random-node form1))
(rnode2 (random-node form2)))
(when debug
(format t "form1: ~S~%form2: ~S~%rnode1: ~S~%rnode2: ~S~%"
form1 form2 rnode1 rnode2))
(replace-node form1 (getf rnode1 :index) (getf rnode2 :node))))
CROSS-OVER 接受两个 form 作为参数并返回一个新 form。新 form 在很大程度上与 FORM1 相似,但一个随机节点被 FORM2 中的一个随机节点替换。函数 RANDOM-NODE 用于挑选这些随机节点。
CL-USER> (cross-over '(1 (2 3) (4 (5 6) 7) 8) '(a (b c) (d (e f) g) h) :debug t)
form1: (1 (2 3) (4 (5 6) 7) 8)
form2: (A (B C) (D (E F) G) H)
rnode1: (:INDEX 1 :NODE 1)
rnode2: (:INDEX 3 :NODE B)
(B (2 3) (4 (5 6) 7) 8)
CL-USER> (cross-over '(1 (2 3) (4 (5 6) 7) 8) '(a (b c) (d (e f) g) h) :debug t)
form1: (1 (2 3) (4 (5 6) 7) 8)
form2: (A (B C) (D (E F) G) H)
rnode1: (:INDEX 3 :NODE 2)
rnode2: (:INDEX 7 :NODE (E F))
(1 ((E F) 3) (4 (5 6) 7) 8)
CL-USER> (cross-over '(1 (2 3) (4 (5 6) 7) 8) '(a (b c) (d (e f) g) h) :debug t)
form1: (1 (2 3) (4 (5 6) 7) 8)
form2: (A (B C) (D (E F) G) H)
rnode1: (:INDEX 2 :NODE (2 3))
rnode2: (:INDEX 6 :NODE D)
(1 D (4 (5 6) 7) 8)
CL-USER> (cross-over '(1 (2 3) (4 (5 6) 7) 8) '(a (b c) (d (e f) g) h) :debug t)
form1: (1 (2 3) (4 (5 6) 7) 8)
form2: (A (B C) (D (E F) G) H)
rnode1: (:INDEX 5 :NODE (4 (5 6) 7))
rnode2: (:INDEX 5 :NODE (D (E F) G))
(1 (2 3) (D (E F) G) 8)
6.3 Mutation
Mutations 会更改 form 的一部分,而无需其他 form 来进行操作。它可能类似于宇宙射线改变生物实体中的一部分遗传信息。
我们将使用 subtree mutation,因为它易于编写并且具有潜在的巨大影响。
(defun mutate (form operators &key (debug nil))
(let ((rform (random-form operators))
(rnode (random-node form)))
(when debug
(format t "form: ~S~%rform: ~S~%rnode: ~S~%" form rform rnode))
(replace-node form (getf rnode :index) rform)))
Mutation 用 RANDOM-FORM 创建的 form 替换 FORM 的一个随机节点。
CL-USER> (mutate '(a (b c) (d (e f) g) h) *operators* :debug t)
form: (A (B C) (D (E F) G) H)
rform: (+ =INPUT= (- 4.4699216 (+ 9.623513 =INPUT=)))
rnode: (:INDEX 3 :NODE B)
(A ((+ =INPUT= (- 4.4699216 (+ 9.623513 =INPUT=))) C) (D (E F) G) H)
CL-USER> (mutate '(a (b c) (d (e f) g) h) *operators* :debug t)
form: (A (B C) (D (E F) G) H)
rform: (+ 4.5209084 (- 8.943897 (+ 6.657296 =INPUT=)))
rnode: (:INDEX 2 :NODE (B C))
(A (+ 4.5209084 (- 8.943897 (+ 6.657296 =INPUT=))) (D (E F) G) H)
7 Advancing a Generation
7.1 Evaluating a Population
(defun evaluate-population (population fitness-fn test-input)
(loop for form in population
for fitness = (fitness form fitness-fn test-input)
when fitness collect (list :fitness fitness :form form) into result
finally (return (sort result
(lambda (a b)
(> (getf a :fitness) (getf b :fitness)))))))
为了推进 generation,我们需要对 population 中的 form 执行 cross-overs 和 mutations。文献表明,我们应该给予具有更高 fitness 的 form 更多机会成为 cross-over 或 mutation 的候选者。
EVALUATE-POPULATION 评估一个 population,并返回该 population 中 form 的列表及其 fitness(减去任何给出错误的 form)。为了便于在 REPL 上进行测试以及在我们的下一个函数中使用,输出按最佳 fitness 到最差排序 sorted:
CL-USER> (defparameter population (create-initial-population *operators* 10))
POPULATION
CL-USER> (evaluate-population population (lambda (r) (* pi r r)) '(0 1 -2))
((:FITNESS 0.016815323605722716111d0 :FORM (- (- =INPUT= =INPUT=) =INPUT=))
(:FITNESS 0.009437720627636827027d0 :FORM (- 1.5379852 2.1223724))
(:FITNESS 0.007690745276452599039d0 :FORM (* =INPUT= 6.8947406))
(:FITNESS 0.0031801166742824690382d0 :FORM
(* =INPUT= (- 3.0632048 (* (+ (- 5.8116364 =INPUT=) 0.1915878) =INPUT=))))
(:FITNESS 0.0016815216288940106286d0 :FORM (+ (+ 7.147339 =INPUT=) =INPUT=))
(:FITNESS 2.6768631466526024146d-4 :FORM (* (+ =INPUT= 2.1140647) 6.7930226)))
这与 Population 章节中创建的 population 相同。请注意,十个 form 中的四个已被淘汰,因为它们执行了非法代码。
7.2 HEAD
(defun head (sequence &optional (amount 1))
(if (<= amount 0)
nil
(if (< (length sequence) amount)
sequence
(subseq sequence 0 amount))))
ADVANCE-GENERATION 使用的实用程序函数。从 SEQUENCE 的开头返回 AMOUNT 个元素。如果 SEQUENCE 短于 AMOUNT,它将返回整个 SEQUENCE。
7.3 Running the Advancement
(defun advance-generation (population fitness-fn operators test-input
&optional (max-population 100))
(let ((epop (evaluate-population population fitness-fn test-input)))
(format t "Best fitness of current population: ~S~%"
(getf (first epop) :fitness))
(loop for plist in (head epop max-population)
for i from 0
for fitness = (getf plist :fitness)
for form = (getf plist :form)
collect form
when (<= (random 1.0d0) fitness)
collect (if (<= (random 100) 90)
(cross-over form (getf (random-elt epop) :form))
(mutate form operators))
;; Add a new random form to the population now and then.
when (<= (random 100) 2) collect (random-form operators))))
使用 EVALUATE-POPULATION 中的 form 及其 fitness 的列表作为输入,我们仅循环遍历前 MAX-POPULATION 个项目,以保持 population 大小受到控制。然后,我们根据 0.0 到 1.0 之间的随机数检查 form 的 fitness,如果该数字低于 fitness,则将选择该 form 进行 cross-over(90% 的概率)或 mutation(10% 的概率)。
因此,form 的 fitness 是其被选中的机会。由于初始 population 中 form 的 fitness 通常非常低,因此需要进行很多 generations 才能开始发生某些事情。
我们收集 form,如果已选择它,我们还会收集 CROSS-OVER 或 MUTATE 的结果。这些都将被累积并最终作为新 generation 返回。
由于我们目前处理事情的方式,成功的 form 很有可能会接管整个 population 并使其过于同质化,因此很小概率会将新的随机 form 添加到 population 中,以获得良好的效果。这是一种快速简便的 hack,可以在 populations 中引入一些混乱。
7.4 Finding a Solution
让我们对新的 population 运行 ADVANCE-GENERATION 一百次(我们还使用 SETF 在每次迭代中更新 population):
CL-USER> (defparameter population (create-initial-population *operators* 100))
POPULATION
CL-USER> (loop repeat 100
for i from 0
do (format t "[~S] " i)
(setf population
(advance-generation population
(lambda (r) (* pi r r))
*operators*
'(0 1 -2))))
[0] Best fitness of current population: 0.08546627574466652d0
[...]
[43] Best fitness of current population: 0.08626213422506805d0
[...]
[66] Best fitness of current population: 0.10050107335294403d0
[...]
再次:
CL-USER> (loop repeat 100 for i from 0 do (format t "[~S] " i) (setf population (advance-generation population (lambda (r) (* pi r r)) *operators* '(0 1 -2))))
[...]
[26] Best fitness of current population: 0.3865141184760903d0
[...]
[93] Best fitness of current population: 0.49884414719455095d0
[...]
让我们再进行 300 次运行,这样我们总共进行了 500 次运行,看看最后最好的 form 是什么样子的:
CL-USER> (loop repeat 300 for i from 0 do (format t "[~S] " i) (setf population (advance-generation population (lambda (r) (* pi r r)) *operators* '(0 1 -2))))
[...]
[35] Best fitness of current population: 0.5397727425319018d0
[...]
[62] Best fitness of current population: 0.559234953025742d0
[...]
[117] Best fitness of current population: 0.6436990901489741d0
[...]
[179] Best fitness of current population: 0.9657338407311192d0
[...]
[201] Best fitness of current population: 0.970596840