Q学习深度指南

一句话总结

Q学习就是让智能体在”摸爬滚打”中学会判断每个状态下哪个动作最值得做——就像你家狗子学会在路口等红灯一样,靠的是不断试错和”这个动作值几分”的直觉积累。

关键词速览

核心概念探索策略TD更新收敛性Q表
ε-greedy软更新离策略学习Bellman方程价值迭代

核心关键词表

术语英文符号大白话解释
Q学习Q-Learning给每个”状态+动作”组合打分
时序差分Temporal Difference边走边学,不用等走完才知道对不对
ε-greedyEpsilon-Greedy大部分时候选最优,偶尔瞎试试运气
TD误差TD Error”我觉得值5分,结果只得了3分,差2分”
学习率Learning Rate每次纠正多少,太大容易摇摆,太小学得慢
折扣因子Discount Factor未来的钱不值钱,打几折
Q值Q-Value最乐观情况下这个动作能拿多少分

第一部分:零基础入门——Q学习是什么?

零、一个牧羊犬的故事:让你彻底理解Q学习

想象你是个牧羊人,养了一只牧羊犬。你的目标是训练它把羊群从草地A赶到羊圈B。

问题来了:狗子怎么知道在该拐弯的时候拐弯,而不是傻愣愣直着跑?

狗子的”Q表”思维

你会发现,聪明的狗子脑子里其实有一张隐形的表格:

当前情况(状态)往左跑往前跑往右跑往后跑
羊在左边8分3分0分1分
羊在右边0分3分8分1分
羊在正前方2分9分2分0分
羊四散5分5分5分3分

这个表格就是”Q表”,每个格子的数字就是Q值——狗子认为这个动作”值多少分”。

狗子是怎么学这张表的?

第一步:瞎跑试试 小狗刚出生啥也不懂,你就让它随便跑。一开始它可能往右跑,但羊其实在左边,结果羊被吓跑了。

第二步:打分纠错

  • 狗子跑了5步终于把羊赶进羊圈了
  • 狗子回看:最后一步(进羊圈)其实值10分
  • 但倒数第二步(拐弯)只值5分
  • 狗子就想:“哦,原来拐弯那个动作虽然当时看起来一般,但对最终成功很重要!”
  • 所以狗子更新了对”拐弯”这个动作的评分

这就是TD学习的核心:不等走完全程才知道对不对,走一步就大概知道刚才那个动作值多少分。

关键洞察:为什么Q学习有效?

Q学习之所以有效,是因为它利用了递推关系——

一个动作的价值 = 立刻能拿到的奖励 + 未来可能拿到的最大奖励

用公式说就是:

翻译成人话就是:

“在路口往右转值多少分?” = “往右转后羊群不会乱跑(+3分奖励),然后我继续走最优路线还能再得10分” = 3 + 0.9 × 10 = 12分

这个递推关系就是Bellman方程的核心。理解了这个,你就理解了Q学习90%的精髓。

牧羊犬故事的启示

  1. 狗子不是背路线图:它学会的是”在什么情况下做什么动作最值”,而不是死记硬背每一步
  2. 狗子会犯错:有时候选了”8分”的动作,但羊突然发疯跑了,所以Q值要不断更新
  3. 狗子需要探索:如果狗子永远只走同一条路,它可能错过更高效的路线
  4. 狗子有耐心:它不是跑一趟就学会了,而是跑了几十趟才把Q表填准确

这就是Q学习的精髓:在试错中学习,在学习中改进


一、Q学习算法原理

1.1 先问一个问题:什么是”好动作”?

在强化学习里,“好动作”不是指动作本身,而是指从这个动作开始,最终能拿到的总奖励有多少

举个例子:

  • 动作A:立刻给你1块钱,但后面啥也没有了
  • 动作B:现在亏1块钱,但后面能慢慢赚10块钱

从长远来看,B才是好动作。Q学习就是要学会计算这个”长远价值”。

1.2 核心公式

Q学习的更新公式看起来吓人,但其实很简单:

让我拆开来讲:

Q(s,a) <—— 更新后的分数
    ↑
    |   + α × [TD误差]
    |
    |        ┌─────────────────┬────────────────────┐
    |        │                 │                    │
    |        │ r + γ·max Q(s',a')  -  Q(s,a)       │
    |        │    ↑               ↑                 │
    |        │    │               │                 │
    |        │ 这一刻的        之前认为              │
    |        │ 实际收获        这个动作值多少分       │
    |        │                                │
    |        └────────────────────────────────┘
    |
    α = 学习率(0到1之间)

用人话说就是:

“我之前觉得这个动作值5分(Q(s,a)),现在走了一步发现其实值7分(r + γ·max Q(s’,a’)),差了2分。那我就把评分从5分更新到5 + α×2。“

1.3 离策略特性:这个特性很厉害

Q学习是个”离策略”算法。啥意思?

  • 行为策略:你实际执行的策略(比如ε-greedy,到处乱跑)
  • 目标策略:你学习的目标(比如总是选最优动作)

普通的学习方法:你用什么策略走路,就只能从这种走法中学东西

Q学习:你随便跑,但学到的是”如果我每次都选最优,会怎么样”

这就好比:

  • 普通方法:只有跟着好老师学才能进步
  • Q学习:看别人跑一遍,自己就知道”最好的跑法应该是这样”

这个特性让Q学习的样本利用率很高——你跑过的每一步都能拿来学习。


第二部分:手把手实现——从安装到训练

二、环境准备与依赖安装

2.1 安装必要的库

pip install numpy matplotlib gym torch torchvision

或者一次性安装强化学习常用的库:

pip install numpy matplotlib gymnasium torch torchvision pyvirtualdisplay

关于gym版本

  • gym==0.26.0 及以上版本API有变化
  • gymnasium 是社区维护的新版本 新版本返回的是 (state, info) 而不是原来的 state,注意区分

2.2 第一个Q学习程序:FrozenLake走迷宫

FrozenLake是一个经典入门环境:4x4的格子,冰面很滑(可能滑到相邻格子),目标是走到终点。

"""
第一个Q学习程序:让AI学会走FrozenLake
环境:4x4格子世界
目标:从起点走到终点G,不要掉进洞里
"""
 
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
from collections import defaultdict
 
