ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

80行Python实现Q-Learning:从零理解强化学习核心算法

2026/8/6 2:24:10 拓冰建站 浏览量
80行Python实现Q-Learning:从零理解强化学习核心算法

1. 项目概述:从“人狗大作战”到理解智能决策

最近在社区里看到不少朋友在讨论用Python写游戏,比如那个挺火的“人狗大作战”的变体,很多代码背后其实都藏着同一个核心思想:如何让程序自己学会做决策。这让我想起了刚入门机器学习那会儿,觉得“强化学习”这个词儿特别高大上,好像离我们很远。其实不然,你看游戏里的AI自动寻路、自动战斗,甚至是量化交易里程序自己调整买卖策略,底层逻辑都可能和它有关。而Q-Learning,可以说是打开强化学习大门最直接、最经典的一把钥匙。

简单来说,Q-Learning是一种无模型的强化学习算法。“无模型”意味着智能体不需要事先知道环境的运作规则(比如游戏地图全貌、物理引擎公式),它通过不断地试错,自己摸索出一套在什么状态下、该做什么动作能获得最大长远回报的经验表。这个经验表,就是我们常说的Q表。你可以把它想象成一个打游戏的经验本:在“遇到Boss”(状态S)时,你尝试了“放大招”(动作A),结果“赢了”(奖励R),并且发现“Boss残血时靠近攻击”下次可能更赚(更新对未来回报的估计)。你把这条经验记在本子上,下次再遇到类似情况,翻翻本子就知道怎么做了。Q-Learning干的就是这个“记经验”和“更新经验”的活儿。

为什么80行Python代码就能实现它?因为其核心思想非常简洁优美:用一张表格(Q表)存储所有“状态-动作对”的价值,通过一个迭代更新的公式,让这张表越来越接近最优决策。它不依赖复杂的神经网络(那是Deep Q-Learning的事了),纯用矩阵和循环就能说清楚,特别适合初学者理解强化学习的精髓。无论你是想为小游戏添加一个会学习的AI,还是理解自动化决策系统的原理,从Q-Learning入手都是绝佳的选择。接下来,我就带你彻底拆解这个算法,并用最纯粹的Python代码实现它,你会发现,看似神秘的智能学习,其基础构建块如此清晰易懂。

2. Q-Learning核心原理拆解:与“摸石头过河”的类比

要理解Q-Learning,我们需要先搞懂几个关键概念,它们共同构成了一个名为**马尔可夫决策过程(MDP)**的框架。别被名词吓到,我们用一个“摸石头过河”的游戏来类比:

  • 智能体 (Agent):就是过河的你。
  • 环境 (Environment):就是那条河,以及河里分布不均匀、可能滑动的石头(状态)。
  • 状态 (State, s):你当前所在的那块石头。比如“3号石头上”。
  • 动作 (Action, a):你能做的事。比如“向前跳一块石头”、“向左跳一块石头”、“原地不动”。
  • 奖励 (Reward, R):环境对你动作的即时反馈。比如“成功跳到下一块石头”奖励+10,“踩空落水”奖励-50,“原地不动”奖励-1(鼓励你前进)。
  • 策略 (Policy, π):你过河的方法。可以是一张地图(知道哪块石头稳),也可以是根据当前感觉(状态)临时决定往哪跳。Q-Learning的目标就是学出一个最好的策略。
  • Q值 (Q-value, Q(s, a))这是核心中的核心。它不代表即时奖励,而是代表在状态s下,选择动作a,并且此后一直按照最优策略行动,所能获得的所有未来奖励的总和(考虑折扣)。它衡量的是一个动作的长期价值。比如,虽然从当前石头跳到旁边那块小石头(动作a)有点晃(即时奖励R不高),但从那块小石头能一步跳到对岸(未来回报高),那么这个动作的Q值就会很高。

Q-Learning的更新公式,就是其学习的灵魂:Q(s, a) = Q(s, a) + α * [ R + γ * max(Q(s’, a’)) - Q(s, a) ]

