遗传算法(GA)原理和Python实现

1、遗传算法概述

遗传算法是根据模拟生物进化的方式提出来的。假设,想要培养出能够适应高原气候的羊群。那么首先,我们应该先挑选出不同的羊放在高原上进行饲养,这些被挑选出来的羊被称为是一个群体。在我们挑选出来在高原上进行饲养的群体中,每一只羊在对于高原气候的适应情况是不同的,我们将能够在这种高原气候下生存的时间越长的,称为适应能力越强。我们将这种用存活时间的长短衡量的适应能力称为适应度。在这个群体中,不同的羊会进行交配,繁殖出下一代,也可以看做是不同的羊之间的产生新的基因组合。我们将这个过程称为交叉。同时,根据自然法则,羊的基因可能会发生一些基因突变的过程,我们将整个过程称为变异。最后,群体中的一部分羊会因为一些问题死去,存活下来的羊会跟随繁殖出来的下一代羊共同组成一个新的群体。这个过程周而复始,直到产生了能够很好的适应高原气候的羊群,也就是适应度达到了一定的年限。

在计算机中,我们就借助了这种生物进化的思想,提出来了遗传算法,这种算法被广泛的应用到了搜索假设空间并产生最优假设的问题。简单的来说,针对一个具体的问题,如何在其解空间中搜索出最优解。现在我们将求解的过程类比到上面的羊群进化,首先,解空间就是一个群体,群体中的每一个解或者假设就类比于每一只羊,将不同的解进行交叉,变异,就类似于羊群繁衍的基因交叉和变异。而衡量是否是最优解的标准,就是我们之前提出来的适应度,能够使得适应度最优的解就是最优解。类比到羊群就是羊能够生存的年限。

为了更为直观的表示,下面给出图示:

maddpg算法代码Pytorch python ga算法_遗传算法

2、遗传算法原理

