心得:基于遗传算法的虚拟捡罐子机器人

心得:基于遗传算法的虚拟捡罐子机器人

一、概述

      上周我在阅读Melanie Mitchell 《复杂》一书中看到了一种用遗传算法进化的虚拟机器人。存在一个虚拟的10 × 10 棋盘,上面随机放着一些罐子。有一个机器人,他的体内有一串由 243 0~6 数字组成的序列(基因)。机器人能够探知自己所在格子以及东西南北四个方向格子的状态(空 / 罐子 / 墙壁)。机器人根据观察到的布局在基因序列中找到对应的行动方式——向北、向南、向东、向西、静止、捡拾、随机移动七种方式。最初机器人的基因序列是随机的,但是通过遗传进化数百代后机器人具有了一定的智能(至少是表观智能),能够完成捡罐子的任务。
      这是一个简单的人工智能,我对此也是兴趣浓厚想要自己尝试着做一个。不过书中仅仅给出机器人与进化系统的描述以及一些控制判断参数,没有给出任何具体实现方法,所以得自己来。我书架里另一本书《机器学习》中讲解了遗传算法的工作机制,不过和这里的应用略有不同。我在看书的时候想到过几种拓展,不过我想先遵照书上的模式验证一下再去实现新的。

      在一周的努力之后我实现了这个程序,为了给自己的努力留点纪念,也不让被自己玩物丧志消耗掉的时间不至于全部浪费,也为保存一下自己在实现这个程序过程中流动的思维,我写一篇小心得笔记。

二、程序实现

      由于本人关于程序的课程基础只有《C语言程序设计》的选手水平,只是大一寒假里自己看过一点OpenGL的绘图编程,外加大二寒假也就是一个月前草草看过一点点C++,所以只用原始的方式去实现。大神们请轻吐槽。

      整个工程分为两个独立程序,其中一个是进化程序,包含了虚拟机器人的控制和进化系统;另一个是演示程序,功能是将基因读入然后用图形的方式展现出机器人的运动(进化系统里的机器人控制仅仅是数据的变动,没有图形意义)。本文描述的是前者的实现,后者和遗传算法无关只是个显示系统,不是讨论的重点。

      本程序使用C++语言实现,虽然即使以基础课本的水平衡量C++我也只是知道一些皮毛,但是这种明显有对象概念的程序用C貌似真吃不消。这个程序总共大约是1000行多一点,已经算是我写过的最长的程序了。我曾经一度完成了这个程序,但是随着零碎的一些改动程序变得很混乱,最后我又重新写了一遍完成现在的程序。其中一些我会在后面提到的。

1. 结构

      我一共编写了三个类来完成这个系统,第一个类是“遗传物质(class GermPlasm)”,负责处理基因,以及该条基因对应的分数、适应度。里面的函数实现了包括基因突变和基因重组的功能。第二个类是“虚拟机器人(class IropI)”,这个类的名字意思是智能捡拾机器人I型(Intelligent Robot of Picking I),随手造的名字。很显然这个类实现了和虚拟机器人有关的一切操作:生成棋盘空间并布置罐子,根据机器人当前的位置得到周围环境的信息,并从基因中选取相应的操作编码,并计算分数。第三个类是“基因库(class GenePool)”,是进化系统的核心,负责生产初代种群,让种群中每个个体“度过其短暂的一生”——然后轮回几百次以求得分数平均值(真是灭绝人性的上帝),根据分数随机选择个体“幸存”并且对其进行“繁殖”,如此往复,均为该类的任务。

