유전자 알고리즘 기반 TSP 문제 해결(파이썬 구현)

문제 설명

여행하는 사업가가 N개의 도시를 방문하기를 원한다고 가정하면, 그는 가고자 하는 경로를 선택해야 하며, 경로의 한계는 각 도시는 한 번만 방문할 수 있고 결국 원래 시작 도시로 돌아가야 한다는 것입니다. 경로 선택의 목표는 필요한 경로 거리가 모든 경로 중 최소값이라는 것입니다.

요구 사항: 유전자 알고리즘의 핵심 코드를 참조하고 유전자 알고리즘의 솔루션 프로그램을 실현하기 위해 TSP 문제를 예로 들었습니다.

이 문제 예제에서는 20개의 도시를 임의로 생성하고 평면 데카르트 좌표계의 좌표(x, y)를 사용하여 도시의 위치를 ​​나타냅니다.

솔루션

유전알고리즘은 TSP 문제를 풀기 위해 사용되며 전역 최적해를 찾을 확률이 높다는 특성을 이용하며 모집단의 지속적인 재생산을 통해 경로의 최소값을 반복적으로 찾는 과정을 시뮬레이션한다. 구체적인 구현 단계는 다음과 같습니다.

  1. 모집단 초기화: 모집단은 개인으로 구성되며, 개인은 문제에서 유효한 후보 솔루션에 해당합니다. 유전자 알고리즘에서 염색체는 개인을 나타내는 데 사용되며 TSP 문제에 매핑될 때 따라서 우리는 다음 설명에서 염색체가 유효한 경로를 나타내는지 확인할 수 있습니다.

  1. 인구 적합도 계산: 적합도는 개체가 개체군에서 생존하여 자손을 낳을 확률을 반영합니다.TSP 문제에서 각 반복의 목적은 더 짧은 경로를 찾는 것이므로 개체의 경로 길이가 짧을수록 더 쉽습니다. 반복 과정에서 유지하고 적합성이 더 높습니다.

  1. 자연 선택: 현재 인구에서 유리한 개인을 선택합니다. 토너먼트 선택 방법을 사용합니다.

  1. 매번 3명의 개인이 모집단에서 무작위로 선택됩니다(즉, 3중 토너먼트 선택 방법).

  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