遗传算法(Genetic Algorithm, GA)附代码案例

遗传算法(Genetic Algorithm, GA)

简介

遗传算法(Genetic Algorithm, GA)是一种模拟自然选择和遗传学原理的搜索算法,属于进化计算的一种。它是由约翰·霍兰德(John Holland)在20世纪70年代提出的,用于解决优化问题,是一种启发式算法。遗传算法的基本思想是通过模拟生物进化过程中的遗传和变异机制来优化问题的解。

遗传算法(Genetic Algorithm, GA)附代码案例_第1张图片

算法流程

  • 初始化:随机生成一组染色体(解的编码),构成初始种群。
  • 适应度评估:根据问题的目标函数(或自定义适应度函数)计算每个染色体的适应度值,适应度越高的染色体越有可能被选中。
  • 选择:从当前种群中选择个体以用于产生后代。选择过程基于个体的适应度,常用的选择方法包括轮盘赌选择、锦标赛选择、截断选择等。
  • 交叉:从选中的染色体中随机选择一对进行交叉操作,生成新的染色体。交叉操作可以是单点交叉、多点交叉或均匀交叉等。
  • 变异:对新生成的染色体进行变异操作,以增加种群的多样性,防止算法过早收敛。变异操作可以是位翻转、随机扰动等。
  • 迭代:重复执行适应度评估、选择、交叉和变异步骤,直到满足停止条件,如达到最大迭代次数或解的质量达到预定标准。

编码方式

  • 二进制编码:将染色体表示为01的序列。
  • 实数编码:将染色体表示为实数序列。
  • 混合编码:结合二进制编码和实数编码,适用于不同类型的问题。

适应度函数

适应度函数是遗传算法中评价染色体好坏的标准,它通常与问题的目标函数相对应。适应度函数的设计对算法的性能有很大影响。

参数设置

  • 种群大小:种群中的染色体数量。
  • 交叉率:进行交叉操作的概率。
  • 变异率:进行变异操作的概率。
  • 迭代次数:算法运行的最大迭代次数

优缺点

  • 优点:全局搜索能力强,适用于非线性、多峰值、多目标等问题,易于实现并行计算。
  • 缺点:可能需要较长的运行时间,参数选择对算法性能有较大影响,有时难以保证找到全局最优解。

细节讨论

如何确定编码长度?

我们需要知道决策变量的上界与下界 [a, b],以及需要的精度 Acc,设编码长度为l

根据公式:
2 l − 1 < ( b − a ) × 1 0 A c c ≤ 2 l − 1 2^{l - 1} \lt (b - a) \times 10^{Acc} \leq 2^l - 1 2l1<(ba)×10Acc2l1
以此,我们可以求解出l的值。

l = 1
stand = (b - a) * 10 ** Acc
while 2 ** (l - 1) < stand:
    if stand < 2 ** l - 1:
        break
    l = l + 1

要想实现每个数值都能分配一个独一无二的串,那么串所能表示的数值个数就要大于等于数值解的个数。

解码

一般的,区间范围为[a, b],区间长度为L,即: L = b − a L = b - a L=ba,串长度为n,当前串对应十进制为T,则该串对应实数解为:
x = a + T × b − a 2 n − 1 x = a + T \times \frac{b - a}{2^n - 1} x=a+T×2n1ba

