• 遗传算法求解旅行商问题(含python源代码)


    目录

    前言

    编码初始化种群

    计算适应度

    选择

    交叉

    变异

    完整代码

    总结


    前言

    这次的算法有一点不能确定是否正确,希望有大佬能够批评指正。

    遗传算法的一般步骤

    编码初始化种群

    种群(population)指同一时间生活在一定自然区域内,同种生物的所有个体。

    所以种群是由个体组成的,所以先需要得到个体,然后再随机产生一定数目的个体。

    在本算法中个体采用的是实数编码(表示城市的编号,对应列表的下标,按照顺序就是在城市之间的移动路线)

    先对城市的位置进行初始化,采用的是用列表来表示城市的坐标,可以单个定义,也可以随机生成

    先生成一串有序的数字用来表示城市的编号,再每次随机进行打乱后储存到pop列表中,pop表示种群,里面装着的是列表用来表示个体。(个体表示城市的编号,种群表示编号打乱后所有个体的集合

    群体规模太小,不能提供足够的采样点,以致算法性能很差,易陷入局部最优解。

    群体规模太大,尽管可以增加优化信息,阻止早熟收敛的发生,但无疑会增加计算量,造成收敛时间太长,表现为收敛速度缓慢。

    1. City_Map = 100 * np.random.rand(20, 2) # 随机产生20个城市(20行2列,数值乘以100)
    2. DNA_SIZE = len(City_Map) # 编码长度(返回行的个数)
    3. POP_SIZE = 100 # 种群大小
    4. # 生成初代种群pop
    5. pop = []
    6. list = list(range(DNA_SIZE)) # 生成[0,DNA_SIZE)的列表
    7. for i in range(POP_SIZE): # POP_SIZE是指种群大小,在程序中是一个固定的值(打乱POP_SIZE次之后把结果储存到pop列表中
    8. random.shuffle(list) # 随机打乱list,进行初始化操作
    9. l = list.copy() # 把list中的数据拷贝到l中
    10. pop.append(l) # 将l添加到pop列表中

    计算适应度

    适应度函数值只能是正值,越大越好。

    DNA表示个体,根据个体的值(表示城市的编号)来计算距离。

    旅行商问题要求距离越短越好,所以距离越大越不满足要求,故而可以通过对距离求倒数来表示适应度。

    在最后减去适应度最小的值,可以保证适应度都为正值。(如果有负数,减去一个更小的负数,会变成正值)

    适应度表示的是个体对种群的适应程度,distance函数返回的是按照pop[i]路线计算出来的路径距离大小,故而getfitness函数返回的列表表示的是每一个个体pop[i]通过distance函数计算出来距离的倒数归一化(减去最小的距离的倒数)之后的结果。

    distance求出来的距离不是两个城市之间的距离,而是某条路线经过城市的距离之和(是所有的距离)。

    1. def distance(DNA): # 根据DNA的路线计算距离
    2. dis = 0
    3. temp = City_Map[DNA[0]]
    4. for i in DNA[1:]:
    5. # sqrt(pow(x-x0,2)+pow(y-y0,2))
    6. dis = dis + ((City_Map[i][0] - temp[0]) ** 2 + (City_Map[i][1] - temp[1]) ** 2) ** 0.5
    7. temp = City_Map[i]
    8. return dis + ((temp[0] - City_Map[DNA[0]][0]) ** 2 + (temp[1] - City_Map[DNA[0]][1]) ** 2) ** 0.5
    9. def getfitness(pop): # 计算种群适应度,这里适应度用距离的倒数表示
    10. temp = []
    11. for i in range(len(pop)):
    12. temp.append(1 / (distance(pop[i])))
    13. # 减去最小值是为了防止适应度出现负值
    14. return temp - np.min(temp)

    选择

    选择操作也称为复制( reproduction) 操作:从当前群体中按照一定概率选出优良的个体, 使它们有机会作为父代繁殖下一代子孙。
    判断个体优良与否的准则是各个个体的适应度值:个体适应度越高, 其被选择的机会就越多。

    在程序中个体的选择方法采用的是轮盘赌的方法:

    按个体的选择概率产生一个轮盘,轮盘每个区的角度与个体的选择概率成比例。
    产生一个随机数, 它落入转盘的哪个区域就选择相应的个体交叉。
    适应度的小的个体也有可能被选中。

    1. def select(pop, fitness): # 根据适应度选择,以赌轮盘的形式,适应度越大的个体被选中的概率越大
    2. # print(fitness)
    3. s = fitness.sum()
    4. # np.random.choice(a,size,replace,p=None)随机抽取样本a,表示范围,replace=True被抽中后仍有机会被再次抽中,p没抽中的概率
    5. temp = np.random.choice(np.arange(len(pop)), size=POP_SIZE, replace=True, p=(fitness / s))
    6. p = []
    7. for i in temp:
    8. p.append(pop[i])
    9. return p

    交叉

    程序中交叉采用部分匹配交叉,如果直接采用两点交叉会导致一个个体中出现两个重复的城市,显然是和题目要求是不符合的。

    部分匹配交叉保证了每个染色体中的基因仅出现一次,通过该交叉策略在一个染色体中不会出现重复的基因,所以部分匹配交叉经常用于旅行商(TSP)或其他排序问题编码。部分匹配交叉类似于两点交叉,通过随机选择两个交叉点确定交叉区域。执行交叉后一般会得到两个无效的染色体,个别基因会出现重复的情况,为了修复染色体,可以在交叉区域内建立每个染色体的匹配关系,然后在交叉区域外对重复基因应用此匹配关系就可以消除冲突。

    交叉概率太大时,种群中个体更新很快,会造成高适应度值的个体很快被破坏掉;

    概率太小时,交叉操作很少进行,从而会使搜索停滞不前,造成算法的不收敛。

    1. def crossmuta(pop, CROSS_RATE): # 交叉变异
    2. new_pop = []
    3. for i in range(len(pop)): # 遍历种群中的每一个个体,将该个体作为父代
    4. n = np.random.rand()
    5. if n >= CROSS_RATE: # 大于交叉概率时不发生变异,该子代直接进入下一代
    6. temp = pop[i].copy()
    7. new_pop.append(temp) # 直接进行拷贝
    8. if n < CROSS_RATE: # 小于交叉概率时发生变异
    9. list1 = pop[i].copy()
    10. list2 = pop[np.random.randint(POP_SIZE)].copy() # 选取种群中另一个个体进行交叉(随机选择)
    11. status = True
    12. while status: # 产生2个不相等的节点,中间部分作为交叉段,采用部分匹配交叉(直到k1
    13. k1 = random.randint(0, len(list1) - 1)
    14. k2 = random.randint(0, len(list2) - 1)
    15. if k1 < k2:
    16. status = False
    17. k11 = k1 # 保存切片起始的下标
    18. # 先对部分片段进行切片,把切片出来的内容进行交换(完全交换)
    19. fragment1 = list1[k1: k2]
    20. fragment2 = list2[k1: k2]
    21. list1[k1: k2] = fragment2
    22. list2[k1: k2] = fragment1
    23. del list1[k1: k2] # 删除list1中[k1,k2)的内容
    24. left1 = list1
    25. # 进行部分匹配的交叉
    26. offspring1 = []#后代
    27. #对left1中的每一个位置pos遍历
    28. for pos in left1:
    29. #检查它是否存在于frag2中
    30. if pos in fragment2:
    31. #从fragment1中找到对应的基因
    32. pos = fragment1[fragment2.index(pos)]
    33. #直到基因不再fragment2中为止(遍历fragment2,确保每一个基因都和pos不同)
    34. while pos in fragment2:
    35. pos = fragment1[fragment2.index(pos)]
    36. offspring1.append(pos)
    37. continue
    38. #如何pos不存在fragment2中,那么就直接将其添加到新的后代中
    39. offspring1.append(pos)
    40. # 插入新片段
    41. for i in range(0, len(fragment2)):
    42. offspring1.insert(k11, fragment2[i])
    43. k11 += 1
    44. temp = offspring1.copy()
    45. mutation(temp, MUTA_RATE) # 进行变异
    46. new_pop.append(temp) # 把部分匹配交叉后形成的合法个体加入到下一代种群
    47. return new_pop

    变异

    互换变异:随机选取染色体的两个基因进行简单互换。
    采用随机的形式,在[0,DNA_SIZE)的范围内生成两个下标(确保两个位置不一致),在将两个位置上面的值进行交换。

    变异概率太小则很难产生新模式,变异概率太大则会使遗传算法成为随机搜索算法。

    1. def mutation(DNA, MUTA_RATE): # 进行变异
    2. # 两点变异
    3. if np.random.rand() < MUTA_RATE: # 以MUTA_RATE的概率进行变异
    4. mutate_point1 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置
    5. mutate_point2 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置
    6. while (mutate_point1 == mutate_point2): # 保证2个所选位置不相等
    7. mutate_point2 = np.random.randint(0, DNA_SIZE) #如果相等将mutate_point2重新进行随机生成位置
    8. DNA[mutate_point1], DNA[mutate_point2] = DNA[mutate_point2], DNA[mutate_point1] # 2个所选位置进行互换

    逆转变异:在个体码串中随机选择两点( 逆转点) ,然后将两点之间的基因值以逆向排序插入到原位置中。

    随机生成两个下标,如x1,x2(确保x1

    1. def mutation(DNA, MUTA_RATE): # 进行变异
    2. # 逆转变异
    3. if np.random.rand() < MUTA_RATE: # 以MUTA_RATE的概率进行变异
    4. status = True
    5. while status: # 产生2个不相等的节点,中间部分作为交叉段,采用部分匹配交叉(直到mutate_point1
    6. mutate_point1 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因片段的起始的位置
    7. mutate_point2 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因片段的结束的位置
    8. if mutate_point1 < mutate_point2:
    9. status = False
    10. k1 = mutate_point1 # 保存切片起始的下标
    11. temp = DNA[mutate_point1:mutate_point2] # 把需要逆转的片段先提取出来
    12. del DNA[mutate_point1:mutate_point2] # 先暂时删除这段片段
    13. temp.reverse() # 反转基因序列
    14. # 插入翻转后的新片段
    15. for i in range(0, len(temp)):
    16. DNA.insert(k1, temp[i])
    17. k1 += 1

    插入变异:在个体码串中随机选择一个码, 然后将此码插入随机选择的插入点中间。

    随机生成两个实数,这次不是交换,是插入的方式,也就是插入点之后的元素的位置都会发生改变。

    1. def mutation(DNA, MUTA_RATE): # 进行变异
    2. #插入变异
    3. if np.random.rand() < MUTA_RATE: # 以MUTA_RATE的概率进行变异
    4. mutate_point1 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置(选中一个基因)
    5. mutate_point2 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置(插入点)
    6. while (mutate_point1 == mutate_point2): # 保证2个所选位置不相等
    7. mutate_point2 = np.random.randint(0, DNA_SIZE)
    8. temp=DNA[mutate_point1]#先保存mutate_point1对应的值
    9. del DNA[mutate_point1]#删除mutate_point1对应的值
    10. DNA.insert(mutate_point2,temp)#重新插入到列表中

    完整代码

    1. import time
    2. import numpy as np
    3. import random
    4. import matplotlib.pyplot as plt
    5. # 各个城市的坐标
    6. City_Map = 100 * np.random.rand(10, 2) # 随机产生10个城市(10行2列,数值乘以100)
    7. DNA_SIZE = len(City_Map) # 编码长度(返回行的个数)
    8. POP_SIZE = 100 # 种群大小
    9. CROSS_RATE = 0.85 # 交叉率
    10. MUTA_RATE = 0.15 # 变异率
    11. Iterations = 500 # 迭代次数
    12. def distance(DNA): # 根据DNA的路线计算距离
    13. dis = 0
    14. temp = City_Map[DNA[0]]
    15. for i in DNA[1:]:
    16. # sqrt(pow(x-x0,2)+pow(y-y0,2))
    17. dis = dis + ((City_Map[i][0] - temp[0]) ** 2 + (City_Map[i][1] - temp[1]) ** 2) ** 0.5
    18. temp = City_Map[i]
    19. return dis + ((temp[0] - City_Map[DNA[0]][0]) ** 2 + (temp[1] - City_Map[DNA[0]][1]) ** 2) ** 0.5
    20. def getfitness(pop): # 计算种群适应度,这里适应度用距离的倒数表示
    21. temp = []
    22. for i in range(len(pop)):
    23. temp.append(1 / (distance(pop[i])))
    24. # 减去最小值是为了防止适应度出现负值
    25. return temp - np.min(temp)
    26. def select(pop, fitness): # 根据适应度选择,以赌轮盘的形式,适应度越大的个体被选中的概率越大
    27. # print(fitness)
    28. s = fitness.sum()
    29. # np.random.choice(a,size,replace,p=None)随机抽取样本a,表示范围,replace=True被抽中后仍有机会被再次抽中,p没抽中的概率
    30. temp = np.random.choice(np.arange(len(pop)), size=POP_SIZE, replace=True, p=(fitness / s))
    31. p = []
    32. for i in temp:
    33. p.append(pop[i])
    34. return p
    35. def mutation(DNA, MUTA_RATE): # 进行变异
    36. # 两点变异
    37. if np.random.rand() < MUTA_RATE: # 以MUTA_RATE的概率进行变异
    38. mutate_point1 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置
    39. mutate_point2 = np.random.randint(0, DNA_SIZE) # 随机产生一个实数,代表要变异基因的位置
    40. while (mutate_point1 == mutate_point2): # 保证2个所选位置不相等
    41. mutate_point2 = np.random.randint(0, DNA_SIZE) #如果相等将mutate_point2重新进行随机生成位置
    42. DNA[mutate_point1], DNA[mutate_point2] = DNA[mutate_point2], DNA[mutate_point1] # 2个所选位置进行互换
    43. def crossmuta(pop, CROSS_RATE): # 交叉变异
    44. new_pop = []
    45. for i in range(len(pop)): # 遍历种群中的每一个个体,将该个体作为父代
    46. n = np.random.rand()
    47. if n >= CROSS_RATE: # 大于交叉概率时不发生变异,该子代直接进入下一代
    48. temp = pop[i].copy()
    49. new_pop.append(temp) # 直接进行拷贝
    50. if n < CROSS_RATE: # 小于交叉概率时发生变异
    51. list1 = pop[i].copy()
    52. list2 = pop[np.random.randint(POP_SIZE)].copy() # 选取种群中另一个个体进行交叉(随机选择)
    53. status = True
    54. while status: # 产生2个不相等的节点,中间部分作为交叉段,采用部分匹配交叉(直到k1
    55. k1 = random.randint(0, len(list1) - 1)
    56. k2 = random.randint(0, len(list2) - 1)
    57. if k1 < k2:
    58. status = False
    59. k11 = k1 # 保存切片起始的下标
    60. # 先对部分片段进行切片,把切片出来的内容进行交换(完全交换)
    61. fragment1 = list1[k1: k2]
    62. fragment2 = list2[k1: k2]
    63. list1[k1: k2] = fragment2
    64. list2[k1: k2] = fragment1
    65. del list1[k1: k2] # 删除list1中[k1,k2)的内容
    66. left1 = list1
    67. # 进行部分匹配的交叉
    68. offspring1 = []#后代
    69. #对left1中的每一个位置pos遍历
    70. for pos in left1:
    71. #检查它是否存在于frag2中
    72. if pos in fragment2:
    73. #从fragment1中找到对应的基因
    74. pos = fragment1[fragment2.index(pos)]
    75. #直到基因不再fragment2中为止(遍历fragment2,确保每一个基因都和pos不同)
    76. while pos in fragment2:
    77. pos = fragment1[fragment2.index(pos)]
    78. offspring1.append(pos)
    79. continue
    80. #如何pos不存在fragment2中,那么就直接将其添加到新的后代中
    81. offspring1.append(pos)
    82. # 插入新片段
    83. for i in range(0, len(fragment2)):
    84. offspring1.insert(k11, fragment2[i])
    85. k11 += 1
    86. temp = offspring1.copy()
    87. mutation(temp, MUTA_RATE) # 进行变异
    88. new_pop.append(temp) # 把部分匹配交叉后形成的合法个体加入到下一代种群
    89. return new_pop
    90. def print_info(pop): # 用于输出结果
    91. fitness = getfitness(pop)
    92. maxfitness = np.argmax(fitness) # 得到种群中最大适应度个体的索引
    93. # 打印结果
    94. print("最优的基因型:", pop[maxfitness])
    95. print("最短距离:", distance(pop[maxfitness]))
    96. # 按最优结果顺序把地图上的点加入到best_map列表中
    97. best_map = []
    98. for i in pop[maxfitness]:
    99. best_map.append(City_Map[i])
    100. best_map.append(City_Map[pop[maxfitness][0]])
    101. X = np.array((best_map))[:, 0]
    102. Y = np.array((best_map))[:, 1]
    103. # 绘制地图以及路线
    104. plt.figure()
    105. plt.rcParams['font.sans-serif'] = ['SimHei']
    106. plt.scatter(X, Y)
    107. for dot in range(len(X) - 1):
    108. plt.annotate(pop[maxfitness][dot], xy=(X[dot], Y[dot]), xytext=(X[dot], Y[dot]))
    109. plt.annotate('start', xy=(X[0], Y[0]), xytext=(X[0] + 1, Y[0]))
    110. plt.plot(X, Y)
    111. if __name__ == "__main__": # 主循环
    112. # 生成初代种群pop
    113. pop = []
    114. list = list(range(DNA_SIZE)) # 生成[0,DNA_SIZE)的列表
    115. for i in range(POP_SIZE): # POP_SIZE是指种群大小,在程序中是一个固定的值(打乱POP_SIZE次之后把结果储存到pop列表中
    116. random.shuffle(list) # 随机打乱list,进行初始化操作
    117. l = list.copy() # 把list中的数据拷贝到l中
    118. pop.append(l) # 将l添加到pop列表中
    119. best_dis = []
    120. # 最好适应度
    121. #goodFitness = 0
    122. # 最差适应度(如果进行归一化处理之后,适应度都减去最小值,那么他的最差适应度不都就是0了)
    123. #chaFitness = 0
    124. # 总体适应度
    125. #sumFitness = 0
    126. # 所有数量
    127. #sumCount = 0
    128. # 平均适应度
    129. #averageFitness = 0
    130. # 获取当前时间(算法开始时间)
    131. start_time = time.time()
    132. # 进行选择,交叉,变异,并把每代的最优个体保存在best_dis中
    133. for i in range(Iterations): # 迭代N代
    134. pop = crossmuta(pop, CROSS_RATE) # CROSS_RATE交叉率
    135. fitness = getfitness(pop) # 得到适应度种群的适应度
    136. # 更新最差适应度
    137. # print(np.min(fitness))
    138. tmpfitness = np.max(fitness)
    139. # 更新最好适应度
    140. #if tmpfitness > goodFitness:
    141. # goodFitness = tmpfitness
    142. # print(goodFitness)
    143. # 记录所有适应度的值
    144. #sumFitness = np.sum(fitness) + sumFitness
    145. # 记录所有适应度的个数
    146. #sumCount = sumCount + np.size(fitness)
    147. maxfitness = np.argmax(fitness) # 返回数值最大的索引
    148. best_dis.append(distance(pop[maxfitness]))
    149. pop = select(pop, fitness) # 选择生成新的种群(适应度最大的)
    150. print("iteration", i)
    151. #averageFitness = sumFitness / sumCount
    152. # 获取当前时间(算法结束时间)
    153. end_time = time.time()
    154. print_info(pop) # 打印信息
    155. print('逐代的最小距离:', best_dis)
    156. #print(f'最好适应度:{goodFitness:.4f}')
    157. #print(f'最差适应度:{chaFitness:.4f}')
    158. #print(f'平均适应度:{averageFitness:.4f}')
    159. print(f'程序运行时间:{(end_time - start_time):.4f}秒')
    160. #print(pop)
    161. # 画图
    162. plt.figure()
    163. plt.plot(range(Iterations), best_dis)
    164. plt.show()
    165. plt.close()

    参考信息

    遗传算法入门详解 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/100337680遗传算法python进阶理解+论文复现(纯干货,附前人总结引路)_python神经网络遗传算法_不想秃头的夜猫子的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/golden_knife/article/details/128510731通俗易懂地解释遗传算法 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/136393730遗传算法解决旅行商问题(详细解释+代码分享) - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/344588977用遗传算法求解旅行商问题_中国旅行商问题,34个省会-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/breeze_blows/article/details/102992997遗传算法(三)——适应度与选择_遗传算法适应度函数-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_30239361/article/details/101540896

    总结

    这个最好适应度,最差适应度以及平均适应度的概念没完全掌握,不知道是不是这一个意思,所以在程序中注释了,大家可以根据自己的理解来添加。

    我理解到的:

    最好适应度就是每一次的迭代都会有一个适应度最高的个体,把每一代适应度最高的个体进行比较选出最大的那个,得到的就是整体适应度最好的。

    最差适应度就是0,因为每一次都进行了归一化操作。

    平均适应度就是保存每一代的适应度以及个体的数量,最后进行求平均值。

    运行时间就是在算法运行前记录当前系统的时间,算法运行后记录当前系统的时间,所形成的差值(运行后-运行前)就是程序运行的时间。

    适应度因为采用的是距离的倒数,加上还进行了归一化的处理,随机生成城市的的坐标*100,导致两点之间的距离较大,如果想得到较大一点的适应度,可以缩小城市之间的距离。

    City_Map = 100 * np.random.rand(20, 2)

    City_Map =  np.random.rand(20, 2)

    遗传算法的思路是“适者生存,优胜劣汰”,模拟生物的进化,以一个初始生物群体为起点,经过竞争后,一部分个体被淘汰而无法再进入这个循环圈,而另一部分则胜出成为种群。对于算法的选择的个体,适应度高的并不一定进入种群,只是进入种群的可能性比较大;而适应度低的个体并不一定被淘汰,只是进入种群的可能性比较小,模拟生物进化的过程。

  • 相关阅读:
    深度学习机器学习面试题——自然语言处理NLP,transformer,BERT,RNN,LSTM
    数学建模入门
    淘宝卖家如何批量采集竞品sku进行分析?推荐2个商品sku获取API
    ansible模块示例及说明
    基于SpringBoot的防疫物资管理平台设计与实现
    【ElasticSearch笔记】
    微信小程序ibeacon搜索功能制作
    Matlab模式分类代码和手册
    Feign源码解析4:调用过程
    CSS点击切换或隐藏盒子的卷起、展开效果
  • 原文地址:https://blog.csdn.net/weixin_64066303/article/details/133961096