这个公式怎么理解?我们一步步拆:

  1. R:在状态s下执行动作a后,环境给的即时奖励
  2. γ * max(Q(s’, a’)):这是未来回报的估计s’是执行动作后到达的新状态。max(Q(s’, a’))代表在新状态s’下,所有可能动作中最大的Q值(即认为在s’后会采取最优动作)。γ是折扣因子(0≤γ<1),意味着未来的奖励没有眼前的奖励值钱,离得越远,打折越狠。
  3. R + γ * max(Q(s’, a’)):可以理解为对“在状态s下执行动作a”这件事的新的、更全面的价值评估目标,它结合了即时奖励和未来最优路径的折现价值。
  4. [目标 - 当前估计]:即[R + γ * max(Q(s’, a’)) - Q(s, a)],这被称为时序差分误差。它衡量了我们当前的Q值估计和新的目标值之间有多大差距。
  5. α:学习率(0<α≤1)。这个误差不会100%地被采纳,而是按一定比例α来更新当前的Q值。如果α=1,则直接用目标值替换旧值;如果α较小,则缓慢地向目标值靠近。这有助于学习过程更稳定。

注意:折扣因子γ是平衡“短视”与“远见”的关键。γ接近0,智能体变得非常短视,只在乎眼前一步的奖励;γ接近1,智能体非常有远见,会为长远的高回报牺牲即时利益。在“过河”游戏里,设置一个合理的γ(如0.9),能让智能体愿意为了最终过河的大奖励(+100),而忍受中途一些小心翼翼(奖励为0或小负值)的步骤。

这个更新过程是离线策略的:它学习的是最优策略的Q值(通过max(Q(s’, a’))),但实际探索环境时使用的行为策略可以是不同的(比如ε-greedy策略,下面会讲)。这意味着它可以在“随机瞎逛”的过程中,不断更新和逼近那个“最优路径”的知识。

3. 算法流程与关键设计抉择

理解了核心公式,我们来看Q-Learning完整的算法步骤。你会发现,代码实现几乎就是对这个步骤的直译。

3.1 标准Q-Learning算法步骤

  1. 初始化:初始化Q表Q(s, a),通常全部设为0。设定学习率α,折扣因子γ,探索率ε,总训练回合数episodes
  2. 对于每个训练回合: a.初始化状态:环境重置,智能体获得初始状态s。 b.当状态s不是终止状态时,循环: i.动作选择:根据当前Q表和状态s,使用某种策略(如ε-greedy)选择一个动作a。 ii.执行动作:在环境中执行动作a,观察到新的状态s’和即时奖励R。 iii.Q值更新:使用公式Q(s, a) = Q(s, a) + α * [ R + γ * max(Q(s’, a’)) - Q(s, a) ]更新Q表。 iv.状态转移:将当前状态更新为新状态,s = s’。 c. 回合结束。

3.2 核心设计:探索与利用的权衡(ε-greedy策略)

这是实现中第一个关键点。如果智能体总是选择当前Q表认为最好的动作(贪婪策略),它可能很快陷入一个局部最优解,而永远发现不了真正更好的路径。就像你过河每次都跳最稳的那块石头,可能永远到不了对岸,因为中途需要冒险跳一块有点滑但关键的石头。

因此,我们引入ε-greedy策略

  • 以概率ε(探索率),随机选择一个动作(探索:尝试新可能)。
  • 以概率1-ε,选择当前状态下Q值最大的动作(利用:使用已知最佳知识)。

通常,训练初期ε设置得较高(如0.9),鼓励多探索;随着训练进行,逐渐衰减ε(如每个回合乘以0.995),让智能体后期更多地利用学到的知识。

3.3 环境设计:以“格子世界”为例

为了用80行代码清晰展示,我们需要一个极简的环境。经典的“格子世界”再合适不过:

  • 状态:网格中的每个格子坐标,如(0,0),(0,1)等。
  • 动作:上、下、左、右四个移动动作。
  • 奖励:到达目标格子获得大奖励(如+100),掉入陷阱格子获得大惩罚(如-100),每走一步消耗小惩罚(如-1,鼓励尽快到达目标)。
  • 终止状态:目标格子和陷阱格子。

