☰
强化学习求解车辆路径问题:CVRP建模、PPO训练与避坑实践
2026/10/21 15:40:27 网站建设 项目流程

简介:一份同时包含学术论文与可运行代码的强化学习求解车辆路径问题(VRP)资料包,面向计算机相关专业本科及研究生,适用于毕业论文、课程大作业或组合优化算法复现。包内含《Reinforcement Learning for Solving the Vehicle Routing Problem》论文 PDF,以及基于 PyTorch 的代码工程,主要有 4 个 Python 脚本、6 个模型权重 pt 文件、12 张说明用图片、2 个 pyc 缓存文件和 1 个 README 说明文档,全部二十六个文件压缩后仅 5.89MB,便于快速了解代码结构。目前已有 150 人学习下载,是入门强化学习落地应用的完整样例。通过对照论文阅读源码,能掌握端到端训练策略网络求解 VRP 的完整思路,包括随机策略建模、策略梯度优化以及容量受限场景下的批次交付扩展;训练好的权重也可直接用于快速验证,为后续改进模型或撰写论文实验部分节省时间。代码目录按 tasks、model、docs 等模块划分,结构清晰,便于按需拆解复现。

1. 强化学习求解车辆路径问题:这套代码能帮你走到哪一步

前阵子帮A同学整理毕业论文里“车辆路径问题”的强化学习代码,发现真正卡人的不是神经网络层数,而是三件事:动作掩码、奖励设计和训练稳定性。这份资源是一套用Python实现的强化学习闭环代码,目标是带容量约束的车辆路径问题(CVRP),从环境仿真、PPO训练、路线可视化到评估脚本都齐了,定位很明确:给毕业论文、大作业和想从零跑通RL的人用。适用范围是单仓库、车辆容量一致、目标为总路径最短的场景,不适合带时间窗或分钟级响应的实时调度。读完你至少能复现一条从环境到策略再到对比实验的完整链路。

2. 问题建模:把CVRP拆成有奖励信号的马尔可夫决策过程

2.1 为什么不用精确算法和启发式,非要上强化学习

精确算法在客户数只有几十的时候很漂亮,节点数一过百,组合爆炸的程度会让求解时间变得不可接受。工业界真正常用的是各种启发式算法,比如节约算法、邻域搜索之类,但它们每换一个业务约束,就要重新设计一套算子,调试成本很高。强化学习换个思路:不写规则,用一个策略网络直接学“下一个访问谁”的条件分布。推理时每一步根据当前状态查概率表,挑一个合法节点走过去,形式上就是一个构造型求解器。

但你得清醒一点,强化学习不是用来替代成熟运筹工具的。真要固定网络做日常配送调度,直接上开源运筹工具包加邻域搜索,稳、快、省心。可毕业论文和大作业不一样,需要展示“模型怎么设计、训练怎么收敛、实验怎么对比”,强化学习的完整链路比传统求解器更有故事性,也更适合做改进型课题。而且约束扩展很方便,加时间窗、多车型时,改动作掩码和奖励函数就行,不需要重写整个求解内核。

所以这份资源给你的不是一个“更快”的求解器,而是一个“能学习”的求解器。它在小规模同分布实例上通常能接近甚至超过简单启发式,在路线质量上的提升过程肉眼可见,用来做算法对比和消融实验都很顺手。

2.2 CVRP的MDP五要素:状态、动作、转移、奖励、折扣

把CVRP写成强化学习,先要定义好马尔可夫决策过程的五个要素。先说状态,我在环境里用一个向量拼接表示当前情况:所有节点的坐标、每个节点的需求量、当前车辆的剩余容量、每个节点是否已被访问,以及当前所在节点。坐标要提前归一化到0到1之间,需求量除以容量做归一化,否则数值尺度不稳定,网络很难收敛。

动作空间是0到N的整数索引。0代表仓库,其余代表客户节点。很多初学实现只让模型输出客户节点,忽略了“回仓库补货”本身也是一个决策动作,结果训练到一半发现车辆永远不回场,或者回场时机完全不对。正确的做法是把回仓库显式建模为一个动作,能不能选这个动作由动作掩码控制。

