遗传算法生成代码

32

进化编程似乎是解决许多优化问题的好方法。这个想法非常简单,实现也不困难。

我想知道是否有办法在Ruby/Python脚本(或任何其他语言)中通过进化方式创建程序?

这个想法很简单:

  1. 创建一组程序种群
  2. 执行遗传操作(轮盘选择或任何其他选择),从最佳程序中继承等等,创建新程序
  3. 重复步骤2直到找到满足条件的程序

但仍然存在一些问题:

  1. 如何表示染色体?例如,染色体的一个单元是否应该是一行代码?
  2. 如何生成染色体?如果它们将是代码行,如何生成它们以确保它们在语法上正确等等?

可以生成的程序示例:

创建脚本,该脚本将N个数字作为输入并返回它们的平均值作为输出。

如果有任何尝试创建此类算法的链接/来源,我会很高兴看到它们。


4
如果其中一个生成的程序抹掉了硬盘,那可能会很有趣。当然,你需要找到一种方法来封锁它,然后小心地打开潘多拉魔盒。我相信有本书(虽然我记不得名字了),其中这样的程序最终演化到掌控全球所有机器并开始杀人的地步😉。 - Sébastien Nussbaumer
4
一本书?这个想法在廉价的科幻文学中已经被炒得毫无新意了。 - Ira Baxter
9
我曾经尝试过这个。程序自我意识后接管了打印机,并使用激光枪击中建筑物里的所有人。 - Jay
1
什么是适应函数?这些进化方法需要一种计算它们与某些标准匹配程度的方式。 - Paddy3118
Jivlain的回答是你想要“标记为答案”的那一个。 - JohnIdol
8个回答

23
如果您确定要这样做,您需要使用遗传程序设计而不是遗传算法。GP允许您进化树形程序。您需要做的是给它一堆原始操作(while($register),read($register),increment($register),decrement($register),divide($result $numerator $denominator),print,progn2(这是GP用于“按顺序执行两个命令”的术语))。你可以生成像这样的东西:
progn2(
  progn2(
    read($1)
    while($1
      progn2(
        while($1
          progn2( #add the input to the total
            increment($2)
            decrement($1)
          )
        )
        progn2( #increment number of values entered, read again
          increment($3)
          read($1)
        )
      )
    )
  )
  progn2( #calculate result
    divide($1 $2 $3)
    print($1)
  )
)  
你需要使用接近真实解决方案的适应度函数。这里有一个问题,你必须以传统方式计算它。然后需要有一些东西将其翻译成(您选择的语言)的代码。请注意,由于其中存在潜在的无限循环,您将不得不在一段时间后停止执行(无法避免停机问题),并且它可能无法正常工作。此外,请注意,我提供的代码将尝试除以零。
*虽然有方法可以避开这个问题,但通常也无法大幅度避开。

请注意,您可以确信 GP 实际生成的任何解决方案都比这个更难懂 :) - Joel Rein
1
我会指向这个项目 http://jgap.sourceforge.net/doc/robocode/robocode.html - yoosiba
1
GP演化计算机程序。这些程序通常在内存中表示为树形结构,但也常见于线性结构(一系列命令)或图形。无论哪种表示方式适合您 - 没有任何限制GP使用树形结构。 - Jay
“你必须传统地计算它”并不一定,特别是当你试图找到一个反函数时(这通常比原函数更难计算)。例如,如果你正在演化一个平方根函数f(x),你可以通过测量f(x*x)x的接近程度来衡量适应性。 - Peter Olson

15

可以这样做,但对大多数应用程序来说效果非常差。

遗传算法只在适应度函数是连续的情况下有效,也就是说,您可以确定当前种群中的哪些候选者比其他人更接近解决方案,因为只有这样,您才能从一代到下一代获得改进。我曾经使用过一种适应度函数组成,其中一个部分是强权重的不连续的组件,结果发现这样很难使用遗传算法。它支配着所有其他因素,而且由于它是不连续的,所以无法逐步向更高的适应度发展,因为那些几乎正确但在该方面上不完全正确的候选者没有被视为比完全不正确的候选者更适应。

不幸的是,程序正确性是完全不连续的。一个在A行停止并出现错误X的程序是否比一个在B行停止并出现错误Y的程序好?您的程序可能距离正确还差一个字符,但仍会因错误而中止,而返回固定硬编码结果的程序至少可以通过一个测试。

