☰
Q Learning强化学习实战:Python完整代码包与参数调优指南
2026/9/29 4:56:19 网站建设 项目流程

简介:这是一份Python实现Q Learning强化学习的完整代码包,面向机器学习入门者、算法学习者以及需要在自定义环境中落地Q Learning的开发者。资源以Q表更新、epsilon-greedy策略和gym环境交互为主线,既有可直接运行的qlearningAgents.py,也有介绍算法原理的README.md与报告.pdf。代码展示了经典Q值更新规则如何利用奖励与折扣因子迭代优化动作选择,PDF报告则对算法步骤、参数影响和收敛过程有简要梳理,能帮助读者理解状态、动作、奖励、探索与利用平衡等核心概念。包内共5个文件,涵盖Python脚本、Markdown说明、PDF文档及License授权说明,压缩包仅852KB,轻量且便于本地实验。已有1108人学习下载,适合配合CartPole等经典环境动手调试,也可作为课程作业、算法对比实验或项目原型的参考实现。

1. 为什么说 Q Learning 是强化学习里最适合手撕的算法:一张表就能跑通

如果你第一次接触强化学习,直接去看 PPO、DDPG 这些深度强化学习算法,大概率会被策略梯度、Actor-Critic 这些概念劝退。但 Q Learning 不一样,它不依赖神经网络,不依赖 GPU,核心就是一个二维表格——状态是行,动作是列,表格里存的是“在这个状态下做这个动作能拿多少回报”。这个特性让 Q Learning 成为入门强化学习最合适的切口,也是理解后续 DQN、Double DQN 这些深度强化学习算法的基础。

这份 python 实现的 Q Learning 完整代码包,包含环境定义、Agent 逻辑、训练循环、可视化四部分,解压后不用改任何配置就能跑通一个完整的强化学习闭环。适合三类人:刚入门强化学习、被数学公式卡住的新手;需要快速验证某个环境里 Q Learning 效果、做 baseline 对比的研究者;以及想在格子世界、悬崖行走这类经典环境里观察算法收敛过程的学生。它就是一个能跑的、结构清晰的参考实现,拿去做实验、改参数、二次开发都顺手。

2. Q Learning 的核心公式:从贝尔曼方程到 Python 代码的映射

2.1 先理解 Q 值更新在做什么

Q Learning 要解决的核心问题是:在一个未知的环境里,Agent 怎么通过试错学会“在哪个状态做什么动作最划算”。它的更新逻辑用一句话概括就是——用“当前奖励 + 未来的最优估计”去修正“当前的估计”。这就是时序差分的思想,比蒙特卡洛方法好在不用等一个完整回合结束就能更新。

更新的数学形式是:

Q(s, a) ← Q(s, a) + α [ r + γ max(Q(s', a')) - Q(s, a) ]

拆开来看就是:新 Q 值 = 旧 Q 值 + 学习率 × (当前奖励 r + 折扣因子 γ × 下一个状态的最大 Q 值 - 旧 Q 值)。这个公式最反直觉的地方在于,它更新的是上一个状态的 Q 值,而不是当前状态的。你在状态 s 做了动作 a,到达 s' 拿到奖励 r,然后回头去更新 Q(s, a)——它是拿未来的信息去修正过去,这就是强化学习里“滞后更新”的精髓。

公式里的 γ(折扣因子)决定了 Agent 有多“短视”。γ 越接近 1,Agent 越看重长期回报;γ 接近 0,Agent 只关心眼前奖励。α(学习率)决定新信息覆盖旧信息的速度。在 Python 代码里,这个公式通常就写成一行赋值语句,但要把维度对齐这件事做好——Q 表索引是 (state, action),如果状态是坐标,就得先做坐标到整数索引的映射。

# 核心更新逻辑:单步 Q Learning 更新 def update_q_table(self, state, action, reward, next_state, done): """ state: 当前状态索引 action: 当前动作索引 reward: 执行动作后拿到的奖励 next_state: 转移后的状态索引 done: 是否到达终止状态 """ if done: # 终止状态下没有未来回报,Q 值直接向当前奖励收敛 target = reward else: # 非终止状态:当前奖励 + 折扣后的未来最优 Q 值 target = reward + self.gamma * np.max(self.q_table[next_state, :]) # 时序差分误差:目标值与当前估计的差 td_error = target - self.q_table[state, action] # 沿着误差方向修正,学习率控制修正步长 self.q_table[state, action] += self.alpha * td_error

