第三十九 强化学习(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} │ │
└──────────┘ └──────────┘
每一步交互的流程:
- 智能体观察当前状态 $s_t$
- 智能体根据策略选择动作 $a_t$
- 环境接收动作,转移到下一状态 $s_{t+1}$
- 环境返回奖励 $r_{t+1}$
- 智能体根据反馈更新策略
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-Learning | SARSA |
|---|---|---|
| 策略类型 | 异策略(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 的两个关键创新:
- 经验回放(Experience Replay):将转移元组 $(s, a, r, s’)$ 存入回放缓冲区,训练时随机采样,打破数据相关性
- 目标网络(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 / 项目 | 说明 | 适用场景 |
|---|---|---|
| candle | HuggingFace 出品的深度学习框架 | DQN/PPO 等深度 RL 算法的网络部分 |
| burn | 灵活的深度学习框架,支持多种后端 | 神经网络训练与推理 |
| smartcore | 纯 Rust 机器学习库 | 基础数学运算、线性代数 |
| ndarray | N 维数组库,类似 NumPy | 矩阵运算、张量操作 |
| rand | 随机数生成 | 探索策略、经验采样 |
| tch | PyTorch 的 Rust 绑定 | 需要直接使用 PyTorch 模型 |
| rugraph | 图计算库 | 状态转移图建模 |
| gym-rs | OpenAI 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 的优势
- 内存安全:无需垃圾回收,无数据竞争,适合长时间训练任务
- 高性能:零成本抽象,适合大规模环境模拟
- 并发友好:
rayon等库轻松实现并行环境采样 - WebAssembly:可将训练好的 RL 模型部署到浏览器
- 嵌入式部署:适合边缘设备和实时控制系统
十、总结与练习
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 实现。