转移逻辑是这样的:从当前节点移动到动作指向的节点,路径总长度累加欧氏距离。如果动作是0,则剩余容量重置为容量上限;否则标记该客户已访问,并从剩余容量里扣掉它的需求量。单步奖励用移动距离的负值,也就是走一步给一个负奖励,距离越短奖励越大。有同学问为什么不用回合结束时的负总距离作为稀疏奖励,那样梯度方差特别大,小规模还能碰运气收敛,规模稍大就彻底不动了。逐步奖励等于给策略网络一个密集的回归目标,训练稳定很多。整个回合终止条件是所有客户都被访问过。

折扣因子gamma我习惯设0.99。因为每步都有负奖励,如果gamma设得小,模型就会更在意眼前那几步的负距离,忽略整条路线的长远代价,最后学出来的路径非常短视。gamma=0.99能让模型尽量把全线路程放在同一量级上比较。

2.3 基线启发式:先写一个最近邻,拒绝“没有对比”

很多同学训练完强化学习模型,兴奋地截个loss曲线就完事了。但评审最常问的问题是:你的模型比基线好多少?所以代码里一定要先写一个最简单、最不会出错的启发式基线。最近邻加容量回程是最合适的起点,逻辑很直白:每次从当前节点出发,找距离最近且需求量不超过剩余容量的客户;如果所有未访问客户都装不下,就回仓库补货再出发。

def nearest_neighbor(coords, demands, capacity): n = len(demands) visited = [False] * n route = [] total = 0.0 cur = 0 cap_left = capacity while not all(visited): best = -1 best_dist = float("inf") for j in range(n): if visited[j]: continue if demands[j] > cap_left: continue d = ((coords[cur][0] - coords[j][0]) ** 2 + (coords[cur][1] - coords[j][1]) ** 2) ** 0.5 if d < best_dist: best_dist = d best = j if best == -1: # 剩余容量装不下任何未访问客户,强制回仓库补货 total += ((coords[cur][0] - coords[0][0]) ** 2 + (coords[cur][1] - coords[0][1]) ** 2) ** 0.5 route.append(0) cur = 0 cap_left = capacity else: total += best_dist visited[best] = True cap_left -= demands[best] cur = best return route, total

这段代码的核心是best == -1分支。它解决了CVRP里的容量约束:不是每个客户在当前容量下都能服务,装不下时必须先回仓库。注意visited里下标0对应仓库,但仓库本身不参与客户访问;跑完后route里可能有多个0,表示车辆回场补货。如果你的坐标是经纬度,记得先投影成平面坐标再算欧氏距离,否则直接算经纬度距离,路线会偏差很大。

这个基线的价值在于给强化学习定了一个及格线。后面训练完做评估时,用同一批测试实例跑这个最近邻,再跑强化学习策略,如果平均总路程比最近邻还长,先别调超参数,回去检查状态表示和奖励是不是有问题。模型连最朴素的算法都打不过,问题多半不在网络结构上。

3. 环境与数据生成:先把仿真器写好,再谈训练

3.1 训练数据分布:随机生成CVRP实例

强化学习训练CVRP有个天然优势:每回合都可以重新生成一个随机实例,模型接触的样本更丰富,不容易把某个固定实例背下来。生成实例时,我一般用这样的参数:客户数N取20或50,坐标在0到1的方形区域内均匀随机生成,仓库放在(0.5, 0.5)附近;需求在容量的10%到30%之间均匀抽样。这个比例很关键,如果需求上限到容量的80%,一车只能装一两个客户,车辆回场频率太高,模型很容易学成“每个客户跑一趟”,路径质量很差。

import random def generate_instance(num_customers, capacity, seed=None): if seed is not None: random.seed(seed) coords = [(0.0, 0.0)] for _ in range(num_customers): coords.append((random.random(), random.random())) # 需求只占容量的一小部分,保证一辆车能服务多个客户 demands = [random.uniform(0.1, 0.3) * capacity for _ in range(num_customers)] return coords, demands

这个生成函数的核心参数有三个:num_customers控制问题规模,capacity控制单趟装载能力,seed保证实验可复现。注意需求用的是浮点数,不是整数,这在网络训练里没有语义问题;如果你做的是带整数箱数的场景,把demands改成从1到5的整数抽样也行。重点是需求范围要和容量匹配,让平均每趟能塞进三到五个客户,训练难度才合适。

我在代码里额外注意了一种边界情况:如果随机生成的需求全部很小,剩余容量永远充足,模型就不需要学回仓库的时机;如果需求全部接近容量上限,模型又只剩回仓库一条路。两种极端都会让策略退化。所以在生成脚本里可以加一句检查,统计平均每辆车服务客户数,低于3就调大容量,高于5就调小容量。

