Q-learning强化学习算法入门:理解Q表、更新公式与最小实现
上个月朋友递给我一个半成品的小游戏Demo角色在不到一百格的地图里乱转规则很简单但就是走不到终点。我试过写死规则换一张地图就崩试过用搜索算法环境稍微改一下就跑不动。最后把角色换成强化学习算法里的Q-learning训练了大概两分钟角色自己就学会了绕开陷阱、直奔目标。这件事给我的感触挺深很多看起来“需要智能”的问题其实并不需要多复杂的手段一张Q表加一个更新公式就能解决。这篇文章我就用最直白的话把Q-learning从头到尾拆开讲清楚并且给你一份能直接跑通的最小实现。它是什么、能解决什么问题、有哪些坑、学到什么程度该换更高级的算法都会讲到。不管你是刚接触强化学习的新手还是想快速评估这个算法能不能用在项目里的开发都应该能从里面拿到一些实在的东西。1. 为什么是Q-learning它能解哪类问题1.1 一个“事后才知道对错”的学习范式先想一个问题一个角色在迷宫里走它并不知道哪一步是“正确”的。除非你亲自告诉它“先左转再直行”否则每一步都不存在标准答案。但反过来想它的目标很明确——到达终点。这个矛盾点就是强化学习与普通监督学习最大的区别。监督学习里你有输入和标签模型负责学习输入到标签的映射。可在这里你没有办法给角色标注“这一步动作的标签是什么”因为一个好的动作要放在整个行动序列里看才成立现在多绕两步可能换来后面更快的路径现在贪图近路可能一头撞进陷阱。Q-learning就是专门处理这类“序列决策问题”的强化学习算法。它不关心当前这一步是不是“最像人类”只关心这一步往后的累计收益能不能最大化。它也不需要你对环境有任何内部模型——不需要知道地图的公式、障碍的坐标分布只需要能与环境交互拿到反馈。所以它属于model-free免模型方法。1.2 适合Q-learning的场景特征不是所有问题都适合用Q-learning我在实际判断一个需求时通常看三个特征第一状态空间是离散且有限的。比如一个2D网格、一个棋类游戏的局面、一个工控系统里的离散档位这类场景天然适合用表格记录学习结果。如果状态是连续坐标、多关节角度这类连续变量用Q-learning会立刻撞上“状态爆炸”问题后面第5节会详细说。第二动作空间是离散的。上下左右、出击或防守、购买或出售这些都可以。连续动作控制比如油门踩多少不是Q-learning的强项。第三环境可以接受大量试错。Q-learning靠不断探索积累数据如果你的环境跑一次要十分钟、成本极高那要谨慎。它更适合仿真环境、游戏、低成本的模拟系统。我用一个表格来对比Q-learning和另外两类常见做法方便你判断方法类别依赖环境模型适合任务典型痛点基于规则不需要环境固定、逻辑明确规则写不完环境一变就崩搜索算法需要短路径、状态可穷举大状态空间性能差Q-learning不需要离散状态、可交互状态空间大时Q表膨胀我在Demo里遇到的其实就是规则写不完的问题地图复杂度稍微提高分支逻辑就指数级膨胀而Q-learning完全不吃这一套它只在乎“在这个位置做这个动作长期回报是高还是低”。2. 核心直觉Q表、奖励与“未来的钱要打折”2.1 Q值到底是什么如果你打开任何一本强化学习教材都会看到“Q函数”和“Q值”。我第一次学的时候也懵后来发现把Q值理解成一笔“长期收益账”就顺了。假设你现在处于状态s准备执行动作a。如果从这一刻开始你后续每一步决策都很合理那么你最终能积累多少奖励这个“最终能积累多少”就是Q(s, a)。比如网格世界里有一个位置往上走会被陷阱弹回往右走能通向终点。那么在这个位置“往上”的Q值会很低“往右”的Q值会很高。角色做完训练后决策方式非常简单看当前状态选Q值最大的那个动作。所以整个Q-learning的难点就变成了两件事怎么定义“长期收益”以及怎么不断修正对这套收益的估计。2.2 Q-learning的更新公式是怎么来的Q-learning的核心更新公式长这样Q(s, a) ← Q(s, a) α [ r γ · max_{a} Q(s, a) − Q(s, a) ]先别急着背公式。我们来逐项拆。Q(s, a) 是旧账本上的值表示“在状态s做动作a”原本估计的长期收益。r 是这一步立刻拿到的奖励是即时反馈。比如走到陷阱给-10走到终点给20每走一步给-1表示时间成本。γ称为衰减因子范围在0到1之间控制“未来的奖励值多少钱”。形象点说今天的100块比十年后的100块更值钱未来的奖励需要“打折”后才能和眼前奖励放在一起比较。max_{a} Q(s′, a′) 表示进入下一个状态s′之后后续所有可行动作里最好的那个收益。它代表“如果后续做得不错理想情况下还能赚多少”。把这几项加起来r γ · max_{a} Q(s′, a′)就是事后对这一轮的“目标值”——你实际走了一步后回头计算这个位置应该值多少。然后拿目标值和旧值做差用学习率α控制修正幅度。如果还不直观可以把它想象成一家店的选址评估。你评估一块地段好不好不能只看今天路过的客流量即时奖励r还要看它未来长期能带来多少生意未来累计收益。你一边开一边根据实际经营情况修正评估最终得到一个靠谱的“地段Q值”。2.3 为什么非要用max离轨学习的乐观底色这里有一个非常关键的设计很多人会忽略更新目标里用的是 max_{a}而不是“按当前策略会选择的动作”的价值。这意味着Q-learning在用“事后最优”来教当前的自己。举个例子。某状态s′下旧策略有80%概率选择动作A只有20%概率选择动作B。假如A的Q值很低B的Q值很高那Q-learning在更新前一个状态时还是会用B的高值作为目标。也就是说它假设“就算我平时很少选B但我知道B一旦被选中收益就是高”于是会把这个高值向上传递给前一步。这个特性在专业上叫off-policy——学习时用的行为策略和最终要学的目标策略可以不一样。带来的好处是探索效率高、可以用别人的数据训练坏处是它天生乐观容易高估真实收益。这算是Q-learning家族一个暗藏的缺陷后面引申出Double DQN就是为了修正它这里先埋个伏笔。2.4 epsilon-greedy与探索利用平衡训练前期角色必须足够“莽”。如果它只按已有Q表选动作刚开始Q表全空所有动作价值都是0角色会一直原地打转永远学不到新东西。所以我需要让它在大多数时候按“当下最看好”的动作走同时留一小部分概率随机探索。这就是epsilon-greedy策略以ε的概率随机选动作探索以1−ε的概率选择当前Q值最大的动作利用。训练初期ε可以设成1.0让角色像无头苍蝇一样乱跑随着训练推进ε逐渐衰减到0.01左右让角色越来越依赖学到的经验。一句话总结q-learning的策略逻辑先用随机性积累经验再用确定性收割经验。3. 从零实现一个会学习的网格世界智能体3.1 环境定义与奖励设定这一节我们直接动手写一个最小可运行案例网格世界智能体。环境是一个5×5的格子起点在左上角 (0, 0)终点在右下角 (4, 4)到达即成功奖励20三个陷阱踩中立即结束奖励-10普通一步奖励-1用于施加“尽快到达”的压力为什么普通步数要给负奖励这是很多新手容易忽略的点。如果你把普通步数奖励设成0角色就会找到一个“陷阱和终点都不碰”的死循环路径因为每步不亏不赚绕圈反而不会结束。给一个小的负奖励就是告诉角色时间是有成本的绕远路要付出代价。3.2 完整代码环境、Q表与训练主循环我不打算在这里贴一个几百行的项目框架而是把核心代码拆成三块环境类、Q表操作、训练主循环。你把这几个代码块拼到一起就能跑。首先是环境类import random class GridWorld: def __init__(self, size5): self.size size self.start (0, 0) self.goal (size - 1, size - 1) self.traps [(1, 3), (3, 3), (2, 2)] self.actions [(0, 1), (0, -1), (1, 0), (-1, 0)] # 右、左、下、上 self.state self.start def reset(self): self.state self.start return self.state def step(self, action_idx): x, y self.state dx, dy self.actions[action_idx] nx, ny x dx, y dy # 撞墙时原地不动也算一步 if nx 0 or nx self.size or ny 0 or ny self.size: nx, ny x, y next_state (nx, ny) reward -1 done False if next_state in self.traps: reward -10 done True elif next_state self.goal: reward 20 done True self.state next_state return next_state, reward, done然后是Q表。这里我用字典键是(状态, 动作)的组合值就是Q值。用字典的好处是代码简单不用预先开一个大数组缺点后续讲Q {} def get_q(state, action): return Q.get((state[0], state[1], action), 0.0) def set_q(state, action, value): Q[(state[0], state[1], action)] value训练主循环如下alpha 0.1 # 学习率 gamma 0.9 # 衰减因子 epsilon 1.0 # 初始探索率 epsilon_min 0.01 # 最小探索率 decay 0.995 # 每轮衰减系数 episodes 800 env GridWorld() for ep in range(episodes): state env.reset() total_reward 0 while True: # epsilon-greedy 选动作 if random.random() epsilon: action random.choice(range(4)) else: q_values [get_q(state, a) for a in range(4)] max_q max(q_values) # 多个动作同值时随机选一个避免固定偏向第一个 best_actions [a for a in range(4) if get_q(state, a) max_q] action random.choice(best_actions) next_state, reward, done env.step(action) # Q-learning 更新公式 q_next_max max([get_q(next_state, a) for a in range(4)]) td_target reward gamma * q_next_max * (0 if done else 1) current_q get_q(state, action) set_q(state, action, current_q alpha * (td_target - current_q)) state next_state total_reward reward if done: break epsilon max(epsilon_min, epsilon * decay)有两个细节我特意在代码里处理了也是容易写错的地方。第一个是终止状态的处理。当done为True时下一个状态其实已经结束不存在“未来收益”所以td_target里的未来部分要乘0。如果不做这一步智能体从陷阱和终点学到的信息会被“未来收益”污染导致陷阱看起来没那么危险终点看起来没那么诱人。第二个是Q值相同时的随机选择。如果直接用argmax训练初期所有Q值都是0角色永远朝第一个动作走探索会非常低效。用“从所有最优动作里随机挑一个”这个小技巧能明显提升训练稳定性。3.3 训练完怎么看策略可视化与收敛曲线训练结束后把Q表转成策略图我可以把每个格子里Q值最大的动作画成箭头arrows {0: →, 1: ←, 2: ↓, 3: ↑} size 5 for i in range(size): line for j in range(size): if (i, j) env.goal: line G elif (i, j) in env.traps: line X else: q_values [get_q((i, j), a) for a in range(4)] best q_values.index(max(q_values)) line arrows[best] print(line)跑出来的结果大致会是一个从起点指向目标的有效路线。我自己跑完这组参数得到的策略大概是这样→ → → ↓ ↓ ↓ → X ↓ ↓ → → X → ↓ X → → → ↓ → → → → G注意看角色在危险点附近会自发绕开比如第一行第三列它选择继续向右是因为右边的路径规避了中间那个陷阱第二行第二列选择向右而不是直行冲进左侧区域是因为左侧区域靠近陷阱。这些行为并没有任何人显式教它都是Q表在大量试错中沉淀下来的。训练过程中还有一个直观指标每轮的总奖励。前期因为探索随机、频繁踩陷阱总奖励可能是-100甚至更低训练到200轮以后奖励会逐渐稳定在十几到二十几——此时角色基本就是最短路径直冲终点。我自己习惯画一条奖励随轮数的滚动平均曲线比看单轮曲线平滑得多更容易判断是否收敛。4. 参数选择与训练中常见的坑4.1 学习率α太大震荡α太小学不动学习率α控制每次更新时修正旧估计的幅度。α0.1是我在网格世界里常用的起点但不是所有问题都适用。α偏大时更新步长过猛。前期可能学得快但后期会看到一个现象Q值在正确值附近来回震荡策略图每次打印都不一样。我试过把α调到0.9发现总奖励曲线像锯齿一样怎么都压不平。α偏小时学习速度极慢。比如α0.01跑完同样800轮角色还在乱走因为每次从一次经验里吸取的教训太少Q表要磨很久才能积累出可靠信号。经验法则先从0.1开始试跑几百轮看奖励曲线幅度如果震荡厉害就往0.05、0.03降如果曲线平滑但上升很慢就往0.2、0.3升。没有万能值环境复杂度不同合适区间差别很大。4.2 衰减因子近期奖励与远期目标的权重γ直接决定智能体是“近视”还是“远视”。γ0时智能体只看即时奖励完全不管后续影响γ越接近1未来的奖励越值钱智能体越愿意为长期目标放弃眼前小利。网格世界例子中γ0.9已经够用因为终点奖励最多往后推十几步就能拿到0.9的累积衰减不会把它折得太低。但如果你的任务需要长距离规划比如一个动作的回报在50步之后才兑现γ最好调大到0.99甚至更高否则远期回报折到眼前几乎等于0智能体会直接无视长期目标。这里有个隐藏风险γ越接近1越容易导致Q值估计在无限步任务中发散。理论上有限步且奖励有界时问题不大实际中如果训练不收敛检查第一眼就应该是γ和学习率。4.3 探索率衰减什么时候该“不再乱试”探索率ε的衰减策略有讲究。线性衰减容易理解每轮减少固定值指数衰减就是代码里那种“每轮乘一个小于1的系数”前期降得快、后期趋稳。关键问题在于ε降得太快智能体还没看够世界就开始“固执己见”会收敛到一个局部最优策略比如永远绕着某条安全但不短的路走ε降得太慢后期智能体已经学得差不多还是在大量随机乱走浪费时间。推荐的做法是分阶段观察奖励曲线。曲线还在快速上升时说明探索还有价值别急着把ε压到最低曲线已经平稳时ε就应该降到接近0.01左右让策略稳定下来。我有时候会刻意在前200轮保持ε0.3以上让智能体充分遍历状态后续再快速衰减。4.4 奖励设计别让智能体钻空子刚开始做强化学习的人很容易犯一个错奖励设得太“好心”反而培养出奇怪行为。我见过一个极端的例子某开发者希望智能体靠近目标于是给每个“离目标近一步”的动作额外1结果智能体学会在一个离目标很近但永远到不了的位置反复横跳刷分。这类问题在圈里叫reward hacking。防范思路就一句话奖励只描述“真正想要的最终结果”中间过程的“引导性奖励”越少越好。我的网格世界里只用了“步数-1、陷阱-10、终点20”这三级奖励没有额外的“走近鼓励”倒逼智能体自己学习路径规划。当你发现智能体行为让你觉得“奇怪但分数很高”优先怀疑奖励函数而不是怀疑学习算法。智能体永远在忠实优化你给的奖励它没有任何动机“做正确的事”。4.5 乐观初始化这个小技巧大多数Q-learning教程会把Q表初始值设成0但这其实有一个隐藏问题探索初期新动作带来的负反馈会被正常吸收但如果某些动作一直没被尝试它们的Q值一直为0而其他已被试过的动作Q值已经被经验修正成负数那么“未试过的动作”反而看起来比“已试过的动作”更好角色会倾向于不断去碰这些未探索区域——在网格世界里不一定坏事但如果环境里有很强的负反馈区可能会让角色反复进入陷阱。反过来的做法叫乐观初始化把Q表初始值设成一个正数比如10。这样未被探索的动作初始价值很高智能体天生想去尝试随着尝试次数增加Q值被真实奖励修正回合理区间。在我自己的项目中乐观初始化经常能让前期探索更高效、更容易发现全局最优路径。代价是改动确实很小但很多人不知道这个技巧所以我特别提一句。如果你发现训练前期智能体过于“恋旧”死守一小片区域试着把Q表初始值从0改成正数。5. Q表格方法的天花板与下一步思路5.1 表格为什么撑不住大状态空间网格世界5×5状态只有25个动作4个Q表一共100个键值内存开销完全可以忽略。但如果换成10×10的网格状态就有100个换成100×100的网格状态就有10000个Q表也有几万个键值还是能撑。真正麻烦的是从离散格子走向连续空间。想象一个自动驾驶场景车速是一个连续量可以取50.1km/h、50.2km/h、50.11km/h……无限多个值。不进行离散化的话一张表根本列不完所有状态。即使强行离散化成0.1km/h一档变量一旦多起来表格规模就是指数爆炸10个连续变量、每个离散成10档就是10^10个状态地球上没有任何表格放得下。这就是Q-learning这类表格方法的天花板它不具备泛化能力。它只能记住“见过”的状态无法把“在A位置学到的经验”迁移到“类似的B位置”。5.2 从Q表到参数化Q函数要突破这个天花板思路是用一个函数f(s, a)来替代表格把“记住所有状态”变成“学会一种映射关系”。常用的做法是用神经网络拟合Q函数输入状态和动作输出对应的Q值。更新的时候不再修改表格里的某个格子而是用梯度下降调整网络参数让预测的Q值更接近td_target。这就是深度Q网络DQN的基本思路。严格来说DQN并不是一个新算法而是“用神经网络做函数近似”的Q-learning。它保留了Q-learning的更新逻辑和off-policy特性只是在表示Q函数的方式上换了一个更强的工具。从网格世界迁移到DQN时你还会额外接触经验回放replay buffer和目标网络target network这两个关键机制它们分别用来打破数据相关性、让训练目标更稳定。这些都是顺着Q-learning的短板一步步补出来的。5.3 该不该继续用Q-learning工程决策清单我经常被问“我要不要直接上DQN”我的建议很直接先在Q-learning上验证环境是否可行再决定往哪走。你可以按这个清单来判断状态空间能压缩到几十万以内、动作离散、环境交互成本低直接用Q-learning简单、稳定、可解释性强。状态空间大但可以合理离散化、奖励比较稀疏先试Q-learning加乐观初始化和精细的探索率调度。状态本身就是图像、语音、连续量别指望Q表直接上参数化Q函数系列方法。训练数据中需要处理长时间依赖Q-learning的max估计会偏乐观后期可以研究带修正的变体。网格世界里我至今仍然觉得Q-learning是最好的入门算法不是因为最简单虽然它确实简单而是因为它把强化学习的核心机制全部串了一遍奖励、价值、探索、利用、时序差分更新。这些概念你吃透了后面学策略梯度、Actor-Critic、DQN都会快很多。再说回文章开头那个Demo。后来我把网格世界的代码稍作修改换成地图文件驱动的环境Q表还是那张表训练逻辑一行没动。这让我对“先跑通最小实现再扩展”这件事更有体会——很多项目其实不需要一开始就上复杂方案先把最小闭环跑起来再把瓶颈处的轮子换成更好的才是真实做项目的节奏。Q-learning的代码我已经全部贴在前面了如果你正想动手试试建议先原样跑一遍再把格子改成6×6、把陷阱位置换一换观察策略图怎么变化。自己亲手调过一轮参数之后你对这张Q表的信任度和理解深度会和只看教程完全不同。