尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

遗传算法实战:如何用Python快速解决旅行商问题(附完整代码)

遗传算法实战:如何用Python快速解决旅行商问题(附完整代码) 遗传算法实战如何用Python快速解决旅行商问题附完整代码最近在和一些做物流路径规划的朋友聊天他们经常被一个经典问题困扰手上有几十个配送点怎么规划路线才能让总里程最短这其实就是著名的旅行商问题。理论上这个问题随着城市数量增加计算量会爆炸式增长用穷举法根本不现实。于是我们很自然地聊到了各种启发式算法其中遗传算法因为其强大的全局搜索能力和易于理解的生物进化隐喻成了很多人的首选入门工具。但我也发现不少开发者虽然对遗传算法的原理有所了解真到了自己动手用Python实现去解决一个具体问题时却常常卡在几个关键环节染色体怎么编码才高效交叉和变异操作具体怎么写代码种群规模、迭代次数这些参数到底设多少合适代码跑起来慢吞吞的怎么优化这篇文章我就想抛开复杂的数学公式以一个实战者的角度带你用Python从头构建一个解决旅行商问题的遗传算法。我们会一步步拆解并附上可以直接运行的完整代码。无论你是想快速上手应用于自己的项目还是希望深入理解算法背后的“手感”相信都能有所收获。1. 问题定义与核心思路拆解在动手写代码之前我们必须把旅行商问题和遗传算法的对接点想清楚。旅行商问题描述起来很简单一个商人要访问N个城市每个城市只去一次最后回到起点目标是找到总距离最短的那条环游路线。这里的“城市”可以替换成任何你需要顺序访问的节点比如物流配送点、电路板上的钻孔点、甚至是数据聚类中的中心点。遗传算法解决这个问题的核心隐喻是把一条可能的路线看作一个生物个体把路线的总距离或其倒数看作这个个体的适应度距离越短适应度越高。然后我们模拟自然选择的过程初始化随机生成一群个体即一堆随机路线构成初始种群。选择让适应度高的个体短路线有更大的概率“生存”下来并参与繁殖。交叉随机选取两个父代个体交换它们的一部分“基因”即城市访问顺序产生新的子代个体。变异以一个小概率随机改变某个个体中部分城市的顺序引入新的可能性避免算法陷入局部最优。迭代用新产生的子代种群替代旧的种群重复步骤2-4直到满足终止条件如达到最大迭代次数或连续多代最优解不再改善。这里最大的挑战在于编码。对于旅行商问题最自然、最常用的编码方式是顺序编码也叫路径编码。即直接用城市访问顺序的列表来表示一条染色体。例如有城市A、B、C、D那么[A, C, B, D]就表示一条从A出发依次经过C、B、D最后回到A的路线。这种编码直观但交叉和变异操作需要特别设计以保证生成的新个体仍然是有效的路径每个城市出现且仅出现一次。注意除了顺序编码还有诸如邻接编码、序号编码等方法但对于旅行商问题顺序编码因其直观性和操作实现的相对简便是初学者和实践中最常见的选择。2. 环境准备与基础工具函数我们使用纯Python的标准库和NumPy来进行计算。确保你的环境中已安装NumPy如果没有可以通过pip install numpy安装。我们将从定义城市距离和计算路径长度开始。首先我们模拟一个包含20个城市的问题。为了可复现性我们固定随机数种子并随机生成城市在二维平面上的坐标。import numpy as np import random import matplotlib.pyplot as plt # 设置随机种子确保结果可复现 np.random.seed(42) random.seed(42) # 生成城市坐标假设有20个城市坐标范围在0到100之间 num_cities 20 cities_coord np.random.rand(num_cities, 2) * 100 # 计算城市间的距离矩阵欧氏距离 def calculate_distance_matrix(coords): n len(coords) dist_matrix np.zeros((n, n)) for i in range(n): for j in range(i1, n): dist np.linalg.norm(coords[i] - coords[j]) dist_matrix[i][j] dist dist_matrix[j][i] dist return dist_matrix distance_matrix calculate_distance_matrix(cities_coord)有了距离矩阵计算任意一条路径的总长度就非常高效了。我们定义一个函数输入一个城市索引的列表即一条染色体返回该路径的总距离。def calculate_total_distance(route, dist_mat): 计算一条路径的总距离 total_dist 0.0 num_cities len(route) for i in range(num_cities): from_city route[i] to_city route[(i 1) % num_cities] # 对最后一个城市下一个是起点形成闭环 total_dist dist_mat[from_city][to_city] return total_dist为了后续的选择操作我们还需要将距离转换为适应度。一个简单直接的方法是让适应度与距离成反比。为了避免除零错误虽然距离不会为零和数值问题我们常用以下方式def calculate_fitness(route, dist_mat): total_dist calculate_total_distance(route, dist_mat) # 适应度是距离的倒数距离越短适应度越高 # 加1防止除零但更常见的做法是直接取倒数因为距离不可能为0除非同一城市 return 1.0 / total_dist基础工具准备好后我们就可以进入遗传算法的核心循环了。3. 遗传算子选择、交叉与变异的Python实现这是整个算法最具技巧性的部分。不同的实现方式会极大影响算法的收敛速度和最终解的质量。3.1 种群初始化与选择操作初始化种群就是随机生成一堆合法的路径。选择操作则决定了哪些优秀的个体有资格将“基因”传递给下一代。最常用的选择方法是轮盘赌选择它根据个体的适应度占总适应度的比例来确定其被选中的概率。def initialize_population(pop_size, num_cities): 初始化种群生成pop_size个随机排列路径 population [] for _ in range(pop_size): # 生成一个0到num_cities-1的随机排列 individual list(range(num_cities)) random.shuffle(individual) population.append(individual) return population def selection(population, fitness_list): 轮盘赌选择根据适应度比例选择两个父代个体 total_fitness sum(fitness_list) # 计算累积概率 probs [f / total_fitness for f in fitness_list] cum_probs np.cumsum(probs) # 选择第一个父代 rand_val random.random() selected_idx np.searchsorted(cum_probs, rand_val) parent1 population[selected_idx] # 选择第二个父代确保与第一个不同 parent2 parent1 while np.array_equal(parent1, parent2): # 简单防止选到同一个实际中可放宽 rand_val random.random() selected_idx np.searchsorted(cum_probs, rand_val) parent2 population[selected_idx] return parent1, parent2提示轮盘赌选择虽然简单但在后期种群适应度趋同时选择压力会变小。实践中可以结合精英保留策略即直接让当代适应度最高的几个个体无条件进入下一代以保证最优解不会丢失。3.2 交叉操作有序交叉OX对于顺序编码交叉操作不能简单地截取一段交换那样极大概率会产生非法路径重复或缺失城市。有序交叉是旅行商问题中最经典、效果较好的交叉方法之一。它的基本思想是在父代1中随机选取一个片段将这个片段直接复制给子代然后从父代2中按顺序填充子代剩余位置跳过父代1片段中已有的城市。def ordered_crossover(parent1, parent2): 有序交叉Order Crossover, OX size len(parent1) # 随机选择两个交叉点 cxpoint1 random.randint(0, size - 1) cxpoint2 random.randint(0, size - 1) if cxpoint1 cxpoint2: cxpoint1, cxpoint2 cxpoint2, cxpoint1 # 初始化子代先用-1填充 child [-1] * size # 将父代1的片段复制到子代 child[cxpoint1:cxpoint21] parent1[cxpoint1:cxpoint21] # 从父代2填充剩余位置 parent2_ptr 0 child_ptr 0 while child_ptr size: # 如果当前位置已填充跳过 if cxpoint1 child_ptr cxpoint2: child_ptr 1 continue # 从父代2取一个城市 city parent2[parent2_ptr] parent2_ptr 1 # 如果这个城市不在子代已有的片段中则填入 if city not in child: child[child_ptr] city child_ptr 1 # 移动child_ptr跳过已填充的片段区域 if child_ptr cxpoint1: child_ptr cxpoint2 1 return child为了更直观地理解OX交叉我们来看一个简单的例子步骤父代1父代2子代构建过程初始[A, B, C, D, E, F, G][E, F, G, A, B, C, D][-,-,-,-,-,-,-]选择片段假设选择索引2-4:[C, D, E]-[-,-,C,D,E,-,-]从父代2填充-从E开始遍历E已在子代跳过-下一个FF不在子代填入位置0:[F,-,C,D,E,-,-]-下一个GG不在子代填入位置1:[F,G,C,D,E,-,-]-下一个AA不在子代填入位置5:[F,G,C,D,E,A,-]-下一个BB不在子代填入位置6:[F,G,C,D,E,A,B]最终子代--[F, G, C, D, E, A, B]3.3 变异操作交换变异与倒位变异变异以一个小概率发生是维持种群多样性、跳出局部最优的关键。对于路径编码常用的变异操作有交换变异随机选择路径中的两个城市交换它们的位置。倒位变异随机选择路径中的一个片段将该片段内的城市顺序完全反转。def swap_mutation(individual, mutation_rate): 交换变异以mutation_rate概率随机交换两个城市 if random.random() mutation_rate: size len(individual) idx1, idx2 random.sample(range(size), 2) individual[idx1], individual[idx2] individual[idx2], individual[idx1] return individual def inversion_mutation(individual, mutation_rate): 倒位变异以mutation_rate概率随机反转一个片段 if random.random() mutation_rate: size len(individual) idx1 random.randint(0, size - 2) idx2 random.randint(idx1 1, size - 1) # 反转片段 individual[idx1:idx21] reversed(individual[idx1:idx21]) return individual在实际应用中可以随机选择一种变异方式或者结合使用。倒位变异有时能产生更有意义的改变因为它保持了邻接关系的大规模调整。4. 算法主循环、参数调优与可视化现在我们将所有部分组装起来形成完整的遗传算法主循环。同时我们需要讨论几个关键参数的选择并加入可视化来观察算法的演进过程。4.1 主循环构建与精英策略主循环负责迭代地执行选择、交叉、变异并更新种群。我们引入精英保留策略确保每一代的最优个体不被破坏。def genetic_algorithm_tsp(cities_coord, pop_size100, generations500, crossover_rate0.8, mutation_rate0.1, elite_size2): 遗传算法求解TSP主函数 参数 cities_coord: 城市坐标数组 pop_size: 种群大小 generations: 迭代代数 crossover_rate: 交叉概率 mutation_rate: 变异概率 elite_size: 每代保留的精英个体数量 num_cities len(cities_coord) dist_mat calculate_distance_matrix(cities_coord) # 1. 初始化种群 population initialize_population(pop_size, num_cities) best_distance_history [] # 记录每代最优距离 avg_distance_history [] # 记录每代平均距离 for gen in range(generations): # 2. 计算当前种群中每个个体的适应度 fitness_list [calculate_fitness(ind, dist_mat) for ind in population] distances [calculate_total_distance(ind, dist_mat) for ind in population] # 记录本代数据 best_idx np.argmin(distances) best_distance distances[best_idx] best_individual population[best_idx] avg_distance np.mean(distances) best_distance_history.append(best_distance) avg_distance_history.append(avg_distance) # 3. 精英选择直接保留适应度最高的elite_size个个体 elite_indices np.argsort(distances)[:elite_size] new_population [population[i] for i in elite_indices] # 4. 生成下一代剩余个体 while len(new_population) pop_size: # 选择父代 parent1, parent2 selection(population, fitness_list) # 交叉 if random.random() crossover_rate: child ordered_crossover(parent1, parent2) else: # 不交叉则随机选择一个父代作为子代克隆 child parent1.copy() if random.random() 0.5 else parent2.copy() # 变异 child swap_mutation(child, mutation_rate) # child inversion_mutation(child, mutation_rate) # 可选择使用倒位变异 new_population.append(child) population new_population # 每50代打印一次进度 if gen % 50 0: print(fGeneration {gen}: Best Distance {best_distance:.2f}, Avg Distance {avg_distance:.2f}) # 最终结果 final_fitness [calculate_fitness(ind, dist_mat) for ind in population] final_distances [calculate_total_distance(ind, dist_mat) for ind in population] best_idx np.argmin(final_distances) best_route population[best_idx] best_dist final_distances[best_idx] print(f\nFinal Result after {generations} generations:) print(fBest route found: {best_route}) print(fBest distance: {best_dist:.2f}) return best_route, best_dist, best_distance_history, avg_distance_history4.2 关键参数的经验性调优指南遗传算法的表现很大程度上依赖于参数设置。这里没有“银弹”但有一些经验法则和调优思路参数典型范围影响与调优建议种群大小 (pop_size)50 - 500太小则多样性不足容易早熟太大则计算开销大收敛慢。一般设为城市数量的1到5倍是个不错的起点。对于20个城市100-150是合理的。迭代代数 (generations)200 - 2000取决于问题规模和收敛情况。可以观察“历史最优距离”曲线当曲线在较长时间内如50-100代几乎持平即可停止。交叉概率 (crossover_rate)0.6 - 0.9控制算法利用现有优良基因进行探索的强度。太高0.9可能导致种群不稳定太低0.6则像随机搜索。常用0.8。变异概率 (mutation_rate)0.01 - 0.2维持种群多样性、跳出局部最优的关键。通常设置较低如0.01到0.1。对于路径问题0.05到0.1是常见范围。精英数量 (elite_size)1 - 5% of pop_size保证最优解不丢失。通常设为种群大小的1%-5%。对于pop_size100elite_size2或3。一个实用的调优流程是固定其他调整种群大小和迭代次数先确保算法有足够的“探索时间”和“探索空间”。调整交叉与变异的平衡如果算法收敛太快但结果不好可能是早熟可以尝试提高变异率或降低交叉率。引入自适应机制进阶让变异率随着迭代代数增加而减小初期广泛探索后期精细搜索或者当种群多样性下降时自动提高变异率。4.3 结果可视化与性能评估“一图胜千言”。我们将最优路径和算法收敛过程画出来能直观地评估算法效果。def plot_results(cities_coord, best_route, best_dist_history, avg_dist_history): 绘制最优路径和收敛曲线 fig, axes plt.subplots(1, 2, figsize(14, 5)) # 子图1最优路径 ax1 axes[0] # 按最优路径顺序获取城市坐标 best_route_coords cities_coord[best_route] # 闭合路径 best_route_coords np.vstack([best_route_coords, best_route_coords[0]]) ax1.plot(best_route_coords[:, 0], best_route_coords[:, 1], o-, linewidth2, markersize8) ax1.set_xlabel(X Coordinate) ax1.set_ylabel(Y Coordinate) ax1.set_title(fBest TSP Route Found\nTotal Distance: {calculate_total_distance(best_route, calculate_distance_matrix(cities_coord)):.2f}) ax1.grid(True, linestyle--, alpha0.7) # 子图2收敛曲线 ax2 axes[1] generations range(len(best_dist_history)) ax2.plot(generations, best_dist_history, b-, labelBest Distance, linewidth2) ax2.plot(generations, avg_dist_history, r--, labelAverage Distance, linewidth1.5, alpha0.7) ax2.set_xlabel(Generation) ax2.set_ylabel(Distance) ax2.set_title(Convergence Plot of Genetic Algorithm) ax2.legend() ax2.grid(True, linestyle--, alpha0.7) plt.tight_layout() plt.show() # 运行算法并绘图 if __name__ __main__: best_route, best_dist, best_hist, avg_hist genetic_algorithm_tsp( cities_coord, pop_size150, generations800, crossover_rate0.85, mutation_rate0.08, elite_size3 ) plot_results(cities_coord, best_route, best_hist, avg_hist)运行这段代码你会看到两个图。左边显示了算法找到的最短路径右边则展示了“每代最优距离”和“每代平均距离”随迭代次数的变化。理想的收敛曲线是最优距离和平均距离都随着迭代快速下降随后最优距离曲线逐渐趋于平稳而平均距离曲线围绕最优距离上下波动表明种群仍保持一定多样性。如果平均距离曲线很早就紧贴最优距离曲线可能意味着种群多样性丧失过快陷入了局部最优。5. 进阶优化与工程实践思考一个能跑起来的基础遗传算法已经完成了。但在实际项目中我们往往需要它跑得更快、找到的解更好、更稳定。这就需要一些进阶的优化技巧和工程化考量。5.1 性能瓶颈分析与优化对于Python实现的遗传算法最大的性能瓶颈通常出现在适应度计算即路径距离计算和选择操作上。在每一代我们需要对种群中每一个个体计算一次总距离这是一个O(N*M)的操作N为城市数M为种群大小。当N和M较大时计算量会非常可观。优化策略1向量化距离计算与记忆化如果城市坐标固定距离矩阵是静态的。我们可以预先计算好距离矩阵然后在calculate_total_distance函数中通过索引和np.sum进行向量化操作替代循环。def calculate_total_distance_fast(route, dist_mat): 快速计算路径距离向量化版本 # 将路径构造成“从i城市到i1城市”的索引对 from_indices route to_indices route[1:] [route[0]] # 将第一个城市移到末尾形成闭环 # 使用NumPy的高级索引一次性获取所有距离并求和 total_dist np.sum(dist_mat[from_indices, to_indices]) return total_dist在我的测试中对于20个城市、150个个体的种群这个优化可以将单次适应度评估的总时间减少约60%。优化策略2更高效的选择算子轮盘赌选择需要计算累积概率和进行搜索当种群很大时也有开销。锦标赛选择是另一种常用且易于并行化的方法随机从种群中选取k个个体k称为锦标赛规模取其中适应度最高的一个作为父代。重复此过程直到选够父代数量。这种方法无需计算所有适应度的总和且参数k可以调节选择压力k越大选择压力越大越容易选中优秀个体。def tournament_selection(population, fitness_list, tournament_size3): 锦标赛选择 selected [] for _ in range(2): # 选择两个父代 # 随机选择tournament_size个参赛者 contenders_idx random.sample(range(len(population)), tournament_size) # 找出其中适应度最高的 winner_idx max(contenders_idx, keylambda idx: fitness_list[idx]) selected.append(population[winner_idx]) return selected[0], selected[1]5.2 应对早熟收敛与局部最优遗传算法尤其是基础版本很容易“早熟”即种群过早地收敛到一个并非全局最优的局部最优解。除了调整变异率还有几个策略可以尝试增加种群多样性初始化不要完全随机初始化可以结合一些简单的启发式方法如最近邻法生成几个较好的个体放入初始种群给算法一个更好的起点。使用多种变异算子结合使用交换变异、倒位变异和插入变异随机选择一个城市插入到另一个随机位置以不同的方式扰动路径。引入灾难操作当连续多代最优解没有改善时以一定概率对种群进行“大变异”比如随机重新初始化一部分个体或者对最优个体以外的所有个体进行强变异。尝试其他交叉算子除了有序交叉OX还有如循环交叉CX、**部分映射交叉PMX**等对于不同特点的问题可能效果不同。可以实验性地在算法中随机选择不同的交叉算子。5.3 遗传算法与其他启发式算法的结合在实际的复杂优化问题中很少单独使用一种算法。遗传算法常与其他启发式算法结合形成混合算法以发挥各自优势。例如GA 局部搜索在遗传算法每一代产生新个体后对其应用一个快速的局部搜索如2-opt算法进行“微调”然后再评估适应度。这相当于让遗传算法负责宏观的“勘探”局部搜索负责微观的“开采”。GA 模拟退火思想在变异操作中引入模拟退火的“Metropolis准则”即以一定概率接受使解变差的变异这个概率随着迭代“温度”下降而减小。这有助于算法在早期跳出局部最优。多种群GA并行运行多个子种群定期在子种群间迁移一些优秀个体。这类似于生态学中的“岛屿模型”能更好地维持全局多样性。例如一个简单的2-opt局部搜索可以这样实现并嵌入到GA中def two_opt_swap(route, i, k): 执行2-opt交换反转route中i到k之间的片段 new_route route[:i] route[i:k1][::-1] route[k1:] return new_route def local_search_2opt(individual, dist_mat, max_iterations50): 对个体进行2-opt局部搜索优化 improved True iteration 0 best_route individual[:] best_dist calculate_total_distance_fast(best_route, dist_mat) n len(best_route) while improved and iteration max_iterations: improved False for i in range(1, n-2): for k in range(i1, n-1): # 尝试交换 new_route two_opt_swap(best_route, i, k) new_dist calculate_total_distance_fast(new_route, dist_mat) if new_dist best_dist: best_route new_route best_dist new_dist improved True break # 找到改进就跳出内层循环重新开始扫描 if improved: break iteration 1 return best_route然后在主循环中生成子代后可以以一定概率对其调用local_search_2opt。注意局部搜索会增加计算成本需要权衡收益。最后关于参数调优手动尝试不同组合固然可以但对于大型项目更系统的方法是使用参数自动化调优工具如网格搜索、随机搜索甚至是元启发式算法如用另一个GA来调这个GA的参数。不过那又是另一个有趣的话题了。
返回列表