ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DQN求解三维在线装箱:从状态建模到训练可视化实战

DQN求解三维在线装箱:从状态建模到训练可视化实战 简介这是一份基于DQN深度强化学习解决三维在线装箱问题的Python项目包以货车车厢长宽高为约束通过智能体在线决策箱子摆放策略力求提升空间填满率适合物流优化、人工智能及相关专业的毕设学生、课程设计者与研究人员学习。项目中train.py负责模型训练eval.py评估装箱效果data.py处理箱子与车厢数据container.py定义三维容器模型draw.py输出可视化结果并附带已训练好的模型权重文件cnn.pth可直接加载验证。资源包共10个文件含5个Python脚本、2张效果图、1个图表文件、1份README说明文档及1个权重文件压缩包约5.64MB文件组织清晰便于按模块调用和二次开发。代码均测试运行成功目前已有104人学习浏览读者既可借此掌握DQN在三维组合优化中的落地流程也能在现有框架上替换数据、调整奖励或扩展场景用于毕设、课设或初期实验演示。1. 三维在线装箱为什么难DQN 入场的第一个理由物流行业里有个很现实的问题箱子一个接一个地来到流水线上你必须立刻决定把它放进车厢的哪个位置不能等所有箱子到齐再统一规划——这就是「在线装箱」。它和离线装箱最大的区别在于你永远不知道下一个箱子长什么样却要在瞬间做出一个影响后续所有箱子的决策。传统的启发式算法比如按体积降序、按剩余空间匹配在箱子种类少、尺寸规整时还能对付一旦箱子尺寸离散度大、到达顺序随机填充率很快就会掉到 70% 以下而且无论如何调整规则都上不去。这个场景天然适合深度强化学习状态是当前车厢的占用情况动作是箱子的放置位置奖励是装箱后的空间利用率。用 DQN 做三维在线装箱本质上是让网络从大量装箱经验里学到一个「看到当前局面就知道该往哪放」的映射而不是靠人手写规则。这份资源正是围绕这个目标整理的一套完整 Python 实现包含训练、评估、可视化全套代码和一份文档说明适合正在做毕设、课设或者想入门深度强化学习落地应用的人直接上手。2. 先把问题建模状态、动作、奖励与容器表示的选型依据2.1 在线装箱和离线装箱的差别为什么必须实时决策在线装箱的输入是一个箱子序列每个箱子按顺序到达。你需要在箱子到达时决定它的放置位置以及旋转方向一旦放下就不能再调整。离线装箱则允许先看完整批箱子再做全局最优规划。两者的复杂度差了好几个量级离线可以用整数规划、遗传算法这类全局搜索方法去逼近最优解在线问题因为信息不完整必须依赖策略函数直接输出动作所以深度学习在这里才有用武之地。这个资源里的 DQN 方案核心是把在线装箱问题转化为一个马尔可夫决策过程MDP状态当前车厢内已放入箱子的占用情况三维矩阵或高度图动作下一个箱子的候选放置位置离散化后的坐标 旋转方式奖励放入箱子后获得的体积利用率增量或放置失败的惩罚转移放置成功则更新车厢占用状态放置失败则跳过该箱子。这样定义之后问题就从「怎么装得满」变成了「在每一步状态里哪个动作能带来最大累计奖励」。DQN 学习的正是这个 Q 函数Q(s, a) 表示在状态 s 下执行动作 a 后未来期望奖励的总和。2.2 状态空间的表示方式从点云到场地的离散化三维装箱的状态空间如果直接用连续坐标表示动作空间会变成无穷大Q 网络根本没法收敛。这个项目用的是离散化表示把车厢的长宽高划分成一个个小格栅格每个格子的状态表示该位置是否已被箱子占据或者剩余高度是多少。这种表示有几个好处一是动作空间变成了有限个候选位置二是可以直接用 CNN 提取局部空间特征——相邻格子之间存在明显的空间相关性卷积天然擅长捕捉这种「哪里还有空位、哪里的形状适合放什么尺寸的箱子」的模式。常见的离散化有两种做法等体积栅格和高度图。等体积栅格是把车厢划分成固定大小的三维矩阵1 表示被占、0 表示空闲空间信息完整但计算量大高度图是把每个平面格子记录当前堆叠高度本质上是把三维压缩成二维。这个项目里用的是三维矩阵的方式通过 data.py 生成装箱场景数据再由 container.py 维护装箱状态。# container.py 中状态初始化示意 class Container: def __init__(self, length, width, height, grid_size1): self.length length self.width width self.height height # 三维离散化矩阵0空闲, 1已占 self.grid np.zeros((length // grid_size, width // grid_size, height // grid_size), dtypenp.uint8) self.grid_size grid_size def check_placement(self, box, x, y, z, rotation): 检查在 (x, y, z) 处以某个旋转方向放置箱子是否合法 dx box.length // self.grid_size dy box.width // self.grid_size dz box.height // self.grid_size if x dx self.grid.shape[0]: return False if y dy self.grid.shape[1]: return False if z dz self.grid.shape[2]: return False region self.grid[x:xdx, y:ydy, z:zdz] return not np.any(region)grid_size 的选择直接影响训练效果。设得太小矩阵维度爆炸CNN 参数量和训练时间成倍上涨设得太大装箱精度变差很多箱子尺寸覆盖不了整数个格子。我一般会让 grid_size 满足「车厢最长边除以它的结果在 8~20 之间」比如 60×40×30 的车厢用 grid_size5得到 12×8×6 的矩阵精度和计算量都相对均衡。实际跑这个项目时默认参数已经是调好的改容器尺寸时记得同步调整 grid_size。2.3 动作空间与奖励函数怎么定放不下、放得下与“惩罚”的设计动作空间是所有候选放置位置的集合。这个项目的做法是把车厢底面划分为若干候选点每个候选点同时对应箱子的所有合法旋转方式。也就是说一个动作由「放置点 旋转索引」共同决定。给网络输出的 Q 值做 argmax 时如果选中的位置放不下当前箱子有两种处理方式一是在动作空间里把所有非法动作的 Q 值直接遮罩为 -inf然后在剩余动作里选最大值二是允许网络选择非法动作但给予较大负奖励。这个项目用的是遮罩方式这也是 DQN 收敛更快的做法因为网络不需要去学「哪些位置是永远不可能的」。奖励函数没有太多花哨的设计但它是整个训练是否有效的关键。我拆开代码后发现它的奖励逻辑是# train.py 中奖励计算的简化逻辑 def compute_reward(gained_volume, box_volume, success): if not success: # 放置失败超出边界 / 重叠 / 悬空 return -0.5 # 一个小的固定惩罚避免网络“破罐子破摔” if gained_volume 0: return 0.0 # 按当前箱子体积占车厢总体积的比例给正向奖励 return gained_volume / total_container_volume这个设计选得比较谨慎。用「增量体积占比」而不是「累计体积占比」做奖励好处是网络每一步的奖励只跟当前箱子有关不会被历史装满状态稀释掉梯度对失败的惩罚只有 -0.5而不是更大的负值是因为在线场景里箱子放不下是正常情况惩罚过大会让网络倾向于什么都不放反而降低整体装箱率。reward shaping 的边界就在这里既要让失败有反馈又要让探索期网络敢去尝试复杂位置。3. 搭建可复现的 DQN 训练闭环从文件结构到关键参数3.1 项目文件结构与入口train.py、eval.py 各自承担的职责整个项目解压后是一个叫 RF_binbox-main 的目录核心文件包括 train.py训练入口、eval.py评估入口、container.py车厢与箱子数据结构、data.py数据生成、draw.py可视化绘制以及一个训练好的模型权重文件 cnn.pth。README.md 里有最基本的说明但比较简略我建议按下面的顺序读代码container.py 先看数据结构data.py 看输入分布train.py 看训练循环和奖励计算最后再看 eval.py 和 draw.py 的评估与可视化。train.py 的主循环是一个标准的 DQN 流程循环每个 episode在每个 step 里把状态交给网络用 ε-greedy 策略选择动作执行后观察奖励和下一状态存入经验回放池每隔固定步数从池里采样一批数据更新网络参数。如果你已经跑过别的 DQN 项目train.py 的逻辑几乎是复制粘贴级别的熟悉真正需要花时间的反而是数据生成和状态更新这两块因为它们和装箱场景强相关。# 安装依赖建议 Python 3.8PyTorch 1.10 pip install torch numpy matplotlib # 开始训练默认跑 1000 个 episode python train.py --episodes 1000 --batch-size 64 --gamma 0.99从命令行参数也能看出来这个项目的训练入口暴露的参数不多重点在 episodes、batch-size、gamma 三个episodes 控制总训练轮数batch-size 控制每次参数更新的样本量gamma 是未来奖励的折扣因子。初学者最容易忽略 gamma 的作用——它越接近 1网络就越看重长期收益装箱问题里这一步决策确实会影响后续很多步所以设为 0.99 是合理的。3.2 核心类与数据流container.py 和 data.py 怎么配合container.py 负责维护车厢状态提供「检查能否放置」和「执行放置」两个核心方法。data.py 负责生成箱子序列——它决定了训练数据的分布。这两个文件配合起来形成了完整的数据流data.py 生成一批箱子 → 训练循环依次取出每个箱子 → 调用 container 的检查方法判断当前动作是否合法 → 合法则更新 matrix、累计体积、计算奖励。箱子序列的生成方式直接决定模型学到的策略适用范围。这个项目的 data.py 里箱子尺寸是从一个指定范围里随机采样的长宽高各自服从均匀分布。这意味着训练出来的策略是「针对这种尺寸分布的箱子」优化的。如果你换一批完全不同尺寸分布的箱子做测试性能会有明显下降这不叫过拟合而是任何学习算法的必然特性——它学的是分布内的映射关系。# data.py 中箱子序列生成的核心逻辑简化 def generate_boxes(num_boxes, size_range(10, 30)): boxes [] for _ in range(num_boxes): length random.uniform(*size_range) width random.uniform(*size_range) height random.uniform(*size_range) boxes.append(Box(length, width, height)) return boxessize_range 就是箱子尺寸的采样范围。这里有个容易被忽视的点如果 size_range 的上限接近车厢尺寸箱子就会变得非常大每个箱子几乎独占一层平面DQN 能做的决策空间被大幅压缩训练出来的策略没有参考价值。所以做实验时建议保持箱子尺寸和车厢尺寸的比例在 1:3 到 1:6 之间让网络有充分的腾挪空间去学习。3.3 训练参数怎么调batch size、学习率、经验回放池的边界DQN 训练中有一组核心参数取值不对会出现各种「玄学」问题。我把这个项目里的默认参数和调试经验整理成了一张表参数推荐范围说明learning rate1e-4 ~ 5e-4太高会震荡不收敛太低训练速度慢到无法接受batch size32 ~ 128取决于显存越小越容易震荡越大越稳定但更新慢gamma0.95 ~ 0.99装箱问题建议 0.99保留长期收益信息epsilon 起始0.9 ~ 1.0前期需要大量探索随机位置epsilon 终止0.01 ~ 0.05训练后期要让网络基本按 argmax 决策replay buffer50000 ~ 200000至少能覆盖最近几十个 episode 的经验target 更新频率1000 ~ 5000 步更新太频繁会让训练不稳定epsilon 衰减曲线是 DQN 最容易翻车的地方。衰减太快网络还没见过足够多的状态就进入纯利用模式策略很容易锁死在某个局部最优衰减太慢训练后期还在大量随机探索装箱率上不去。这个项目用的是线性衰减从 1.0 衰减到 0.01跨度大概是总 episode 数的一半。我一般会在训练时打印每 100 个 episode 的平均奖励如果看到奖励增长曲线有一个明显的「平台期」大概率是 epsilon 已经降得太低网络还在试探新策略但探索量不够可以把衰减跨度拉长 20% 再试。target 网络的同步频率也值得一提。DQN 的收敛性严重依赖 target 网络的稳定性——如果每一步都用当前网络自己更新自己很容易出现 Q 值被高估然后发散的问题。这个项目里 target 网络每 2000 步同步一次在当前场景下是比较合适的节奏如果你改了容器尺寸或箱子序列长度同步频率可能需要重新调一般换算成几个 episode 的步数来估算。4. 把训练结果做成可视化验证eval.py、draw.py 与实际装箱率4.1 eval.py 的评估逻辑不能只看 loss要看装箱率训练跑完以后模型到底行不行不能只看训练 loss 曲线——DQN 是自举式学习loss 降下去只能说明 Q 值预测和 target 的差距在缩小不代表策略变好了。正确的评估方式是关掉探索epsilon0让网络纯按 Q 值 argmax 决策跑完一整批箱子序列统计最终的体积利用率。这个项目里 eval.py 做的就是这件事。它跟 train.py 最大的区别在于train 模式里箱子放不下时会给予负奖励并继续eval 模式里放不下的箱子直接跳过最后统计「累计放入体积 ÷ 车厢总体积」。这个指标是真正的装箱率也是你在答辩或者汇报时应该拿出来的数字。# 评估训练好的模型权重 python eval.py --model-path cnn.pth --num-episodes 100eval.py 的 num-episodes 建议设到 50 以上。装箱率是随机过程的结果——同一套模型在 100 组不同箱子序列上表现会有几个百分点的浮动。跑 100 组取平均值得出的结果才具有可汇报性。另外注意 eval.py 不会更新网络所以它跑完之后模型的权重文件不需要保存。4.2 draw.py 的可视化输出图1、图片1、图片2 里该看到什么draw.py 的作用是把装箱过程可视化输出图片文件包括图1装箱完成后的最终形态、图片1、图片2不同角度的中间过程截图。这些图片对于答辩的价值远大于任何指标表格——评审老师看到一张「车厢内部被箱子填满的三维渲染图」比看到一串数字更有说服力。# draw.py 中绘制装箱结果的简化逻辑 def draw_result(container, save_path): fig plt.figure() ax fig.add_subplot(111, projection3d) # 逐个绘制已放置箱子的立方体 for box in container.placed_boxes: x, y, z box.position dx, dy, dz box.size ax.bar3d(x, y, z, dx, dy, dz, alpha0.7, colorsteelblue) plt.savefig(save_path, dpi150) plt.close()看可视化输出时有两个重点一是看箱子之间的贴合度如何如果大量箱子之间存在明显的悬空间隙说明网络学到的策略偏向「先到先得」而不是「规划性摆放」二是看剩余空间的分布如果剩余空间集中在几个大块区域说明策略还有改进空间因为理想情况下剩余空间应该被分散到各个缝隙里。draw.py 生成的三视图其实是在帮你判断「策略是真正的空间规划还是在机械地按某种顺序堆叠」。4.3 85% 装箱率是怎么算出来的一个可复核的评估公式装箱率的计算必须和官方口径一致否则答辩时容易被打回来。这个项目采用的公式是装箱率 所有已放置箱子的体积之和 ÷ 车厢内部总容积这个定义和实际物流行业里约定俗成的「装载率」一致。举个例子车厢尺寸是 60×40×30总容积是 72000 立方单位如果最终放入了总容积 61200 的箱子装箱率就是 61200 ÷ 72000 85%。评估时要注意一个边界如果你的箱子尺寸和车厢尺寸用同一个 grid_size 离散化可能会出现「物理上能放下但离散网格放不下」的情况——比如箱子长 19.5 格离散化后变成 20 格多占了半格空间。这时实际装箱率会被低估。处理办法是评估时把箱子尺寸精确到原始浮点数重新计算体积而不是用网格对齐后的体积。我一般会在 eval.py 里额外记录「原始体积」和「网格体积」两个字段对比它们之间的差距就能确认离散化误差是否在可接受范围内。5. 避坑与排查DQN 训练中常见的“玄学”问题与修复记录5.1 现象训练了 500 个 episode 奖励仍然不涨原因epsilon 衰减速度不合理网络一直处于随机探索状态经验回放池里的高质量样本太少Q 值学习不出来。另一个常见原因是奖励函数里「成功放置的箱子和失败的箱子」的奖励差距太小网络感受不到明显的梯度方向。解决先检查 epsilon 曲线——如果 500 个 episode 后 epsilon 还没降到 0.2 以下说明衰减跨度设得太长需要把衰减速度加快如果 epsilon 已经很低但奖励还是原地不动检查奖励函数——把成功放置的奖励从「体积占比」改成「体积占比 × 10」让正负奖励差距拉大。改动奖励后一定要重新调 epsilon因为奖励尺度变了Q 值的收敛速度也会变。5.2 现象eval.py 跑出来的装箱率比训练时低一大截原因训练时的行为是 ε-greedy 的一部分动作是随机探索eval 时是纯 argmax。如果网络学到的 Q 值本身分布有问题比如所有动作的 Q 值都接近argmax 选出的动作可能不如随机动作。这通常是经验回放池里正样本太少导致的「Q 值坍缩」现象。解决检查经验回放池里成功放置的样本比例。如果低于 15%说明训练时大多数动作都在失败这时候不要急着改奖励先把箱子尺寸范围缩小让更多箱子能放得下给网络积累正样本的机会。还有一种情况是 eval 时的箱子序列和训练时的分布不一致比如 data.py 在 eval 时用了新的随机种子导致箱子尺寸偏离训练分布。调度的时候统一随机种子保证评估时用的是训练时的数据分布。5.3 现象加载 cnn.pth 时提示参数不匹配missing keys / unexpected keys原因cnn.pth 是作者用特定 PyTorch 版本和特定网络结构训练出来的权重。如果你换了 PyTorch 版本、改了网络结构比如改了卷积层的通道数权重的 key 名或维度就会对不上。另一个可能是 cnn.pth 只保存了 state_dict而你没有先按原结构实例化模型就直接 load_state_dict。解决先检查网络结构是否和原始代码一致——不要改 conv 层的输出通道数然后确认加载方式# 正确的加载方式 model build_cnn(state_dim, action_dim) # 先构造与原结构一致的模型 model.load_state_dict(torch.load(cnn.pth, map_locationcpu)) model.eval()如果确认结构没问题却仍报错检查 PyTorch 版本差异。PyTorch 2.x 的 state_dict 和 1.x 的 .data 字段有格式差异用torch.load(..., weights_onlyTrue)或map_location都能缓解。最笨但有效的办法是删掉 cnn.pth用 train.py 重新训练一个新的权重文件。5.4 现象同一份代码、同一个参数两次训练结果差异很大原因训练循环里没有固定随机种子导致初始化、探索、箱子序列生成都有随机性。Python 的 random、numpy.random 和 torch 的 RNG 是三套独立的随机源必须分别固定。解决在 train.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) set_seed(42)注意固定种子只是让实验可复现并不保证结果更好。做超参数对比实验时固定种子能让你准确判断「结果差异来自参数改动」还是「来自随机波动」。如果你想让策略更 robust反而要故意使用多个种子训练同一组参数取平均值做对比。5.5 现象GPU 训练时报 CUDA out of memory但显存明明够用原因三维离散化的状态矩阵虽然看着不大比如 12×8×6但输入 CNN 之前通常要 reshape 成 batch 维度多个样本同时前向传播就容易吃显存。经验回放池如果直接存储原始三维矩阵而不是压缩后的特征也会占用大量显存。解决把经验回放池里的状态存储改成压缩格式——存下每个箱子放置后的 container.grid 变化记录而不是存整个三维矩阵。回放时再按需重建状态。如果不想动代码结构最直接的办法是把 batch size 从 64 降到 32并且把torch.backends.cudnn.benchmark设置为 False减少显存碎片。还有一种做法是把状态矩阵降维成高度图只保留每个位置的最大堆叠高度显存占用直接降低一个量级代价是网络无法感知悬空结构。6. 从答辩到真实场景把这套代码改成你自己的装箱策略6.1 换箱子分布调整 data.py 的随机生成器在做课设或毕设时最容易被问到的问题就是「你的模型能不能处理其他尺寸的箱子」。答案是换尺寸分布就要重新训练。修改 data.py 里的 size_range把原来的均匀分布改成某种实际场景的分布。比如电商订单里小箱子数量远多于大箱子可以用对数正态分布模拟# data.py 中改为小箱子为主的分布 def generate_boxes(num_boxes, base_range(8, 25)): boxes [] for _ in range(num_boxes): # 用幂律分布让出现大量小箱子、少量大箱子 length base_range[0] (base_range[1] - base_range[0]) * (random.random() ** 2) width base_range[0] (base_range[1] - base_range[0]) * (random.random() ** 2) height base_range[0] (base_range[1] - base_range[0]) * (random.random() ** 2) boxes.append(Box(length, width, height)) return boxes改完数据分布后必须重新训练不能直接用原来的 cnn.pth。训练完后用新的箱子分布跑 eval看装箱率是否达到 80% 以上。如果达不到优先检查是不是「小箱子太多导致网络倾向于只挑容易放的位置」这时适当增大成功放置的奖励权重会有效果。6.2 换容器尺寸修改 container.py 的初始参数集装箱尺寸从默认的 60×40×30 改成别的比例比如 50×40×40需要同步调整两处container.py 里的车厢初始化参数以及 train.py 里的状态矩阵维度。如果你的容器是细长型CNN 的卷积核感受野可能会不够网络看不到「远端」的空位。我的做法是把第一层卷积核从 3×3 改成 5×5 来扩大初始感受野或者直接加一层 stride2 的下采样把特征图尺寸缩小。改容器尺寸时grid_size 也要跟着变。保持「最长边方向格数在 8~20 之间」这个原则就行。容器比例变了之后训练收敛速度大概率会变慢因为状态空间的有效形状变了需要更多的 episode 去探索。6.3 验证可视化结果让图表为自己说话答辩展示时不要只甩一张最终装箱率数字。用 draw.py 输出三个阶段的图早期训练策略的表现大概几十个 episode 时、中途的表现、最终收敛后的表现。三张图并列展示可以直观看出策略的改进过程。再用 matplotlib 把「装箱率曲线」和「平均奖励曲线」画在同一张图里横轴是 episode 数双纵轴分别对应两个指标。# 绘制训练过程中的装箱率曲线 import matplotlib.pyplot as plt plt.figure(figsize(10, 5)) plt.plot(episodes, avg_rewards, labelAvg Reward) plt.plot(episodes, avg_packing_rates, labelPacking Rate) plt.xlabel(Episode) plt.ylabel(Value) plt.legend() plt.savefig(training_curve.png, dpi150)如果两条曲线在训练后期同步上升说明策略确实在进步而不是仅仅在优化某一个指标。如果数据有「装箱率上升但平均奖励下降」的矛盾情况一定要在答辩前查清楚原因——很可能是奖励函数里成功放置的奖励权重不对导致网络追求高密度堆叠但牺牲了总体积。这套代码最有价值的地方不在于 DQN 算法本身多么先进——它的网络结构、训练技巧都是 2015 年以来的经典套路——而在于它把一个真实的物流场景完整地做成了可训练、可评估、可视化的闭环。我拆过很多强化学习项目好多代码要么只有训练没有可视化要么评估时直接用训练时的随机策略结果完全不可信。这份资源里 train、eval、draw 三件套齐全彼此的衔接也合理是那种可以直接拿来跑实验、然后逐步替换模块做改进的项目。从那以后我每次拿到类似资源都会先跑通 eval 确认基线再去动训练逻辑这个习惯让我避免了很多「训了半天不知道好坏」的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表