问题描述
假设有一个旅行商人要拜访N个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值
要求:参考遗传算法核心代码,以TSP问题为例实现遗传算法的求解程序
在本问题示例中,我们随机生成20个城市,用平面直角坐标系中的坐标(x,y)表示城市的位置
解决思路
遗传算法用于解决TSP问题,利用的是其更有可能找到全局最优解的特点;通过种群的不断繁衍,来模拟迭代寻找路径最小值的过程,其具体实现步骤如下图:
初始化种群:种群是由个体组成的,而一个个体对应着问题中的一个有效候选解;在遗传算法中用染色体来代表个体,映射到TSP问题中,一条有效的路径就代表着一个个体;因此我们可以明确,在以下的叙述中,染色体及代表一条有效路径
计算种群适应度:适应度反映了个体在种群中存活下去,产生后代的概率,在TSP问题中,因为每一次的迭代的目的都是找到更短的路径,因此个体的路径长度越短,就更容易在迭代的过程中保留下来,适应度也就更高
自然选择:从当前的种群中选择有优势的个体;我选用的是锦标赛选择法:
每次从种群中随机选择出三个个体(即三元锦标赛选择法),作为样本
在样本中选择适应度最好的一个(即路径最短的方案),进入下一代种群
重复步骤ab,知道新的种群和原来的种群规模相同
交叉:在经过自然选择生成的新的种群中选择个体,创建后代,在遗传算法中是通过两个被选定的个体来互换染色体实现的;
本问题交叉操作中选用的是“单点交叉”:
设置发生交叉的概率
从两个新种群中分别选择一个个体,作为子代个体的双亲
如果未发生交叉,则在双亲中随机选择一个,作为子代个体
如果发生交叉,则执行以下步骤:
选择父亲(或母亲),复制其染色体作为子代
随机截取母亲(或父亲)的一段染色体,来替换掉子代相应位置上的染色体
由此生成新的子代
重复步骤abcd,直到子代个体组成的种群与上一代种群规模相同
变异:将每个新创建的个体的某些基因进行改变(由于染色体是由基因组成的,因此发生改变的即是该条染色体上的某一位置);变异的概率通常较低
本问题变异操作选用的是“交换突变”:随机选择一条染色体上的两个基因,交换其位置,形成新的染色体
精英主义策略:能够保证种群中最优秀的个体进入下一代;我们通过选择、交叉和变异,由上一代种群生成的后代,并不直接用来形成子代种群,而是与上一代种群中的个体进行比较,选择更为优秀的个体进入子代种群,从而保证每一次迭代生成的子代种群都是由最优秀的个体组成的,也能更快地得到最优解
终止条件的选择:设置迭代次数,到达迭代次数则算法终止,输出最佳方案
算法源码
import math
import random
import time
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.pylab import mpl
import numpy as np
# 计算适应度
def compute_fitness(pop_line,distance):
sum_dis = 0
temp_dis = 0
nums = len(pop_line)
for i in range(nums):
if i < nums - 1:
temp_dis = distance[pop_line[i],pop_line[i+1]]
sum_dis += temp_dis
else:
temp_dis = distance[pop_line[i],pop_line[0]]
sum_dis += temp_dis
return sum_dis
# 轮盘赌选择(不太好实现,先不用了)
# def select(pops,fitness):
# fitness = np.array(fitness)
# index = np.random.choice(np.arange(POP_SIZE), size=1, replace=True,p=( fitness / fitness.sum()))[0]
# print(fitness[index])
# return pops[index]
# 自然选择,采用锦标赛选择
def tournament_select(pops,pop_size,fitness,tournament_size):
new_pops = []
new_fitness = []
# 直到新的种群规模到达当前种群规模
while len(new_pops) < len(pops):
# 从原始样本中选出的样本
checked_list_pop = [random.randrange(0,pop_size) for i in range(0,tournament_size)]
checked_list_fitness = np.array([fitness[i] for i in checked_list_pop])
min_fitness = checked_list_fitness.min() # 最小适应度
idx = np.where(checked_list_fitness == min_fitness)[0][0] # 获取索引
min_pop = pops[idx] #获取对应的个体
new_pops.append(min_pop) #放入新的种群中
new_fitness.append(min_fitness)
# print(new_pops)
return new_pops #返回新的种群
# 交叉算子 选择单点交叉
def crossover(pop_size,pops_parent1,pops_parent2,trans_rate):
pops_children = []
# 双亲交叉一次得到一个子代
for i in range(0,pop_size):
child = [None] * len(pops_parent1[i])
parent1 = pops_parent1[i]
parent2 = pops_parent2[i]
# print("双亲为:")
# print(parent1)
# print(parent2)
if random.random() > trans_rate:
# 不发生交叉,则随机保留父代中的一个
if random.random() > 0.5:
child = parent1.copy()
else:
child = parent2.copy()
else:
#发生交叉
#确定交叉的位置
begin = random.randrange(0,len(parent1))
end = random.randrange(0,len(parent1))
if begin > end:
temp = begin
begin = end
end = temp
child[begin:end+1] = parent1[begin:end+1].copy()
list1 = list(range(end+1,len(parent2)))
list2 = list(range(0,begin))
list_idx = list1 + list2
j = -1
for i in list_idx:
for j in range(j+1,len(parent2)):
if parent2[j] not in child:
child[i] = parent2[j]
break
#print("子代为:",child)
pops_children.append(child)
return pops_children
#变异算子 采用交换突变
def mutate(pop_size,pops,mutation_rate):
pops_mutated = []
for i in range(pop_size):
child = pops[i].copy()
if random.random() < mutation_rate:
# 发生变异
pos_first = random.randrange(0,len(child))
pos_second = random.randrange(0,len(child))
if pos_first != pos_second:
temp = child[pos_first]
child[pos_first] = child[pos_second]
child[pos_second] = temp
pops_mutated.append(child)
return pops_mutated
# elitism策略处理(精英注意)
def elitism(pop_size,pops,children_pops,pop_fitness,children_fitness):
#如果父代适应度更高的话,则不会被淘汰
for i in range(0,pop_size):
if pop_fitness[i] > children_fitness[i]:
pops[i] = children_pops[i]
pop_fitness[i] = children_fitness[i]
return pops,pop_fitness
# 画路径图
def draw_path(line, CityCoordinates):
x, y = [], []
for i in line:
Coordinate = CityCoordinates[i]
x.append(Coordinate[0])
y.append(Coordinate[1])
x.append(x[0])
y.append(y[0])
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('y')
plt.show()
if __name__ == '__main__':
CITY_NUM = 20 # 城市数量
ITERATIONS = 1000 # 终止条件的选择 种群繁衍1000代终止
POP_SIZE = 100 # 种群大小
TOURNAMENT_SIZE = 2 # 锦标赛采样大小
TRANS_RATE = 0.7 # 交叉概率
MUTATION_RATE = 0.1 # 变异概率
# 获取城市坐标(可以读取文件获取或者随机生成)
coordinates = [] # 城市坐标
with open("data.txt","r") as f:
lines = f.readlines()
for line in lines:
line = line.strip()
coordinates.append(list(map(int,line.split(" "))))
# coordinates = 100 * np.random.rand(20, 2) # 随机产生20个城市
coordinates = np.array(coordinates)
row,col = coordinates.shape
#计算各个城市之间的距离
distance = np.zeros((row,row))
for i in range(row):
for j in range(row):
distance[i,j] = distance[j,i] = math.sqrt((coordinates[i][0] - coordinates[j][0]) ** 2 + (coordinates[i][1] - coordinates[j][1]) ** 2 )
#print(distance)
iteration = 0 #迭代次数为0
#生成种群 100个 每一种路径的选择相当于一条染色体
pops = [random.sample([i for i in list(range(len(coordinates)))],len(coordinates)) for j in range(POP_SIZE)]
#print(pops)
#计算适应度
pop_fitness = [0] * POP_SIZE
for i in range(POP_SIZE):
pop_fitness[i] = compute_fitness(pops[i],distance)
# print(pop_fitness)
# 找到初代最优解
min_pop_fitness = min(pop_fitness)
optimal_pop = pops[pop_fitness.index(min_pop_fitness)]
print("第1代的最短距离为:",min_pop_fitness)
optimal_pops = [optimal_pop]
start_time = time.perf_counter()
x = []
y = []
# 开始迭代
while iteration <ITERATIONS:
pops_parent1 = tournament_select(pops,POP_SIZE,pop_fitness,TOURNAMENT_SIZE)
pops_parent2 = tournament_select(pops,POP_SIZE,pop_fitness,TOURNAMENT_SIZE)
# 双亲交叉
pops_children = crossover(POP_SIZE,pops_parent1,pops_parent2,TRANS_RATE)
# print(pops_children)
# 变异
pops_mutated = mutate(POP_SIZE,pops_children,MUTATION_RATE)
# print(pops_mutated)
# 统计发生变异的个体
# print( "发生变异的个体有%d个" %(len(np.where(np.array(pops_children) != np.array(pops_mutated))[0]) / 2) )
# 获取子代种群
children_fitness = []
for i in range(POP_SIZE):
children_fitness.append(compute_fitness(pops_mutated[i],distance))
# 应用精英策略
pops,pop_fitness = elitism(POP_SIZE,pops,pops_children,pop_fitness,children_fitness)
# 找到当代最优解
if min_pop_fitness > min(pop_fitness):
min_pop_fitness = min(pop_fitness)
optimal_pop = pops[pop_fitness.index(min_pop_fitness)]
optimal_pops.append(optimal_pop)
print("第%d代的最短距离为:" % (iteration+2),min_pop_fitness)
# XY是趋势图的横轴和纵轴
x.append(iteration)
y.append(min_pop_fitness)
iteration += 1
print("最佳路径为:",optimal_pop)
end_time = time.perf_counter()
# 根据绘制每一代最优解的走向图
plt.figure()
plt.rcParams['font.sans-serif'] = ['SimHei']
title = "迭代次数:"+str(ITERATIONS)+"种群大小:"+ str(POP_SIZE)+"\n"+"交叉概率:"+ str(TRANS_RATE) +"变异概率:" + str(MUTATION_RATE)
plt.title(
label=title,
fontdict={
"fontsize": 20, # 字体大小
"color": "black", # 字体颜色
}
)
plt.plot(x, y)
plt.show()
#画路径图
# draw_path(optimal_pop,coordinates)
print("迭代所需时间为:%f ms" % ((end_time-start_time)*1000))