这个环境简单到可以用一个二维数组表示,但其包含了强化学习的所有要素:状态空间、动作空间、奖励函数、终止条件。我们将基于这个环境进行实现。

4. Python实现详解:逐行构建智能体

下面,我们开始用Python实现一个在4x4格子世界中学习的智能体。我会将代码分成几个部分,并详细解释每一行。

4.1 环境搭建

首先,我们定义一个简单的网格世界环境。

import numpy as np class GridWorld: """一个简单的4x4格子世界环境""" def __init__(self): self.size = 4 self.start = (0, 0) # 起点 self.goal = (3, 3) # 终点,奖励+100 self.trap = (1, 1) # 陷阱,奖励-100 self.state = self.start def reset(self): """重置环境到起点""" self.state = self.start return self.state def step(self, action): """ 执行动作 :param action: 0:上, 1:下, 2:左, 3:右 :return: 下一个状态, 即时奖励, 是否终止 """ x, y = self.state # 定义动作效果 if action == 0: # 上 x = max(x - 1, 0) elif action == 1: # 下 x = min(x + 1, self.size - 1) elif action == 2: # 左 y = max(y - 1, 0) elif action == 3: # 右 y = min(y + 1, self.size - 1) self.state = (x, y) # 计算奖励 if self.state == self.goal: reward = 100 done = True elif self.state == self.trap: reward = -100 done = True else: reward = -1 # 每走一步的代价 done = False return self.state, reward, done def get_all_states(self): """返回所有可能的状态列表,用于初始化Q表""" states = [] for i in range(self.size): for j in range(self.size): states.append((i, j)) return states

实操心得:在自定义环境中,step函数的设计至关重要。奖励reward的设置是引导智能体行为的“指挥棒”。这里我们使用稀疏奖励(只在终点和陷阱有较大数值),并配合每步-1的生存代价。这种设置非常经典,它鼓励智能体尽快找到目标,同时避开陷阱。你可以通过调整这些奖励值来 dramatically 改变智能体的学习行为。

4.2 Q-Learning智能体实现

接下来是核心的智能体类,它包含Q表和学习逻辑。

class QLearningAgent: """Q-Learning智能体""" def __init__(self, actions, learning_rate=0.1, discount_factor=0.9, epsilon=0.9): """ 初始化 :param actions: 动作列表 [0,1,2,3] :param learning_rate: 学习率 alpha :param discount_factor: 折扣因子 gamma :param epsilon: 初始探索率 """ self.actions = actions self.lr = learning_rate self.gamma = discount_factor self.epsilon = epsilon self.epsilon_decay = 0.995 # 探索率衰减因子 self.epsilon_min = 0.01 # 最小探索率 self.q_table = {} # 使用字典存储Q表,键为(state),值为一个列表对应各个动作的Q值 def init_q_table(self, states): """初始化Q表,所有Q值为0""" for state in states: self.q_table[state] = [0.0] * len(self.actions) def choose_action(self, state): """ 根据epsilon-greedy策略选择动作 """ if np.random.uniform(0, 1) < self.epsilon: # 探索:随机选择动作 action = np.random.choice(self.actions) else: # 利用:选择当前状态Q值最大的动作 state_q = self.q_table[state] # 处理多个动作Q值相同的情况:随机选一个 max_q = max(state_q) count = state_q.count(max_q) if count > 1: best_actions = [i for i in range(len(self.actions)) if state_q[i] == max_q] action = np.random.choice(best_actions) else: action = state_q.index(max_q) return action def learn(self, state, action, reward, next_state, done): """ 执行Q-Learning更新 """ current_q = self.q_table[state][action] if done: # 如果是终止状态,则没有下一个状态的最大Q值 target = reward else: # 非终止状态,计算目标值 next_max_q = max(self.q_table[next_state]) target = reward + self.gamma * next_max_q # Q值更新公式 self.q_table[state][action] += self.lr * (target - current_q) # 衰减探索率,但不低于最小值 if self.epsilon > self.epsilon_min: self.epsilon *= self.epsilon_decay