2. 遗传物质

      遗传物质里贮存了和基因有关的一切信息,除了基因序列外记录了分数以及由此计算出的适应度。一个原创的属性是“家族”,这是一个辅助用(或者是娱乐)的数据。另外还有两个所谓的幸存计数器和相应的满标志,这两个我将在基因库一节中提到生存斗争机制时解释。

      基因:基因序列是这个类的核心,实质是一个动态分配的整型数组。与之配套的操作函数有转录(Transcription)(虽然叫复制比较科学)、重组(Recombination)、突变(Mutation)、幸存(Survive)。

      转录: 顾名思义,转录便是将一段基因复制到另一段上(喂转录是RNA好伐)。什么特别的,输入一个整型指针然后将指针对应的数据复制到自己这儿,如果指针为空则随机生成一段序列。

      重组:重组函数用于生成子代基因,输入为两个遗传物质的引用,这个函数将双亲的基因数据各取部分生成自身的基因,同时继承双亲的家族属性(下文提及)。在组合基因的时候方式是不定的:一种方式是前半段取自父亲,后半段取自母亲;另一种方式是对每个基因都通过随机数从双亲中的一个里取。后一个看起来似乎比较合理,但一来计算随机数开销增加,二来可能会导致子女均错过优良的单个基因,因为生成子女是交换双亲顺序产生的两个个体,前一种方法确保双亲的基因全部遗传,但带来的另一个问题是如果两个优良单个基因分别在双亲前半段,那么这两个基因是铁定没机会合并了。但这些问题对程序有实际影响尚不知道,未作试验。

      突变:很显然无论怎么重组都只能在有限范围内变动,如果要产生进化变异是不可少的。和重组一样,此处的变异有两种机制:一是整条基因上每个单个基因都有一定小概率突变,二是在基因上随机选择一个单个基因,以一个大概率发生随机改变。第一种情形可以使基因迅速大程度改变,可能形成基因多样性促进进化,同样可能破坏已有的优良基因;而后者的速度较慢,命中重要基因的概率也小一点(具体取决于实际的概率选取)。在本程序中出于偶然的灵感采用了一种“近亲惩罚”的机制,使得在不同的情况下应用了两种变异方式。后文将叙述这种机制。

      幸存:幸存是当基因所属的个体有幸在生存斗争中幸存下来时所调用的函数。幸存和转录类似,都是基因的复制。此处需要将幸存的基因信息从父代种群拷贝到子代种群。转录只是单纯的基因序列的复制,而幸存函数则同时复制得分、家族情况等(幸存函数调用了转录函数)。

      分数、适应度:分数记录了该基因在机器人身上取得的平均得分。机器人的运转和平均分的计算由基因库类负责,此处只是单纯地接受传递进的数据。适应度很类似分数,事实上它就是直接由得分计算而来,直接衡量了基因的生存概率。该参数的使用在后文解释。对应的操作便是适应度的计算。

      适应度计算:这个操作只是单纯的一个计算。在最早的版本里,计算的公式只是一个线性的转换,将所有基因的得分偏置到正数的范围内,以便计算概率。但是不久发现这对优良基因是不公平的。拥有单个优良基因的个体的分数可能比同类高上十几二十分,但是得分的基数可能是三位数的(刚开始演化的时候可能是负几百份),这样这些优势给个体的生存机会可能只比没有优势基因的个体高几个百分点,如果统计基数大而演化时间足够长这种微弱优势可能是占优的,但是我们的程序演化时间并不长(核心原因并不是算不了那么长而是不能接受低效率的计算),基数也很小:两百次轮回的平均值并不是稳定的,同样的基因计算两次轮回组得出的平均值也有相当的波动。因此我们必须放大优良基因的优势。考虑到分数演化的跨度不小,因此最好是生存率奖励稳定的公式。这种时候很自然是想到指数函数,指数函数不仅能够放大优势,还能直接实现正数化,同时还是单调函数——几乎再适合不过了。直接以e为底的指数函数对优势的放大太过了,经常能让初期差距较大的基因拉开好多个数量级,因此我们让输入的数字先除了一个整数弱化放大作用。整数的具体取值取决于分数计算系统,定量即可无须精确。

      家族:家族概念是我灵机一动想到的,不过我后来瞄了一些文献的题目发现很多实现里都有类似的机制。最早想到这个机制是因为我想知道那些占优势的基因是如何在种群中扩散的。因此我在最初初始化的群体中给每个基因都编了号,称为不同的家族。每个基因都有父系家族和母系家族两个编号(初始个体两个编号相同)。每当基因重组产生新个体时,父系和母系家族编号会分别继承自双亲的父代家族编号。本来这个设计只是为了好玩,和功能毫无关系,但是后来我发现这个编号的用途。在实验中发现,初始种群只需要并不很多的若干代,就会被一个家族所独占,似乎偶尔会出现两个家族联姻共治。这就带来一个问题,当一个家族独占种群后,所有个体的基因都几乎相同,这就导致基因库的贫乏。为了阻止——或者说减缓这种独占的速度,我很自然想到现实个体中近亲结婚多杯具的现象。家族机制在此就起作用了,我设定如果重组后的个体父母系家族相同,则在变异时对每个基因都产生相对大概率的变异(概率是普通情形下的1/3,考虑到这是对每个基因的突变概率这是一个很大的数了)。