常用的选择方式:

  1. 轮盘赌选择(Roulette-wheel selection):这是一种概率选择方法,个体被选中的概率与其适应度值成正比。通过将适应度值转换为选择概率,可以确保适应度较高的个体有较大的机会被选中。如果适应度值为负,可以通过适应度值的正负转换来处理。轮盘赌选择可能会导致适应度较低的个体被淘汰,从而减少种群多样性 。

  2. 锦标赛选择(Tournament selection):在这种选择方法中,随机选取一部分个体,然后根据适应度选择最优个体。这种方法的特点是简单易行,并且可以通过改变锦标赛的大小来调整选择压力 。

  3. 截断选择(Truncation selection):根据个体的适应度值进行排序,然后只选择排名在前面的一定数量的个体。这种方法可以快速选择优秀个体,但可能会降低种群的多样性 。

  4. 线性排序选择(Linear-rank selection):首先按适应度值对个体进行排序,然后根据个体的排名赋予选择概率,排名越靠前的个体选择概率越高。这种方法在适应度值差异较大时特别有用 。

  5. 指数排序选择(Exponential-rank selection):与线性排序选择类似,但是选择概率的分配更加偏向于排名靠前的个体,即适应度较高的个体被选中的概率会更高 。

  6. 随机遍历选择(Stochastic-universal selection):类似于轮盘赌选择,但是通过在轮盘上设置多个固定点来选择个体,这样可以一次性选择多个个体,并且鼓励适应度较高的个体至少被选择一次 。

  7. 精英选择(Elite selection):确保每一代中最优秀的个体能够直接遗传到下一代,以保持种群的最优特性 。

  8. 蒙特卡洛选择(Monte Carlo selection):随机选择个体,不依赖于适应度值,主要用于比较其他选择方法的性能 。

  9. 概率选择(Probability selection):基于选择概率从种群中选择个体,不需要对种群进行排序,选择概率可以基于适应度值来确定 。

  10. 玻尔兹曼选择(Boltzmann selection):选择概率基于个体的适应度值和温度参数,类似于物理中的玻尔兹曼分布,可以增加选择的随机性 。

交叉复制的方式

  1. 将个体适应度大小映射为概率进行复制,适应度高的个体有更大概率复制,且复制分数越多 —— 轮盘赌注法
  2. 对适应度高的前 N 4 \frac{N}{4} 4N的个体进行复制,然后用这些个体把后 N 4 \frac{N}{4} 4N个体替换掉 —— 精英产生精英
  3. 可以随机进行交叉复制
  4. 不一定只替换掉坏解,适应度高的解能替换掉。

变异的方式

  1. 可以对每个个体都进行变异
  2. 可以选择适应度第的后 N 4 \frac{N}{4} 4N的个体或后 N 2 \frac{N}{2} 2N的个体进行变异
  3. 可以按适应度大小映射为概率来进行变异
  4. 可以选择多个位点进行变异

轮盘赌选择

轮盘赌选择(Roulette Wheel Selection)是遗传算法中的一种选择方法,其思想来源于轮盘赌博中的随机选择机制。在轮盘赌中,轮盘被分成多个相等大小的扇区,每个扇区对应一个染色体,然后通过旋转轮盘来随机选择一个扇区,从而选择一个染色体。在遗传算法中,轮盘赌选择的思想是将染色体的选择概率与其适应度值相关联,适应度值越高的染色体被选中的概率越大。

轮盘赌选择的基本思想:

  1. 计算每个染色体的适应度值。
  2. 计算所有染色体适应度值的总和。
  3. 为每个染色体分配一个选择概率,其选择概率与其适应度值成正比。
  4. 通过随机生成一个[0, 总适应度值]范围内的数来选择染色体。从第一个染色体开始,累加其适应度值,直到累加值大于或等于随机生成的数。

代码实现:

import random

# 假设有4个染色体,每个染色体的适应度值如下
fitness_values = [10, 20, 30, 40]
chromosomes = ['A', 'B', 'C', 'D']

# 计算总适应度值
total_fitness = sum(fitness_values)

# 定义轮盘赌选择函数
def roulette_wheel_selection(fitness_values):
    # 生成[0, 总适应度值]范围内的随机数
    random_value = random.uniform(0, total_fitness)
    
    # 累加适应度值,直到超过随机数
    current_sum = 0
    for i, fitness in enumerate(fitness_values):
        current_sum += fitness
        if current_sum > random_value:
            return chromosomes[i]

