基于遗传算法求解旅行商问题(python,有源码)

524 阅读4分钟

这是一份机器学习作业,我觉得遗传算法是一种很有趣的算法
GitHub项目链接github.com/K1ndred-zsq…
我的代码并不完美欢迎大佬来修改

1 旅行商问题

  • 题目:
    设有n个城市和距离矩阵D=[dij],其中dij表示城市i到城市j的距离,i,j=1,2 … n, 则问题是要找出遍访每个城市恰好一次的一条回路并使其路径长度为最短。
  • 思路:
    • 所有符合“每个城市恰好一次的一条回路”的路线均视为可行解
    • 则该问题只需要找出最优解

2 遗传算法

  • 具体的原理这里暂时不做赘述
  • 算法的流程包括
    1. 初始化种群
    2. 计算适应度
    3. 选择
    4. 交叉
    5. 变异
    6. 产生新一代种群
    7. 重复2 - 6,优胜劣汰
graph TD 
s(Start) --> 初始化种群 --> 计算适应度--> p{判断进化是否结束} --no--> 
选择,精英保留 --> 交叉 --> 概率变异 --> 计算适应度
p -- yes --> e(Stop) 

3 算法描述

3.1 环境搭建

版本:python3.8.8
编译器:vscode

  1. 导包
import random
from matplotlib import pyplot as plt
import numpy as np
import math
# 防止中文和特殊字符乱码
plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False 

3.2 城市定义

定义城市类

  1. 建立size个城市,编码为"0" - "size - 1"
  2. 每座城市均为一个坐标点
  3. 假设区域的大小为100 x 100,区域左下角为坐标系原点
  4. 为了保证结果准确度,城市的坐标暂时规定为整数
  5. size=5,则一条染色体为"0-1-2-3-4"
class City:

    def __init__(self, size:int):
        # 城市数量
        self.size = size
        # 位置矩阵
        self.position = np.zeros((size, 2))
        x_list = random.sample(range(0, 100), size)
        y_list = random.sample(range(0, 100), size)
        self.position[:, 0] = x_list[:]
        self.position[:, 1] = y_list[:]
        # 距离矩阵D
        self.D = np.zeros((size, size), dtype='float64')
        for i in range(size):
            for j in range(size):
                self.D[i, j] = math.sqrt(pow((self.position[i, 0] - self.position[j, 0]), 2) + pow((self.position[i, 1] - self.position[j, 1]), 2))

    # 可视化方法
    def show_city(self):
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(self.position[:, 0], self.position[:, 1], s=30, color='b')
        ax.legend()
        x_list = self.position[:, 0].copy().tolist()
        y_list = self.position[:, 1].copy().tolist()
        # zip打包函数:将两个列表按顺序打包成一个个元组,如[1, 2]和[3, 4]会被打包成类似于[(1, 3), (2, 4)]这样的格式
        for a, b in zip(x_list, y_list):
            plt.text(a, b, (a,b), ha='center', va='bottom', fontsize=10)
        plt.show()

3.3 个体定义以及适应度计算

定义个体类

  1. 适应度值为一次回路进行的距离
class Individuality:

    def __init__(self, city: City):
        # 染色体
        self.chromosome = []
        # 适应度
        self.fitness = 0.0
        self.size = city.size
        self.city = city

    # 设置起点城市完善个体
    def initByStart(self, num):
        self.chromosome = [i for i in range(self.size)]
        random.shuffle(self.chromosome)
        self.chromosome.remove(num)
        self.chromosome.insert(0, num)
        self.setFitness()

    # 根据染色体完善个体
    def initByChromosome(self, chromosome):
        self.chromosome = chromosome
        self.setFitness()

    # 计算适应度
    def setFitness(self):
        for i in range(self.size - 1):
            self.fitness += self.city.D[self.chromosome[i], self.chromosome[i + 1]]
        self.fitness += self.city.D[self.chromosome[-1], self.chromosome[0]]
    
    # 可视化方法
    def show(self, title:str):
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(self.city.position[:, 0], self.city.position[:, 1], s=30, color='b')
        ax.legend()
        plt.plot(self.city.position[self.chromosome, 0], self.city.position[self.chromosome, 1], 'r')
        plt.plot([self.city.position[self.chromosome[-1], 0], self.city.position[self.chromosome[0], 0]], [self.city.position[self.chromosome[-1], 1], self.city.position[self.chromosome[0], 1]], 'r')
        plt.title(title+f"/适应度:{self.fitness}")
        x_list = self.city.position[:, 0].copy().tolist()
        y_list = self.city.position[:, 1].copy().tolist()
        for a, b in zip(x_list, y_list):
            plt.text(a, b, (a,b), ha='center', va='bottom', fontsize=10)
        plt.show()

