工具
MuJoCo environments 指的是使用 MuJoCo 物理引擎构建的模拟环境 . MuJoCo (Multi-Joint dynamics with Contact) 是一款用于机器人、生物力学、图形和动画等领域的研究和开发的物理引擎,它能够进行快速而精确的仿真。
CartPole-v1 环境主要存在于 Gymnasium 库中,它是 Gym 库的后继者 。Gym 库已经停止更新,所有开发工作都已转移到 Gymnasium。
CartPole-v1 环境信息
首先要知道 CartPole-v1 环境的动作和观测空间:
1. 动作空间 (Action Space):
Discrete 类型,数量2,取值0(将车向左推),或取值1(将车向右推)
2. 观测空间 (Observation Space):
Box 类型,取值4个,具体的:
- 0: 车的位置(Cart Position),范围为
[-4.8, 4.8],但如果超出[-2.4, 2.4]范围,则 episode 结束 - 1: 车的速度(Cart Velocity),范围为
[-Inf, Inf]。 - 2: 杆的角度(Pole Angle),范围约为
[-0.418 rad (-24°), 0.418 rad (24°)],但如果超出[-0.2095, 0.2095](±12°) 范围,则 episode 结束 - 3: 杆的角速度(Pole Angular Velocity),范围为
[-Inf, Inf]。
所以Pole 是不会倒下的,倒下之前就 terminated 了,这是CartPole的定义。
KAQ: Observation 和 State 的区别
在完全可观测环境中(如CartPole),Agent 可以直接获取环境的真实状态,即 observation 等于 state。
3. rewards 初始化为 0,完整训练后,rewards 是全1
reward 从 next_obs, reward, terminateds, truncateds, info = envs.step(action.cpu().numpy()) 来。所以对于 Cartpole-v1 的环境,需要了解其 reward 是如何设计的。
CartPole-v1 环境中,奖励(reward)的设计非常简单:
每次调用
env.step(action),如果环境没有终止(terminated=False)或中止(truncated=False),智能体获得 +1 的奖励。即,只要杆没有倒下(角度在 ±12 度内)且小车没有超出轨道边界(±2.4),每一步都会得到 reward=1.0。如果环境终止(terminated=True,例如杆倒下或小车出界),当前步的奖励仍然是 +1,但环境会重置(reset),后续步数不再获得奖励。
如果环境中止(truncated=True,例如达到最大步数 500),当前步的奖励也是 +1,然后环境重置。
故,对于 CartPole-v1,rewards[t] 通常是全 1 的数组,除非某些环境终止或中止。
Setup
1.环境准备 使用 conda
conda create -n ppo-env python=3.9
conda activate ppo-env
# conda install pytorch torchvision torchaudio cudatoolkit=11.6 -c pytorch -c conda-forge
conda install -vvv pytorch torchvision torchaudio cpuonly -c pytorch
conda install -v -c conda-forge gym tensorboard stable-baselines3 wandb
另外的 prerequests:
conda install -c conda-forge moviepy
conda install -c conda-forge pygame
2.wandb 机器学习 track 平台
python ppo.py --track : 首先需要创建一个 Create a W&B account 才能使用对应的云服务
细节
EWY2Y: 描述 ppo 的完整过程
环境是 CarPole-v1,其目标是坚持更长的 timestep,不失败。每一个 timestep 只要没有失败就返回 reward 1,如此就是,累计 reward 越大越好。即回合持续 100 个 timestep,累计奖励 = 100;回合持续 500 个 timestep,累计奖励 = 500。
PPO 是 on-policy,需要在每个 rollout 开始时,根据当前的 policy 进行采样(采样之后才会进行训练)。policy: $\pi(a|s)$ 通过 actor-critic 中的 actor 建模。一次 rollout 就是一次 Agent 根据当前的 policy 执行与环境的交互,它的输出包括:
- 各个 Observation 的 action 即其概率
它为每个 Observation 计算得到每个 action 及其概率,表示在这个 state 时,Agent 执行各个 action 的概率。Observation 的数量在此与 State 数量相同。
- 另一个重要输出:critic 计算得到的 Value
他是 actor-critic 类算法的另一个重要组件,它给出值估计 $V(s)$ 。当这个 rollout 解结束时,需要时间倒流计算累计的 return 命名为 batch_return,它直接与这个 rollout 过程每一个执行了的 action 的 rewards 相关。Critic的作用是在一整个 rollout 中跟具当前 Policy 可以获得的总奖励,间接判断当前 policy 的好坏。Critic 给policy 打分的同时,自身也会学习,通过优化 value-loss,它的打分能力会越来越好。
上述两个部分输出都会被记录在存储空间中,待后续或下一次的 rollout 访问。
这次 rollout 结束后之后开始训练,过程中计算 loss,并通过反向传播更新参数。所有学习到的内容通过 Agent 的网络参数表示。但是在开始训练之前 PPO 提出来:计算优势函数(Advantage Function) 或者 广义优势估计(Generalized Advantage Estimation, GAE)。(它与 rewards 相关)。
优势函数 或 GAE 用于计算,advantage 表示了哪些动作更有价值,他会驱动 policy 向高回报倾斜向。具体讲:
通过一次 reverse 循环,得到从当前状态到回合技术的累计回报。
本次 rollout 的更新结束之后,下一次 rollout 使用更新后的 actor 和 critic,生成新的 values 和 returns。
PPO 的目标有 3 个:loss,分别是 policy loss, value loss 和 entropy loss。clip 体现在 policy loss 中,体现在 value loss 中,用于限制 value 的变化幅度。
- policy-loss
PPO 提出计算新旧 policy 的概率比,ratio。policy-loss 是 ratio 和上述 GAE 的函数。目标是最 (大/小)?
- value-loss
训练阶段,根据更新了的 policy 得到新的值估计 new_value,与存储空间中的上一个 rollout 的累计 return 计算均方误差,作为 value-loss。目标是最小化,它的作用是让Critic 学习,提升它的能力。
- entropy-loss
目标是最大化熵,使得 state 中的各个 action,执行的概率接近,用于鼓励探索,避免陷入局部最优。他是Policy 学习的目标函数之一。
最终通过梯度反向传播,更新 Actor 和 Critic 两个网络的参数。
KAQ: value loss的计算为什么需要计算 return 值?
。。。
KAQ: 数据采集和更新的核心设计理念
这里的多重循环,提现了数据采集和更新的设计。下 code 中包含了 ppo 算法步骤。
parser.add_argument("--total-timesteps", type=int, default=25000,
help="total timesteps of the experiments")
parser.add_argument("--num-envs", type=int, default=6,
help="the number of parallel game environments")
parser.add_argument("--num-steps", type=int, default=128,
help="the number of steps to run in each environment per policy rollout")
parser.add_argument("--num-minibatches", type=int, default=4,
help="the number of mini batches to split the data into")
parser.add_argument("--update-epochs", type=int, default=4,
help="the K epochs to update the policy")
args.batch_size = int(args.num_envs * args.num_steps) # 6 x 128 = 768
args.minibatch_size = int(args.batch_size // args.num_minibatches) # 768 // 4 = 129
num_rollout = args.total_timesteps // args.batch_size
for rollout_id in range(1, num_rollout + 1):
...
for step_id in range(0, args.num_steps):
...
根据policy,进行采样,得到actions,logprobs
执行 envs.step 得到next-obs 和 rewards
...
# 计算优势函数 或 GAE 计算
for t in reversed(range(args.num_steps)):
计算 GAE (与 rewards 有关): advantage
...
...
for epoch in range(args.update_epochs):
for start in range(0, args.batch_size, args.minibatch_size):
end = start + args.minibatch_size
mb_idx = batch_idx[start:end]
对 b_obs[mb_idx] 的操作。。。
进行训练过程
...
根据新 policy,得到 newlogprob 等
计算 Probability Ratio,他与上述GAE有关
计算 entropy_loss 和 value_loss
得到目标:loss = policy_loss - args.ent_coef * entropy_loss + value_loss * args.vf_coef
进行梯度更新:
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm) # extra clipping
optimizer.step()
...
上述是 code 训练框框 提现了数据采集和更新。概括讲,就是 什么时候执行 policy,什么时候更具 policy 输出概率,entropy,等,用于计算各个loss,以及最终的参数更新。
KAQ: 什么是 RL 中的 rollout,它是否是 RL 领域的概念
Rollout 是 RL 中使用当前策略在环境中执行一系列交互( num_steps 步,每一步都有交互,并返回交互数据),收集轨迹数据的过程。数据包括 观测值、动作、奖励、值估计 等,用于 policy 和 value 优化。收集数据的方式是通过 agent.get_action_and_value 即当前 policy 进行环境交互。
rollout 是 RL 的核心概念,尤其在策略梯度算法(如 PPO)和蒙特卡洛方法中。
KAQ: 遍历一个 rollout 中的所有 time-step 收集轨迹数据的目的是?与后续的 loss 计算和 loss.backward 的关系是
这是 on-policy 的提现?
三个动作:
1. 执行 policy,进行采样
采样点结果包括action,log_prob, value,next_obs,reward:
for t in range(num_steps):
observations[t] = obs
action, log_prob, _, value = agent.get_action_and_value(obs)
actions[t] = action
log_probs[t] = log_prob
values[t] = value
next_obs, reward, terminated, truncated, _ = envs.step(action.cpu().numpy())
obs = torch.tensor(next_obs, dtype=torch.float32)
log_prob 用于计算 policy-loss,在第三步的时候还会执行一次 policy,得到newlogprob,用于计算ratio。
value:结合GAE得到的 return,一同表达优势(advantages = returns - values)。优势同样用于表达 policy-loss。
第三步中,返回 newvalue,value-loss 是(newvalue - return)一个函数。即这一步中的 return 参与 valueloss 的计算。
2. 计算优势估计
即 advantages = returns - values,return 是 rewards 的函数。
伪代码:
for t in reversed(range(num_steps)):
if t == num_steps - 1:
next_non_terminal = 1.0 - terminateds[t]
next_return = next_value.squeeze(-1)
else:
next_non_terminal = 1.0 - terminateds[t]
next_return = returns[t + 1]
returns[t] = rewards[t] + gamma * next_non_terminal * next_return
advantages = returns - values.squeeze(-1)
3. 执行 time-step
主要是执行一次 get_action_and_value 得到新的 newlogprob,新的 newvalue。进一步得到 各个loss,所以 loss 是依赖与环境交互生成的。
KAQ: 通过循环 epoch 次,充分利用已有的环境交互信息
“一次性收集 num_steps 次交互的数据,然后进行多次 epoch 更新(例如 3-10 次),比每次交互更新更高效。”
epoch: 对一次 rollout 收集的数据, 进行多次梯度更新。实现上,一次 rollout 收集的数据 会被分成多个 mini-batch,然后对每个 mini-batch 进行一次梯度计算和参数更新。
一个 epoch 表示对整个 rollout buffer 数据的一次完整遍历,即对所有 mini-batch 各更新一次(循环遍历所有 mini-batch)。
单次 epoch(即对数据遍历一次)可能无法充分挖掘数据的潜力,尤其是当 rollout 数据量较大时。多次 epoch 允许模型从同一批数据中学习更多信息,探索更准确的梯度方向。
每次调用 env.step() 需要模拟环境(例如,CartPole-v1 的物理模拟),可能涉及复杂的计算,尤其在复杂环境(如 Atari 或 MuJoCo)中。code 中每一个 rollout 中的每一 step,都会执行一次环境模拟计算(即一次next-obs, rewards = env.step()) 。
KAQ: optimizer 更新时,如何与 agent 关联,即如何更新 policy
optimizer = optim.Adam(agent.parameters(), lr=args.learning_rate, eps=1e-5)
for rollout
for step
for epoch # 充分利用从环境生成的数据
for minibatch
...
loss = policy_loss - args.ent_coef * entropy_loss + value_loss * args.vf_coef
...
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm) # extra clipping
optimizer.step()
首先查看 agent.parameters() 中内容:(即一个 nn.Module 对象中的 parameters() 中的内容)。迭代并打印每个参数:
for name, param in agent.named_parameters():
print(f"Layer: {name}, Size: {param.size()}, Requires_grad: {param.requires_grad}")
print(param.data)
print("----------")
每次调用优化器(optimizer.step())都涉及梯度计算、反向传播和参数更新,这是计算密集型的。
optimizer.zero_grad()
loss.backward()
optimizer.step()
此三句话表示了对 Agent 网络参数的更新,并且会同时更新 self.actor 和 self.critic 两个网络的参数:
1. optimizer 根据 传入的 agent.parameters() 实例化
optimizer 包含 Agent 类的所有可训练参数(self.actor 和 self.critic 的权重和偏置), 创建 optimizer 对象时,传入了 agent.parameters(), zero_grad() 会将这些参数的 .grad 属性置为零。
loss 是 PPO 算法的损失函数,包括:
- 策略损失(policy loss):基于 PPO 的 clipped surrogate objective,优化
self.actor。 - 值函数损失(value loss):基于值函数的均方误差,优化
self.critic。 - 熵正则化(entropy loss):鼓励探索,影响
self.actor。
通过loss.backward(), 反向计算梯度,更新 Agent 网络参数。
2. 调用 loss.backward() 时
PyTorch 的自动求导(autograd)会计算 loss 对 Agent 中所有可训练参数(self.actor 和 self.critic 的权重和偏置)的梯度,并存储在参数的 .grad 属性中。
具体讲:code 中 loss 同时依赖 self.actor(通过 policy_loss 和 entropy_loss)和 self.critic(通过 value_loss), 当调用 loss.backward() 时,梯度会传播到两个网络的参数。
PPO 算法中,self.actor 和 self.critic 的联合更新是核心设计。
KAQ: optimizer autograd
optimizer.step() 使用梯度(.grad)按照优化算法(Adam)更新 self.actor 的参数:
$$\theta \gets \theta - \eta \cdot \nabla_\theta \text{loss}$$
- $\theta$:self.actor 的参数(权重和偏置)。
- $\eta$:学习率(learning_rate)。
- $\nabla_\theta \text{loss}$: loss 对参数的梯度。
梯度传播调整 self.actor 的权重和偏置,改变 logits 的输出,从而改变 $\pi(a|s)$ 的概率分布。即更新了policy。
在下一次 rollout 中,get_action_and_value(obs) 使用更新后的 self.actor,生成新的动作和概率分布,反映策略的改进。
KAQ: 在一个 rollout 中,如何提现新策略(self.actor)和值函数(self.critic)的更新
通过 loss,loss 中含有 actor 和 critic 两个网络的参数,通过 loss.backward() 计算梯度后,进而通过 optimizer.step() 更新两个网络参数。
KAQ: env.step(action) 根据 policy 执行 timestep
env.step(action) 函数在 Gym 环境中执行一个时间步 (timestep),并返回环境对该动作的反馈。它根据给定的动作更新环境的状态,并返回新的观测、奖励、是否终止、是否截断以及调试信息。
env.step(action) 执行一个时间步,是根据传入的 action 来执行的。它代表了智能体 (agent) 在当前状态下选择执行的操作。
通常情况下是根据 policy,比如一个随机 policy:
def random_policy(observation):
return env.action_space.sample()
# 1. 根据当前观测选择动作
action = random_policy(observation)
# 2. 将动作传递给 env.step()
observation, reward, terminated, truncated, info = env.step(action)
一次 rollout 的最开始,即训练的最开始,policy 中的动作是随机的,但是随着训练的进行,policy 会逐渐变得更好。即
在 rollout 的第一步,智能体仍然会使用当前的策略来选择动作。 由于策略是随机初始化的,因此第一个动作本质上是随机的,但它仍然是由当前的 (随机) 策略生成的。
在 rollout 过程中,即后续更新的过程中,策略会不断地根据收集到的数据进行更新(参数迭代)。 每次更新都会使策略更接近于最优策略。
KAQ: PPO 的 policy
策略(policy)定义为一个映射函数 $\pi(a|s)$,从状态(state)到动作(action)的概率分布。是随机性策略。在 ppo 中通过由 self.actor 建模。
KAQ: policy get_action_and_value 传入 observation,输出 action 等信息他是如何与 ppo 中的步骤对应的?
这个过程其实就是执行一次 policy。那么这个过程附和 ppo 的算法中的步骤吗?
def get_action_and_value(self, x, action=None):
logits = self.actor(x)
probs = Categorical(logits=logits)
if action is None:
action = probs.sample()
return action, probs.log_prob(action), probs.entropy(), self.critic(x)
x 是当前观测值(obs),形状 (num_envs, obs_dim),来自 envs.step() 或 envs.reset()。
这里的 policy 执行是 ppo 的核心,self.actor(x) 计算动作的 logits,Categorical 转换为概率分布 $\pi(a|s)$,从中采样动作或计算概率。
什么是“计算动作的 logits”? self.actor 接受观测值 x 作为输入,通过神经网络计算动作的 logits(未归一化的分数),这些 logits 是策略 $\pi(a|s)$ 的中间表示,之后通过 Categorical 转换为概率分布。比如,如果 x 是 [[0.1, -0.2, 0.01, 0.05], …],self.actor(x) 可能输出 [[1.2, -0.3], …]。紧接着 转换为概率,例如 [[0.73, 0.27], …]。
上述表达了 PPO 的 rollout 就是需要策略 $\pi_\theta(a|s)$ 生成动作及其概率。
并且值估计 $V(s)$ 是 Actor-Critic 方法的核心,policy 中通过 self.critic 计算得出。
这正是 $\pi(a|s)$ 的执行过程:给定状态 $s$(即 x),输出动作分布并采样动作 $a$。
因此,get_action_and_value() 的过程可以看作执行一次策略(通过 self.actor)并同时获取值估计(通过 self.critic) 。这里是 actor 和 critic 之间的关系。未完,值估计怎么使用?
KAQ: 估计值如何用?
get_action_and_value()在采样时执行一次,在mini-batch的训练前即计算value-loss 时。
第一次的value用于计算 GA 估计中的 TD。
第二次的new-value 用于计算 value-loss。
KAQ: policy 的更新依据(提现在 policy-loss 部分)
使用 clipped surrogate objective 来优化策略:
ratio = torch.exp(new_log_prob - batch_log_probs)
surr1 = ratio * batch_advantages
surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * batch_advantages
policy_loss = -torch.min(surr1, surr2).mean()
1. 首先计算新旧 policy 的比率:
$$\text{ratio} = \frac{\pi_{\theta_{\text{new}}}(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} = \exp(\log \pi_{\theta_{\text{new}}}(a|s) - \log \pi_{\theta_{\text{old}}}(a|s))$$
2. 然后计算 advantage:
$$A(s, a) = Q(s, a) - V(s) \approx \text{returns} - \text{values}$$
returns:折扣回报,基于奖励(rewards)计算。values:由 self.critic 在 rollout 阶段计算(它估计状态,用于计算 advantage)。
其中的advantage是 returns 的函数,而returns又是 rewards的函数,advantage 表示了哪些动作更有价值,驱动策略向高回报动作倾斜。
3. 目的:
然后计算 clip ratio,最终得到policy-loss,目标是最大化 policy-loss。policy_loss 驱动 self.actor 向高优势(batch_advantages)的动作倾斜。
KAQ: policy 在什么时候发挥作用
get_action_and_value 如何与 PPO 算法中的步骤对应?
凡是执行 agent.get_action_and_value 的时候,就是 policy 发挥作用的时候
1. 在 rollout 阶段,PPO 使用当前策略 $\pi_\theta(a|s)$(self.actor)在环境中执行动作,收集轨迹数据。
每次交互需要:
- 根据当前状态 $s$(观测值)选择动作 $a$。
- 记录动作的对数概率 $\log \pi_\theta(a|s)$ 和熵 $H(\pi_\theta)$ 用于后续优化。
- 记录状态值 $V(s)$ 用于优势估计,即 GAE 的计算。
2. PPO 的策略损失(policy_loss)基于新旧策略的概率比,优化 self.actor 的参数。
KAQ: value-loss 如何发挥作用
value_loss = ((new_value.squeeze(-1) - batch_returns) ** 2).mean() 是 new_value 和真实回报的均方误差,它驱动 self.critic 优化。即,梯度会流向 self.critic 的参数,因为 value_loss 是 self.critic 输出(状态值)的函数。(new_value 就是 self.critic 的输出)。
其中 returns 是基于 rewards 的。目标最小化 value_loss 就会使 self.critic 预测更准确的状态值。
value-loss 也会剪裁,用于限制 new_value 的变化幅度:
v_clipped = values + torch.clamp(new_value - values, -clip_eps, clip_eps)
v_loss_clipped = ((v_clipped.squeeze(-1) - batch_returns) ** 2).mean()
value_loss = torch.max(value_loss, v_loss_clipped).mean()
即 这减少值函数更新的剧烈变化,增强稳定性。
KAQ: 反向传播作用于 value_loss,进而作用于 critic 的参数
value_loss = ((new_value.squeeze(-1) - batch_returns) ** 2).mean()
在 rollout 收集轨迹的过程中,得到 values 是 self.critic(obs) 的状态值估计。以及在 env.step() 得到的 next_obs, reward 。
但是,rollout 结束后,在优化阶段之前还有一步骤计算累计收益奖励(这里是标准优势估计):
with torch.no_grad():
next_value = agent.get_value(obs) # boostrap
returns = torch.zeros_like(rewards)
for t in reversed(range(num_steps)):
if t == num_steps - 1:
next_non_terminal = 1.0 - terminateds[t]
next_return = next_value.squeeze(-1)
else:
next_non_terminal = 1.0 - terminateds[t]
next_return = returns[t + 1]
returns[t] = rewards[t] + gamma * next_non_terminal * next_return
看,一步一步退回去的过程中计算折扣回报:
$$R_t = r_t + \gamma \cdot (1 - \text{terminated}t) \cdot R{t+1}$$
$\gamma$ 折扣因子,表示未来奖励的重要性降低,PPO 不仅看当前 reward,还考虑未来几步的reward。
$R_{t+1}$ 是下一 timestep 的回报,需要先计算。根据公式,如果直接从 $t = 0$ 开始正向计算,$R_{t+1}$ 还没有计算到,便无法得到 $R_t$。reverse 循环,可以通过一次循环计算所有 timestep 的 return。
在 CarPole 中,要算每一步的 returns,需要知道从这一步到游戏结束能拿多少奖励。
对于最后一步,critic 估计最后一个状态的值,作为初始的 $R_{t+1}$。即critic 直接评估最后一个step的obs。这称作boostrap。
如果某个环境在第 5 步终止(terminateds[5] = 1),后续步数会重置(envs.reset()),继续收集数据直到 num_step 步。 使用 gym.vector.SyncVectorEnv 或 gym.vector.AsyncVectorEnv env.step() 会在 terminated 或 truncated 为 True 时自动调用 reset。
所以 batch_returns 是在 rollout 结束后,在优化阶段之前 得到。
在下一次 rollout 循环中,使用更新了的 policy,更新过了的 actor & critic 估计新的 value:
- 更新后的
self.actor生成新的动作分布($\pi(a|s)$)。 - 更新后的
self.critic估计新的状态值(values)。
这里 reverse 循环当前 rollout 中的所有时间步。在CarPole环境中一个 step 相当于一个快照,在这个快照中,carpole 处于某个状态,这个状态包括车速、移动距离、杆的角度、杆移动的角速度。reverse 循环得到的是这个 rollout 中的 batch_return 累计奖励,用于评估当前step 状态的好坏。
Bootstrap(自举)是 PPO 中一种估计未来奖励的方法,用于处理回合未结束或数据截断的情况。简单来说,当我们无法直接知道未来的全部奖励时,用 Critic 来“猜”未来的累积奖励,作为一个近似值。这个“猜”的值就是 bootstrap 值。
KAQ: 实现上 actor 和 critic 的关系
见上
self.actor表示策略 $\pi(a|s)$,负责选择动作。self.critic表示值函数 $V(s)$,估计状态的长期回报,用于计算优势(advantages = returns - values)。actor 向高优势的 action 倾斜。
KAQ: critic 在 ppo 中的作用
Critic 评估的是从当前 step 到结束所有的奖励,而不是仅仅当前step的奖励。当前step奖励无法区分状态的好坏,竖直回报是1,快要倒了回报还是1。
Critic 在env中观察,之后猜这个env中一个 rollout 的价值有多少。这里的价值指的是,从这个场景开始,Agent follow当前的 Policy 能拿多少总奖励, 比如回合持续越长,值越高。如果没有 critic,Actor就不知道自己行为的好坏,只能瞎走。
Critic 给 Policy 打分的同时自身也会学习(通过 value-loss),所以它的打分能力会越来越好。PPO 在两个阶段计算Critic:
在 rollout 阶段
计算 obs 的所有 values,表示当前Policy 和 当前Critic 对状态的估计,用于计算 advantage:advantages = returns - values 的baseline,即初始分数,这个value会被记录下来,后面用来跟实际奖励(returns)比,判断动作好不好。
在优化阶段
提供当前估计值:_, new_log_prob, entropy, new_value = agent.get_action_and_value(batch_obs, batch_actions),这个估计是通过当前的 critic 网络估计的。它不准确,所以需要优化。
KAQ: critic 如何建模
Critic 的目标是使 $V(s)$ 接近真实的状态值,即折扣回报(returns)。Critic 通过 值函数损失(value_loss) 优化。最小化 value-loss 就是最小化预 测值和实际回报的均方误差,使 $V(s)$ 更准确。
critic 建模状态值函数 $ V(s) $,估计从状态 $ s $ 开始,遵循当前策略 $\pi$ 到回合结束的期望累积折扣回报:
$$ V(s) = \mathbb{E} \left[ \sum_{t=0}^\infty \gamma^t r_t \mid s_0 = s, \pi \right] $$
KAQ: critic 为什需要通过神经网络表达
critc 的输出用于计算优势 advantages = returns - values,驱动策略向高回报动作倾斜。
之所以 critic 要通过学习,是因为状态值函数 $V(s)$ 的真实值在复杂环境中难以直接计算。,且随策略 $\pi$ 和环境动态变化而变化。神经网络通过学习从 rollout 数据中估计 $V(s)$,能够适应复杂的状态空间、策略变化和环境的不确定性。
$V(s)$ 是从状态 $s$ 开始,遵循当前策略 $\pi$ 的期望累积回报。
通过神经网络可以捕捉状态和回报之间的复杂关系。并且可以结合其泛化能力,评估未见的状态。
KAQ: entropy loss 如何发挥作用
entropy-loss 的目的是鼓励更大的熵,即更均匀的概率分布,促进探索。更小的熵意味着利用。实际中 entropy loss是 - args.ent_coef * entropy_loss。
在PPO 初期,agent 需要进行大量的探索,以了解环境并发现有潜力的策略。Entropy loss 的作用是鼓励 agent 采取更多样化的行动,从而促进探索。随着训练的进行,agent 逐渐学习到较好的策略,探索的需求降低,应该更多地进行利用(exploitation),即采取已知的最优行动。
entropy 由 agent.get_action_and_value 计算,表示 $\pi_{\theta_{\text{new}}}(a|s)$ 的熵:
$$ H(\pi) = -\sum_a \pi(a|s) \log \pi(a|s) $$
为什么需要更均匀的概率分布?
熵:是策略 $\pi(a|s)$ 的动作概率分布的随机性度量。
- 高熵:概率分布更均匀(例如,[0.5, 0.5]),表示动作选择更随机。
- 低熵:概率分布更集中(例如,[0.99, 0.01]),表示动作选择更确定。
比如,如果 action 的概率是 [0.73, 0.27],熵是: $$H = -[0.73 \log(0.73) + 0.27 \log(0.27)] \approx 0.61$$
如果 action 的概率更均匀(如 [0.5, 0.5]),熵更高: $$H = -[0.5 \log(0.5) + 0.5 \log(0.5)] \approx 0.69$$
目标是让分布更均匀,即最大化 entropy。
更均匀的概率分布意味着动作选择更随机,agent 更可能尝试不同的动作,而不是总是选择概率最高的动作。例如,在 CartPole-v1 中,初始策略可能输出 [0.99, 0.01],几乎总是选择动作 0(向左推)。通过鼓励更大熵,分布可能变为 [0.6, 0.4],增加动作 1(向右推)的尝试概率。所以均匀的 entropy 表示鼓励 agent 进行探索,避免过载收敛,或者落入局部最优。
KAQ: reward 如何计算?如何与训练相关
reward 与 value-loss 相关,Value-loss 直接是 reward 的函数。PPO 计算的不是当个step的回报,而是整个rollout的累计回报 batch_return,用它表达策略的好坏。
KAQ: rollout 阶段 & 优化阶段的 get_action_and_value 输入 obs 的关系 【todo】
batch_obs 是 rollout 阶段 observations 的子集,确实是“之前 rollout 阶段已经生成的”。同一个 rollout 循环中,rollout 阶段和优化阶段的 obs 数据来源相同(但可能被打乱或分批)。
这里就是针对这个 Rollout的 obs 进行采样。也就是说,Rollout 阶段收集了一堆状态(比如 1000 个状态,observations),就像拍了1000张照片。优化阶段从这些照片里随机抽一些(batch_obs),让 Critic 重新看这些状态,给新的分数(new_value)。与obs的顺序无关,
Rollout 阶段和优化阶段的 obs 是否变化?不变化:在同一个 rollout 循环中,优化阶段的 batch_obs 是从 rollout 阶段的 observations 采样来的,状态数据本身(obs 的值)不变化。但是,优化阶段的 batch_obs 是从 rollout 阶段的所有 observations 中随机采样得到的。
KAQ: 两阶段计算 value 的区别
Rollout 阶段的 value 是基于当时的 Critic 参数(critic)计算的。优化阶段可能更新了 critic 参数(通过 value_loss),导致对相同状态的预测不同。优化阶段是循环的,即有多次执行 backward,计算 new_value 反映 Critic 优化后的预测能力,用于计算 value_loss,进一步优化 Critic。
比如 Rollout 阶段有个状态: [0.1, -0.2, 0.01, 0.05],value = 100.0。在优化阶段,同一状态,new_value = 118.0(因为 Critic 参数更新),b_returns = 120.0,value_loss 让 Critic 继续逼近 120.0。
KAQ: 关于两个阶段的 observation
rollout 阶段的 obs 是从环境中获得的原始观测数据,用于生成动作和计算价值。优化阶段的 obs 是从 rollout 阶段中采样得到的,所以优化阶段的 obs 一定存在与 rollout 的 obs 中,不会引入新的观测数据。
那么下一次 rollout 可能会生成之前未见过的新的 obs。这些新 obs 会在下一次优化阶段被采样,用于优化 Actor 和 Critic。
所以可以说,只要rollout 次数足够多,理论上就可以优化所有的obs。非也,CarPole 环境中的状态空间是连续的,不可能遍历到所有的 state(obs)。计算资源会限制 obs 的范围,同时我们希望Agent中的两个网络有好的泛华能力。
有与资源限制,在优化阶段,多次循环 (for epoch for batch),也是为了充分利用已经有了的 obs,让Actor 和 critic 尽可充分地学习。
KAQ: GAE
GAE 引入 $\lambda$ 平滑多步 TD 误差。
- $ \lambda = 1 $:等价于标准优势(returns - values),方差高但偏差低。
- $ \lambda = 0 $:仅用单步 TD 误差(delta),方差低但偏差高(因为忽略未来)。
- $ 0 < \lambda < 1 $(如 0.95):平衡偏差和方差,常用值。
单步TD误差: $$ \delta_t = r_t + \gamma V(s_{t+1}) \cdot (1 - \text{done}_{t+1}) - V(s_t) $$
递归计算 GAE 形式:
$$ A_t^{\text{GAE}(\gamma, \lambda)} = \delta_t + (\gamma \lambda) A_{t+1}^{\text{GAE}(\gamma, \lambda)} $$
返回累计回报: $$ R_t = A_t^{\text{GAE}} + V(s_t) $$
在代码中的实现:
for t in reversed(range(args.num_steps)):
if t == args.num_steps - 1:
nextnonterminal = 1.0 - next_done
nextvalues = next_value
else:
nextnonterminal = 1.0 - dones[t + 1]
nextvalues = values[t + 1]
# 计算 TD 误差
delta = rewards[t] + args.gamma * nextvalues * nextnonterminal - values[t]
# GAE
advantages[t] = lastgaelam = delta + args.gamma * args.gae_lambda * nextnonterminal * lastgaelam
# 累计回报
returns = advantages + values
KAQ: GAE 和一般的优势估计都区别和优势
一般价值估计计算,直接计算 实际折扣回报 与 critic 预测值之差: $$A_t = R_t - V(s_t)$$
其中实际折扣回报,是从当前开始直到回合结束的累计回报: $$R_t = r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + \dots + \gamma^{T-t} r_T$$
预测值 critic(obs),表示从 $s_t$ 开始的预期累积奖励。
所以,这是直接用折扣回报减去状态值。
性能指标 & tensorboard 上的指标 【todo】
在 Atori 游戏中的表象,使用平均游戏得分衡量。得分就是游戏中玩家在游戏结束时获得的分数。会在屏幕上显示的数值。
SPS是单位时间内完成的全局步数 Steps Per Second,理论上随 global_step 增加而增加。 Entropy loss 应该随着 global_step 的增加而减小,因为 agent 逐渐从探索转向利用。
======
将训练好的 Agent 用于环境中
1. 训练结束后保存模型
Agent 类继承 nn.Module.
agent = Agent(envs).to("cpu")
torch.save(agent.state_dict(), 'ppo_agent.pth')
2. 加载训练好的模型用于推理
torch.load('ppo_agent.pth')