1. 三维路径规划与Q-learning的基础架构
1.1 为什么三维路径规划比二维更值得用Q-learning
三维空间的路径规划做起来比二维要麻烦不少。平面网格只需要考虑横向和纵向两个自由度,换到三维之后多了一个高度方向的自由度,状态空间从“格子”变成“体素块”,规模直接是三次方的增长。比如一个10×10的二维地图只有100个状态,而一个10×10×10的三维地图就是1000个状态,动作空间也从4个方向扩展到了6个甚至更多。很多在二维环境里跑得很顺的算法,换到三维就会明显变慢,甚至直接跑不动。
Q-learning这种基于表格的强化学习方法在三维场景里反而有独特优势。因为三维空间的结构不会像游戏那样复杂到状态无限,路径规划的地图范围通常都是有限、离散、可枚举的,每个体素块都是一个明确的状态,动作也相对固定。这种情况下用一张Q表就能把“在哪个位置、往哪个方向走、价值是多少”完整记录下来,训练过程虽然比二维慢,但不会出现维度爆炸到无法计算的程度。这个项目里我把地图控制在几个不同尺度的体素空间里做实验,10³和20³规模都很适合用Q-learning直接跑,30³加一点优化技巧也能在几分钟内完成训练。
这个系列的001篇已经把三维网格环境的搭建、基本的Q-learning更新流程讲过了,这一篇002主要聚焦两个容易被忽略但实际上很关键的问题:训练出来的策略怎么保存下来,以及怎么在后续任务里把保存的策略读取回来直接用。很多人跑强化学习demo的时候只关心训练曲线好不好看,真正需要上线或者做连续实验的时候才发现,策略不能持久化是个大问题——每次启动程序都要重新训练一遍,时间成本完全浪费了。
1.2 核心概念:状态、动作、奖励怎么定义
在三维路径规划这个具体问题里,Q-learning的四个核心要素要重新梳理一遍。
状态的定义很直接,就是智能体当前所在的体素坐标。假设地图是size×size×size的体素空间,那么状态可以表示为(sx, sy, sz),每个坐标的取值范围是0到size-1。要注意的是,状态空间的大小直接影响Q表的形状,我在代码里用grid_size统一控制三个维度,这样Q表初始化起来很方便:q_table = np.zeros((grid_size, grid_size, grid_size, action_num))。这里用NumPy数组而不是Python字典,主要考虑到后面保存和读取策略的时候数组格式可以直接用np.save和np.load,不用自己写序列化逻辑。
动作的定义是这个项目里值得多说两句的地方。三维空间的基本动作可以设定为6个方向:上、下、左、右、前、后,分别对应Y轴正负、X轴正负、Z轴正负。有些实现会把对角线方向也加进来,变成18个甚至26个动作,这样智能体移动更灵活,但Q表规模会跟着翻倍,训练收敛速度也会变慢。我的建议是初学者先用6个方向把流程跑通,等确认奖励函数和保存读取逻辑都没问题了,再考虑扩展动作空间。
奖励函数是Q-learning里最需要花心思设计的部分。我在项目里用了三个层次的奖励叠加:
- 到达目标点:奖励+100,这是稀疏的最终目标反馈。
- 每一步的微小惩罚:奖励-1,用来约束路径长度,避免智能体绕远路。
- 距离引导奖励:每次移动后计算当前位置到目标点的欧氏距离,如果距离比上一步更近,额外给一个小的正奖励,比例系数取0.5。
这个距离引导奖励在三维空间里非常重要。没有它的情况下,大尺寸地图里前期探索几乎拿不到任何正向反馈,Q表全是负值,训练半天智能体还在原地打转。加了这个引导之后,即使探索初期经常撞墙,Q表也能逐步学到“靠近目标是有好处”的规律,收敛速度直观提升了一大截。
2. 策略保存与读取的价值和实现思路
2.1 为什么保存策略很关键,不只是为了方便
强化学习训练好的东西本质上就是一个策略函数,在Q-learning里就是那张Q表。Q表保存下来意味着什么?意味着之前花了几百轮训练出来的经验可以跨进程复用。我在实际项目里遇到过几个非常现实的场景,每一个都让我觉得策略持久化是必做功能而不是可有可无的加分项。
第一个场景是训练和推理分离。训练阶段要跑几百上千个回合,耗时几分钟甚至更久,如果每次做路径规划测试都要从零开始训练一遍,工作效率低到没法接受。把策略保存下来之后,训练归训练、推理归推理,加载Q表直接用贪心策略走一遍就能得出路径,毫秒级出结果。
第二个场景是实验对比。调试奖励函数参数的时候,经常需要对比不同参数组合的效果。如果没有保存策略的功能,每次改参数都要重新训练,而且不同次训练之间因为随机性还有偏差;有了保存功能,可以把每次训练出的策略存档命名好,统一加载后在同一条测试路径上做对比,结论可靠得多。
第三个场景是增量学习和迁移学习。Q-learning支持从已有的Q表继续训练,也就是说,保存的不只是一个最终结果,还是一份可以继续优化的初始化参数。比如先在10×10×10的小地图里训练500轮保存策略,之后换到20×20×20的地图时,把之前的小图Q表加载出来,对应位置的值作为新Q表的初始值,再训练100轮就能达到不错的水平,比从零开始快了不少。
从技术选型角度来说,Q-learning保存Q表也比神经网络类的强化学习方法简单太多。DQN要保存模型结构加权重参数,至少要处理两三个文件;Q-learning就是一张数组表,存文件读文件几行代码搞定。这也是Q-learning在中小规模路径规划场景里依然值得用的一个重要原因。
2.2 保存与读取的几种方案对比
在动手写代码之前,我先把几个候选的保存方案认真对比了一下,因为不同方案在适用场景、读写效率、可移植性上差别还挺大的。
| 方案 | 文件格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| NumPy原生 | .npy | 读写极快,代码简单,数组结构完全保留 | 跨语言跨版本兼容性弱 | 同一项目内Python环境复用 |
| Pickle序列化 | .pkl | Python对象通用,字典、列表都能存 | 有安全隐患,读取时必须信任来源 | 需要保存完整对象结构(如含超参数) |
| JSON文本 | .json | 可读性好,方便人工检查,跨语言通用 | 对大数组读写慢,占用空间大 | 需要调试查看策略内容时 |
| CSV/文本 | .txt/.csv | 最通用,任何工具都能打开 | 维度信息容易丢失,效率最低 | 极少数需要外部工具分析的情况 |
实际项目里我优先推荐np.save/np.load方案。Q表本质就是多维数组,用NumPy原生格式保存不会丢失任何维度信息,加载回来之后和训练结束时的内存布局完全一致,不需要额外解析处理。NumPy底层是二进制格式,读写性能非常稳定。
Pickle我在特殊情况也会用。比如我想把Q表连同训练参数(学习率、折扣因子、当前回合数)一起打包存档,用字典包一下再pickle就比较方便。这里要提醒一下:pickle读取时不要加载来源不明的文件,毕竟它执行任意Python代码的风险一直存在。自己训练自己用完全没问题,但给别人交付代码时最好换成更安全的格式。
JSON方案我一般只在调试的时候用。Q表规模比较小时输出成可读文本,能直观检查某个状态下哪个动作的Q值最高、策略决策是否合理;规模大了之后就不推荐了,几万个数字写成JSON文件打开都要卡一下。而且JSON的浮点精度问题偶发会出现,存进去再读出来Q值可能和原来有一点点误差,对策略影响不大但不是零。
3. 核心代码实现:从环境构建到策略存取
3.1 环境建模和Q表初始化
这个项目里的三维环境我用了最朴素的体素网格表示法。整个空间就是一个三维NumPy数组,数组的值为0表示该位置可通行,值为1表示障碍物。起点和终点是固定坐标,在环境初始化时确定。
import numpy as np class ThreeDGridEnv: def __init__(self, grid_size=10, obstacle_density=0.1): self.grid_size = grid_size self.grid = np.zeros((grid_size, grid_size, grid_size), dtype=np.int8) # 固定起终点 self.start = (0, 0, 0) self.goal = (grid_size - 1, grid_size - 1, grid_size - 1) # 随机生成障碍物 np.random.seed(42) for x in range(grid_size): for y in range(grid_size): for z in range(grid_size): if (x, y, z) == self.start or (x, y, z) == self.goal: continue if np.random.random() < obstacle_density: self.grid[x, y, z] = 1 # 动作空间:上下左右前后 self.actions = [ (1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1) ] self.action_num = len(self.actions)Q表初始化和参数定义:
q_table = np.zeros((grid_size, grid_size, grid_size, 6)) alpha = 0.1 # 学习率 gamma = 0.9 # 折扣因子 epsilon = 0.5 # 探索率,训练中线性衰减Q表初始值用全0是一个常用的做法,可以让智能体在前期更愿意尝试不同的动作。如果把初始值设成一个较大的正数,智能体会更倾向于先探索而不是利用,适合在稀疏奖励环境下使用;但反过来,如果初始值过大,Q值的更新过程会变得很慢。对于这个项目,全0初始化加epsilon探索已经够用了。
3.2 训练主循环与Q值更新
训练主循环的逻辑比较标准,但有几个三维环境特有的细节需要注意。先看核心代码:
def get_distance(pos): return np.sqrt((pos[0] - goal[0])**2 + (pos[1] - goal[1])**2 + (pos[2] - goal[2])**2) def train(episodes=500): global q_table, epsilon step_counts = [] for episode in range(episodes): state = env.start total_reward = 0 step_count = 0 while state != env.goal and step_count < 200: # epsilon-greedy策略选择动作 if np.random.random() < epsilon: action_idx = np.random.randint(0, env.action_num) else: action_idx = np.argmax(q_table[state[0], state[1], state[2]]) old_distance = get_distance(state) # 执行动作 dx, dy, dz = env.actions[action_idx] next_state = (state[0] + dx, state[1] + dy, state[2] + dz) # 边界检查和碰撞检查 if (next_state[0] < 0 or next_state[0] >= env.grid_size or next_state[1] < 0 or next_state[1] >= env.grid_size or next_state[2] < 0 or next_state[2] >= env.grid_size or env.grid[next_state[0], next_state[1], next_state[2]] == 1): # 撞墙:给惩罚,不移动 reward = -10 next_state = state else: new_distance = get_distance(next_state) if next_state == env.goal: reward = 100 else: reward = -1 + 0.5 * (old_distance - new_distance) # Q值更新 best_next = np.max(q_table[next_state[0], next_state[1], next_state[2]]) q_table[state[0], state[1], state[2], action_idx] += alpha * ( reward + gamma * best_next - q_table[state[0], state[1], state[2], action_idx] ) state = next_state total_reward += reward step_count += 1 step_counts.append(step_count) # epsilon线性衰减 epsilon = max(0.05, epsilon * 0.995)这里有几个容易踩坑的细节。
第一个是边界碰撞的处理方式。如果智能体下一步会走出地图边界或者撞上障碍物,我的处理是“惩罚但不移动”,也就是下一步状态保持为当前状态。有些人会写成直接终止回合,那会让智能体变得过度保守,稍微走到地图边缘动不动就结束一轮,不利于探索路径。相比之下“原地惩罚”能给智能体更多试错机会。
第二个是距离引导奖励的传递顺序。我在执行动作之前就算好了old_distance,执行动作之后计算new_distance,两者的差值才是真正的引导信号。如果顺序搞反了,奖励和状态更新就不匹配了,训练会变得很不稳定。
第三个是step_limit。三维空间里Q-learning前期探索效率不高,一步一格的情况下,10³的地图极限步数也就1000步左右,但我设了200步的截断。这个数字要结合地图大小来调整,太小了会导致很多回合正常找不到路就被强行终止,训练信息大量浪费;太大了又会让部分回合一直空转到结束,浪费时间。20³地图我就把上限调到了500。
3.3 策略的保存与读取完整实现
训练完成后,保存策略的核心就是保存Q表,用NumPy自带方法就够了。
# 保存策略 def save_policy(q_table, file_path="qtable_3d.npy"): np.save(file_path, q_table) print(f"策略已保存至: {file_path},形状: {q_table.shape}")这只是两行代码的事情,但实际项目里我强烈建议把保存函数写得稍微完备一点,至少加上这里的维度校验:
# 读取策略 def load_policy(file_path="qtable_3d.npy"): loaded_q = np.load(file_path) print(f"策略已加载: {file_path},形状: {loaded_q.shape}") return loaded_q我把保存和读取的逻辑封装成一个更完整的接口后,整个使用流程就变成这样:
# 完整使用流程示例 # 1. 训练 q_table = train(episodes=500) # 2. 保存 save_policy(q_table, "policy_10x10x10.npy") # 3. 读取并直接用贪心策略规划路径 q_table = load_policy("policy_10x10x10.npy") def extract_path(q_table, env): state = env.start path = [state] max_steps = 200 for _ in range(max_steps): if state == env.goal: break action_idx = np.argmax(q_table[state[0], state[1], state[2]]) dx, dy, dz = env.actions[action_idx] next_state = (state[0] + dx, state[1] + dy, state[2] + dz) # 如果下一步撞墙,说明当前Q表在该状态下的最优动作不可行 if (next_state[0] < 0 or next_state[0] >= env.grid_size or next_state[1] < 0 or next_state[1] >= env.grid_size or next_state[2] < 0 or next_state[2] >= env.grid_size or env.grid[next_state[0], next_state[1], next_state[2]] == 1): break state = next_state path.append(state) return path使用extract_path函数时要注意一个问题:如果加载回一个训练不充分的Q表,贪心策略会把智能体引导到某个状态后推荐一个撞墙的动作,此时路径提取会直接中断。这个现象其实很有用,它可以作为一个质量检测信号——如果频繁出现路径中断,说明策略还没收敛,需要继续训练而不是强行使用。我一开始没意识到这点,把训练不足的策略直接拿去部署,跑出来的路径总是半途断掉,还以为是环境代码写错了。
4. 训练与调试中的常见问题
4.1 参数怎么调:学习率、折扣因子、探索率
Q-learning的三个核心参数在三维路径规划里的调法跟二维环境有一些不同。这里直接给出一份我实测有效的参数区间,并结合我的调试经验解释原因。
- 学习率alpha在0.05到0.3之间比较合适。alpha太大,Q值更新幅度过猛,智能体刚学到一点东西很快又被新经验冲掉,训练曲线剧烈震荡;alpha太小,每次更新只挪动一点点,训练时间大幅拉长。我实测下来取0.1是个稳健的起点值。
- 折扣因子gamma在0.85到0.99之间。gamma越接近1,智能体越看重远期收益,理论上更容易找到全局最优路径;但三维空间路径比较长,gamma太大会让奖励在时间轴上过度传播,早期探索阶段Q表更新存在滞后效应。路径规划通常更看重“最终能到达终点”这个结果而非未来收益的微小差异,所以取0.9很合理。
- 探索率epsilon是训练最重要的控制旋钮。固定值方案里0.2到0.5都有人用,但我推荐线性衰减方案:从0.5开始,每回合乘以0.995,衰减到0.05下限。这样前期充分探索环境结构,后期收敛到利用最优策略,兼顾了探索广度和收敛稳定性。
我踩过最大的坑是在大尺寸地图里把epsilon衰减设得太快。20×20×20的地图,epsilon从0.5按每回合0.99衰减,300回合后就跌到0.05,但此时智能体还没把环境探索完整,过早进入利用阶段后长期滞留在局部最优路径上,成功率一直上不去。后来把衰减系数改成0.995,给足了探索时间,问题就解决了。
4.2 奖励函数设计的常见坑
奖励函数是三维路径规划里最“细节决定成败”的部分。我列几个实际遇到过的坑和对应解法。
第一个是只给终点奖励的稀疏奖励陷阱。只设置到达终点+100、其他情况0奖励,在地图规模小(比如5×5×5)时还能勉强收敛,但一旦体积增大,前期探索成功率极低,Q表更新频率低到等于没有学习。必须加每步-1的基础惩罚,让智能体有“少走冤枉路”的基本压力。
第二个是距离引导奖励过度主导的问题。如果把距离引导系数设得太高(比如系数大于1),会出现一个奇怪的现象:Q表会被近距离奖励主导,智能体特别执着于“朝目标方向直线冲”,遇到障碍物也不会绕路,直接在障碍物前撞墙拿惩罚。距离引导的本质是辅助信号,不能盖过避障和到达终点的核心目标。
第三个是终点奖励和碰壁惩罚之间的相对量级。终点奖励+100,碰壁惩罚-10,每步惩罚-1,这个配比是我调了几轮之后确定的。碰壁惩罚不能太大,否则智能体宁可原地试探也不愿靠近障碍物密集的区域;也不能太小,否则碰到墙毫无压力,路径会反复穿越危险区域。100比10的配比让智能体在“绕路避障”和“直接莽撞”之间倾向于前者,但又不至于因为一次撞墙就完全放弃一条路径。
经验法则:终点奖励应该是单步惩罚绝对值的50到100倍,碰壁惩罚应该是单步惩罚的5到10倍。在这个量级区间内调整,训练曲线基本都不会出大问题。
4.3 路径规划的评估指标
评估一个三维路径规划策略好不好,光看训练损失曲线是不够的,路径规划任务需要自己的指标。我每次跑完训练都会固定调用测试函数计算几个关键指标:
- 成功率:在测试环境中从起点出发,使用贪心策略能否在步数上限内到达终点。这个指标最基础也最重要。
- 平均路径长度:成功回合中,从起点到终点的路径总长度。Q-learning的目标是最大化累计奖励,由于有每步-1的惩罚,路径越短对应奖励越高,可以用它衡量路径的优劣程度。
- 路径平滑度:路径中方向变化的次数。三维路径经常会出现锯齿状抖动,明明可以直线走的非要上下左右反复横跳,这类路径视觉效果差,实际应用也不行。这个指标我主要靠可视化观察,没有严格量化。
- 收敛回合数:累计奖励到达稳定水平的回合序号。通过打印每回合总奖励并绘制曲线来观察。
在10×10×10的测试环境里,我训练500轮后成功率达到98%左右,平均路径长度在27到35之间,而理论上最短路径大概是27步(从(0,0,0)到(9,9,9)对角方向距离27各单位,加绕障修正)。偶尔出现的失败回合基本都是因为随机障碍物把必经之路堵死导致的。
5. 实验结果与策略复用的实际体验
5.1 一个具体实验的参数和效果
我拿一个完整的实验来演示保存和读取策略的实际效果。环境尺寸设为10×10×10,障碍物密度0.1,起点(0,0,0),终点(9,9,9),训练回合数600。
训练完成后Q表保存为policy_10.npy,文件大小只有几千字节。为什么这么小?因为10×10×10的状态空间是1000个状态,每个状态对应6个动作,Q表总共6000个浮点数,用float64存储也只有48KB。这个体量在工程上完全不是问题,可以做内存驻留,嵌入到路径规划服务里每次查询都是毫秒级响应。
加载策略后跑了100次测试,结果如下:
| 指标 | 数值 |
|---|---|
| 测试次数 | 100 |
| 成功率 | 98% |
| 平均路径长度 | 31.2 |
| 最短路径长度 | 27 |
| 平均消耗时间 | 3ms |
那2%的失败案例我专门跟踪过,问题出在地图边界附近有几个状态被引导到了一个被障碍物包围的局部区域,Q表在那些状态下记录的最优动作是撞墙后的惩罚状态,但智能体已经钻进去了,又没有设计“回头”的机制,结果卡死。这个不是Q-learning本身的致命问题,而是动作空间设计里缺少了诊断逻辑,正常应用时可以通过增加随机逃生动作或者加大探索率规避。
5.2 策略读取后的三种复用方式
把保存好的策略加载回来,可以做的事情不止“用贪心走一遍”这一种。我在项目里试过三种复用方式,效果都不错。
第一种是热启动训练。在20×20×20的新地图上训练时,把之前10×10×10的训练所得Q表先加载进来,然后把超出原表范围的新状态初始化为0,再继续训练。这样新地图的前几百回合就不是完全随机探索了,智能体带着小地图的避障和趋近目标的经验直接上手,整体收敛速度比我对比的从零训练快了大约40%。这个技巧在工程里做地图渐进式扩展时非常有用。
第二种是固定策略的快速推理。加载策略后把所有状态的贪心动作预先计算好,用一个字典存下来,比如{(sx,sy,sz): action_idx},之后每次路径查询直接查字典,连argmax都不用重复计算。查询时间复杂度O(1),极快极稳,适合对延迟敏感的场景。
第三种是多策略对比评估。要验证奖励函数参数调整的效果,只需要保存两份Q表:policy_before.npy和policy_after.npy,然后在同一张测试地图上分别跑路径、统计路径长度和成功率。同图异策略对比,排除随机性干扰,结论可信度远高于“跑一遍看效果”的粗糙对比方式。
5.3 从Q-learning到DQN的扩展思路
前文提到Q-learning在30³以上的地图里会吃力,这正好是策略保存要跨出的下一步。规模变大时,Q表从数组变成神经网络逼近函数,策略保存的粒度从“一个文件装全表”演变成“模型结构+权重参数+优化器状态”的复杂集合,但核心思想不变:把训练成果持久化,让学习到的经验可以复用。
我的建议是先把Q-learning版本的三维路径规划完整跑通,把状态、动作、奖励、策略存取这套逻辑消化掉,再往DQN方向扩展。因为DQN中经验回放、目标网络、探索策略这些概念在Q-learning的框架下都有对应物,只是实现形式更复杂。有了这套Q-learning的基础,后面理解DQN的网络结构更新方式会顺利很多。
另外,三维体素路径规划还有一个提升路径质量的空间值得提一下。体素网格天生的特点就是路径锯齿化严重,从(0,0,0)到(9,9,9)的路径在体素空间里看起来是一节一节折线。后续可以用路径平滑算法(比如拉格朗日插值或者B样条拟合法)对规划结果做后处理,把折线变曲线,输出更接近实际运动轨迹的路径。这个和策略保存读取是两条独立的优化线,结合起来用会让整个方案完全体面起来。
我这个项目整体跑下来最大的体会是:强化学习解决路径规划问题,最大的障碍反而不是算法本身,而是工程闭环是否完整。训练、评估、保存、加载、再训练,这个循环每缺一段,项目就卡一段时间。把保存和读取策略这环补上之后,整个基于Q-learning的三维路径规划方案才真正具备持续迭代的可能性。下一篇我打算把迷宫地图生成和动态障碍物避让加进去,到时候再和各位分享实操中的新坑。