• Python 遗传算法 Genetic Algorithm


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

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

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

    遗传算法有几个关键词:

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

    下面是我编写的遗传算法模板,在使用时需要重写 new_unit(群体初始化方法)、cross(两个体交叉方法)、variation(个体变异方法)、fitness(群体适应度计算方法) 函数(后面我将会以旅行商问题进行举例)

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

    • 对当前的群体进行重叠检测,去除重复的个体
    • 计算每个个体的适应度,排序后对适应度前 5% 的个体进行“保优”,得到规模 0.05n 的新群体
    • 对原有群体进行天择,选出 0.95n 的个体(可重复),根据概率对每个个体进行交叉、变异操作,加入新群体得到规模 n 的群体
    1. import random
    2. import numpy as np
    3. from tqdm import trange
    4. DTYPE = np.float16
    5. class GeneticOpt:
    6. ''' 遗传算法
    7. :param n_unit: 染色体群体规模
    8. :param n_gene: 染色体的基因数
    9. :param well_radio: 最优个体比例
    10. :param cross_proba: 交叉概率
    11. :param var_proba: 变异概率'''
    12. def __init__(self,
    13. n_unit: int,
    14. n_gene: int,
    15. well_radio: float = 0.05,
    16. cross_proba: float = 0.4,
    17. var_proba: float = 0.3):
    18. self.n_unit = n_unit
    19. self.n_gene = n_gene
    20. self._well_radio = well_radio
    21. self._cross_proba = cross_proba
    22. self._var_proba = var_proba
    23. def new_unit(self, size: int) -> np.ndarray:
    24. ''' 初始化染色体群体
    25. :return: [size, n_gene]'''
    26. raise NotImplementedError
    27. def cross(self, unit: np.ndarray, other: np.ndarray) -> np.ndarray:
    28. ''' 交叉遗传
    29. :return: [n_gene, ]'''
    30. raise NotImplementedError
    31. def variation(self, unit: np.ndarray) -> np.ndarray:
    32. ''' 基因突变
    33. :return: [n_gene, ]'''
    34. gene_idx = np.arange(self.n_gene)
    35. l = random.choice(gene_idx)
    36. r = random.choice(gene_idx[l:])
    37. np.random.shuffle(unit[l: r + 1])
    38. return unit
    39. def fitness(self, group: np.ndarray) -> np.ndarray:
    40. ''' 适应度函数 (max -> best)'''
    41. raise NotImplementedError
    42. def fit(self, epochs: int,
    43. patience: int = np.inf,
    44. prefix: str = 'GA_fit') -> np.ndarray:
    45. ''' :param epochs: 训练轮次
    46. :param patience: 允许搜索无进展的次数'''
    47. cur_group = self.new_unit(self.n_unit)
    48. pbar = trange(epochs)
    49. last_fitness, angry = - np.inf, 0
    50. # 最优个体数, 随机选取数
    51. n_well = max(2, round(self.n_unit * self._well_radio))
    52. n_choose = self.n_unit - n_well
    53. for _ in pbar:
    54. cur_group = np.unique(cur_group, axis=0)
    55. # 计算每个个体的适应度并排序
    56. fitness = self.fitness(cur_group)
    57. order = np.argsort(fitness)[::-1]
    58. cur_group, fitness = cur_group[order], fitness[order]
    59. # 收敛检测
    60. angry = 0 if fitness[0] > last_fitness else angry + 1
    61. last_fitness = fitness[0]
    62. if angry == patience: break
    63. # 保留一定数量的个体
    64. new_group = cur_group[:n_well]
    65. pbar.set_description((f'%-10s' + '%-10.4g') % (prefix, fitness[0]))
    66. # 使用轮盘赌法进行筛选
    67. proba = fitness - fitness.min()
    68. proba = proba / proba.sum()
    69. for pc, pv in np.random.random([n_choose, 2]):
    70. unit = random.choices(cur_group, weights=proba)[0]
    71. # 交叉遗传 / 基因突变
    72. if pc <= self._cross_proba:
    73. unit = self.cross(unit.copy(), random.choices(cur_group, weights=proba)[0].copy())
    74. if pv <= self._var_proba:
    75. unit = self.variation(unit.copy())
    76. # 拼接新个体
    77. new_group = np.concatenate([new_group, unit[None]])
    78. cur_group = new_group
    79. return cur_group[0]

    求解示例

    对于 15 个地点的旅行商问题,重写的函数思路如下:

    • init_adj:使用实例属性 pos 记录 15 个地点的位置,实例属性 adj 记录这 15 个地点的邻接矩阵
    • new_unit:生成 n 个 [0, 1, ..., 14],使用 np.random.shuffle 进行打乱
    • fitness:对每个个体,依次遍历个体中的地点叠加距离(越小表示该解越优),并取负值(越大表示该解越优,符合 fit 函数的设计)
    • cross:因为旅行商问题中的解在进行交叉时(交换片段),容易出现“重复经过一地点”的情况,故此处不使用交叉
    • variation:随机选取区间的左右边界,使用 np.random.shuffle 对该区间的基因进行打乱(已编写在模板中)
    1. if __name__ == '__main__':
    2. import matplotlib.pyplot as plt
    3. class ShortestPath(GeneticOpt):
    4. def init_adj(self):
    5. # 初始化邻接矩阵
    6. self.pos = np.random.random([self.n_gene, 2]) * 10
    7. self.adj = np.zeros([self.n_gene] * 2, dtype=DTYPE)
    8. for i in range(self.n_gene):
    9. for j in range(i + 1, self.n_gene):
    10. self.adj[i][j] = self.adj[j][i] = \
    11. np.sqrt(((self.pos[i] - self.pos[j]) ** 2).sum())
    12. def new_unit(self, size):
    13. ''' 初始化染色体群体'''
    14. group = []
    15. for _ in range(size):
    16. unit = list(range(self.n_gene))
    17. np.random.shuffle(unit)
    18. group += [unit]
    19. return np.array(group, dtype=np.int32)
    20. def fitness(self, group):
    21. ''' 适应度函数 (max -> best)'''
    22. group = np.concatenate([group, group[:, :1]], axis=-1)
    23. return - self.adj[group[:, :-1], group[:, 1:]].sum(axis=-1)
    24. ga = ShortestPath(80, 15, cross_proba=0, var_proba=0.6)
    25. ga.init_adj()
    26. unit = ga.fit(500)
    27. # 绘制最优路径
    28. fig = plt.subplot()
    29. for key in 'right', 'top':
    30. fig.spines[key].set_color('None')
    31. plt.plot(*ga.pos[unit].T, c='deepskyblue')
    32. plt.scatter(*ga.pos.T, marker='p', c='orange')
    33. plt.show()

  • 相关阅读:
    攻防演习防御体系构建之第一篇之介绍和防守的四个阶段
    DockerFile笔记
    【jquery Ajax】接口的学习与Postcode插件的使用
    C++从入门到出门
    前端vue实现讲师新增和修改功能
    IT冷知识--每日一练
    【云原生 | Docker 基础篇】06、本地镜像发布到阿里云
    RepVGG 核心讲解
    4-1网络层-网络层的功能
    js 删除数组中指定元素——5种方式
  • 原文地址:https://blog.csdn.net/qq_55745968/article/details/126858094