python入门遗传编程

来源:互联网 发布:ipad图片分类软件 编辑:程序博客网 时间:2024/06/02 04:10

遗传编程(GeneticProgramming)

《集体智慧编程》总结 

一、   背景

随着信息时代的发展,计算机的运算速度得到了飞速发展,与此同时数据量也是爆炸增长,人工智能这门课程的作用也正在于此,让我们从众多的解空间内快速地找到自己想要的数据。前段日子比较热门的“人机大战”,李世石与ALPHA GO进行一场围棋界的巅峰之战,最终以人工智能ALPHA GO的胜利而告终。早在上世纪五十年代,就已经提出了“人工智能”的概念了,并提出了一系列问题,但由于技术的限制,使得发展受到了限制。事实上,人工智能包含自然语言处理、图像识别、专家系统等,而其中涉及的算法更是众多,如神经网络、智能搜索、模式识别等,但人工智能终归是对人的意识、思维的信息过程的模拟,算法也具有许多共性。达尔文在《物种起源》里面提出“进化论”,物竞天择,优胜劣汰,一举成名天下知,所以在人工智能领域内,借助自然领域的启发,数据与算法的选择同样可以进化,这就使得在程序运行过程中,具备了进化的功能,使得算法具有自我学习的功能。本文选取人工智能算法中的一个智能进化算法----遗传算法Genetic Algorithm),借助自然界的思想,发展并深入学习遗传编程Genetic Programming),最终可以实现一种自动化的发明机器。

在此引用人工智能界著名书籍《集体智慧编程》的一段前言:

“无论是有意还是无意,越来越多投身于互联网的人们已经制造出了相当多的数据,这给我们无数潜在的机会来洞悉用户体验、商业营销、个人偏好和通常所谓的人类行为(human behavior)。一个新兴领域被称为集体智慧(collective intelligence),这一领域涵盖了诸多方法,借助这些方法我们可以从众多Web站点处提取到值得关注的重要数据;借助这些方法我们还可以从使用自己应用程序的用户那里搜集信息,并对我们所掌握的数据进行分析和理解。”

信息时代,学习人工智能很有必要。

二、   优化

在解空间内搜索最优解的过程中有许多算法,如回溯策略、图搜索策略等。有一系列优化算法被称为随机优化(stochasticoptimization),主要用于处理:受多种变量的影响,存在许多可能解的问题,以及结果因这些变量的组合而产生很大变化的问题。应用广泛,用来研究分子运动、测量蛋白质的结构、测定算法的最坏可能运行时间等问题,但由于算法是通过尝试不同题解并给题解打分以确定其质量的方式来找到最优解,所以效率比较低,提出更有效率的方法,来对随机生成的解进行智能化地修改。

随机搜索,每次产生一个随机解,打分后进行比较,查看是否为最佳解。

爬山法,由于随机尝试非常低效,所以从一个随机解开始,选择在其邻近的解集合中寻找更好的题解,重复直到相邻没有更好的题解为止。

随机重复爬山法(random-restart hill climbing),爬山法的缺陷是容易陷入局部范围内的最小值,所以随机生成多个初始解,并进行多次运行。

模拟退火算法,也是用于解决爬山法的缺陷,受物理学领域启发而来的一种优化算法。退火算法以一个问题的随机解开始,用一个变量表示温度T,每次迭代期间,随即选中题解中的某个数字,然后朝某个方向变化,即使变化后题解更差,新的题解仍可能成为新的题解。因为某种情况下,更优解转向更差的解是有必要的,但随着退火过程的不断进行,温度降低,算法越不可能接受较差的解。一个概率用来表示接受更差解的概率:

P=e(-(highcost-lowcost)/temperature)

随着temperature的降低,概率下降,接受优解概率增加。

    优化在实际的使用中很有必要,可能由于最优解的寻找过于困难,或者没有必要寻找出最优解,在有些时候,寻找到最优解并没有意义,在要求时间内寻找到一个我们满意的解就可以,例如在买水果的时候,没有必要买整个摊位的最好的水果,只要在我们面前的几个水果中挑选出我们满意的,就可以达到效果了。算法也正是模拟人的思维方式,输出一个满意的结果,这也正是人工智能中优化的作用。