3.2 环境类:状态数组、掩码更新、step函数

环境类是强化学习代码的核心模块,但它特别容易写错,主要错在掩码更新逻辑上。我提供一个精简版的环境框架,实际使用时会在这个基础上增加状态向量的拼装和批量处理。

import math class VRPEnv: def __init__(self, coords, demands, capacity): self.coords = coords self.demands = demands self.capacity = capacity self.num_nodes = len(coords) def reset(self): self.visited = [False] * self.num_nodes self.visited[0] = True self.cur = 0 self.cap_left = self.capacity self.distance = 0.0 self._update_mask() return self._get_state() def _any_feasible(self): for i in range(1, self.num_nodes): if not self.visited[i] and self.demands[i] <= self.cap_left: return True return False def _update_mask(self): self.mask = [False] * self.num_nodes if self._any_feasible(): for i in range(1, self.num_nodes): if not self.visited[i] and self.demands[i] <= self.cap_left: self.mask[i] = True # 当前不在仓库时,允许主动回场补货 if self.cur != 0 and self.cap_left < self.capacity: self.mask[0] = True else: # 所有客户都装不下,强制回仓库 self.mask[0] = True self.mask[1:] = [False] * (self.num_nodes - 1) def step(self, action): if not self.mask[action]: raise ValueError(f"非法动作 {action} 被掩码拒绝") prev = self.cur prev_distance = self._distance(prev, action) self.distance += prev_distance self.cur = action if action == 0: self.cap_left = self.capacity else: self.visited[action] = True self.cap_left -= self.demands[action] reward = -prev_distance done = all(self.visited[1:]) self._update_mask() return self._get_state(), reward, done def _distance(self, i, j): return math.hypot(self.coords[i][0] - self.coords[j][0], self.coords[i][1] - self.coords[j][1]) def _get_state(self): # 实际实现会拼成向量:坐标、需求、剩余容量、访问标记、掩码 return None

这段代码里最重要的是_update_mask的两个分支。第一个分支是当前剩余容量还能装下至少一个未访问客户,此时可选的节点是那些需求不超过剩余容量的未访问客户,同时允许回仓库补货;第二个分支是剩余容量已经装不下任何客户,必须强制回仓库。很多翻车现场都在这里:忘了把已访问节点屏蔽,或者回仓库动作没有显式放行,模型就会在一个全False的掩码里选动作,训练直接崩掉。

step函数里有一个细节容易忽略:计算奖励时要用prev保存移动前的节点,而不是移动后的cur,否则奖励永远是0甚至反向。reward设为负的欧氏距离,意味着模型每多走一步都会收到负反馈,它自然倾向于用更少的步数完成配送,这与最小化总路程的目标一致。_get_state在当前版本里返回None,实际使用时会把坐标、需求、剩余容量、访问标记和掩码拼接成一个一维向量或节点特征矩阵,再喂给策略网络。

环境引入一个初始化参数capacity后,reset里把cap_left重置为capacity,这一点别忘了。我遇到过有人把容量初始化写成常量,导致每次回合之间容量状态残留,训练出来完全是垃圾策略。

3.3 环境自检:随机策略跑10个回合,看约束是否被违反

训练大型模型之前,花三分钟先跑一遍环境自检非常划算。常见做法是写一个随机策略,随机采样合法动作跑几个回合,然后检查三类问题:是否访问过已访问节点、每辆车装载量是否超过容量、所有客户是否都被访问过。

env = VRPEnv(coords, demands, capacity) for ep in range(10): state = env.reset() done = False while not done: legal = [i for i, m in enumerate(env.mask) if m] action = random.choice(legal) state, reward, done = env.step(action) assert all(env.visited[1:]), "存在未访问客户" # 简单校验装载量 for i in range(1, len(demands)): if env.visited[i]: pass print("环境自检通过")

随机策略虽然完全没智能,但它能暴露环境里的结构性错误。比如掩码更新错位、动作空间越界、访问标记没更新,这些问题在随机采样下就能暴露。如果随机策略都跑不通,训练策略网络只会放大环境错误。我一般会把这段自检脚本放在训练入口处,每次训练前自动执行一遍,省得反复调试同一个环境bug。

4. 训练实现:PPO策略网络与参数配置