# ============ 第一部分:Q-Learning Agent ============
class QLearningAgent:
    """
    Q学习智能体
    
    做的事情很简单:
    1. 看到状态s,选个动作a(ε-greedy策略)
    2. 做了动作a,得到奖励r和新状态s'
    3. 更新Q(s,a)的值
    4. 重复直到熟练
    """
    
    def __init__(
        self,
        n_states: int,
        n_actions: int,
        learning_rate: float = 0.1,
        gamma: float = 0.99,
        epsilon: float = 1.0,
        epsilon_decay: float = 0.995,
        epsilon_min: float = 0.01
    ):
        """
        初始化智能体
        
        参数说明:
        - n_states: 有多少个状态(FrozenLake是16个格子)
        - n_actions: 有多少个动作(通常4个:上下左右)
        - learning_rate (α): 每次更新走多远,0.1是常用的默认值
        - gamma (γ): 折扣因子,0.99意味着100步后的奖励只值现在的0.37
        - epsilon (ε): 探索率,1.0意味着100%随机探索
        """
        self.n_states = n_states
        self.n_actions = n_actions
        self.alpha = learning_rate
        self.gamma = gamma
        self.epsilon = epsilon
        self.epsilon_decay = epsilon_decay
        self.epsilon_min = epsilon_min
        
        # Q表:核心数据结构
        # Q[s][a] = 在状态s下做动作a预计能拿多少分
        self.Q = np.zeros((n_states, n_actions))
    
    def choose_action(self, state: int) -> int:
        """
        ε-greedy动作选择
        
        有ε的概率随机选(探索),有(1-ε)的概率选当前最优(利用)
        """
        if np.random.random() < self.epsilon:
            # 随机选:探索新可能
            return np.random.randint(self.n_actions)
        else:
            # 贪心选:选Q值最高的
            return np.argmax(self.Q[state])
    
    def update(self, state: int, action: int, reward: float, 
               next_state: int, done: bool) -> float:
        """
        Q学习核心更新
        
        公式:Q(s,a) += α × [r + γ × max Q(s',a') - Q(s,a)]
        
        如果游戏结束了(next_state是终态),就不用加未来奖励了
        """
        if done:
            # 终态:下一步没有奖励了
            td_target = reward
        else:
            # 非终态:还有未来奖励可以期待
            td_target = reward + self.gamma * np.max(self.Q[next_state])
        
        # TD误差:实际得到的和预期的差了多少
        td_error = td_target - self.Q[state, action]
        
        # 更新Q值
        self.Q[state, action] += self.alpha * td_error
        
        # ε衰减:越到后面越少探索,多利用已学到的知识
        self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
        
        return td_error
    
    def get_policy(self) -> np.ndarray:
        """从Q表导出确定性策略"""
        return np.argmax(self.Q, axis=1)
 
 
