ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

模拟退火算法在无人机药品配送路径规划中的Matlab实现

模拟退火算法在无人机药品配送路径规划中的Matlab实现 1. 项目概述当无人机遇上“退火”药品配送的最后一公里难题如何破解最近在做一个挺有意思的课题客户想用无人机解决偏远地区或城市内紧急药品的配送问题。需求很明确距离近优先。这听起来简单不就是找最短路径吗但实际操作起来你会发现这远非一个简单的“两点之间直线最短”问题。配送点比如社区医院、药房、居民点可能有几十个无人机从中心药库起飞需要依次访问所有点再返回这就是一个经典的旅行商问题TSP。而TSP是出了名的“组合爆炸”难题当配送点超过20个精确求解的计算量就大到不现实了。这时候就得请出我们的老朋友——模拟退火算法Simulated Annealing, SA。模拟退火这名字听起来挺物理的灵感来源于冶金学中的退火工艺将材料加热到高温后缓慢冷却使其内部原子排列达到能量最低的稳定状态。算法模仿这个过程在解空间中“跳跃”以一定的概率接受比当前解更差的“坏解”从而有机会跳出局部最优的“小水坑”最终找到全局最优的“大海”。对于无人机路径规划这种多峰优化问题SA这种强大的全局搜索能力正好对症下药。这个项目就是围绕这个核心展开的用Matlab实现一个基于模拟退火算法的无人机药品配送路径规划器。目标函数很简单——总飞行距离最短。但整个实现过程从问题建模、算法参数调优到代码的工程化实现每一步都有不少门道。接下来我就把自己从零搭建这个系统时踩过的坑、总结的技巧以及完整的、可运行的Matlab代码毫无保留地分享出来。无论你是做无人机应用的学生、研究智能算法的工程师还是对运筹优化感兴趣的朋友这篇长文都能给你带来可以直接“抄作业”的实战经验。2. 核心思路与问题建模把现实问题装进算法的“盒子”在动手写代码之前我们必须把模糊的现实需求转化为算法能够理解和处理的数学模型。这一步走对了后面就事半功倍。2.1 问题定义与约束条件解析我们的场景是一个配送中心药库拥有多架无人机但为简化我们先考虑单机任务需要向N个配送点运送药品。无人机从中心出发访问所有点各一次且仅一次最后返回中心。目标是规划出一条总飞行距离最短的访问序列。这里有几个关键约束和假设直接影响了我们的建模方式距离近优先这是核心优化目标直接体现为最小化总欧几里得距离。我们假设无人机在点与点之间直线飞行忽略障碍物和空域管制这是TSP的标准假设也是算法验证的第一步。单无人机、单次行程这是经典的对称TSP任意两点间往返距离相同。虽然现实中可能涉及多机协同、多次充电但单机TSP是所有这些复杂问题的基础。配送点坐标已知我们通过GPS或地图API获取所有配送点的经纬度或平面坐标。在Matlab中我们通常在一个二维平面内用(x, y)坐标来模拟。无容量与时间窗约束纯距离优化不考虑无人机载重、电池续航假设足够完成全程以及每个点的服务时间要求。这是基础版本后续可以在此基础上叠加。注意这个模型是高度简化的。真实的无人机配送还需考虑禁飞区、三维地形、风速风向、电池消耗模型等。但“简化”是算法研究的起点先解决核心的路径优化问题再逐步增加现实约束是更稳妥的工程路径。2.2 模拟退火算法适配TSP的改造标准的模拟退火算法框架是通用的但应用到TSP上需要定制几个关键组件解的表达Solution Representation一条路径就是一个解。在Matlab中最自然的表达就是一个长度为N的排列Permutation向量。例如对于5个点0代表配送中心1-4代表配送点一个合法的解可能是[0, 3, 1, 4, 2, 0]表示从中心0出发依次访问点3、1、4、2最后回到0。邻域动作Neighborhood Move这是SA的核心决定了如何从当前解产生一个新解。对于TSP常用的、高效的邻域操作有交换Swap随机选择路径中两个非中心的位置交换它们。逆转Reverse/2-opt随机选择路径中一段子路径将其顺序完全颠倒。这是改善TSP解非常强大的操作。插入Insert随机选择一个点将其插入到另一个随机位置。 在我的实现中主要采用了“逆转”操作因为它能更有效地打破路径中的交叉局部优化效果显著偶尔混合使用“交换”操作来增加扰动多样性。目标函数Objective Function就是总飞行距离。计算给定路径序列下依次遍历所有点并返回起点的欧几里得距离总和。冷却进度表Cooling Schedule这是SA的参数精髓控制着“温度”T如何随时间迭代次数下降。它直接决定了算法的收敛速度和最终解的质量。一个典型的指数冷却方式为T_{k1} α * T_k其中α是冷却系数通常取0.8到0.99之间。2.3 为什么选择模拟退火而非其他算法面对路径规划可选的算法很多比如遗传算法GA、蚁群算法ACO、粒子群算法PSO。我选择SA基于以下几点考量实现相对简单SA的核心逻辑清晰代码框架固定主要工作量在于邻域操作和目标函数的实现容易上手和调试。参数较少调优直观主要参数就是初始温度、终止温度、冷却系数、马尔可夫链长度。这些参数物理意义明确温度代表接受坏解的概率调整起来有迹可循。全局搜索能力强通过“以一定概率接受恶化解”的机制SA能有效避免陷入局部最优对于中等规模的TSP问题50-100个点通常能找到质量很高的近似最优解。与问题规模缩放性较好虽然计算时间随问题规模增长但通过调整冷却进度表可以在求解质量和时间成本之间取得很好的平衡适合作为原型验证和基准算法。当然SA也有缺点比如收敛速度可能不如一些现代启发式算法快对参数设置比较敏感。但对于我们这个以“距离优先”为核心、点数量在几十个级别的药品配送场景SA是一个在效果和复杂度上非常平衡的选择。3. Matlab实现详解从理论到一行行代码理论清晰了我们进入最关键的实操环节。我将分模块拆解Matlab代码并解释每一部分的设计意图和实现细节。3.1 数据准备与初始化首先我们需要生成或加载配送点的坐标数据。为了可复现我们通常在代码中随机生成一组点。%% 1. 参数设置与数据初始化 clear; clc; close all; % 问题参数 numPoints 20; % 配送点数量不包括中心仓库 areaSize 100; % 模拟区域大小 (km) % 随机生成配送点坐标 (仓库固定在区域中心) rng(1); % 固定随机种子确保结果可复现 points rand(numPoints, 2) * areaSize; % 生成[0,100]范围内的点 warehouse [areaSize/2, areaSize/2]; % 仓库坐标 % 将所有点合并第一个点为仓库 allPoints [warehouse; points]; totalNodes size(allPoints, 1); % 总节点数 1个仓库 numPoints个配送点实操心得使用rng固定随机数种子在算法开发中至关重要。它能确保每次运行程序时生成的随机点集相同这样你调整算法参数后看到的性能变化才是真实的算法效果而不是随机数据波动带来的假象。3.2 核心函数距离计算与路径总长目标函数是算法的“指挥棒”必须高效正确。function totalDist calculateTotalDistance(route, points) % 计算给定路径的总欧几里得距离 % route: 路径序列如 [1, 5, 3, 2, 4, 1] % points: 所有节点的坐标矩阵 totalDist 0; numCities length(route); for i 1:numCities-1 node1 route(i); node2 route(i1); totalDist totalDist norm(points(node1, :) - points(node2, :)); end % 加上从最后一个点返回起点的距离对于闭环TSP % totalDist totalDist norm(points(route(end), :) - points(route(1), :)); end这里我注释掉了最后一句因为在我的路径表示中起点和终点都是仓库节点1所以循环i1:numCities-1已经包含了最后一段返回仓库的距离。确保你的路径表示和距离计算逻辑一致是调试中最容易出错的地方之一。3.3 模拟退火主循环实现这是算法的心脏。我将关键参数和步骤都写在注释里。%% 2. 模拟退火算法参数设置 initialTemp 1000; % 初始温度 minTemp 1e-8; % 终止温度 coolingRate 0.995; % 冷却系数 iterPerTemp 100; % 每个温度下的迭代次数马尔可夫链长度 % 初始化当前解从仓库开始随机排列配送点最后回到仓库 currentRoute [1, randperm(numPoints) 1, 1]; % 节点1是仓库 currentDist calculateTotalDistance(currentRoute, allPoints); % 记录最优解 bestRoute currentRoute; bestDist currentDist; % 用于绘制收敛曲线 distHistory []; tempHistory []; %% 3. 模拟退火主循环 temp initialTemp; iteration 0; while temp minTemp for i 1:iterPerTemp iteration iteration 1; % 3.1 产生新解通过邻域操作 newRoute generateNeighbor(currentRoute, numPoints); newDist calculateTotalDistance(newRoute, allPoints); % 3.2 计算能量差 (距离差) deltaE newDist - currentDist; % 3.3 Metropolis准则决定是否接受新解 if deltaE 0 % 新解更好直接接受 currentRoute newRoute; currentDist newDist; % 更新全局最优解 if currentDist bestDist bestRoute currentRoute; bestDist currentDist; end else % 新解更差以一定概率接受 acceptanceProbability exp(-deltaE / temp); if rand() acceptanceProbability currentRoute newRoute; currentDist newDist; end end % 记录数据用于分析 distHistory [distHistory; currentDist]; tempHistory [tempHistory; temp]; end % 3.4 降温 temp temp * coolingRate; % 可选每降温一定次数输出当前状态 if mod(iteration, iterPerTemp*10) 0 fprintf(迭代次数: %d, 温度: %.4f, 当前距离: %.2f, 最优距离: %.2f\n, ... iteration, temp, currentDist, bestDist); end end关键点解析初始温度initialTemp设置太高算法初期会接受大量劣质解搜索随机但收敛慢设置太低则退火过程太短容易陷入局部最优。一个经验法则是让初始接受概率exp(-deltaE_avg / T0)在0.8左右其中deltaE_avg是随机扰动产生解的平均能量差。可以先用随机扰动采样估算。冷却系数coolingRate越接近1降温越慢搜索越充分但耗时越长。0.99是非常慢的冷却适合求高质量解0.9则快很多。我取0.995是在质量和时间间折衷。马尔可夫链长度iterPerTemp在每个温度下进行足够多的尝试以使系统达到平衡状态。通常与问题规模相关可以设为100 * N或一个固定值。这里设为100次迭代/温度。邻域生成函数generateNeighbor这是算法多样性的来源。3.4 邻域生成策略逆转与交换function newRoute generateNeighbor(route, numPoints) % 生成当前路径的一个邻居 % 主要使用2-opt逆转操作偶尔使用交换操作增加扰动 newRoute route; % 避开路径首尾的仓库节点索引1和end idx 2:length(route)-1; % 可操作的位置索引 if rand() 0.7 % 70%的概率使用逆转操作2-opt % 随机选择一段子路径进行逆转 pos sort(randsample(idx, 2)); % 随机选择两个不同的位置并排序 newRoute(pos(1):pos(2)) fliplr(newRoute(pos(1):pos(2))); else % 30%的概率使用交换操作 % 随机选择两个位置交换 swapPos randsample(idx, 2); newRoute([swapPos(1), swapPos(2)]) newRoute([swapPos(2), swapPos(1)]); end end注意事项这里有一个非常重要的细节——路径的固定点。在我们的表示中路径的第一个和最后一个元素都是仓库节点1。在生成新解时绝对不能移动或交换这两个位置否则路径将不再是从仓库出发并返回仓库。这就是为什么idx被定义为2:length(route)-1确保操作只在中间的配送点序列中进行。这是实现中非常容易忽略的bug来源。3.5 结果可视化与输出算法跑完了我们需要直观地看到结果。%% 4. 结果可视化 figure(Position, [100, 100, 1400, 500]); % 子图1最终优化路径 subplot(1, 3, 1); plot(allPoints(:,1), allPoints(:,2), ko, MarkerSize, 8, MarkerFaceColor, y); hold on; plot(warehouse(1), warehouse(2), rp, MarkerSize, 15, MarkerFaceColor, r); % 突出显示仓库 % 绘制最优路径 for i 1:length(bestRoute)-1 node1 bestRoute(i); node2 bestRoute(i1); line([allPoints(node1,1), allPoints(node2,1)], ... [allPoints(node1,2), allPoints(node2,2)], Color, b, LineWidth, 1.5); end title(sprintf(优化后无人机配送路径 (总距离: %.2f km), bestDist)); xlabel(X坐标 (km)); ylabel(Y坐标 (km)); grid on; axis equal; % 子图2距离收敛过程 subplot(1, 3, 2); plot(distHistory, b-, LineWidth, 1); xlabel(迭代次数); ylabel(当前路径距离 (km)); title(模拟退火收敛过程); grid on; % 子图3温度下降过程 subplot(1, 3, 3); semilogy(tempHistory, r-, LineWidth, 1); % 对数坐标显示温度 xlabel(迭代次数); ylabel(温度 (对数尺度)); title(退火温度下降曲线); grid on; %% 5. 输出最终结果 fprintf(\n 模拟退火算法求解完成 \n); fprintf(配送点数量: %d\n, numPoints); fprintf(最优路径总距离: %.4f km\n, bestDist); fprintf(最优路径序列 (1为仓库): \n); disp(bestRoute); fprintf(\n);可视化不仅是为了好看更是重要的调试工具。通过收敛曲线你可以判断算法是否在有效搜索距离应总体下降并伴有波动以及温度下降曲线是否符合设定。4. 参数调优与性能分析让算法从“能用”到“好用”写完能跑的代码只是第一步让算法高效、稳定地找到高质量解才是真正的挑战。这部分分享我的调优经验和性能评估方法。4.1 关键参数的影响与调优指南模拟退火算法的性能对参数非常敏感。下面这个表格总结了我的调参经验参数典型范围影响调优建议与技巧初始温度 (T0)10 - 10000过高初期接受太多劣解搜索盲目收敛慢。过低退火过程太短易陷入局部最优。1.经验法T0 -ΔE_avg / ln(P0)其中P0是初始接受概率如0.8ΔE_avg可通过随机扰动当前解1000次计算平均距离变化得到。2.试探法从100或1000开始观察初期接受率。若几乎全接受则T0太高若几乎全拒绝则T0太低。终止温度 (Tmin)1e-3 - 1e-8过高算法提前停止解未充分优化。过低增加无意义的计算因为温度极低时已几乎不接受劣解。通常设一个极小的值如1e-6。更实用的停止条件是连续若干温度下最优解未改进或温度已低于某个阈值。冷却系数 (α)0.8 - 0.999接近1降温慢搜索充分耗时极长。接近0.8降温快可能未达平衡就冷却解质量差。对于100个点以内的问题0.95-0.99是常用范围。追求质量用0.995需要快速结果用0.985。可以尝试自适应冷却如果当前温度下接受率很高可以减慢冷却增大α反之则加快。马尔可夫链长度 (L)50 - 5000过长每个温度下计算太久。过短系统未达平衡就降温影响最终解质量。通常与问题规模N成正比如 L 100 * N。一个简单有效的策略固定总迭代次数然后根据冷却系数反推L。例如希望总迭代约10万次若从T0到Tmin约需log(Tmin/T0)/log(α)个温度阶段则可算出每个温度下的L。邻域操作策略交换/逆转/插入决定新解的“步长”和搜索方向。逆转2-opt对TSP局部优化极好应作为主力70%概率。交换操作扰动性更强有助于跳出局部最优作为辅助30%概率。可以动态调整高温时多用交换扩大搜索低温时多用逆转精细优化。我的调参流程实录固定其他调T0和α先用一组默认参数T01000 α0.99 L100跑一次观察收敛曲线。如果曲线初期下降太慢说明T0可能偏高或α太大尝试降低T0到500或减小α到0.985。观察接受率在算法运行时可以输出每个温度下的接受率。理想情况是初期接受率在40%-80%末期接近0。如果初期接受率就低于10%T0需要提高如果末期接受率仍高于10%可能需要更低的Tmin或更小的α。用标准算例验证从TSPLIB公开的TSP问题库下载一个小规模标准算例如eil5151个城市用你的算法求解与已知最优解对比。这是检验算法正确性和参数有效性的黄金标准。4.2 算法性能评估与对比为了客观评价我们的SA实现我设计了一个简单的对比实验。实验设置问题规模分别测试20、50、100个随机配送点。对比算法除了SA我还实现了最近邻贪心算法Nearest Neighbor作为基准。贪心算法从仓库出发每次都飞往最近的未访问点简单快速但结果通常远非最优。评价指标最终路径总距离、算法运行时间。环境Matlab R2022b CPU i7-12700H。核心对比代码片段% 贪心算法作为基准对比 function greedyRoute greedyTSP(points, startIdx) numNodes size(points, 1); visited false(1, numNodes); visited(startIdx) true; greedyRoute startIdx; currentIdx startIdx; for i 1:numNodes-1 % 找出当前点未访问的最近邻点 dists sqrt(sum((points - points(currentIdx, :)).^2, 2)); dists(visited) inf; % 已访问的点距离设为无穷大 [~, nextIdx] min(dists); visited(nextIdx) true; greedyRoute [greedyRoute, nextIdx]; currentIdx nextIdx; end greedyRoute [greedyRoute, startIdx]; % 回到起点 end实验结果与分析节点数算法平均距离 (km)平均运行时间 (秒)相对于贪心的改进20贪心算法412.30.01基准20模拟退火378.12.1降低8.3%50贪心算法648.70.01基准50模拟退火580.28.5降低10.6%100贪心算法942.50.02基准100模拟退火821.935.7降低12.8%结论有效性模拟退火算法在不同规模下均显著优于简单的贪心算法距离缩短了8%-13%。这证明了SA在求解TSP问题上的价值。时间成本SA的运行时间随问题规模增长较快近似O(N²)或更高而贪心算法极快。对于实时性要求极高的场景如无人机动态避障SA可能不适合在线计算更适合离线规划。规模适应性对于100个点SA在30多秒内找到了一个明显更优的解这个时间对于药品配送的离线路径规划是完全可接受的。通常配送点不会在短时间内剧烈变化可以提前规划好路线。4.3 进阶优化技巧如果你需要处理更大规模如200点或要求更快的速度可以考虑以下优化距离矩阵预计算在循环中反复调用norm函数计算两点距离是耗时的。可以预先计算一个N x N的距离矩阵distMatrix其中distMatrix(i,j)存储点i到点j的距离。这样计算路径总长就从O(N)次开方运算变为O(N)次查表加法速度提升一个数量级。% 预计算距离矩阵 distMatrix zeros(totalNodes); for i 1:totalNodes for j i1:totalNodes d norm(allPoints(i,:) - allPoints(j,:)); distMatrix(i,j) d; distMatrix(j,i) d; % 对称矩阵 end end % 修改目标函数使用距离矩阵查表增量更新目标函数在SA中每次邻域操作如逆转一段路径只改变了路径的局部。与其重新计算整条路径的距离不如只计算被改变部分带来的距离变化。这需要更精细的代码设计但能极大加速内层循环。并行化尝试每个温度下的iterPerTemp次迭代是相互独立的理论上可以并行计算。Matlab的parfor循环可以用于此但需要注意随机数生成和变量更新的同步问题。混合策略用贪心算法或最近插入法生成一个较好的初始解而不是完全随机初始解可以大大缩短SA的“预热”时间。5. 常见问题排查与实战心得在实际编码和调试过程中我遇到了不少典型问题。这里整理出来希望能帮你绕过这些坑。5.1 算法不收敛或收敛极慢现象距离曲线一直上下剧烈波动没有明显下降趋势或者下降非常缓慢。可能原因与排查初始温度过高温度T太高时接受劣解的概率exp(-ΔE/T)接近1算法几乎完全随机游走无法聚焦。解决降低initialTemp或采用前述公式计算一个合理的初始温度。冷却系数太小降温太快如果α0.9温度下降太快系统来不及在每个温度下达到平衡就冷却了相当于快速淬火结果容易陷入局部最优。解决增大coolingRate到0.99或更高。邻域操作过于“温和”如果只使用“交换相邻点”这种微小扰动搜索空间探索能力太弱。解决引入更强的扰动如我使用的“路径逆转”2-opt它能对路径结构做较大改变。马尔可夫链长度不足在每个温度下仅尝试几次就降温搜索不充分。解决增加iterPerTemp可以设为问题规模的倍数如200 * sqrt(N)。5.2 结果不稳定每次运行差异大现象相同参数和输入数据多次运行得到的最优距离相差较多。可能原因与排查随机种子未固定算法中涉及随机数的地方初始解、邻域操作、Metropolis判断如果没有固定种子每次运行必然不同。调试时务必使用rng(固定值)确保可复现。生产运行时可以多次运行取最好结果。终止温度过高算法在温度还比较高时就停止了此时系统仍有一定概率接受劣解解的状态不稳定。解决降低minTemp如到1e-8或增加一个停止条件连续若干个温度下最优解未更新。算法本身特性模拟退火是随机算法本身就有一定波动性。对于中小规模问题波动不应太大。如果波动巨大往往是参数设置不合理如T0太高、L太小。可以通过多次运行统计平均性能和方差来评估算法稳定性。5.3 路径出现重复访问或遗漏现象最终路径序列中某个配送点出现了两次或某个点根本没被访问。可能原因与排查邻域操作函数有bug这是最可能的原因。检查你的generateNeighbor函数确保它产生的是一个合法的排列即1到N的每个点恰好出现一次。我强烈建议在函数末尾添加一个断言检查function newRoute generateNeighbor(route, numPoints) % ... 你的邻域操作代码 ... % 检查新路径是否仍是合法排列仅包含1到N1的所有数字且首尾为1 assert(length(unique(newRoute(2:end-1))) numPoints, 邻域操作产生非法路径配送点重复或遗漏); assert(all(newRoute(2:end-1) 2 newRoute(2:end-1) numPoints1), 路径包含非法节点索引); end初始解生成错误确保初始解randperm正确生成不重复的序列并且正确拼接了首尾的仓库节点。5.4 Matlab代码性能瓶颈现象当配送点超过150个时程序运行非常慢。可能原因与排查目标函数计算是热点在SA主循环中calculateTotalDistance会被调用数十万次。使用之前提到的距离矩阵预计算是最大的性能提升点。循环和动态数组增长像distHistory [distHistory; currentDist];这样的语句在循环中动态扩展数组非常耗时。可以预先分配好数组maxIter ceil(log(minTemp/initialTemp)/log(coolingRate)) * iterPerTemp; distHistory zeros(maxIter, 1); % 在循环中赋值 distHistory(iteration) currentDist;可视化绘图开销实时绘图会严重拖慢程序。调试时可以先注释掉绘图代码或者每1000次迭代更新一次图形。5.5 从仿真到现实的思考我们这个模型是理想的二维平面直线飞行。真实的无人机药品配送还需要考虑更多三维地形与障碍物山区或城市楼群中直线距离不等于可飞距离。需要引入地图数据将问题转化为三维空间下的路径规划或者使用栅格法、势场法结合SA。续航与载重约束无人机电池有限。这不再是单纯的TSP而是带容量约束的车辆路径问题CVRP或更复杂的弧路径问题ARP。需要在目标函数中引入惩罚项或者采用先聚类将点分成无人机单次可服务的群组再分别规划的策略。动态与不确定性交通状况、天气、临时订单。这就需要在线重规划的能力SA可能因为计算时间较长而不适用需要考虑更快的反应式算法或结合机器学习的预测规划。我的建议是先用这个经典的2D TSP SA模型打好基础彻底理解算法原理和实现细节。然后选择一两个最迫切的现实约束比如续航尝试修改目标函数或增加判断逻辑一步步让你的模型变得更贴近实际。算法开发就像搭积木从简单稳固的核心开始逐步添加复杂度远比一开始就追求大而全的复杂模型更容易成功。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进