遗传算法(白话文版)

遗传算法是一种全局最优化算法,其很好地借鉴了自然界中生物进化的规律,通过模拟这种优胜劣汰的规律以寻找极值点。本文将从其原理,实现过程以及可行性的角度通俗地解剖这种算法!


一、算法基本思想

       遗传算法是进化算法中的一种,初见其字面其实就可以大致推知出其的思想了。遗传算法就是建立在达尔文的生物进化论基础上的,通过模拟自然界不断筛选适应度比较好的个体,并通过使它们发生交叉(生物学中的专用名词,简单地理解就是生儿育女),变异,产生的新的个体会组成新的种群,继续进行新的一轮进化,最终留下的个体都有很高的适应度,这些个体中往往包含我们需要寻找的极值点。
       太抽象不理解?没关系,笔者还为你准备了精美的演示图片:
 

  1. 很久之前,有一群快乐小猪生活在山区里,它们生活的位置都是不同的。
  1. 然而,有一天突然从山脚下升起了一股毒烟,并慢慢向山上蔓延,此时,一些生活在低海拔的小猪就被毒死了,而生活在较高海拔的小猪侥幸活了下来,但它们知道毒烟不断向上蔓延,所以它们会向高处迁居。
  1. 迁居的过程是漫长的,在这个过程中,有些小猪会相爱?,并生下后代,然后,它们自身就会死去,它们的子孙从它们父母已达到的高度继续向上迁居。这样,一代又一代,有一天,这些小猪发现自己达到了最高点附近,此时,毒烟渐渐散去,小猪们在最高点继续快乐地生活下去。

以上就是遗传算法的基本思想了。

 


二、实现过程

       接下来,我们根据上面的图片讲解遗传算法怎么实现。
       首先,我们注意,小猪的初始生活点是随机的,也就是,有的小猪生活的海拔高,有的就比较低,对应到算法中就是,算法开始,我们会随机生成一组点(当然这些点需要在可行域内)。
       然后,毒气的出现,使得一些海拔低的小猪被毒死了,而海拔高的小猪活了下来,对应到算法就是选择(Selection)的过程(有?轮盘赌算法?和锦标赛算法),在这个过程中,我们会把适应度高的点保留下来,而遗弃适应度低的点(所谓适应度可以通俗地理解为这个点对应的函数值的大小)。
       接着,活下来的小猪会生育后代,其后代会在父母已达到的高度继续向上迁居,对应算法的过程是交叉(Crossover)过程,这个过程中,我们随机抽取几对点,每对点会进行类似“中和”的操作,适应度高的点和适应度低的点“中和”生成平均适应度更高的点。这个地方我们通过一个具体的例子来进行理解:我们假设抽取的一个点的坐标为(1,1),其适应度为8,而另一个点的坐标为(2,2),适应度为2,此时,这对点的平均适应度为5,接着,我们随机生成一个随机数a(0~1之间),然后,a(1,1)+(1-a)(2,2)和(1-a)(1,1)+a(2,2)这两个点(也就是(1,1)和(2,2)连线上的两点)就是之前两个点“中和”的结果,我们会发现这两个新生成的点平均适应度往往更好,最后我们用新生成的点来取代原先的两个点。当然,交叉的操作的实现方式有很多种,上面的只是其中一种实现方式。
       然而,我们知道,生物在进化过程中可能会发生变异(上面的漫画中未体现),变异后的个体适应度可能更好,当然也可能更差,所以我们会回到选择的过程,将适应度好的保留下来,将差的剔除掉,接着继续交叉,变异,这样就开始了新的一轮循环。当迭代到一定次数时,这些点渐渐都来到了最高点附近。在上面的迭代过程中,我们会记录下适应度最好的那个体,如果,新的迭代中产生更好的个体,则将该个体记录为最好的,算法结束后,我们返回这个适应度最好的个体即可。

 