更不用说代码本身在修改过程中也是不连续的问题了...


1
你是说遗传编程不存在吗?http://en.wikipedia.org/wiki/Genetic_programming - 这种方法目前在大量搜索问题中被非常成功地使用。 - JohnIdol
1
@JohnIdol:我的印象是,这是一些让学者因为有趣的论文潜力而感到欣喜若狂的东西,但实际上产生的实用结果很少,要么是微不足道的,要么是局限于特定问题类别的适当适应函数。 - Michael Borgwardt
由于我不是学术界的人,我可以理解这可能是你的印象(因为在我研究这些东西之前,我也曾有过同样的想法),但是遗传编程解决问题的效率和结果比标准遗传算法更好的情况有很多。例如,这是第一个将这种方法应用于元胞自动机的著名例子[http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.7754],它的日期是1996年。从那时起,我们已经走了很长的路。 - JohnIdol
@JohnIdol: 这就是维基百科文章中所说的,但我注意到很多链接的项目似乎都是停滞不前的。至于你提供的论文,我会说它证明了我的观点:适应性函数方便连续,被演化的程序非常简单(布尔函数甚至是图灵完备的吗?),而它解决的问题具有纯学术意义。 - Michael Borgwardt
这个问题并不仅仅是学术上的相关性,因为CA同步在数字传输、密码学等方面具有直接应用价值(即使如此,我也不认为纯粹的学术相关性会有任何区别,因为我们谈论的是通用程序)。我不会再推动这个问题了,因为我并不是在进行十字军战役或其他什么事情,但如果你有兴趣在进一步发表之前了解更多材料,那么这些材料是可以在外面找到的。 - JohnIdol

9
这是完全可能的,@Jivlain在他的(不错的)答案中指出了遗传编程是您要寻找的内容(而不是简单的遗传算法)。
遗传编程是一个尚未被广泛接受的领域,部分原因是@MichaelBorgwardt在他的答案中提到的一些复杂性。但这些只是“复杂性”,远非不可能实现。关于这个主题的研究已经进行了20多年。
Andre Koza是该领域的主要研究人员之一(请查看他的1992年的作品),他在早在1996年就展示了遗传编程如何在某些经典计算问题上(例如演化细胞自动机同步程序)表现出比朴素的遗传算法更好的性能。

这里有一篇来自Koza和Poli于2003年的良好遗传编程教程

对于最近的参考资料,你可能想看看遗传编程指南(2008年)。


遗传编程教程的链接已经失效了,你知道它已经移到哪里了吗? - Wolf
1
@Wolf,我修复了链接。可能是几年前geneticprogramming.com重新设计时出了问题。说到这个,geneticprogramming.com本身就是另一个很棒的资源 - 它有很多易于理解的高级概述不同遗传编程技术。 - seaotternerd

5
自从这个问题被提出以来,遗传编程领域已经有了一些进展,并且已经有一些尝试在传统遗传编程的树形结构之外演化代码的配置。以下是其中的几个:
  • PushGP - 设计目标是演化像人类编码者使用的模块化函数,该系统中的程序将所有变量和代码存储在不同的堆栈上(每种变量类型一个)。程序是通过推入和弹出命令和数据来编写的。
  • FINCH - 一种演化Java字节码的系统。这已经被广泛用于演化游戏代理。
  • 各种算法已经开始演化C++代码,通常有一个步骤来纠正编译器错误。这已经产生了一些成果,但并非完全没有前途。 这里有一个例子
  • Avida - 一种系统,在该系统中,代理通过使用非常简单的汇编代码演化程序(主要是布尔逻辑任务)。基于较旧(且不太灵活)的Tierra

1

语言不是问题。无论使用哪种语言,您都必须定义一些更高级别的变异,否则学习将需要很长时间。

例如,由于任何 Ruby 语言都可以用文本字符串来定义,因此您可以随机生成文本字符串并进行优化。更好的方法是仅生成合法的 Ruby 程序。但是,这也需要很长时间。

如果您正在尝试构建排序程序,并且具有“交换”、“移动”等高级操作,则成功的机会要高得多。

