ARTICLE DETAIL

资讯详情

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

Q-learning从原理到实战:Python网格世界与调参避坑指南

Q-learning从原理到实战:Python网格世界与调参避坑指南 简介深度学习算法 Q-learning 原理是一份面向强化学习入门者与算法工程师的 PDF 笔记系统讲解 Q-learning 为何属于 value-based 方法以及 critic 网络、value function、Q-function 等核心概念。内容重点对比蒙特卡洛MC与时间差分TD两种价值估计方式的原理、方差差异和适用场景帮助读者理解如何通过迭代更新找到最优策略。资源还覆盖 target network 固定目标以稳定训练、epsilon greedy 与 Boltzmann exploration 等实用技巧配合图示和逐步推导降低理解门槛。压缩包仅 1 个 PDF 文件大小约 999KB内容精炼适合快速通读与反复查阅。已有 1313 人学习适合希望系统梳理 Q-learning 理论脉络、准备算法面试或开展强化学习实践的读者。1. 为什么 Q-learning 常被误当成深度学习却又常和深度学习绑定出现一说到深度学习算法 Q-learning很多刚入门的同学会以为它是某个深度神经网络结构。实际上 Q-learning 是强化学习里最经典的基于价值迭代的算法它的“学习”发生在维护一张 Q 表而不是调整神经网络的权重。我在做无人车绕障的仿真项目时最开始就用 Q-learning 控制小车在离散栅格地图上寻找路线收敛速度快、策略可解释效果非常直观后来状态空间扩大到连续坐标表格方法直接撑不住了才换成深度 Q 网络。这篇文章就从 Q-learning 的原理讲起给出一份可以直接复现的 Python 网格世界代码再把我调参时踩过的坑逐个拆开。适合准备做小规模控制任务、课程设计以及从传统强化学习过渡到深度强化学习算法的工程师和同学。2. Q-learning 的核心迭代状态、动作、奖励与 Q 表的更新规则2.1 从“走迷宫”说起状态、动作与奖励的建模Q-learning 处理的问题通常建模为马尔可夫决策过程也就是当前状态只影响下一步的转移不依赖历史状态。我在实际项目中习惯用网格世界作为最小可运行样例因为它足够简单又能完整暴露 Q-learning 的所有行为特征。以一个 5x5 的网格为例智能体从左上角出发目标是到达右下角地图里有几个陷阱。状态就是智能体所在的格子编号动作是上、下、左、右四个方向奖励函数按任务需要自定义。关键点在于奖励并不等同于 Q 值。奖励是环境给的一次性反馈比如到达目标给 10踩到陷阱给 -10普通移动给 -0.1。Q 值则是“从当前格子出发执行某个动作后”未来累计奖励的折扣期望。这个建模过程有一个容易被忽视的细节普通步数的惩罚值一定要小但又不能是零。我最早偷懒把普通步奖励设为 0结果智能体学到的方法是满地图乱逛因为反正没有负奖励就算多走几步也能到终点。加上 -0.1 之后它才会主动寻找最短路径。奖励的含义越明确Q-learning 学到的策略越贴近任务本意这是一个在前面阶段就决定训练成败的设计点。2.2 Bellman 方程与 Q 值更新为什么下一步的估计也参与当前更新Q-learning 的核心更新公式如下Q[s][a] Q[s][a] alpha * (r gamma * max(Q[s1]) - Q[s][a])其中s是当前状态a是当前动作r是执行动作后拿到的奖励s1是转移后的新状态。max(Q[s1])表示新状态下所有动作中最大的 Q 值也就是假设下一步采取最优动作的价值估计。我在理解这个公式时走了很多弯路。初学者最容易犯的错是把r gamma * max(Q[s1])看成“一步奖励加上未来最大奖励”但实际它的意义是用一个已经存在的估计去修正当前估计。训练刚开始时 Q 表全是 0目标值就是r随着训练继续靠近目标的格子先获得非零 Q 值这些 Q 值会像水波一样向远处传播。因此最终每个格子都能学到“离终点越近Q 值越大”的规律策略也就是每一步都选 Q 值最大的方向。Bellman 方程之所以叫“方程”是因为它描述了一个自洽关系最优策略下当前状态的价值必须等于一步奖励加上下一个最优状态的价值。Q-learning 没有直接解这个方程而是通过不断采样试错去逼近它这也是时序差分方法的精髓。它和动态规划的区别很明显动态规划需要完整的转移概率Q-learning 只需要转移样本它和蒙特卡洛的区别在于蒙特卡洛必须等一个回合结束才更新Q-learning 每走一步就能更新一次非常适合长时间运行的在线控制任务。2.3 学习率、折扣因子与探索率三个参数分别管什么这三个参数是 Q-learning 里绕不开的旋钮它们影响的是完全不同的训练维度分别说下我在调参时的经验。学习率alpha控制每次更新的修正幅度。alpha 太大Q 值会在目标值附近来回震荡很难稳定alpha 太小更新过程会很慢而且一旦环境发生变化旧的 Q 值需要大量步数才能纠正过来。早期我做网格实验时用 0.8结果每轮结果都剧烈跳动后来降到 0.1曲线平缓了但到 3000 回合才收敛。我现在的惯例是确定性环境下用 0.2 到 0.3随机环境下先用 0.1观察训练曲线的方差再微调。折扣因子gamma代表智能体对未来奖励的重视程度。gamma 越接近 1远期奖励和近期奖励权重越接近智能体越有远见gamma 太小它只会盯住眼前几步。对于迷宫这类确定性任务0.9 是个常用起点如果环境里存在随机转移我建议不超过 0.85否则未来的噪声会被指数放大Q 值方差会很大。探索率epsilon决定智能体在每一步是选当前最优动作还是随机动作。为了学到全局最优必须保证有概率尝试非最优动作。我的做法是从 0.5 开始每 100 回合乘以 0.95最低保底到 0.05。这个“保底”很重要因为如果 epsilon 最终降到 0智能体就完全失去了探索能力一旦环境非平稳策略就会僵死。后面我会专门讲一个同学把 epsilon 线性降到 0 导致训练停滞的案例。2.4 Q-learning 的标准训练循环一回合内发生了什么把上面的概念串起来一个完整的训练循环包括初始化 Q 表为 0设置当前状态为起点在每一步用 epsilon-greedy 策略选择动作执行动作得到奖励和新状态用更新公式更新 Q 表进入下一个状态直到到达目标或陷阱结束一回合然后重复足够多的回合。这里有一个隐藏的关键点每回合内智能体看到的轨迹可能完全不同但 Q 表的更新是逐步累积的所以哪怕一开始全是随机移动只要探索足够充分最终也能学到最优策略。我习惯在训练过程中记录每个回合拿到总奖励值用它画一条学习曲线。如果曲线的整体趋势是上升的说明 Q-learning 在正常工作如果曲线纹丝不动或者在下跌那问题往往出在奖励设置、epsilon 衰减或转移逻辑上而不是算法本身。这也是我在验证新环境时第一个会去看的信号。3. 用 Python 从零写一个 Q-learning 智能体以网格世界为例3.1 定义环境用 5x5 网格模拟一个带陷阱的寻宝任务我先把网格世界环境封装成一个类这样做的好处是后续换地图、换奖励都很方便不用反复改训练逻辑。环境里定义 25 个格子编号从 0 到 24起点是 0终点是 24。陷阱放在几个固定编号上智能体掉进陷阱会立刻结束这一回合。奖励规则如下到达终点得到 10 分掉进陷阱得到 -10 分普通移动得到 -0.1 分撞墙试图走出边界同样得到 -0.1 分并留在原地。这里的撞墙不算终止只是惩罚一步相当于训练智能体尽早学会绕开边界。import numpy as np class GridWorld: def __init__(self, size5, traps[6, 12, 18]): self.size size self.start 0 self.end size * size - 1 self.traps traps self.state self.start def reset(self): self.state self.start return self.state def step(self, action): # action: 0上, 1下, 2左, 3右 row self.state // self.size col self.state % self.size if action 0: row - 1 elif action 1: row 1 elif action 2: col - 1 elif action 3: col 1 if row 0 or row self.size or col 0 or col self.size: next_state self.state # 撞墙留在原地 reward -0.1 else: next_state row * self.size col if next_state self.end: reward 10.0 elif next_state in self.traps: reward -10.0 else: reward -0.1 self.state next_state done (next_state self.end) or (next_state in self.traps) return next_state, reward, done这段代码逻辑很直白先根据动作计算新坐标再由坐标映射回一维编号。重点看撞墙的处理我把撞墙动作的下一个状态设成原状态这样智能体知道“撞击”不会有进一步转移同时也会因为 -0.1 的惩罚逐渐避开墙。奖励分支的判断顺序非常关键先判断终点再判断陷阱否则如果陷阱和终点重合智能体会得到相反信号。实际项目中这个顺序往往就是 bug 的来源我建议写环境时明确把奖励判定做成独立函数方便单元测试。3.2 实现 Q 表迭代可复制的完整代码Q 表用二维数组表示行数等于状态数列数等于动作数。这里不涉及神经网络所以直接用一个 NumPy 数组存储每个状态-动作对的 Q 值。训练主体就是一个双层循环外层跑回合数内层跑单回合的每一步。alpha 0.2 # 学习率 gamma 0.9 # 折扣因子 epsilon 0.5 # 初始探索率 epsilon_min 0.05 epsilon_decay 0.95 # 每回合衰减为原来的95% episodes 3000 env GridWorld() Q np.zeros((env.size * env.size, 4)) def choose_action(state): if np.random.uniform(0, 1) epsilon: return np.random.choice(4) else: return np.argmax(Q[state]) for ep in range(episodes): state env.reset() total_reward 0 while True: action choose_action(state) next_state, reward, done env.step(action) # Q-learning 更新公式 best_next np.max(Q[next_state]) td_target reward gamma * best_next Q[state][action] alpha * (td_target - Q[state][action]) total_reward reward state next_state if done: break # 探索率衰减但保留最低保底 if epsilon epsilon_min: epsilon * epsilon_decay if (ep 1) % 200 0: print(fEpisode {ep1}, total reward: {total_reward:.2f}, epsilon: {epsilon:.3f})这里最关键的一行是td_target reward gamma * best_next它对应前面讲的 Bellman 目标值。best_next是Q[next_state]的最大值代表从下一个状态出发能获得的最高价值估计。注意动作选择用了epsilon-greedy但更新时使用的是贪心动作的 Q 值而不是实际采集动作的 Q 值这就是 Q-learning 作为 off-policy 算法的典型特征。epsilon_decay我设置为每回合乘 0.95所以到第 200 回合时 epsilon 约为 0.5 的 0.0003 倍但因为有epsilon_min0.05的下限它不会被压到 0。保底探索的意义在于即使训练后期也有 5% 的概率尝试非最优动作这能帮助智能体在环境变化时重新找出路。3.3 训练循环中的关键参数设置与作用上面的代码里出现了四个超参数alpha、gamma、epsilon 初始值、epsilon 衰减率。我给它们定的默认值是经过多次实验的经验起点但不代表所有环境都适用。alpha0.2 意味着每次更新只向目标值移动 20%。这在确定性环境中已经够快而且不会导致 Q 值震荡。如果环境中存在随机转移比如“向上走有 10% 概率滑到左边”我会把 alpha 降到 0.1让智能体慢慢平均这些随机样本带来的噪声。gamma0.9 表示未来 10 步的奖励对当前决策的影响大约是 0.9^10 ≈ 0.35衰减较快。如果任务要求智能体为了更远的终点牺牲眼前短期收益可以调高到 0.95但要注意 gamma 越高Q 值的变化越依赖对远期状态的估计训练初期的 Q 值全是虚的过高 gamma 会导致初始阶段学得很不稳定。epsilon 从 0.5 开始在 3000 回合里衰减到 0.05前半段大部分时间都在试探环境后半段才开始利用学到的策略。如果你只想观察收敛结果把衰减率设成 0.98这样训练过程会更平滑。如果你希望智能体更早进入利用阶段可以把初始 epsilon 降到 0.3。总之这三个参数是互相影响的调整时一次只动一个否则出了问题很难定位。4. Q-learning 的五个常见坑从奖励稀疏到 Q 表爆炸4.1 现象学习曲线一直不动原因探索率归零太快解决使用衰减但保底有次帮师弟调一个倒立摆环境他设置的 epsilon 线性下降从 1.0 每回合减 0.001到第 1000 回合就严格变成 0。理论上这没问题但问题是他的环境在 2000 回合内还没学会基本动作探索空间已经锁死了。结果就是 Q 表里绝大多数状态-动作对仍然是 0智能体只会机械地按照最初的随机经验行动学习曲线在中后段完全是一条平线。原因是探索率和任务难度不匹配。Q-learning 需要足够的随机动作样本才能覆盖所有状态-动作对尤其是在奖励稀疏的环境里可能需要几千甚至上万步的随机尝试才能偶然碰到一次正奖励。如果探索窗口提前关闭等于把搜索过程截断。解决方法很简单epsilon 设置下限比如 0.05并且衰减策略从线性改成指数衰减。我习惯写成epsilon max(epsilon_min, epsilon * 0.95)这样探索比重前期下降快后期保持一个稳定底线。如果环境特别复杂还可以使用 epsilon 周期性回升的策略比如每隔 500 回合把 epsilon 重新拉高到 0.3让智能体重新探索一部分状态空间。4.2 现象Q 值越来越大不收敛原因奖励绝对值过大或折扣因子接近 1解决归一化奖励和调整 gamma训练到后期打印 Q 表发现某些值达到了几千上万而且还在不断增长。这通常有两个原因一是单步奖励设得太大比如 100加上 gamma 接近 1长期累积价值会在数值上持续膨胀二是折扣因子设成 0.99 甚至 1而环境没有终止时刻Q 值就会因为无限期累积而发散。奖励值的量级要跟 Q 值的性质匹配。Q 值是对未来总奖励的折扣求和如果单步奖励是 10gamma 是 0.9那么一个最优路径长度为 5 的状态Q 值大致在 10 * (0.9^0 0.9^1 ... 0.9^4) 左右也就是几十的量级。如果你发现 Q 值比这个估算高出几个数量级说明奖励设置或 gamma 有问题。解决方式是把奖励压缩到 -1 到 1 之间比如奖励 1.0惩罚 -1.0普通步 -0.01。这样即使 gamma 接近 1Q 值的上限也被限制在可预测的范围内。另一个常见做法是使用奖励归一化即统计最近 100 回合的平均奖励和标准差将奖励标准化后再更新。这种方法在深度 Q 网络里更常见但 Q-learning 表格版本同样适用。4.3 现象状态太多导致 Q 表过大原因表格型方法的维度灾难解决状态聚合或转向深度 Q 网络当状态是多维连续量比如机器人的关节角度、车辆的坐标与速度用表格记录是灾难级的。一个 10 维连续状态每维离散成 10 档就有 10^10 个状态每个状态 4 个动作Q 表需要存储 400 亿个浮点数显然不现实。我在一个机械臂轨迹规划任务里就栽过跟头一开始把关节角度直接离散化成整数状态维度 6每个维度取 20 档Q 表大小是 20^6 * 46.4 亿条目内存直接爆掉。后来我把连续的关节角度聚合为粗粒度区间再合并相似状态才勉强能跑。但这只是权宜之计真正靠谱的方案是用函数近似也就是把 Q(s, a) 从查表改为神经网络预测这就是深度强化学习算法的出发点。对于小规模离散状态状态聚合依然有效。你可以用人工规则把连续状态映射到若干区间比如把速度分为“慢、中、快”三档。但要注意聚合粒度不能太粗否则不同状态被合并后最优动作可能互相冲突导致 Q 值反复横跳。如果遇到这种情况就该考虑 DQN 的路线了。4.4 现象明明找到路径却绕路原因奖励设计问题解决给每步微惩罚在迷宫里智能体最终学会了从起点到终点但是走的路线明显绕了一个大弯。检查 Q 表发现它其实知道终点附近的状态有高 Q 值但从远处到终点附近的过程并不短。原因是我把每一步的奖励设成了 0只有终点给正奖励。在没有步数惩罚的情况下绕多长的路都能拿到同样的正奖励智能体就没有动机去找最短路径。这是奖励稀疏环境的典型问题也是强化学习里最容易被吐槽的“玄学”之一。解决办法有两个方向一是给每一步设一个小的负奖励比如 -0.05这样路径越长累计负奖励越多智能体自然倾向短路径二是增加引导型奖励比如每靠近终点一步就多给 0.1 分但这需要额外计算距离有可能引入局部最优让智能体困在“靠近但永远到不了”的地方。我更推荐第一种方案因为它不改变任务语义只是对步数施加了惩罚对于离散网格问题尤其好用。但要注意惩罚值不能大于正奖励否则智能体会为了减少步数而故意踩陷阱尽快终止形成另一种坏策略。一个经验是步数惩罚的绝对值应小于单步正奖励除以平均路径长度。4.5 现象训练时好时坏不稳定原因随机种子与初始化问题解决固定种子并记录随机状态同一份代码跑第一次收敛得很快第二次换台机器或者换个时间运行可能完全学不出来。这种不稳定性在黑盒调试时非常恼人。根源躲藏在随机数里动作选择用随机数状态转移可能带随机性Q 表初始值也可能随机。只要随机源不同训练的轨迹就完全不同。我现在的习惯是在训练脚本最前面固定随机种子比如设置np.random.seed(42)。固定种子带来的好处是实验可复现调参时的判断能准确归因到参数本身而不是被随机波动干扰。同时我会在训练过程中每隔一定回合数打印累积奖励和 Q 表变化及时判断是收敛还是发散。但固定种子也有坑如果种子选得不好恰好让探索轨迹非常倒霉整个训练可能错过关键状态。遇到这种情况不要急着改参数换一个种子跑一遍观察是否仍然无法收敛。如果多个种子都失败才说明问题出在奖励或环境逻辑上。我会同时记录每个种子的最终策略用多个种子取最优结果作为该参数的评估而不是凭单次结果下结论。5. 从 Q-learning 到深度学习当 Q 表变成一个神经网络5.1 为什么表格 Q-learning 在复杂状态前会失效前面说过表格方法把每个状态-动作对独立存储好处是精确、可解释坏处是完全没有泛化能力。状态稍微连续化、维度稍微提高表就会指数膨胀学不完也存不下。就算勉强离散化相邻状态之间学到的 Q 值也是隔离的一个状态的经验无法迁移到另一个类似状态导致采样效率极低。我在做行人运动预测的项目时曾尝试把行人坐标、速度、方向离散成多个区间结果状态空间轻松超过百万级。Q 表在数百万条目下更新极慢每次随机探索只能影响其中很小一部分整体收敛需要天文数字般的回合数。这个阶段深度学习是必然出路因为它能用有限参数去拟合一个函数通过神经网络对输入状态自动做特征映射从而对相近状态输出相近 Q 值。5.2 DQN 的基本结构用深度神经网络拟合 Q 函数深度 Q 网络DQN的基本思想是用一个多层神经网络替代 Q 表输入是状态的特征表示输出是每个动作对应的 Q 值。网络结构通常是全连接层输入维度等于状态特征数输出维度等于动作数。损失函数用均方误差目标是让网络输出的 Q 值逼近“即时奖励 gamma * 下一状态最大 Q 值”这个目标。训练过程有两个 Q-learning 没有的关键组件经验回放和目标网络。经验回放把每一步采样到的(state, action, reward, next_state, done)存在一个缓冲区里每次从缓冲区随机抽一个小批量样本来更新网络打乱了样本相关性让梯度更新更稳定。目标网络是另一个参数冻结的 Q 网络它的参数不随每次训练立即更新而是每隔固定步数从主网络复制一次用来计算 TD 目标。这里用目标网络是为了防止“用一个正在变化的网络去估计它自己”避免训练发散。如果你已经能跑通前面的表格 Q-learning改造到 DQN 的路径其实很清晰把Q[s,a]的读取变成Q_network(state)[a]把max(Q[s1])变成target_network(next_state)输出的最大值把更新公式从手动更新 Q 表改为计算损失并反向传播。动作选择依然可以用 epsilon-greedy探索率和衰减策略保持不变。实际落地时深度学习环境配置通常需要安装 PyTorch 或 TensorFlow并确认 GPU 驱动是否可用但单卡 CPU 也能跑小规模 DQN 训练只是速度慢几倍。5.3 用深度强化学习算法改造已有网格世界代码我在自己项目里写过一个最小改造沿用前面 5x5 网格世界但把即时 Q 表替换成一个三层全连接网络。状态输入不再用格子编号而是一个 one-hot 向量也就是长度为 25 的向量当前状态对应的位置为 1其余为 0。网络隐藏层各 16 个神经元输出 4 个动作的 Q 值。训练时每 200 步从经验池里采样 32 个样本更新一次网络。关键代码如下我贴的是训练循环中一小段展示目标 Q 的计算逻辑# 假设 q_net 和 target_net 是两个结构相同的网络 # replay_buffer 是一个存了 (state, action, reward, next_state, done) 的列表 batch_states np.array([t[0] for t in batch]) batch_actions np.array([t[1] for t in batch]) batch_rewards np.array([t[2] for t in batch]) batch_next_states np.array([t[3] for t in batch]) batch_dones np.array([t[4] for t in batch]) # 当前状态下所有动作的 Q 值 current_q q_net(batch_states) # 取出实际执行动作的 Q 值 current_q current_q.gather(1, batch_actions.unsqueeze(1)).squeeze(1) # 目标网络计算下一状态的最大 Q next_q target_net(batch_next_states).max(1)[0] # 终止状态下没有未来奖励 target_q batch_rewards (1 - batch_dones) * gamma * next_q loss mse_loss(current_q, target_q.detach()) optimizer.zero_grad() loss.backward() optimizer.step()我特别强调batch_dones的使用当回合终止时next_state已经不存在未来奖励目标 Q 值就只有即时奖励。不少初学 DQN 的同学忘记乘(1 - batch_dones)导致终点状态的 Q 值被严重高估。这种错误的表现是学习曲线前期很好后期突然发散因为 Q 值在终点附近被不存在的未来奖励拉偏了。改造完成后网络要通过梯度下降学到的 Q 值理论上和表格版本收敛到同一套最优策略但泛化能力更强。实际项目中神经网络会带来新的问题比如超参数增多、训练不稳定、复现困难。所以我的建议是如果任务的状态空间能控制在几千以内优先用表格 Q-learning只有状态维度真正威胁到内存和采样效率时才值得投入深度学习改造毕竟深度强化学习算法的调试成本要高出不止一个量级。6. 验证你的 Q-learning 是否真学会了三条检验手段训练完成后不要只看最终累积奖励指标那容易被局部最优骗过。我的习惯是同时做三件事画出学习曲线、打印贪心策略、可视化 Q 值热力图。学习曲线看趋势如果总奖励随回合上升且后期稳定在一个较高值说明算法收敛。如果曲线反复震荡检查 alpha 是否过大、epsilon 是否没有保底。策略可视化则是把每个状态上np.argmax(Q[state])对应的方向画成箭头一眼能看出智能体是否在朝目标移动。这种方法最直接能立刻暴露“绕路”“原地踏步”“陷阱附近处理错误”等问题。Q 值热力图把每个状态的最大 Q 值涂成颜色深浅目标终点的 Q 值最高起点最低中间应该平滑过渡如果出现明显断层比如某些格子 Q 值异常高很可能是奖励设计或转移逻辑的 bug。我也会做稳定性验证用同一个训练好的 Q 表重新启动五个新环境实例让智能体按照贪心策略跑 100 次统计成功率和平均步数。成功率 100% 且步数接近理论最短路径才算合格。如果成功率偏低我一般会先怀疑 epsilon 保底是不是被减没了。这类验证步骤虽然简单却能在调参时省下大量时间我现在的习惯是每轮完整训练后都必须跑一遍否则不看训练曲线。从表格 Q-learning 到深度 Q 网络的落地最重要的事情就是保持怀疑算法不收敛时先检查环境奖励逻辑再检查探索策略最后才去调整网络结构。我用这套排查顺序解决过不少翻车现场也把成功的经验沉淀成了固定的参数模板。希望帮到你。本文还有配套的精品资源点击获取
返回列表