Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第三十九 强化学习(Reinforcement Learning)

第三十九 强化学习
├── 什么是强化学习(RL)
├── 智能体与环境
├── 马尔可夫决策过程(MDP)
│   ├── 状态、动作、奖励
│   ├── 转移概率与折扣因子
│   └── MDP 的形式化定义
├── 贝尔曼方程
│   ├── 状态价值函数
│   ├── 动作价值函数
│   └── 最优价值函数
├── 策略与价值函数
│   ├── 策略(确定性 / 随机性)
│   ├── 策略评估
│   └── 策略改进
├── 探索与利用
│   ├── ε-贪心策略
│   └── 其他探索方法
├── 强化学习算法
│   ├── Q-Learning
│   ├── SARSA
│   ├── 深度强化学习(DQN)
│   └── 近端策略优化(PPO)
├── 实战:用 Rust 实现 Q-Learning
│   └── FrozenLake 环境
├── Rust 强化学习生态
└── 总结与练习

强化学习(Reinforcement Learning,简称 RL)是人工智能中最接近“学习“本质的分支。与监督学习依赖标注数据、无监督学习挖掘数据结构不同,强化学习让智能体(Agent)通过与环境(Environment)的不断交互,在试错中学习最优行为策略——这与人类婴儿学走路、学说话的过程惊人地相似。

$$\text{强化学习} = \text{试错探索} + \text{延迟奖励} + \text{策略优化}$$


一、什么是强化学习

1.1 定义

强化学习是机器学习的一大范式,其核心思想是:智能体在环境中采取动作,环境返回奖励信号,智能体通过最大化累积奖励来学习最优策略。它既不依赖标注数据,也不需要显式的问题建模,而是通过“做中学“(Learning by Doing)来获取知识。

1.2 强化学习与其他机器学习范式的对比

范式输入反馈目标典型场景
监督学习特征 + 标签即时、正确答案预测标签图像分类、机器翻译
无监督学习特征无反馈发现数据结构聚类、降维
强化学习状态延迟奖励信号最大化累积奖励游戏AI、机器人控制

1.3 发展简史

  • 1989:Watkins 提出 Q-Learning,奠定值函数方法基础
  • 1992:Tesauro 用 TD-Gammon 程序挑战西洋双陆棋
  • 2013:DeepMind 发表 Playing Atari with Deep RL(DQN)
  • 2016:AlphaGo 击败李世石,强化学习走入大众视野
  • 2017:PPO 算法提出,成为 OpenAI 默认策略优化方法
  • 2020s:RLHF(基于人类反馈的强化学习)赋能大语言模型

二、智能体与环境

强化学习的基本框架由两个核心角色构成:智能体(Agent)环境(Environment)。二者通过循环交互完成学习过程。

2.1 交互循环

┌──────────┐   动作 a_t    ┌──────────┐
│          │ ────────────> │          │
│  智能体   │               │   环境   │
│ (Agent)  │ <──────────── │  (Env)   │
│          │  状态 s_{t+1}  │          │
│          │  奖励 r_{t+1}  │          │
└──────────┘               └──────────┘

每一步交互的流程:

  1. 智能体观察当前状态 $s_t$
  2. 智能体根据策略选择动作 $a_t$
  3. 环境接收动作,转移到下一状态 $s_{t+1}$
  4. 环境返回奖励 $r_{t+1}$
  5. 智能体根据反馈更新策略

2.2 用 Rust 描述交互循环