# ============ 第二部分:训练循环 ============
def train_frozenlake():
    """
    训练Q学习智能体走FrozenLake
    """
    # 创建环境
    # FrozenLake-v1: 冰面会滑(有一定随机性)
    # is_slippery=True: 动作有随机性,可能滑到相邻格子
    env = gym.make('FrozenLake-v1', is_slippery=True)
    
    # 获取状态和动作数量
    n_states = env.observation_space.n  # 16
    n_actions = env.action_space.n      # 4 (上下左右)
    
    print(f"环境信息:{n_states}个状态,{n_actions}个动作")
    print("动作映射:0=左, 1=下, 2=右, 3=上")
    
    # 创建智能体
    agent = QLearningAgent(
        n_states=n_states,
        n_actions=n_actions,
        learning_rate=0.1,
        gamma=0.99,
        epsilon=1.0,
        epsilon_decay=0.999,
        epsilon_min=0.01
    )
    
    # 训练参数
    num_episodes = 10000
    
    # 记录训练曲线
    episode_rewards = []
    episode_lengths = []
    success_count = 0
    
    print("\n开始训练...")
    print("-" * 50)
    
    for episode in range(num_episodes):
        # 重置环境,获取初始状态
        state, info = env.reset()
        total_reward = 0
        steps = 0
        done = False
        
        while not done:
            # 1. 选动作
            action = agent.choose_action(state)
            
            # 2. 执行动作
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated
            
            # 3. 更新Q表
            agent.update(state, action, reward, next_state, done)
            
            # 4. 准备下一步
            state = next_state
            total_reward += reward
            steps += 1
        
        episode_rewards.append(total_reward)
        episode_lengths.append(steps)
        
        if total_reward == 1:
            success_count += 1
        
        # 每1000个回合打印一次进度
        if (episode + 1) % 1000 == 0:
            recent_success = success_count / 1000 * 100
            avg_reward = np.mean(episode_rewards[-1000:])
            print(f"回合 {episode+1:5d} | "
                  f"成功率: {recent_success:5.1f}% | "
                  f"平均奖励: {avg_reward:.3f} | "
                  f"ε: {agent.epsilon:.3f}")
            success_count = 0
    
    env.close()
    
    # ============ 第三部分:结果可视化 ============
    print("\n" + "=" * 50)
    print("训练完成!")
    print("=" * 50)
    
    # 绘制训练曲线
    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
    
    # 奖励曲线(用移动平均平滑)
    window = 100
    if len(episode_rewards) >= window:
        smoothed = np.convolve(episode_rewards, 
                              np.ones(window)/window, mode='valid')
        axes[0].plot(smoothed)
    else:
        axes[0].plot(episode_rewards)
    axes[0].set_xlabel('Episode')
    axes[0].set_ylabel('Reward')
    axes[0].set_title('Training Reward Curve')
    axes[0].grid(True, alpha=0.3)
    
    # 回合长度曲线
    if len(episode_lengths) >= window:
        smoothed_len = np.convolve(episode_lengths,
                                   np.ones(window)/window, mode='valid')
        axes[1].plot(smoothed_len)
    else:
        axes[1].plot(episode_lengths)
    axes[1].set_xlabel('Episode')
    axes[1].set_ylabel('Steps')
    axes[1].set_title('Episode Length')
    axes[1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('frozenlake_training.png', dpi=150)
    plt.show()
    
    # 打印学到的策略
    print("\n学到的策略(Q表):")
    print(agent.Q.round(2))
    
    print("\n最终策略(每个状态往哪走):")
    policy = agent.get_policy()
    action_names = ['← 左', '↓ 下', '→ 右', '↑ 上']
    for s in range(n_states):
        if s == 0:
            print(f"  起点 → {action_names[policy[s]]}")
        elif s == 15:
            print(f"  终点 → {action_names[policy[s]]} (实际是终点)")
        else:
            print(f"  格子{s}{action_names[policy[s]]}")
    
    return agent, episode_rewards
 
 
# 运行训练
if __name__ == "__main__":
    agent, rewards = train_frozenlake()

2.3 运行结果解读

运行上面的代码,你会看到类似这样的输出:

环境信息:16个状态,4个动作
动作映射:0=左, 1=下, 2=右, 3=上

开始训练...
--------------------------------------------------
回合  1000 | 成功率:  10.2% | 平均奖励: 0.102 | ε: 0.368
回合  2000 | 成功率:  45.3% | 平均奖励: 0.453 | ε: 0.135
回合  3000 | 成功率:  58.7% | 平均奖励: 0.587 | ε: 0.050
回合  4000 | 成功率:  72.1% | 平均奖励: 0.721 | ε: 0.018
回合  5000 | 成功率:  76.8% | 平均奖励: 0.768 | ε: 0.010
...

你会发现:

  • ε从1.0慢慢降到0.01:说明智能体从”瞎跑”逐渐变成”用经验”
  • 成功率从10%提升到70%+:说明它真的学到了
  • 训练曲线逐渐上升:证明学习在起作用

第三部分:深入原理

三、为什么Q学习有效?—— 从直觉到Bellman方程

3.1 一个简单的例子

假设你在玩一个游戏:

  • 从A出发
  • 每走一步扣1分
  • 走到终点Z得100分

最短路线是 A → C → Z,共3步,得分 = 100 - 3 = 97分。

如果走错了绕远路,可能要10步,得分 = 100 - 10 = 90分。

Q学习要学会的就是:在每个格子,应该往哪个方向走,最终能拿最高分。

3.2 Bellman方程:递归的智慧

Bellman方程的核心洞察是:

当前位置的价值 = 现在的奖励 + 未来位置的价值

用公式写出来:

或者Q值版本:

这个方程好在哪?

它把一个大问题拆成了两个小问题

  1. “现在能得到多少”(R)
  2. “从新位置出发能再得到多少”(γ·max Q)

3.3 迭代求解:一步一步逼近真相

我们不知道正确答案,但可以猜一个初始值,然后不断改进。

第0轮猜测:每个格子的Q值都猜0
第1轮:根据Bellman方程更新一遍
第2轮:再更新一遍
第3轮:再更新一遍
...
最终:Q值收敛到正确答案

这就是价值迭代。数学上可以证明,只要满足一些条件,这个过程一定会收敛到最优解。

3.4 为什么叫”时序差分”?

“差分”就是差别的意思,“时序差分”就是”时间上的差别”。

TD误差 = 实际得到的 - 之前的预期

之前觉得这个动作值 5 分
实际走了一步得到 3 分 + 未来还有 2 分的潜力
TD误差 = (3 + 2) - 5 = 0

但如果实际走了一步得到 8 分
TD误差 = 8 - 5 = +3
说明我之前低估了,应该把评分调高

边走边学,这就是”时序差分学习”的含义。


四、ε-greedy探索策略:怎么平衡”尝试新路线”和”走已知的路”?

4.1 探索的两难

想象你在找一家好吃的餐厅:

  • 利用:去你吃过的、确定好吃的餐厅(稳妥但可能错过更好的)
  • 探索:去一家没去过的餐厅(可能踩雷但也可能发现宝藏)

这就是强化学习里的探索-利用困境(Explore-Exploitation Dilemma)

4.2 ε-greedy:最简单的解决方案

ε-greedy的思路很直接:

def choose_action(Q_values, epsilon):
    """
    Q_values: 每个动作的Q值
    epsilon: 探索概率
    """
    if random.random() < epsilon:
        # 探索:随机选一个
        return random.choice(len(Q_values))
    else:
        # 利用:选当前最优
        return argmax(Q_values)

4.3 ε衰减:先广撒网,后精准钓鱼

一开始啥都不知道,应该多探索;后来经验丰富了,就主要用经验。

class EpsilonScheduler:
    """
    ε调度器:控制探索率的变化
    """
    
    def __init__(
        self,
        initial_epsilon: float = 1.0,      # 一开始完全随机
        final_epsilon: float = 0.01,          # 最后保留1%探索
        exploration_fraction: float = 0.1    # 用10%的训练时间从1降到0.01
    ):
        self.initial_epsilon = initial_epsilon
        self.final_epsilon = final_epsilon
        self.exploration_fraction = exploration_fraction
    
    def get_epsilon(self, current_step: int, total_steps: int) -> float:
        """
        线性衰减:按训练进度从initial降到final
        """
        fraction = min(current_step / (total_steps * self.exploration_fraction), 1.0)
        return self.initial_epsilon + fraction * (self.final_epsilon - self.initial_epsilon)
 
 
class ExponentialEpsilonScheduler:
    """
    指数衰减:每一步都乘一个小于1的数
    """
    
    def __init__(
        self,
        initial_epsilon: float = 1.0,
        final_epsilon: float = 0.01,
        decay_steps: int = 10000
    ):
        self.decay_rate = (final_epsilon / initial_epsilon) ** (1 / decay_steps)
        self.initial_epsilon = initial_epsilon
    
    def get_epsilon(self, current_step: int) -> float:
        return max(self.initial_epsilon * (self.decay_rate ** current_step), 0.01)

4.4 其他探索策略

Boltzmann探索:不用ε,而是看Q值的”温度”

def boltzmann_exploration(Q_values, temperature=1.0):
    """
    温度高:所有动作概率差不多(高探索)
    温度低:概率集中在高Q值动作(高利用)
    """
    exp_values = np.exp(Q_values / temperature)
    probs = exp_values / exp_values.sum()
    return np.random.choice(len(Q_values), p=probs)

UCB(Upper Confidence Bound):给访问少的动作加分

def ucb_action(Q_values, visit_counts, t, c=2.0):
    """
    c: 探索常数,越大越鼓励探索
    """
    ucb_values = Q_values + c * np.sqrt(np.log(t) / (visit_counts + 1))
    return np.argmax(ucb_values)

第四部分:完整可运行代码——从安装到训练到可视化

五、实战项目一:用Q学习走迷宫

5.1 自定义迷宫环境

"""
自定义迷宫环境
"""
 
import numpy as np
import gymnasium as gym
from gymnasium import spaces
 
class MazeEnv(gym.Env):
    """
    自定义迷宫环境
    
    地图:
    S  .  .  .  #
    .  #  .  #  .
    .  #  .  .  .
    .  .  .  #  .
    #  #  .  .  G
    
    S: 起点 (0,0)
    G: 终点 (4,4)
    #: 障碍物
    .: 空地
    """
    
    metadata = {'render_modes': ['human', 'ansi']}
    
    def __init__(self, render_mode=None):
        super().__init__()
        
        self.render_mode = render_mode
        
        # 地图:0=空地, 1=障碍, 2=起点, 3=终点
        self.maze = np.array([
            [2, 0, 0, 0, 1],
            [0, 1, 0, 1, 0],
            [0, 1, 0, 0, 0],
            [0, 0, 0, 1, 0],
            [1, 1, 0, 0, 3]
        ])
        
        self.height, self.width = self.maze.shape
        
        # 动作:0=上, 1=下, 2=左, 3=右
        self.action_space = spaces.Discrete(4)
        
        # 状态空间:位置 (y, x)
        self.observation_space = spaces.Box(
            low=0, high=max(self.height, self.width), 
            shape=(2,), dtype=np.int32
        )
        
        # 起点和终点
        self.start_pos = np.argwhere(self.maze == 2)[0]
        self.goal_pos = np.argwhere(self.maze == 3)[0]
        
        # 初始状态
        self.current_pos = self.start_pos.copy()
    
    def reset(self, seed=None, options=None):
        """重置环境"""
        super().reset(seed=seed)
        self.current_pos = self.start_pos.copy()
        return self.current_pos, {}
    
    def step(self, action):
        """执行动作"""
        # 动作转移动
        moves = {
            0: (-1, 0),  # 上
            1: (1, 0),   # 下
            2: (0, -1),  # 左
            3: (0, 1)    # 右
        }
        
        dy, dx = moves[action]
        new_pos = self.current_pos + np.array([dy, dx])
        
        # 检查边界
        if 0 <= new_pos[0] < self.height and 0 <= new_pos[1] < self.width:
            # 检查障碍物
            if self.maze[new_pos[0], new_pos[1]] != 1:
                self.current_pos = new_pos
        
        # 判断是否到达终点
        at_goal = np.array_equal(self.current_pos, self.goal_pos)
        
        # 计算奖励
        if at_goal:
            reward = 100
            terminated = True
        else:
            # 每走一步扣1分,鼓励快速到达
            reward = -1
            terminated = False
        
        truncated = False
        info = {}
        
        return self.current_pos.copy(), reward, terminated, truncated, info
    
    def render(self):
        """渲染迷宫"""
        if self.render_mode == 'ansi':
            display = self.maze.copy().astype(str)
            display[self.current_pos[0], self.current_pos[1]] = 'X'
            display[self.maze == 1] = '#'
            display[self.maze == 0] = '.'
            display[self.maze == 2] = 'S'
            display[self.maze == 3] = 'G'
            print('\n'.join([' '.join(row) for row in display]))
            print()
    
    def get_state(self):
        """获取当前位置作为状态"""
        return self.current_pos[0] * self.width + self.current_pos[1]
 
 
def state_to_pos(state, width):
    """状态转位置"""
    return state // width, state % width
 
 
# ============ 完整可运行的Q学习走迷宫 ============
 
def train_maze_solver():
    """
    训练Q学习智能体走迷宫
    """
    env = MazeEnv(render_mode='ansi')
    
    # 离散化状态空间:每个格子一个状态
    n_states = env.height * env.width
    n_actions = env.action_space.n
    
    print(f"迷宫大小: {env.height}x{env.width}")
    print(f"状态数: {n_states}, 动作数: {n_actions}")
    print("动作: 0=上, 1=下, 2=左, 3=右")
    print()
    
    # Q表初始化
    Q = np.zeros((n_states, n_actions))
    
    # 超参数
    alpha = 0.1       # 学习率
    gamma = 0.95      # 折扣因子
    epsilon = 1.0     # 初始探索率
    epsilon_decay = 0.995
    epsilon_min = 0.01
    
    # 训练
    num_episodes = 500
    episode_rewards = []
    steps_list = []
    
    for episode in range(num_episodes):
        state, _ = env.reset()
        state = env.get_state()
        total_reward = 0
        steps = 0
        done = False
        
        while not done:
            # ε-greedy选择动作
            if np.random.random() < epsilon:
                action = env.action_space.sample()
            else:
                action = np.argmax(Q[state])
            
            # 执行
            next_pos, reward, terminated, truncated, _ = env.step(action)
            next_state = env.get_state()
            done = terminated or truncated
            
            # Q学习更新
            Q[state, action] += alpha * (
                reward + gamma * np.max(Q[next_state]) - Q[state, action]
            )
            
            state = next_state
            total_reward += reward
            steps += 1
        
        episode_rewards.append(total_reward)
        steps_list.append(steps)
        epsilon = max(epsilon_min, epsilon * epsilon_decay)
        
        if (episode + 1) % 100 == 0:
            avg_reward = np.mean(episode_rewards[-100:])
            avg_steps = np.mean(steps_list[-100:])
            print(f"Episode {episode+1}: 平均奖励={avg_reward:.1f}, "
                  f"平均步数={avg_steps:.1f}, ε={epsilon:.3f}")
    
    # 测试:让智能体走一次
    print("\n" + "=" * 50)
    print("测试:智能体走迷宫")
    print("=" * 50)
    
    state, _ = env.reset()
    state = env.get_state()
    env.render()
    
    done = False
    step = 0
    
    while not done and step < 100:
        action = np.argmax(Q[state])
        next_pos, reward, terminated, truncated, _ = env.step(action)
        next_state = env.get_state()
        done = terminated or truncated
        
        print(f"步骤{step+1}: 动作={action}, ", end="")
        state = next_pos
        state = env.get_state()
        env.render()
        
        step += 1
    
    return Q, episode_rewards
 
 
if __name__ == "__main__":
    Q, rewards = train_maze_solver()

5.2 训练结果可视化

"""
绘制训练曲线和Q值热力图
"""
 
import matplotlib.pyplot as plt
import numpy as np
 
def visualize_results(rewards, Q, maze_shape=(5, 5)):
    """
    可视化训练结果
    """
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    
    # 1. 训练奖励曲线
    window = 50
    if len(rewards) >= window:
        smoothed = np.convolve(rewards, np.ones(window)/window, mode='valid')
        axes[0].plot(smoothed, 'b-', alpha=0.8)
    axes[0].set_xlabel('Episode')
    axes[0].set_ylabel('Total Reward')
    axes[0].set_title('Training Reward Curve')
    axes[0].grid(True, alpha=0.3)
    
    # 2. Q值热力图(每个状态的最高Q值)
    max_q = np.max(Q, axis=1).reshape(maze_shape)
    im = axes[1].imshow(max_q, cmap='YlOrRd')
    axes[1].set_title('Max Q-Value per State')
    axes[1].set_xlabel('X')
    axes[1].set_ylabel('Y')
    plt.colorbar(im, ax=axes[1], label='Max Q')
    
    # 3. 最优动作可视化
    best_actions = np.argmax(Q, axis=1).reshape(maze_shape)
    action_arrows = ['↑', '↓', '←', '→']
    action_chars = np.array([[action_arrows[a] for a in row] 
                              for row in best_actions])
    
    for i in range(maze_shape[0]):
        for j in range(maze_shape[1]):
            axes[2].text(j, i, action_chars[i, j], 
                         ha='center', va='center', fontsize=14)
    
    axes[2].set_xlim(-0.5, maze_shape[1] - 0.5)
    axes[2].set_ylim(maze_shape[0] - 0.5, -0.5)
    axes[2].set_title('Optimal Policy')
    axes[2].set_xlabel('X')
    axes[2].set_ylabel('Y')
    axes[2].set_xticks([])
    axes[2].set_yticks([])
    
    plt.tight_layout()
    plt.savefig('maze_results.png', dpi=150)
    plt.show()

六、实战项目二:用Q学习玩CartPole(倒立摆)

CartPole是控制论的经典问题:让小车上的杆子保持平衡。

"""
Q学习玩CartPole(连续状态空间的离散化处理)
"""
 
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
 
class DiscretizedQLearning:
    """
    离散化Q学习:处理连续状态空间
    
    核心思路:把连续空间分成很多小格子,每个格子当一个离散状态
    """
    
    def __init__(self, n_bins=8):
        """
        n_bins: 每个状态维度分成几个格子
        """
        self.n_bins = n_bins
        
        # CartPole的状态定义
        # 0: 小车位置 (-2.4 ~ 2.4)
        # 1: 小车速度 (-inf ~ inf)
        # 2: 杆子角度 (-0.21 ~ 0.21 rad)
        # 3: 杆子角速度 (-inf ~ inf)
        self.state_bounds = [
            [-2.4, 2.4],     # 小车位置
            [-3.0, 3.0],     # 小车速度(截断)
            [-0.21, 0.21],   # 杆子角度
            [-2.0, 2.0]      # 杆子角速度(截断)
        ]
        
        # Q表:4维离散状态 × 2个动作
        shape = [n_bins] * 4 + [2]
        self.Q = np.zeros(shape)
        
        # 访问计数(用于计算学习率)
        self.counts = np.zeros(shape)
    
    def discretize(self, state):
        """
        把连续状态转成离散索引
        """
        indices = []
        for i, (val, (low, high)) in enumerate(zip(state, self.state_bounds)):
            # 归一化到[0, 1]
            ratio = (val - low) / (high - low)
            ratio = np.clip(ratio, 0, 1)
            # 转成格子索引
            index = int(ratio * (self.n_bins - 1))
            indices.append(index)
        return tuple(indices)
    
    def choose_action(self, state):
        """ε-greedy动作选择"""
        if np.random.random() < self.epsilon:
            return np.random.randint(2)
        return np.argmax(self.Q[self.discretize(state)])
    
    def update(self, state, action, reward, next_state, done):
        """Q学习更新"""
        s = self.discretize(state)
        ns = self.discretize(next_state)
        
        # 动态学习率:访问越多,学得越少
        self.counts[s + (action,)] += 1
        alpha = 1.0 / self.counts[s + (action,)]
        
        if done:
            target = reward
        else:
            target = reward + 0.99 * np.max(self.Q[ns])
        
        td_error = target - self.Q[s + (action,)]
        self.Q[s + (action,)] += alpha * td_error
        
        # ε衰减
        self.epsilon = max(0.01, self.epsilon * 0.995)
 
 
def train_cartpole():
    """训练CartPole"""
    env = gym.make('CartPole-v1')
    
    agent = DiscretizedQLearning(n_bins=8)
    agent.epsilon = 1.0
    
    num_episodes = 5000
    episode_rewards = []
    recent_rewards = []
    
    print("开始训练CartPole...")
    print("-" * 50)
    
    for episode in range(num_episodes):
        state, _ = env.reset()
        total_reward = 0
        done = False
        
        while not done:
            action = agent.choose_action(state)
            next_state, reward, terminated, truncated, _ = env.step(action)
            done = terminated or truncated
            
            agent.update(state, action, reward, next_state, done)
            state = next_state
            total_reward += reward
        
        episode_rewards.append(total_reward)
        recent_rewards.append(total_reward)
        
        if (episode + 1) % 500 == 0:
            avg = np.mean(recent_rewards[-500:])
            print(f"Episode {episode+1}: 最近500回合平均奖励 = {avg:.1f}")
    
    env.close()
    
    # 绘制结果
    plt.figure(figsize=(12, 4))
    
    plt.subplot(1, 2, 1)
    window = 100
    smoothed = np.convolve(episode_rewards, np.ones(window)/window, mode='valid')
    plt.plot(smoothed)
    plt.xlabel('Episode')
    plt.ylabel('Reward')
    plt.title('Training Curve (Smoothed)')
    plt.axhline(y=475, color='r', linestyle='--', label='Solved threshold')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    plt.subplot(1, 2, 2)
    plt.hist(episode_rewards[-500:], bins=30, edgecolor='black', alpha=0.7)
    plt.xlabel('Episode Reward')
    plt.ylabel('Count')
    plt.title('Reward Distribution (Last 500 Episodes)')
    plt.axvline(x=np.mean(episode_rewards[-500:]), color='r', 
                linestyle='--', label=f'Mean: {np.mean(episode_rewards[-500:]):.1f}')
    plt.legend()
    
    plt.tight_layout()
    plt.savefig('cartpole_results.png', dpi=150)
    plt.show()
    
    # 测试:跑10回合看平均分
    print("\n" + "=" * 50)
    print("测试:跑10回合")
    print("=" * 50)
    
    test_env = gym.make('CartPole-v1')
    for ep in range(10):
        state, _ = test_env.reset()
        total = 0
        done = False
        while not done:
            action = agent.choose_action(state)
            state, reward, terminated, truncated, _ = test_env.step(action)
            done = terminated or truncated
            total += reward
        print(f"回合{ep+1}: 得分 = {total}")
    test_env.close()
    
    return agent, episode_rewards
 
 
if __name__ == "__main__":
    agent, rewards = train_cartpole()

第五部分:进阶内容

七、深度Q网络(DQN):当状态空间太大时

7.1 Q表解决不了的问题

FrozenLake只有16个状态,CartPole离散化后也只有8^4=4096个状态。

但如果是自动驾驶:输入是摄像头图像(640×480×3 = 92万个像素),每个像素取值0-255。

这种情况Q表根本没法用——没有足够的内存存储这么大的表,也没有足够的数据填满它。

7.2 用神经网络代替Q表

DQN的核心思想:用神经网络近似Q函数。

Q表:Q[state][action] = 值
DQN:Q_network(state) → [值1, 值2, 值3...] = 每个动作的值

PyTorch实现:

"""
DQN实现:用神经网络玩CartPole
"""
 
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
from collections import deque
import random
 
 
# ============ 第一部分:Q网络定义 ============
class QNetwork(nn.Module):
    """
    简单的Q网络
    
    输入:状态(4维向量)
    输出:每个动作的Q值
    """
    
    def __init__(self, state_dim=4, action_dim=2, hidden_dim=128):
        super().__init__()
        
        self.network = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)
        )
    
    def forward(self, x):
        """前向传播"""
        return self.network(x)
 
 
