Q-Learning算法实战指南:从原理到代码落地

先搞懂Q-Learning的核心逻辑:像学骑车一样“记教训”

我们可以把Q-Learning的智能体比作一个学骑车的小孩——
状态(State):比如“车把歪了”“要撞树了”;
动作(Action):比如“往左掰车把”“刹车”;
奖励(Reward):比如“没摔倒(+1)”“撞到树(-10)”;
Q值(Q-Value):小孩脑子里的“经验笔记”——“在‘车把歪了’的状态下,‘往左掰车把’能得到多少好处?”

Q-Learning算法实战指南:从原理到代码落地

Q-Learning的本质,就是让智能体通过试错不断更新这份“经验笔记”,最终学会在每个状态下选最“划算”的动作。

举个更具体的例子:假设你训练一个智能体玩“走迷宫”——
– 状态是“迷宫中的位置(比如A/B/C点)”;
– 动作是“上下左右走”;
– 奖励是“走到出口(+100)”“撞到墙(-10)”“走一步(-1)”;
– Q值就是“在A点走右,能拿到多少总奖励?”

智能体一开始像个路痴,乱走乱撞,但每走一步都会更新自己的“Q笔记”——比如在A点走右撞到墙(-10),它就会把“A点→右”的Q值调低;如果走右能到B点,再走B点→上到出口(+100),它就会把“A点→右”的Q值调高(因为后续能拿到大奖励)。

Q表:智能体的“经验备忘录”

Q值不是零散的,而是被整理成一张Q表(Q-Table)——行是状态,列是动作,单元格里的数值就是对应状态-动作对的Q值。

比如“红绿灯路口”的简化Q表示例:

状态(s) 动作:走(a1) 动作:等(a2)
红灯 -10(闯红灯扣分) 5(遵守规则奖)
绿灯 10(通行奖) -5(等红灯浪费时间)

智能体做决策时,只要看当前状态对应的行,选Q值最大的动作就行——比如红灯时,“等”的Q值(5)比“走”(-10)大,所以选“等”。

注意:Q表的大小由状态数×动作数决定。如果状态是连续的(比如CartPole的“小车位置”是-2.4到2.4之间的任意数),需要先把状态离散化(比如分成10个区间),否则Q表会无穷大(后面代码部分会详细讲)。

贝尔曼方程:Q值更新的“数学发动机”

Q表不是天生的,而是通过贝尔曼方程(Bellman Equation)不断迭代更新的。这个方程看起来有点抽象,但拆开讲其实很简单:

Q(s,a) = Q(s,a) + α × [ R + γ × maxQ(s’,a’) – Q(s,a) ]

我们逐个拆解每个符号的含义,再用“学骑车”的例子翻译一遍:
Q(s,a):当前状态s下做动作a的旧Q值(比如“车把歪了→往左掰”的旧经验);
α(学习率):“更新幅度”——比如α=0.1,意味着只把旧Q值的10%换成新经验;
R:当前动作带来的即时奖励(比如“往左掰车把没摔倒,得+1”);
γ(折扣因子):“未来奖励的权重”——比如γ=0.95,意味着“1步后的奖励”只值当前的95%,“2步后”是95%×95%=90.25%(毕竟未来的奖励不一定能拿到,比如骑车时下一步可能撞树);
maxQ(s’,a’):下一个状态s’下,所有动作中最大的Q值(比如“往左掰车把后,车把正了,这时做什么动作能拿到最大奖励?”);
[R + γ×maxQ(s’,a’) – Q(s,a)]TD误差(时序差分误差)——旧经验和新经验的差距(比如旧经验认为“车把歪了→往左掰”能拿+0.5,但实际拿到了“即时+1 + 未来+0.95×1”=1.95,差距就是1.95-0.5=1.45)。

翻译成人话就是:“我之前以为做这个动作能拿这么多好处,现在实际试了下,好处比我想的多(或少),那我把经验更新一点,往实际情况靠。”

