【未完结】使用python的numpy实现遗传算法

一、关于遗传算法

遗传算法的原理可以听清华的马少平老师讲解,马少平的个人空间-马少平个人主页-哔哩哔哩视频

其中《第四篇 如何用随机方法求解组合优化问题(八)~(十一)》是关于遗传算法的讲解。

下面把视频中的一些精华内容进行摘录。

(一)算法原理

遗传算法主要是受到进化论的启发,根据生物在进化中优胜劣汰的自然选择,使得种群逐步优化,逐渐保留优良物种。

其中,生物进化与遗传算法之间的对应关系如图所示。

(二)名词解释

1、编码与染色体

编码是指针对被求解的对象,进行合适的变换,使之能够满足使用遗传算法的要求。举个例子,要求解x∈[0,31]的函数最值问题,那对自变量x,可以使用二进制编码:使用5位二进制表示,00000表示0,00001表示1,....,其中00000就代表了染色体

需要注意的是:编码方式与自变量真实值无特别要求,比如你可以要求00000代表x=31时的

染色体,但是为了符合我们的认知,默认00000为x=0时的染色体。

另外,编码方式不止十进制转二进制这一种,针对不同的问题,要选择合适的编码方式。

2、选择

假设有N个可能的个体,用x_i表示第i个个体(i=1,...N),则x_1,x_2,...x_N表示了一个种群

假设F(x_i)为个体x_i的适应度,那么选择操作就是要求:适应度大的个体被选中的概率越大,满足适者生存的条件。而选择的个数等于种群中个体的个数(要保证每次迭代的个体数量不变)。

而选择概率可以用公式表示:

常用的选择方法主要有两种,第1种最常用,但是不好调试。第2种方便调试。

(1)轮盘赌选择法

如图所示,在这张图中, x_1,x_2,...x_N首尾相连连成了一个圆盘,每个个体都在这个圆盘中占有一定空间。这个空间的大小可以用上面提到的选择概率P(x_i)表示。

轮盘赌算法的伪代码如下:

python代码如下:

import numpy as np
def select(population,fitness):
    # 方法1: 赌轮盘选择法
    idx = np.random.choice(np.arange(POP_SIZE),size=POP_SIZE,replace=True,
                           p = fitness/(fitness.sum()))
    return population[idx]

这里使用了np.random.choice函数,可以按照概率p,重复选择(replace=True)POP_SIZE个个体,被选择的为[0,POP_SIZE)的数组,选择后将返回对应的索引。

(2)确定性选择算法

确定性选择算法的原理如下:

用一个例子解释:

根据选择概率和个体数,可以算出每个个个体的期望次数(如序号1的期望次数为14.44%×4=0.5776 ≈0.58)。

确定性选择算法的选择过程是从左到右比较每一位数的大小

在个位数上,只有序号2和序号4不为0,所以选中次数各+1,此时选中次数为2,2<4,继续执行选择操作;

再看小数点后1位,序号2为9,序号1为5,都比序号3和序号4的大,选中次数各+1,此时选中次数为2+2=4,停止选择。

python代码:

def select(population,fitness):
    # 确定性方法:
    # 选择概率x个体数量 -> float -> 根据个位、小数点各个位数的大小从左到右选择
    num = 5                                             # 取num位有效数字, 1.895表示4位有效数字
    p = fitness/(fitness.sum())                         # 每个个体被选中的概率
    Expect_Times = POP_SIZE * p                         # 每个个体被选中的次数的期望值(float型数组)
    Expect_Times_str_list = []
    for i in range(POP_SIZE):
        Expect_Times_str_list.append(str(Expect_Times[i]))
    
    # 从左到右比较,进行确定性选择
    cnt_lst = [0 for i in range(POP_SIZE)]              # 初始化每个个体的被选择次数都为0
    Expect_Times_dict = {}                              # str: int, 位数数字(str):索引
    for i in range(num + 1):                            # 控制位数,考虑小数点所以要+1
        temp_lst= []                                    # 用于存储每一个个体的第i位数
        for j in range(POP_SIZE):                       # 控制个体
            Expect_Times_dict[Expect_Times_str_list[j][i]] = j
            if Expect_Times_str_list[j][i] == '.':      # 遇到小数点就跳过
                pass
            elif Expect_Times_str_list[j][i] != '0':    # 只保存大于0的位数
                temp_lst.append(Expect_Times_str_list[j][i])
        if Expect_Times_str_list[j][i] != '.':
            temp_lst = sorted(temp_lst)                 # 排序,优先选择数字大的
            for k in temp_lst:
                cnt_lst[Expect_Times_dict[k]] += 1
                if sum(cnt_lst) == POP_SIZE:            # 当选择数量足够就退出
                    return population[cnt_lst]