# ============ 第二部分:经验回放缓冲区 ============
class ReplayBuffer:
    """
    经验回放:存储agent的经历,用于随机采样打破数据相关性
    """
    
    def __init__(self, capacity=10000):
        self.buffer = deque(maxlen=capacity)
    
    def push(self, state, action, reward, next_state, done):
        """存储一条经验"""
        self.buffer.append((state, action, reward, next_state, done))
    
    def sample(self, batch_size):
        """随机采样一批经验"""
        batch = random.sample(self.buffer, batch_size)
        states, actions, rewards, next_states, dones = zip(*batch)
        
        # 转成tensor
        return (
            torch.FloatTensor(np.array(states)),
            torch.LongTensor(actions),
            torch.FloatTensor(rewards),
            torch.FloatTensor(np.array(next_states)),
            torch.FloatTensor(dones)
        )
    
    def __len__(self):
        return len(self.buffer)
 
 
# ============ 第三部分:DQN Agent ============
class DQNAgent:
    """
    DQN智能体
    
    和普通Q学习的区别:
    1. 用神经网络代替Q表
    2. 使用经验回放
    3. 使用目标网络稳定训练
    """
    
    def __init__(self, state_dim=4, action_dim=2):
        self.state_dim = state_dim
        self.action_dim = action_dim
        
        # Q网络:用于选择动作
        self.q_network = QNetwork(state_dim, action_dim)
        
        # 目标网络:用于计算目标Q值
        self.target_network = QNetwork(state_dim, action_dim)
        self.target_network.load_state_dict(self.q_network.state_dict())
        
        # 优化器
        self.optimizer = optim.Adam(self.q_network.parameters(), lr=0.001)
        
        # 经验回放
        self.replay_buffer = ReplayBuffer(capacity=50000)
        
        # 训练参数
        self.gamma = 0.99          # 折扣因子
        self.batch_size = 64       # 每次从buffer里取多少条经验
        self.target_update_freq = 10  # 每多少步更新一次目标网络
        self.train_step = 0
        
        # 探索参数
        self.epsilon = 1.0
        self.epsilon_decay = 0.995
        self.epsilon_min = 0.01
    
    def choose_action(self, state):
        """ε-greedy动作选择"""
        if np.random.random() < self.epsilon:
            return np.random.randint(self.action_dim)
        
        with torch.no_grad():
            state_tensor = torch.FloatTensor(state).unsqueeze(0)
            q_values = self.q_network(state_tensor)
            return q_values.argmax().item()
    
    def store_transition(self, state, action, reward, next_state, done):
        """存储经验"""
        self.replay_buffer.push(state, action, reward, next_state, done)
    
    def train(self):
        """训练一步"""
        if len(self.replay_buffer) < self.batch_size:
            return None
        
        # 采样
        states, actions, rewards, next_states, dones = \
            self.replay_buffer.sample(self.batch_size)
        
        # 当前Q值
        current_q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze()
        
        # 目标Q值(用目标网络)
        with torch.no_grad():
            next_q = self.target_network(next_states).max(1)[0]
            target_q = rewards + (1 - dones) * self.gamma * next_q
        
        # 计算损失
        loss = nn.MSELoss()(current_q, target_q)
        
        # 反向传播
        self.optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), 1.0)  # 梯度裁剪
        self.optimizer.step()
        
        # 更新目标网络
        self.train_step += 1
        if self.train_step % self.target_update_freq == 0:
            self.target_network.load_state_dict(self.q_network.state_dict())
        
        # ε衰减
        self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
        
        return loss.item()
 
 
