Python 遗传算法 Genetic Algorithm

粒子群算法常常用于在连续解空间内搜索,而在不连续、离散的空间内常常会出现搜索越界的问题

例如旅行商问题,寻找可以遍历 15 个地点的最短路径(当然可以用二进制状态压缩 + 动态规划解决),以 {0, 1, ..., 14} 表示这些地点,并以 {0, 1, ..., 14} 的一种排列方式为一个解

当这个问题的解集映射在 15 维空间中时,这个空间中的可行解将非常的稀疏,从而阻碍粒子群的搜索

遗传算法有几个关键词:

  • 保优:当种群更新时,不改变最优的几个个体,为交叉提供优质基因并与新个体进行比较
  • 天择:根据每个个体的适应度,使用轮盘赌法进行挑选
  • 交叉:对天择生成的每个个体,以一定概率与原来的个体进行交叉
  • 变异:对天择生成的每个个体,以一定概率进行基因突变

下面是我编写的遗传算法模板,在使用时需要重写 ChromosomeBase 的 __init__(用于生成随机个体)、__eq__(用于重叠检测)、fitness(群体适应度计算方法)、variation(个体变异方法)、cross_with(两个体交叉方法),后面我将会以旅行商问题进行举例

其中的 fit 方法为主函数,记群体规模为 n,for 循环体的内容为:

  • 调用 sort_unique 函数计算个体的适应度,并去除异常个体、重复个体
  • 根据个体的适应度,排序后对适应度前 2% 的个体进行“保优”,得到规模 0.02n 的新群体
  • 对原有群体进行天择,选出 0.98n 的个体(可重复),根据概率对每个个体进行交叉、变异操作,加入新群体得到规模 n 的群体
import random
from typing import Type

import numpy as np
import pandas as pd
from tqdm import trange


class ChromosomeBase:
    ''' 染色体基类'''

    def __init__(self, *args, **kwargs):
        ''' 无参构造: 用于生成随机个体
            有参构造: 用于遗传交叉、基因突变'''
        raise NotImplementedError

    def __eq__(self, other):
        ''' 重载 == 运算符, 用于去重'''
        raise NotImplementedError

    def fitness(self) -> float:
        ''' 适应度函数 (max -> best)'''
        raise NotImplementedError

    def variation(self):
        ''' 基因突变'''
        raise NotImplementedError

    def cross_with(self, other):
        ''' 交叉遗传'''
        raise NotImplementedError


class GeneticOpt:
    ''' 遗传算法
        :param chromosome: 染色体类
        :param n_unit: 染色体群体规模
        :param cross_proba: 交叉概率
        :param var_proba: 变异概率
        :param well_radio: 最优个体比例
        :ivar group: 染色体群体'''

    def __init__(self,
                 chromosome: Type[ChromosomeBase],
                 n_unit: int,
                 cross_proba: float = 0.4,
                 var_proba: float = 0.3,
                 well_radio: float = 0.2):
        self.chromosome = chromosome
        self.n_unit = n_unit
        self.group = self.new_unit(self.n_unit)
        self.log = []

        assert 0 <= cross_proba <= 1, 'cross_proba must be in [0, 1]'
        assert 0 <= var_proba <= 1, 'var_proba must be in [0, 1]'
        self._cross_proba = cross_proba
        self._var_proba = var_proba
        assert cross_proba + var_proba > 0, 'cross_proba + var_proba must be greater than 0'

        assert 0 <= well_radio <= 1, 'well_radio must be in [0, 1]'
        self._well_radio = well_radio

    def new_unit(self, size: int) -> list:
        ''' 初始化染色体群体'''
        return [self.chromosome() for _ in range(size)]

    def sort_unique(self, group: list):
        ''' 计算每个染色体的适应度, 排序、去重'''
        fitness = np.array([x.fitness() for x in group])
        # 去除无限值
        cond = np.isfinite(fitness)
        group, fitness = np.array(group)[cond], fitness[cond]
        # 去除重叠染色体
        order = np.argsort(fitness)[::-1]
        for i in range(len(order)):
            for j in range(i + 1, len(order)):
                if fitness[order[i]] == fitness[order[j]] and group[order[i]] == group[order[j]]:
                    order[i] = -1
                    break
        # 应用排序结果
        order = order[order != -1]
        return group[order].tolist(), fitness[order]

    def fit(self, epochs: int,
            patience: int = np.inf,
            prefix: str = 'GA-fit') -> np.ndarray:
        ''' :param epochs: 训练轮次
            :param patience: 允许搜索无进展的次数'''
        pbar = trange(epochs)
        angry = 0
        # 最优个体数, 随机选取数
        n_well = max(2, round(self.n_unit * self._well_radio))
        n_choose = self.n_unit - n_well
        # 轮盘赌法采样
        f_sample = lambda p: random.choices(self.group, weights=p)[0]
        for _ in pbar:
            self.group, fitness = self.sort_unique(self.group)
            # 收敛检测
            if self.log and np.isfinite(self.log[-1][0]):
                angry = 0 if fitness[0] > self.log[-1][0] else angry + 1
                if angry == patience: break
            self.log.append([fitness[0], fitness.mean(), fitness.std(), len(fitness)])
            # 保留一定数量的个体
            tmp_group = self.group[:n_well]
            pbar.set_description((f'%-10s' + '%-10.4g') % (prefix, fitness[0]))
            # 使用轮盘赌法进行筛选
            p = fitness - fitness.min()
            p = p / p.sum()
            for _ in range(n_choose):
                pc, pv = np.random.random(2)
                unit = f_sample(p)
                # 交叉遗传 / 基因突变
                if pc <= self._cross_proba: unit = unit.cross_with(f_sample(p))
                if pv <= self._var_proba: unit = unit.variation()
                tmp_group.append(unit)
            self.group = tmp_group
        pbar.close()
        return self.group[0], pd.DataFrame(self.log, columns=['fit-best', 'fit-mean', 'fit-std', 'n-unique'])