三、   遗传算法

遗传算法,同样是为了解决爬山法的缺陷,受自然科学的启发。但是有一个特别的地方在于他是用进化的方式来实现优化,每一步都是选取最好的程序结果,所以我们有理由相信,最终获得的结果也是相对比较好的解法,同时借助遗传算法的思想,我们又提出遗传编程的概念,借助遗传算法中的解法,自动生成算法来解决问题,在下一节进行详细阐述,本节主要讨论遗传算法。

遗传算法的开始同一般随机优化算法类似,首先随机生成一组解,这组解的好坏是不确定的,也把这一组解称之为种群(group)。对有关问题应当编写一个评价函数,对每个解的好坏进行标准评断。

对生成的所有组解进行排序后,根据几种办法来生成下一代,一个新的种群。

(1)  精英选拔法(elitism):将当前种群中根据评价函数计算后,表现最好的题解加入到新种群中。

(2)  变异(mutation):是一种比较简单的修改题解的方法,通常只是从题解中随机进行微小的、简单的、随机的改变。

(3)  交叉(crossover)配对(breeding):选取最优的两个解,按某种方式进行结合,如从一个解中随机取出一个数字作为新题解中的某个元素,而剩余元素来自另一个解。

采取对最优解进行随机的变异和配对处理的方法,构建出一个新的种群,新种群的大小通常与旧的种群相同,重复这一过程。

三种情况会终结迭代:

(1)  达到指定迭代次数。

(2)  连续经过数代后题解都没有得到改善。

(3)  得到满意的结果。

在了解了遗传算法的大概流程后,我们就可以来了解遗传编程了。

四、   遗传编程

1、真正的人工智能

在以往遇到问题的时候,我们都会选择一种适合于解决该问题的算法,对于特定的问题还需要对参数进行调整,或者借助前文讲述的优化手段来找出一个满意的参数集来。计算机的工作就是将算法进行运行后输出结果,我们希望能够找到一种能够自动构造出解决某一问题的最佳程序的方法,通用地用于解决各种问题。即,构造出一个能够构造算法的算法。但值得注意的是,遗传编程的概念提出十分早,但受限于计算能力(computational power),随着计算机硬件的发展,我们已经能够根据遗传编程的思想构造出一个简单的程序,但仍无法完全发挥遗传编程的能力,“计算能力才是真正制约遗传编程问题解决能力的唯一因素”

2、什么是遗传编程

    遗传编程是受到生物进化理论的启发而形成的一种机器学习技术。

    它的基本工作流程如下:

(1)  初始化。算法的开始随机产生许多程序,称之为种群,这些程序可以是随机产生的,也可以使认为设计的(hand-designed),并且他们被认为是在某种程度上的一组优解(goodsolution)。

(2)  排序。这些程序会在一个由用户定义的任务(user-defined task)中展开竞争。竞争也就是一种竞赛(game),测试程序的执行效果,按表现结果进行排序。这与前文优化算法中的评价函数相同,我们都是要选择随机生成的算法中比较好的一个或几个,不同的是,优化算法中生成的解是否满足条件,而遗传编程的评价函数可能包含运行时间,空间复杂度等问题。

(3)  进化。同遗传算法类似,对表现最好的程序实施复制和修改。

1) 变异(mutation),随机修改。

2) 交叉(crossover)配对:将某个最优程序的一部分去掉,再选择其他最优程序的某一部分来代替,这样复制和修改的过程中会产生出许多新的程序来,这些程序基于原来的最优程序,但又不同于他们。

(4)  繁衍。每一次复制和修改的阶段,算法都会借助一个适当的函数对程序的质量做出评估。由于种群的大小始终保持不变,许多表现极差的程序都会从种群中被剔除出去,从而为新的程序腾出空间,这样产生的新种群被称为“下一代”,而整个过程则会一直不断地重复下去。

因为最优秀的程序一直被保留下来,而且算法又是在此基础上进行复制和修改的,所以我们有理由相信,每一代的表现都会比前一代更加出色。适者生存、优胜劣汰,同样适用于计算机的算法竞争。

(5)  结束条件:

1) 找到了最优解

2) 找到了表现足够好的解

3) 题解在历经数代之后都没有得到改善

4) 繁衍的代数达到了规定的限制

