1.重要性采样
假设我们想要计算一个复杂分布 P下某个函数f(x)的期望值。这个分布 P 很复杂,直接从中采样非常困难。为了解决这个问题,可以使用重要性采样。
-
选择一个提议分布Q: 我们选择一个更简单的分布 Q,从这个分布中采样相对容易。例如,如果 P 是一个复杂的多峰分布,我们可以选择一个简单的高斯分布作为 Q。
-
从 Q 中采样: 假设我们从 Q 中随机抽取了一系列样本 x1,x2,...,xn。
-
计算重要性权重: 对于每个样本 xi,我们计算重要性权重 wi=Q(xi)/P(xi)。这个权重衡量了在两个分布之间转换样本的比例。
-
估算 f(x) 的期望值: 最后,我们使用加权的样本来估计 f(x) 的期望值,公式为,如果是连续分布,期望可以用积分表示
这里wi应尽可能等于1,目的是减少方差。
2.TRPO,PPO
2.1TRPO
回顾一下A2C中Actor目标函数:
这种策略梯度使用采样的方式来估计梯度,即通过在环境中执行动作并观察奖励来收集数据。由于环境的随机性和不确定性,每次采样得到的奖励可能会有很大的差异,导致梯度估计的方差较高,这是策略梯度采样估计的通病。
TRPO是ppo的前身,它所做的就是使用上面提到的重要性采样,把Actor—Critic框架中的策略梯度写成了这样的形式:
上面是优化目标,下面是约束,KL代表KL散度,来确保前后策略尽可能接近。这样的方法确保每次的策略梯度估计都不会过分偏离当前的策略,也就是减少了策略梯度估计的方差,从而提高算法的稳定性和收敛性。
2.2PPO
TRPO是一个复杂的有约束问题,解决是很困难的,PPO用一个惩罚项和惩罚系数β把约束去掉,把优化目标变为了无约束问题:
除了KL散度,还有另外一种约束写法,clip约束
其中这个 clip 约束的意思就是始终将重要性权重 r(θ) 裁剪在 1 的邻域范围内,保证新旧策略相似。
算法伪代码可以写成这样:
初始化策略网络参数θ_actor和价值网络参数θ_critic
初始化经验回放池memory
for 每一训练回合 from 1 to 训练回合总数 do
初始化累计奖励为0
获取初始状态s
for 每一步 from 1 to 最大步数 do
通过策略网络与当前状态s,选择动作a
执行动作a,观察奖励r和新状态s'
将(s, a, log_prob(a), r, done)存储到经验回放池memory中
s ← s'
累计奖励 += r
if 步数 % 更新频率 == 0 then
对经验回放池中的每个样本进行蒙特卡洛估计,计算回报
对每个样本计算优势估计A
for k次迭代 do
计算策略比率ratio = πθ_new(a|s) / πθ_old(a|s)
计算裁剪后的目标函数L_CPI(θ)
计算价值损失L_VF(θ)
计算总损失L_total = L_CPI(θ) - c1 * L_VF(θ) + c2 * S[πθ](s)
更新策略网络参数θ_actor和价值网络参数θ_critic以最小化L_total
end for
清空经验回放池memory
end if
if done then
记录累计奖励
break
end if
end for
每n回合评估一次策略性能,如果性能提升,则保存当前模型
end for
以下是agent更新部分的代码(使用的是上面的clip约束):
def update(self):
# 每n步更新一次策略
if self.sample_count % self.update_freq != 0:
return
# 从旧策略采样
old_states, old_actions, old_log_probs, old_rewards, old_dones = self.memory.sample()
# 转换成tensor
old_states = torch.tensor(np.array(old_states), device=self.device, dtype=torch.float32)
old_actions = torch.tensor(np.array(old_actions), device=self.device, dtype=torch.float32)
old_log_probs = torch.tensor(old_log_probs, device=self.device, dtype=torch.float32)
# 计算蒙特卡洛回报(从后往前计算每个epoch的当前s的Q值)
returns = []
discounted_sum = 0
for reward, done in zip(reversed(old_rewards), reversed(old_dones)):
if done:
discounted_sum = 0
discounted_sum = reward + (self.gamma * discounted_sum)
returns.insert(0, discounted_sum)
# 把回报标准化为0均值
returns = torch.tensor(returns, device=self.device, dtype=torch.float32)
returns = (returns - returns.mean()) / (returns.std() + 1e-5) # 1e-5 为了避免除0
for _ in range(self.k_epochs):
# 计算V值和优势函数A
values = self.critic(old_states)
advantage = returns - values.detach()#detach来防止此处梯度传播
# 计算当前策略下取得每个动作的概率
probs = self.actor(old_states)
dist = Categorical(probs)
#Categorical函数创建一个以参数probs为概率分布的类别分布(Categorical Distribution)对象。dist.sample()可以根据概率选取动作,dist.log_prob(action)可以计算对数概率
new_probs = dist.log_prob(old_actions)# 在当前策略下,采取历史动作的对数概率
# 计算r值即当前策略下采取历史动作的概率与旧策略下采取同一动作的概率之比
ratio = torch.exp(new_probs - old_log_probs)
# 计算actor loss
surr1 = ratio * advantage
surr2 = torch.clamp(ratio, 1 - self.eps_clip, 1 + self.eps_clip) * advantage
actor_loss = -torch.min(surr1, surr2).mean() + self.entropy_coef * dist.entropy().mean()##后面那一项是熵,用来促进探索
# 计算critic loss
critic_loss = (returns - values).pow(2).mean()
self.actor_optimizer.zero_grad()
self.critic_optimizer.zero_grad()
actor_loss.backward()
critic_loss.backward()
self.actor_optimizer.step()
self.critic_optimizer.step()
#每次更新都要清空memory
self.memory.clear()
可见ppo算法使用了缓冲区,和DQN不同的是,
1.保存了旧策略取各个动作的概率old_log_probs
2.每次update后都清空缓冲区,保证取出的都是前一次策略的经验。
3.sample直接取出其中全部的经验。
debug时各个变量的取值:
跑的是cart pole,离散环境,4个状态维度,2个动作维度。这里更新频率update_freq取的100,所以每当缓冲池大小为100时就更新一次策略,取出来都是第1维为100的张量。
每次都因为新旧策略相似,同时缓冲区经验只取自于上一个策略,可以近似为当前策略形成的动作,因此是on—policy算法。
优点:1.适用范围广:同时适用于连续和离散动作空间,适用性比TD3,DDPG更广。2.更易调参:超参数比TD3,DDPG更少,更容易调整。3.样本效率更高:通过优化一个目标函数来避免策略更新过程中的过大变动,这通常能够带来更稳定的学习过程和更好的样本效率。
缺点:1.性能瓶颈:在某些复杂的环境中,PPO的性能可能达不到像TD3这样使用基于值函数的算法的水平,特别是在处理具有复杂动作空间的任务时。2.超参数敏感:尽管超参数少,但依然对超参数非常敏感,比如裁剪参数。