# 使用轮盘赌选择方法选择一个染色体
selected_chromosome = roulette_wheel_selection(fitness_values)
print(f"Selected chromosome: {selected_chromosome}")

代码实验

1. 二进制优化问题

import random

# 定义遗传算法参数
POPULATION_SIZE = 100  # 种群大小
GENES = 100  # 染色体长度
MUTATION_RATE = 0.01  # 突变率
CROSS_RATE = 0.6  # 交叉率
GENERATIONS = 1000  # 迭代次数

# 初始化种群
population = [[random.randint(0, 1) for _ in range(GENES)] for _ in range(POPULATION_SIZE)]


# 评估函数
def evaluate(chromosome):
    return sum(chromosome)


# 轮盘赌选择函数
def selection(population, fitness):
    fitness_sum = sum(fitness)
    probability = [f / fitness_sum for f in fitness]
    return random.choices(population, weights=probability, k=2)[0]


# 交叉函数
def crossover(parent1, parent2):
    if random.random() < CROSS_RATE:
        crossover_point = random.randint(1, GENES - 1)
        child1 = parent1[:crossover_point] + parent2[crossover_point:]
        child2 = parent2[:crossover_point] + parent1[crossover_point:]
        return child1, child2
    return parent1, parent2


# 突变函数
def mutate(chromosome):
    for i in range(GENES):
        if random.random() < MUTATION_RATE:
            chromosome[i] = 1 - chromosome[i]
    return chromosome


# 遗传算法主循环
for generation in range(GENERATIONS):
    # 评估当前种群
    fitness = [evaluate(chromosome) for chromosome in population]

    # 创建新的种群
    new_population = []
    while len(new_population) < POPULATION_SIZE:
        # 选择
        parent1, parent2 = selection(population, fitness), selection(population, fitness)

        # 交叉
        child1, child2 = crossover(parent1, parent2)

        # 突变
        child1 = mutate(child1)
        child2 = mutate(child2)

        # 将新产生的个体加入到新种群
        new_population.append(child1)
        if len(new_population) < POPULATION_SIZE:
            new_population.append(child2)

    # 替换种群
    population = new_population

    if generation % 100 == 0:  # 每100代打印一次
        best_index = fitness.index(max(fitness))
        print(f"迭代次数为: {generation} - 当前迭代适应度最好的值: {max(fitness)}")

# 打印最终结果
best_chromosome = max(population, key=evaluate)
best_fitness = evaluate(best_chromosome)
print(f"总共的迭代次数: {GENERATIONS} - 最终适应度最好的染色体: {best_chromosome} - 最终的适应度: {best_fitness}")

2. 单目标优化问题

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 函数f1 x和y的取值范围
x_bound = [-5.12, 5.12]
y_bound = [-5.12, 5.12]

# 函数f2 x和y的取值范围
# x_bound = [-2.048, 2.048]
# y_bound = [-2.048, 2.048]

# 函数f3 x和y的取值范围
# x_bound = [-6, 6]
# y_bound = [-6, 6]

# 函数f4 x和y的取值范围
# x_bound = [-5, 5]
# y_bound = [-5, 5]

# 函数f5 x和y的取值范围
# x_bound = [-1, 1]
# y_bound = [-1, 1]

X_precision = 5  # 决策变量的精度,表示小数点后n位
y_precision = 6  # 目标变量的精度

pop_size = 30  # 种群个体数
crossover_rate = 0.75  # 交叉算子
mutation_rate = 0.01  # 变异算子
max_generation = 1000  # 最多迭代的进化次数
min_generation = 100  # 最少需迭代的进化次数
without_optim_tolerate = 200  # 如果持续200代最优值改善很小(1e-6)的话,提前终止迭代