3. 智能捡拾机器人I

      简称虚拟机器人,这个类里处理了所有有关机器人运转的功能。重要数据包括棋盘数据、基因数据、机器人当前的坐标、得分,以及一些基础参数。

      在最初的版本设计中,这部分功能被分为三个类:棋盘、机器人以及控制两者交互的类(我称为灵魂?),但是这种设计相当失败,最后控制很混乱。因此在新的版本中我将其并成一个类。

      机器人生命的计算流程并不复杂也不是重点,因此我简述一下。

      首先根据参数对棋盘进行初始化(尺寸,放置罐子的比例,机器人最大允许的步数),分配储存棋盘数据(根据概率随机生成空地或者罐子)和基因数据的空间。基因数据我本考虑使用指向之前的遗传物质类的指针,但是在老版本中这么做导致对象建立销毁时动态空间混乱所以在新版本中单独开辟一段空间储存基因数据,以使两个类撇清纠结的关系。

      随后,重复一下步骤。首先机器人进行观察,根据机器人所在坐标观察东西南北中五个格子的状态(空/罐/墙),将观察到的结果排成一个三进制序列(北南东西中,挪用了书中的顺序),然后转换成十进制数返回。比如北南东西中分别是01201,那十进制数便是1+2*9+1*27=46。机器人将这个十进制数(0~3^5)作为下标在基因数据中找寻到对应的基因。基因如此定义:

0:向北走一步

1:向南走一步

2:向东走一步

3:向西走一步

4:停机

5:捡拾

6:随机移动一部

      机器人根据基因的结果返回他的行动,交由另一个函数判断是否成功并执行相应的动作,并且根据不同的动作及成功与否进行加分或者扣分。当步长超过最大限制时,返回所取得的分数。

      实现过程中会有一些小小的麻烦,但这不是本文要说明的。虚拟机器人类和剩下两个类完全独立。对外界而言,所有要做的便是初始化世界状态然后调用一个函数Run一下程序,函数便会返回所取得的分数。

4. 基因库

      基因库是进化系统的核心,功能上类似于进化论本身。主要的数据是种群和其子代的遗传物质信息(这便是上文中遗传物质类的对象),此外还有一些描述种群的参数,如种群规模、历史长度(进化次数)、每代的繁衍率(或者存活率,用这个数据计算出每代留存的个体和基因重组新生的个体)、一些用于统计和作为标志的数据,另外一些参数是用来传递给虚拟机器人的。

      整个进化过程分为如下步骤(每个步骤由分立函数实现):

  • 令种群的每个个体度过其一生计算分数和适应度
  • 生存斗争,根据适应度以概率选出一定数量个体幸存
  • 令幸存个体相互繁衍,通过基因重组产生子女个体
  • 交换子代和父代,并清理中间变量,输出统计日志

  • 生活度日

      在这个函数中,父代的每个个体基因被取出转录入虚拟机器人的基因中,并运行200次,累加每次的得分然后取平均值记入该个体的分数中,计算出适应度。同时统计最高分个体和整个种群的平均分作为统计参考。

    • 生存斗争

      在生存斗争中,个体依据适应度被随机选出送入子代种群中,未被选中的个体会被时代淘汰。随机选取的过程的最简单版本是这样实现的:计算整个种群所有个体的适应度总和,然后在0和总和之间产生一个随机数。另取一个初始值为0的变量Sum,依照下标顺序累加每个个体的适应度,每次都与那个随机数比较,若Sum的值超过了这个固定的随机数,则返回此时的个体下标。这个个体便是有幸被选中的个体。这个算法是寒假里想要取得正态分布随机数时想到的做法。也许还有更好的算法但我不知道。

      这个算法本身是可行的,但在应用时出现了麻烦。因为先前在计算适应度时应用了指数函数,因此当有杰出个体脱颖而出时,计算所得的生存几率要比其他个体高得多,有时甚至超过了70%(尤其是开局时)。这样一来的结果就是只需一代某个家族便能子孙满天下塞爆这个种群。考虑到基因的多样性,这是不希望的。我们希望这些个体既能够确定无疑地将基因在种群中传递下去,也希望能够不要垄断,给其他的基因一些生存空间。因此我给算法加入了另一个机制:幸存限制。我在遗传物质类中加入了所谓的幸存计数器和幸存满标志。每当一个基因成功“幸存”了一次,计数器就将加一,当计数器到达一定数值时满标志置位,此时就说明该个体不能再幸存了。

      这个机制联动着随机算法本身也要修改。原本适应度计算一次便够了,如今每当一个个体不再列入选择名单,这个个体的适应度便要被减去。但是因为某些高适应度要比其他适应度高出几个数量级,相减之后就会使浮点数的精度不够。因此必须从头计算以便总和,并且在计算过程中排除掉那些不在名单中的个体。这让计算过程复杂了许多。但是整个进化系统所花费的时间要远远小于虚拟机器人运行的过程,多出的时间完全可以忽略。

    • 种族繁衍

      这个函数负责随机从幸存的子代个体里选择两个个体进行基因重组,产生出两个后代——两个亲代个体分别相互作为父母。选择的方式和前者所用的选择方法完全相同,在我的程序中两者各自使用一组计数器和标志位。但是事实上这两个完全可以复用,只要及时做好清理工作。此外在我的程序中如果父母个体“用完了”这种情况也是没定义的,如果有人想要自行实现的话可以将标志和计数器全部清空重新选。

    • 交换父子代及日志输出