理论上,一堆猴子在打字机上敲打无限长的时间将输出莎士比亚的所有作品。实际上,这不是写作的实用方法。仅仅因为遗传算法可以解决优化问题,并不意味着这很容易或者甚至是一个好的解决方法。


理论上,一群猴子在无限的时间里敲打打字机将输出莎士比亚的全部作品。关键词是“理论”和“无限的时间”。假设我们忽略空格、标点和大小写,一个猴子随机击键得到一行文本,比如“生存还是毁灭,这是个问题”,概率大约是26^30的1分之1,即2.8E42的1分之1。如果你有100亿只猴子每秒钟打出一行,即使在100亿年内,你得到那一行的几率仍然只有9e14的1分之1。 - Jay
@Jay和Larry,这就是为什么遗传算法只能用于连续的适应度函数。如果你唯一的适应度测试是字符串x是否等于字符串y,那么遗传算法/程序无法确定字符串x是否更接近字符串y。这就是Michael指出的:你可以让一个程序像另一个程序一样进化(逐字逐句),但你不能逐个函数地进化它,因为没有渐进状态...程序要么工作,要么不工作(例如抛出异常)。 - Kiril
如果你知道期望的最终状态,并且以某种增量方式将每个新迭代与期望的最终状态进行比较,那么肯定最终会到达目标。但这需要知道期望的最终状态并能够确定某些东西是更接近还是更远。如果你能做到这一点,为什么不直接编写期望的最终状态的代码呢?我将不得不阅读一些引用的文章,也许有些我所缺少的东西,但我就是看不到它。 - Jay

0

遗传算法最大的卖点,就像你所说的那样,是它们非常简单易懂。它们的性能和数学背景并不是最好的,但是只要你能将问题定义为优化问题,即使你不知道如何解决问题,你也可以将其转换为遗传算法。

程序并不真正适合于遗传算法,因为代码并不是很好的染色体材料。我曾经看到有人使用(更加简单的)机器码而不是 Python 来实现类似的东西(尽管更多是生态系统模拟而不是遗传算法本身),如果您使用自动机/LISP或类似的东西对程序进行编码,您可能会有更好的运气。


另一方面,考虑到GA的吸引力以及基本上每个看到它们的人都会问同样的问题,我相信已经有人在某个地方尝试过这个 - 只是我不知道他们中是否有成功的。


成千上万的人成功地编写了能够竞争或胜过人类编写解决方案的计算机程序(遗传编程,GP):-) - Jay
哦,顺便说一下:从一个完全随机的“程序”原始池中观察到有意义的行为出现真的是纯粹的乐趣;-) - Jay
2
只是稍微纠正一下。遗传算法确实有数学背景(甚至不太容易理解):它们最大化适应度函数,并通过交叉和变异来避免局部最优解。 - Fil

0

祝你好运。

当然,你可以编写一个“变异”程序,读取一个程序并随机添加、删除或更改一些字符。然后你可以编译结果,看看输出是否比原始程序更好。(不过我们如何定义和衡量“更好”还有待商榷。)当然,99.9%的时间结果会是编译错误:语法错误、未定义的变量等等。而且肯定大部分都是完全错误的。

试试一些非常简单的问题。比如,从一个读取两个数字、将它们相加并输出总和的程序开始。假设目标是一个读取三个数字并计算总和的程序。这样的程序的长度和复杂程度当然取决于语言。假设我们有一种非常高级的语言,只需要一行代码就可以读取或写入一个数字。那么起始程序只有4行:

read x
read y
total=x+y
write total

最简单的程序来达到预期目标可能是这样的:
read x
read y
read z
total=x+y+z
write total

因此,通过随机突变,我们必须添加“read z”和“+z”,总共9个字符,包括空格和换行符。让我们在突变程序上简化操作,假设它总是插入恰好9个随机字符,并且保证它们在正确的位置,并且从仅包含26个字母加上10个数字加上14个特殊字符的字符集中进行选择,即50个字符。那么它选出正确的9个字符的概率是多少?1/50^9 = 1/2.0e15。(好吧,如果它插入的是“read w”和“+w”而不是“read z”和“+z”,那么程序也可以正常工作,但我假设它会神奇地插入恰好正确数量的字符,并且总是将它们插入正确的位置,所以我认为这个估计仍然很慷慨。)