3.4 进化流程

定义训练类,后文逐一讲述思路

class Train:

    def __init__(self, city:City, lens, groups):
        # 导入城市布局
        self.city = city
        # 种群列表
        self.gen = []
        # 组大小
        self.lens = lens
        # 组数
        self.groups = groups
        # 种群大小
        self.size = lens * groups
    
    # 初始化种族
    def init_gen(self):
        pass
    
    # 交叉
    def cross(self):
        pass
    
    # 变异
    def mutate(self, new_chros):
        pass

    # 选择迭代
    def select(self):
        pass

    # 排序
    def mySort(self):
        pass

3.4.1 初始化种群

  1. 虽然最终路线是回路,但不同的起点总能诞生更多的可能
  2. 将个体分成groups个组,每组起点不一样,每组lens个个体
  3. 0<=groups<=city.size
  4. 则总个体数size = groups * lens ,总数最好为偶数
    def init_gen(self):
        self.gen = []
        for i in range(self.groups):
            for j in range(self.lens):
                ind = Individuality(self.city)
                ind.initByStart(i)
                self.gen.append(ind)

3.4.2 交叉

  1. 将两个个体直接互换某一片段可能会导致基因缺失/重复
    如 0-1-2-3-4-5和5-4-3-2-1-0,将前三个基因交换,
    则变成5-4-3-3-4-5和0-1-2-2-1-0
  2. 所以我们可以将两个个体交换基因片段转变为单个个体逐一改变基因位置
    如上面的两个个体交换前两个个基因,变成单个个体交换0-5,1-4的位置
    交叉完变成5-4-2-3-1-0和0-1-3-2-4-5
  3. 我们可以以字典的形式记录每个基因的位置,方便交换
    def cross(self):
        new_chros = []
        # 按顺序两两交叉,若总个数为奇数则忽略最后一名,所以建议总数为偶数
        for i in range(self.size // 2):
            chro1 = self.gen[i * 2].chromosome.copy()
            chro2 = self.gen[i * 2 + 1].chromosome.copy()
            # 随机交换片段
            index1 = random.randint(0, self.city.size - 2)
            index2 = random.randint(index1 + 1, self.city.size - 1)
            vk1 = {v: k for k, v in enumerate(chro1)}
            vk2 = {v: k for k, v in enumerate(chro2)}
            for j in range(index1, index2 + 1):
                v1 = chro1[j]
                v2 = chro2[j]
                nk1 = vk1[v2]
                nk2 = vk2[v1]
                chro1[j], chro1[nk1] = chro1[nk1], chro1[j]
                chro2[j], chro2[nk2] = chro2[nk2], chro2[j]
                vk1[v1], vk1[v2] = nk1, j
                vk2[v1], vk2[v2] = j, nk2
            new_chros.append(chro1)
            new_chros.append(chro2)
        return new_chros

3.4.3 变异

  1. 抽取大约50%交叉后染色体的一个片段,打乱以后放回去,生成新个体,加入种群
    def mutate(self, new_chros):
        for chro in new_chros:
            # 50%概率
            n = random.randint(0, 1)
            if n == 0:
                self.gen.append(ind)
                continue
            index1 = random.randint(0, self.city.size - 2)
            index2 = random.randint(index1 + 1, self.city.size - 1)
            mu_chro = chro[index1: index2 + 1]
            random.shuffle(mu_chro)
            new_chro = chro[:index1]
            new_chro.extend(mu_chro)
            new_chro.extend(chro[index2 + 1: ])
            ind = Individuality(self.city)
            ind.initByChromosome(new_chro)
            self.gen.append(ind)

3.4.4 选择

  1. 锦标赛算法
  2. 按适应度大小升序排序
  3. 此时两代加起来理论有size*2个个体,保留前size
# 选择迭代
    def select(self):
        self.mySort()
        self.gen = self.gen[:self.size]

    # 排序
    def mySort(self):
        for i in range(self.size * 2 - 1):
            for j in range(i + 1, self.size * 2):
                if self.gen[i].fitness > self.gen[j].fitness:
                    self.gen[i], self.gen[j] = self.gen[j], self.gen[i]

3.4.5 一次迭代

  1. 综合上面的方法
    def train(self, n):
        self.mutate(self.cross())
        self.select()
        print(f"第{n}次迭代最优个体适应度:{self.gen[0].fitness}")

3.5 迭代

  1. 建议按次数迭代,观察适应度曲线来判断是否已经结束

举例并打印最终结果

if __name__ == '__main__':
    # 迭代次数
    count = 1000
    # 10个城市
    city = City(10)
    # 种群初始化,4组,每组10个,共40个个体
    train = Train(city, 10, 4)
    train.init_gen()
    for i in range(count):
        train.train(i + 1)
    train.gen[0].show(f"{count}次迭代最优解")

部分结果展示

image.png

image.png

4 验证

下面提供一个普通的深度优先搜索方法,枚举每一种情况,得出理论最小适应度,大家可以参考

def dfs(chro: list, city: City):
    if len(chro) == city.size:
        ind = Individuality(city)
        ind.initByChromosome(chro)
        return ind
    inds = []
    for i in range(city.size):
        if i not in chro:
            new_chro = chro.copy()
            new_chro.append(i)
            ind = dfs(new_chro, city)
            inds.append(ind)
    n = len(inds)
    for i in range(n - 1):
        for j in range(i + 1, n):
            if(inds[j].fitness < inds[i].fitness):
                inds[i], inds[j] = inds[j], inds[i]
    # 返回最优个体
    return inds[0]

5 结论

遗传算法也可以作为一种智能搜索算法,但也有局限性,有很大的发展和改善空间,结合轮盘赌算法能更好,我的方法仅作为参考。实际生活中,不同路段的损耗,城市之间的通行限制会让这个算法更复杂,更有趣。

6 完整源代码(不包含枚举)

import random
from matplotlib import pyplot as plt
import numpy as np
import math
# 防止中文和特殊字符乱码
plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False 


# 城市类
class City:

    def __init__(self, size:int):
        # 城市数量
        self.size = size
        # 位置矩阵
        self.position = np.zeros((size, 2))
        x_list = random.sample(range(0, 100), size)
        y_list = random.sample(range(0, 100), size)
        self.position[:, 0] = x_list[:]
        self.position[:, 1] = y_list[:]
        # 距离矩阵D
        self.D = np.zeros((size, size), dtype='float64')
        for i in range(size):
            for j in range(size):
                self.D[i, j] = math.sqrt(pow((self.position[i, 0] - self.position[j, 0]), 2) + pow((self.position[i, 1] - self.position[j, 1]), 2))

    # 可视化方法
    def show(self):
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(self.position[:, 0], self.position[:, 1], s=30, color='b')
        ax.legend()
        x_list = self.position[:, 0].copy().tolist()
        y_list = self.position[:, 1].copy().tolist()
        for a, b in zip(x_list, y_list):
            plt.text(a, b, (a,b), ha='center', va='bottom', fontsize=10)
        plt.show()



# 个体类
class Individuality:

    def __init__(self, city: City):
        # 染色体
        self.chromosome = []
        # 适应度
        self.fitness = 0.0
        self.size = city.size
        self.city = city

    # 设置起点城市完善个体
    def initByStart(self, num):
        self.chromosome = [i for i in range(self.size)]
        random.shuffle(self.chromosome)
        self.chromosome.remove(num)
        self.chromosome.insert(0, num)
        self.setFitness()

    # 根据染色体完善个体
    def initByChromosome(self, chromosome):
        self.chromosome = chromosome
        self.setFitness()

    # 计算适应度
    def setFitness(self):
        for i in range(self.size - 1):
            self.fitness += self.city.D[self.chromosome[i], self.chromosome[i + 1]]
        self.fitness += self.city.D[self.chromosome[-1], self.chromosome[0]]
    
    # 可视化方法
    def show(self, title:str):
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(self.city.position[:, 0], self.city.position[:, 1], s=30, color='b')
        ax.legend()
        plt.plot(self.city.position[self.chromosome, 0], self.city.position[self.chromosome, 1], 'r')
        plt.plot([self.city.position[self.chromosome[-1], 0], self.city.position[self.chromosome[0], 0]], [self.city.position[self.chromosome[-1], 1], self.city.position[self.chromosome[0], 1]], 'r')
        plt.title(title+f"/适应度:{self.fitness}")
        x_list = self.city.position[:, 0].copy().tolist()
        y_list = self.city.position[:, 1].copy().tolist()
        for a, b in zip(x_list, y_list):
            plt.text(a, b, (a,b), ha='center', va='bottom', fontsize=10)
        plt.show()


class Train:

    def __init__(self, city:City, lens, groups):
        # 导入城市布局
        self.city = city
        # 种群列表
        self.gen = []
        # 组大小
        self.lens = lens
        # 组数
        self.groups = groups
        # 种群大小
        self.size = lens * groups
    
    # 初始化种族
    def init_gen(self):
        self.gen = []
        for i in range(self.groups):
            for j in range(self.lens):
                ind = Individuality(self.city)
                ind.initByStart(i)
                self.gen.append(ind)

    # 交叉
    def cross(self):
        new_chros = []
        # 按顺序两两交叉,若总个数为奇数则忽略最后一名,所以建议总数为偶数
        for i in range(self.size // 2):
            chro1 = self.gen[i * 2].chromosome.copy()
            chro2 = self.gen[i * 2 + 1].chromosome.copy()
            # 随机交换片段
            index1 = random.randint(0, self.city.size - 2)
            index2 = random.randint(index1 + 1, self.city.size - 1)
            vk1 = {v: k for k, v in enumerate(chro1)}
            vk2 = {v: k for k, v in enumerate(chro2)}
            for j in range(index1, index2 + 1):
                v1 = chro1[j]
                v2 = chro2[j]
                nk1 = vk1[v2]
                nk2 = vk2[v1]
                chro1[j], chro1[nk1] = chro1[nk1], chro1[j]
                chro2[j], chro2[nk2] = chro2[nk2], chro2[j]
                vk1[v1], vk1[v2] = nk1, j
                vk2[v1], vk2[v2] = j, nk2
            new_chros.append(chro1)
            new_chros.append(chro2)
        return new_chros
 
    # 变异
    def mutate(self, new_chros):
        for chro in new_chros:
            # 50%概率
            n = random.randint(0, 1)
            if n == 0:
                self.gen.append(ind)
                continue
            index1 = random.randint(0, self.city.size - 2)
            index2 = random.randint(index1 + 1, self.city.size - 1)
            mu_chro = chro[index1:index2 + 1]
            random.shuffle(mu_chro)
            new_chro = chro[:index1]
            new_chro.extend(mu_chro)
            new_chro.extend(chro[index2 + 1: ])
            ind = Individuality(self.city)
            ind.initByChromosome(new_chro)
            self.gen.append(ind)

    # 选择迭代
    def select(self):
        self.mySort()
        self.gen = self.gen[:self.size]

    # 排序
    def mySort(self):
        for i in range(self.size * 2 - 1):
            for j in range(i + 1, self.size * 2):
                if self.gen[i].fitness > self.gen[j].fitness:
                    self.gen[i], self.gen[j] = self.gen[j], self.gen[i]
    
    # 进化一次
    def train(self, n):
        self.mutate(self.cross())
        self.select()
        print(f"第{n}次迭代最优个体适应度:{self.gen[0].fitness}")



if __name__ == '__main__':
    # 迭代次数
    count = 1000
    # 10个城市
    city = City(10)
    # 种群初始化,4组,每组10个,共40个个体
    train = Train(city, 10, 4)
    train.init_gen()
    for i in range(count):
        train.train(i + 1)
    train.gen[0].show(f"{count}次迭代最优解")