关键代码解析

  1. Q表结构:我们使用Python字典q_table来存储Q值。键是状态(如(0,0)),值是一个长度为4的列表,分别对应[上,下,左,右]动作的Q值。这种结构比二维数组更灵活,易于处理非网格或状态空间很大的情况。
  2. 动作选择choose_action函数实现了ε-greedy策略。注意处理了多个动作Q值相同的情况,此时随机选择一个,避免算法陷入固定的选择顺序。
  3. 学习更新learn函数是算法核心。它根据当前状态、动作、奖励、下一个状态,计算目标值并更新Q表。特别注意对done(终止状态)的处理:在终止状态,没有下一个状态,因此未来回报为0,目标值就是即时奖励reward
  4. 探索率衰减:在每次学习后,我们对epsilon进行衰减(乘以epsilon_decay),并设置一个下限epsilon_min。这是为了让智能体在训练初期充分探索,在后期稳定利用学到的知识。衰减策略可以调整,比如线性衰减或指数衰减,这里用的是指数衰减。

4.3 训练循环与主程序

最后,我们将环境和智能体组合起来,进行训练。

def train(episodes=500): """训练函数""" env = GridWorld() agent = QLearningAgent(actions=[0, 1, 2, 3], learning_rate=0.1, discount_factor=0.9, epsilon=0.9) # 获取所有状态并初始化Q表 all_states = env.get_all_states() agent.init_q_table(all_states) success_count = 0 # 记录成功到达终点的次数 steps_per_episode = [] # 记录每回合步数 for episode in range(episodes): state = env.reset() total_reward = 0 steps = 0 done = False while not done: # 智能体选择动作 action = agent.choose_action(state) # 环境执行动作,返回反馈 next_state, reward, done = env.step(action) # 智能体学习 agent.learn(state, action, reward, next_state, done) state = next_state total_reward += reward steps += 1 # 安全措施:防止无限循环 if steps > 100: break steps_per_episode.append(steps) if env.state == env.goal: success_count += 1 # 每100回合打印一次进度 if (episode + 1) % 100 == 0: avg_steps = np.mean(steps_per_episode[-100:]) success_rate = success_count / 100 if episode >= 99 else success_count / (episode + 1) print(f"Episode {episode+1}, Avg Steps: {avg_steps:.2f}, Success Rate: {success_rate:.2%}, Epsilon: {agent.epsilon:.3f}") success_count = 0 # 重置计数 print("\n训练结束!") # 打印最终策略(每个状态下选择的最优动作) print("\n学习到的最优策略(箭头表示动作方向):") arrow_map = {0: '↑', 1: '↓', 2: '←', 3: '→'} for i in range(env.size): row_str = "" for j in range(env.size): state = (i, j) if state == env.goal: row_str += " G " elif state == env.trap: row_str += " T " else: action = agent.choose_action(state) # 此时epsilon应很小,接近贪婪选择 row_str += f" {arrow_map[action]} " print(row_str) return agent, steps_per_episode if __name__ == "__main__": agent, steps_history = train(episodes=500)

训练过程分析

  1. 外层循环for episode in range(episodes),每个episode代表智能体从起点开始,直到到达终点/陷阱或步数超限的一次完整尝试。
  2. 内层循环while not done,在单个回合内,智能体与环境持续交互(选择动作->获得反馈->学习->状态转移)。
  3. 监控指标:我们记录了每回合的步数steps和成功到达目标的次数success_count。步数越少,说明策略越高效;成功率越高,说明学习越有效。
  4. 安全措施if steps > 100: break是一个重要的保险。防止因Q值未收敛或环境设计问题导致智能体在某个局部死循环。
  5. 结果可视化:训练结束后,我们打印出智能体学到的策略。用箭头表示在每个格子(状态)下它会选择的最优动作。你可以直观地看到它是否学会了绕过陷阱走向目标。