# ============ 第四部分:训练主循环 ============
def train_dqn_cartpole():
    """训练DQN玩CartPole"""
    
    env = gym.make('CartPole-v1')
    agent = DQNAgent(state_dim=4, action_dim=2)
    
    num_episodes = 500
    episode_rewards = []
    losses = []
    
    print("开始训练DQN...")
    print("-" * 50)
    
    for episode in range(num_episodes):
        state, _ = env.reset()
        total_reward = 0
        done = False
        
        while not done:
            # 选择动作
            action = agent.choose_action(state)
            
            # 执行
            next_state, reward, terminated, truncated, _ = env.step(action)
            done = terminated or truncated
            
            # 存储
            agent.store_transition(state, action, reward, next_state, done)
            
            # 训练
            loss = agent.train()
            if loss is not None:
                losses.append(loss)
            
            state = next_state
            total_reward += reward
        
        episode_rewards.append(total_reward)
        
        if (episode + 1) % 50 == 0:
            avg_reward = np.mean(episode_rewards[-50:])
            avg_loss = np.mean(losses[-1000:]) if losses else 0
            print(f"Episode {episode+1:4d} | "
                  f"奖励: {avg_reward:6.1f} | "
                  f"损失: {avg_loss:.4f} | "
                  f"ε: {agent.epsilon:.3f}")
    
    env.close()
    
    # 绘制结果
    plt.figure(figsize=(12, 4))
    
    plt.subplot(1, 2, 1)
    window = 20
    smoothed = np.convolve(episode_rewards, np.ones(window)/window, mode='valid')
    plt.plot(smoothed)
    plt.xlabel('Episode')
    plt.ylabel('Reward')
    plt.title('DQN Training Curve')
    plt.axhline(y=475, color='r', linestyle='--', label='Solved threshold')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    plt.subplot(1, 2, 2)
    if losses:
        smoothed_loss = np.convolve(losses, np.ones(100)/100, mode='valid')
        plt.plot(smoothed_loss)
        plt.xlabel('Training Step')
        plt.ylabel('Loss')
        plt.title('Training Loss')
        plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('dqn_results.png', dpi=150)
    plt.show()
    
    return agent, episode_rewards
 
 
