强化学习基础
深度学习专题 · 智能体与环境交互学习
专题:深度学习系统学习 —— 强化学习基础篇
关键词:强化学习, RL, MDP, Q-Learning, DQN, 策略梯度, PPO, Actor-Critic
前置知识:Python基础, 线性代数, 概率论, 深度学习基础
一、强化学习核心概念
强化学习(Reinforcement Learning,RL)是机器学习的一个重要分支,其核心思想是智能体(Agent)通过与环境的交互学习,获得最大化累积奖励的策略。与监督学习不同,RL没有标注好的"正确答案",而是通过不断的试错(trial-and-error)来学习最优行为。
1.1 RL的八大核心要素
| 要素 | 符号 | 说明 |
| 智能体(Agent) | - | 学习和决策的主体,根据策略选择动作 |
| 环境(Environment) | Env | 智能体之外的一切,接收动作并反馈新状态和奖励 |
| 状态(State) | s / s_t | 环境在时刻t的表示,是智能体决策的依据 |
| 动作(Action) | a / a_t | 智能体在状态s_t时采取的行为 |
| 奖励(Reward) | r / r_t | 环境对动作的即时反馈信号,RL优化的目标 |
| 策略(Policy) | π | 状态到动作的映射,分为确定性策略和随机性策略 |
| 值函数(Value Function) | V(s), Q(s,a) | 对状态或状态-动作对的长期价值的估计 |
| 模型(Model) | - | 环境动力学,描述状态转移和奖励函数 |
关键理解:RL的目标不是最大化即时奖励,而是最大化累积折扣奖励(Cumulative Discounted Reward)。智能体需要学会"延迟满足"——有时候短期看似不利的动作,长期来看可能是最优选择。
1.2 RL与监督/无监督学习的区别
强化学习
通过试错学习,奖励信号延时且稀疏。数据由智能体自身生成(在线学习),探索与利用需要权衡。
监督学习
依赖标注数据,每个样本都有"正确答案"。数据独立同分布,训练后评估。
无监督学习
从无标签数据中发现隐藏结构(聚类、降维等)。没有明确的反馈信号。
1.3 RL的分类体系
根据智能体是否依赖环境模型,RL可分为基于模型(Model-Based)和无模型(Model-Free)方法。无模型方法进一步分为基于值函数(Value-Based)和基于策略(Policy-Based)方法。混合两者优点的 Actor-Critic 方法则是当前主流。
- Model-Based:学习环境模型,利用模型规划(如 Dyna-Q, AlphaGo 的蒙特卡洛树搜索)
- Model-Free Value-Based:学习值函数,通过值函数隐式定义策略(如 Q-Learning, DQN)
- Model-Free Policy-Based:直接学习策略参数(如 REINFORCE, PPO)
- Actor-Critic:同时学习策略(Actor)和值函数(Critic),结合两类方法的优势
二、马尔科夫决策过程(MDP)
马尔科夫决策过程(Markov Decision Process,MDP)是强化学习问题的数学框架。几乎所有的RL问题都可以抽象为MDP。
2.1 MDP的五元组定义
一个MDP由五元组 (S, A, P, R, γ) 构成:
- S:状态空间(State Space),所有可能状态的集合
- A:动作空间(Action Space),智能体可执行的动作集合
- P(s' | s, a):状态转移概率(Transition Probability),在状态s执行动作a后转移到s'的概率
- R(s, a, s'):奖励函数(Reward Function),转移到新状态后获得的即时奖励
- γ ∈ [0, 1]:折扣因子(Discount Factor),平衡即时奖励和未来奖励的重要性
马尔科夫性质:未来状态只依赖于当前状态,与过去状态无关。形式化:P(s_{t+1} | s_t, a_t, s_{t-1}, a_{t-1}, ...) = P(s_{t+1} | s_t, a_t)。这一性质极大简化了问题建模。
2.2 回报(Return)与折扣因子
RL的核心目标是最大化累积折扣回报(Discounted Cumulative Return)。从时间步t开始的回报定义为:
定义 回报 (Return):
G_t = r_t + γ · r_{t+1} + γ² · r_{t+2} + ... = Σ_{k=0}^{∞} γ^k · r_{t+k}
说明:
- 当 γ → 0:智能体"目光短浅",只关心即时奖励
- 当 γ → 1:智能体"高瞻远瞩",同样重视未来奖励
- γ < 1 保证了无限时间步下回报的收敛性
2.3 状态值函数与动作值函数
值函数是RL的核心概念,用于评估状态或状态-动作对的"长期价值":
状态值函数 (State Value Function):
V_π(s) = E_π[ G_t | s_t = s ]
→ 在状态s,遵循策略π,所能获得的期望累积回报
动作值函数 (Action Value Function):
Q_π(s, a) = E_π[ G_t | s_t = s, a_t = a ]
→ 在状态s执行动作a,之后遵循策略π,所能获得的期望累积回报
2.4 Bellman方程
Bellman方程是RL中最优美也最重要的递归关系,它将当前状态的值函数与下一状态的值函数联系起来:
Bellman期望方程 (Bellman Expectation Equation):
V_π(s) = Σ_a π(a|s) · Σ_{s',r} p(s',r|s,a) [ r + γ · V_π(s') ]
Q_π(s,a) = Σ_{s',r} p(s',r|s,a) [ r + γ · Σ_{a'} π(a'|s') · Q_π(s',a') ]
Bellman最优方程 (Bellman Optimality Equation):
V*(s) = max_a Σ_{s',r} p(s',r|s,a) [ r + γ · V*(s') ]
Q*(s,a) = Σ_{s',r} p(s',r|s,a) [ r + γ · max_{a'} Q*(s',a') ]
直觉理解:Bellman方程表达的递归思想是——"当前状态的价值 = 即时奖励 + 下一个状态价值的折扣"。这种递归结构使得我们可以通过动态规划或迭代方法逐步求解最优策略。
2.5 最优策略
在值函数之间,存在确定性的偏序关系:如果策略π在所有状态下的值函数都不劣于策略π',则称π优于π'。至少存在一个最优策略π*,它同时最大化所有状态的值函数。求解最优策略的方法包括策略迭代(Policy Iteration)和值迭代(Value Iteration):
策略迭代算法 (Policy Iteration):
1. 初始化:随机策略 π
2. 重复直到收敛:
a. 策略评估:计算 V_π(s) (通过迭代求解Bellman期望方程)
b. 策略改进:π'(s) = argmax_a Σ_{s',r} p(s',r|s,a)[r + γ·V_π(s')]
c. 若策略不变则停止,否则更新 π = π'
值迭代算法 (Value Iteration):
1. 初始化:V(s) = 0 for all s
2. 重复直到收敛:
V_{k+1}(s) = max_a Σ_{s',r} p(s',r|s,a)[r + γ·V_k(s')]
3. 从收敛的V*中提取最优策略:π*(s) = argmax_a Σ p[ r + γ·V*(s') ]
策略迭代 vs 值迭代:策略迭代在每次评估步骤中完整求解V_π,收敛速度较快但每次迭代计算量大;值迭代将策略评估和策略改进合并为一步,计算量小但需要更多迭代次数。实际中值迭代更常用。
三、Q-Learning与SARSA
当环境模型未知时(无模型RL),我们无法直接使用动态规划求解MDP。此时需要时序差分学习(Temporal-Difference Learning,TD Learning)——它结合了蒙特卡洛方法的采样思想和动态规划的bootstrapping思想,是RL中最核心的学习范式。
3.1 TD误差与Q表更新
时序差分学习的核心是TD误差(TD Error),它衡量实际观测到的奖励与预期值之间的差距:
δ_t = r_t + γ · V(s_{t+1}) - V(s_t)
解释:
- r_t + γ · V(s_{t+1}) 称为 "TD目标"(用下一状态的值函数估计当前状态的值)
- V(s_t) 是当前对状态值的"估计"
- TD误差是目标与估计的差值
值函数更新公式:V(s_t) ← V(s_t) + α · δ_t
其中 α 是学习率(步长)
3.2 Q-Learning(离线策略TD)
Q-Learning由Watkins于1989年提出,是RL领域最具影响力的算法之一。它直接学习最优动作值函数Q*,而不依赖于当前策略:
Q-Learning 更新公式:
Q(s_t, a_t) ← Q(s_t, a_t) + α [ r_t + γ · max_a Q(s_{t+1}, a) - Q(s_t, a_t) ]
关键特点:
- 离线策略(Off-Policy):行为策略使用ε-greedy探索,但目标策略使用贪婪选择 max_a Q
- Q-Learning直接学习最优策略,不需要等策略收敛
- 在合适的条件下(状态动作对无限次访问、步长满足Robbins-Monro条件),Q-Learning以概率1收敛到Q*
3.3 ε-greedy探索策略
探索与利用(Exploration vs. Exploitation)的权衡是RL的核心挑战。ε-greedy是最简单有效的探索策略:
ε-greedy策略:
π(a|s) = {
1 - ε + ε/|A|, if a = argmax_a Q(s,a) (贪婪动作)
ε/|A|, otherwise (随机探索)
}
策略说明:
- 以概率 (1-ε) 选择当前最优动作(利用)
- 以概率 ε 随机选择动作(探索)
- 实际应用中通常让 ε 随时间衰减(如从1.0衰减到0.01)
- 更高级的探索策略包括:UCB、Thompson Sampling、Noisy Networks等
3.4 SARSA(在线策略TD)
SARSA与Q-Learning几乎同时被提出,它们的唯一区别在于更新目标使用的下一个动作不同:
SARSA 更新公式(命名取自 (S, A, R, S', A')):
Q(s_t, a_t) ← Q(s_t, a_t) + α [ r_t + γ · Q(s_{t+1}, a_{t+1}) - Q(s_t, a_t) ]
关键区别:
- 在线策略(On-Policy):目标中使用的 a_{t+1} 由当前行为策略(含ε-greedy)实际采样得到
- SARSA必须等下一个动作a_{t+1}选定后才能更新
- SARSA的行为策略和目标策略是同一个策略
3.5 Q-Learning vs SARSA 对比
| 对比维度 | Q-Learning | SARSA |
| 策略类型 | 离线策略(Off-Policy) | 在线策略(On-Policy) |
| 更新目标 | max_a Q(s_{t+1}, a)(贪婪) | Q(s_{t+1}, a_{t+1})(采样) |
| 探索态度 | 不考虑探索的代价,学习最优路径 | 考虑探索风险,学习安全路径 |
| 收敛特性 | 有偏但收敛到最优值 | 渐进收敛到接近最优 |
| 悬崖场景 | 可能学到"贴着悬崖走"的最优策略 | 倾向于"远离悬崖"的安全策略 |
| 适用场景 | 风险可承受的环境 | 风险敏感的场景(如机器人控制) |
经典例子——悬崖行走(Cliff Walking):智能体从起点走到终点,路径上有悬崖区域。Q-Learning会学到沿着悬崖边的最短路径(因为最优Q值不考虑探索噪声),而SARSA会学到远离悬崖的较远但更安全的路径。这是理解两种算法区别的最直观案例。
3.6 Q-Learning完整实现
import numpy as np
import gymnasium as gym
class QLearningAgent:
def __init__(self, n_states, n_actions, lr=0.1, gamma=0.99,
epsilon=1.0, epsilon_decay=0.995, epsilon_min=0.01):
self.n_actions = n_actions
self.lr = lr # 学习率 α
self.gamma = gamma # 折扣因子 γ
self.epsilon = epsilon # 探索率 ε
self.epsilon_decay = epsilon_decay
self.epsilon_min = epsilon_min
self.Q_table = np.zeros((n_states, n_actions)) # Q表
def choose_action(self, state):
# ε-greedy策略选择动作
if np.random.random() < self.epsilon:
return np.random.randint(self.n_actions) # 探索
else:
return np.argmax(self.Q_table[state]) # 利用
def update(self, s, a, r, s_next, done):
# Q-Learning更新:使用 max_a Q(s', a) 作为TD目标
td_target = r + (1 - done) * self.gamma * np.max(self.Q_table[s_next])
td_error = td_target - self.Q_table[s, a]
self.Q_table[s, a] += self.lr * td_error
# 衰减探索率
if done:
self.epsilon = max(self.epsilon_min,
self.epsilon * self.epsilon_decay)
def train(self, env, episodes=500):
rewards = []
for ep in range(episodes):
s, _ = env.reset()
ep_reward = 0
done = False
while not done:
a = self.choose_action(s)
s_next, r, done, truncated, _ = env.step(a)
done = done or truncated
self.update(s, a, r, s_next, done)
s = s_next
ep_reward += r
rewards.append(ep_reward)
if (ep + 1) % 100 == 0:
avg_reward = np.mean(rewards[-100:])
print(f"Episode {ep+1}, Avg Reward: {avg_reward:.2f}, ε: {self.epsilon:.3f}")
return rewards
# 使用示例:CliffWalking环境
env = gym.make("CliffWalking-v0")
agent = QLearningAgent(
n_states=env.observation_space.n,
n_actions=env.action_space.n,
lr=0.1, gamma=0.99,
epsilon=1.0, epsilon_decay=0.999
)
rewards = agent.train(env, episodes=1000)
# 测试训练好的策略
s, _ = env.reset()
total_reward = 0
done = False
while not done:
a = np.argmax(agent.Q_table[s])
s, r, done, truncated, _ = env.step(a)
total_reward += r
done = done or truncated
print(f"Test reward: {total_reward}")
四、深度Q网络(DQN)
传统Q-Learning使用表格存储Q值,面对高维或连续状态空间时,Q表会遭遇"维度灾难"。深度Q网络(Deep Q-Network,DQN)由DeepMind在2013年提出,使用深度神经网络作为Q函数逼近器,开创了深度强化学习时代。2015年,DQN在Atari 2600游戏的49个任务中达到了超越人类玩家的水平。
4.1 DQN的两大核心技术
DQN成功的关键在于两项技术创新,解决了深度学习与强化学习结合时的不稳定性问题:
技术一:经验回放(Experience Replay)
智能体与环境交互产生的经验元组 (s, a, r, s') 被存储在一个回放缓冲区中。训练时,从中随机采样小批量进行更新:
经验回放的优势:
1. 打破数据相关性:RL产生的连续样本高度相关,直接使用会导致
梯度更新方差大。随机采样打乱了时间相关性。
2. 提高数据效率:每条经验可被多次使用(类似监督学习中一个样本
被多个epoch使用),而不是用完即弃。
3. 避免灾难性遗忘:网络不会"忘记"之前学到的经验。
实现细节:
- 缓冲区通常用固定大小的环形队列实现
- 采样时均匀随机采样(后续Prioritized Replay改进了采样策略)
- 缓冲区大小通常为 10^5 ~ 10^6 量级
技术二:目标网络(Target Network)
在标准Q-Learning中,TD目标 r + γ·max_a Q(s', a) 使用了与当前网络相同的参数,导致"移动靶"问题——目标随着网络更新而不断变化,训练不稳定。DQN引入了一个冻结的目标网络:
双网络结构:
- 主网络 Q(s, a; θ) — 用于选择动作和计算TD误差
- 目标网络 Q(s, a; θ⁻) — 用于计算TD目标值
更新流程:
1. 主网络每步更新(梯度下降)
2. 目标网络参数定期从主网络复制(硬更新):θ⁻ ← θ(每C步)
或软更新(Polyak平均):θ⁻ ← τθ + (1-τ)θ⁻,τ ∈ (0,1)
Loss函数(均方误差):
L(θ) = E_{(s,a,r,s')~D} [ (r + γ·max_{a'} Q(s', a'; θ⁻) - Q(s, a; θ))² ]
4.2 DQN完整实现
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from collections import deque
class DQN(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=128):
super().__init__()
self.net = 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.net(x)
class ReplayBuffer:
def __init__(self, capacity=100000):
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):
indices = np.random.choice(len(self.buffer), batch_size, replace=False)
batch = [self.buffer[i] for i in indices]
states, actions, rewards, next_states, dones = zip(*batch)
return (
torch.FloatTensor(np.array(states)),
torch.LongTensor(actions).unsqueeze(1),
torch.FloatTensor(rewards).unsqueeze(1),
torch.FloatTensor(np.array(next_states)),
torch.FloatTensor(dones).unsqueeze(1)
)
def __len__(self):
return len(self.buffer)
class DQNAgent:
def __init__(self, state_dim, action_dim, lr=1e-3, gamma=0.99,
epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.998,
buffer_capacity=100000, batch_size=64, target_update=100):
self.q_net = DQN(state_dim, action_dim)
self.target_net = DQN(state_dim, action_dim)
self.target_net.load_state_dict(self.q_net.state_dict())
self.optimizer = optim.Adam(self.q_net.parameters(), lr=lr)
self.memory = ReplayBuffer(buffer_capacity)
self.batch_size = batch_size
self.gamma = gamma
self.epsilon = epsilon
self.epsilon_min = epsilon_min
self.epsilon_decay = epsilon_decay
self.target_update = target_update
self.action_dim = action_dim
self.steps = 0
def choose_action(self, state):
if np.random.random() < self.epsilon:
return np.random.randint(self.action_dim)
state_t = torch.FloatTensor(state).unsqueeze(0)
q_values = self.q_net(state_t)
return q_values.argmax().item()
def update(self):
if len(self.memory) < self.batch_size:
return
states, actions, rewards, next_states, dones = self.memory.sample(self.batch_size)
# 当前Q值
current_q = self.q_net(states).gather(1, actions)
# 目标Q值: r + γ · max_{a'} Q_target(s', a')
with torch.no_grad():
max_next_q = self.target_net(next_states).max(1, keepdim=True)[0]
target_q = rewards + self.gamma * max_next_q * (1 - dones)
loss = nn.MSELoss()(current_q, target_q)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 定期更新目标网络(硬更新)
self.steps += 1
if self.steps % self.target_update == 0:
self.target_net.load_state_dict(self.q_net.state_dict())
return loss.item()
def decay_epsilon(self):
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
4.3 DQN的改进变体
原始DQN存在过高估计(Overestimation)问题——由于 max 操作,Q值会被系统性地高估。后续研究提出了多种改进方案:
Double DQN
Double DQN 核心思想:
使用主网络选择动作,目标网络评估动作价值,解耦选择和评估:
Q_target = r + γ · Q_target(s', argmax_{a'} Q_main(s', a'))
效果:
- 有效降低Q值的过高估计偏差
- 在多个Atari游戏中取得比DQN更好的性能
- 实现简单,计算成本几乎不变
实现改动(只修改DQN update中的目标计算):
def compute_target(self, rewards, next_states, dones):
with torch.no_grad():
# 主网络选择最优动作
best_actions = self.q_net(next_states).argmax(1, keepdim=True)
# 目标网络评估该动作的价值
max_next_q = self.target_net(next_states).gather(1, best_actions)
target_q = rewards + self.gamma * max_next_q * (1 - dones)
return target_q
Dueling DQN
Dueling DQN将Q网络分解为状态值函数V(s)和优势函数A(s,a)两条独立流:
Dueling网络架构:
Q(s, a; θ, α, β) = V(s; θ, β) + A(s, a; θ, α) - (1/|A|) Σ_{a'} A(s, a'; θ, α)
结构特点:
- 共享卷积层(特征提取)
- 分成两支:V流输出标量,A流输出|A|维向量
- 通过减去A的均值确保可辨识性
优势:
- 在状态差异大但动作差异小的场景中更有效
- V(s)的学习更稳定,因为每个更新影响所有动作
- 无需额外监督信号即可学习哪些状态本身更有价值
Dueling DQN 代码实现:
class DuelingDQN(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=128):
super().__init__()
# 共享特征层
self.feature = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU()
)
# 值函数流: 输出标量 V(s)
self.value_stream = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
# 优势函数流: 输出向量 A(s, a)
self.advantage_stream = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, action_dim)
)
def forward(self, x):
features = self.feature(x)
V = self.value_stream(features)
A = self.advantage_stream(features)
return V + A - A.mean(dim=1, keepdim=True)
Prioritized Experience Replay(优先经验回放)
并非所有经验都同等重要。优先经验回放根据TD误差的大小为经验分配采样权重:
- 采样概率:P(i) = p_i^α / Σ_k p_k^α,其中 p_i = |δ_i| + ε 是TD误差的绝对值
- 重要性采样权重:w_i = (1/N · 1/P(i))^β,用于修正采样偏差
- β从0.4逐渐增加到1:开始时完全接受偏差,结束时完全修正
五、策略梯度方法
值函数方法(如DQN)通过学习值函数间接得到策略。而策略梯度方法(Policy Gradient Methods)直接学习策略参数θ,将策略π_θ(a|s)参数化,并通过梯度上升最大化期望累积奖励。这种方法的核心优势是能处理连续动作空间和自然引入随机策略。
5.1 策略梯度定理
策略梯度定理是策略梯度方法的理论基础,它给出了目标函数J(θ) = E[Σ γ^t r_t]对策略参数θ的梯度公式:
策略梯度定理 (Policy Gradient Theorem):
∇_θ J(θ) = E_{π_θ} [ ∇_θ log π_θ(a|s) · Q^{π_θ}(s, a) ]
直观解释:
- ∇_θ log π_θ(a|s) 是"得分函数",指向增加动作a概率的方向
- Q^{π_θ}(s, a) 是"权重",告诉智能体这个动作有多好
- 整体效果:提升好动作的概率,降低差动作的概率
- 这是"好东西就多做"原则的数学形式化
5.2 REINFORCE算法(蒙特卡洛策略梯度)
REINFORCE(又称Monte Carlo Policy Gradient)是最基础的策略梯度算法:
REINFORCE 算法流程:
1. 初始化策略网络参数 θ
2. 对于每个episode:
a. 使用当前策略 π_θ 生成完整轨迹 (s_0, a_0, r_1, ..., s_T)
b. 计算每个时间步的回报 G_t = Σ_{k=t}^{T} γ^{k-t} · r_k
c. 更新策略参数:
θ ← θ + α · Σ_t ∇_θ log π_θ(a_t|s_t) · G_t
REINFORCE vs 带基线的REINFORCE:
基本REINFORCE使用 G_t 作为权重,但G_t的方差很大。
引入基线 b(s) 可以降低方差:
∇_θ J(θ) = E[ ∇_θ log π_θ(a|s) · (Q(s,a) - b(s)) ]
自然选择的基线是 状态值函数 V(s):
∇_θ J(θ) = E[ ∇_θ log π_θ(a|s) · (Q(s,a) - V(s)) ]
= E[ ∇_θ log π_θ(a|s) · A(s, a) ]
其中 A(s, a) = Q(s, a) - V(s) 称为 优势函数 (Advantage Function)
class REINFORCEAgent:
def __init__(self, state_dim, action_dim, lr=1e-3, gamma=0.99):
self.policy_net = PolicyNetwork(state_dim, action_dim)
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
self.gamma = gamma
self.log_probs = []
self.rewards = []
def choose_action(self, state):
state_t = torch.FloatTensor(state).unsqueeze(0)
probs = self.policy_net(state_t)
dist = torch.distributions.Categorical(probs)
action = dist.sample()
self.log_probs.append(dist.log_prob(action))
return action.item()
def store_reward(self, reward):
self.rewards.append(reward)
def update(self):
# 计算折扣回报 G_t
returns = []
G = 0
for r in reversed(self.rewards):
G = r + self.gamma * G
returns.insert(0, G)
returns = torch.FloatTensor(returns)
returns = (returns - returns.mean()) / (returns.std() + 1e-8) # 归一化降方差
# 策略梯度更新
policy_loss = []
for log_prob, G_t in zip(self.log_probs, returns):
policy_loss.append(-log_prob * G_t) # 负号因为梯度上升用优化器的梯度下降
self.optimizer.zero_grad()
policy_loss = torch.cat(policy_loss).sum()
policy_loss.backward()
self.optimizer.step()
self.log_probs.clear()
self.rewards.clear()
REINFORCE的局限性:① 完整episode才能更新,学习效率低;② 回报G_t的方差很大,导致训练不稳定;③ 在线策略特性,无法重复利用历史数据。
5.3 Actor-Critic算法
Actor-Critic结合了策略梯度(Actor)和值函数(Critic)的优势。Actor负责学习策略,Critic负责评估动作的好坏并提供低方差的学习信号:
Actor-Critic 架构:
┌─────────────────┐ ┌─────────────────┐
│ Actor │ │ Critic │
│ (策略网络) │ │ (值函数网络) │
│ π_θ(a|s) │ │ V_φ(s) / Q_φ │
│ 输出动作分布 │ │ 评估状态价值 │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────┬────────────────┘
│
TD误差 δ_t = r + γ·V(s') - V(s)
Actor梯度: ∇_θ log π_θ(a|s) · δ_t
Critic损失: MSE(δ_t)
算法流程:
1. 初始化 Actor θ 和 Critic φ
2. 对于每个时间步:
a. Actor 根据状态 s 选择动作 a ~ π_θ(·|s)
b. 执行动作 a,观测奖励 r 和新状态 s'
c. 计算 TD误差:δ = r + γ·V_φ(s') - V_φ(s)
d. 更新 Critic:φ ← φ - α_c · ∇_φ δ²
e. 更新 Actor:θ ← θ + α_a · ∇_θ log π_θ(a|s) · δ
Actor-Critic的优势:① 单步更新而非等待episode结束,学习更高效;② Critic提供低方差的TD误差作为学习信号;③ 自然支持连续动作空间;④ 可扩展到大规模问题。Actor-Critic是后续几乎所有先进RL算法(A2C, A3C, PPO, SAC, DDPG, TD3)的共同基础框架。
5.4 A2C 与 A3C
A2C(Advantage Actor-Critic)使用优势函数 A(s,a) = Q(s,a) - V(s) 替代原始的TD误差,进一步降低方差。A2C可并行执行多个环境,收集多样化数据:
A2C 损失函数:
1. Actor损失(策略梯度 + 熵正则):
L_actor = -E[ log π_θ(a|s) · A(s,a) ] - β · H(π_θ(·|s))
(熵正则项 H(π) 鼓励探索,防止策略过早收敛到确定性策略)
2. Critic损失(值函数逼近):
L_critic = E[ (V_φ(s) - G_t)² ] 或 E[ δ_t² ]
3. 总损失:L = L_actor + c_v · L_critic
A3C(Asynchronous Advantage Actor-Critic)是A2C的异步版本:
- 使用多个并行worker独立采样和训练
- 每个worker有自己的环境副本和环境副本
- worker异步将梯度推送到全局参数
- 无经验回放,依赖异步带来的数据多样性打破相关性
六、近端策略优化(PPO)
近端策略优化(Proximal Policy Optimization,PPO)由OpenAI于2017年提出,是当前最流行的强化学习算法之一。PPO解决了传统策略梯度方法中步长难以选择的问题:步长太大会导致性能崩溃,步长太小则学习缓慢。
6.1 PPO的核心思想:裁剪替代目标
PPO的思想是在每次更新中限制策略的变化幅度,避免一步更新过大导致策略"跳下悬崖"。具体通过替代目标函数和裁剪机制实现:
PPO裁剪目标函数 (Clipped Surrogate Objective):
L^{CLIP}(θ) = E_t[ min( r_t(θ) · A_t, clip(r_t(θ), 1-ε, 1+ε) · A_t ) ]
其中:
- r_t(θ) = π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t) :新旧策略的概率比
- A_t:优势函数估计
- ε:裁剪范围(通常设为 0.1 或 0.2)
- clip 将概率比限制在 [1-ε, 1+ε] 范围内
理解裁剪机制:
- 当 A_t > 0(好动作):max限为 (1+ε)·A_t,不会过度提升好动作的概率
- 当 A_t < 0(差动作):min限为 (1-ε)·A_t,不会过度降低差动作的概率
- 裁剪后取 min,确保目标函数是对未裁剪目标的"悲观下界"
6.2 PPO的完整损失函数
PPO完整损失函数:
L^{PPO}(θ) = L^{CLIP}(θ) - c_1 · L^{VF}(φ) + c_2 · S[π_θ](s)
其中:
1. 裁剪策略损失 L^{CLIP}(θ):
限制策略更新幅度的主体损失
2. 值函数损失 L^{VF}(φ) = (V_φ(s_t) - V_t^{target})²:
Critic网络的均方误差
3. 策略熵 S[π_θ](s):
鼓励探索的正则项
多步优势估计(GAE, Generalized Advantage Estimation):
A_t^{GAE(λ,γ)} = Σ_{l=0}^{∞} (γλ)^l · δ_{t+l}
其中 δ_t = r_t + γ·V(s_{t+1}) - V(s_t)
GAE在偏差和方差之间提供了精细的控制:
- λ → 0:高偏差、低方差(类似单步TD)
- λ → 1:低偏差、高方差(类似蒙特卡洛)
6.3 PPO完整实现代码
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
class ActorCritic(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=256):
super().__init__()
# 共享特征提取层
self.feat = nn.Sequential(
nn.Linear(state_dim, hidden_dim), nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim), nn.Tanh(),
)
# Actor: 输出动作分布参数
self.actor_mean = nn.Linear(hidden_dim, action_dim)
self.actor_logstd = nn.Parameter(torch.zeros(action_dim))
# Critic: 输出状态值
self.critic = nn.Linear(hidden_dim, 1)
def forward(self, x):
feat = self.feat(x)
mean = self.actor_mean(feat)
std = torch.exp(self.actor_logstd)
value = self.critic(feat)
return mean, std, value
def get_action(self, state, deterministic=False):
mean, std, _ = self.forward(state)
dist = torch.distributions.Normal(mean, std)
if deterministic:
action = mean
else:
action = dist.sample()
log_prob = dist.log_prob(action).sum(dim=-1)
return action, log_prob
class PPOAgent:
def __init__(self, state_dim, action_dim,
lr=3e-4, gamma=0.99, clip_epsilon=0.2,
epochs=10, batch_size=64, gae_lambda=0.95):
self.actor_critic = ActorCritic(state_dim, action_dim)
self.optimizer = optim.Adam(self.actor_critic.parameters(), lr=lr)
self.gamma = gamma
self.clip_epsilon = clip_epsilon
self.epochs = epochs
self.batch_size = batch_size
self.gae_lambda = gae_lambda
def compute_gae(self, rewards, values, dones, next_value):
# 计算GAE优势函数估计
advantages = []
gae = 0
values = values + [next_value]
for t in reversed(range(len(rewards))):
delta = rewards[t] + self.gamma * values[t + 1] * (1 - dones[t]) - values[t]
gae = delta + self.gamma * self.gae_lambda * (1 - dones[t]) * gae
advantages.insert(0, gae)
return advantages
def update(self, trajectories):
# 从trajectories中提取数据并转换为tensor
states, actions, old_log_probs, returns, advantages = \
self.prepare_data(trajectories)
# 多轮PPO更新(重要性采样)
for _ in range(self.epochs):
indices = np.arange(len(states))
np.random.shuffle(indices)
for start in range(0, len(states), self.batch_size):
batch = indices[start:start + self.batch_size]
batch_states = states[batch]
batch_actions = actions[batch]
batch_old_log_probs = old_log_probs[batch]
batch_returns = returns[batch]
batch_advantages = advantages[batch]
# 归一化优势值
batch_advantages = (batch_advantages - batch_advantages.mean()) \
/ (batch_advantages.std() + 1e-8)
# 获取新策略的log_prob和value
_, new_log_probs = self.actor_critic.get_action(batch_states)
values = self.actor_critic.forward(batch_states)[2].squeeze()
# 计算概率比 r_t(θ)
ratio = torch.exp(new_log_probs - batch_old_log_probs)
# 裁剪替代目标
surr1 = ratio * batch_advantages
surr2 = torch.clamp(ratio, 1 - self.clip_epsilon,
1 + self.clip_epsilon) * batch_advantages
actor_loss = -torch.min(surr1, surr2).mean()
# Critic损失
critic_loss = nn.MSELoss()(values, batch_returns)
# 总损失(可添加熵正则)
total_loss = actor_loss + 0.5 * critic_loss
self.optimizer.zero_grad()
total_loss.backward()
nn.utils.clip_grad_norm_(self.actor_critic.parameters(), 0.5)
self.optimizer.step()
PPO的实践技巧:① 使用GAE计算优势函数,λ推荐0.95-0.99;② 优势值归一化(减均值除以标准差)可大幅稳定训练;③ 梯度裁剪(gradient clipping)防止梯度爆炸;④ PPO的epochs通常设为3-15,过大会导致过度优化;⑤ 应在每个PPO更新前后用KL散度监控策略变化,过大则减小学习率。
6.4 PPO vs 其他策略梯度算法
| 算法 | 策略更新方式 | 数据效率 | 稳定性 | 实现复杂度 |
| REINFORCE | 完整episode后更新 | 低 | 低(高方差) | 简单 |
| A2C/A3C | 单步/多步更新 | 中 | 中 | 中 |
| PPO | 裁剪约束更新 | 高 | 高 | 中 |
| TRPO | KL散度约束 | 高 | 高 | 复杂(共轭梯度) |
| SAC | 最大熵框架 | 很高 | 很高 | 中 |
| DDPG/TD3 | 确定性策略梯度 | 高 | 中-高 | 中 |
PPO的成功之处:PPO几乎成了深度RL的"默认选择"——它兼具TRPO的稳定性(有约束)和A2C的简洁性(一阶优化)。PPO在连续控制、Atari游戏、机器人操纵、自动驾驶、游戏AI(如OpenAI Five在Dota 2中的应用)等领域都取得了顶尖效果。ChatGPT的RLHF训练阶段也使用了PPO算法。
七、强化学习算法总结对比
以下是对本笔记中所有介绍算法的完整对比:
| 算法 | 类型 | 状态空间 | 动作空间 | 策略类型 | 关键特点 |
| 策略迭代/值迭代 | Model-Based | 离散 | 离散 | - | 需已知模型,动态规划 |
| Q-Learning | Value-Based, Off-Policy | 离散 | 离散 | ε-greedy | Q表更新,学习最优Q* |
| SARSA | Value-Based, On-Policy | 离散 | 离散 | ε-greedy | 考虑探索代价,更安全 |
| DQN | Value-Based, Off-Policy | 连续 | 离散 | ε-greedy | 神经网络+经验回放+目标网络 |
| Double DQN | Value-Based, Off-Policy | 连续 | 离散 | ε-greedy | 解耦动作选择和评估,降低过估计 |
| Dueling DQN | Value-Based, Off-Policy | 连续 | 离散 | ε-greedy | V流和A流分离架构 |
| REINFORCE | Policy-Based, On-Policy | 连续 | 均可 | 随机 | 蒙特卡洛梯度,高方差 |
| Actor-Critic | Hybrid, On-Policy | 连续 | 均可 | 随机 | 策略+值函数,单步更新 |
| A2C/A3C | Hybrid, On-Policy | 连续 | 均可 | 随机 | 优势函数+多worker并行 |
| PPO | Hybrid, On-Policy | 连续 | 均可 | 随机 | 裁剪约束,SOTA稳定 |
八、核心要点总结
强化学习基础核心要点:
- RL三要素:状态、动作、奖励,智能体在环境中通过试错学习最优策略
- MDP框架:强化学习问题的数学形式化,关键概念包括状态转移、折扣因子γ、回报Return
- Bellman方程:RL的"递归灵魂",将当前价值与未来价值联系起来,是几乎所有RL算法的数学基础
- Q-Learning vs SARSA:核心区别是离线策略(学习最优路径)vs 在线策略(学习安全路径),选择取决于风险承受能力
- DQN技术创新:经验回放打破数据相关性,目标网络稳定训练,这两项技术让深度学习与RL的结合成为可能
- 从值函数到策略梯度:值函数方法适合离散动作,策略梯度方法适合连续动作,Actor-Critic结合两者优势
- PPO的裁剪机制:通过限制策略更新幅度平衡学习速度与稳定性,是目前最通用的RL算法
- 探索-利用权衡:ε-greedy、熵正则、Noisy Networks等方法都在解决这一RL核心难题
学习路径建议:从Q-Learning理解基本思想 → 用DQN学习深度RL → 用REINFORCE理解策略梯度 → 用Actor-Critic掌握混合方法 → 用PPO解决实际问题。建议在OpenAI Gym或MuJoCo环境中动手实验每个算法。
九、进一步思考与实践
掌握了上述基础算法后,可以进一步探索以下方向:
- 基于模型的RL:MuZero算法(AlphaZero的扩展),学习环境模型并通过规划决策
- 逆强化学习:从人类演示中推断奖励函数,用于模仿学习和RLHF
- 多智能体强化学习:MADDPG, QMIX等,用于博弈和协作环境
- 分层强化学习:将复杂任务分解为子任务的学习框架
- 离线强化学习:从静态数据集中学习策略,无需与环境交互(BCQ, CQL, IQL等)
- 分布式RL:IMPALA, R2D2, Agent57等大规模分布式RL系统
- 应用落地:机器人控制、自动驾驶决策、推荐系统、资源调度、金融交易、游戏AI
"强化学习不是关于知道答案,而是关于学会提问——通过与世界的交互,算法发现了超越任何人类直觉的最优解。"
实践建议:推荐使用Stable-Baselines3(SB3)库作为PPO等算法的可靠实现基准,在Gymnasium环境中进行实验。先从CartPole等简单环境开始,逐步挑战HalfCheetah、Humanoid等连续控制任务。