运行这段代码,你会观察到在训练初期,由于探索率高,智能体步数很多,成功率低。随着训练进行,探索率下降,Q表逐渐收敛,平均步数会下降并稳定在一个较优值(对于4x4网格,避开陷阱的最短路径可能是6步),成功率会接近100%。

5. 参数调优与结果分析

代码跑起来了,但你可能发现结果时好时坏,或者收敛速度不理想。这很大程度上取决于几个超参数的设置。我们来深入分析一下:

5.1 核心参数影响分析

参数含义设置过高影响设置过低影响经验取值建议
学习率 α控制每次Q值更新的幅度学习不稳定,Q值震荡,难以收敛学习速度极慢,需要更多回合通常从0.1开始尝试,稳定环境可取0.1-0.5,复杂环境可取0.01-0.1
折扣因子 γ衡量未来奖励的重要性智能体过于“远视”,可能忽视近期关键奖励,学习慢智能体过于“短视”,只追求即时奖励,可能学不到长远策略对于有明确终止目标的任务(如到达终点),0.9-0.99是常见选择
初始探索率 ε控制探索新动作的概率前期盲目随机,浪费训练时间,学习效率低过早陷入局部最优,可能永远找不到全局最优解通常设为0.9或1.0,保证充分初始探索
探索率衰减控制探索率随训练降低的速度衰减太快,探索不充分,策略可能不是最优衰减太慢,训练后期仍在大量随机,策略不稳定指数衰减因子常取0.995-0.999,使ε在训练中期降至较低水平
训练回合数总的训练次数计算资源浪费,可能过拟合(在训练环境上表现过好)训练不充分,Q表未收敛,策略性能差需要观察学习曲线(如平均步数),直到曲线平稳

实操心得没有一套参数放之四海而皆准。最好的方法是观察学习曲线。将每100回合的平均步数或成功率画成图。如果曲线持续下降后趋于平稳,说明参数合适,学习有效。如果曲线剧烈震荡,可能是学习率α太高;如果曲线下降极其缓慢,可能是α太低或γ太低导致智能体缺乏学习动力。对于我们的格子世界,α=0.1, γ=0.9, ε=0.9(衰减0.995)是一个不错的起点。

5.2 学习效果评估与策略解读

训练完成后,我们打印的策略图可能如下所示(一种可能的结果):

↑ → → → → T → ↓ ↑ ← ← ↓ ↑ ↑ ↑ G

(假设起点(0,0)在左上角,终点G在(3,3),陷阱T在(1,1))

如何解读?

  • 从起点(0,0)开始,箭头是,但上方是边界,实际会留在原地。由于初始Q表为0,且探索率衰减后很低,这里的选择可能没有意义,但通常智能体会很快学到应该向右或向下走。
  • 关键看陷阱(1,1)周围:比如(0,1)(1,0)的箭头都指向远离陷阱的方向,(2,1)指向左(远离陷阱),(1,2)指向下(远离陷阱)。这说明智能体成功学到了避开陷阱
  • 从网格右下部分指向终点的箭头,形成了一个指向目标G的“流场”,这说明智能体学到了走向目标的最优路径。

你可以尝试修改陷阱的位置或奖励值,观察学到的策略如何变化。例如,把每步代价从-1改为-0.1,智能体可能就不那么急于到达终点,学到的路径可能不同。

6. 常见问题、扩展与避坑指南

在实际动手实现和调试的过程中,你几乎一定会遇到下面这些问题。这里我把它们和解决方案整理出来,希望能帮你节省大量时间。

6.1 Q-Learning实战常见问题排查表