# 利用决策变量的精度和取值区间计算基因数
def cal_DNA_size(x_bound, y_bound, X_precision):
    '''
    :param x_bound: x的边界
    :param y_bound: y的边界
    :param X_precision: 要求的精度
    :return: x的基因个数,y的基因个数
    '''
    x_size, y_size = 1, 1
    # 公式
    discriminant_X = (x_bound[1] - x_bound[0]) * 10 ** X_precision
    discriminant_Y = (y_bound[1] - y_bound[0]) * 10 ** X_precision
    while 2 ** (x_size - 1) < discriminant_X:
        if discriminant_X < 2 ** x_size - 1:
            break
        x_size = x_size + 1

    while 2 ** (y_size - 1) < discriminant_Y:
        if discriminant_Y < 2 ** y_size - 1:
            break
        y_size = y_size + 1

    return x_size, y_size


x_size, y_size = cal_DNA_size(x_bound, y_bound, X_precision)
DNA_size = x_size + y_size


# 适应度计算,即函数值,函数表达式乘-1
def get_fitness(x, y):
    # return -1 * (x ** 2 + y ** 2)
    # return  -1 * (100 * (y - x ** 2) ** 2 + (x - 1) ** 2)
    # return  -1 * ((x ** 2 + y - 11) ** 2 + (x + y ** 2 - 7) ** 2)
    # return  -1 * (4 * x ** 2 - 2.1 * x ** 4 + x ** 6 / 3 + x * y + 4 * y ** 4 - 4 * y ** 2)
    return -1 * (x ** 2 + 2 * y ** 2 - 0.3 * np.cos(3 * np.pi * x) * np.cos(4 * np.pi * y) + 0.3)


# 二进制转十进制,变换到决策变量取值区间
def binary_to_decimal(pop, bound, size):
    return np.around(bound[0] + pop.dot(2 ** np.arange(size)[::-1]) * (bound[1] - bound[0]) / (2 ** x_size - 1),
                     decimals=X_precision)


# 模拟自然选择,适应度越大,越大概率被保留
def select(pop, fitness):
    '''
    选择保留的部分,使用轮盘赌注法
    :param pop: 种群
    :param fitness: 拟合度列表
    :return:选择的群体
    '''
    idx = np.random.choice(np.arange(pop_size), size=pop_size - 1, replace=True,
                           p=(fitness + (abs(min(fitness)) if min(fitness) < 0 else 0)) / (
                                   fitness.sum() + abs(min(fitness) if min(fitness) < 0 else 0) * pop_size))
    return pop[idx]


# 模拟交叉,生成新的子代
def crossover(parent, pop):
    '''
    返回一个新的子代
    :param parent: 父代DNA序列
    :param pop: 群体
    :return: 交叉/不交叉的DNA序列
    '''
    if np.random.rand() < crossover_rate:
        # 随机得到群体中的某个DNA序列
        i_ = np.random.randint(0, pop_size, size=1)
        # 随机生成一个终点
        cross_points = np.random.randint(0, DNA_size, size=1)
        cross_points = cross_points[0]
        # 将父代的DNA序列的0 ~ cross_points 与群体的cross_points ~ DNA_size进行交叉
        return np.append(parent[0:cross_points], pop[i_, cross_points:])
    return parent


# 模拟变异
def mutate(child):
    '''
    返回变异的子代
    :param child: 子代的一个DNA序列
    :return: 变异后的基因序列
    '''
    for point in range(DNA_size):
        if np.random.rand() < mutation_rate:
            child[point] = 1 if child[point] == 0 else 0
    return child


# 随机初始化一个种群,大小为[种群个数,DNA长度]
pop = np.random.randint(2, size=(pop_size, DNA_size))

# 初始化
best_F = float('-inf')
worse_F = float('inf')
best_x, best_y = 0, 0
actual_generation = 0

# 存储每次迭代中适应度最好的个体
cur_best_F_list = []
# 存储所有迭代中值最好的
best_F_list = []