3、交叉

交叉操作就比较好理解了,但是根据不同的编码方式有不同的交叉类型。

一般交叉操作如下:

需要注意的是,不是每一个个体都会交叉,且被交叉的染色体是随机,并非一定是相邻的染色体。有时为了讲解方便,会指定与相邻染色体进行交叉。

 python代码实现:

def cross_over(population,P_C):
    for i in range(POP_SIZE):
        # 不是每一个个体都会执行交叉操作
        if np.random.rand() < P_C:            
            parent1 = population[i]                                 # 父本
            idx = np.random.randint(0,POP_SIZE)
            while idx == i:
                idx = np.random.randint(0,POP_SIZE)               # 随机选择一条不是自己的个体进行交叉
            parent2 = population[idx]                               # 母本
            # 交叉方式为常规交叉法
            point = np.random.randint(0,POP_SIZE)
            child = np.r_[parent1[:point+1,],parent2[point+1:,]]    # 合并数组
            population[i] = child
    return population

 4、变异

对于使用二进制编码的染色体,变异操作就很简单了:无非是取反罢了。

同样的,不是所有个体都会发生变异

python代码实现:(我如果使用位运算的话会报错,所以直接使用if-else,也很方便)

def mutation(population,P_M):
    for i in range(POP_SIZE):
        # 不是所有个体都会变异
        if np.random.rand() < P_M:
            point = np.random.randint(0,CHROMOSOME_LENGTH)  # [0,CHROMOSOME_LENGTH)
            if population[i][point] == 1:  # 相当于取反操作
                population[i][point] = 0
            else:
                population[i][point] = 1
    return population

(三) 算法流程

至此,所有名词解释都已完成,读者需要根据实际问题选择合适的编码方式。后面我会把一些编码、选择、变异的技巧写在文章后半部分,感兴趣的可以观看马少平老师的视频

(四)注意事项

1. 遗传算法的最优结果不一定在最后一代,可能在进化中的某一代

2. 遗传算法的终止条件为:适应度收敛/达到一定的迭代次数/...

3. 遗传算法只能用于求最大值,但是遇到求最小值的问题时可以做一个变换(非线性加速):

F(x) = 1/f(x)-f_min, if f(x) > f_min

     = M, else  (M为一个较大的值,超参数)

f_min为目前得到的最好解

4. 编码长度和精度之间的关系:

    n >= log2 ((b - a) / epsilon + 1), 取值范围[a,b], 允许误差epsilon

这意味着精度不再是1,而可以是0.5、0.1等。

二、案例一:使用遗传算法求函数最大值问题

这里以香蕉函数为例:

香蕉函数的表达式为:

函数图像为:

 完整代码如下:(之前是测试视频中的例子,求x∈[0,31],y=x^2的最大值。感兴趣的读者可以修改一下代码查看结果)

import numpy as np

CHROMOSOME_LENGTH = 5   # 编码长度,与问题规模有关
POP_SIZE = 10           # 个体数量
P_C = 0.6               # 交叉率(Percentage of Cross over)
P_M = 0.1              # 变异率(Percentage of Mutation)
GENERATIONS = 100       # 迭代次数
ERR = 1e-10              # 退出循环要求的适应度精度
# X_BOUND = [0,31]        # 自变量范围
X_BOUND = [-2.048,2.048]