4.1 策略网络:不是只有Transformer才叫注意力

策略网络接收的是节点特征序列和掩码,输出每个节点的动作概率和状态价值。网络结构不必过于复杂,一个embedding层加一层自注意力就足够处理20到50个节点的CVRP。代码里我用了一个简化版本,便于阅读和复现。

import torch import torch.nn as nn class PolicyNet(nn.Module): def __init__(self, input_dim, hidden_dim): super().__init__() self.embed = nn.Linear(input_dim, hidden_dim) self.attn = nn.MultiheadAttention(hidden_dim, num_heads=8, batch_first=True) self.ff = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim) ) self.policy_head = nn.Linear(hidden_dim, 1) self.value_head = nn.Linear(hidden_dim, 1) def forward(self, node_feats, mask): emb = self.embed(node_feats) attn_out, _ = self.attn(emb, emb, emb) out = self.ff(attn_out) logits = self.policy_head(out).squeeze(-1) logits = logits.masked_fill(mask, float('-inf')) values = self.value_head(out).mean(dim=1) return logits, values

这里mask的形状是[batch_size, seq_len],值为True的位置会被masked_fill置为负无穷,softmax之后概率就是0,模型永远不可能选中非法节点。value_head输出状态价值估计,用来计算优势函数。我特别提醒一点:如果你在batch里对不同实例做了padding,掩码里必须包含padding位置,否则模型会去选择那些全是0的虚拟节点,训练时会莫名出现一条走到原点又回到原点的不合理路径。

我不建议在小作业里硬套大型Transformer模型,训练开销大不说,还可能因为数据量不足导致过拟合。一个embedding加一层attention的容量对N=20到50的CVRP已经够了,先把闭环跑通再逐步加深网络。

4.2 PPO的损失函数与GAE

PPO的核心目标是让策略更新不过猛。它用新旧策略的概率比值,配合裁剪项,限制每次参数更新的幅度,从而保证训练稳定。计算优势函数时我习惯用GAE,也就是广义优势估计,它平衡了偏差和方差。

def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95): advantages = torch.zeros_like(rewards) gae = 0.0 next_value = 0.0 for t in reversed(range(len(rewards))): delta = rewards[t] + gamma * next_value * (1 - dones[t]) - values[t] gae = delta + gamma * lam * (1 - dones[t]) * gae advantages[t] = gae next_value = values[t] returns = advantages + values return advantages, returns

gamma是折扣因子,lam是GAE的平滑系数。dones用来区分真实回合终止还是截断。训练时要注意:CVRP每个回合都要跑到所有客户访问完,dones里大部分是False,只有最后一步为True,所以代码里(1 - dones[t])保证了跨回合不会错误传播价值估计。

策略损失的计算是PPO的核心:

ratio = (new_logp - old_logp).exp() surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages policy_loss = -torch.min(surr1, surr2).mean() value_loss = (returns - values).pow(2).mean()

new_logp是当前策略下动作的对数概率,old_logp是采样的策略下动作的对数概率,两者相减再取指数就是概率比值。clip_eps我设0.2,这是PPO论文里的常见取值,意思是最多允许单次更新改变20%的采样概率。policy_loss里面取min的操作,保证当优势为正时不会过度提高概率,当优势为负时不会过度压低概率。value_loss则让价值网络拟合真实的回报。

4.3 训练主循环:实例级RL与经验收集

这里给出训练主循环的框架,实际跑的时候需要根据显存和速度调整batch大小。

for iteration in range(total_iterations): states, actions, rewards, dones, old_logps, values = [], [], [], [], [], [] env = VRPEnv(coords, demands, capacity) state = env.reset() done = False while not done: action, logp, value = select_action(policy, state) next_state, reward, done = env.step(action) states.append(state) actions.append(action) rewards.append(reward) dones.append(done) old_logps.append(logp) values.append(value) state = next_state states = torch.stack(states) rewards = torch.tensor(rewards, dtype=torch.float32) dones = torch.tensor(dones, dtype=torch.float32) values = torch.stack(values) advantages, returns = compute_gae(rewards, values, dones) for _ in range(ppo_epochs): # 小批量更新策略网络 new_logps, new_values = policy(states) ...