#![allow(unused)]
fn main() {
/// 智能体 trait:定义智能体的基本行为
trait Agent {
    /// 根据当前状态选择动作
    fn select_action(&mut self, state: usize) -> usize;
    /// 根据转移结果更新策略
    fn update(&mut self, state: usize, action: usize, reward: f64, next_state: usize);
}

/// 环境 trait:定义环境的基本接口
trait Environment {
    /// 重置环境到初始状态
    fn reset(&mut self) -> usize;
    /// 执行动作,返回 (下一状态, 奖励, 是否结束)
    fn step(&mut self, state: usize, action: usize) -> (usize, f64, bool);
}

/// 训练循环
fn train<A: Agent, E: Environment>(agent: &mut A, env: &mut E, episodes: usize) {
    for episode in 0..episodes {
        let mut state = env.reset();
        let mut total_reward = 0.0;
        let mut steps = 0;

        loop {
            let action = agent.select_action(state);
            let (next_state, reward, done) = env.step(state, action);
            agent.update(state, action, reward, next_state);
            total_reward += reward;
            steps += 1;
            state = next_state;

            if done {
                break;
            }
        }

        if (episode + 1) % 100 == 0 {
            println!("Episode {}: 总奖励 = {:.2}, 步数 = {}", 
                     episode + 1, total_reward, steps);
        }
    }
}
}

三、马尔可夫决策过程(MDP)

马尔可夫决策过程(Markov Decision Process, MDP)是强化学习的数学框架,它用精确的数学语言描述了智能体与环境交互的全部要素。

3.1 MDP 的五元组

一个 MDP 由一个五元组 $\langle S, A, P, R, \gamma \rangle$ 定义:

要素符号含义
状态空间$S$智能体可能处于的所有状态的集合
动作空间$A$智能体可以执行的所有动作的集合
转移概率$P(s’ \mid s, a)$在状态 $s$ 执行动作 $a$ 后转移到状态 $s’$ 的概率
奖励函数$R(s, a, s’)$执行动作后获得的即时奖励
折扣因子$\gamma \in [0, 1)$衡量未来奖励的衰减程度

3.2 马尔可夫性质

MDP 的核心假设是马尔可夫性质(Markov Property):当前状态包含了做出最优决策所需的全部信息,即未来只与现在有关,与过去无关。

$$P(s_{t+1} \mid s_t, a_t, s_{t-1}, a_{t-1}, \ldots) = P(s_{t+1} \mid s_t, a_t)$$

3.3 转移概率与奖励函数

状态转移概率满足归一化条件:

$$\sum_{s’ \in S} P(s’ \mid s, a) = 1, \quad \forall s \in S, \forall a \in A$$

奖励函数 $R(s, a, s’)$ 定义了智能体在状态 $s$ 执行动作 $a$ 后转移到 $s’$ 时获得的即时反馈。

3.4 折扣因子

折扣因子 $\gamma$ 决定了智能体对远期奖励的重视程度:

  • $\gamma \to 1$:智能体有远见,重视长期回报
  • $\gamma \to 0$:智能体目光短浅,只关注即时奖励

累积回报(Return)定义为从时刻 $t$ 开始的所有折扣奖励之和:

$$G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1}$$

3.5 用 Rust 定义 MDP

#![allow(unused)]
fn main() {
/// MDP 的五元组定义
struct Mdp {
    num_states: usize,       // |S|
    num_actions: usize,      // |A|
    transitions: Vec<Vec<Vec<f64>>>, // P[s][a][s'] 转移概率
    rewards: Vec<Vec<Vec<f64>>>,    // R[s][a][s'] 奖励函数
    gamma: f64,              // 折扣因子
}

impl Mdp {
    fn new(num_states: usize, num_actions: usize, gamma: f64) -> Self {
        // 初始化转移概率和奖励矩阵
        let transitions = vec![vec![vec![1.0 / num_states as f64; num_states]; num_actions]; num_states];
        let rewards = vec![vec![vec![0.0; num_states]; num_actions]; num_states];
        Mdp { num_states, num_actions, transitions, rewards, gamma }
    }

    /// 获取转移概率 P(s'|s,a)
    fn get_transition(&self, s: usize, a: usize, s_prime: usize) -> f64 {
        self.transitions[s][a][s_prime]
    }

    /// 获取奖励 R(s,a,s')
    fn get_reward(&self, s: usize, a: usize, s_prime: usize) -> f64 {
        self.rewards[s][a][s_prime]
    }
}
}