这里最关键的设计选择是if done分支。很多第一次写 Q Learning 的人会把 target 统一写成reward + gamma * max(Q[s']),在终止状态时就会把终止状态的 Q 值当成可用的未来值,导致训练不收敛或者收敛到错误策略。终止状态意味着轨迹结束,没有后续状态,未来回报是零,必须单独处理。

np.max(self.q_table[next_state, :])这一行是 Q Learning 和 SARSA 的本质区别:Q Learning 在更新时使用“下一步能拿到的最优动作”的 Q 值,不管 Agent 实际会按什么策略走。这个 off-policy 特性让 Q Learning 可以在探索的同时学习最优策略,也是它比 SARSA 激进、在某些环境下更容易收敛的原因。

2.2 探索与利用:epsilon-greedy 策略是怎么逐步让位的

Q 表只有在被充分访问过的状态-动作对上才有意义。如果 Agent 一直按当前 Q 表取最大值动作,它可能永远走不出初期的坏策略——这就是“利用”过度;但如果一直随机探索,又学不到稳定策略。epsilon-greedy 是工业界最常用的折中:以 epsilon 的概率随机动作,以 1-epsilon 的概率选 Q 值最大的动作。

# epsilon-greedy 策略:探索率随训练进度衰减 def choose_action(self, state): """ state: 当前状态索引 返回: 动作索引 """ self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay) if np.random.random() < self.epsilon: # 探索:均匀随机选动作 action = np.random.randint(self.n_actions) else: # 利用:选 Q 值最大的动作 action = np.argmax(self.q_table[state, :]) return action

epsilon 的初始值一般设 1.0,让 Agent 前期充分探索;epsilon_decay 每次选择动作后乘一次,让它逐渐退化为贪心策略。这里有个容易翻车的细节:epsilon_decay 的值要结合回合数来设定。假设一共训练 500 回合,每回合大约 200 步,那就是 100000 次衰减,如果 decay 设 0.999,100000 步后 epsilon 是 1.0 × 0.999^100000,这个值已经趋近于 0 了,如果设 0.99,衰减过快,Agent 还没探索够就锁死在当前策略上。

我的经验是,先算总步数,再反推 decay。想让 epsilon 在训练中期降到 0.1 附近,就用公式 decay = 0.1^(1/总步数) 去估算,虽然不算精确,但比试错快得多。epsilon_min 这个下限也很重要,一般 0.01,保证在训练后期仍然有少量探索,避免因为环境随机性导致策略僵化。

2.3 Q 表初始化方式为什么会影响收敛速度

Q 表初始化为全零,是最保守的选择。全零初始化的意思是,Agent 一开始对所有动作的价值估计都是 0,它在中性状态下选动作纯粹靠 epsilon-greedy 的随机性。这个方案安全,在大多数环境里都能收敛。

还有一种常见做法是初始化为一个小的正值,比如 0.1。这样做是为了鼓励探索:因为 Q 值都是正的,Agent 会倾向于尝试那些还没怎么访问过的状态-动作对。这看起来像是一个“自信的探索”策略,但在奖励为负的环境里会有问题——如果环境给负奖励,正的初始 Q 值会被不断拉低,反而拖慢收敛。

# Q 表初始化:两种常见方案对比 # 方案 A:全零初始化,中性且无偏 self.q_table = np.zeros((self.n_states, self.n_actions)) # 方案 B:小正数初始化,鼓励探索,但负奖励环境下慎用 self.q_table = np.full((self.n_states, self.n_actions), 0.1)

如果环境的状态空间连续(比如小车爬坡的连续速度),Q 表没法直接建,需要先做离散化——把连续的速度、位置切成若干个区间,每个区间对应一行。离散化的粒度直接决定 Q 表大小和收敛速度:切太细,Q 表爆炸,训练时间成倍增加;切太粗,策略粗糙,学出来的动作不精细。格子世界这类环境天然就是离散状态,所以最适合作为 Q Learning 的第一次实践。

3. 完整代码包解析:环境、Agent、训练循环怎么协同工作

3.1 代码包的文件结构与运行入口

这份完整代码包解压后是标准的单文件结构,核心逻辑集中在 q_learning.py 里,另外附带一个简单的环境和可视化输出。整个包的核心模块拆开来看,就是环境定义、Agent 逻辑、训练循环三层。拿到代码包后,第一步要看清楚文件里环境是怎么定义的——这是后续改参数的基础。

# 解压并运行代码包 unzip python实现QLearning强化学习_完整代码.zip cd QLearning_Project # 安装依赖(只需要 numpy 和 matplotlib) pip install numpy matplotlib # 直接运行训练脚本 python q_learning.py

常见做法是用 numpy 做 Q 表存储和更新计算,matplotlib 画收敛曲线。这套依赖在任何 Python 3.6+ 环境里都能跑,不涉及深度学习框架。如果电脑上还没有 Python 环境,去 python 官网下载安装包,安装时勾选 Add Python to PATH,然后重新打开终端就能用。

3.2 环境定义:网格世界的状态转移和奖励逻辑

代码包里的环境通常是一个 n×n 的网格世界,Agent 从起点出发,要走到终点,中途有障碍物和陷阱。状态就是坐标,动作是上下左右四个方向,奖励规则是:到达终点给正奖励,踩到陷阱给负奖励,正常移动给一个小幅度的负奖励(或者 0)。网格世界是一个理想的教学环境,状态空间小、转移确定、奖励稀疏程度可调。

# 环境定义:6x6 网格世界的核心逻辑 class GridWorld: def __init__(self, size=6): self.size = size self.n_actions = 4 # 0: 上, 1: 下, 2: 左, 3: 右 self.n_states = size * size # 状态数 = 格子数 # 奖励配置 self.goal_state = 35 # 终点:右下角,状态索引 = 5*6+5 self.trap_states = [17, 23] # 陷阱:固定两个格子 self.reward_goal = 10.0 # 到达终点奖励 self.reward_trap = -5.0 # 踩陷阱惩罚 self.reward_step = -0.1 # 每步惩罚,鼓励走最短路径 def step(self, state, action): """ 状态转移逻辑 返回: next_state, reward, done """ row = state // self.size col = state % self.size # 根据动作更新坐标 if action == 0: row = max(0, row - 1) elif action == 1: row = min(self.size - 1, row + 1) elif action == 2: col = max(0, col - 1) else: col = min(self.size - 1, col + 1) next_state = row * self.size + col # 判定奖励和终止条件 if next_state == self.goal_state: return next_state, self.reward_goal, True elif next_state in self.trap_states: return next_state, self.reward_trap, True else: return next_state, self.reward_step, False

注意max(0, row - 1)和min(self.size - 1, row + 1)这两组边界裁剪——Agent 撞墙不会出界,而是停在原地。边界处理方式对训练结果有明显影响:如果允许出界并返回一个大的负奖励,Agent 会额外学会“别撞墙”这个隐式规则;如果只是停在原地,Agent 学的是“撞墙是无效动作”。两种方案都能收敛,后者收敛更快因为状态空间更小。陷阱状态的索引是硬编码的,改环境尺寸时需要同步改这两个值,这是读者第一次改代码最容易踩的坑。

3.3 训练循环:回合制学习与收敛判据

训练循环是一个 while/for 嵌套结构:外层遍历回合(episode),内层遍历单步(step)。每个回合开始把 Agent 放回起点,回合内反复执行“选动作-执行-观察奖励和下一状态-更新 Q 表-判断终止”。单回合结束条件有两种:到达目标/陷阱(done=True),或者超过最大步数(强制截断)。强制截断很重要,否则在稀疏奖励环境里 Agent 可能在一个回合里无限游荡。

# 训练循环主体 def train(self, episodes=500, max_steps_per_episode=100): """ episodes: 训练回合数 max_steps_per_episode: 单回合最大步数,防止无限循环 """ rewards_history = [] for episode in range(episodes): state = 0 # 起点状态 total_reward = 0 for step in range(max_steps_per_episode): action = self.agent.choose_action(state) next_state, reward, done = self.env.step(state, action) self.agent.update_q_table(state, action, reward, next_state, done) state = next_state total_reward += reward if done: break rewards_history.append(total_reward) # 每 50 回合打印一次进度,观察收敛趋势 if (episode + 1) % 50 == 0: avg_reward = np.mean(rewards_history[-50:]) print(f"Episode {episode + 1}, 平均奖励: {avg_reward:.3f}") return rewards_history

state = next_state这一行容易被忽略,但它是环境推进的关键,少了它整个训练就退化成单步循环。回合结束后的break要放在if done里面,否则即使到了终止状态,循环还会继续跑,Q 表会收到错误的 transition 数据。每 50 回合打印一次平均奖励,是一种低成本监控收敛的手段。平均奖励曲线从负值逐渐爬升、最后稳定在一个区间,说明 Q 表正在收敛;如果曲线反复震荡或者长期趴在同一个值附近不抬头,说明参数有问题。

收敛判据方面,除了看平均奖励,还可以直接检查 Q 表的变化量。每隔若干回合计算一次np.sum(np.abs(new_q - old_q)),这个值趋近于 0 就说明 Q 表不再大幅变动了。两种判据配合使用更可靠——平均奖励可能存在“环境随机性好、碰巧拿高分”的假象,Q 表变化量更能反映真实学习状态。

3.4 策略提取:训练完之后怎么把 Q 表变成可执行的路径

训练完成后,Q 表只是存了一堆数值,真正要展示“Agent 学会了”,需要把 Q 表转成一条具体的行走路径。做法很简单:从起点开始,每一步都选当前状态下 Q 值最大的动作,然后沿着状态转移链走,直到终点。

# 从训练好的 Q 表提取最优路径 def extract_policy(self, start_state=0): """ 根据 Q 表生成一条从起点到终点的路径 返回: 途经的状态列表 """ path = [start_state] state = start_state visited = set() # 防止死循环:如果路径重复访问状态,说明策略有问题 while len(path) < self.env.n_states: if state in visited: print("警告:策略进入循环,Q 表可能未收敛") break visited.add(state) action = np.argmax(self.agent.q_table[state, :]) next_state, _, done = self.env.step(state, action) path.append(next_state) state = next_state if done: break return path

visited集合是一个防御性设计:如果 Q 表还没收敛,贪心策略可能在一个局部环里打转(比如反复在格子 8 → 9 → 8 之间横跳)。没有 visited 保护,这个 while 循环会无限跑下去。打印策略路径时,建议把格子编号映射成坐标 (row, col) 再输出,直接看图比看编号直观得多。一个正常的收敛路径大概长这样:(0,0) → (0,1) → (1,1) → (2,1) → ... → (5,5),路径是连通的,不会出现跳到非相邻格子的情况。

4. 参数调优指南:alpha、gamma、epsilon_decay 怎么配合着调

4.1 参数作用与合理区间:先搞清楚每个参数在控制什么

Q Learning 的可调参数不多,但每个参数都直接影响收敛行为。学习率 alpha 决定单步更新的步长,折扣因子 gamma 决定长期回报的权重,epsilon_decay 控制探索到利用的切换速度。这三个参数不是独立作用的,它们之间有耦合关系。

参数作用合理区间调大后果调小后果
alpha新信息覆盖旧信息的速度0.1 ~ 0.5振荡发散,Q 值跳变收敛慢,容易卡在局部最优
gamma未来回报的折扣程度0.8 ~ 0.99Agent 过度关注远期,路径绕远Agent 短视,只盯眼前奖励
epsilon_decay探索率衰减速度0.95 ~ 0.9995探索期变短,可能错过全局最优探索期过长,训练尾期仍在随机游荡
epsilon_min探索率下限0.01 ~ 0.1后期探索过多,策略不稳定后期完全贪心,无法应对环境随机性

这几个参数之间最常见的一组矛盾是:alpha 调大了振荡,alpha 调小了收敛慢。实际上 alpha 也可以做衰减,在训练后期用更小的学习率去做精细调整。这是一种常见做法:alpha = max(0.01, alpha * 0.999),效果是前期大步学、后期小步修。这和深度学习里学习率衰减的思路一致,在 Q Learning 里同样有效。

gamma 的设置有讲究。如果环境的单步奖励是负数(比如每走一步 -0.1),gamma 太大会让 Agent 更在意“少走冤枉路”,因为长期累积的步数惩罚变大;如果奖励全是正数,gamma 太大反而可能导致 Q 值发散——Q 值会被不断累加成很大的数,需要配合合适的 alpha 来压制。我的习惯是:负奖励为主的密集奖励环境,gamma 取 0.9 左右;稀疏奖励环境(只有终点有大奖励),gamma 取 0.95 以上,让 Agent 学会“为远期的甜头绕路”。

4.2 奖励设计的四个常见层次与参数联动

奖励设计比调 alpha、gamma 更影响训练效果。同样的环境,奖励函数不同,Q Learning 学出来的策略可能完全不同。常见的奖励设计有四种:

第一种:全零 + 终点正奖励。最简单的稀疏奖励环境,Agent 前期纯靠随机探索找终点,训练速度极慢,但一旦找到一次就能学会。适合演示“探索的艰难”。

第二种:每步 -0.1 + 终点 +10。在稀疏奖励基础上加了步数惩罚,Agent 学会走最短路径。这个方案训练速度比第一种快,因为即使没找到终点,Agent 也能感受到“走一步扣一分”的压力,倾向于少走回头路。

第三种:每步 -0.1 + 终点 +10 + 陷阱 -5。增加了障碍物避让信号。这种方案最容易观察到“学习曲线下降-爬升”的过程:初期踩陷阱吃大惩罚,奖励曲线很低,后期学会避开陷阱后曲线跳升。

第四种:中间状态设人工势场奖励,比如离终点越近奖励越大。这种方案收敛最快,但工程上设计势场函数需要额外工作,而且容易学出投机取巧的策略——Agent 可能在势场高峰处反复徘徊不走。

# 奖励函数对比:步数惩罚 + 陷阱惩罚的联动效果 # 这是方案三的奖励配置在代码里的样子 REWARDS = { "step": -0.1, # 每步微惩罚 "goal": 10.0, # 终点大奖励 "trap": -5.0 # 陷阱中惩罚 } # 一个值得注意的细节:如果陷阱和终点相邻, # Agent 可能在陷阱附近学到“绕远路也不靠近陷阱”的保守策略。 # 这是因为步数惩罚(-0.1)远小于陷阱惩罚(-5),绕远的代价远低于踩陷阱的代价。

这个“绕远避险”现象是奖励设计里的常见坑:当惩罚绝对值远大于步数代价时,Agent 会发展出过度保守的策略。机械臂在真实环境中很容易因为过度保守而不敢靠近障碍物附近的目标点。遇到这种情况,可以适当降低惩罚绝对值,或者给目标点方向加引导信号。

4.3 训练过程监控:从奖励曲线里读出问题

记录每个回合的总奖励,绘制成曲线,是判断训练状态最朴素也最直观的方法。一份完整的训练监控应该同时记录两个量:单回合总奖励,以及 Q 表变化量的 L2 范数。

# 训练完成后绘制奖励曲线 import matplotlib.pyplot as plt def plot_training_curve(rewards_history): """ rewards_history: 每回合的总奖励列表 """ plt.figure(figsize=(10, 5)) # 原始曲线 plt.plot(rewards_history, alpha=0.3, label="Raw") # 滑动平均曲线(平滑噪声) window = 20 smoothed = np.convolve(rewards_history, np.ones(window) / window, mode="valid") plt.plot(smoothed, linewidth=2, label=f"MA{window}") plt.xlabel("Episode") plt.ylabel("Total Reward") plt.legend() plt.grid(True) plt.title("Q Learning Training Curve") plt.savefig("training_curve.png", dpi=150)

看奖励曲线时,重点关注三个阶段。前期(约前 10% 回合):曲线应该在低位波动,偶尔出现小峰值,说明 Agent 在探索。如果前期曲线一动不动,说明 epsilon 初始值太低,或者奖励完全没有信号;中期(约 20% 到 70%):曲线开始爬升,并且爬升速率逐渐变慢,说明 Q 表在修正策略。如果中期出现突然跳崖式下跌,通常是 Agent 发现了之前没遇到过的陷阱,或者随机性导致一次极端差的路径;后期(最后 30%):曲线应该在一个窄区间平稳波动,波动幅度取决于 epsilon_min 和环境随机性。如果后期还在大幅震荡,检查 alpha 是否过大、epsilon_min 是否过高。

5. 避坑指南:Q Learning 代码包常见的五个翻车现场

5.1 现象:训练很久奖励曲线不抬头,Q 表全零

原因分析:epsilon 衰减太快,Agent 在还没遍历足够多状态-动作对时就已经完全贪心化,锁死在一个局部策略里出不来。典型场景是总步数很多但 epsilon_decay 设成 0.99,按 100000 步算,后期 epsilon 已经小于 0.001,几乎纯贪心。另一个原因是奖励信号过于稀疏,Agent 前期完全拿不到非零奖励,Q 表没有任何正向信号可以传播。

解决:调大 epsilon_decay 到 0.995 以上,并确认 epsilon_min 不要低于 0.01;同时检查奖励配置,如果在 100 步内 Agent 平均只能拿到一次非零奖励,先降低环境的稀疏程度——比如加一步微惩罚 (-0.1),让 Q 表在探索阶段就有梯度可以学习,不要指望奖励全靠终点那一下。

5.2 现象:训练曲线冲高后崩盘,Q 值越来越大最终发散

原因分析:alpha 太大,单步更新的步长超过了 Q 值稳定所需的范围;gamma 太大且奖励全为正,Q 值被累加成超大数,导致数值溢出或策略震荡。Q Learning 本身没有理论保证一定能收敛,在 alpha、gamma 同时取大值时,很容易出现振荡甚至发散。

解决:把 alpha 从 0.5 降到 0.1 或 0.2,gamma 从 0.99 降到 0.9 试跑一下。如果环境奖励有正有负(正负抵消),通常不会发散;如果全是正奖励,要特别注意。出现 Q 值数量级超过 1e3 时,基本可以判定参数组合有问题。

5.3 现象:路径提取时卡在循环里走不出来

原因分析:Q 表未完全收敛时,贪心策略可能在一个局部环上打转——状态 A 选动作去 B,状态 B 选动作回 A。这种情况在 alpha 过小、训练回合不足时最容易出现,因为 Q 值还没有区分出不同动作的优劣差异。

解决:训练回合数设大一些,比如从 200 加到 500;同时把 epsilon_min 适当调高到 0.05,让 Agent 在后期仍有小概率跳出局部环。路径提取函数里加上 visited 集合做防护(代码见第 3 章),避免程序死循环。判断 Q 表是否够收敛,用前面提到的 Q 表变化量指标。

5.4 现象:代码包在自己电脑上跑出和 README 里不一样的结果

原因分析:最常见的是环境尺寸改动后,终点状态索引、陷阱状态索引没有同步更新。比如 6×6 网格的终点是第 36 个状态(索引 35),改成 8×8 后终点索引应该是 63,如果忘了改,环境逻辑直接错乱。另一个常见原因是 Python 版本差异,Q Learning 代码本身不依赖高版本特性,但 print 语法或 f-string 在不同版本下可能有兼容差别。

解决:改环境尺寸时,先算清终点索引——goal_state = size * size - 1,不要手写硬编码数字;陷阱索引也要按新坐标重新计算。运行前用python --version确认版本,代码包需要 Python 3.6+,如果用的是老版本 Python 2,先升级。

5.5 现象:可视化输出没有更新,训练好 Q 表后路径展示不出来

原因分析:通常是因为路径提取函数和环境 step 函数的交互方式不匹配。Q Learning 训练时更新的是 (state, action) 的 Q 值,而路径提取时如果直接用 Q 表 argmax 选动作,需要保证环境 step 函数的转移逻辑和训练时用的完全一致——比如动作顺序是上、下、左、右还是上、左、下、右,不一致就全乱。

解决:把动作编号的对应关系单独写成一个字典或注释,放在环境定义里,例如action_map = {0: "up", 1: "down", 2: "left", 3: "right"}。路径可视化模块直接引用环境里已有的 step 函数,不要重新写一套转移逻辑。如果输出路径里的状态编号跳变不连续,优先检查这个映射。

6. 从 Q 表到深度 Q Learning:训练完这份代码后下一步能做什么

6.1 拿 Q 表做策略对比实验

这份代码包训练完成后,Q 表本身就是一个可迁移的产物。你可以把它保存成 numpy 文件,然后换一个测试环境去对比不同训练策略的效果。保存和加载 Q 表就是两行:

# 保存训练好的 Q 表 np.save("q_table.npy", agent.q_table) # 加载 Q 表做测试 loaded_q_table = np.load("q_table.npy")

一个值得做的实验是:用同一份环境、同一份代码,分别用 alpha=0.1 和 alpha=0.5 训练两份 Q 表,然后放在一个带随机风场的环境里对比两个策略的鲁棒性。你会发现 alpha=0.5 的那份 Q 表虽然在训练阶段收敛快,但在扰动环境下表现反而更差——因为它的 Q 值被大学习率带了过多的随机噪声。这类对比实验很适合写进技术笔记里,比单纯贴一张奖励曲线更有说服力。

6.2 把 Q 表换成神经网络:一个最小的 DQN 改造路径

Q Learning 学完,下一步自然是往深度强化学习走。把 Q 表替换成神经网络的核心变化是:Q 值不再用np.max(self.q_table[next_state, :])查表,而是用神经网络前向传播输出。这是 DQN 的本质改变,也是从表格型算法到函数逼近型算法的分水岭。

# DQN 的 Q 值计算方式:查表变成网络前向传播 def compute_target(self, reward, next_state, done): if done: return reward else: # double dqn 的改进:用目标网络计算 max next_q = self.target_net(next_state).max(dim=1)[0] return reward + self.gamma * next_q

如果直接跑这份 PyTorch 版的 DQN 代码,你会发现一个现象:在网格世界里 DQN 反而比 Q Learning 更难收敛,甚至可能完全不收敛。这不是代码写错了,而是函数逼近在小的离散状态空间里不如表格直接。Q 表的优势在于每个状态都有一个独立的参数去拟合,而神经网络是共享参数的,会在相邻状态之间做平滑插值,这种平滑在状态空间足够大时是优势,在小状态空间里反而是劣势。理解了这个原因,你就知道 Q Learning 的真正价值边界——状态空间小到可以建表时,表格型方法永远是最优选择;状态空间大到建不下表时,才需要往 DQN 这类深度强化学习算法迁移。

6.3 三个最常见的二次开发方向

方向一是换环境。把代码包里的 GridWorld 替换成更复杂的环境,比如奥赛罗、井字棋这类双人博弈环境。这时 Q Learning 需要做一个小改动——匹配对手动作。因为环境不再是一步一转移的马尔可夫过程,状态里要加入对手的最后一步动作。

方向二是加 eligibility trace。在 Q Learning 更新逻辑里引入衰减迹(eligibility trace),让一次奖励可以沿着轨迹回溯传播到之前的多步状态。这会加速稀疏奖励环境的训练,代码量大约增加十行,但收敛速度提升明显。

方向三是改成 Double Q Learning。维护两张 Q 表,交替更新,每步用其中一张表选动作、另一张表评估价值。这个做法的价值是削弱 Q 值的过估计问题——表格型 Q Learning 在奖励波动大时容易高估某些状态-动作对的价值,Double Q Learning 通过解耦选择和评估来缓解这个问题。

这些改动都不大,但每改一个都值得重新画一遍奖励曲线、重新跑一遍路径提取,观察差距。从那以后,我每次拿到一个新环境,都会先跑一遍 Q Learning baseline,不管后面要用多复杂的深度强化学习算法,这个 baseline 都不会丢——它不只是用来对比,更是帮你理解当前环境的奖励结构、转移确定性、探索难度。

希望这套 Q Learning 代码包和这份实战笔记能帮你在强化学习的路上打通第一个闭环。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询