注:可能对于某些问题,如棋类游戏,根本就不存在最优解,所以只需要找到表现足够好的解,并且在学习过程中,程序的表现也依赖于对抗者的表现,遇强则强。因为算法是具有自我学习能力的,通过策略,逐步构造出一个近乎完美的算法树,而由于是随机生成的,所以很多时候生成的解看起来并不像是人类所能设计出来的算法,例如过于复杂等,但最终获得的结果都是正确的。

特别地,在遗传编程中,连同算法本身及其所有的参数,都是按照优胜劣汰的进化规律(evolutionary pressure)自动设计得到的。

    3、算法流程图


       

                                    图1

   4、遗传编程的基础框架

为了自动构造一个程序,我们首先要了解算法在计算机中的存储方式,从而设计和构造和运行一个程序。

学过《编译原理》课程的同学们都知道,我们经过“词法分析”、“语法分析”、“语义分析”以后构造出了中间代码,而中间代码的形式是多种多样的,如三元式、四元式、语义树等,根据中间代码我们就可以在对应平台上生成目标机器代码了。可想而知,我们既然要自动化构造算法,那么直接构造出中间代码形式的算法就可以运行了,而不用写成自然语言形式的代码来方便我们理解,而且中间代码也是具有可读性的,所以生成中间代码的形式是我们首先需要确定下来的。

大多数编程语言,都会生成一颗解析树,所以我们也不妨采用树的形式来构建程序。


图2

如图2所示,生成的就是一颗简单的解析树,转换为高级语言代码为:

if(X > 3)

     Value = Y + 5;

else

     Value = Y – 2;

其中Value为IF节点的值。

根据文法,我们可以递归地生成非常多的树,只需要增加节点的种类,就可以让我们的程序拥有更复杂的计算能力。如果随机初始化多棵树,就可以构建出我们的初始种群。

下面开始编码,由于python的简便性以及在科学计算中不可忽视的地位,所以我们在学习的过程中,用python来进行编码表示。

注意,运行环境为python3.5

1)创建基本的类。

fwrapper是一个记录函数的函数,说起来有点绕口,主要是保存我们需要进行操作的名字、内容、操作对象的数目。

node是一个节点类,根据fwrapper保存函数的操作名字、操作内容、操作对象,是解析树中的基本单位,设置一个函数evaluate用来求节点的值。

paramnode是一个参数节点类,保存参数的索引,例如索引值为2,传入的列表项为[1,2,3],那么evaluate返回的值就是3。

constnode是一个常数项,非变量,值即为初始值。

2)编码实现

# class fwrapper:

# function node: contains functionName,function, parameterNumber

class fwrapper:

    def__init__(self, function, childcount, name):

       self.function = function

       self.childcount = childcount

       self.name = name

 

# class node:

# contain children nodes

# initialize it with a fwrapper class

class node:

    def__init__(self, fw, children):

       self.function = fw.function

       self.name = fw.name

       self.children = children

 

    #evalutate:call the function in fwrapper class to caculate result

    defevaluate(self, inp):

       results = [n.evaluate(inp) for n in self.children]

       return self.function(results)

 

    defdisplay(self, indent=0):

       print((' ' * indent) + self.name)

       for c in self.children:

           c.display(indent + 1)

 

# class paramnode:

# return some of parameters in the givenparameters

class paramnode:

    def__init__(self, idx):

       self.idx = idx

 

    #idx is the location in the parameters tuple

    defevaluate(self, inp):

       return inp[self.idx]

 

    #print the index of parameters

    defdisplay(self, indent=0):

       print('%sp%d' % (' ' * indent, self.idx))

 

# class constnode

# return const evaluation nodes

class constnode:

    def__init__(self, v):

       self.v = v

 

    defevaluate(self, inp):

       return self.v

 

    defdisplay(self, indent=0):

        print('%s%d' % (' ' * indent, self.v))

 

具体的注释以及上文有说明,在此不再对代码解释,由于python中的编码格式十分混乱,所以方便起见,用了英语进行注解。

        3)构造操作函数集

上文中提出构造fwrapper类时需要指定操作的内容,即函数的内容,所以接下来需要构造多种函数,函数的种类将直接影响我们生成树的复杂性以及算法的功能,所以针对不同的程序需要设定不同的函数集。