当子代个体数量达到种群规模的时候,一个新的种群就诞生了。将父代和子代交换——直接交换两个指针即可。

这个新的种群是由老种群的优秀个体及其组合后代构成的集合,对上述过程反复操作知道达到指定次数后便可完成进化。为了更好观察过程我同时也做了个日志输出的功能,将每代的最高分和平均分记录,并记录最高分个体的基因序列。

三、进化收敛

进化系统的收敛问题是能否成功的关键。从机器学习的观点来看,进化算法便是在假设空间中用一大波完全随机的假设产生随机位移,每次筛选出误差较小(或者分数较高)的一批并进行相互组合插值,最后试图收敛到全局最优的过程。

我的程序远远有到全局最优,我估计它也做不到。进化程序的优良程度和太多的参数和算法有关系,要完全弄清需要很多的研究(但我只是个听说有这东西就凑上来试一下的渣渣)。但我可以根据这一周来无数次的失败大致猜一下一些基本的条件。

首先一个判断误差的算法必须能够真实反映出人类对这个智能好坏的评价。在这个程序中判断的标准便是得分的高低(还受到适应度计算公式的影响)。在原先的诸多版本中,我所使用的评分机制和《复杂》一书中给出的例子相同:如果机器人成功搜集了一个罐子则加10分,如果对一个没有罐子的地方进行搜集则扣1分,撞到墙扣5分。相当长的实验时间中我都是按照这个分数来运行的。但是在早期一些相对成功的实验中,进化的速度很慢,尽管很快从负分变成接近0的分数,但是后面的增长几乎是蜗牛爬式的线性增长。而在后期的程序中,不知道为什么算法虽然能很快收敛到0,但却始终进化不出正分的个体。后来我尝试性地将搜集罐子的奖励提高到50分,结果机器人迅速进化。但是这并不代表给目标要求尽可能高的给分就能达到目的,假设搜集罐子的给分很高而其他扣分很低,那么机器人会“发现”撞到墙是无关紧要的,于是他会完全采用随机误打误撞的方式,只要遇到罐子能捡起来就好了,无所谓撞几次墙——不过限制最大步数也能间接促进机器人用有效的方式捡罐子。从另一个角度理解,机器人最后进化到的程度是对各个衡量指标(捡罐子数量、撞墙次数)的一个综合均衡。比如机器人为了能捡到罐子可能会不介意随机移动稍微多撞几次墙。

