一、关于遗传算法
遗传算法的原理可以听清华的马少平老师讲解,马少平的个人空间-马少平个人主页-哔哩哔哩视频
其中《第四篇 如何用随机方法求解组合优化问题(八)~(十一)》是关于遗传算法的讲解。
下面把视频中的一些精华内容进行摘录。
(一)算法原理
遗传算法主要是受到进化论的启发,根据生物在进化中优胜劣汰的自然选择,使得种群逐步优化,逐渐保留优良物种。
其中,生物进化与遗传算法之间的对应关系如图所示。
(二)名词解释
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],的最大值。感兴趣的读者可以修改一下代码查看结果)
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
(有待补充)