遺伝的アルゴリズムに基づくTSP問題解決(Python実装)

問題の説明

旅行中のビジネスマンが N 都市を訪れたいとすると、どの都市にも一度しか訪問できず、最終的には元の出発都市に戻らなければならないという制限があります。パス選択の目標は、必要なパス距離がすべてのパスの中で最小値になることです。

要件: 遺伝的アルゴリズムのコア コードを参照し、TSP 問題を例として遺伝的アルゴリズムの解法プログラムを実現する

この問題の例では、20 の都市をランダムに生成し、平面デカルト座標系の座標 (x, y) を使用して都市の位置を表します。

ソリューション

TSP 問題の解決には遺伝的アルゴリズムが使用され、大域的な最適解を見つけやすいという特性を利用し、母集団の継続的な再生産を通じて、経路の最小値を反復的に見つけるプロセスをシミュレートします。具体的な実装手順は次のとおりです。

  1. 母集団を初期化します。母集団は個人で構成され、個人は問題の有効な候補解に対応します。遺伝的アルゴリズムでは、染色体が個人を表すために使用され、TSP 問題にマッピングされる場合、有効なパスは次のことを表します。したがって、以下の説明では、染色体が有効なパスを表すことを確認できます。

  1. 集団適応度の計算: 適応度は、集団内で個体が生存し、子孫を生み出す確率を反映します。TSP 問題では、各反復の目的はより短い経路を見つけることであるため、個体の経路長が短いほど、より容易になります。反復プロセスで保持され、適合度が高くなります

  1. 自然選択: 現在の母集団から有利な個人を選択します。私はトーナメント選択方法を使用します。

  1. 毎回 3 人の個人がサンプルとして母集団からランダムに選択されます (つまり、トリプル トーナメント選択法)。

  1. サンプル内で最も適合度の高いスキーム (つまり、最短経路を持つスキーム) を選択し、次世代の母集団に入力します。

  1. 新しい母集団が元の母集団と同じサイズになるまで、手順 a~b を繰り返します。

  1. 交叉:自然選択によって生じた新たな集団の中から個体を選択し、子孫を残すこと。遺伝的アルゴリズムでは、選択された2個体間の染色体を交換することで実現されます。

この質問の交差操作では「単一点交差」が選択されています。

  1. クロスオーバーの確率を設定する

  1. 2 つの新しい集団のそれぞれから子孫個体の親として個体を選択します。

  1. 交叉がない場合は、親の 1 つが子孫個体としてランダムに選択されます。

  1. クロスオーバーが発生した場合は、次の手順を実行します。

  1. 子孫のために染色体がコピーされる父親 (または母親) を選択します

  1. 母親(または父親)からランダムに染色体の一部を傍受し、子の対応する位置の染色体を置き換えます。

  1. そこから新たな子孫を生み出す

  1. 子孫の個体群が前の世代の個体群と同じサイズになるまで、ステップ abcd を繰り返します。

  1. 突然変異:新しく作られた個体の遺伝子の一部を変える(染色体は遺伝子で構成されているため、変化するのは染色体上の特定の位置である);通常、突然変異の確率は低い。

この問題に対する突然変異操作は「交換突然変異」です。染色体上の 2 つの遺伝子をランダムに選択し、その位置を交換して新しい染色体を形成します。

  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