求解示例

对于 15 个地点的旅行商问题,使用全局变量 POS 记录 15 个地点的位置,实例属性 ADJ 记录这 15 个地点的邻接矩阵

ChromosomeBase 的重写思路如下:

  • __init__:将路径信息记入实例属性 data。无参数传入时,生成随机路径;有参数传入时,使用给定的路径
  • __eq__:使用 np.all 比较两个实例的 data 属性
  • fitness:针对 data 所描述的路径,对邻接矩阵查询距离信息得出路径长度(越小表示该解越优),并取负值(越大表示该解越优,符合 fit 函数的设计)
  • variation:随机选取区间的左右边界,使用 np.random.shuffle 对该区间的基因进行打乱
  • cross_with:因为旅行商问题中的解在进行交叉时(交换片段),容易出现“重复经过一地点”的情况,故此处不使用交叉
if __name__ == '__main__':
    import matplotlib.pyplot as plt

    N_NODE = 15

    POS = np.random.random([N_NODE, 2]) * 10
    ADJ = np.zeros([N_NODE] * 2, dtype=np.float16)
    # 初始化邻接矩阵
    for i in range(N_NODE):
        for j in range(i + 1, N_NODE):
            ADJ[i][j] = ADJ[j][i] = np.sqrt(((POS[i] - POS[j]) ** 2).sum())


    class Path(ChromosomeBase):

        def __init__(self, data=None):
            self.data = data if isinstance(data, np.ndarray) else np.random.permutation(N_NODE)

        def __eq__(self, other):
            return np.all(self.data == other.data)

        def fitness(self) -> float:
            data = np.concatenate([self.data, self.data[:1]])
            return - ADJ[data[:-1], data[1:]].sum()

        def variation(self):
            ''' 基因突变'''
            l = np.random.randint(0, N_NODE - 1)
            r = np.random.randint(l + 1, N_NODE)
            # note: 对数据进行深拷贝, 否则会影响其他个体
            data = self.data.copy()
            np.random.shuffle(data[l: r + 1])
            return __class__(data)

        def cross_with(self, other):
            ''' 交叉遗传'''
            raise NotImplementedError


    ga = GeneticOpt(Path, 80, cross_proba=0, var_proba=0.6)
    unit, log = ga.fit(500)
    print(log)

    # 绘制最优路径
    fig = plt.subplot()
    for key in 'right', 'top':
        fig.spines[key].set_color('None')
    plt.plot(*POS[unit.data].T, c='deepskyblue')
    plt.scatter(*POS.T, marker='p', c='orange')
    plt.show()

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
遗传算法是一种模拟自然进化过程的优化算法,主要用于解决复杂的优化问题。在遗传算法中,通过对个体(解)进行基因操作(交叉、变异等),不断地产生新的个体,并通过选择机制,筛选出适应度高的个体,从而逐步优化得到最优解。 下面是一个简单的遗传算法Python实现代码: ```python import random # 定义适应度函数 def fitness(individual): # 适应度函数为 x^2 的形式,其中 x 为个体的染色体长度 return sum([gene**2 for gene in individual]) # 初始化种群 def init_population(pop_size, gene_size): population = [] for i in range(pop_size): individual = [random.randint(0, 1) for j in range(gene_size)] population.append(individual) return population # 选择操作 def selection(population): # 轮盘赌选择 fitness_values = [fitness(individual) for individual in population] total_fitness = sum(fitness_values) probabilities = [fitness/total_fitness for fitness in fitness_values] selected_population = [] for i in range(len(population)): selected_individual = None while selected_individual is None: for j in range(len(population)): if random.random() < probabilities[j]: selected_individual = population[j] break selected_population.append(selected_individual) return selected_population # 交叉操作 def crossover(parent1, parent2, crossover_rate): # 一点交叉 if random.random() > crossover_rate: return parent1, parent2 crossover_point = random.randint(1, len(parent1)-1) child1 = parent1[:crossover_point] + parent2[crossover_point:] child2 = parent2[:crossover_point] + parent1[crossover_point:] return child1, child2 # 变异操作 def mutation(individual, mutation_rate): # 每个基因以 mutation_rate 的概率发生变异 for i in range(len(individual)): if random.random() < mutation_rate: individual[i] = 1 - individual[i] return individual # 遗传算法 def genetic_algorithm(pop_size, gene_size, max_generation, crossover_rate, mutation_rate): population = init_population(pop_size, gene_size) for i in range(max_generation): population = selection(population) new_population = [] while len(new_population) < pop_size: parent1, parent2 = random.sample(population, 2) child1, child2 = crossover(parent1, parent2, crossover_rate) child1 = mutation(child1, mutation_rate) child2 = mutation(child2, mutation_rate) new_population.append(child1) new_population.append(child2) population = new_population best_individual = min(population, key=lambda individual: fitness(individual)) return best_individual # 测试 best_individual = genetic_algorithm(pop_size=100, gene_size=10, max_generation=1000, crossover_rate=0.8, mutation_rate=0.1) print(best_individual, fitness(best_individual)) ``` 在上面的代码中,定义了适应度函数、初始化种群、选择、交叉、变异等操作,并通过遗传算法不断迭代,最终得到最优解。在测试中,我们设定种群大小为100,染色体长度为10,最大迭代次数为1000,交叉率为0.8,变异率为0.1,得到的最优解为[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],适应度函数的值为0。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

荷碧TongZJ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值