1.遗传算法的简单介绍
基本思想:适者生存,基因交流
遗传算法是智能优化算法的一种,通过不断缩小解集的搜索域来寻找最优解。
先来以一个简单直观的的自然现象来掀开遗传算法的面纱。
一个叫张三的大学生回乡创业,开了个养鸡场,初期购入1000只小鸡仔(公母各半)。每只小鸡仔的状况都是未知的(把这些小鸡仔称为初始解集),长大后可能有的生蛋多,有的长得肥,有的肉质好......市场上好的肉鸡能买个好价钱(把市场需求叫目标函数),所以张三倾向于多一点肉鸡,春天到了,小鸡仔长成了大公鸡和大母鸡,张三为了能让自己的养鸡场里多获得一些好的肉鸡鸡仔,他开始想办法了。他把不同的鸡按照肉质进行分级(假设不用杀鸡就能看出肉质)。级数越高(肉质越好,即在目标函数的评价标准之下越接近最优解,适应度越高)的有更多的交配机会(优质肉的基因就有更大概率能遗传下去)。这样,下一代的鸡中的肉鸡大大增加,且肉质更好的鸡可能产生(优质肉的基因可能突变为更优质的,或者优质肉基因组合在一起变得更优质)。张三赚到了钱,就继续这样的操作,最后,肉鸡在鸡群中的比例越来越大,并且肉质逐代提高(即较优解占比提高,并向最优解靠拢)。
这个故事虽然简单,但是,我们能够看到张三清晰的解题思路即:
获取初始解集→根据目标确定适应度函数→按适应度大小给予基因遗传概率→交配带来基因的组合和突变基因的遗传→获得更好的下一代
话不多说,接下来我们就开始实践吧:
2.实战搭建遗传算法
我们会用到:python 、numpy 、matplotlib(用来画图,可以不用)
我们以鸡为出发点,来尝试培养一群有趣的飞鸡
假设我们的飞鸡能够瞬间长大,也就是根据基因直接获得适应度
我们给予飞鸡两个不同的运动方向,每个运动方向有着不同的运动速度
让他们从同一起点出发,飞向目标点,假设他们不会在途中有速度的改变
示意图如下:
我们的目标是飞鸡飞的准。
我们设置的起点坐标为(0,50),目标点坐标为(100,30)。
下面开始编程实战:
第一步:编码
我们把飞鸡的两个不同的速度方向用简单的二进制编码来表示,假设单向最大速度为15,且我们只取整数,则每个速度方向我们用4位来编码,因为2的四次方减一是15。
由于比较简单,我们初始种群为10只飞鸡。注意:一般来说种群太小是极其不利的,只有10往往会过早收敛,即得出的解只是较优解。
import numpy as np
import matplotlib.pyplot as plt
start=(0,0)
end=(100,30)
def encode():
raw=np.random.randint(0,2,(20,4))#生成一个20*4的矩阵,每两行确定一只飞鸡,先X后Y
return raw
第二步:计算适应度
我们的适应度很容易计算,用飞鸡在x轴运动到100时所对应的y轴量与目标点的差值来代表,当然此时要求距离越小越好,对于极端情况,比如x轴速度为零的直接淘汰。注意:一定要用到归一化,使适应度加起来是1。下一步要用。
def adapt(n):
n1=np.array(range(4))
n1=2**n1#n1[1 2 4 8],n1.T表示n1的转置
n=n.dot(n1.T)#获取一个40*1的矩阵,每一行有一个速度分量
vx=np.zeros((10,1))
vy = np.zeros((10, 1))
for i in range(10):
vx[i]=n[2*i-1]#获取x速度
vy[i]=n[2*i]#获取y速度
adp=np.zeros((10,1))#生成一个新的矩阵来保存适应度
for i in range(10):
if vx[i]!=0:
t=(end[0]-start[0])/vx[i]#飞行时间
dety=abs(start[1]+t*vy[i]-(end[1]-start[1]))#距离差
adp[i]=1/(dety+1)#交配概率,加一是为了避免除0
else:
adp[i] = 0
adp=adp/sum(adp)#归一化
return adp
第三步.生成下一代
生物学的研究表明,遗传与染色体的行为有着密不可分的关系。
把我们获取的初始解集中不同的编码当作染色体上不同的基因,为了操作简单,我们在这个例子中只考虑自由组合与变异,不考虑交叉,因为编码比较短,交叉意义不大。
组合方式就按随机组合,在这里我们通过不同的适应度给出不同的遗传概率,使得优秀的基因有更大的概率传给下一代。
这里我们采用保留上一代中最优个体的进化方法(这样会在理论上收敛到最优解,已经被论证过了,有兴趣的读者可以去找一找证明过程。)。
def new(n,adp):
byl=0.1#0.1的变异率
new=np.zeros((20,4))
x = np.zeros((10,4))
y = np.zeros((10, 4))
for i in range(10):
x[i,:] = n[2 * i ,:] # 获取x
y[i,:] = n[2 * i+1,:] # 获取y
new[0, :] = x[np.where(adp==max(adp))[0][0]]#找到上一代最优的那一个的x
new[1, :] = y[np.where(adp==max(adp))[0][0]]#找到上一代最优的那一个的y
for j in range(1,10):
p1=np.random.choice(list(range(10)),1,p=adp[:,0])#x速度
p2 = np.random.choice(list(range(10)),1,p=adp[:,0])#y速度
new[2*j+1,:]=y[p1]
new[2 * j, :] = x[p2]
if np.random.randint(0,20,1)<10*byl:#变异操作
k=np.random.randint(0,20,1)#20个速度(包括两个方向)中选一个进行变异
new[k,0]=abs(new[k,0]-1)#将0变1,1变0,选择0号位置是为了减小误差,使变化小一些
return new
第四步.重复操作,进化多代
用一个while循环,或者for循环就好
x=encode()#获取初代,不要写到循环里
for ii in range(20):#20代
pp=adapt(x)#适应度
x=new(x,pp)#产生新一代
huitu(x)#绘图
最后,上效果图:
可以看到,在3到4代进化后就已经趋于最优解了。而且从第二代开始有愈来愈收敛的趋势。
看起来还不错 ,但其实会出现算法早熟的情况,过早收敛,还没到最优解就进化不动了。这个图是运行了几次后才出来的。如果要避免早熟,可以把种群弄大一些。但是这个问题一共就16*16=256种可能,种群弄大了难免有随机搜索之嫌。
遗传算法要真正的使用其实还有许多要考虑的地方,比如编码肯定不是像这么简单,产生下一代的方法也不会这么简单,进化的代数一般会超过500代。但是该有的思想都在这里面了,希望能给遗传算法的初学者一点点启发。
最后贴上全部代码:
import numpy as np
import matplotlib.pyplot as plt
import time
start = (0, 0)
end = (100, 30)
def encode():
raw = np.random.randint(0, 2, (20, 4)) # 生成一个40*4的矩阵,每两行确定一只飞鸡,先X后Y
return raw
def adapt(n):
n1 = np.array(range(4))
n1 = 2 ** n1 # n1[1 2 4 8],n1.T表示n1的转置
n = n.dot(n1.T) # 获取一个40*1的矩阵,每一行有一个速度分量
vx = np.zeros((10, 1))
vy = np.zeros((10, 1))
for i in range(10):
vx[i] = n[2 * i] # 获取x速度
vy[i] = n[2 * i + 1] # 获取y速度
adp = np.zeros((10, 1)) # 生成一个新的矩阵来保存适应度
for i in range(10):
if vx[i] != 0:
t = (end[0] - start[0]) / vx[i] # 飞行时间
dety = abs(start[1] + t * vy[i] - (end[1] - start[1])) # 距离差
adp[i] = 1 / (dety + 1) # 交配概率,加一是为了避免除0
else:
adp[i] = 0
adp = adp / sum(adp) # 归一化
return adp
def new(n, adp):
byl = 0.1 # 0.1的变异率
new = np.zeros((20, 4))
x = np.zeros((10, 4))
y = np.zeros((10, 4))
for i in range(10):
x[i, :] = n[2 * i, :] # 获取x
y[i, :] = n[2 * i + 1, :] # 获取y
new[0, :] = x[np.where(adp == max(adp))[0][0]] # 找到上一代最优的那一个的x
new[1, :] = y[np.where(adp == max(adp))[0][0]] # 找到上一代最优的那一个的y
for j in range(1, 10):
p1 = np.random.choice(list(range(10)), 1, p=adp[:, 0]) # x速度
p2 = np.random.choice(list(range(10)), 1, p=adp[:, 0]) # y速度
new[2 * j + 1, :] = y[p1]
new[2 * j, :] = x[p2]
if np.random.randint(0, 20, 1) < 10 * byl: # 变异操作
k = np.random.randint(0, 20, 1) # 20个速度(包括两个方向)中选一个进行变异
new[k, 0] = abs(new[k, 0] - 1) # 将0变1,1变0,选择0号位置是为了减小误差,使变化小一些
return new
def huitu(n):
n1 = np.array(range(4))
n1 = 2 ** n1 # n1[1 2 4 8],n1.T表示n1的转置
n = n.dot(n1.T) # 获取一个40*1的矩阵,每一行有一个速度分量
vx = np.zeros((10, 1))
vy = np.zeros((10, 1))
mo = np.zeros((10, 1)) # 末位置
for i in range(10):
vx[i] = n[2 * i] # 获取x速度
vy[i] = n[2 * i + 1] # 获取y速度
for i in range(10):
if vx[i] != 0:
t = (end[0] - start[0]) / vx[i] # 飞行时间
mo[i] = abs(start[1] + t * vy[i])
print(mo.T)
plt.scatter(start[0], start[1])
plt.scatter(end[0], end[1])
plt.grid()
plt.ion()
for i in range(10):
plt.plot((0, 100), (0, mo[i, :]))
plt.show()
plt.pause(0.1)
f = plt.gcf() # 获取当前图像
f.savefig(f'0\{str(time.time())}.png') # 保存图像
plt.clf()
pass
x = encode()
for ii in range(20): # 20代
pp = adapt(x)
x = new(x, pp)
huitu(x)
n1 = np.array(range(4))
n1 = 2 ** n1 # n1[1 2 4 8],n1.T表示n1的转置
x = x.dot(n1.T) # 获取一个40*1的矩阵,每一行有一个速度分量
print(x)