if __name__ == "__main__":
    agent, rewards = train_dqn_cartpole()

7.3 DQN为什么work?

DQN有三个核心技巧:

  1. 经验回放(Experience Replay)

    • 问题:连续的经验可能是相关的(比如一直往前走)
    • 解决:从buffer里随机采样,打破相关性
  2. 目标网络(Target Network)

    • 问题:用同一个网络计算当前Q和目标Q,会导致训练不稳定
    • 解决:用一个”老”的网络算目标,定期更新
  3. 梯度裁剪(Gradient Clipping)

    • 问题:梯度可能爆炸
    • 解决:把梯度裁剪到[-1, 1]范围

八、调参实战:学习率、折扣因子、ε怎么调?

8.1 调参的基本思路

调参不是玄学,有一些基本规律:

参数太大太小推荐范围调节经验
学习率 α震荡不收敛收敛太慢0.001-0.1常用0.01或0.001
折扣因子 γ可能过度考虑未来只看眼前利益0.9-0.999任务越长,值越大
初始 ε探索不够探索太多浪费0.5-1.0常用1.0
ε衰减率探索不充分收敛太慢0.99-0.9999保证最后还有1%探索

8.2 实战调参建议

"""
调参经验总结
"""
 
TUNING_TIPS = {
    "学习率": {
        "问题": "Q值爆炸、振荡不收敛",
        "检查": "梯度是否爆炸",
        "解决": [
            "降低学习率(×0.1)",
            "使用学习率衰减",
            "添加梯度裁剪",
            "用Adam代替SGD"
        ]
    },
    
    "折扣因子": {
        "问题": "短视行为 / 收敛慢",
        "检查": "智能体是否只看眼前奖励",
        "解决": [
            "任务越长,γ越接近1",
            "γ=0.9适合短任务",
            "γ=0.99适合长任务(如Atari)",
            "γ=0.999适合超长任务"
        ]
    },
    
    "探索率": {
        "问题": "早熟收敛 / 探索不够",
        "检查": "ε是否降得太快",
        "解决": [
            "增大衰减步数",
            "增大最小ε(0.01→0.1)",
            "使用线性衰减而非指数衰减",
            "考虑Boltzmann探索"
        ]
    },
    
    "批量大小": {
        "问题": "训练不稳定 / 收敛慢",
        "检查": "batch内的TD误差方差",
        "解决": [
            "小batch(32):高方差但更新快",
            "大batch(256):稳定但可能陷入局部最优",
            "常用64或128",
            "DQN建议至少32"
        ]
    }
}