1/2.0e15是一个非常小的概率。即使程序每秒运行一千次,并且您可以快速测试输出,每秒的成功概率仍然只有1/2.0e12,每小时的成功概率为1/5.4e8,每天的成功概率为1/2.3e7。让它运行一年,成功的几率仍然只有1/62,000。

即使是一个适度有能力的程序员也应该能在10分钟内做出这样的改变吧?

请注意,更改必须至少以正确的“数据包”形式进行。也就是说,如果一个突变产生了“reax z”,那么它距离“read z”只有一个字符,但它仍然会产生编译错误,因此失败。

同样地,添加“read z”但将计算更改为“total=x+y+w”是行不通的。根据语言的不同,你要么会因为未定义的变量而得到错误,要么最好将其设为某个默认值,比如零,并给出不正确的结果。

我想,你可以理论上进行渐进式解决方案。也许一个突变添加了新的读取语句,然后将来的一个突变更新计算。但没有计算,额外的读取是毫无意义的。如何评估程序以确定额外的读取是否“朝着正确的方向迈出了一步”?我唯一看到的方法就是让一个智能实体在每次突变后阅读代码,并查看变化是否朝着期望的目标前进。如果你有一个能做到这一点的智能设计者,那么这意味着他知道期望的目标是什么以及如何实现它。此时,与其等待随机发生想要的变化,不如直接进行所需的更改会更有效率。

这是一个相当简单易懂的程序,使用一门非常容易的编程语言。大多数程序都是由几百行甚至几千行代码组成,而且所有代码都必须协同工作。任何随机过程能够写出一个可运行的程序的可能性都是微乎其微的。

也许有一些方法可以在某些非常专业的应用程序中实现类似此类的功能,其中您并不是真正进行随机变异,而是对解决方案的参数进行逐步修改。例如,我们有一个带有一些常量值的公式,但这些值我们不知道。我们知道在一些小型输入集上的正确结果。因此,我们随机更改常量,并且如果结果更接近正确答案,则从那里开始更改;否则,回到先前的值。但即使如此,我认为进行随机更改很少会产生有效结果。按照严格的公式尝试更改常量,例如从更改千位数开始,然后是百位数,十位数等等,可能会更有帮助。


@Jay,你可以通过制定严格的变异规则来使程序编译,以符合编译器的要求,这并不是最困难的部分...最困难的部分是进化出一个能够正常工作的程序(即在运行时不会崩溃或执行某些有用的操作)。除此之外,你的分析很好。 - Kiril
1
@Jay,编译器是程序的最高级别。DNA是一个很好的例子:A只能连接T,C只能连接G,即每个DNA“节点”只能连接到特定的一组节点,GP中的节点也是如此。这可能会对你有所帮助:从技术上讲,你可以用一组最小的CPU指令来表达任何计算机程序:ADD、OR、XOR。你几乎可以保证任何这些指令的组合都将始终“编译”,但即使它确实编译了,你也无法衡量它是否更接近于打开文件或不打开文件(即文件不能是50%打开或75%打开)。 - Kiril
@Jay,基本上...看看Jivlain的回答,他说得很对。 - Kiril
@Link 好的,你可以绕过编译器直接编写机器码。如果你的计算机将每个可能的位组合实现为可执行操作码,那么它就会运行。然后,你的变异器可以随机更改或插入程序中的位。但是,通过偶然突变得到任何有用的程序的可能性甚至更小了。甚至没有编译器语言的更高级结构来强加一些理性。"程序运行"和"程序产生有意义的输出"之间存在很大的差异。 - Jay
另一个@Jay:当然,这就是我在早些时候的帖子中所说的:您可以有一组规则来限制对有效内容的更改。我毫不怀疑您可以构建一个系统,使每个突变都能编译。足够的约束可能确保它不会崩溃。但是,您添加的约束越多,这种随机突变就越少,人工智能就越多。而且,您仍然必须找出如何决定程序是否“接近正确的解决方案”,而无需进行智能分析。 - Jay
显示剩余4条评论

0
我只是想给你提个建议。虽然我不知道你能否成功,但或许你可以尝试使用基因编程来进化一个核心战争机器人。你的适应度函数很简单:就是让这些机器人在游戏中进行竞争。你可以首先用一些已知的机器人和几个随机的机器人开始,看看会发生什么。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接