遗传算法简介
遗传算法(GA)是模拟生物在自然环境中的遗传和进化的过程而形成的自适应全局优化搜索算法。它遵循适者生存、优胜劣汰的原则
几种概念
1、基因
: 是生物的遗传物质,是遗传的基本单位,存储遗传信息。
2、个体
: 若干个器官和系统协同完成复杂生命活动的单个生物
3、种群
: 指同一时间生活在一定自然区域内,同种生物的所有个体。
4、适应度
: 适应度是指一个个体在特定环境下生存和繁殖的能力
5、表现型
: 生物所表现的形态和生理等的性状
6、遗传
: 遗传是指遗传物质从父母传给后代的现象,可能发生基因重组、基因突变或染色体变异
7、进化
:是一种基于变异、选择和适应的过程,导致了物种的多样性。自然选择是进化的主要驱动力之一,它会筛选出最适合当前环境的个体,并使其后代更有可能生存和繁殖。
算法流程图
算法步骤
编码
常见的两种编码方式是二进制编码
和浮点数编码
二进制编码
:不同于人类基因的四种碱基序列,在计算机中,我们使用0和1代表两种碱基,然后将他们串成一条染色体,比如0011010。
这种编码方式容易实现交叉变异,但也存在一定缺点,比如二进制编码对应的实数之间是离散的,染色体长度较短时,精度达不到要求(后面会举个例子),而较高的精确度会使得解码的难度更大。
浮点数编码
:使用一定范围的浮点数串成一条染色体,并且保证交叉、变异等操作产生的新个体的基因也是在这个范围内的。通常不需要解码。它适用于要求精度较高的时候
为了下面讲解的方便一些,将使用二进制编码作为讲解对象,可能会谈及一点浮点数编码
二进制编码
首先需要知道的一点是,长度为n的二进制串总共有
2
n
2^n
2n种情况(每个位置不是取0就是取1嘛)
区间在[1, 10],二进制编码为两位,是不是被分成3块,每一块占了3个长度(准确来说是精度),这里看成每两个二进制之间的距离。这个3怎么计算来的?不就是
10
−
1
2
2
−
1
=
3
\frac{10-1}{2^2-1}=3
22−110−1=3。
如果在区间[a, b],我们需要的精度为E,那又怎么求出二进制编码长度呢?这时我们就需要将每块的长度缩小到E或者比E更小。假设编码长度为n,是不是只要满足
b
−
a
2
n
−
1
≤
E
\frac{b-a}{2^n-1} \le E
2n−1b−a≤E就行了,因此把E代进去,我们就能知道n至少为多少
比如在区间长度b-a=10,要精确到E=1e-5,带入
b
−
a
2
n
−
1
≤
E
\frac{b-a}{2^n-1} \le E
2n−1b−a≤E,可得
n
≥
20
n \ge 20
n≥20
初始化种群
也就是随机初始种群的个体,可以根据二进制编码或者浮点数编码随机产生。为什么要随机产生呢?因为要尽可能提高全局搜索的能力,换句话说就是提高种群的多样性(我始终相信这个群体里会诞生一个"天才"找到最优的位置,嘿嘿)。初始化个体的数目越多,能找到全局最优解可能性相对会大些,但有可能随机初始的个体全都在局部最优的位置上,不禁感叹,不翻过这座大山,你永远不知道山后面有什么好东西。
是吧,个体数多一点,万一就诞生一个"天才"了呢。但也不要太多,收敛速度会变慢(警惕!)
解码
还是以二进制为例,因为浮点数编码不需要解码,没法说。
二进制需解码成实数制,怎么解呢?以[0, 1]解码实数为4举例,是不是就是1(a) + 1([0, 1]的十进制数) * 3 (精度),同理7= 1 + 2 * 3,以此类推,不难得出一个结论:
在区间[a, b],二进制编码长度为n, 解码所对应的实数 x = a + x t ∗ E x = a + x^t * E x=a+xt∗E,其中 E = b − a 2 n − 1 E = \frac{b-a}{2^n-1} E=2n−1b−a, x t = ∑ i = 0 n b i ∗ 2 i x^t=\sum_{i=0}^{n}b_i * 2^i xt=∑i=0nbi∗2i就是二进制对应的十进制数
适应度
就是要优化的目标函数,如果要求目标函数最大值,那么函数值越大,适应度也就越好;如果要求目标函数最小值,那么函数值越小,适应度也就越好
自然选择
自然选择的方式很多,包括但不仅限于下面几种
轮盘赌法(最常用)
:每个个体的适应度除以总个体的适应度,然后按此概率选择个体,也就是说适应度越高的个体被选中的概率越大,但这不是绝对的。
如同一个圆盘,当你往圆盘上扎一飞镖,pia~的一下,是不是飞镖扎在蓝色区域的概率会更大,上述也是一个道理。统一确定性选择
:是最简单的一种方式,把所有个体都给选上 从而产生均匀的选择均匀随机选择
:选择每个个体的概率都是一样的二进制比赛
:每次从种群中随机选两个个体,比较两个个体的适应度,选最好的一个进入下一代,然后把两个全放回去,循环往复,直至达到原来种群规模。
截断
:最优秀的个体必被选中,最差的个体永远不会被选择,例如有100个个体,我们只需要30个个体进入下一代,那么适应度排在前30位的个体就是我们要选择的。
当然,方法肯定不止这么多,比如可不可以将个体按适应度划分为几个区间,然后选每个区间的前几个适应度高的个体进入下一代?of course~
,还有很多可以自己大胆尝试一下
交叉
如果说无法确定种群的个体找到的解是不是全局最优,那么我们可以进行交叉
和突变
两种操作,看它能不能成为"天才",这是有一定概率的。两种操作本质上还是提高一下个体间的多样性。交叉
可以看成将两个个体基因结合起来形成新的个体。
这里列出几个经典的交叉操作:
6. 一点交叉
:随机选择一个切点,然后将两个体在切点处的基因交换
7. 两点交叉
:就是随机取两个切点进行交叉互换
8. 均匀交叉
:每个基因都有同等的交换概率
9. 混合交叉
:通常用于浮点数编码
,它不是简单的交换基因,而是尝试将两个基因混合在一起,计算算术平均值。这里可以应用权重,比如适应度高的个体基因权重大。
还有很多其他的交叉操作,比如能不能随机选几个切点进行混合交叉呢?当然可以。
突变
也列出几个经典的突变操作:
Bitflip突变
:常用于二进制编码
,由预定义的概率指定,每个基因都是概率的地倒转
的,即0变成1,1变成0。随机突变
:应用许多编码方式,也是由预定义的概率指定,每个基因概率地变为随机值
,这个随机值不得超过指定区域。
打个比方,在区间[0,5]范围内,预定义概率为0.3,对于个体第一个基因,先随机产生0-1的随机数c,如果c<0.3,那么这个基因就可突变成[0,5]内的随机数。Delta突变
:常用于实数编码
,由预定义的概率指定,每个基因的突变都是概率性的,同时以一定步长递增\递减,这个步长是预先指定的。
比如预定义概率为0.3,步长为p,若按步长递增,对于个体上的一个基因 x n x_n xn,如果产生的随机数c<0.3,那么 x n x_n xn会突变成 x n + p x_n +p xn+p。高斯突变
:与Delta突变类似,也常用于实数编码
,不同之处在于步长是一个高斯随机数
终止条件
通常使用迭代次数作为终止条件,比如设置迭代1000此算法终止。也有其他终止条件,比如计算每一代的适应度,如果适应度的改进变化量小于某个阈值,那就stop。
代码实现(python)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import warnings
warnings.filterwarnings("ignore")
需要找到以下函数在 x ∈ [ 0 , 50 ] x \in [0, 50] x∈[0,50]的最大值
x = np.arange(0,50,0.01)
z = abs(x * np.sin(x) * np.cos(2 * x) - 2 * x * np.sin(3 * x) + 3 * x * np.sin(4 * x))
plt.figure(figsize=(8,4))
plt.plot(x, z)
plt.show()
适应性函数
def F(x):
return abs(x * np.sin(x) * np.cos(2 * x) - 2 * x * np.sin(3 * x) + 3 * x * np.sin(4 * x))
def fitness_func(X):
X = X[:,0]
return F(X) - np.min(F(X)) + 1e-3
解码
#解码
def decode(X, a, b):
st = 0
#二进制转十进制
for i in range(len(X)):
st += 2 ** i * X[i]
return a + st * (b - a) / (2 ** len(X) - 1)
def decode_X(X, a, b):
m = X.shape[0]
X2 = np.zeros((m,1))
for i in range(m):
x = decode(X[i], a, b)
X2[i,:] = x
return X2
自然选择
#自然选择
def select(X, fitness):
m = X.shape[0]
#按概率随机选取,selected里面可能含有重复值
selected = np.random.choice(np.arange(m), size=m, p=fitness / (fitness.sum()))
return X[selected]
交叉操作
#交叉,c_rate表示预定义的概率
def crossover(X, c_rate):
for i in range(X.shape[0]):
#随机选取一个切点
cross_pos = np.random.randint(X.shape[0])
for j in range(X.shape[1]):
#随机数<c_rate,进行交叉
if np.random.randn() <= c_rate:
X[i, j], X[cross_pos, j] = X[cross_pos, j], X[i, j]
return X
变异操作
#变异,m_rate表示预定义的概率
def mutation(X,m_rate):
for i in range(X.shape[0]):
if np.random.randn() <= m_rate:
X[i] = X[i] ^ 1
return X
绘图函数,用于最后确定找的是不是最大值而不是局部最大
def plot(ax):
x = np.arange(0,50,0.01)
z = abs(x * np.sin(x) * np.cos(2 * x) - 2 * x * np.sin(3 * x) + 3 * x * np.sin(4 * x))
ax.plot(x, z)
主函数
def GA():
size, l = 50, 23 #初始化个数和染色体长度
a, b = 0, 50
c_rate = 0.5
m_rate = 0.05
best_fitness = [] #记录每一代最好的适应度
best_x = [] #记录每一代最好的适应度所对应的x值
iter_num = 100 #迭代次数
X0 = np.random.randint(0,2,(size, l))
for i in range(iter_num):
X1 = decode_X(X0, a, b)
fitness = fitness_func(X1)
X2 = select(X0, fitness) #自然选择
X3 = crossover(X2, c_rate) #交叉
X4 = mutation(X3, m_rate) #变异
#变异后重新计算适应度
X5 = decode_X(X4, a, b)
fitness = fitness_func(X5)
best_fitness.append(fitness.max())
x = X5[fitness.argmax()]
best_x.append(x)
X0 = X4
print(f"最大值:{best_fitness[-1]:5f}")
print(f"最优解: x = {best_x[-1]}")
fig, ax = plt.subplots(1,1,figsize=(7,4))
plot(ax)
ax.scatter(best_x[-1],best_fitness[-1],c='r')
plt.figure(figsize=(4,4))
plt.plot(best_fitness, color='r')
plt.show()
GA()
每次随机产生的二进制编码不同,可能运行出来的解是局部最大的,多运行几次,选最大的即可
插一句话,遗传算法不仅能运用在优化处理方面,还能在多目标规划方面产生良好的效果。将在下一讲中把遗传算法运用在多目标规划上
感谢读者阅读到这里!