# 核心代码,进行迭代进化
for i in range(max_generation):
    # 先将初始化的种群中的x,y进行提取出来,然后转换为十进制
    x = binary_to_decimal(pop[:, 0:x_size], x_bound, x_size)
    y = binary_to_decimal(pop[:, x_size:], y_bound, y_size)
    # 计算适应度
    fitness = np.around(get_fitness(x, y), decimals=y_precision)

    # 计算当前种群中最好的变量x和y
    cur_best_x = binary_to_decimal(pop[np.argmax(fitness), 0:x_size], x_bound, x_size)
    cur_best_y = binary_to_decimal(pop[np.argmax(fitness), x_size:], y_bound, y_size)
    if (i + 1) % 50 == 0:
        print("当前迭代次数:", i + 1)
        # 输出当前种群中的最好的DNA序列
        print("当前迭代中适应度最好的DNA序列: ", pop[np.argmax(fitness), :])
        print("当前迭代中适应度最好的变量x和y: ", cur_best_x, cur_best_y)
        # 输出当前种群中的最小值
        print("适应度最好的值Value: ", -1 * fitness[np.argmax(fitness)])

    # 判断更新最终的最好适应度,最好变量x,y
    if fitness[np.argmax(fitness)] > best_F:
        best_F = fitness[np.argmax(fitness)]
        best_x = cur_best_x
        best_y = cur_best_y
    # 判断更新最坏的适应度
    if fitness[np.argmax(fitness)] < worse_F:
        worse_F = fitness[np.argmax(fitness)]

    # 判断是否需要提前终止迭代
    if i + 1 > min_generation and (
            best_F - best_F_list[i - (without_optim_tolerate if i > without_optim_tolerate else i)]) < 10 ** (-y_precision):
        actual_generation = i + 1
        break

    # 精英保留,np.vstack()可以垂直堆叠,这里是直接将选择的精英与原始群体适应度较高的群体进行拼接
    pop = np.vstack((select(pop, fitness), pop[np.argmax(fitness), :]))
    pop_copy = pop.copy()  # parent会被child替换,所以先copy一份pop
    for parent in pop:
        # 进行交叉
        child = crossover(parent, pop_copy)
        # 进行突变
        child = mutate(child)
        # 将变异后的个体重新赋值给原个体
        parent[:] = child
    # 添加当前迭代适应度最好的适应度
    cur_best_F_list.append(fitness[np.argmax(fitness)])
    # 添加截至到当前迭代次数最好的适应度
    best_F_list.append(best_F)

# 迭代完输出最优值和对应的决策变量
print("所有迭代中最优的值", -1 * best_F)
print(f"所有迭代中最优的决策变量:x = {best_x}, y = {best_y}")

3. TSP问题

旅行商问题(Traveling Salesman Problem,TSP)是一个经典的组合优化问题。它描述的是一个旅行商人需要访问一系列城市,目标是找到一条最短的路径,使得每个城市恰好访问一次并最终返回到起始城市。这个问题可以被看作是在带权完全无向图中寻找一个权值最小的Hamilton回路。由于问题的解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,因此TSP是一个NP完全问题。

# coding=utf-8
import math
from math import floor
import numpy as np
import time
import matplotlib.pyplot as plt  # 导入所需要的库