举个数值例子:
假设旧Q(s,a)=0.5,α=0.1,R=1,γ=0.95,maxQ(s’,a’)=1(下一个状态的最优Q值),那么:
Q(s,a) = 0.5 + 0.1×[1 + 0.95×1 – 0.5] = 0.5 + 0.1×(1.45) = 0.645

也就是说,“车把歪了→往左掰”的Q值从0.5涨到了0.645——智能体记住了“这个动作更划算”。

手把手写Q-Learning代码:用OpenAI Gym玩CartPole

理论讲再多,不如写一行代码。我们用OpenAI Gym的CartPole环境(经典入门环境:控制小车平衡一根杆子,不倒则得分)来实现Q-Learning。

1. 准备环境

先安装依赖:

pip install gymnasium numpy matplotlib  # Gymnasium是新版Gym,兼容旧代码

2. 核心步骤拆解

CartPole的状态是连续的4维向量(小车位置、速度;杆子角度、角速度),动作是离散的2个(左推/右推)。我们需要先把连续状态离散化,再构建Q表。

3. 完整代码实现(带详细注释)

import gymnasium as gym
import numpy as np
import random
from matplotlib import pyplot as plt

# 1. 初始化环境
env = gym.make('CartPole-v1', render_mode="rgb_array")  # 用rgb_array模式,后续可渲染视频

# 2. 状态离散化函数(把连续状态切成10个区间)
def discretize_state(state):
    # 每个维度的范围(来自CartPole的官方文档)
    state_bins = [
        np.linspace(-2.4, 2.4, 10),  # 小车位置
        np.linspace(-3.0, 3.0, 10),  # 小车速度
        np.linspace(-0.209, 0.209, 10),  # 杆子角度(弧度)
        np.linspace(-3.0, 3.0, 10)   # 杆子角速度
    ]
    # 把每个维度的连续值转换成区间索引(比如位置-2.4→0,-1.2→1,…,2.4→9)
    discretized = tuple(np.digitize(s, bin) for s, bin in zip(state, state_bins))
    return discretized

# 3. 初始化Q表(用字典存储,键是离散状态,值是动作Q值的列表)
q_table = {}
# 超参数设置(我踩过无数坑后的“入门友好值”)
gamma = 0.95  # 未来奖励权重
alpha = 0.1   # 学习率
epsilon = 0.1 # 探索率(10%概率随机选动作,90%选最优)
episodes = 1000  # 训练轮数

# 4. 训练循环
reward_history = []  # 记录每轮的总奖励,方便画图
for episode in range(episodes):
    state, _ = env.reset()  # 重置环境,得到初始状态
    discretized_state = discretize_state(state)
    total_reward = 0
    done = False

    while not done:
        # 4.1 ε-贪心策略:兼顾探索(试新动作)和利用(用已知最优)
        if random.uniform(0, 1) < epsilon:
            action = env.action_space.sample()  # 随机选动作(探索)
        else:
            # 如果状态不在Q表中,初始化动作Q值为0
            if discretized_state not in q_table:
                q_table[discretized_state] = [0.0, 0.0]
            action = np.argmax(q_table[discretized_state])  # 选Q值最大的动作(利用)

        # 4.2 执行动作,得到下一个状态、奖励、是否结束
        next_state, reward, done, _, _ = env.step(action)
        discretized_next_state = discretize_state(next_state)
        total_reward += reward

        # 4.3 更新Q表
        # 如果当前状态不在Q表,初始化
        if discretized_state not in q_table:
            q_table[discretized_state] = [0.0, 0.0]
        # 如果下一个状态不在Q表,初始化(避免KeyError)
        if discretized_next_state not in q_table:
            q_table[discretized_next_state] = [0.0, 0.0]

        # 计算TD误差,更新Q值
        current_q = q_table[discretized_state][action]
        max_next_q = np.max(q_table[discretized_next_state])
        td_error = reward + gamma * max_next_q - current_q
        new_q = current_q + alpha * td_error
        q_table[discretized_state][action] = new_q

        # 4.4 切换到下一个状态
        discretized_state = discretized_next_state

    # 记录每轮奖励
    reward_history.append(total_reward)
    # 每100轮打印进度
    if (episode + 1) % 100 == 0:
        print(f"Episode {episode+1}, Average Reward: {np.mean(reward_history[-100:]):.2f}")