8.3 推荐的起始参数

# 小型离散环境(FrozenLake, Taxi等)
default_params_small = {
    "alpha": 0.1,           # 表格型Q学习可以用较大的学习率
    "gamma": 0.99,
    "epsilon": 1.0,
    "epsilon_decay": 0.999,
    "epsilon_min": 0.01
}
 
# 连续环境 + 神经网络
default_params_dqn = {
    "learning_rate": 0.001,   # 神经网络用小一点
    "gamma": 0.99,
    "batch_size": 64,
    "replay_buffer_size": 50000,
    "target_update_freq": 10,
    "epsilon": 1.0,
    "epsilon_decay": 0.995,
    "epsilon_min": 0.01
}

九、Q学习的局限性:什么时候不该用它?

9.1 表格型Q学习的局限

只适用于离散、状态空间小的问题
问题原因解决方案
状态空间爆炸10个二值状态=1024种组合函数逼近
连续动作空间Q表无法索引DDPG, SAC, TD3
部分可观测看不全环境POMDP方法
延迟奖励TD误差传播慢增加γ, 使用候选轨迹

9.2 函数逼近Q学习的局限

即使用了神经网络,Q学习也有根本性问题:

1. 不稳定甚至发散

  • 理论:非线性函数逼近下的Q学习没有收敛保证
  • 实践:可能学到完全错误的Q值
  • 解决:DQN的双网络、梯度TD、ExpectedSARSA

2. Q值过估计

  • 原理:max操作会系统性地高估Q值
  • 表现:学到的策略可能不是最优
  • 解决:Double DQN

3. 灾难性遗忘

  • 原理:新经验覆盖旧经验
  • 表现:学了新的忘了旧的
  • 解决:经验回放、优先级回放

9.3 什么时候选其他方法?

场景推荐方法
离散、小状态空间Q学习 ✓
高维连续输入(图像)DQN, Dueling DQN, Rainbow
连续动作空间DDPG, SAC, TD3
需要稳定训练PPO, A3C
在线交互代价高离线RL(CQL, IQL)
需要学习随机策略策略梯度(REINFORCE, PPO)

9.4 Q学习 vs 策略梯度

Q学习策略梯度
优点样本效率高、收敛稳定连续动作、自然随机策略
缺点连续动作处理困难高方差、需要大量样本
适用离散、小空间连续、大空间

十、常见问题排查

10.1 Q值不收敛

症状:Q值一直在波动,没有稳定趋势

检查清单

  1. 学习率太大?

    # 尝试:降低学习率
    alpha = 0.001  # 从0.1降到0.001
     
    # 或者:学习率衰减
    alpha = initial_alpha / (1 + decay * episode)
  2. 探索不够?

    # 尝试:增大ε衰减步数
    epsilon_decay = 0.9999  # 从0.999变得更慢
     
    # 或者:增大最小ε
    epsilon_min = 0.1  # 从0.01增加到0.1
  3. TD目标不稳定?

    # DQN:检查目标网络是否正确更新
    if step % target_update_freq == 0:
        target_network.load_state_dict(q_network.state_dict())

10.2 训练振荡

症状:奖励曲线上下剧烈波动

解决方案

# 1. 降低学习率
agent.alpha *= 0.1
 
# 2. 增大经验回放缓冲区
buffer = ReplayBuffer(capacity=200000)  # 从50000增大
 
# 3. 增大批量大小
batch_size = 128  # 从64增大到128
 
# 4. 添加梯度裁剪(DQN)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
 