三、可行性评估

       遗传算法既然流传了几十年,解决了很多问题,从实践角度看,其肯定是可行的,在接下来的程序运行中我们也会看到。我们还需要从理论角度评估其可行性。
       首先,选择过程中,我们默认采用了?轮盘赌算法?,这个算法可以使得适应度高的个体有更大的概率保留下来,而适应度低的个体保留下来的概率则小,当然,也有可能,适应度低的个体保留下了而抛弃了适应度高的,但从概率角度看,随着测试次数的增加,往往适应度高的个体能被保留下来。
       我们再来看交叉的过程,这个过程中,当有一对个体发生交叉时,则产生的后代的适应度是居于其父母的适应度的之间的,这样看起来把适应度高的个体“拖累”了,但这样的结果是产生的后代的平均适应度往往增大了,也就是种群的平均水平提高了。
       上面讲到交叉操作使得种群的平均适应度提高了而“拖累”了适应度高的点,这样不就实现不了找最高的目标了呀。所以,我们还需要变异操作,使其中一些点发生“变异”,变异的点可能适应度更高,在下一轮的选择过程中,这些适应度高的点更有可能保留下来,组成新的种群,整个过程下来,整个种群的平均适应度基本上一直在提高(除非都到了最高点附近),而变异这种碰运气的操作使得我们可能瞎猫拖到一个最高点。
       可以说,整个遗传算法的各个阶段都是碰运气的过程,但碰到适应度高的点的可能性大,所以,在多次迭代和多次测试中,这些点往往能收敛于最高点。

 


四、具体代码

激动人心的时候来了,有了上述基础后,我们就可以阅读具体代码了(这段代码笔者在anaconda3中多次测试通过,但并不保证代码完全正确,如果你发现问题的话,可以在评论中留言,谢谢)。

# -*- coding: utf-8 -*-
"""
If you find bugs,please contact us :sj2050@vip.163.com or
									zzzain46@gmail.com or
									ruanyipu@gmail.com
Thank you! 

@author: SJ2050
"""

import numpy as np
import matplotlib.pyplot as plt

# Solve Chinese garbled problem
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False 

def ga_real(population_size,fit_func,feasible_range,**options):
    """
    input:
        popolation_size = population_size (int)
        fit_func = name of fitness value function (function) 
        feasible_range = feasible restrain region (two dimensional array)
        
        options : is_print(plot,default = True) ,
                max_iter(iteration number,default = 3*population_size),
                p_c(crossover possibility, default = 0.75),
                p_m(mutation_possibility, default = p_c/20)

    return:
        best_value_so_far,
        best_chromosome_so_far,
        search_points,
        upper,
        lower,
        average
    """
    # initialize input
    is_print = options.get('is_print')
    if (is_print==None or is_print!=False):
        is_print = True
        
