网上见过很多采用遗传算法解决旅行商问题(TSP,也译作:货郎担问题)的文章,本人经过实际尝试,发现大多效果不佳。为此本人经过思考,进行了如下几个改进:
1、充分利用Python的numpy库,尽量采用numpy数组运算,避免人工循环造成过多的硬件开销。
2、编码。一般认为TSP问题的解是一个环,可以直接采用经过的城市代码为编码。但是这就忽视了,同一个解因为出发城市的不同和行进方向的不同,可以有多种(最多为城市数的2倍)序列表示形式。遗传算法恰恰对是以码为基因,如此则会极大干扰进化进程。为此对每个解都要进行清洗:强制规定0号城市为出发点,1号城市位于前半程。这样,除了极端情况(城市数量为偶数,并且1号城市恰好位于半程,也仅有2种表示)以外,都能保证同一个解只有一种表达形式。
3、简单地采用 适应度=1/(本次解的总路程-已知最总短路程+很小的正的常数),即可实现动态自适应的适应度函数,避免适应度的区分度过小的问题。极大增加接近已知最优的解的选择概率。
4、使用简单的np函数,既实现了轮盘赌选择(淘汰),又实现了最优个体保留,还实现了外来基因引入(故意对种群的基因进行污染)。
5、采用某段路程逆行的变异方式。网上文章多见采用两点互换变异,本人实践发现效果并不理想,往往导致原来已经形成的良好基因片段被打破。
6、动态确定进化终止代数。如果在接近预先确定的终止代数的时候发现更好的解,则自动延长进化代数。(由于代码和算法已经优化,硬件开销完全能够承受)。
经上述优化后,代码运行效率高,计算结果好。计算dantzig42问题时,多次得到最优解699。而且,对于初始种群没有特别要求,即便人为给与一个已经全部为最大值(最劣)的种群,也不会过早地收敛到比较差的解。
需要说明:上述的改进,全部都是针对称旅行商问题(STSP)的,对于非对称旅行商问题(ATSP),上述第2点改进不能使用,第5点改进不建议使用。
顺便说一句:crossover翻译成“杂交”更为妥帖,因为这个词本来就有“杂交育种”的意思。
附代码:
import numpy as np
#准备工作
#读取城市矩阵,提前计算成numpy.array形式,并保存在distance_matrix.npy
distance_matrix=np.load(r'distance_matrix.npy')
global city_num#城市数为全局变量
city_num=len(distance_matrix)
#计算一个解的总路程函数
def distance_total(route):
return distance_matrix[route[np.arange(-1,city_num-1)],route].sum()
#解的清洗:规定0在出发位,1在前半程,避免同一路径的不同形式之间相互干扰。对于ATSP问题,仅能规定出发点,不能规定方向。
def wash_route(route):
one_loc=np.argwhere(route==1)[0,0]
zero_loc=np.argwhere(route==0)[0,0]
if (one_loc-zero_loc)%city_num<=city_num//2:
return np.append(route[zero_loc:],route[:zero_loc])
else:
return np.append(route[zero_loc::-1],route[:zero_loc:-1])
#遗传算法
global population,best_route,best#种群规模、当前最佳路径及对应距离为全局变量
population=city_num+city_num%2#必须为偶数,推荐取值=city_num或+1
best_route=wash_route(np.random.permutation(city_num))#初始最佳路径,随机设定
best=distance_total(best_route)#初始最优路程
#随机产生一组解(初始种群)
def species_random(population1=population):
species=wash_route(best_route)
for i in range(population1-1):
species=np.vstack([species,wash_route(np.random.permutation(city_num))])#隐含现行最优解在第一位置,适应度与此有关
return species
#选择(淘汰)
def choice(species,result):
fit=1/(result-best+1)#转化为适应度
choice_probality=fit/sum(fit)
chosen=species[np.random.choice(a=np.arange(population),size=population-3,p=choice_probality)]#选中的个体
return np.vstack((wash_route(np.random.permutation(city_num)),chosen,species[result.argmin()],best_route))
#杂交
def crossover(species,n=22):
species=np.vstack([np.hstack([species[::2,:n],species[1::2,n:]]),np.hstack([species[1::2,:n],species[::2,n:]])])#每一个与下一个进行杂交
for route in species:
np.place(route[1:n],np.ma.in1d(route[1:n],route[n:],assume_unique=True,invert=False),np.setdiff1d(np.arange(city_num),route,assume_unique=True))
return species
#变异,按种群
def mutate(species,p=0.1):
chosen=np.random.random(size=population)<p#选中的变异个体
mut_locs=np.random.randint(1,city_num,size=2)#变异点位
while mut_locs[0]==mut_locs[1]:
mut_locs=np.random.randint(1,city_num,size=2)#如果两点相同,重选
species[chosen,mut_locs[0]:mut_locs[1]+1]=species[chosen,mut_locs[1]:mut_locs[0]-1:-1]#两城之间路径逆转,解STSP问题时推荐使用,解ATSP问题时不建议使用
return np.apply_along_axis(wash_route,1,species)
#主程序
species=species_random(population)#初始种群,随机产生
#开始运算
i,i_max=0,2000
while i<i_max:
result=np.apply_along_axis(distance_total,axis=1,arr=species)#计算本种群各解的总路程
if min(result)<best:#发现更优解
best=min(result)
best_route=species[result.argmin()]
i_max=max(i_max,i+1000)#在后期发现更优解,增加迭代次数
print(r"%.2f"%best,i+1)
species=choice(species,result)#淘汰
species=crossover(species,n=np.random.randint(1,city_num//2))#杂交
species=mutate(species,p=0.3)#变异
i+=1
print(r"%.2f"%best,best_route,i)
#不含绘图