class GeneticAlgorithm_TSP(object):

    def __init__(self, data, max_epoch=1000, pop_size=200, cross_prob=0.80, pmuta_prob=0.2, select_prob=0.8):
        self.max_epoch = max_epoch  # 最大迭代次数
        self.pop_size = pop_size  # 群体个数
        self.cross_prob = cross_prob  # 交叉概率
        self.pmuta_prob = pmuta_prob  # 变异概率
        self.select_prob = select_prob  # 选择概率
        # 城市数据
        self.data = data
        # 城市个数
        self.city_nums = len(data)
        # 任意两个城市之间的距离矩阵
        self.distance_matrix = self.distance_matrix()

        # 通过选择概率确定子代的选择个数(群体个数 * 选择概率) + 0.5,floor向下取整,保证至少选择两个个体
        self.select_num = max(floor(self.pop_size * self.select_prob + 0.5), 2)

        # 父代和子代群体的初始化(不直接用np.zeros是为了保证单个染色体的编码为整数,np.zeros对应的数据类型为浮点型)
        self.parent = np.array([0] * self.pop_size * self.city_nums).reshape(self.pop_size,
                                                                             self.city_nums)  # 父 print(chrom.shape)(200, 14)
        self.child = np.array([0] * int(self.select_num) * self.city_nums).reshape(self.select_num,
                                                                                   self.city_nums)  # 子 (160, 14)

        # 存储群体中每个染色体的路径总长度
        self.total_path = np.zeros(self.pop_size)
        # 存储最优的距离
        self.best_distance = []
        # 存储最优的路径
        self.best_path = []

    def distance_matrix(self):
        '''
        计算任意两个城市之间的距离
        :return:返回一个距离矩阵,[i, j]表示城市i到城市j之间的距离
        '''
        value_matrix = np.zeros((self.city_nums, self.city_nums))
        for i in range(self.city_nums):
            for j in range(i + 1, self.city_nums):
                # 求解两点之间的距离(欧氏距离)
                # value_matrix[i, j] = math.sqrt(np.sum(self.data[i][0] - self.data[j][0])**2 + np.sum(self.data[i][1] - self.data[j][1])**2)
                value_matrix[i, j] = np.linalg.norm(self.data[i, :] - self.data[j, :])
                # 距离是无向的,i -> j  == j -> i
                value_matrix[j, i] = value_matrix[i, j]
        return value_matrix

    def init_parent(self):
        '''
        随机初始化种群
        :return:None
        '''
        # 初始化一条路径为[0, 1, ..., self.city_nums - 1]
        init_gene = np.array(range(self.city_nums))
        # 为种群中的每一个个体赋初值
        for i in range(self.pop_size):
            # 随机打乱路径序列
            np.random.shuffle(init_gene)
            self.parent[i, :] = init_gene
            # 计算当前路径的总长度,并进行存储
            self.total_path[i] = self.compute_one_total_path(self.parent[i, :])

    def compute_one_total_path(self, one_path: list):
        '''
        计算一条路径的总长度
        :param one_path: 一条路径
        :return: 路径总长度(int)
        '''
        one_distance = 0
        for i in range(self.city_nums - 1):
            one_distance += self.distance_matrix[
                one_path[i], one_path[i + 1]]  # matrix_distance n*n, 第[i,j]个元素表示城市i到j距离
        one_distance += self.distance_matrix[one_path[-1], one_path[0]]  # 最后一个城市 到起点距离
        return one_distance


    # 子代选取,根据选中概率与对应的适应度函数,采用随机遍历选择方法
    def select_child(self):
        '''
        选取子代
        :return:
        '''
        # 适应度函数
        fit = 1. / (self.total_path)
        fit_sum = np.cumsum(fit)  # 累积求和   a = np.array([1,2,3]) b = np.cumsum(a) b=1 3 6
        # pick值分布在[0, fit_sum[-1]) 范围内, 加上一个[0, 1]的随机数来模拟随机的情况,否则整体是一个均分的情况
        pick = fit_sum[-1] / self.select_num * (np.random.rand() + np.array(range(int(self.select_num))))
        i, j = 0, 0
        index = []
        while i < self.pop_size and j < self.select_num:
            # 如果第i个适应度的累加值大于第j个随机的适应度,就选择第i个为子代(适应度越小越好)
            # 并且可以重复选取第i个为子代,直到第i个的适应度值比随机选取的更小
            if fit_sum[i] >= pick[j]:
                index.append(i)
                j += 1
            else:
                i += 1
        # 将从父代中选择的index[]个位置的物种赋值给子代
        self.child = self.parent[index, :]

    def cross(self):
        '''
        进行随机交叉
        :return:None
        '''
        # 判断选择的个体是奇数还是偶数
        if self.select_num % 2 == 0:  # select_num160
            num = range(0, int(self.select_num), 2)
        else:
            num = range(0, int(self.select_num - 1), 2)
        for i in num:
            if self.cross_prob >= np.random.rand():  # 模拟选择概率
                # 选择第i个个体与第i + 1个个体进行交叉
                self.child[i, :], self.child[i + 1, :] = self.one_cross(self.child[i, :], self.child[i + 1, :])

    def one_cross(self, parent_a, parent_b):
        '''
        计算任意两个染色体进行交叉的情况
        :param parent_a: 染色体1号
        :param parent_b: 染色体2号
        :return: 返回交叉后的两个染色体
        '''
        left, right = self.get_tow_random()
        parent_a_copy = parent_a.copy()  # 父亲
        parent_b_copy = parent_b.copy()  # 母亲
        # 将[left, right]之间的基因进行交换
        for i in range(left, right + 1):
            # 赋值当前父母亲的染色体
            parent_a_inner_copy = parent_a.copy()
            parent_b_inner_copy = parent_b.copy()
            # 将第i个基因进行交换
            parent_a[i] = parent_b_copy[i]
            parent_b[i] = parent_a_copy[i]
            # 给出第i个基因的所有索引(可能重复)
            repeat_x = np.argwhere(parent_a == parent_a[i])  # 返回出非0数据的索引
            repeat_y = np.argwhere(parent_b == parent_b[i])
            # 判断一个染色体内是否有相同的基因,如果有,重新交换回来
            if len(repeat_x) == 2:
                # repeat_x[repeat_x != i] 得到不是当前交换基因的另一个重复基因的索引
                parent_a[repeat_x[repeat_x != i]] = parent_a_inner_copy[i]
            if len(repeat_y) == 2:
                parent_b[repeat_y[repeat_y != i]] = parent_b_inner_copy[i]
        return parent_a, parent_b

    def mutation(self):
        '''
        进行概率变异,方法:染色体随机交换两个位置
        :return:None
        '''
        # 遍历每一个选择的子代
        for i in range(int(self.select_num)):
            # 进行概率变异
            if np.random.rand() <= self.pmuta_prob:
                # 随机生成两个位置
                position_1, position_2 = self.get_tow_random()
                # 将两个位置进行交换
                self.child[i, [position_1, position_2]] = self.child[i, [position_2, position_1]]

    def reverse_child(self):
        '''
        随机选择两个基因进行反转,如果能得到更好的
        :return:
        '''
        # 遍历所有选择的子代
        for i in range(int(self.select_num)):
            # 得到两个有序的随机数
            left, right = self.get_tow_random()
            sel = self.child[i, :].copy()
            # 将对应区间的染色体进行反转赋值
            sel[left:right + 1] = self.child[i, left:right + 1][::-1]
            # 如果反转后的路径更短,则进行替换
            if self.compute_one_total_path(sel) < self.compute_one_total_path(self.child[i, :]):
                self.child[i, :] = sel

    # 子代插入父代,得到相同规模的新群体
    def fusion(self):
        '''
        将子代插入到父代中,得到同父代相同规模的群体
        :return:None
        '''
        # np.argsort返回排序后的原始索引,这里返回的是父代的索引
        index = np.argsort(self.total_path)[::-1]
        # 将父代中最坏的给替换掉
        self.parent[index[:self.select_num], :] = self.child

    def get_tow_random(self):
        '''
        随机生成[0 ,city_nums)范围内的随机数,并且进行顺序排序
        :return: left < right
        '''
        # 随机生成两个随机数
        r1 = np.random.randint(self.city_nums)
        r2 = np.random.randint(self.city_nums)
        # 避免 r1 == r2 的情况
        while r2 == r1:
            r2 = np.random.randint(self.city_nums)
        # 将两个随机数进行排序
        left, right = min(r1, r2), max(r1, r2)
        return left, right

    def draw_path(self, one_path):
        '''
        显示最优的路径
        :param one_path: 一条最优的路径
        :return:
        '''
        res = str(one_path[0] + 1) + '-->'
        for i in range(1, self.city_nums):
            res += str(one_path[i] + 1) + '-->'
        res += str(one_path[0] + 1) + '\n'
        print(res)

    def draw(self, x, y, flag=1):
        '''
        进行绘图
        :param x:
        :param y:
        :param flag: 值为1:表示绘制路径,值为0:表示绘制城市
        :return:
        '''
        if not flag:
            fig, ax = plt.subplots()
            ax.scatter(x, y, linewidths=0.1)
            for i, txt in enumerate(range(1, len(data) + 1)):
                ax.annotate(txt, (x[i], y[i]))
        else:
            for i in range(len(self.data) - 1):
                plt.quiver(x[i], y[i], x[i + 1] - x[i], y[i + 1] - y[i], color='r', width=0.005, angles='xy', scale=1,
                           scale_units='xy')
            plt.quiver(x[-1], y[-1], x[0] - x[-1], y[0] - y[-1], color='r', width=0.005, angles='xy', scale=1,
                       scale_units='xy')
            plt.show()