四、贝尔曼方程

贝尔曼方程(Bellman Equation)是强化学习的基石,它将价值函数表达为即时奖励与未来价值的递推关系。

4.1 状态价值函数

状态价值函数 $V^\pi(s)$ 表示在策略 $\pi$ 下,从状态 $s$ 出发的期望累积回报:

$$V^\pi(s) = \mathbb{E}\pi \left[ G_t \mid S_t = s \right] = \mathbb{E}\pi \left[ \sum_{k=0}^{\infty} \gamma^k R_{t+k+1} \mid S_t = s \right]$$

根据贝尔曼方程,它可以分解为即时奖励加上下一状态的价值:

$$V^\pi(s) = \sum_a \pi(a \mid s) \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^\pi(s’) \right]$$

4.2 动作价值函数

动作价值函数 $Q^\pi(s, a)$ 表示在状态 $s$ 执行动作 $a$ 后,遵循策略 $\pi$ 的期望累积回报:

$$Q^\pi(s, a) = \mathbb{E}_\pi \left[ G_t \mid S_t = s, A_t = a \right]$$

贝尔曼方程形式:

$$Q^\pi(s, a) = \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma \sum_{a’} \pi(a’ \mid s’) Q^\pi(s’, a’) \right]$$

4.3 最优价值函数

最优状态价值函数和最优动作价值函数:

$$V^*(s) = \max_\pi V^\pi(s)$$

$$Q^*(s, a) = \max_\pi Q^\pi(s, a)$$

最优贝尔曼方程:

$$V^(s) = \max_a \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^(s’) \right]$$

$$Q^(s, a) = \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma \max_{a’} Q^(s’, a’) \right]$$

4.4 用 Rust 实现价值迭代

#![allow(unused)]
fn main() {
/// 价值迭代算法求解最优价值函数
fn value_iteration(mdp: &Mdp, max_iterations: usize, theta: f64) -> Vec<f64> {
    let mut v = vec![0.0; mdp.num_states];

    for _ in 0..max_iterations {
        let mut delta = 0.0;

        for s in 0..mdp.num_states {
            let mut max_value = f64::NEG_INFINITY;

            for a in 0..mdp.num_actions {
                let mut action_value = 0.0;
                for s_prime in 0..mdp.num_states {
                    let p = mdp.get_transition(s, a, s_prime);
                    let r = mdp.get_reward(s, a, s_prime);
                    action_value += p * (r + mdp.gamma * v[s_prime]);
                }
                max_value = max_value.max(action_value);
            }

            delta = delta.max((max_value - v[s]).abs());
            v[s] = max_value;
        }

        if delta < theta {
            println!("价值迭代在 {} 次迭代后收敛", max_iterations);
            break;
        }
    }

    v
}

/// 从最优价值函数提取最优策略
fn extract_policy(mdp: &Mdp, v: &[f64]) -> Vec<usize> {
    let mut policy = vec![0; mdp.num_states];

    for s in 0..mdp.num_states {
        let mut best_action = 0;
        let mut best_value = f64::NEG_INFINITY;

        for a in 0..mdp.num_actions {
            let mut action_value = 0.0;
            for s_prime in 0..mdp.num_states {
                let p = mdp.get_transition(s, a, s_prime);
                let r = mdp.get_reward(s, a, s_prime);
                action_value += p * (r + mdp.gamma * v[s_prime]);
            }
            if action_value > best_value {
                best_value = action_value;
                best_action = a;
            }
        }
        policy[s] = best_action;
    }

    policy
}
}

五、策略与价值函数

5.1 策略的定义

策略 $\pi$ 是智能体的行为规则,定义了在每个状态下应该采取什么动作。

确定性策略:每个状态对应唯一动作

$$a = \pi(s)$$

随机性策略:每个状态下各动作的概率分布