# 5. 可视化训练过程
plt.plot(reward_history)
plt.xlabel("Episode")
plt.ylabel("Total Reward")
plt.title("Q-Learning Training Progress (CartPole)")
plt.show()

# 6. 测试训练好的智能体(不用探索,全选最优动作)
test_episodes = 10
test_rewards = []
for _ in range(test_episodes):
    state, _ = env.reset()
    discretized_state = discretize_state(state)
    total_reward = 0
    done = False
    while not done:
        if discretized_state not in q_table:
            action = 0
        else:
            action = np.argmax(q_table[discretized_state])
        next_state, reward, done, _, _ = env.step(action)
        total_reward += reward
        discretized_state = discretize_state(next_state)
    test_rewards.append(total_reward)
print(f"Test Average Reward: {np.mean(test_rewards):.2f}")

env.close()

代码运行说明:

  • 训练1000轮后,Average Reward应该能从初始的20左右涨到200(CartPole的上限是200,意味着杆子能一直立着);
  • 测试时的平均奖励如果能稳定在180以上,说明Q表训好了。

踩过的坑:这些错误我帮你先避了

我刚开始学Q-Learning时,踩过无数“看起来没问题,结果跑不通”的坑,总结几个高频雷区:

坑1:ε不衰减,智能体一直“瞎试”

一开始我把ε固定为0.1,结果训练到2000轮,奖励还是波动很大——因为智能体永远有10%的概率乱选动作(比如明明该往左推,它偏往右推,直接把杆子摔了)。
解决办法:让ε随着训练轮数衰减,比如从0.1慢慢降到0.01(比如epsilon = max(0.01, epsilon * 0.995)),后期减少探索,专注利用已知经验。

坑2:状态离散化太粗/太细

  • 太粗:比如把小车位置分成2个区间(“左/右”),Q表不够精确,智能体学不会微调车把;
  • 太细:比如分成100个区间,Q表大到无法训练(状态数=100^4=1亿,内存爆炸)。
    解决办法:入门用10个区间,刚好平衡精度和计算量(像我代码里那样)。

坑3:超参数乱调

  • γ太大(比如>0.99):智能体太看重未来奖励,比如为了“后面拿100奖励”,现在愿意承受-100的惩罚(结果直接摔杆子);
  • α太大(比如>0.5):Q值波动太大,像个情绪不稳定的人,今天觉得“这个动作好”,明天又觉得“不好”;
  • α太小(比如<0.01):学习速度太慢,1000轮都学不会。
    解决办法:先按我代码里的“入门友好值”来,再慢慢调——比如把γ从0.95改成0.9,看奖励怎么变;把α从0.1改成0.2,看波动是不是变大。

坑4:Q表初始化错误

我一开始没处理“新状态”的情况,结果训练时经常报KeyError——因为智能体走到一个新状态(比如小车位置到了-2.0),Q表里没有这个状态的记录。
解决办法:每次访问状态前,先检查是否在Q表中,如果不在,初始化动作Q值为0(像代码里的if discretized_state not in q_table: q_table[discretized_state] = [0.0, 0.0])。

最后:Q-Learning的局限与进阶方向

Q-Learning是强化学习的“入门砖”,但它也有缺点:
无法处理高维连续状态:比如玩Atari游戏(状态是210×160的像素),Q表会大到无法存储(210×160×动作数,根本不可能);
Q值估计不准:因为Q值是“经验平均”,如果某个状态-动作对试的次数少,Q值可能不准。

进阶方向
– 用DQN(深度Q网络)代替Q表:用神经网络拟合Q值,处理高维状态;
– 用Double DQN解决“maxQ高估”问题(Q-Learning会高估未来奖励,Double DQN用两个网络互相验证);
– 用Dueling DQN分开估计“状态价值”和“动作优势”,提高Q值的准确性。

不过别急,先把Q-Learning吃透——这是所有强化学习算法的“地基”,搞懂它再学进阶算法,会轻松很多!

原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/225

(0)

相关推荐