基于遗传算法的TSP问题求解(python实现)

问题描述

假设有一个旅行商人要拜访N个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值

要求:参考遗传算法核心代码,以TSP问题为例实现遗传算法的求解程序

在本问题示例中,我们随机生成20个城市,用平面直角坐标系中的坐标(x,y)表示城市的位置

解决思路

遗传算法用于解决TSP问题,利用的是其更有可能找到全局最优解的特点;通过种群的不断繁衍,来模拟迭代寻找路径最小值的过程,其具体实现步骤如下图:

  1. 初始化种群:种群是由个体组成的,而一个个体对应着问题中的一个有效候选解;在遗传算法中用染色体来代表个体,映射到TSP问题中,一条有效的路径就代表着一个个体;因此我们可以明确,在以下的叙述中,染色体及代表一条有效路径

  1. 计算种群适应度:适应度反映了个体在种群中存活下去,产生后代的概率,在TSP问题中,因为每一次的迭代的目的都是找到更短的路径,因此个体的路径长度越短,就更容易在迭代的过程中保留下来,适应度也就更高

  1. 自然选择:从当前的种群中选择有优势的个体;我选用的是锦标赛选择法:

  1. 每次从种群中随机选择出三个个体(即三元锦标赛选择法),作为样本

  1. 在样本中选择适应度最好的一个(即路径最短的方案),进入下一代种群

  1. 重复步骤ab,知道新的种群和原来的种群规模相同

  1. 交叉:在经过自然选择生成的新的种群中选择个体,创建后代,在遗传算法中是通过两个被选定的个体来互换染色体实现的;

本问题交叉操作中选用的是“单点交叉”:

  1. 设置发生交叉的概率

  1. 从两个新种群中分别选择一个个体,作为子代个体的双亲

  1. 如果未发生交叉,则在双亲中随机选择一个,作为子代个体

  1. 如果发生交叉,则执行以下步骤:

  1. 选择父亲(或母亲),复制其染色体作为子代

  1. 随机截取母亲(或父亲)的一段染色体,来替换掉子代相应位置上的染色体

  1. 由此生成新的子代

  1. 重复步骤abcd,直到子代个体组成的种群与上一代种群规模相同

  1. 变异:将每个新创建的个体的某些基因进行改变(由于染色体是由基因组成的,因此发生改变的即是该条染色体上的某一位置);变异的概率通常较低

本问题变异操作选用的是“交换突变”:随机选择一条染色体上的两个基因,交换其位置,形成新的染色体

  1. 精英主义策略:能够保证种群中最优秀的个体进入下一代;我们通过选择、交叉和变异,由上一代种群生成的后代,并不直接用来形成子代种群,而是与上一代种群中的个体进行比较,选择更为优秀的个体进入子代种群,从而保证每一次迭代生成的子代种群都是由最优秀的个体组成的,也能更快地得到最优解

  1. 终止条件的选择:设置迭代次数,到达迭代次数则算法终止,输出最佳方案

算法源码

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))

猜你喜欢

转载自blog.csdn.net/qq_51235856/article/details/129737332