简介这是一份面向物流运筹优化与深度强化学习研究者的学术论文PDF聚焦旅行商问题与无人机TSP-D的协同路径规划提出融合注意力编码器与LSTM解码器的混合模型解决传统注意力模型难以协调卡车与无人机多智能体动作序列的痛点。论文从问题背景、混合模型结构到mmCVRP扩展实验完整展开重点阐释了LSTM解码器如何利用隐藏状态记忆多车辆动作序列以支持车辆在节点汇合时的等待与协同决策同时给出随机数据集与真实场景下的性能对比以及与传统运筹学基线的结果分析。资源共1个PDF文件大小约2.7MB内容结构紧凑适合车辆路径规划、最后一公里配送、组合优化等方向的工程师和科研人员作为方法参考与复现起点。该资料已有103人学习对于希望快速掌握DRL在异构车队调度中建模思路与实验设计的读者是一份高信息密度的入门到进阶材料。1. 无人机辅助旅行商问题不是把TSP换个坐标而是多了一组“可飞节点”无人机辅助旅行商问题UAV-assisted TSP这几年从论文走向落地主要卡在一个信号上到底该怎么把“无人机从起降平台起飞、服务完一个点再回来”这个动作塞进标准旅行商问题的迭代式求解里。用深度强化学习求解时如果只把坐标拼成一个长向量丢给网络模型大概率学到的是“把所有能飞的点都交给无人机”结果总成本反而比纯卡车方案更差。原因是经典TSP的reward只算路径长度而无人机辅助场景里多了起降约束、飞行里程约束和“必须由卡车访问的节点”约束三者耦合在一起时奖励信号和动作空间的建模顺序几乎决定了训练是收敛还是原地打转。这篇文章针对的是有真实任务的从业者要做无人机路径规划、巡检航线或应急配送手里已经有一套TSP求解代码想用深度强化学习算法再往前提一档。我会先把一个能跑通又不过度简化的协同模型定义清楚再给出TransformerPOMO框架下的状态、mask和训练循环最后重点讲几个会让复现翻车的参数和坑。整个方案不依赖闭源工具训练完成后可以导出策略网络直接接到现有路径规划服务里。2. 先把协同模型写清楚无人机辅助TSP的决策变量与奖励为什么这样定无人机辅助TSP的变体非常多有的让无人机跟随卡车飞行并在多个节点回收有的把无人机当作“可以跳过某些客户点的飞行快递员”。做可复现工程时最忌讳一上来就追求广义模型因为变长回收点会给Transformer解码器带来极其复杂的mask组合。这里我采用的是一种在物流调度里最常见的落地方案卡车按一条TSP回路访问一部分节点剩余标记为可空投的节点由无人机从某个卡车停靠点起飞、服务后返回同一停靠点无人机每次只服务一个客户点。这个模型虽然简化了“卡车继续行进同时无人机回收”的真实同步关系但保留了最核心的组合决策——哪些点交给无人机、卡车访问顺序如何安排这两个问题不解决任何更精细的时间同步都是空中楼阁。2.1 带无人机辅助之后TSP从“排一条序”变成“排两条路”先从决策变量的角度看看它和经典TSP的差距。经典TSP的解是一条包含所有客户的排列无人机辅助TSP的解则分成两部分一条卡车回路以及一个“哪些节点由无人机服务”的分配。卡车速度通常比无人机直线速度快但无人机不受道路约束所以两者在不同尺度上互相补充。举例来说有10个客户点其中4个点在河边、只能由无人机飞过去那么卡车回路至少要包含剩下的6个点而是否再多走几个点取决于无人机飞行成本与卡车绕路成本的比值。这是典型的“集合划分排序”混合问题直接用OR-Tools分支定界可以算但当客户点数量超过80并且每轮要实时求解时延迟会高到没法用。我用一个很小但足够表达协同的公式来说明。假设从站点0出发卡车依次访问点集合T[v_1, v_2, ..., v_m]后回到0用dist(a,b)表示两点欧氏距离。卡车的行驶成本是C_truck dist(0, v_1) Σ_{i1}^{m-1} dist(v_i, v_{i1}) dist(v_m, 0)被无人机服务的点集合D其中每个点u需要从某个卡车停靠点s起飞并返回s飞行成本是2×dist(s,u)于是总成本C_total C_truck beta × Σ_{u∈D} 2×dist(s_u, u)beta是无人机成本权重默认取1.0表示无人机飞行距离与卡车行驶距离等权。训练时可以把它理解为任务偏好beta小于1表示更倾向用无人机beta大于1表示更倾向保守的卡车路线。需要注意D中每个点都必须是允许无人机服务的点那些“不可飞”的点必须出现在卡车回路T里这是mask合法性判断的核心。2.2 让reward成为可计算的标量引入“结束动作”与强制约束当把上面的公式转换成强化学习reward时网络输出的动作序列不能直接等同于“所有点的排列”因为D中的点根本不在卡车回路上。为了让模型学会“哪种点该留在后面交给无人机”我给解码器增加了一个“结束动作end token”。解码器从起点开始每一步输出一个卡车要访问的节点也可以选择结束。一旦触发结束所有还没被访问的可飞节点自动分配给无人机而未访问的不可飞节点会导致该解被判非法得一个很大的惩罚项。这个设计的好处是把组合优化问题变成与经典构造式TSP一致的“逐步解码”问题代价是mask更复杂。我不建议把“非法解”塞进reward里做软惩罚因为训练初期模型会大量探索非法动作软惩罚会导致梯度极不稳定。正确做法是用mask从动作空间中直接禁掉非法选择def get_mask(state, remaining, drone_budget, done): B, _ remaining.shape mask torch.full((B, N 1), True) # 最后一位是end token mask[torch.arange(B), remaining] False # 如果剩余的不可飞节点数大于0则禁止end token remaining_forbidden state.need_truck.sum(dim1) 0 mask[:, N] mask[:, N] | remaining_forbidden # 如果剩余可飞节点数大于剩余无人机架次预算也必须禁止结束 remaining_drone_need state.drone_need.sum(dim1) state.remain_budget mask[:, N] mask[:, N] | remaining_drone_need return mask上面这段是mask的核心逻辑逻辑说明可以展开为三点。第一remaining里保存的是还没被卡车访问的节点编号网络只能从这些节点里选择一个加入卡车路径。第二不可飞节点必须由卡车访问所以只要remaining里还存在着这类节点end token就不应该出现。第三无人机架次预算由业务决定比如现场只有两架无人机那么最多只能服务两个点这个限制不写进网络而是写进end token的合法性判断里能让模型在训练时就直接避开不可行动作。beta参数的选择我一般会在训练集生成后先做一次小规模网格搜索。备选值取{0.5, 1.0, 2.0}看纯卡车路线与纯无人机路线的成本差。如果可飞节点比例是30%beta1.0时模型通常能学到混合策略如果可飞节点超过70%beta1.0会让模型产生“全无人机”的倾向此时应调大beta给每段无人机飞行一个合理成本。这个参数在后面第4章的训练循环里还会再出现。2.3 从物流到应急场景山区洪涝灾害下的运输与通信协同优化上面这套建模在纯成本优化任务上已经可以工作但标题和实际工作中更常见的诉求是“无人机运输与通信协同优化”特别是在山区洪涝灾害这类场景里无人机不仅要送物资还要充当中继节点保障前方通信。这时候可以把通信约束转化为一个附加距离约束任何一个无人机服务节点u其起降点s与u之间的直线距离必须小于通信半径R否则该点不允许纳入D集合。在代码里只需要改实例生成阶段for i in range(n): if drone_flag[i] and dist_to_nearest_landing_platform[i] comm_radius: drone_flag[i] 0这种改法把通信问题折叠成合法的无人机点集合比在训练reward里加通信覆盖率要稳得多因为模型不需要自己发现“飞太远会让信号中断”这条规则。如果你要处理的是公网回传、基站回传等业务可以在评估脚本里再额外统计一条“是否所有无人机点均在R内”作为安全指标而不是训练信号。3. 为什么采TransformerPOMO框架而不是DQN或指针网络领域里求解组合优化问题的DRL方法大致有三条路线一是把问题建模成图用GNN或Transformer编码后通过pointer机制逐步输出解代表工作是Pointer Network和Attention Model二是用DQN之类的价值方法去学习状态价值每一步贪心选最优动作三是直接端到端输出整条路径比如用自回归模型一次性生成全部序列。针对无人机辅助TSP这种强约束问题POMOPolicy Optimization with Multiple Optima是我目前复现性最好的选择它的核心优势不是网络结构多先进而是训练范式解决了构造式方法常见的方差问题。3.1 Pointer Network到POMO为什么一定要多起点采样先看一张不太严格但有用的对比表是我在实际选型时给自己列的方法动作输出约束处理训练方差复现难度Pointer Network REINFORCE逐步输出排列靠mask辅助较高baseline设计敏感容易发散DQN / Double DQN每一步选择下一个节点mask可用价值函数估计偏差大调参成本高Attention Model REINFORCE逐步输出排列mask完善中用baseline可控POMO逐步输出排列多起点并行mask完善低共享baseline最稳POMO最核心的改动是训练时不止用一个起点生成解而是从n个不同起点并行rollout。对于无人机辅助TSP来说起点就是卡车离开站点后访问的第一个客户点。因为每一条合法卡车路线都可以旋转经典REINFORCE只采样一个起点时同一对状态动作在不同epoch里可能对应完全不同的最优路径梯度方向反复横跳。POMO把n个起点全部当做独立轨迹并对这批轨迹的reward取平均值作为baseline。这样做的好处是单步内就能得到一个天然对照哪些起点的解优于平均哪些劣于平均梯度方向明确得多。而且我在实际工程里发现多起点采样几乎不增加额外算力因为batch内部就是并行矩阵运算batch维度从B扩到B×nTransformer编码器一次前向就全算了。3.2 状态与编码把无人机辅助信息拼进注意力层的三个通道Transformer编码器接收的输入需要包含三组信息节点坐标可飞标记剩余无人机架次。大部分刚转过来的人会把可飞标记拼进坐标向量但这会让注意力矩阵学到“可飞节点与不可飞节点之间不能互相转换”的假象。我的常见做法是额外构造一个类型嵌入每个点有四种状态站点、必须卡车访问、可飞但未分配、已经被访问过。POMO里的状态嵌入由三部分相加得到node_embedding linear_proj(coords) type_embedding time_step_embeddingtype_embedding是一个可学习的二维向量不必做太复杂。time_step_embedding我用正弦位置编码作用在于让模型知道“序列已经走到第几步”否则在变长解码时模型很难理解为何某个节点一直没被选择。Mask在解码器中通过注意力掩码实现。经典TSP的mask只屏蔽已访问节点这里需要额外屏蔽两种动作一是不可飞节点对应的“分配无人机”动作二是当前剩余无人机架次不足时所有新分配动作。我把“分配无人机”这个动作隐含在end token后处理里所以解码器的动作空间只有一个选择下一个卡车节点。这比动作空间同时含“卡车节点无人机节点”要简单很多实验中也更稳定。3.3 训练目标共享baseline下的策略梯度训练目标使用REINFORCE with shared baseline。对第i个训练实例从n个起点rollout得到奖励r_1, r_2, ..., r_n共享baseline是它们的平均值b_bar。策略梯度计算公式为∇θ L Σ_{k1..n} (r_k - b_bar) × ∇θ log π(a_k | s_k)这里的r是我在第2章定义的C_total的反向即reward -C_total。用共享baseline而不是critic网络的经验是当问题带强约束时critic很容易过拟合到“非法状态”的伪低价值导致baseline失真。共享baseline没有额外参数还能天然应对无人机辅助TSP的“多起点等价性”。解码时是否用贪心、采样还是beam search也会影响训练效果。训练阶段建议用采样让模型探索更多可能路径评估阶段用贪心或多样本采样后挑选最优解。千万不能在评估阶段用随机采样直接报告结果这会让“可复现”变成“抽卡式复现”。4. 从零搭一套可复现的无人机辅助TSP训练器实例生成、mask与训练循环现在进入可以抄作业的部分。我会按照一次最小可运行训练实验的目录结构来组织这套结构我在多个TSP变体上复用只要替换cost函数和mask部分就能适配其他约束。先说明环境选择PyTorch 2.x即可不需要分布式单张24GB显存能跑到n100节点、batch_size64、多起点并行。4.1 项目目录与环境uav_tsp_drl/ ├── config.yaml ├── data/ │ ├── train_instances.pt │ └── test_instances.pt ├── models/ │ ├── encoder.py │ ├── decoder.py │ └── attention_model.py ├── solver/ │ ├── instance_gen.py │ ├── mask.py │ └── cost.py ├── train.py ├── evaluate.py └── utils/ ├── log.py └── seed.pyproject通常要注意三个细节train和test的实例必须在同一个随机分布下生成且固定种子否则评估集过拟合会被误判成泛化能力日志里要把每个epoch的beta、无人机架次预算、节点数打全否则后面回放时会忘记当时的约束强度seed工具要同时锁住Python随机数、NumPy和PyTorch各自的随机源。# utils/seed.py import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)这里不展开每行注释了三个seed源缺一不可。尤其是PyTorch的CUDA算子如果没锁即使前面全部固定卷积或Transformer相关的算子也可能引入非确定性最后在多卡或重载状态下复现不一致。4.2 训练集生成节点分布、可飞标记与无人机架次预算实例生成这一步决定了模型最终学到的分布。节点坐标建议在单位正方形内随机生成然后归一化到[0,1]区间这能避免量纲影响注意力中的距离计算。无人机可飞节点的生成需要结合业务如果是物流配送靠近公路的点优先给卡车远离公路的点设为可飞如果是巡检可以按障碍物或用例手动标记。# solver/instance_gen.py import torch def make_instances(batch_size, n_nodes, drone_prob, drone_budget, seed): g torch.Generator().manual_seed(seed) coords torch.rand(batch_size, n_nodes, 2, generatorg) # 站点固定为第一个点 coords[:, 0, :] torch.tensor([0.5, 0.5]) # drone_flag: 1表示该节点可用无人机服务0表示必须卡车访问 drone_flag (torch.rand(batch_size, n_nodes, generatorg) drone_prob).long() drone_flag[:, 0] 0 # 站点不能交给无人机 return { coords: coords.float(), drone_flag: drone_flag.float(), drone_budget: torch.tensor(drone_budget).repeat(batch_size), }参数说明drone_prob指的是单个节点可飞的概率我一般取0.3到0.5太高会让模型觉得卡车一点用都没有太低则退化成经典TSPdrone_budget是每单次任务最多由无人机服务的节点数量不小于零。这里有个容易被忽略的问题如果drone_budget大于batch内可飞节点数模型会直接把所有可飞节点都甩给无人机这种情况下要调大beta。生成后建议立即保存为train_instances.pt避免每次启动训练都重新生成导致数据漂移。4.3 构造式解生成与cost计算不带时间同步的最小闭环核心的生成循环如下。它模拟的是第2.2章设计的“逐步选择卡车节点结束动作触发无人机配送”的解码过程。我这里给出的是评估阶段使用的确定性解码版本训练阶段只需把greedy换成sample。# train.py (节选) def decode_truck_tour(model, instance, use_greedyTrue): coords instance[coords] # (B, N, 2) drone_flag instance[drone_flag] # (B, N) drone_budget instance[drone_budget] B, N, _ coords.shape visited torch.zeros(B, N, dtypetorch.bool) remaining_need_truck (drone_flag 0).clone() # 尚未被服务的不可飞节点 remaining_drone (drone_flag 1).clone() # 尚未被访问的可飞节点 remain_budget drone_budget.clone() current_node torch.zeros(B, dtypetorch.long) # 站点 tour_nodes [] for step in range(N): mask build_mask(visited, remaining_need_truck, remaining_drone, remain_budget) logits model.decode(coords, visited, current_node, step) logits logits.masked_fill(mask, -1e9) if use_greedy: action logits.argmax(dim1) else: action torch.multinomial(logits.softmax(dim1), 1).squeeze(1) if (action N).all(): # 所有实例都触发了end token break for i in range(B): a action[i].item() if a N: # 该实例结束不更新 continue tour_nodes.append((i, a.item())) visited[i, a] True if drone_flag[i, a] 0: remaining_need_truck[i, a] False else: remaining_drone[i, a] False current_node action.clamp(maxN - 1) return visited, tour_nodes这段代码有两个地方需要特别注意一是mask不能只屏蔽已访问点还要把“end token非法时刻”屏蔽掉二是action等于N表示触发结束之后该实例就不能再追加任何节点否则会破坏自动驾驶的顺序。上面的写法是简化版实际工程中我会把结束动作按实例拆开处理避免batch里某一条路径已经结束还继续被迫输出节点。cost函数的写法如下。无人机分配时我默认找卡车路径上最近的停靠点这比让模型直接输出无人机归属点更稳因为无人机归属本质上是一个匹配问题让策略网络自学这个匹配既慢又容易出现局部最优。# solver/cost.py def compute_cost(instance, visited, tour_nodes, beta1.0): coords instance[coords] # (B, N, 2) drone_flag instance[drone_flag] B, N, _ coords.shape costs torch.zeros(B) for i in range(B): tour [0] [n for (idx, n) in tour_nodes if idx i] [0] truck_cost path_length(coords[i], tour) drone_points [n for n in range(N) if visited[i, n] False and drone_flag[i, n] 1] drone_cost 0.0 for n in drone_points: best_dist min(((coords[i, tour_j] - coords[i, n]) ** 2).sum().sqrt() for tour_j in tour) drone_cost 2 * best_dist costs[i] truck_cost beta * drone_cost return costsreward -cost。为什么无人机距离要乘2因为起飞和返回都要走过这段直线这是最基础的双程代价。真实场景中无人机如果能在下一个卡车停靠点回收而不是返回原起飞点这个系数会降到接近1但模型会变得更复杂需要引入动态回收逻辑。先做双程模型能让你把注意力集中在DRL的mask与训练稳定性上不会一上来被时间同步问题淹没。4.4 POMO式训练循环共享baseline与模型参数训练循环的骨架和经典POMO一致。每个step把batch_size个实例复制成n份每份从不同起点开始解码。起点不同意味着动作序列的旋转不同但问题最优解不变。# train.py (训练主循环节选) def train_one_epoch(model, optimizer, train_instances, n_start20): model.train() total_loss 0.0 for batch in data_loader(train_instances): coords batch[coords] # (B, N, 2) drone_flag batch[drone_flag] drone_budget batch[drone_budget] # 多起点复制 B, N, _ coords.shape coords_multi coords.repeat_interleave(n_start, dim0) drone_flag_multi drone_flag.repeat_interleave(n_start, dim0) drone_budget_multi drone_budget.repeat_interleave(n_start, dim0) visited, tour_nodes decode_truck_tour( model, {coords: coords_multi, drone_flag: drone_flag_multi, drone_budget: drone_budget_multi}, use_greedyFalse ) costs compute_cost({coords: coords_multi, drone_flag: drone_flag_multi}, visited, tour_nodes, betacurrent_beta) rewards -costs # POMO共享baseline按n_start维度分组 rewards rewards.view(B, n_start) baseline rewards.mean(dim1, keepdimTrue) advantage rewards - baseline # log_prob 在decode时已经从模型内部计算并保存 loss -(advantage.detach() * log_probs.view(B, n_start)).mean() optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(data_loader)上面的log_probs需要由decode函数在采样时同时返回经典Attention Model实现里通常通过一个全局list收集。关键参数是n_start也就是多起点个数。n_start太小baseline效果不明显太大会让单step解码时间翻倍。常见做法n_start取20到50之间当n100时取20就足够稳定。4.5 训练与评估常用参数速查参数推荐值说明n_nodes20 / 50 / 100前两个用于快速验证明100用于正式训练drone_prob0.3~0.5可飞节点比例过高易产生全无人机解drone_budget等于可飞节点数×0.5逼着模型做权衡beta0.5~2.0无人机成本权重调大防止无人机滥用n_start20POMO多起点个数batch_size64~128显存不够时优先降n_startlearning_rate1e-4Transformer常用Adam优化器epochs100~200前50个epoch主要学会合法路径后50个epoch才优化成本这套写法和原版POMO的差距在于多了一个“可飞标记”和一个“无人机架次预算”。原版TSP中visited全部置True即可结束这里必须满足不可飞节点全部被访问且剩余点不超过drone_budget。我建议先把无人机辅助问题在n_nodes20上跑通再逐步增加节点数因为节点数变大后end token的触发时机更难学容易出现模型一直不结束导致序列过长的情况。5. 避坑手册无人机辅助TSP排错与复现的五个典型问题这一章是我自己在复现这类深度强化学习算法时踩过的坑每条都按现象、原因、解决三步写。标题里说的“可复现”很大的功夫其实花在这些不起眼的地方。5.1 训练loss不降但Greedy解质量远低于启发式现象训练了20个epochloss基本平稳用greedy解码跑评估集平均路径长度比“所有可飞点交给无人机”的简单启发式还差15%以上。原因最常见的是reward里惩罚缺失或惩罚太弱。非法解如果只是被加了一个常数惩罚模型会把“非法”和“差解”混为一谈。另一种原因是网络输出层的节点数与状态空间不一致导致模型一直在无效动作里打转。解决先彻底抛弃软惩罚把所有非法动作用mask屏蔽掉确保模型只能在合法动作上做softmax。然后检查cost函数确认不可飞节点未被访问时cost是0还是一个大正数应该返回一个远大于正常解的值比如1000。一次排查后如果还在原地踏步把beta先调到0.1这时模型只要学会“多派无人机”就能看到loss下降以此验证训练管线本身是通的。5.2 模型把所有能飞节点都交给无人机总成本反而变高现象无人机利用率接近100%但总成本比“卡车全部访问”高30%。这是因为beta设置过低无人机的双程直线距离被低估模型误以为飞出去更划算。原因当可飞节点比例高且beta≤1时模型很容易发现“只要结束所有剩余点都由无人机服务”于是策略退化。特别是drone_budget等于可飞节点数时完全没有约束力。这不是网络不行而是奖励函数诱导了懒惰策略。解决提高beta到1.5或2.0并把drone_budget限制为可飞节点数量的一半左右。这样即使模型想全派无人机也会因为budget不足而被迫让卡车访问部分可飞点。另一个有效办法是在cost里给无人机加一个固定起降惩罚比如每次起飞多算0.2个单位距离这会抑制“为了省五十米路让无人机飞一趟”的碎优化。5.3 复现结果完全对不上明明代码一样gap却有5%波动现象同一份代码在两台机器上跑评估结果差了5%。而且单卡训练两次第二次和第一次在100个epoch后的gap也不同。原因第一是torch.backends.cudnn.deterministic没打开部分算子在不同架构上有非确定性第二是实例生成依赖系统当前时间没有通过固定种子生成训练集和测试集。解决在train.py最前面加上下面这段并且保证每次启动前删除旧的datasets缓存torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False同时把测试实例永远从同一个.pt文件里加载不要每次重新生成。如果验证曲线和原论文还是有偏差最典型的原因是原论文的POMO用了增量rollout和2-opt后处理而你只跑了裸模型。可复现不等于完全相同而是“相同代码、相同参数、相同数据生成方式”下得到相同曲线。5.4 end token触发太早大量可飞点被强行丢给无人机现象greedy解码得到的路径里卡车只访问了两三个点就结束剩余几十个全部被无人机服务且飞行距离明显超过实际续航。原因mask里只屏蔽了“仍有不可飞节点未访问”的状态没有把无人机总飞行距离或单次飞行半径加进去。模型没有续航概念自然倾向于让无人机包办一切。解决在end token的合法性里加两个条件一是未被访问的可飞点中若存在某点到最近卡车停靠点的距离超过无人机最大飞行半径则禁止结束二是所有无人机航线总长超过单架次最大续航时也禁止结束。这部分不能靠penalty必须用硬mask。同时建议在实例生成时就把飞行半径限制体现在drone_flag里比如距任意站点或潜在停靠点超过R的点直接置为不可飞这样反而更简单。5.5 评估结果忽高忽低测试时用了和训练一样的随机采样现象每次评估时用随机采样解码并只跑一次结果波动大重复实验时报出的gap忽高忽低。原因采样解码天然带随机性而无人机辅助TSP的解空间存在多个高成本陷阱。如果模型没学到强约束随机采样时经常触发end token过早或过晚方差会很大。解决评估阶段统一用greedy解码如果想拿更优结果可以用多样本采样比如同一测试实例跑50次生成50条路径取cost最小的一条作为最终解。这种做法比greedy能提升3%~5%但必须在报告里说明“使用n50的采样解码”不能混入greedy的指标里。判断模型真正性能时只看greedy采样只是部署阶段的提升手段。6. 验证一个复现结果用三张图判断模型有没有真学到协同决策训练跑完之后不要只盯着一行gap数字。我会至少画三张图它们能在五分钟内告诉你模型是真正学到了无人机辅助协同还是在“猜合法路径”的边缘挣扎。第一张图是训练曲线横轴是epoch纵轴是平均reward。合格的曲线应该是前20个epoch快速上升然后缓慢平稳而不是剧烈震荡。如果在第50个epoch还在波动优先检查beta和drone_budget是不是让奖励信号出现了多个峰值。第二张图是“无人机利用率分布”即测试集每个实例里被无人机服务的点数占可飞点数的比例。如果这个比例集中在0%和100%两端说明模型没有学会权衡健康的状态应该是一个宽分布有些实例卡车多跑一点、有些无人机多飞一点。这张图画法很简单drone_ratios [] for visited, instance in zip(visited_list, test_set): fly_cnt (visited False) (drone_flag 1) drone_ratios.append(fly_cnt.sum().item() / drone_flag.sum().item())第三张图是“是否违反硬约束”单独统计每个实例里不可飞节点有没有被漏掉、无人机飞行半径有没有超限。模型即使loss收敛也可能在小概率上漏掉不可飞点这类情况必须零容忍。验证完模型后我习惯把确定性greedy解码器封装成一个可以被调用的类输入是坐标数组和可飞标记输出是卡车节点序列与无人机点列表。边界处理上有一个细节站点必须作为初始点加入卡车序列但如果无人机从站点起飞去服务某个点然后返回站点那就成了“站到站往返”在计算cost时不要重复计算站点到第一个客户点的距离。这一步很多人会踩坑建议在测试阶段先拿一个n5的实例手算一遍成本再和代码输出对照。关于进一步提效果我常做两件事。第一是热启动先跑100个epoch的纯TSP把drone_prob设为0再用学到的模型权重初始化无人机辅助TSP训练。这样模型先建立基本的路径规划能力再学“哪些点可以甩给无人机”收敛速度快很多。第二是使用增量rollout评估时在greedy解上尝试把某个卡车节点改成无人机服务并重新计算成本如果成本下降就保留这等价于一次局部搜索能让gap再降2%~3%。这个技巧对beta偏小的模型尤其有效相当于用后处理弥补训练时奖励函数的偏差。最后说一个个人习惯每次修改参数后我会把配置hash值打印到日志里。某个版本突然跑出很好或很差的结果时能快速定位是哪次改动造成的。这个方法救过我很多次比把希望寄托在随机种子上靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?