$$\pi(a \mid s) = P(A_t = a \mid S_t = s)$$

#![allow(unused)]
fn main() {
/// 策略 trait
trait Policy {
    /// 在给定状态下选择动作
    fn select_action(&self, state: usize) -> usize;
}

/// 确定性策略:每个状态映射到一个动作
struct DeterministicPolicy {
    action_map: Vec<usize>,
}

impl DeterministicPolicy {
    fn new(action_map: Vec<usize>) -> Self {
        Self { action_map }
    }
}

impl Policy for DeterministicPolicy {
    fn select_action(&self, state: usize) -> usize {
        self.action_map[state]
    }
}

/// 随机性策略:每个状态下各动作的概率
struct StochasticPolicy {
    /// prob[s][a] = 在状态 s 选择动作 a 的概率
    prob: Vec<Vec<f64>>,
    num_actions: usize,
}

impl StochasticPolicy {
    fn new(num_states: usize, num_actions: usize) -> Self {
        // 均匀初始化
        let prob = vec![vec![1.0 / num_actions as f64; num_actions]; num_states];
        Self { prob, num_actions }
    }
}

impl Policy for StochasticPolicy {
    fn select_action(&self, state: usize) -> usize {
        // 按概率分布采样
        let r = rand::random::<f64>();
        let mut cumulative = 0.0;
        for a in 0..self.num_actions {
            cumulative += self.prob[state][a];
            if r < cumulative {
                return a;
            }
        }
        self.num_actions - 1
    }
}
}

5.2 策略评估

策略评估的目标是计算给定策略 $\pi$ 下的状态价值函数 $V^\pi$。采用迭代法:

$$V_{k+1}(s) = \sum_a \pi(a \mid s) \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V_k(s’) \right]$$

5.3 策略改进

在获得价值函数后,通过贪婪地选择每个状态的最优动作来改进策略:

$$\pi’(s) = \arg\max_a Q^\pi(s, a) = \arg\max_a \sum_{s’} P(s’ \mid s, a) \left[ R(s, a, s’) + \gamma V^\pi(s’) \right]$$

策略评估与策略改进交替执行,构成**策略迭代(Policy Iteration)**算法,保证收敛到最优策略。


六、探索与利用

强化学习面临一个核心困境:探索(Exploration) 还是 利用(Exploitation)

  • 利用:选择当前已知最优的动作,获取最大即时收益
  • 探索:尝试未知动作,可能发现更好的策略

$$\text{探索与利用的权衡} \iff \text{已知的好 vs 未知的好}$$

6.1 ε-贪心策略

最常用的探索方法是 $\varepsilon$-贪心($\varepsilon$-greedy):以概率 $1 - \varepsilon$ 选择当前最优动作,以概率 $\varepsilon$ 随机选择动作。

$$a_t = \begin{cases} \arg\max_a Q(s_t, a) & \text{以概率 } 1 - \varepsilon \ \text{随机动作} & \text{以概率 } \varepsilon \end{cases}$$

#![allow(unused)]
fn main() {
/// ε-贪心策略实现
struct EpsilonGreedy {
    q_table: Vec<Vec<f64>>,  // Q(s, a) 表
    epsilon: f64,            // 探索率
    num_actions: usize,
}

impl EpsilonGreedy {
    fn new(num_states: usize, num_actions: usize, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            epsilon,
            num_actions,
        }
    }

    /// ε-贪心选择动作
    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            // 探索:随机选择
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            // 利用:选择 Q 值最大的动作
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }

    /// ε 衰减:随着训练进行逐渐减少探索
    fn decay_epsilon(&mut self, decay: f64, min_epsilon: f64) {
        self.epsilon = (self.epsilon * decay).max(min_epsilon);
    }
}
}

6.2 其他探索方法