# 5. 减小γ
gamma = 0.95  # 从0.99减小

10.3 早熟收敛

症状:很快收敛到一个次优策略,之后再也学不到更好的

诊断和解决

# 1. 检查ε是否降太快
# 日志输出epsilon值
if episode % 100 == 0:
    print(f"ε = {agent.epsilon:.4f}")
 
# 2. 增加探索时间
epsilon_decay = 0.9999  # 更慢的衰减
 
# 3. 使用Boltzmann探索替代ε-greedy
def boltzmann(Q, temperature=1.0):
    exp_q = np.exp(Q / temperature)
    probs = exp_q / exp_q.sum()
    return np.random.choice(len(Q), p=probs)
 
# 4. 减少初始Q值(防止过早选择某个动作)
Q = np.random.uniform(-1, 0, (n_states, n_actions))

10.4 Q值爆炸

症状:Q值变成inf或非常大的数

解决方案

# 1. 检查奖励是否有异常值
print(f"奖励范围: min={rewards.min()}, max={rewards.max()}")
 
# 2. 归一化奖励
reward = (raw_reward - reward_mean) / reward_std
 
# 3. 限制Q值范围
Q = np.clip(Q, -100, 100)
 
# 4. 使用Huber损失代替MSE
loss = nn.SmoothL1Loss()(current_q, target_q)
 
# 5. 梯度裁剪
torch.nn.utils.clip_grad_norm_(parameters, max_norm=1.0)

十一、进阶路线图:从Q学习到RainbowDQN

11.1 学习路径

第一阶段:基础(1-2周)
├── 理解MDP基础
├── 实现表格型Q学习
├── 实现ε-greedy
└── 在FrozenLake/Taxi上测试

第二阶段:深度Q学习(1-2周)
├── 实现DQN
├── 经验回放
├── 目标网络
└── 在CartPole上测试

第三阶段:DQN改进(1-2周)
├── Double DQN(解决过估计)
├── Dueling DQN(分离V和A)
├── Prioritized ER(优先级回放)
└── Noisy Networks(更好的探索)

第四阶段:现代方法(2-4周)
├── Rainbow DQN(集大成者)
├── DDPG(连续动作)
├── TD3(双延迟DDPG)
├── SAC(最大熵RL)
└── PPO(稳定策略梯度)

第五阶段:前沿与应用(持续)
├── 离线RL
├── 元学习
├── 多智能体RL
└── 模型预测控制

11.2 RainbowDQN:集大成者

Rainbow(2017)把7种DQN改进合在一起:

"""
RainbowDQN的组成部分
 
1. Double DQN:减少Q值过估计
2. Dueling DQN:分离状态价值和优势
3. Prioritized ER:智能采样重要经验
4. Multi-step bootstrap:TD(λ)思想
5. Distributional RL:预测奖励分布
6. Noisy Nets:可学习的探索
7. Categorical DQN:分布视角的DQN
"""

11.3 各变体对比

变体解决的问题提升效果
Double DQNQ值过估计稳定训练
Dueling DQN学习效率尤其状态多时有效
Prioritized ER样本利用2倍加速
Noisy Nets探索效率持续探索
Distributional精确估计更准确的值函数

第六部分:理论深化

十二、Bellman方程的直观理解

12.1 什么是Bellman方程?

Bellman方程(1957)是由动态规划大师Richard Bellman提出的。它把”长远利益”拆成两部分:

现在能拿到的 + 未来可能拿到的

12.2 为什么重要?

  1. 递归结构:把N步问题变成1步问题
  2. 最优子结构:最优策略由最优子策略组成
  3. 理论基础:Q学习、DQN都是这个方程的迭代求解

12.3 用例子理解

假设你在选择职业:

  • 状态s:你的技能水平
  • 动作a:选什么工作
  • 奖励R:工资
  • V(s):技能水平为s时,未来能赚多少钱

Bellman方程告诉你:

“选这份工作的价值 = 现在的工资 + 未来技能提升后能赚的钱”

12.4 压缩映射与收敛性

定义Bellman最优算子

核心性质-收缩映射

这意味着:反复应用 ,Q值必收敛到唯一不动点


十三、数学形式化总结

Q学习核心更新公式

等价形式(在线更新):

ε-greedy策略

TD误差

Bellman最优方程

Double DQN更新

Dueling DQN Q值分解


第七部分:参考与延伸

十四、相关文档


十五、参考文献

  1. Watkins, C. J. C. H. (1989). Learning from delayed rewards. PhD Thesis, Cambridge University.

  2. Watkins, C. J., & Dayan, P. (1992). Q-learning. Machine Learning, 8(3-4), 279-292.

  3. Sutton, R. S., & Barto, A. G. (2018). Reinforcement Learning: An Introduction (2nd ed.). MIT Press.

  4. Mnih, V., et al. (2015). Human-level control through deep reinforcement learning. Nature, 518(7540), 529-533.

  5. Mnih, V., et al. (2013). Playing Atari with deep reinforcement learning. arXiv:1312.5602.

  6. Hasselt, H. V. (2010). Double Q-learning. Advances in Neural Information Processing Systems, 23.

  7. Van Hasselt, H., Guez, A., & Silver, D. (2016). Deep reinforcement learning with double Q-learning. AAAI.

  8. Wang, Z., et al. (2016). Dueling network architectures for deep reinforcement learning. ICML.

  9. Schaul, T., et al. (2016). Prioritized experience replay. ICLR.

  10. Fortunato, M., et al. (2018). Noisy networks for exploration. ICLR.

  11. Hessel, M., et al. (2018). Rainbow: Combining improvements in deep reinforcement learning. AAAI.

  12. Lillicrap, T. P., et al. (2016). Continuous control with deep reinforcement learning. ICLR.

  13. Fujimoto, S., et al. (2018). Addressing function approximation error in actor-critic methods. ICML.

  14. Haarnoja, T., et al. (2018). Soft actor-critic algorithms and applications. arXiv.

  15. Schulman, J., et al. (2017). Proximal policy optimization algorithms. arXiv.

  16. Kumar, A., et al. (2020). Conservative q-learning for offline reinforcement learning. NeurIPS.


Q学习是强化学习殿堂的入门钥匙——学会了它,你就能理解”智能”是怎么从”试错”中诞生的。从1989年Watkins的论文到今天的RainbowDQN,Q学习的思想一直在进化,但核心从未改变:让机器通过与环境交互,学会判断每个状态下哪个选择最值得。