强化学习PPO算法入门和实践
文章目录
1. 强化学习
- 在深度学习中,我们习惯于监督学习的范式:给模型一堆
(输入, 正确标签)对,模型通过最小化预测与标签之间的差距来学习。比如,给一张猫的图片,标签是“猫”。但强化学习完全不同。想象一下你在教一只小狗“坐下”:你不会直接告诉它“你的神经元权重应该这样调整”。你只会给它一个指令(比如“坐下”),然后观察它的动作。如果它做对了,你就给它一个零食(奖励);如果做错了,你可能什么也不给,或者说“不”(负奖励)。经过多次尝试,小狗会自己学会哪些动作(在什么状态下)能获得最多的零食。
- 智能体(Agent)与环境(Environment)的交互。
- 状态(State):环境在某时刻的描述。
- 动作(Action):智能体可以执行的行为。
- 奖励(Reward):环境对动作的反馈。
- 策略(Policy):决定在某状态下采取什么动作。
整个学习过程:
- AI观察当前画面(状态)
- 根据学到的策略选择动作(转向/加速等)
- 执行动作,状态发生变化
- 获得相应的奖励(分数)
- 根据这次经验调整策略
- 不断重复这个过程
- RL(Reinforcement Learning)的目标:训练一个智能体,让它学会一个策略,这个策略能在任何状态下,选择能最大化长期累积奖励的动作。
与深度学习的连接:在深度学习中,我们用神经网络来拟合一个从输入到标签的函数
f(x) -> y。在强化学习中,我们同样用神经网络,但目标是拟合一个策略函数π(a|s),它输入一个状态s,输出一个概率分布,表示在当前状态下执行每个动作a的概率。
2. 强化学习算法基础概念
2.1 马尔可夫决策过程(MDP)
- 马尔可夫决策过程的思想很简单:“未来只取决于现在,与过去无关”。就像下棋,下一步该怎么走,只取决于当前棋盘的局面(当前状态),而与你之前是怎么走到这个局面的(历史动作序列)无关。MDP就是对这个“无记忆”过程的数学描述。它确保了我们只需要关注当前状态 S t S_t St来做决策,大大简化了问题。
马尔可夫决策过程是一个数学框架,用于描述智能体与环境交互的过程。它包含5个基本要素 ( S , A , P , R , γ ) (S, A, P, R, γ) (S,A,P,R,γ):
- 状态空间(State Space) S S S:描述环境在某一时刻的所有可能状态
- 动作空间(Action Space) A A A:智能体在每个状态下可以采取的所有可能动作
- 状态转移概率(State Transition Probability) P ( s ′ ∣ s , a ) P(s'|s,a) P(s′∣s,a):在状态 s s s下执行动作 a a a后,转移到状态 s ′ s' s′的概率
- 奖励函数(Reward Function) R ( s , a , s ′ ) R(s,a,s') R(s,a,s′):执行动作a从状态s转移到s’时获得的即时奖励
- 折扣因子(Discount Factor) γ γ γ:用于权衡即时奖励和未来奖励的重要性
- 状态转移概率:描述环境的不确定性,帮助AI预测动作后果
- 折扣因子:平衡短期和长期收益,防止无限累积
- 回报:提供优化目标,AI的目标是最大化期望回报
- 轨迹:记录完整经验,用于策略学习和评估
5个要素共同定义一个完整的决策问题:
- 智能体观察当前状态
- 根据策略选择动作
- 环境根据转移概率产生新状态
- 智能体获得相应奖励
- 不断重复这个过程
- 目标是找到一个最优策略,使得期望的累积折扣奖励最大化。
2.2 折扣因子
- 我们希望最大化“长期累积奖励”,但这个“长期”是多久?而且,未来的奖励不如眼前的奖励“值钱”。今天的100块比明天的100块更有吸引力。
- 数学表达:当前时刻t的奖励 r t r_t rt 的未来价值 = r t r_t rt × γ t γ^t γt
- 折扣因子
γ(gamma) 就是用来衡量这个概念的。它是一个介于0和1之间的数。我们计算的不是所有未来奖励的简单相加,而是折扣累积奖励: G t = R t + R t + 1 ∗ γ + R t + 2 ∗ γ 2 + . . . G_t = R_t + R_{t+1} * γ +R_{t+2} *γ^2 + ... Gt=Rt+Rt+1∗γ+Rt+2∗γ2+...- 如果
γ接近0,智能体会非常“短视”,只关心眼前的奖励。 - 如果
γ接近1,智能体会非常有“远见”,愿意为了长远的巨大回报而牺牲眼前的利益。
- 如果
- 通常γ设置得很接近1(如0.99),表示远期收益仍然很重要。
- 考虑设计折扣因子主要有两方面的原因:首先是现实原因,在真实环境下,越远的未来越不确定,近期的决策比远期的决策更重要。其次是技术原因,如果不打折扣,在无限时间中,总回报可能变成无穷大,导致无法比较不同策略的好坏。
2.3 价值、动作、优势函数
- 状态价值函数 V ( s ) V(s) V(s):从状态 s s s开始,按照当前策略 π π π行动,能获得的期望累积折扣奖励。数学表达式为 V ( s ) = E [ r t + γ r t + 1 + γ 2 r t + 2 + ⋯ ] V(s) = E[r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + \cdots] V(s)=E[rt+γrt+1+γ2rt+2+⋯]
- 动作价值函数 Q ( s , a ) Q(s,a) Q(s,a):在状态s下执行动作a,之后按策略π行动能获得的期望累积折扣奖励。数学表达式为 Q ( s , a ) = R ( s , a ) + γ E [ V ( s ′ ) ] Q(s,a) = R(s,a) + γE[V(s')] Q(s,a)=R(s,a)+γE[V(s′)]。
- R(s,a) = 10分
下一状态可能性:
- 60%:最佳位置 V(s1’) = 30分
- 30%:一般位置 V(s2’) = 10分
- 10%:危险位置 V(s3’) = -20分
Q(s,a) = 10 + 0.9×(0.6×30 + 0.3×10 + 0.1×(-20)) = 20.9分
- 优势函数 A ( s , a ) A(s,a) A(s,a):评估在状态s下,执行动作a比平均表现好差异。数学表达式为 A ( s , a ) = Q ( s , a ) − V ( s ) A(s,a) = Q(s,a) - V(s) A(s,a)=Q(s,a)−V(s)
- 在PPO算法中,使用神经网络估计 V ( s ) V(s) V(s),用优势函数 A ( s , a ) A(s,a) A(s,a)来指导策略更新,优势值为正的动作会被强化,优势值为负的动作会被弱化。
三个函数的关系:
- V ( s ) V(s) V(s)评估状态的总体价值
- Q ( s , a ) Q(s,a) Q(s,a)评估状态-动作对的价值
- A ( s , a ) A(s,a) A(s,a)评估具体动作相对于平均表现的优劣
- 奖励函数与马尔科夫决策过程的关系:MDP基本要素描述环境,如定义问题的框架、描述环境的动态特性,定义即时奖励;价值函数基于MDP计算而来,用于评估长期收益、帮助做出决策、指导策略学习。
2.4 策略梯度
-
假如有一个策略网络
π_θ(θ是网络参数),我们想调整θ来让智能体表现更好。但问题来了:我们没有“正确动作”的标签,怎么计算梯度呢? -
策略参数化:使用神经网络来表示策略 π ( a ∣ s ; θ ) π(a|s;θ) π(a∣s;θ), θ θ θ是网络参数。在状态s下选择动作a的概率。
-
目标函数:在当前策略参数θ下,期望能获得的长期回报, J ( θ ) = E [ R 1 + γ R 2 + γ 2 R 3 + . . . ] J(θ) = E[R₁ + γR₂ + γ²R₃ + ...] J(θ)=E[R1+γR2+γ2R3+...]。
-
策略梯度定理:告诉如何调整参数θ来提高策略性能 ∇ θ J ( θ ) = E [ ∇ θ log π ( a ∣ s ; θ ) × Q ( s , a ) ] \nabla_{\theta} J(\theta) = \mathbb{E}\left[ \nabla_{\theta} \log \pi(a|s;\theta) \times Q(s,a) \right] ∇θJ(θ)=E[∇θlogπ(a∣s;θ)×Q(s,a)]。
-
策略梯度的思想非常巧妙:如果一个动作带来了比预期更好的结果,我们就应该增加它在未来被选择的概率;反之,则降低其概率。 其核心梯度公式可以简化理解为: ∇ θ J ( θ ) = E [ ∇ θ log π ( a ∣ s ; θ ) × Q ( s , a ) ] \nabla_{\theta} J(\theta) = \mathbb{E}\left[ \nabla_{\theta} \log \pi(a|s;\theta) \times Q(s,a) \right] ∇θJ(θ)=E[∇θlogπ(a∣s;θ)×Q(s,a)]
训练循环:
- 使用当前策略 π ( a ∣ s ; θ ) π(a|s;θ) π(a∣s;θ)进行行动
- 收集状态-动作-奖励序列
- 计算每个状态-动作对的 Q Q Q值
- 使用策略梯度定理计算梯度
- 更新策略参数 θ θ θ
- 重复这个过程
3. PPO的核心
- 传统的策略梯度方法有一个大问题:数据利用率低。每次更新策略后,旧策略产生的数据就不能再用了,必须用新策略重新收集数据。这太浪费了!
- PPO就是为了解决这个问题而生的,它的全称是Proximal Policy Optimization(近端策略优化),核心思想是:用旧策略的数据来更新新策略,但要小心,别让新策略“走得太远”。
3.1 Actor-Critic 架构
PPO通常采用Actor-Critic架构:
- Actor(演员):策略网络 π θ π_θ πθ。它负责观察当前状态,输出动作概率,根据概率选择动作。
- Critic(评论家):价值网络 V φ V_φ Vφ。它负责观看Actor的表演,并给出评价,即计算状态价值 V ( s ) V(s) V(s) 或优势
A(s, a)。
- Actor根据Critic的评价来调整自己的“演技”(更新策略参数),而Critic也在不断学习如何更准确地评价(更新价值参数)。两者相互促进,共同进步。
在PPO中的应用:
- 数据收集:Actor与环境交互、收集(状态,动作,奖励)。
- 优势计算:Critic评估状态价值、计算实际回报与预测的差。
- 策略更新:使用PPO的裁剪目标函数,同时更新Actor和Critic。
3.2 重要性采样
- 想用旧策略 π θ o l d π_{θ_{old}} πθold 收集的数据 ( s , a , r ) (s,a,r) (s,a,r) 来更新新策略 π θ π_θ πθ。
- 直接用旧数据计算新策略的梯度会有偏差,因为数据分布不同。重要性采样提供了一个修正系数: r t ( θ ) = π θ ( a ∣ s ) π θ old ( a ∣ s ) r_t(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} rt(θ)=πθold(a∣s)πθ(a∣s)
- 这个权重衡量新旧策略在同一个状态
s下,选择同一个动作a的概率变化。通过这个权重,我们就可以“校正”旧数据,使其适用于新策略的更新。 - PPO中的训练策略是批量数据训练和多次更新。
- 批量数据训练:
收集一批数据 → 用这批数据更新多次 → 再收集新数据 → 再更新多次 → ...。 - 多次更新:不是选择某一次更新的结果,而是通过多次更新得到一个更好的策略,最终使用最后更新的策略继续收集新数据。
初始策略:30%左转 ↓ 第一次更新 40%左转 ↓ 第二次更新 45%左转 ↓ 第三次更新 48%左转 - 多次更新的意义在于逐步优化(每次做小幅度的改变,逐步完整改进,避免一次性大幅度改变策略),数据充分利用(多次利用同一批数据,充分学习数据中的内容),稳定性(防止策略突变、保持学习的连续性)。
3.3 裁剪功能
- 重要性采样虽然强大,但有一个致命风险:如果新旧策略差异太大,这个权重会变得极不稳定,可能导致训练崩溃。
- PPO的精髓就在于裁剪。它给重要性权重加了一个“限制器”,确保新策略不会偏离旧策略太远。设置裁剪范围: [ 1 − ε , 1 + ε ] [1-ε, 1+ε] [1−ε,1+ε],通常 ε = 0.2 ε = 0.2 ε=0.2,所以范围是 [ 0.8 , 1.2 ] [0.8, 1.2] [0.8,1.2]。例如,旧概率30%,允许的新概率范围。上限:30% × 1.2 = 36%;下限:30% × 0.8 = 24%。
3.4 PPO核心公式
- PPO的目标函数(简化版)如下:
L CLIP ( θ ) = E t [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] L^{\text{CLIP}}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta) A_t, \ \text{clip}\left(r_t(\theta), 1-\epsilon, 1+\epsilon\right) A_t \right) \right] LCLIP(θ)=Et[min(rt(θ)At, clip(rt(θ),1−ϵ,1+ϵ)At)] - E t \mathbb{E}_t Et 表示对所有时间步 t t t 的数据取期望。在实践中,这通常意味着对一批从环境中采样的数据(一个 batch)计算平均值。
- 重要性采样比率 r t ( θ ) r_t(\theta) rt(θ):它衡量了新策略 π θ \pi_{\theta} πθ 与旧策略 π θ old \pi_{\theta_{\text{old}}} πθold 在同一个状态 s t s_t st 下,选择同一个动作 a t a_t at 的概率变化比率。如果 r t ( θ ) > 1 r_t(\theta) > 1 rt(θ)>1,说明新策略更倾向于选择这个动作;如果 r t ( θ ) < 1 r_t(\theta) < 1 rt(θ)<1,说明新策略更不倾向于选择这个动作;如果 r t ( θ ) = 1 r_t(\theta) = 1 rt(θ)=1,说明新旧策略在这个选择上没有变化。
- 优势函数 A ^ t \hat{A}_t A^t: A ^ t ( s t , a t ) = Q ( S t , a t ) − V ( s t ) \hat A_t(s_t, a_t)=Q(S_t,a_t)-V(s_t) A^t(st,at)=Q(St,at)−V(st)衡量的是在状态 s t s_t st 下,选择动作 a t a_t at 到底有多“好”。它比传统的回报 Q t Q_t Qt 更精确。 A t A_t At 告诉我们,这个动作 a t a_t at 是比平均水平好还是坏。 A t > 0 A_t > 0 At>0:这个动作比当前策略下的平均动作要好; A t < 0 A_t < 0 At<0:这个动作比平均动作要差; A t ≈ 0 A_t \approx 0 At≈0:这个动作不好不坏。
- 未被裁剪的目标 r t ( θ ) A t r_t(\theta) A_t rt(θ)At:这部分是经典的策略梯度目标,通过重要性采样进行修正。如果 A t > 0 A_t > 0 At>0 (好动作),我们希望增加选择它的概率。此时目标函数希望 r t ( θ ) r_t(\theta) rt(θ) 越大越好,从而让梯度推动 θ \theta θ 朝这个方向更新;如果 A t < 0 A_t < 0 At<0 (坏动作),我们希望减少选择它的概率。此时目标函数希望 r t ( θ ) r_t(\theta) rt(θ) 越小越好,从而让梯度推动 θ \theta θ 朝相反方向更新。
- 裁剪函数 clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) clip(rt(θ),1−ϵ,1+ϵ):限制重要性采样比率 r t ( θ ) r_t(\theta) rt(θ) 的范围,强制它落在 [ 1 − ϵ , 1 + ϵ ] [1-\epsilon, 1+\epsilon] [1−ϵ,1+ϵ] 区间内。如果 r t ( θ ) > 1 + ϵ r_t(\theta) > 1+\epsilon rt(θ)>1+ϵ,它会被“裁剪”到 1 + ϵ 1+\epsilon 1+ϵ;如果 r t ( θ ) < 1 − ϵ r_t(\theta) < 1-\epsilon rt(θ)<1−ϵ,它会被“裁剪”到 1 − ϵ 1-\epsilon 1−ϵ;否则,保持原样。
- 取最小值 min ( ⋅ , ⋅ ) \min(\cdot, \cdot) min(⋅,⋅):
- 情况一: A t > 0 A_t > 0 At>0 (好动作),未裁剪项是 r t ( θ ) A t r_t(\theta) A_t rt(θ)At。裁剪项是 clip ( r t ( θ ) , . . . ) A t \text{clip}(r_t(\theta), ...) A_t clip(rt(θ),...)At。当 r t ( θ ) r_t(\theta) rt(θ) 在 [ 1 , 1 + ϵ ] [1, 1+\epsilon] [1,1+ϵ] 之间时,裁剪项等于未裁剪项,
min函数取它们,目标函数正常增长;当 r t ( θ ) > 1 + ϵ r_t(\theta) > 1+\epsilon rt(θ)>1+ϵ 时,裁剪项被固定为 ( 1 + ϵ ) A t (1+\epsilon)A_t (1+ϵ)At,而未裁剪项 r t ( θ ) A t r_t(\theta)A_t rt(θ)At 会继续变大。此时min函数会取那个更小的、被裁剪过的值 ( 1 + ϵ ) A t (1+\epsilon)A_t (1+ϵ)At。 效果:一旦新策略对好动作的偏好增加超过 ϵ \epsilon ϵ,目标函数就不再增长了!阻止策略过度优化好动作。 - 情况二: A t < 0 A_t < 0 At<0 (坏动作):未裁剪项是 r t ( θ ) A t r_t(\theta) A_t rt(θ)At(一个负数)。裁剪项是 clip ( r t ( θ ) , . . . ) A t \text{clip}(r_t(\theta), ...) A_t clip(rt(θ),...)At(也是一个负数)。当 r t ( θ ) r_t(\theta) rt(θ) 在 [ 1 − ϵ , 1 ] [1-\epsilon, 1] [1−ϵ,1] 之间时,裁剪项等于未裁剪项,目标函数正常减小(我们希望减小,所以目标是最大化这个负值,会推动 r t ( θ ) r_t(\theta) rt(θ) 变小)。当 r t ( θ ) < 1 − ϵ r_t(\theta) < 1-\epsilon rt(θ)<1−ϵ 时,裁剪项被固定为 ( 1 − ϵ ) A t (1-\epsilon)A_t (1−ϵ)At,而未裁剪项 r t ( θ ) A t r_t(\theta)A_t rt(θ)At 会变得更负(更小)。此时
min函数会取那个更小的、未被裁剪的值 r t ( θ ) A t r_t(\theta)A_t rt(θ)At。 效果:一旦新策略对坏动作的偏好减少超过 ϵ \epsilon ϵ,目标函数就会变得更小,从而产生一个惩罚性的梯度,阻止策略过度“躲避”坏动作。
- 情况一: A t > 0 A_t > 0 At>0 (好动作),未裁剪项是 r t ( θ ) A t r_t(\theta) A_t rt(θ)At。裁剪项是 clip ( r t ( θ ) , . . . ) A t \text{clip}(r_t(\theta), ...) A_t clip(rt(θ),...)At。当 r t ( θ ) r_t(\theta) rt(θ) 在 [ 1 , 1 + ϵ ] [1, 1+\epsilon] [1,1+ϵ] 之间时,裁剪项等于未裁剪项,
这个公式看起来复杂,但思想很直观:
- 当优势 A t > 0 A_t > 0 At>0(好动作):希望增加这个动作的概率,即希望 r t ( θ ) r_t(θ) rt(θ) 变大。但
min函数限制了它。如果 r t ( θ ) r_t(θ) rt(θ) 超过了 1 + ε 1+ε 1+ε,clip函数会把它“裁剪”到1+ε。此时,min会选择被裁剪后的值,这意味着目标函数不会再增加了,梯度也消失了。
- 效果:对于好动作,我们允许其概率增加,但最多只能增加到旧策略的
1+ε倍,防止步子迈得太大。
- 当优势 A t < 0 A_t < 0 At<0(坏动作):希望降低这个动作的概率,即希望 r t ( θ ) r_t(θ) rt(θ) 变小。
min函数同样会限制它。如果 r t ( θ ) r_t(θ) rt(θ) 小于 1 − ε 1-ε 1−ε,clip会把它裁剪到 1 − ε 1-ε 1−ε。此时,min会选择未被裁剪的、更小的 r t ( θ ) ∗ A t r_t(θ) * A_t rt(θ)∗At(因为 A t A_t At是负数,乘以更小的 r t r_t rt会得到更大的值,即更小的损失)。
- 效果:对于坏动作,我们允许其概率降低,但最多只能降低到旧策略的
1-ε倍。
3.4 KL散度
- KL散度是衡量两个概率分布“差异”的指标。在PPO的一些变体中,它被用作另一种“安全带”。比如,可以设定一个目标,让新旧策略的KL散度不能超过某个阈值。如果超过了,就停止更新或施加惩罚。裁剪目标函数实际上是一种隐式地约束KL散度的方法。
- 剪裁和KL散度监控的不同点:剪裁方法只关注单个动作,没有关注到整体策略的变化。KL散度会提供整体策略的度量,计算两个分布的差异,差异小说明策略变化温和,差异大说明策略变化剧烈,防止多个小变化积累成不稳定的大变化。
5. PPO代码实现思路
- 纸上得来终觉浅,绝知此事要躬行。在理解了PPO的理论之后,让我们通过一个经典的案例——CartPole(倒立摆)——来亲手实现PPO算法。
5.1 案例目标
目标是训练一个智能体,让它学会控制一个小车上的杆子,使其保持竖直平衡。
- 环境:
CartPole-v1,一个由OpenAI Gymnasium提供的标准测试环境。 - 状态:智能体能观察到4个维度的信息:小车的位置、小车的速度、杆子的角度、杆子的角速度。
- 动作:智能体可以执行2个离散动作: 向左施力、向右施力。
- 奖励:每成功保持杆子平衡一步,智能体获得+1的奖励。如果杆子倒下或小车移出边界,则回合结束。
- 成功标准:杆子在连续500步内不倒下,即认为任务解决。
5.2 案例内容
使用PyTorch构建两个核心神经网络:
- Actor(策略网络):输入当前状态(4个维度),输出执行每个动作的概率(2个概率值之和为1)。它负责做决策。
- Critic(价值网络):输入当前状态(4个维度),输出一个标量,表示对该状态未来能获得总奖励的估计。它负责评估Actor决策的好坏。
–
- 训练过程将遵循我们之前讨论的PPO流程:与环境交互收集数据 -> 计算优势函数 -> 使用裁剪的目标函数更新Actor和Critic网络。
5.3 代码实现思路
代码分为以下几个部分:
- 网络定义:创建
Actor和Critic两个PyTorch模型类。 - PPO算法类:创建一个
PPO类,封装所有核心逻辑,包括:- 初始化网络和优化器。
update方法:接收一个回合的数据,执行PPO的核心更新逻辑(计算裁剪损失、价值损失、熵奖励,并更新网络)。train方法:主训练循环,负责与环境交互、收集数据,并定期调用update方法。
- 主程序:实例化环境和PPO智能体,并启动训练。
import gymnasium as gym
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import clear_output
# 策略网络:决定在给定状态下采用的动作
class Actor(nn.Module):
def __init__(self, state_dim, action_dim):
super(Actor, self).__init__()
# 构建三层神经网络
self.network = nn.Sequential(
nn.Linear(state_dim, 64),
nn.Tanh(),
nn.Linear(64, 64),
nn.Tanh(),
nn.Linear(64, action_dim),
nn.Softmax(dim=-1)
)
# 前向传播
def forward(self,state):
return self.network(state)
# 价值网络:用于评估状态的价值
class Critic(nn.Module):
def __init__(self, state_dim):
super(Critic, self).__init__()
self.network = nn.Sequential(
nn.Linear(state_dim, 64),
nn.Tanh(),
nn.Linear(64, 64),
nn.Tanh(),
nn.Linear(64, 1)
)
# 前向传播,输出状态价值
def forward(self, state):
return self.network(state)
# PPO算法实现
class PPO:
def __init__(self, state_dim, action_dim, lr=0.0003, gamma=0.99, epsilon=0.2):
'''
PPO算法实现
:param state_dim: 状态维度 小车维度(-4.8~4.8m) 速度(负无穷,正无穷) 角度(-45~45) 角速度(负无穷,正无穷)
:param action_dim: 动作维度 向左推、向右推
:param lr: learning_rate 学习率 控制每次更新网络参数的步长 太大不稳,太小学习缓慢
:param gamma: 折扣因子 表示对未来奖励的关注 0~1 接近1表示重视长期奖励 接近0表示重视即时奖励
:param epsilon: PPO裁剪参数 限制策略的更新幅度 防止一次更新改变太大
'''
# 初始化策略网络和价值网络
self.actor = Actor(state_dim, action_dim)
self.critic = Critic(state_dim)
# 创建优化器
self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=lr)
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=lr)
# 设置超参数 折扣因子和裁剪参数
self.gamma = gamma
self.epsilon = epsilon
# 存储每个episode的状态、动作、奖励等信息
self.states = [] # 存储所有经历的状态
self.actions = [] # 存储所有执行的动作
self.old_probs = [] # 存储所有动作的原始概率
self.rewards = [] # 存储所有经历的奖励
self.dones=[] # 存储所有经历的结束状态
# 训练历史记录
self.rewards_history = [] # 记录每个训练episode的奖励
self.policy_losses = [] # 记录策略网络的损失
self.value_losses = [] # 记录价值网络的损失
self.ratios =[] # 记录策略更新比率
def update(self,states,actions,old_probs,rewards,dones):
# 计算优势函数 不计算梯度
with torch.no_grad():
# 使用价值网络计算当前状态的价值
values = self.critic(states)
'''
self.critic(states[1:]) - 使用critic网络评估除第一个状态外的所有状态价值
torch.tensor([[0]]) - 为序列末尾添加一个零值(表示终止状态价值)
torch.cat() - 将上述两部分拼接成完整的next_values张量
'''
next_values = torch.cat([self.critic(states[1:]), torch.tensor([[0]])])
# 计算优势函数:A(s,a) = Q(s,a) - V(s) = r + γV(s') - V(s)
# rewards.unsqueeze(1): 将奖励张量增加一个维度,便于后续计算
# self.gamma * next_values * (1-dones.unsqueeze(1)):
# - 如果dones为1(终止状态),则后续价值为0
# - 如果dones为0(非终止状态),则保留γ*V(s')
# (1-dones.unsqueeze(1)):构建掩码,处理终止状态
# - values: 减去当前状态的价值,得到优势函数
advantages = rewards.unsqueeze(1) + self.gamma * next_values * (1 - dones.unsqueeze(1)) - values
# 标准化优势函数
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
advantages = advantages.squeeze(1)
# 创建新计算图,避免梯度计算错误
states = states.clone().detach()
actions = actions.clone().detach()
old_probs = old_probs.clone().detach()
advantages = advantages.clone().detach()
rewards = rewards.clone().detach()
# 记录更新过程中的各种指标
update_ratios = []
update_policy_losses = []
update_value_losses = []
# 多次更新策略和价值网络
for _ in range(10):
# 1 更新策略网络
# 调用 Actor 网络对输入的 states 进行前向传播, 输出动作概率分布 [p1, p2, ..., pn]
probs = self.actor(states)
# 创建分类分布(Categorical Distribution)对象 分布对象可以用于采样动作和计算概率
dist = torch.distributions.Categorical(probs)
# 计算在当前策略下执行实际动作 actions 的对数概率,返回每个状态下对应动作的对数似然值
new_probs = dist.log_prob(actions)
ratio = torch.exp(new_probs - old_probs) # 新策略概率/旧策略概率
# 记录平均比率
update_ratios.append(ratio.mean().item())
# 2 剪裁目标函数
surr1 = ratio * advantages # 计算未裁剪的目标 策略比率 × 优势值
# 计算裁剪后的目标 取两者中的较小值作为损失
surr2 = torch.clamp(ratio, 1 - self.epsilon, 1 + self.epsilon) * advantages
actions_loss = -torch.min(surr1, surr2).mean()
# 3 更新策略网络
self.actor_optimizer.zero_grad()
actions_loss.backward()
self.actor_optimizer.step()
# 记录损失值
update_policy_losses.append(actions_loss.mean().item())
# 4 更新价值网络(Critic)
value_pred =self.critic(states) # 预测状态价值
value_loss = nn.MSELoss()(value_pred, rewards.unsqueeze(1)) # 计算均方误差损失
# 更新网络参数 清除梯度 计算梯度 更新梯度
self.critic_optimizer.zero_grad()
value_loss.backward()
self.critic_optimizer.step()
# 记录损失值
update_value_losses.append(value_loss.mean().item())
# 记录平均策略比率
self.ratios.append(np.mean(update_ratios))
# 记录平均策略损失
self.policy_losses.append(np.mean(update_policy_losses))
# 记录平均价值损失
self.value_losses.append(np.mean(update_value_losses))
# 绘制训练过程中的各种指标
def plot_training_data(self, episode):
clear_output(wait=True)
fig, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(20, 5))
# 绘制奖励历史
ax1.plot(self.rewards_history)
ax1.set_title('Episode Rewards')
ax1.set_xlabel('Episode')
ax1.set_ylabel('Reward')
# 绘制策略损失
ax2.plot(self.policy_losses)
ax2.set_title('Policy Loss')
ax2.set_xlabel('Update')
ax2.set_ylabel('Loss')
# 绘制价值损失
ax3.plot(self.value_losses)
ax3.set_title('Value Loss')
ax3.set_xlabel('Update')
ax3.set_ylabel('Loss')
# 绘制策略比率
ax4.plot(self.ratios)
ax4.axhline(y=1 + self.epsilon, color='r', linestyle='--')
ax4.axhline(y=1 - self.epsilon, color='r', linestyle='--')
ax4.set_title('Policy Ratio')
ax4.set_xlabel('Update')
ax4.set_ylabel('Ratio')
plt.tight_layout()
plt.show()
def train(self,env,episodes=20):
for episode in range(episodes):
# 1 初始化环境
state, _ = env.reset() # 重置环境
done = False # 游戏是否结束
truncated = False # 是否达到最大步数
episode_reward = 0 # 当前回合的总奖励
while not (done or truncated):
state_tensor = torch.tensor(state, dtype=torch.float32)
action = self.actor(state_tensor)
dist = torch.distributions.Categorical(action)
action = dist.sample()
'''
next_state: 执行动作后的新状态 [小车位置, 小车速度, 杆子角度, 杆子角速度]
reward: 一步的奖励 保持平衡 一步得1分
terminated: 游戏是否因失败而结束 杆子倒了或小车出界
truncated: 是否因达到最大步数而结束 最大500步
'''
next_state, reward, terminated, truncated, _ = env.step(action.item())
done = terminated
# 存储的数据
self.states.append(state_tensor) # 存储状态
self.actions.append(action) # 存储动作
self.old_probs.append(dist.log_prob(action)) # 存储动作概率
self.rewards.append(reward) # 存储奖励
self.dones.append(float(done)) # 是否结束
# 更新状态和奖励
state = next_state # 更新状态
episode_reward += reward # 累加奖励
# 处理收集到的数据 将列表转换为张量
states = torch.stack(self.states)
actions = torch.stack(self.actions)
old_probs = torch.stack(self.old_probs)
rewards = torch.tensor(self.rewards, dtype=torch.float32)
dones = torch.tensor(self.dones, dtype=torch.float32)
# 更新策略
self.update(states, actions, old_probs, rewards, dones) # 更新网络
self.rewards_history.append(episode_reward) # 记录回合的总奖励
# 每10个回合显示一次训练进度
if (episode + 1) % 100 == 0: # 每训练10个回合
self.plot_training_data(episode) # 绘制训练图表
# 打印最近10个回合的平均奖励
print(f"Episode {episode + 1}, Average Reward: {np.mean(self.rewards_history[-10:]):.2f}")
if __name__ == "__main__":
# 创建CartPole-v1环境,render_mode="human"表示可视化渲染
env = gym.make('CartPole-v1', render_mode="human")
# 获取环境的状态空间维度(对于CartPole是4维:小车位置、小车速度、杆子角度、杆子角速度)
state_dim = env.observation_space.shape[0]
# 获取环境的动作空间大小(对于CartPole是2个动作:向左推/向右推)
action_dim = env.action_space.n
# 创建PPO算法实例,传入状态维度和动作维度
ppo = PPO(state_dim, action_dim)
# 开始训练,设置训练回合数为200
ppo.train(env, episodes=200)
# 训练完成后关闭环境
env.close()


更多推荐

所有评论(0)