这个循环每回合只推一个随机生成的实例,所以模型每一次经验都来自不同的问题规模分布,不容易过拟合。select_action里需要把当前状态拼成节点特征,再用策略网络输出logits,最后从合法动作中按概率采样。注意训练时一定要用采样,不要贪心选择概率最大的动作,否则策略会被锁死在局部最优里。

ppo_epochs我一般设3,意思是对同一批经验重复利用三次。次数太少,样本利用率低;次数太多,又会违反PPO对单次更新幅度的限制。total_iterations可以按问题规模设,N=20时跑一万个回合基本能看到明显收敛,N=50时需要多跑一些。如果loss出现剧烈震荡,就把lr从1e-4降到3e-5,同时检查奖励是否出现了极端值。

4.4 训练参数速查

这里整理了一份我常用的参数表,可以直接抄进代码里,也可以按自己机器的显存做调整。

参数名取值作用
gamma0.99折扣因子,控制长期回报权重
lambda0.95GAE平滑系数,平衡偏差与方差
clip_eps0.2PPO裁剪阈值,限制更新幅度
lr1e-4Adam优化器学习率
ppo_epochs3同一批经验复用次数
hidden_dim128网络隐层维度
num_heads8注意力头数
batch_size128小批量更新样本数

这批参数在设计上偏向保守,主要保证训练过程稳定。如果你的实验环境显卡显存不大,优先降低batch_size和hidden_dim,不要先动clip_eps。很多同学为了追求训练速度把clip改到0.5,结果策略波动剧烈,奖励曲线像心电图一样上下跳,最后还得调回来。

5. 避坑:车辆路径强化学习训练中的五个翻车现场

5.1 训练loss下降,总路程不降

这是最常见的翻车现象:看PPO的策略损失一路下滑,满怀期待去跑评估脚本,发现平均路线长度和随机策略差不多,甚至更差。原因通常是奖励数值没有归一化。CVRP里坐标如果直接用经纬度,距离可能动辄几十上百,每一步的负奖励数值巨大,价值网络根本拟合不过来。PPO的优势函数被极端奖励主导,策略更新方向就偏了。

解决方法是:坐标统一归一化到0到1之间;奖励可以用一个滑动平均统计量做标准化,比如实时维护reward的均值和方差,让优势函数在一个稳定的量级上。我一般做法是每收集一批经验,先对奖励做标准化,再算GAE。经过处理后,就算坐标范围变化,训练也会稳定很多。

5.2 mask实现出错,智能体原地打转

另一个常见翻车是训练过程中动作概率几乎均匀,路线图呈现来回走的锯齿。原因大多出在掩码逻辑上:要么没有屏蔽已访问节点,要么强制回仓库分支没写,导致模型在剩余容量不足时还能继续选客户,环境就给你报“非法动作”或者干脆让车辆原地打转。这种情况训练不会立刻崩溃,但策略会非常差。

解决方法是把掩码更新写成一个独立函数,加入断言。我在每次step之后强制检查:合法动作数量至少为1,所有已访问节点都被屏蔽,所有需求超过剩余容量的节点也被屏蔽。如果断言不通过,立即打印当前状态和掩码,定位是哪个分支写错了。这条断言留在训练代码里开销很低,能省掉大量排查时间。

5.3 Padding导致策略对虚拟节点输出概率

做批量训练时,很多同学会把不同长度实例padding到同一长度,方便一次性送入网络。结果就出现模型选中某个坐标为(0,0)、需求为0的虚拟节点,路线在图上画出一个奇怪的拐点。这是因为padding节点参与了attention计算,而掩码里忘记把它们屏蔽掉,网络以为这些虚拟节点是合法客户。

解决方法是给padding节点单独设置掩码为True,并且在计算loss时把对应位置的logits排除掉。还有一种更省心的做法是让训练数据始终使用相同数量的客户节点,也就是batch内全部是N=20或全部是N=50,不做跨长度padding。这样虽然牺牲一点数据随机性,但换来的是实现简单、不易出错,作业场景下非常推荐。

5.4 模型只学会“回仓库”

有同学跑完训练,发现生成路线里大量出现“访问一个客户就回仓库”的短趟,平均每车装载量低得离谱。原因多半是容量C设置太小,或者需求生成范围太宽,导致车辆一趟装不下几个客户,模型被迫频繁回场。另外,逐步奖励也会诱导模型拆成短趟,因为每次回到仓库虽然多走一段路程,但也让剩余容量重置,模型可以在后续几步拿到相对较低的负奖励。