方法原理适用场景
Boltzmann 探索按 Q 值的 softmax 概率选择动作需要平滑探索
UCB(上置信界)选择置信区间上界最大的动作多臂老虎机问题
乐观初始化将 Q 值初始化为较大值,鼓励探索表格型方法
噪声网络在网络参数中添加噪声连续动作空间

七、强化学习算法

7.1 Q-Learning

Q-Learning 是最经典的**异策略(off-policy)**时序差分算法。它直接学习最优动作价值函数 $Q^*(s, a)$,不依赖当前策略的执行。

更新规则:

$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma \max_{a’} Q(s’, a’) - Q(s, a) \right]$$

其中 $\alpha$ 为学习率,$\gamma$ 为折扣因子。

#![allow(unused)]
fn main() {
/// Q-Learning 智能体
struct QLearningAgent {
    q_table: Vec<Vec<f64>>,
    learning_rate: f64,  // α
    discount_factor: f64, // γ
    epsilon: f64,
    num_actions: usize,
}

impl QLearningAgent {
    fn new(num_states: usize, num_actions: usize, lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            learning_rate: lr,
            discount_factor: gamma,
            epsilon,
            num_actions,
        }
    }

    /// Q-Learning 更新规则
    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, done: bool) {
        // 计算目标值
        let max_q_next = if done {
            0.0
        } else {
            self.q_table[s_next]
                .iter()
                .cloned()
                .fold(f64::NEG_INFINITY, f64::max)
        };

        let target = r + self.discount_factor * max_q_next;
        // 时序差分更新
        let td_error = target - self.q_table[s][a];
        self.q_table[s][a] += self.learning_rate * td_error;
    }

    /// 选择动作(ε-贪心)
    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }
}
}

7.2 SARSA

SARSA 是一种**同策略(on-policy)**算法,它使用实际执行的动作来更新 Q 值,而非贪婪地选择最大 Q 值。

更新规则:

$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma Q(s’, a’) - Q(s, a) \right]$$

其中 $a’$ 是在状态 $s’$ 下实际执行的动作(也遵循 $\varepsilon$-贪心策略)。

#![allow(unused)]
fn main() {
/// SARSA 智能体
struct SarsaAgent {
    q_table: Vec<Vec<f64>>,
    learning_rate: f64,
    discount_factor: f64,
    epsilon: f64,
    num_actions: usize,
}

impl SarsaAgent {
    fn new(num_states: usize, num_actions: usize, lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: vec![vec![0.0; num_actions]; num_states],
            learning_rate: lr,
            discount_factor: gamma,
            epsilon,
            num_actions,
        }
    }

    /// SARSA 更新规则(使用实际选择的下一个动作 a')
    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, a_next: usize, done: bool) {
        let next_q = if done { 0.0 } else { self.q_table[s_next][a_next] };
        let target = r + self.discount_factor * next_q;
        let td_error = target - self.q_table[s][a];
        self.q_table[s][a] += self.learning_rate * td_error;
    }

    fn select_action(&self, state: usize) -> usize {
        if rand::random::<f64>() < self.epsilon {
            (rand::random::<f64>() * self.num_actions as f64).floor() as usize
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }
}
}

7.3 Q-Learning 与 SARSA 的对比

特性Q-LearningSARSA
策略类型异策略(off-policy)同策略(on-policy)
更新目标$\max_{a’} Q(s’, a’)$$Q(s’, a’)$(实际动作)
探索影响不受探索策略影响受探索策略影响
收敛性更激进,可能过估计更保守,更稳定
适用场景目标明确的任务需要安全探索的任务

7.4 深度 Q 网络(DQN)

当状态空间或动作空间非常大时,Q 表无法存储所有状态-动作对。深度 Q 网络(Deep Q-Network, DQN)用神经网络近似 $Q(s, a)$,将状态作为输入,输出各动作的 Q 值。

DQN 的两个关键创新:

  1. 经验回放(Experience Replay):将转移元组 $(s, a, r, s’)$ 存入回放缓冲区,训练时随机采样,打破数据相关性
  2. 目标网络(Target Network):使用独立的网络计算目标 Q 值,定期同步,提高训练稳定性