# 路径坐标
data = np.array([20.00, 96.10, 16.47, 94.44,
                 20.09, 92.54, 22.39, 93.37,
                 25.23, 97.24, 22.00, 96.05,
                 20.47, 97.02, 17.20, 96.29,
                 16.30, 97.38, 25.05, 98.12,
                 16.53, 96.50, 21.52, 95.59,
                 19.41, 97.13, 20.09, 92.55,
                 20.10, 95.10, 20.20, 94.10,
                 20.60, 96.88, 18.66, 90.20]).reshape(18, 2)


def main(data):
    GAT = GeneticAlgorithm_TSP(data)
    # 初始化父代种群
    GAT.init_parent()
    x, y = data[:, 0], data[:, 1]
    # 绘制初始化的路径图
    GAT.draw(x, y, flag=0)
    # 绘制初始化种群的第一条路径
    path_init = GAT.parent[0]
    GAT.draw(x[path_init], y[path_init])

    print('初始染色体的路程: ' + str(GAT.total_path[0]))

    # 循环迭代遗传过程
    for i in range(GAT.max_epoch):
        GAT.select_child()  # 选择子代
        GAT.cross()  # 交叉
        GAT.mutation()  # 变异
        GAT.reverse_child()  # 进化逆转
        GAT.fusion()  # 子代插入

        # 重新计算新群体的距离值
        for j in range(GAT.pop_size):
            GAT.total_path[j] = GAT.compute_one_total_path(GAT.parent[j, :])

        # 每50步打印信息
        index = GAT.total_path.argmin()
        if (i + 1) % 50 == 0:
            # 获取当前时间戳 记录运算时间
            timestamp = time.time()
            formatted_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
            print(formatted_time)
            print('第' + str(i + 1) + '代后的最短的路程: ' + str(GAT.total_path[index]))
            print('第' + str(i + 1) + '代后的最优路径:')
            GAT.draw_path(GAT.parent[index, :])  # 显示每一步的最优路径

        # 存储每一步的最优路径及距离
        GAT.best_distance.append(GAT.total_path[index])
        GAT.best_path.append(GAT.parent[index, :])
    path_ans = GAT.parent[0]
    GAT.draw(x[path_ans], y[path_ans])

    return GAT  # 返回遗传算法结果类


if __name__ == '__main__':
    GAT = main(data)
    print(GAT.best_distance[0: -1: 50])
    print(GAT.best_path[-2:-1])

参考文章:

  • 利用遗传算法解决函数最优化问题(单目标)

  • 遗传算法 (解决TSP旅行商问题) 附Python代码_遗传算法求解旅行商问题python+matplotlib-CSDN博客

你可能感兴趣的:(机器学习,python高级用法,遗传算法,启发式算法,python)