2.1 算法描述
GA(Fitness,Fitness_threshold,p,r,m)
 /*
 	Fiteness:评分函数,为给定的假设赋予一个评估函数。(用就是用来判断一只羊可以存活多少年的函数)
 	Fitness_threshold : 终止的阈值。(当羊的存活年限达到这个阈值之后,我们认为培养成功)
 	p:群体的数量。(羊群中羊的数量)
 	r:每一步通过交叉取代群体成员的比例。(也就是下一代的羊群中,新出生的羊所占的比例)
 	m: 变异率 (新产生的羊发生基因突变的比例)
 */
 1. 初始化群体:P<-随机产生p个假设。(也就是随机选取p之羊)
 2. 评估:对于群体P中的每一个假设h,计算其适应度函数Fitness(h).(也就是算每只羊能活多少年)
 3. 迭代:条件(当前羊的最大存活年限 < 我们设定的存活年限的阈值)
 	3.1 选择:用概率的方法选择p*(1-r)个假设加入到下一代群体Ps(y也就是选择一部分羊作为可以没有
 	被自然淘汰的羊,加入到下一代的羊群中)。对于每一个假设hi而言,其被遗传到下一代的概率为
 		Pr(hi) = Fitness(hi)/∑(j=1 -> j)Fitness(hj)
 	3.2 交叉:根据上面计算出来的Pr(hi),从P中选择出r*p/2对假设,每一对的假设<h1,h2>,应用交叉算
 	子来产生两个后代。(也就是选出两只羊进行繁殖,每次可以产生两个下一代的羊。)
 	3.3 变异:使用均匀的概率从Ps中选择出m%的成员,对于每一成员,在它的表示中,随机一位取反。
 		(m%的羊的基因发生了突变)
 	3.4 更新:P <- Ps (新产生的这一代羊,取代了上一代羊)
 	3.5 评估:对于P中的每一个h计算Fitness(h)(计算这一代的羊的存活年限)
 4. 迭代结束后,说明有的假设的适应度高于了我们设定的阈值,则选择出最大适应度的假设。(也就是至少
    存在一只羊的存活年限超过了我们所设定的年限,返回这个存活时间最长的羊。
2.2 表示假设

在我们上面的算法描述中,不同的假设之间是要进行交叉来产生下一代的。那么,我们如何表示这种假设,使其能够完成下面的交叉操作呢?(也就是说我们如何表示羊的基因进行表示呢?)

在GA中,假设常常表表示为二进制的数字串,这便于进行交叉和变异操作。现在,我们采用这样的形式对假设中的各个属性进行编码。我们现在举一个实际的例子,假如,天气情况包含晴天、多云、下雨三种具体情况。风力包含强和弱两种情况。我们需要根据天气和风力来判断是否适合打球。具体情况如下所示:

maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_02

这里我只是列出来一部分的情况。由上面的图可以知道,对于判断适不适合打球的每一个假设,都包含两个属性,第一个属性天气,第二个属性是风力。下面,我们利用二进制数字分布对两个属性进行编码。对于天气属性,其有三个取值,我们采用三位二进制来表示,对于风力,其有两个属性值,我们采用的时候两个二进制位来进行表示。

编码

意义

100

晴天

010

多云

001

下雨

110

晴天或者多云

101

晴天或者下雨

011

多云或者下雨

111

三种天气中的任意一种

编码

意义

10

强风

01

弱风

11

强风或者弱风

即有上表和下表中的编码组合成每一种假设的形式。例如 1000 01表示的就是晴天、弱风的假设。注意,表示假设的每一个二进制串,对于假设的每一个属性都一个子串,这个子串不受表示其他属性的字串的影响。

2.3 遗传算子

在上面的算法描述中,我们提到了,假设的交叉操作是通过遗传算子来决定的。所谓的遗传算子,就是基因的组合方式。通过遗传算子,我们就能够确定基因的重组方式。常见的遗传算子主要包括两种形式,一种是交叉,另外一种是变异

2.3.1 交叉算子

首先看一个交叉算子的图示:

maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_03


maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_04


maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_05

交叉算子从两个双亲串中通过复制选定位来产生两个后代。在上面的图示中,左侧表示的双亲的基因表示,右侧表示通过交叉操作生成的新的两个基因表示。而中间的二进制串称为交叉掩码。交叉掩码中,对应位置为1,表示该位置的基因是从左侧上面的基因序列中复制下来的,对应位置为0,,表示该位置的基因是从左侧下面的基因序列中复制来的。同时,上面主要描述了三种交叉算子的方式。
单点交叉:在单点交叉中,掩码串的组成形式是由连续的n个1并且由连续的0来结束。这种的交叉的方式是,n个1对应位置的基因来自于第一个双亲,剩余的0的位置来自于第二个双亲。拿上图的来自来说,生成右侧上面的基因时,前5个基因来自于左侧第一个基因,后3个来自第二个基因。生成右侧下面的时候,前5个来自左侧下面,后3个来自左侧上面。
两点交叉:后代的产生通过把一个双亲串中间的一个片段替换成第二个双亲的对应位置片段。替换开始和结束的位置是随机生成的。
均匀交叉:产生一个01随机的掩码串来产生新后代。

2.3.2 变异算子

这种算子产生新的后代方式是基于双亲节点中的一个。变异算子用于对位串产生随机的小变化,方法是选取一个位,然后取反。

2.4 适度函数和假设选择

适度函数计算出了各个假设的适应度结果,在算法描述中的,根据
Pr(hi) = Fitness(hi)/∑(j=1 -> j)Fitness(hj) 可以知道,适应度函数结果越大,越容易进入下一代,越容易通过产生基因交换产生下一代。除了根据这种适应度计算概率的方式,还可以采用锦标赛选择,排序选择的方式。
锦标赛选择:先随机的从群体中挑选出两个假设,再使用事先定义的概率p来选择适应度高的假设,用(1-p)的概率选择适应度低的假设。
排序选择:群体想按照适应度大小进行排序,选择某假设的与适应度大小无关,与排好序之后,假设所处的位置有关。
轮盘赌:在这里,我们主要介绍的这一种常见的假设挑选方式。

2.4.1 轮盘赌

先给出一张图示:

maddpg算法代码Pytorch python ga算法_遗传算法_06


假设我们空间中包含4个假设,假设1,2,3,4,每一个适应度做总的适应度的比例如图所示,由之前的概率公式:

maddpg算法代码Pytorch python ga算法_遗传算法_07


可以知道,我们现在随机产生一个在[0,1]之间的概率r。r在上面的图中,落在哪一个区域内,我们就选择哪一个区域。


1、如果r<q1,就选择1假设。


2、如果qi-1<r<qi,就选择qi,其中i∈[2,n]


注意:qi称为累计率,计算公式为:


maddpg算法代码Pytorch python ga算法_遗传算法_08

3、遗传算法的应用

在实际的应用中,遗传算法可以被看做是最通用的优化算法,对于一个巨大的候选对象的空间,根据适应度函数查找表现最好的对象。在机器学习领域中,遗传算法被应用于函数逼近的问题,还被应用到向像选取人工神经网络的拓扑结构这样的任务。

3.1 遗传算法在GABIL中的应用

GABIL系统是一个概念学习系统。GA算法在GABIL中的应用主要包括以下几个方面。

3.1.1 表示

对于系统空间(群体)中的每一个假设对应的是各个属性(包括最终的判断预测)的析取集合,举一个例子,有如下规则:
IF a1 = T ^ a2 = F THEN c =T IF a2 = T THEN c = F
可以表示为:

a1 a2 c a1 a2 c

10 01 1 11 10 0

3.1.2 遗传算子

根据上面的假设表示,我们可以知道,一条假设中包含的是两个属性,一个IF IF a1 = T ^ a2 = F THEN c =T,另外一个是IF a2 = T THEN c = F。在遗传交叉的过程中,首先在第一个双亲串上选择两个交叉点,两个交叉点之间是一个片段。记录第一个交叉点距离左侧最近的属性边界的距离为d1,第二个交叉点距离左侧最近的的一个属性边界为d2,然后在去从第二个双亲节点中选择两个交叉点,要求选中的交叉点的d1和d2的距离和上一个双亲节点的d1和d2的距离相同。下面举一个例子说明:

maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_09


先给出原始的双亲节点h1和h2.

maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_10


然后,首先在h1上选择交叉点,我们选择的交叉点如红色线所示,位置是1和8。此时,1距离左侧最近的规则为1,8距离左侧最近的规则为3。现在,要在d2上确定两个交叉点,确定的位置由三种线表示出来,位置对应为(1,3),(1,8),(6,8)。假设我们选择(1,3)作为第二个片段的交叉点。则产生的新的基因为:

maddpg算法代码Pytorch python ga算法_遗传算法_11

3.1.3 适度函数

Fitness(h) = (correct(h) )^2 其中correct(h)是分类的正确率

4、遗传算法的Python实现

4.1 目标函数

关于遗传算法的实现,一般是针对目标函数实现的。在这里,我们首先给定目标函数:


maddpg算法代码Pytorch python ga算法_maddpg算法代码Pytorch_12


maddpg算法代码Pytorch python ga算法_遗传算法_13

def targetFunction(self,x):
        y = x * 5 * math.sin(5*x) + 2 * math.cos(3*x)
        return y

下面具体介绍遗传算法的实现步骤:

4.2 编码和解码

进行遗传编程首先要实现的就是,对目标函数确定编码格式,也就是将目标函数中的解采用基因方式的进行表达。编码的方式有很多种,可以直接利用实数,也可以采用二进制编码,在这里,我们选择二进制的形式进行编码。对于上面的目标函数,如果采用暴力搜索的方式,需要计算9000次。也就是在群体空间中,存在9000个假设,对于二进制的形式而言有:


maddpg算法代码Pytorch python ga算法_十进制_14


则可以使用17个二进制编码对每一个假设进行编码,同时可以采用二进制转换成十进制的方式将数据转换成十进制的形式。

def decode(self,x):
        '''
        编码,将二进制转换成十进制
        '''
        number = int(x,2)
        res = 0 + number /(pow(2,self.length) -1) * (9 - 0)
        return res
4.3 适应度函数

在实验中,我们已知了目标函数,并且我们所求的就是目标函数的max值,所以这里我们的适应度函数直接采用的就是适应度计算函数。

def fitness(self,population):
        '''
        定义适度函数
        @参数:population:种群
        @将二进制的基因片段转换成十进制,带入函数,得到的就是适应度
        '''
        
        value = []
        for i in range(len(population)):
            x = self.decode(population[i])
            #获取适应度函数的输入(x) 十进制函数值 /  总的值 * 区间长度
           
            #print("the x is: ",x)
            #计算适应度函数
            fun = self.targetFunction(x)
            #print("the fun is: ",fun)
            #这里求的是目标函数的最大值,所以直接舍弃负值
            if fun > 0:    
                value.append(fun)
            else:
                value.append(0)
        return value
4.4 假设的挑选(轮盘赌方式)

在假设挑选的过程中,主要是轮盘赌方式的应用。

#选择适应度函数值最大的,产生下一代种群
    def selection(self,population):
        #获取当前种群的适应度的值
        value = self.fitness(population)
        fitness_sum = [] #累计适应度
        for i in range(len(value)):
            if i == 0:
                fitness_sum.append(value[i]) #开始的时候,没有适应度可以累计
            else:
                fitness_sum.append(fitness_sum[i-1] + value[i])  # 累计前一步的适应度
        for i in range(len(fitness_sum)):
            if sum(value) != 0:  
                fitness_sum[i] /= sum(value) #每一个累计的适应度在总的适应度中所占的比例。
            else:
                fitness_sum[i] = fitness_sum[i] 
        
        # 选择新的种群
        select_population = []
        for i in range(len(value)):
            rand = np.random.uniform(0,1)
            for j in range(len(value)):
                if j == 0:
                    if 0<rand and rand <= fitness_sum[j]:
                        select_population.append(population[j])
                else:
                    if fitness_sum[j-1] < rand and rand <= fitness_sum[j]:
                        select_population.append(population[j])
        return select_population
4.5 交叉操作

我们这里采用的是单交叉的方式,交叉点是双亲结点的中间部分,以一定的概率从被挑选出来的假设群体中选择假设进行交叉生成新的假设,其余的假设直接进入下一代。
#交叉运算

def crossover(self,select_population,pc):
        '''
        通过挑选出种群,一部分产生下一代,一部分直接到下一代
        @参数:pc 被挑选的概率
        '''
        #选择双亲
        half = int(len(select_population) /2)
        father = select_population[:half]
        mother = select_population[:half]
        np.random.shuffle(father)
        np.random.shuffle(mother)
        next_population = []
        for i in range(half):
            son = None
            son2 = None
            #这里使用单点交叉,同时pc用于控制产生交叉的概率
            if np.random.uniform(0,1) <= pc:
                crosspoint = np.random.randint(0,int(len(father[i])/2))
                son = father[i][:crosspoint]+mother[i][crosspoint:]
                son2 = father[i][crosspoint:]+mother[i][:crosspoint]
            else:
                son = father[i]
                son2 = mother[i]
            next_population.append(son)
            next_population.append(son2)
        return next_population
4.6 变异操作
def mutation(self,next_population,pm):
        '''
        @next_population:通过交叉产生的下一代
        @pm:变异率
        '''
        for i in range(len(next_population)):
            if np.random.uniform(0,1) <= pm:
                position = np.random.randint(0,len(next_population[i])) #确定变异的位置
                if position != 0:
                    if next_population[i][position] == '1':
                        next_population[i] = next_population[i][:position] + '0' + next_population[i][position+1:]
                    else:
                        next_population[i] = next_population[i][:position] + '1' + next_population[i][position+1:]
                else:
                    if next_population[i][position] == '1':
                        next_population[i] = '0' + next_population[i][1:]
                    else:
                        next_population[i] = '1' + next_population[i][1:]
        return next_population