$$\text{损失函数:} \mathcal{L}(\theta) = \mathbb{E} \left[ \left( r + \gamma \max_{a’} Q(s’, a’; \theta^-) - Q(s, a; \theta) \right)^2 \right]$$

其中 $\theta$ 为在线网络参数,$\theta^-$ 为目标网络参数。

#![allow(unused)]
fn main() {
/// 经验回放缓冲区
struct ReplayBuffer {
    buffer: Vec<Experience>,
    capacity: usize,
}

/// 一次转移的经验
#[derive(Clone)]
struct Experience {
    state: usize,
    action: usize,
    reward: f64,
    next_state: usize,
    done: bool,
}

impl ReplayBuffer {
    fn new(capacity: usize) -> Self {
        Self { buffer: Vec::with_capacity(capacity), capacity }
    }

    /// 存入经验
    fn push(&mut self, exp: Experience) {
        if self.buffer.len() >= self.capacity {
            self.buffer.remove(0); // FIFO 淘汰最旧经验
        }
        self.buffer.push(exp);
    }

    /// 随机采样一批经验
    fn sample(&self, batch_size: usize) -> Vec<&Experience> {
        use rand::seq::SliceRandom;
        let mut rng = rand::thread_rng();
        self.buffer.choose_multiple(&mut rng, batch_size).collect()
    }

    fn len(&self) -> usize {
        self.buffer.len()
    }
}
}

7.5 近端策略优化(PPO)

近端策略优化(Proximal Policy Optimization, PPO)是目前最广泛使用的策略梯度算法之一。它通过限制策略更新幅度来保证训练稳定性。

PPO-Clip 目标函数:

$$L^{\text{CLIP}}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta) \hat{A}_t,\ \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]$$

其中:

  • $r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}$ 是新旧策略的概率比
  • $\hat{A}_t$ 是优势函数估计
  • $\epsilon$ 是裁剪参数(通常取 0.1 ~ 0.2)

PPO 的核心思想是:当新策略偏离旧策略太远时,通过裁剪来限制梯度更新,避免策略崩溃。


八、实战:用 Rust 实现 Q-Learning(FrozenLake 环境)

下面我们用一个经典的 FrozenLake(冰湖)环境来演示 Q-Learning 的完整实现。冰湖是一个 4x4 的网格世界,智能体需要从起点 S 走到终点 G,途中要避开冰洞 H。

SFFF
FHFH
FFFH
HFFG

S = 起点, F = 冰面(安全), H = 冰洞(掉入则失败), G = 目标(到达则成功)

use rand::Rng;

/// FrozenLake 环境
struct FrozenLake {
    /// 4x4 网格,共 16 个状态
    grid: [i8; 16],
    current_state: usize,
}

impl FrozenLake {
    fn new() -> Self {
        // 0=安全, 1=冰洞, 2=起点, 3=终点
        let grid = [
            2, 0, 0, 0,  // SFFF
            0, 1, 0, 1,  // FHFH
            0, 0, 0, 1,  // FFFH
            1, 0, 0, 3,  // HFFG
        ];
        Self { grid, current_state: 0 }
    }

    /// 4 个动作:0=左, 1=下, 2=右, 3=上
    fn step(&mut self, action: usize) -> (usize, f64, bool) {
        let s = self.current_state as i32;
        let row = s / 4;
        let col = s % 4;

        let (new_row, new_col) = match action {
            0 => (row, col - 1),     // 左
            1 => (row + 1, col),     // 下
            2 => (row, col + 1),     // 右
            3 => (row - 1, col),     // 上
            _ => (row, col),
        };

        // 边界检查:超出边界则原地不动
        let new_state = if new_row < 0 || new_row > 3 || new_col < 0 || new_col > 3 {
            self.current_state
        } else {
            (new_row * 4 + new_col) as usize
        };

        self.current_state = new_state;

        let reward = if self.grid[new_state] == 3 { 1.0 } else { 0.0 };
        let done = self.grid[new_state] == 1 || self.grid[new_state] == 3;

        (new_state, reward, done)
    }