下面给出几个例子。

addw =fwrapper(lambda l: l[0] + l[1], 2, 'add')

subw =fwrapper(lambda l: l[0] - l[1], 2, 'subtract')

mulw =fwrapper(lambda l: l[0] * l[1], 2, 'multiply')

 

def iffun(l):

    if (l[0] > 0):

        return l[1]

    else:

        return l[2]

ifw =fwrapper(iffun, 3, 'if')

 

def isgreater(l):

    if l[0] > l[1]:

        return 1

    else:

        return 0

gtw = fwrapper(isgreater,2, 'isgreater')

 

flist = [addw,mulw, ifw, gtw, subw]

 

最后得到的flist包含了加、减、乘、判断、比较的功能,生成的树也是从其中随机选取节点进行操作。

4)生成种群

创建随机初始种群,首先需要创建根结点并随机指定一个关联函数,然后再随机创建尽可能多的子节点,从而可以递归地生成一颗完全随机的树,因为子节点仍然可能会有他们自己的随机关联子节点。

下面定义随机生成函数。

def makerandomtree(pc, maxdepth=4, fpr=0.5, ppr=0.6):

    if random() < fpr andmaxdepth > 0:

        f = choice(flist)

        children =[makerandomtree(pc, maxdepth - 1, fpr, ppr) for i in range(f.childcount)]

        return node(f,children)

    elif random() < ppr:

        returnparamnode(randint(0, pc - 1))

    else:

        returnconstnode(randint(0, 10))

其中:

pc为指定的参数节点的范围,根据输入的节点值列表而定,例如输入的节点列表为[1,2,3],自然需要指定pc为2防止随机生成的解越界。

maxdepth为生成的树的最大深度,防止无限递归生成树。

fpr为选择一个函数作为节点,并继续递归调用生成子节点的概率,小于ppr。

ppr为停止递归生成,生成一个参数节点的概率,大于ppr生成一个常数节点。

 

通过调用这个程序我们就可以完全随机地生成一颗算法树,而不需要人工干预。

5、遗传编程的简单实例

尽管我们不断地产生随机的程序,直到找到一个正确的解为止,但有时候这种说法显得无比荒谬,因为解空间无比的大,导致我们在合理的时间范围内,找到一个正确解几乎是不可能的,所以还需要一个评价函数来验证我们离正确解的距离,以便我们能够找到表现比较满意的解来进化。

所以一个简单的数学测试,评价函数也比较容易得到,作为我们刚生成的程序的测试例子实在是再合适不过了。

首先,我们定义一个评价函数。

其中各个参数定义如下:

pc:参数列表计数

popsize:种群大小

rankfunction:评价函数

maxgen:最大的迭代数

mutationrate:变异率

breedingrate:交叉率

pexp:随机数产生的范围

pnew:产生新节点的概率

# generate a serials of solutions to choose the best , and copy andalter the solution

def evolve(pc, popsize, rankfunction, maxgen=500, mutationrate=0.1,breedingrate=0.4, pexp=0.7, pnew=0.05):

    # return a random number,usually it is a small number

    # pexp less, random numberless

    def selectindex():

        a = (log(random()) /log(pexp))

        return int(a)

 

    # generate a random group

    population = [makerandomtree(pc)for i in range(popsize)]

    for i in range(maxgen):

        scores =rankfunction(population)

        print(scores[0][0])

        if scores[0][0] == 0:break

 

        # usually we can gettwo best programs

        newpop =[scores[0][1], scores[1][1]]

 

        # generate nextgeneriation

        while len(newpop) <popsize:

            if random() >pnew:

                newpop.append(

                   mutate(crossover(scores[selectindex()][1], scores[selectindex()][1],probswap=breedingrate), pc,

                          probchange=mutationrate))

            else:

                # put a randomnode into the group, random

                newpop.append(makerandomtree(pc))

 

        population = newpop

    scores[0][1].display()

    return scores[0][1]

 

由此,我们得到了一个可以评价产生的树的优劣情况的函数,不过其中rankfunction显然要根据具体情况来判断。

比如,如下的数学问题,给出包含200个元组的列表,其中元组的内容为x、y,其中y=f(x),而f(x)=x**2+3*x+2*y+5。