#    selection = options.get('selection')
#    if (selection==None or type(selection)!=int):
#        selection = 1
    
    max_iter = options.get('max_iter')
    if(max_iter==None or type(max_iter)!= int or max_iter <= 0):
        max_iter = 3*population_size
    
    p_c = options.get('p_c')
    if(p_c==None or type(p_c) not in [int,float] or p_c <= 0 or p_c > 1 ):
        p_c = 0.75
    
    p_m = options.get('p_m')
    if (p_m==None or type(p_m) not in [int, float] or p_m<=0 or p_m > 1):
        p_m = p_c/20
    
    lower_boundary = np.array([feasible_range[i][0] for i in range(len(feasible_range))])
    upper_boundary = np.array([feasible_range[i][1] for i in range(len(feasible_range))])
    
    upper = np.array(np.zeros(max_iter+1))
    average = np.array(np.zeros(max_iter+1))
    lower = np.array(np.zeros(max_iter+1))
    search_points = np.array([[]])
    
    P = np.array(np.zeros((population_size,len(feasible_range))))
    fitness = np.array(np.zeros(population_size))
    
    # produce groups of chromosomes randomly
    for i in range(population_size):
        P[i,:] = lower_boundary + np.array([np.random.rand()*(upper_boundary[0] - lower_boundary[0]),\
                                            np.random.rand()*(upper_boundary[1] - lower_boundary[1])])
        
    best_value_so_far = 0
    
    for i in range(population_size):
        fitness[i] = fit_func(P[i,:])
    
    max_value_index = np.argmax(fitness)
    max_value = fitness[max_value_index]   

    best_value_so_far = max_value
    best_chromosome_so_far = P[max_value_index,:]  
    
    upper[0] = max_value
    average[0] = np.average(fitness)
    lower[0] = np.min(fitness)
    search_points = np.append(search_points,[best_chromosome_so_far],axis = 1)
    
    # start iteration
    for i in range(max_iter):
        # when all chromosomes become same
        if(not check_same(fitness)):      
             # Selection
             mating_pool = roulette_wheel(P[:],fitness[:]) # deliver a copy of fitness
        
        # Crossover
        new_mating_pool = mating_pool.copy()
        for j in range(population_size//2):
            ind1 = int(np.floor(np.random.rand()*population_size))
            ind2 = int(np.floor(np.random.rand()*population_size))
            while(ind1 == ind2):
                ind2 = int(np.floor(np.random.rand()*population_size))
           
            parent1 = mating_pool[ind1,:]
            parent2 = mating_pool[ind2,:]

            if (np.random.rand()<p_c):
                alpha = np.random.rand()
                offspring1 = alpha*parent1 + (1-alpha)*parent2
                offspring2 = alpha*parent2 + (1-alpha)*parent1
                new_mating_pool[ind1,:] = offspring1
                new_mating_pool[ind2,:] = offspring2
        
        # Mutation
        for j in range(population_size):
            if (np.random.rand() < p_m):
                alpha = np.random.rand()
                new_mating_pool[j,:] = alpha*new_mating_pool[j,:] + \
                                        (1-alpha)*(lower_boundary + np.array([np.random.rand()*(upper_boundary[0] - lower_boundary[0]),\
                                                                              np.random.rand()*(upper_boundary[1] - lower_boundary[1])]))            
        P = new_mating_pool
        
        # Evaluation
        for j in range(population_size):
            fitness[j] = fit_func(P[j,:])
        
        max_value_index = np.argmax(fitness)
        max_value = fitness[max_value_index]   
        
        if (max_value > best_value_so_far): 
            best_value_so_far = max_value
            best_chromosome_so_far = P[max_value_index,:]
          
        upper[i+1] = max_value
        average[i+1] = np.average(fitness)
        lower[i+1] = np.min(fitness)
        search_points = np.concatenate((search_points,[P[max_value_index,:]]))
    
    # Plotting
    if is_print:
        plt.plot(range(0,max_iter+1),upper,'o:',range(0,max_iter+1),average,'x-',range(0,max_iter+1),lower,'*--')
        plt.title('遗传算法(real_num)迭代变化图')
        plt.legend(('最好','平均','最差'))
        plt.xlabel('迭代次数')
        plt.ylabel('适应度')
        plt.show()
    
    return best_value_so_far,best_chromosome_so_far,search_points,upper,lower,average
    
# one way to achieve crossover process    
def roulette_wheel(chromosomes,fitness):
    fitness = fitness - min(fitness) # to keep fitness positive
    each_fitness_proportion = fitness/np.sum(fitness)
    cumulation_proportion = 0
    mating_pool = np.array(np.zeros(chromosomes.shape))
    
    for i in range(len(fitness)):
        random_num =  np.random.rand()
        
        for j in range(len(fitness)):
            if (cumulation_proportion <= random_num and random_num < cumulation_proportion + each_fitness_proportion[j]):
                mating_pool[i,:] = chromosomes[j,:]
                break
            
            cumulation_proportion += each_fitness_proportion[j]

        cumulation_proportion = 0
        
    return mating_pool

# check whether all the chromosomes become same
def check_same(fitness):
    fitness = fitness - min(fitness) # to keep fitness positive
    # to check whether all the chromosomes are same 
    for i in range(len(fitness)):
        if (fitness[i]!=0):
            return False

    return True

if __name__ == '__main__':
    def fit_func(chromosome):
        x,y = chromosome[0],chromosome[1]
        
        return 3*(1-x)**2*np.e**(-x**2-(y+1)**2) - 10*(x/5-x**3-y**5)*np.e**(-x**2-y**2) - (np.e**(-(x+1)**2-y**2))/3
    
    best_value,best_index,search_points,upper,lower,average = ga_real(20,fit_func,[[-3,3],[-3,3]],is_print = True)
    print(best_value)
    print(best_index)

这个代码是实数版的遗传代码实现(我们这里也附上二进制版的遗传代码),选择过程选择了?轮盘赌算法?,交叉过程之前已提到过。我在这个代码的必要部分已经做好了注释,如果读者按照之前讲的那些思想来阅读代码的相应片段的,我相信,这些并不能难倒聪明的你。

 


五、运行结果

       在笔者多次调试和修改后,这个代码的稳定性很好,收敛速度也很快,基本可以找到可行域中的最高点,偏差2%左右,如果多运行几次,所得结果基本可和真值媲美。

在这里插入图片描述

通过这个算法,我们也可以体会到大自然的美,顺自然者兴,逆自然者亡。

 
 

最后,感谢阅读!如果你觉得可以的话,也可以为我点赞?哦!

 
 
 
 
 
 
 
 
 
 
 
 
 
 


  • 参考资料
    1、《An introduction to optimization 4th edition》
    2、某高校最优化课件
    3、轮盘赌算法
posted @ 2019-03-01 17:56  SJ2050  阅读(462)  评论(0编辑  收藏  举报