    fn reset(&mut self) -> usize {
        self.current_state = 0;
        0
    }
}

/// Q-Learning 智能体
struct QLearner {
    q_table: [[f64; 4]; 16], // 16 个状态,4 个动作
    lr: f64,
    gamma: f64,
    epsilon: f64,
    epsilon_min: f64,
    epsilon_decay: f64,
}

impl QLearner {
    fn new(lr: f64, gamma: f64, epsilon: f64) -> Self {
        Self {
            q_table: [[0.0; 4]; 16],
            lr,
            gamma,
            epsilon,
            epsilon_min: 0.01,
            epsilon_decay: 0.995,
        }
    }

    fn select_action(&self, state: usize) -> usize {
        let mut rng = rand::thread_rng();
        if rng.gen::<f64>() < self.epsilon {
            rng.gen_range(0..4)
        } else {
            self.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap()
        }
    }

    fn update(&mut self, s: usize, a: usize, r: f64, s_next: usize, done: bool) {
        let max_q = if done {
            0.0
        } else {
            self.q_table[s_next].iter().cloned().fold(f64::NEG_INFINITY, f64::max)
        };
        let target = r + self.gamma * max_q;
        self.q_table[s][a] += self.lr * (target - self.q_table[s][a]);
    }

    fn decay_epsilon(&mut self) {
        self.epsilon = (self.epsilon * self.epsilon_decay).max(self.epsilon_min);
    }
}

fn main() {
    let mut env = FrozenLake::new();
    let mut agent = QLearner::new(0.1, 0.99, 1.0);
    let episodes = 10_000;

    println!("=== FrozenLake Q-Learning 训练 ===\n");

    for ep in 0..episodes {
        let mut state = env.reset();
        let mut total_reward = 0.0;

        loop {
            let action = agent.select_action(state);
            let (next_state, reward, done) = env.step(action);
            agent.update(state, action, reward, next_state, done);
            total_reward += reward;
            state = next_state;

            if done { break; }
        }

        agent.decay_epsilon();

        if (ep + 1) % 1000 == 0 {
            let wins = evaluate(&mut env, &agent, 100);
            println!(
                "Episode {}: ε={:.4}, 胜率={:.0}%",
                ep + 1, agent.epsilon, wins
            );
        }
    }

    // 打印学到的策略
    println!("\n=== 学到的最优策略 ===");
    let action_names = ["←", "↓", "→", "↑"];
    for s in 0..16 {
        let best = agent.q_table[s]
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
            .map(|(idx, _)| idx)
            .unwrap();
        print!("{}", action_names[best]);
        if (s + 1) % 4 == 0 { println!(); }
    }
}

/// 评估当前策略的胜率
fn evaluate(env: &mut FrozenLake, agent: &QLearner, trials: usize) -> f64 {
    let mut wins = 0;
    for _ in 0..trials {
        let mut state = env.reset();
        loop {
            let action = agent.q_table[state]
                .iter()
                .enumerate()
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
                .map(|(idx, _)| idx)
                .unwrap();
            let (next_state, _reward, done) = env.step(action);
            state = next_state;
            if done {
                if env.grid[state] == 3 { wins += 1; }
                break;
            }
        }
    }
    wins as f64 / trials as f64 * 100.0
}

运行后,智能体将逐渐学会避开冰洞、到达终点的最优路径。


九、Rust 强化学习生态

Rust 在强化学习领域虽然不如 Python 生态成熟,但凭借其性能优势和安全性,正在快速发展。以下是主要的 Rust RL 相关 crate 和工具:

Crate / 项目说明适用场景
candleHuggingFace 出品的深度学习框架DQN/PPO 等深度 RL 算法的网络部分
burn灵活的深度学习框架,支持多种后端神经网络训练与推理
smartcore纯 Rust 机器学习库基础数学运算、线性代数
ndarrayN 维数组库,类似 NumPy矩阵运算、张量操作
rand随机数生成探索策略、经验采样
tchPyTorch 的 Rust 绑定需要直接使用 PyTorch 模型
rugraph图计算库状态转移图建模
gym-rsOpenAI Gym 的 Rust 移植经典 RL 环境接口

9.1 使用 ndarray 进行矩阵运算

use ndarray::{array, Array2};

fn main() {
    // Q 表用 ndarray 表示
    let mut q_table: Array2<f64> = Array2::zeros((16, 4));

    // 设置初始值
    q_table[[0, 2]] = 0.5; // 状态 0,动作"右"
    q_table[[1, 1]] = 0.3; // 状态 1,动作"下"

    // 获取某状态的最大 Q 值
    let state = 0;
    let max_q = q_table.row(state).iter().cloned()
        .fold(f64::NEG_INFINITY, f64::max);
    println!("状态 {} 的最大 Q 值: {:.2}", state, max_q);

    // 获取最优动作
    let best_action = q_table.row(state)
        .iter().enumerate()
        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
        .map(|(idx, _)| idx)
        .unwrap();
    println!("最优动作: {}", best_action);
}

9.2 Rust 实现 RL 的优势

  1. 内存安全:无需垃圾回收,无数据竞争,适合长时间训练任务
  2. 高性能:零成本抽象,适合大规模环境模拟
  3. 并发友好rayon 等库轻松实现并行环境采样
  4. WebAssembly:可将训练好的 RL 模型部署到浏览器
  5. 嵌入式部署:适合边缘设备和实时控制系统

十、总结与练习

10.1 本章要点总结

主题核心内容
强化学习定义智能体通过与环境交互、试错学习来最大化累积奖励
MDP五元组 $\langle S, A, P, R, \gamma \rangle$,马尔可夫性质是核心假设
贝尔曼方程价值函数的递推分解,连接即时奖励与未来价值
策略与价值函数$V^\pi(s)$ 评估状态好坏,$Q^\pi(s,a)$ 评估动作好坏
探索与利用$\varepsilon$-贪心是最常用的平衡方法
Q-Learning异策略、off-policy,直接学习最优 Q 值
SARSA同策略、on-policy,考虑实际执行的动作
DQN用神经网络近似 Q 函数,经验回放 + 目标网络
PPO策略梯度方法,通过裁剪限制策略更新幅度
Rust 生态candle、burn、ndarray 等库支撑 RL 开发

10.2 练习建议

练习 1:实现 SARSA 求解 FrozenLake

在第八节的 FrozenLake 环境基础上,将 Q-Learning 替换为 SARSA 算法,对比两种算法的收敛速度和最终胜率。

练习 2:实现策略迭代

编写完整的策略迭代算法(策略评估 + 策略改进),在小规模 MDP 上验证其收敛性,并与价值迭代对比迭代次数。

练习 3:多臂老虎机

实现一个简单的多臂老虎机(Multi-Armed Bandit)环境,分别用 $\varepsilon$-贪心、UCB 和 Boltzmann 探索三种策略求解,绘制累积奖励曲线。

练习 4:经验回放缓冲区优化

使用 VecDeque 替代 Vec 实现更高效的 FIFO 经验回放缓冲区,并添加优先级采样(Prioritized Experience Replay)功能。

练习 5:扩展 FrozenLake

将 FrozenLake 扩展为 8x8 网格,增加随机风(Wind)效果——智能体有概率被吹到其他方向。调整超参数,观察 Q-Learning 的表现变化。


下一章预告:第四十章将探讨生成式 AI(Generative AI),包括自编码器、变分自编码器(VAE)和生成对抗网络(GAN)的原理与 Rust 实现。