解决方法是控制需求范围。我一般让需求上限为容量的20%到30%,保证平均每车能服务三到五个客户。如果问题规模是N=50,可以把容量调成总需求的四分之一或五分之一。训练时还要同时监控“平均每车客户数”这个指标,低于3就说明容量配置有问题。另外一个补救措施是把奖励改成稀疏的回合总距离,但那样训练会慢很多,优先还是调数据分布。

5.5 测试时遇到不同分布,模型瞬间崩溃

用N=20训练出来的模型,直接拿到N=50的实例上去测试,路线质量通常会大幅下滑,甚至比随机策略还差。这是因为策略网络虽然在同分布实例上能泛化,但客户数量一变,注意力层的输入规模变长,模型对序列边界的敏感度就会显露出来。很多同学误以为自己是“学了解算法”,实际上只是“了解了这个规模下输出节点之间的配对规律”。

解决方法是把测试分成两拨:同分布测试和跨分布测试。同分布测试用于说明模型在训练规模上的表现,跨分布测试放在论文里作为“泛化性讨论”。严格来说,跨分布泛化并不是强化学习求解CVRP的必要条件,但你必须清楚自己的模型边界。我每次训练都固定只报一个规模的数字,绝不说“模型可以求解任意规模VRP”。

6. 验证与演示:从“训练完成”到“论文截图”

6.1 固定测试集批量评估与对比

训练完成后,第一步不是截图,而是跑一个固定测试集。准备100个随机实例,每个实例都用模型推理出一条完整路线,同时跑最近邻基线的结果,最后统计平均总路程和可行率。评估脚本不复杂,但一定要加上可行性校验。

def evaluate_policy(policy, env_generator, num_episodes=100): total_length = 0.0 feasible_cnt = 0 for _ in range(num_episodes): env = env_generator() state = env.reset() done = False while not done: action = select_greedy_action(policy, state) state, reward, done = env.step(action) assert all(env.visited[1:]), "存在未访问客户" total_length += env.distance feasible_cnt += 1 return total_length / num_episodes, feasible_cnt / num_episodes

推理时要用贪心动作,也就是选择概率最大的合法节点,不是采样。采样会引入随机性,导致评估结果每次都不一样。select_greedy_action内部和训练时的采样不同,直接argmax概率分布即可。跑完输出平均总路程,与最近邻基线做差,算出一个百分比,这个数字就是论文里最有说服力的指标。

6.2 画路线图:让约束违反直接暴露在图上

文字指标再好看,也不如一张路线图直观。建议把模型生成的路线和最近邻基线的路线画在同一张坐标图里,不同车辆用不同颜色表示。

import matplotlib.pyplot as plt def plot_routes(coords, routes): plt.figure(figsize=(8, 8)) for route in routes: xs = [coords[i][0] for i in route] ys = [coords[i][1] for i in route] plt.plot(xs, ys, marker='o', linewidth=1.5) plt.scatter([coords[0][0]], [coords[0][1]], c='red', s=100) plt.title("Route Visualization") plt.show()

画完图后要马上做三件事:看是否每辆车都从仓库出发并回到仓库、看车辆之间的路线是否重叠严重、看有没有节点被漏掉。如果图上出现一条横跨全图的长直线,基本可以断定是坐标归一化或距离计算有问题。路线图是演示文稿里最容易引起老师注意的部分,值得多花一点时间调好看一点。

6.3 论文和大作业里的标准呈现方式

最后整理结果时,我习惯做一张对比表,三行内容:随机策略、最近邻基线、强化学习策略。每行列出平均路线长度和平均每车客户数。可行性单独写一列,任何一次实例不满足约束都要标红,这是评审最关注的安全性问题。

方法平均路线长度平均每车客户数可行率
随机策略待填入待填入待填入
最近邻基线待填入待填入待填入
强化学习策略待填入待填入待填入

再加两张图:训练过程中平均路线长度随迭代数的变化曲线,以及上述路线对比图。曲线图要用平滑窗口处理,不然高频抖动会影响阅读体验。整个实验部分四件套就齐了:模型设计、训练曲线、路线可视化、数值对比表。

从那以后,我每次跑完训练都强制走一遍固定测试集加可行性断言,确认没问题再截图放进论文,这个习惯帮我省下的返工时间比任何调参技巧都值。希望这份代码和踩坑记录能让你少折腾几个通宵。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询