另一个会影响收敛的是所谓的“局部极小值问题”。众所周知极小值并不意味着最小值,只是意味着函数曲面(可能是高维曲面)在这里特别的平坦。这就意味着,在这个区域内,不同的参数并不能引起优势的明显变化,也就很难使那些努力往外拓展的基因脱颖而出有机会争取到繁衍的机会。还有一种类似的可能是一个基因的改变效果太弱以至于淹没在进化系统的随机性波动中了,基因突变改变基因太少可能会产生这类现象,种群被一个家族统一基因太过相近以至于基因重组失效也会出现这种情况。所以我们必须尽力避免,一方面用指数函数扩大基因优势的影响(同时也放大了噪声,因此基因优势至少要比噪声强或者依赖大样本的统计),另一方面利用近亲惩罚使近亲重组时产生大量变异。

最小值收敛的学问太复杂了,机器学习翻来覆去研究的就是如何让学习系统误差最小化。遗传算法虽然神奇且强大,但绝非万能和全自动。引用网上一篇文章关于人工神经网络的评价:“要想得到准确度高的模型必须认真的进行数据清洗、整理、转换、选择等工作,对任何数据挖掘技术都是这样。”遗传算法应该也是如此。

四、拓展

如文章开头处所言,我在写这份程序之前已经有一些拓展的想法,但决定先完成这个最简单的版本再说。毕竟已经有前人完成过了,我要做的只是验证她的工作——而我还远远没能做到她的实验结果——尽管在书中描述得很轻巧,但毫无疑问整个系统是由一个这方面的专家来编写和优化的,付出的研究也相当可观。就如同我在写这份心得的时候,想着自己修改和调整程序的时候是多么的纠结和痛苦,但是却发现最后修改完成的结果是那么的简单,几句话就能解释。这也给出了一个新的真理:很多事不亲自实践永远也不知道。

在开始写这个程序之前,我正在试图写人工神经网络。那个过程和写遗传算法一样纠结:明明什么都对就是不工作。直到后来我把某个参数(学习速率)整整调大了两个数量级,整个网络突然能用了。当时我用的是两个样本去训练的,没有完整检验。当时急急地想实现这个工程。我感到进化算法和人工神经网络本身就可以是一体的——前者是生物演化的机制,而后者本身就是生物思维系统的模拟。完全可以用进化算法去训练人工神经网络,而神经网络是既能处理离散值也能处理连续值的。人工神经网络作为虚拟机器人的思维工具有两个明显的好处:一个是可以处理连续值,能够在真实的物理环境下而不是离散的棋盘格环境下工作;第二个是可以对基因信息进行压缩——采用矩阵网络而不是像这个系统一样一对一地输出,大大减少了数据量。想象一下这个系统简单到仅仅只有243中情况,因此才可以用直接寻址的方法去编码基因,而就算是从5格扩展到九宫格,基因量也是不容小觑的;另一方面,一一对应的机器人本身不具有类比性,它可能知道南边有罐子要去捡却不知道北边有罐子也能捡,这用神经网络可能可以解决——谁知道呢,人工神经网络本身的机制至今也没有人能完美解释。

上一个拓展猜想是改变其思维机制和基因作用。另一个拓展猜想则是增加记忆项使机器人拥有记忆。仍然用已经实现的这个简单机器人做讨论。现在的机器人只能依据现状来决定如何行动,因此非常可能发生原地打转或者来来回回的情形——因为他不知道刚才做了什么。让机器人拥有记忆的最好方法不是让上一次观察到的结果也作为本次输入的一部分,而是建立一些标志位,让机器人的行动成为观测结果和标志位共同作用的函数,而机器人的行动清单中也包括对这些标志位进行置位或者清零。想像一个理想的机器人,他可能处在东西两个罐子之间,他也许会置位标志位然后去捡东边的罐子,标志位会提醒他西边仍然有个罐子让他记得回去捡。但是机器人会用这些标志储存些什么信号我们是不知道的,那由遗传和进化决定——就是这么神奇。

以上就是我对这个系统拓展的小想法。我不一定有精力去实现他们,因为作为一个学电路电子的学生把大把经历放在软件上却放着硬件不管好像不太好。但又有什么办法?两边同时开工,但软件比硬件好学的多,自然学着学着就一头栽进软件了(硬件却连大门都还没打开)。但人工智能确实非常非常吸引我(任何人都一样吧),也许那天我会有机会把那些智能在硬件上以某些形式展现吧,一定很好玩。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值