问题现象可能原因排查与解决思路
智能体完全不学习,随机乱走1. 学习率α设置为0。
2. 奖励函数设计不合理,全是0或非常小。
3. Q表初始化全为0,且γ=0,导致目标值target永远等于reward,若reward非正,则Q值永不增长。
1. 检查并确保α > 0
2. 检查step函数,确保有正负奖励区分。给目标一个大的正奖励(如+100),给每步一个小的负奖励(如-1)。
3. 确保γ > 0,让未来回报发挥作用。
学习不稳定,性能曲线剧烈震荡1. 学习率α过高。
2. 探索率ε衰减太慢或始终很高,导致策略一直在随机。
3. 环境本身具有随机性(随机转移或奖励),但Q-Learning是确定性更新,可能不适应。
1. 降低α,例如从0.5降到0.1或0.05。
2. 增加探索率衰减速度,或设置合理的ε_min(如0.01)。
3. 考虑使用期望Sarsa等能更好处理随机性的算法。
收敛到一个明显很差的策略1. 陷入了局部最优。
2. 探索不充分,ε衰减太快或初始值太小。
3. 折扣因子γ太低,智能体太“短视”,看不到长远好处。
1. 增加初始ε,减缓衰减速度,让智能体有机会跳出局部最优。
2. 尝试增加γ,让智能体更有“远见”。
3. 检查奖励函数,是否对期望行为给予了足够正向激励。
训练后期性能突然下降可能出现了“灾难性遗忘”。如果Q表用神经网络近似(DQN),这是常见问题。但在表格型Q-Learning中较少见,除非状态/动作空间极大且访问不均匀。对于表格法,确保所有状态-动作对都被充分访问。可以尝试初始化Q值为小的随机数,而非全0,打破对称性。
代码运行慢(状态空间大时)表格型Q-Learning的Q表大小是(状态数 × 动作数)。当状态空间巨大(如围棋有10^170状态)时,表格无法存储和遍历。这是表格法Q-Learning的根本局限。此时需要升级到深度Q网络(DQN),用神经网络来近似Q函数,处理高维状态输入。

6.2 从表格法到函数逼近:深度Q网络(DQN)简介

当我们的“格子世界”变成真实的游戏屏幕(像素矩阵),状态空间几乎是无限的,表格法就失效了。这时就需要深度Q网络(Deep Q-Network, DQN)。它用神经网络(如CNN)来代替Q表,输入是状态(如游戏画面),输出是每个动作的Q值。

DQN在Q-Learning基础上引入了两个关键技巧:

  1. 经验回放:将智能体的经历(状态,动作,奖励,新状态,是否终止)存储在一个记忆库中。学习时,随机从库中抽取一批经历来训练网络。这打破了数据间的相关性,使训练更稳定。
  2. 目标网络:使用一个结构相同但参数更新较慢的“目标网络”来计算max(Q(s’, a’)),而用另一个“在线网络”来选择动作和持续更新。这解决了目标值(max(Q(s’, a’)))随网络快速变化而导致的训练不稳定问题。

实现DQN的代码量会远超过80行,但其核心思想仍然是Q-Learning。理解了这个80行的基础版本,你再去啃DQN的论文和代码,会感觉脉络清晰很多。

6.3 项目扩展思路

掌握了基础版本后,你可以尝试以下扩展,让这个项目更丰满:

  • 可视化学习过程:使用matplotlib动态绘制智能体在网格中的移动路径,或者绘制平均步数/成功率随训练回合变化的曲线图。
  • 设计更复杂的环境:增加更多陷阱、奖励格、传送门、可移动的障碍物等。
  • 实现其他经典算法:在同一个环境上实现Sarsa(同策略TD控制)、Expected Sarsa,并与Q-Learning比较学习速度和最终策略的差异。
  • 解决经典控制问题:使用Gymnasium(原OpenAI Gym)库中的经典环境,如CartPole(平衡杆)、MountainCar(爬山车),将你的Q-Learning智能体适配过去。这需要你将连续状态(如小车位置、速度)离散化成桶,这是将表格法应用于连续问题的常用技巧。

回过头看,这80行代码构建的不仅仅是一个格子世界的小游戏AI,它完整呈现了强化学习智能体“感知-决策-学习”的核心闭环。Q-Learning的简洁与强大在于,它用一张不断更新的价值表,将试错的经验沉淀为确定的决策知识。这种“在交互中学习最优行为模式”的思想,是贯穿从游戏AI到机器人控制,从推荐系统到金融交易的通用范式。当你下次看到更复杂的深度强化学习模型时,希望你能会心一笑,认出它骨子里还是那个在不断试错、不断更新Q值的智能体。