6.3 动手:摇骰子赌博机
本节目标:在一个只有两个动作的赌博机上实现最小策略梯度实验,观察奖励怎样改变动作概率。
学习路径:2.1 探索与利用 → 6.1 策略梯度与 REINFORCE → 6.3 摇骰子赌博机
本节代码与资源:本页给出环境、策略网络与训练循环的完整实现,可按代码块顺序直接运行。
把“用 PyTorch 实现策略网络”中的完整代码保存为 policy_gradient_bandit.py,然后运行:
python policy_gradient_bandit.py程序会打印训练开始和结束时选择 B 的概率。由于代码没有固定随机种子,每次结果会略有不同;检查重点是多次运行中 B 的最终概率通常高于初始概率。
先看一个只有两个摇臂的赌博机。选择红色摇臂时,中奖概率是 30%;选择蓝色摇臂时,中奖概率是 70%。每轮只能选择一个摇臂,中奖得到 1,未中奖得到 0。
我们知道蓝色摇臂更好,智能体不知道。它从接近均匀的动作概率开始,根据每次中奖或落空的结果更新策略。这个极简环境没有状态转移,可以把注意力集中在策略梯度最基本的学习信号上:得到较高回报的动作,下一次被选中的概率会提高。
第 2 章的猜硬币例子直接指定了确定性策略;这里要学习一个参数化策略 。训练是否成功,可以直接检查蓝色摇臂的概率是否逐步接近 1。
6.3.1 两臂赌博机环境
┌──────────────────────────────────┐
│ │
│ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │
│ │🔴│ │🔵│ │
│ └─┬─┘ └─┬─┘ │
│ │ 赢率 30% │ 赢率 70% │
│ │ │ │
│ └───────────────┘ │
│ │
│ 规则:选一个摇臂,摇中 +1 分 │
│ 目标:让 AI 自己发现 B 更好 │
└──────────────────────────────────┘6.3.2 用 PyTorch 实现策略网络
策略网络只有一个 Softmax 层。这个环境没有变化的状态,因此输入固定为常数 1,输出是两个动作的概率:
import torch
import torch.nn as nn
import torch.optim as optim
import random
import numpy as np
# ==========================================
# 1. 策略网络 与 只有一个 Softmax 层
# ==========================================
class PolicyNetwork(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(1, 2) # 1个输入(常数),2个输出(A和B的概率)
def forward(self, x):
logits = self.linear(x)
return torch.softmax(logits, dim=-1) # Softmax 保证输出是概率分布
policy = PolicyNetwork()
optimizer = optim.Adam(policy.parameters(), lr=0.01)
# ==========================================
# 2. 环境 与 两臂赌博机
# ==========================================
win_probs = [0.3, 0.7] # A: 30%, B: 70%
def pull_arm(action):
return 1.0 if random.random() < win_probs[action] else 0.0
# ==========================================
# 3. REINFORCE 训练(先看效果,原理下一节详解)
# ==========================================
prob_history = []
num_episodes = 300
for ep in range(num_episodes):
state = torch.tensor([1.0])
# 策略网络输出动作概率,按概率采样
probs = policy(state)
dist = torch.distributions.Categorical(probs)
action = dist.sample() # 按概率随机选一个动作
log_prob = dist.log_prob(action) # log π(a|s)
# 执行动作,获取奖励
reward = pull_arm(action.item())
# REINFORCE 核心:好结果 → 增加概率
loss = -log_prob * reward
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录策略概率
with torch.no_grad():
prob_history.append(policy(state)[1].item()) # 选择 B 的概率
print(f"初始 P(B): {prob_history[0]:.3f}")
print(f"最终 P(B): {prob_history[-1]:.3f}")核心更新是 loss = -log_prob * reward。如果这次动作得到 reward=1,梯度会提高该动作再次被选中的概率;如果得到 reward=0,这一步不产生梯度。前面的负号把“最大化奖励”转换为 PyTorch 可以执行的“最小化损失”。
这个公式不是凭空设计的——它是策略梯度公式 在单步赌博机上的特例。下一节将完整推导它。
6.3.3 训练现象
运行代码后,策略概率的演化过程大致如下:
选择 B 的概率演化
1.0 ┤
│ ╱━━━━━━━━━━━━━━━━━━ ← 收敛:稳定在 0.85-0.95
0.9 ┤ ╱━╱
│ ╱╱╱╱╱
0.8 ┤ ╱╱╱╱
│ ╱╱╱╱ ← 爬升:发现 B 更好
0.7 ┤ ╱╱╱╱
│╱╱╱╱
0.6 ┤╲╱
│ ╲ ╱╲ ╱
0.5 ┤─╲╱╲╱╲╱╲──────────── ← 开局:在 0.5 附近波动(两个都试试)
└────────────────────────────────────────
0 50 100 150 200 250 300
Episode三个阶段很清晰:开局策略接近均匀分布("两个都试试",探索为主);然后选择 B 的概率逐渐上升("发现 B 拿到奖励的次数更多");最后稳定在高概率区间("就选 B 了")。
曲线不会平滑上升,而是带有明显波动。原因是每次更新只看到一次随机采样,单次结果可能偏离摇臂的真实中奖率。这种波动称为策略梯度的高方差。
6.3.4 梯度噪声与训练震荡
策略梯度的更新依赖于采样。每次更新时,网络只能看到这一次采样到的结果:
- 如果某次选择 B 但没有中奖,网络收到
reward=0,这一步不会继续提高 B 的概率; - 如果某次选择 A 并且中奖,网络收到
reward=1,A 的概率会提高,虽然 A 的长期中奖率更低。
单次采样可能给出与长期平均值不同的信号,因此策略会在总体正确的方向附近来回波动。
把学习率从 0.01 改成 0.1 后,一次随机中奖会造成更大的概率变化。此时策略容易在 A 和 B 之间剧烈波动,难以稳定在较优动作附近。
6.3.5 探索与利用的权衡
训练曲线的震荡还揭示了 RL 最核心的张力——探索与利用:
- 训练初期:策略接近均匀分布(探索为主)——"两个都试试看"
- 训练后期:策略收敛到确定动作(利用为主)——"就选 B 了"
- 过渡必须平稳:太快收敛可能只找到局部最优,太慢则浪费数据
训练初期需要让两个动作都有被采样的机会。训练后期则可以更多选择当前估计较好的动作;若动作概率过早接近 0 或 1,尚未充分尝试的动作将很难再得到数据。
第 8 章 PPO 中的 entropy bonus(熵奖励)就是强制策略保持探索的机制——它在损失函数里加了一个惩罚项,防止策略过早变得"太确定"。
本节小结
- 两臂赌博机把策略梯度压缩成一个可直接观察的量:选择蓝色摇臂的概率。
- 奖励为正时,对应动作的对数概率会被提高;随机中奖和落空会让曲线产生波动。
- 学习率决定单次反馈改变策略的幅度,动作奖励差距越小,稳定识别最优臂需要的样本越多。
思考题:如果 B 的赢率只有 55%(而不是 70%),策略还能学会吗?
能学会,但学得更慢、震荡更大。因为 A 和 B 的差距更小(55% vs 30%),采样到"误导性数据"的概率更高。这再次说明策略梯度的核心挑战——当"好"和"坏"的差距不大时,高方差会让学习变得极其困难。这也是为什么后续我们需要引入基线(Baseline)来降低方差。
本节从一次实际更新观察了 -log_prob * reward 怎样改变动作概率。策略梯度定理将进一步说明这个更新式从哪里来。