我们希望通过随机生成树的形式来生成解,即根据解来得到方程。

其中,rankfunction是我们定义的与其他解差值的绝对值之和,显然绝对值越小,距离我们想要的结果越接近,当为0的时候,就是我们想要的结果。

def rankfunction(population):

        scores =[(scorefunction(t, dataset), t) for t in population]

        scores =sorted(scores, key=lambda score: score[0])

        return scores

其中scores是根据population的表现排好序的结果。

运行如下Python代码:

random1 = makerandomtree(2)

random2 = makerandomtree(2)

hiddenset = buildhiddenset()

rf = getrankfunction(buildhiddenset())

evolve(2, 500, rf, mutationrate=0.2, breedingrate=0.1, pexp=0.7, pnew=0.1)

并添加一些输出的技巧后,我们可以将树打印出来方便我们查看,

16707

10634

3200

1072

1000

398

398

398

398

398

275

275

78

52

52

52

52

0

不管运行了多久,最终总会得到了正确的结果,尽管结果树与预期的表现形式并不相同,但我们得到的结果的最简形式一定是相同的,似乎过于复杂。

由于解过长,在文中不予展示,读者可以在自己运行一下,查看自己的结果。

其他需要注意的事情:

多样性在遗传编程中是十分重要的,由于随机性,你并不知道新生成的结果是下一个爱因斯坦,亦或是毫无价值的,多样性,为我们的进化提供一条可能的路,他可能带领我们走向山峰,尽管不是目前“看得见”的山峰,也防止了局部最大化。

6、更高级的遗传编程

我们可以将遗传编程用于棋类游戏上,等待在机器群中脱颖而出的结果后,可以与人进行对决,记得ALPHA GO吗,你的程序也可以进化。

下面给出一段示例代码,关于Grid War:

def tournament(pl):

    #count the loss

    losses=[0 for p in pl]

    for i in range(len(pl)):

        for j inrange(len(pl)):

            if i==j: continue;

 

            winner =gridgame([pl[i], pl[j]])

 

            #the loser willget two credits

            #get one creditseveryone if it is a draw

            if winner == 0:

                losses[j]+=2

            elif winner == 1:

                losses[i]+=2

            elif winner == -1:

                losses[i]+=1

                losses[j]+=1

    result=[]

    for i in range(len(pl)):

       result.append((losses[i], pl[i]))

    result = sorted(result,key=lambda results: results[0])

    return result

 

winner=evolve(5, 100, tournament, maxgen=50)

通过进化100代,我们可以得到一个足够强的机器人来与我们对决,在进化的过程中,并不总是稳定地前进,因为棋类游戏与数学结果不同,他们没有一个严格的、标准的答案,也许上一局你是王者,但随机出的一个解,剑走偏锋,可以击败你,但总的来说,机器人的水平是在提升的。

五、   写在最后的话

       遗传编程的发展已有几十年,但由于计算能力的发展的速度而受限。如今随着科技的发展,也算是到了用武之地了。由词条中介绍的内容也可管中窥豹,遗传编程的作用是很明显的,也很通用。如果一个问题有了一个完美的算法,那么一个问题就不需要第二个算法来解决,但事实上,计算机界的算法多到数不完,仅仅排序便有成千上万种意想不到的方法,但没有人说哪一种是绝对好的。自然,遗传编程也并不是完美,至少在速度方面是不成功的,如果利用多项式插值,只消片刻我们便可以求得解方程。遗传编程不仅可以求方程,这便是优势,也是我们不选择其他算法的原因。

       由于本文的作者也是初次接触遗传编程,所以更多地是对本领域经典著作的转述与自我理解,希望能起到词条的一点小小作用,让更多的人了解遗传编程。

       最后的最后,以毛主席《星星之火可以燎原》一文结尾,本说革命“快要”成功的“快要”二字作何解释,我认为刚好适合当下人工智能的发展:

“它是站在海岸遥望海中已经看得见桅杆尖头了的一只航船,它是立于高山之巅远看东方已见光芒四射喷薄欲出的一轮朝日,它是躁动于母腹中的快要成熟了的一个婴儿。”

它就是人工智能。

原创粉丝点击