def Func(x:float,y:float) -> float:
    # 香蕉函数
    func = 100.0 * (y - x ** 2.0) ** 2.0 + (1 - x) ** 2.0
    return func

# 求适应度
def get_fitness(population):
    '''
    input: (POP_SIZE,CHROMOSOME_LENGTH)
    output: (POP_SIZE,)
    '''
    fitness = []
    for i in range(population.shape[0]):
        # fitness.append(B2D(population[i])**2)
        fitness.append(Func(B2D(population[i]),B2D(population[i])))
    return np.array(fitness)

# 解码
def B2D(dna) -> float:
    '''
    二进制转十进制
    input: (CHROMOSOME_LENGTH,)
    output: int
    '''
    ret = dna.dot(2**np.arange(CHROMOSOME_LENGTH)[::-1])    # 第一步
    res = ret/(2 ** CHROMOSOME_LENGTH - 1)                  # 第二部
    res = res * (X_BOUND[1] - X_BOUND[0]) + X_BOUND[0]      # 第三步
    return res

def select(population,fitness):
    # 赌轮盘选择法
    idx = np.random.choice(np.arange(POP_SIZE),size=POP_SIZE,replace=True,
                           p = fitness/(fitness.sum()))
    return population[idx]

def cross_over(population,P_C):
    for i in range(POP_SIZE):
        # 不是每一个个体都会执行交叉操作
        if np.random.rand() < P_C:            
            parent1 = population[i]                                 # 父本
            idx = np.random.randint(0,POP_SIZE)
            while idx == i:
                idx = np.random.randint(0,POP_SIZE)               # 随机选择一条不是自己的个体进行交叉
            parent2 = population[idx]                               # 母本
            # 交叉方式为常规交叉法
            point = np.random.randint(0,POP_SIZE)
            child = np.r_[parent1[:point+1,],parent2[point+1:,]]    # 合并数组
            population[i] = child
    return population
            
def mutation(population,P_M):
    for i in range(POP_SIZE):
        # 不是所有个体都会变异
        if np.random.rand() < P_M:
            point = np.random.randint(0,CHROMOSOME_LENGTH)  # [0,CHROMOSOME_LENGTH)
            if population[i][point] == 1:  # 相当于取反操作
                population[i][point] = 0
            else:
                population[i][point] = 1
    return population

def main():
    fitness_best = 0
    dna_best = np.random.randint(2,size=5)
    # 先在定义域上撒点
    population = np.random.randint(2,size=(POP_SIZE,CHROMOSOME_LENGTH)) # 每一行代表一个个体
    fitness_avg_list = []
    # 开始进化
    for i in range(GENERATIONS):
        fitness = get_fitness(population)               # 计算适应度
        population = select(population,fitness)     # 选择,返回最优个体和选择后的种群

        # 记录最优个体
        if fitness_best < np.max(fitness):
            fitness_best = np.max(fitness)
            dna_best = population[np.argmax(fitness),:]  # 最优个体将解码后输出结果

        population = cross_over(population,P_C)         # 交叉
        population = mutation(population,P_M)           # 变异

        fitness_avg = np.average(fitness)        
        fitness_avg_list.append(fitness_avg)

        print("最优个体:",dna_best)
        print("取最大值时的x:",B2D(dna_best))
        print("最大适应度:",fitness_best)

        if i > 0:
            if fitness_avg_list[i] - fitness_avg_list[i-1] < ERR:
                print(f"现在是第{i}轮")
                break

if __name__ == "__main__":
    main()

 读者可以改变编码长度、交叉率、变异率来查看算法效果。

三、案例二:使用遗传算法求旅行商问题

(有待补充)

四、一些Tricks

(有待补充)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值