本笔记有大量参考蘑菇书EasyRL https://datawhalechina.github.io/easy-rl/#/ 包括其配图和部分文本。
1. 基本概念
1.1 基本流程
强化学习是一种学习框架,其中智能体(Agent) 通过与 环境(Environment) 的交互,在每一步从环境中接收状态(State)和奖励(Reward),并通过选择行动(Action)来学习最优策略(Policy),以最大化其累计奖励。
换句话说,强化学习是让智能体找到一种行为策略,使得它在长期内获得的奖励总和(通常是期望值)最大化。

图中的每个元素代表以下含义:
- Agent(智能体):这是我们的学习者,它会根据当前的状态(State)做出一个动作(Action)。
- Environment(环境):这是智能体所处的外部世界,它会根据智能体的动作,给出相应的反馈:
- Reward(奖励):智能体执行动作后,环境会给出一个数值表示的奖励。正奖励表示动作的好坏,负奖励表示动作的坏处。
- Next State(下一个状态):执行动作后,环境的状态会发生变化,进入下一个状态。
- State(状态):环境在某个时刻的具体情况,比如游戏中的分数、机器人的位置等。
- Action(动作):智能体可以采取的各种行为,比如向左走、开火等。
1.2 马尔可夫过程
**马尔科夫性质:**一个随机过程的下一个状态只取决于当前状态,而与过去的状态无关。即:
p
(
X
t
+
1
=
x
t
+
1
∣
X
0
:
t
=
x
0
:
t
)
=
p
(
X
t
+
1
=
x
t
+
1
∣
X
t
=
x
t
)
p(X_{t + 1}=x_{t + 1}|X_{0:t}=x_{0:t}) = p(X_{t + 1}=x_{t + 1}|X_{t}=x_{t})
p(Xt+1=xt+1∣X0:t=x0:t)=p(Xt+1=xt+1∣Xt=xt)
马尔可夫性质也可以描述为给定当前状态时,将来的状态与过去状态是条件独立的。如果某一个过程满足马尔可夫性质,那么未来的转移与过去的是独立的,它只取决于现在。马尔可夫性质是所有马尔可夫过程的基础。
也就是说,满足以下关系,其中
h
t
=
{
s
1
,
s
2
,
s
3
,
…
,
s
t
}
h_t = \{ s_1, s_2, s_3, \ldots, s_t \}
ht={s1,s2,s3,…,st},
h
t
h_t
ht包含了之前所有状态
p
(
s
t
+
1
∣
s
t
)
=
p
(
s
t
+
1
∣
h
t
)
p(s_{t + 1} | s_{t}) = p(s_{t + 1} | h_{t})
p(st+1∣st)=p(st+1∣ht)
从当前
s
t
s_t
st转移到
s
t
+
1
s_{t+1}
st+1,它是直接就等于它之前所有的状态转移到
s
t
+
1
s_{t+1}
st+1。
**马尔可夫链:**离散时间的马尔可夫过程。其状态是有限的,例如:

可以用状态转移矩阵来描述状态转移过程
P
=
[
p
(
s
1
∣
s
1
)
p
(
s
2
∣
s
1
)
⋯
p
(
s
N
∣
s
1
)
p
(
s
1
∣
s
2
)
p
(
s
2
∣
s
2
)
⋯
p
(
s
N
∣
s
2
)
⋮
⋮
⋱
⋮
p
(
s
1
∣
s
N
)
p
(
s
2
∣
s
N
)
⋯
p
(
s
N
∣
s
N
)
]
P = \begin{bmatrix} p(s_1|s_1) & p(s_2|s_1) & \cdots & p(s_N|s_1) \\ p(s_1|s_2) & p(s_2|s_2) & \cdots & p(s_N|s_2) \\ \vdots & \vdots & \ddots & \vdots \\ p(s_1|s_N) & p(s_2|s_N) & \cdots & p(s_N|s_N) \end{bmatrix}
P=
p(s1∣s1)p(s1∣s2)⋮p(s1∣sN)p(s2∣s1)p(s2∣s2)⋮p(s2∣sN)⋯⋯⋱⋯p(sN∣s1)p(sN∣s2)⋮p(sN∣sN)
状态转移矩阵类似于条件概率(conditional probability),它表示当我们知道当前我们在状态
s
t
s_t
st 时,到达下面所有状态的概率。所以它的每一行描述的是从一个节点到达所有其他节点的概率。
1.3 马尔可夫奖励过程
马尔可夫链加上奖励函数。在马尔可夫奖励过程中,状态转移矩阵和状态都与马尔可夫链一样,只是多了奖励函数(reward function)。在强化学习中我们不知要关注过程,还要关注在过程中每一步所能获得到的奖励。
这里我们进一步定义一些概念。范围(horizon) 是指一个回合的长度(每个回合最大的时间步数),它是由有限个步数决定的。 **回报(return)**可以定义为奖励的逐步叠加,假设时刻 t t t后的奖励序列为 r t + 1 , r t + 2 , r t + 3 , ⋯ r_{t+1},r_{t+2},r_{t+3},⋯ rt+1,rt+2,rt+3,⋯,则回报为
回报(return)
G
t
G_t
Gt可以表示为从时刻t开始直到回合结束获得的所有奖励的折扣总和:
G
t
=
r
t
+
1
+
γ
r
t
+
2
+
γ
2
r
t
+
3
+
.
.
.
+
γ
T
−
t
−
1
r
T
G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t-1} r_T
Gt=rt+1+γrt+2+γ2rt+3+...+γT−t−1rT
其中,γ∈[0, 1]为折扣因子。随着时间的推移,未来的奖励会打折扣,表示我们更看重当前的奖励。
当我们有了回报的概念后,就可以定义状态价值函数(state-value function)V(s)。状态价值函数表示从状态s出发,按照当前策略一直执行下去,直到回合结束,所能获得的期望回报:
V
π
(
s
)
=
E
[
G
t
∣
S
t
=
s
]
=
E
[
r
t
+
1
+
γ
r
t
+
2
+
γ
2
r
t
+
3
+
⋯
+
γ
T
−
t
−
1
r
T
∣
S
t
=
s
]
=
E
[
r
t
+
1
∣
S
t
=
s
]
+
γ
E
[
G
t
+
1
∣
S
t
=
s
]
=
E
[
r
t
+
1
∣
S
t
=
s
]
+
γ
E
[
V
π
(
S
t
+
1
)
∣
S
t
=
s
]
\begin{align} V^{\pi}(s) &= \mathbb{E}[G_t \mid S_t = s] \\ &= \mathbb{E}[r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + \cdots + \gamma^{T-t-1}r_T \mid S_t = s] \\ &= \mathbb{E}[r_{t+1} \mid S_t = s] + \gamma \mathbb{E}[G_{t+1} \mid S_t = s] \\ &= \mathbb{E}[r_{t+1} \mid S_t = s] + \gamma \mathbb{E}[V^{\pi}(S_{t+1}) \mid S_t = s] \end{align}
Vπ(s)=E[Gt∣St=s]=E[rt+1+γrt+2+γ2rt+3+⋯+γT−t−1rT∣St=s]=E[rt+1∣St=s]+γE[Gt+1∣St=s]=E[rt+1∣St=s]+γE[Vπ(St+1)∣St=s]
其中,
G
t
G_t
Gt是之前定义的折扣回报(discounted return)。
R
(
s
)
R(s)
R(s)是奖励函数,我们对
G
t
G_t
Gt取了一个期望,期望就是从这个状态开始,我们可能获得多大的价值。所以期望也可以看成未来可能获得奖励的当前价值的表现,就是当我们进入某一个状态后,我们现在有多大的价值。
这些话说的有点拗口,通俗来说就是 G G G表示当下即时奖励和所有持久奖励等一切奖励的加权和,它是相对与一个序列中的一个状态节点来评估的,而价值函数 V ( s ) V(s) V(s)是对某一个状态来评估而,把这个状态从序列中抽象出来了。
贝尔曼方程
V
(
s
)
=
R
(
s
)
+
γ
∑
s
′
∈
S
P
(
s
′
∣
s
)
V
(
s
′
)
V(s) = R(s) + \gamma \sum_{s' \in S} P(s'|s) V(s')
V(s)=R(s)+γs′∈S∑P(s′∣s)V(s′)
一种快速求
V
(
s
)
V(s)
V(s)的方法。
1.4 马尔可夫决策过程
马尔可夫决策过程(MDP) = 马尔可夫奖励(MRP) + 智能体动作因素
MDP 的目标是找到一个最优策略 π,使得智能体在给定状态下选择最优的动作,从而最大化长期累积的折扣奖励。
状态价值函数:
V
π
(
s
)
=
E
π
[
G
t
∣
S
t
=
s
]
=
E
π
[
R
t
+
1
+
γ
G
t
+
1
∣
S
t
=
s
]
=
E
π
[
R
t
+
1
+
γ
V
π
(
S
t
+
1
)
∣
S
t
=
s
]
\begin{align} V_{\pi}(s) &= \mathbb{E}_{\pi}[G_t | S_t = s] \\ &= \mathbb{E}_{\pi}[R_{t+1} + \gamma G_{t+1} | S_t = s] \\ &= \mathbb{E}_{\pi}[R_{t+1} + \gamma V_{\pi}(S_{t+1}) | S_t = s] \end{align}
Vπ(s)=Eπ[Gt∣St=s]=Eπ[Rt+1+γGt+1∣St=s]=Eπ[Rt+1+γVπ(St+1)∣St=s]
动作价值状态函数:
Q
π
(
s
,
a
)
=
E
π
[
G
t
∣
S
t
=
s
,
A
t
=
a
]
=
E
π
[
R
t
+
1
+
γ
G
t
+
1
∣
S
t
=
s
,
A
t
=
a
]
=
E
π
[
R
t
+
1
+
γ
Q
π
(
S
t
+
1
,
A
t
+
1
)
∣
S
t
=
s
,
A
t
=
a
]
\begin{align} Q_{\pi}(s, a) &= \mathbb{E}_{\pi}[G_t | S_t = s, A_t = a] \\ &= \mathbb{E}_{\pi}[R_{t+1} + \gamma G_{t+1} | S_t = s, A_t = a] \\ &= \mathbb{E}_{\pi}[R_{t+1} + \gamma Q_{\pi}(S_{t+1}, A_{t+1}) | S_t = s, A_t = a] \end{align}
Qπ(s,a)=Eπ[Gt∣St=s,At=a]=Eπ[Rt+1+γGt+1∣St=s,At=a]=Eπ[Rt+1+γQπ(St+1,At+1)∣St=s,At=a]
A t A_t At表示t时刻的动作, Q Q Q函数相对上面的 V V V函数来说多考虑进去了一个动作的因素,而不只是单纯的状态。
马尔可夫决策的贝尔曼方程
V
π
(
s
)
=
∑
a
∈
A
π
(
a
∣
s
)
[
R
(
s
,
a
)
+
γ
∑
s
′
∈
S
P
(
s
′
∣
s
,
a
)
V
π
(
s
′
)
]
V_{\pi}(s) = \sum_{a \in A} \pi(a|s) \left[ R(s, a) + \gamma \sum_{s' \in S} P(s'|s, a) V_{\pi}(s') \right]
Vπ(s)=a∈A∑π(a∣s)[R(s,a)+γs′∈S∑P(s′∣s,a)Vπ(s′)]
Q π ( s , a ) = R ( s , a ) + γ ∑ s ′ ∈ S P ( s ′ ∣ s , a ) [ ∑ a ′ ∈ A π ( a ′ ∣ s ′ ) Q π ( s ′ , a ′ ) ] Q_{\pi}(s, a) = R(s, a) + \gamma \sum_{s' \in S} P(s'|s, a) \left[ \sum_{a' \in A} \pi(a'|s') Q_{\pi}(s', a') \right] Qπ(s,a)=R(s,a)+γs′∈S∑P(s′∣s,a)[a′∈A∑π(a′∣s′)Qπ(s′,a′)]
2. REINFORCE
2.1 策略梯度算法
由于REINFORCE是最简单的侧率梯度算法,所以这里先介绍策略梯度算法
强化学习有 3 个组成部分:演员(actor)、环境和奖励函数。显然我们能控制的只有演员,环境和奖励函数是客观存在的。智能体玩视频游戏时,演员负责操控游戏的摇杆, 比如向左、向右、开火等操作;环境就是游戏的主机,负责控制游戏的画面、负责控制怪兽的移动等;奖励函数就是当我们做什么事情、发生什么状况的时候,可以得到多少分数, 比如打败一只怪兽得到 20 分等。在强化学习里,环境与奖励函数不是我们可以控制的,它们是在开始学习之前给定的。我们唯一需要做的就是调整演员里面的策略,使得演员可以得到最大的奖励。演员里面的策略决定了演员的动作,即给定一个输入,它会输出演员现在应该要执行的动作。
在下面的例子中,我们会用一个神经网络来当做演员,把环境输入,由神经网络给出动作。例如玩一个走格子的游戏,游戏规则如下,创建一个 n × n n \times n n×n的棋盘,旗子在 ( 0 , 0 ) (0, 0) (0,0)处,要到达 ( n , n ) (n, n) (n,n)处,并用最少的步数走到。(也可以为了增加难度在中间设置一点障碍)棋子有四种动作,分别是“上、下、左、右”。
有四个动作,所以神经网络有四个输出,根据四个输出的概率进行采样选择下一步的动作。
智能体的每一步动作都会有一个奖励,为了让他能尽快走到终点,因此它每走到一个格子,除非那个格子是终点,我们就把那个格子的奖励设置为-1,终点的奖励可以设置为10,中间也可以设置障碍,例如将某个点的奖励设置为-100,这样就让模型学会避开这个点。
我们的游戏过程应该是这样的:

在一场游戏里面,我们把环境输出的
s
s
s与演员输出的动作
a
a
a 全部组合起来,就是一个轨迹
τ
=
{
s
1
,
a
1
,
s
2
,
a
2
,
…
,
s
t
,
a
t
}
\tau = \{s_1, a_1, s_2, a_2, \dots, s_t, a_t\}
τ={s1,a1,s2,a2,…,st,at}
给定演员的参数
θ
θ
θ(就是提到的神经网络的参数),我们可以计算某个轨迹
τ
\tau
τ发生的概率为
p
θ
(
τ
)
=
p
(
s
1
)
p
θ
(
a
1
∣
s
1
)
p
(
s
2
∣
s
1
,
a
1
)
p
θ
(
a
2
∣
s
2
)
p
(
s
3
∣
s
2
,
a
2
)
⋯
=
p
(
s
1
)
∏
t
=
1
T
p
θ
(
a
t
∣
s
t
)
p
(
s
t
+
1
∣
s
t
,
a
t
)
\begin{align} p_{\theta}(\tau) &= p(s_1) p_{\theta}(a_1|s_1) p(s_2|s_1, a_1) p_{\theta}(a_2|s_2) p(s_3|s_2, a_2) \cdots \\ &= p(s_1) \prod_{t=1}^{T} p_{\theta}(a_t|s_t) p(s_{t+1}|s_t, a_t) \end{align}
pθ(τ)=p(s1)pθ(a1∣s1)p(s2∣s1,a1)pθ(a2∣s2)p(s3∣s2,a2)⋯=p(s1)t=1∏Tpθ(at∣st)p(st+1∣st,at)
这个计算是一步一步的,并不是并行的。其中
p
(
s
t
+
1
∣
s
t
,
a
t
)
p(s_{t+1} \mid s_t, a_t)
p(st+1∣st,at)意思是在环境为
s
t
s_t
st, 演员采取动作
a
t
a_t
at的情况下环境变成
s
t
+
1
s_{t+1}
st+1的概率。
那既然每一把游戏都是一个轨迹
τ
\tau
τ,每一把游戏都会有一个得分,这个得分按照我之前设定的规则应该是每个动作的回报
G
t
G_t
Gt然后求和,由于整局游戏我已经玩完了,所以之后的动作是什么奖励是什么我也知道,因此我们能算出这个
G
t
G_t
Gt。
G
t
=
r
t
+
1
+
γ
r
t
+
2
+
γ
2
r
t
+
3
+
.
.
.
+
γ
T
−
t
−
1
r
T
G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t-1} r_T
Gt=rt+1+γrt+2+γ2rt+3+...+γT−t−1rT
我们把这局游戏每一步走完之后的
G
t
G_t
Gt都存下来,如果这个
G
t
G_t
Gt大于零,就说明这个动作有好处,可以增大采样采到这个动作的概率,注意,增加的是
p
θ
(
a
t
∣
s
t
)
p_{\theta}(a_t|s_t)
pθ(at∣st)的概率,而不是
p
θ
(
a
t
)
p_{\theta}(a_t)
pθ(at)的值,需要联系环境来考虑问题。就跟人斗地主一样,你是农民,你的队友手里只剩下两张王了,这个时候最好的决策就是将自己手里的炸弹打出来增加奖励。你出炸弹的概率就几乎是百分之百,
p
(
出炸弹
∣
队友手里只剩下两张王了
)
=
0.99
p(出炸弹 \mid 队友手里只剩下两张王了) = 0.99
p(出炸弹∣队友手里只剩下两张王了)=0.99,而在别的情况下你出炸弹的概率就没必要这么高,毕竟很少有人开局就出炸弹。所以模型要提高的是
p
(
出炸弹
∣
队友手里只剩下两张王了
)
p(出炸弹 \mid 队友手里只剩下两张王了)
p(出炸弹∣队友手里只剩下两张王了)的值,而不是
p
(
出炸弹
)
p(出炸弹)
p(出炸弹)。这个
G
t
G_t
Gt暂时不会用到,后面会用到。
在强化学习里面,除了环境与演员以外,还有奖励函数。如下图所示,奖励函数根据在某一个状态采取的某一个动作决定这个动作可以得到的分数。对奖励函数输入 s 1 , a 1 s_1, a_1 s1,a1,它会输出 r 1 r_1 r1,输入 s 2 , a 2 s_2, a_2 s2,a2,它会输出 r 2 r_2 r2。 我们把轨迹所有的奖励 r r r都加起来,就得到了 R ( τ ) R(\tau) R(τ),其代表某一个轨迹 τ \tau τ的奖励。

那一把游戏能不能衡量一个演员(智能体)的能力呢,可能不行,因为你有可能是蒙的,那我们如何评价一个演员的能力呢,多玩几次,求期望:
R
ˉ
θ
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
\bar{R}_{\theta} = \sum_{\tau} R(\tau) p_{\theta}(\tau)
Rˉθ=τ∑R(τ)pθ(τ)
那么这个
R
ˉ
θ
\bar{R}_{\theta}
Rˉθ就是我们要优化的目标了。也可以表达成这样:
R
ˉ
θ
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
=
E
τ
∼
p
θ
(
τ
)
[
R
(
τ
)
]
\bar{R}_{\theta} = \sum_{\tau} R(\tau) p_{\theta}(\tau) = \mathbb{E}_{\tau \sim p_{\theta}(\tau)}[R(\tau)]
Rˉθ=τ∑R(τ)pθ(τ)=Eτ∼pθ(τ)[R(τ)]
因为我们要让奖励越大越好,所以可以使用梯度上升(gradient ascent)来最大化期望奖励。要进行梯度上升,我们先要计算期望奖励
R
ˉ
θ
\bar{R}_{\theta}
Rˉθ的梯度。我们对
R
ˉ
θ
\bar{R}_{\theta}
Rˉθ做梯度运算。
R
ˉ
θ
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
\bar{R}_{\theta} = \sum_{\tau} R(\tau) p_{\theta}(\tau)
Rˉθ=τ∑R(τ)pθ(τ)
对
θ
\theta
θ求偏导:
∇
R
ˉ
θ
=
∑
τ
R
(
τ
)
∇
p
θ
(
τ
)
\nabla \bar{R}_{\theta} = \sum_{\tau} R(\tau) \nabla p_{\theta}(\tau)
∇Rˉθ=τ∑R(τ)∇pθ(τ)
由于
∇
l
o
g
a
(
f
(
x
)
)
=
∇
f
(
x
)
f
(
x
)
l
n
a
\nabla log_a(f(x)) = \frac{\nabla f(x)}{f(x)lna}
∇loga(f(x))=f(x)lna∇f(x),
l
n
a
lna
lna是常数,可以忽略(直接将底数看做是e更好理解下面的公式):
∇
p
θ
(
τ
)
=
p
θ
(
τ
)
∇
log
p
θ
(
τ
)
∇
p
θ
(
τ
)
p
θ
(
τ
)
=
∇
log
p
θ
(
τ
)
\nabla p_{\theta}(\tau) = p_{\theta}(\tau) \nabla \log p_{\theta}(\tau) \\ \\ \frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)} = \nabla \log p_{\theta}(\tau)
∇pθ(τ)=pθ(τ)∇logpθ(τ)pθ(τ)∇pθ(τ)=∇logpθ(τ)
带入
∇
R
ˉ
θ
\nabla \bar{R}_{\theta}
∇Rˉθ得:
∇
R
ˉ
θ
=
∑
τ
R
(
τ
)
∇
p
θ
(
τ
)
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
∇
p
θ
(
τ
)
p
θ
(
τ
)
=
∑
τ
R
(
τ
)
p
θ
(
τ
)
∇
log
p
θ
(
τ
)
=
E
τ
∼
p
θ
(
τ
)
[
R
(
τ
)
∇
log
p
θ
(
τ
)
]
\begin{align} \nabla \bar{R}_{\theta} &= \sum_{\tau} R(\tau) \nabla p_{\theta}(\tau) \\ &= \sum_{\tau} R(\tau) p_{\theta}(\tau) \frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)} \\ &= \sum_{\tau} R(\tau) p_{\theta}(\tau) \nabla \log p_{\theta}(\tau) \\ &= \mathbb{E}_{\tau \sim p_{\theta}(\tau)} \left[ R(\tau) \nabla \log p_{\theta}(\tau) \right] \end{align}
∇Rˉθ=τ∑R(τ)∇pθ(τ)=τ∑R(τ)pθ(τ)pθ(τ)∇pθ(τ)=τ∑R(τ)pθ(τ)∇logpθ(τ)=Eτ∼pθ(τ)[R(τ)∇logpθ(τ)]
由于轨迹是采不完的,因此
E
τ
∼
p
θ
(
τ
)
\mathbb{E}_{\tau \sim p_{\theta}(\tau)}
Eτ∼pθ(τ)无法计算,我们只能以部分估计总体,这也是统计学的精髓,多采几次样,这里假设采样
N
N
N个
τ
\tau
τ并计算每一个
τ
\tau
τ的
[
R
(
τ
)
∇
log
p
θ
(
τ
)
]
\left[ R(\tau) \nabla \log p_{\theta}(\tau) \right]
[R(τ)∇logpθ(τ)]值,再将其求和取平均,用来近似
E
τ
∼
p
θ
(
τ
)
\mathbb{E}_{\tau \sim p_{\theta}(\tau)}
Eτ∼pθ(τ)。
那如何处理
log
p
θ
(
τ
)
\log p_{\theta}(\tau)
logpθ(τ)呢?
∇
log
p
θ
(
τ
)
=
∇
(
log
p
(
s
1
)
+
∑
t
=
1
T
log
p
θ
(
a
t
∣
s
t
)
+
∑
t
=
1
T
log
p
(
s
t
+
1
∣
s
t
,
a
t
)
)
=
∇
log
p
(
s
1
)
+
∇
∑
t
=
1
T
log
p
θ
(
a
t
∣
s
t
)
+
∇
∑
t
=
1
T
log
p
(
s
t
+
1
∣
s
t
,
a
t
)
=
∑
t
=
1
T
∇
log
p
θ
(
a
t
∣
s
t
)
=
∑
t
=
1
T
∇
log
p
θ
(
a
t
∣
s
t
)
\begin{aligned} \nabla \log p_{\theta}(\tau) &= \nabla \left( \log p(s_1) + \sum_{t=1}^T \log p_{\theta}(a_t|s_t) + \sum_{t=1}^T \log p(s_{t+1}|s_t, a_t) \right) \\ &= \nabla \log p(s_1) + \nabla \sum_{t=1}^T \log p_{\theta}(a_t|s_t) + \nabla \sum_{t=1}^T \log p(s_{t+1}|s_t, a_t) \\ &= \sum_{t=1}^T \nabla \log p_{\theta}(a_t|s_t) \\ &= \sum_{t=1}^T \nabla \log p_{\theta}(a_t|s_t) \end{aligned}
∇logpθ(τ)=∇(logp(s1)+t=1∑Tlogpθ(at∣st)+t=1∑Tlogp(st+1∣st,at))=∇logp(s1)+∇t=1∑Tlogpθ(at∣st)+∇t=1∑Tlogp(st+1∣st,at)=t=1∑T∇logpθ(at∣st)=t=1∑T∇logpθ(at∣st)
第二部中有一些与
θ
\theta
θ无关的项直接被求偏导变成零了。因为
p
p
p表示的是根据动作和前面的场景生成的场景为
s
t
+
1
s_{t+1}
st+1的概率,与演员无关。
由上式和之前分析的由部分代替总体的思想一起带入
∇
R
ˉ
θ
\nabla \bar{R}_{\theta}
∇Rˉθ可得:
E
τ
∼
p
θ
(
τ
)
[
R
(
τ
)
∇
log
p
θ
(
τ
)
]
≈
1
N
∑
n
=
1
N
R
(
τ
n
)
∇
log
p
θ
(
τ
n
)
=
1
N
∑
n
=
1
N
∑
t
=
1
T
n
R
(
τ
n
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\begin{aligned} \mathbb{E}_{\tau \sim p_{\theta}(\tau)}[R(\tau) \nabla \log p_{\theta}(\tau)] &\approx \frac{1}{N} \sum_{n=1}^{N} R(\tau^n) \nabla \log p_{\theta}(\tau^n) \\ &= \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} R(\tau^n) \nabla \log p_{\theta}(a_t^n|s_t^n) \end{aligned}
Eτ∼pθ(τ)[R(τ)∇logpθ(τ)]≈N1n=1∑NR(τn)∇logpθ(τn)=N1n=1∑Nt=1∑TnR(τn)∇logpθ(atn∣stn)
那我们就可以将
∇
R
ˉ
θ
\nabla \bar{R}_{\theta}
∇Rˉθ当做损失函数:
∇
R
ˉ
θ
=
1
N
∑
n
=
1
N
∑
t
=
1
T
n
R
(
τ
n
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\nabla \bar{R}_{\theta} = \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} R(\tau^n) \nabla \log p_{\theta}(a_t^n|s_t^n)
∇Rˉθ=N1n=1∑Nt=1∑TnR(τn)∇logpθ(atn∣stn)
既然有了损失函数,那就可以直接套用深度学习神经网络那一套东西,更新参数:
θ
←
θ
+
η
∇
R
ˉ
θ
\theta \leftarrow \theta + \eta \nabla \bar{R}_{\theta}
θ←θ+η∇Rˉθ
那么网络的训练步骤就非常的简单:
采集数据 ——> 更新参数——>采集数据——>……
值得注意的是采集到的数据只会用一次就丢掉,这显然是一个可以优化的点,之后在PPO算法中会对其进行优化。

2.2 策略梯度算法的一些实现技巧
2.2.1 基线
如果奖励一直是正的, ∇ R ˉ θ \nabla \bar{R}_{\theta} ∇Rˉθ就会一直是正的,那概率就只会增加不会减少,什么意思呢?
如图所示:
在某一个状态有 3 个动作 a、b、c可以执行,b的奖励最大,c的奖励最小。但是如果采样没有采到a,只采到了b、c,那么b、c的概率就会增大(因为奖励是正的),因为a、b、c的概率和是1,b、c概率增大了,a的概率就会减小,这显然是不科学的。
因此我们可以引入一个基线,就是把奖励统一减去
b
b
b,让奖励有正有负,即:
∇
R
ˉ
θ
≈
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
R
(
τ
n
)
−
b
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} (R(\tau^n) - b) \nabla \log p_{\theta}(a_t^n|s_t^n)
∇Rˉθ≈N1n=1∑Nt=1∑Tn(R(τn)−b)∇logpθ(atn∣stn)
2.2.2 分配合适的分数
观察损失函数:
∇
R
ˉ
θ
≈
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
R
(
τ
n
)
−
b
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} (R(\tau^n) - b) \nabla \log p_{\theta}(a_t^n|s_t^n)
∇Rˉθ≈N1n=1∑Nt=1∑Tn(R(τn)−b)∇logpθ(atn∣stn)
在同一场游戏里面,在同一场游戏里面,所有的状态-动作对就使用同样的奖励项进行加权。
这显然是不公平的例如:
假设游戏都很短,只有 3 ~ 4 个交互,在 s a s_a sa 执行 a 1 a_1 a1得到 5 分,在 s b s_b sb执行 a 2 a_2 a2得到 0 分,在 s c s_c sc执行 a 3 a_3 a3得到 −2 分。 整场游戏下来,我们得到 +3 分,那我们得到 +3 分 代表在 s b s_b sb执行 a 2 a_2 a2 是好的吗? 这并不一定代表在 s b s_b sb执行 a 2 a_2 a2是好的。因为这个正的分数,主要来自在 s a s_a sa执行了 a 1 a_1 a1,与在 s b s_b sb执行 a 2 a_2 a2是没有关系的,也许在 s b s_b sb执行 a 2 a_2 a2反而是不好的, 因为它导致我们接下来会进入 s c s_c sc,执行 a 3 a_3 a3被扣分。所以整场游戏得到的结果是好的, 并不代表每一个动作都是好的。

理想情况下这种情况不会发生,只要采样足够多,多次遇到 ( s a , a 1 ) (s_a, a_1) (sa,a1)的情况,我们就能通过梯度更新来合理评估它的概率,但是问题是采样成本很高,采样那么多次不太现实,因此我们可以为每个状态-动作对分配合理的分数,要让大家知道它合理的贡献。
我们之前谈到过
G
t
G_t
Gt
G
t
=
r
t
+
1
+
γ
r
t
+
2
+
γ
2
r
t
+
3
+
.
.
.
+
γ
T
−
t
−
1
r
T
G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t-1} r_T
Gt=rt+1+γrt+2+γ2rt+3+...+γT−t−1rT
它就能很好的反应每个状态的价值即:
∇
R
ˉ
θ
≈
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
∑
t
′
=
t
T
n
γ
t
′
−
t
r
t
′
n
−
b
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
=
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
G
t
−
b
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\begin{align} \nabla \bar{R}_{\theta} &\approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} \left( \sum_{t'=t}^{T_n} \gamma^{t'-t} r_{t'}^n - b \right) \nabla \log p_{\theta}(a_t^n|s_t^n) \\ &=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} \left( G_t - b \right) \nabla \log p_{\theta}(a_t^n|s_t^n) \end{align}
∇Rˉθ≈N1n=1∑Nt=1∑Tn(t′=t∑Tnγt′−trt′n−b)∇logpθ(atn∣stn)=N1n=1∑Nt=1∑Tn(Gt−b)∇logpθ(atn∣stn)
说明:
事实上 b b b通常是一个网络估计出来的,它是一个网络的输出。我们把 R − b R-b R−b这一项称为优势函数(advantage function), 用 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)来代表优势函数。优势函数取决于 s s s和 a a a,我们就是要计算在某个状态 s s s采取某个动作 a a a的时候,优势函数的值。在计算优势函数值时,我们要计算 ∑ t ′ = t T n r t ′ n \sum_{t'=t}^{T_n} r_{t'}^n ∑t′=tTnrt′n,需要有一个模型与环境交互,才能知道接下来得到的奖励。优势函数 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)的上标是 θ \theta θ, θ \theta θ 代表用模型 θ \theta θ与环境交互。从时刻 t t t开始到游戏结束为止,所有 r r r的加和减去 b b b,这就是优势函数。优势函数的意义是,假设我们在某一个状态 s t s_t st执行某一个动作 a t a_t at,相较于其他可能的动作, a t a_t at有多好。优势函数在意的不是绝对的好,而是相对的好,即相对优势(relative advantage)。因为在优势函数中,我们会减去一个基线 b b b,所以这个动作是相对的好,不是绝对的好。 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)通常可以由一个网络估计出来,这个网络称为评论员(critic)。
2.3 REINFORCE算法实现
由于在游戏中我的策略是每个格子如果不是终点,奖励分数就是-1,因此无需使用基线,但是代码中使用了“分配合适分数”方法,所有函数作用代码中有详细注释,因此不再赘述。
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
# 环境定义
class GridWorldEnv:
def __init__(self, size=5):
# 初始化网格世界环境
self.size = size # 网格的大小
self.state = (0, 0) # 初始状态
self.goal = (size - 1, size - 1) # 目标状态
self.actions = ['up', 'down', 'left', 'right'] # 可用的动作
self.action_space = len(self.actions) # 动作空间的大小
def reset(self):
# 重置环境到初始状态
self.state = (0, 0)
return self.state
def step(self, action):
# 根据动作更新状态,并返回新的状态、奖励和是否完成
x, y = self.state
if action == 0: # 向上
x = max(0, x - 1)
elif action == 1: # 向下
x = min(self.size - 1, x + 1)
elif action == 2: # 向左
y = max(0, y - 1)
elif action == 3: # 向右
y = min(self.size - 1, y + 1)
self.state = (x, y)
reward = -1 # 默认奖励
if x == 2 and y != 2:
reward = -10
done = self.state == self.goal # 检查是否到达目标
if done:
reward = 10 # 到达目标的奖励
return self.state, reward, done
def render(self):
# 渲染当前状态的网格世界
grid = np.zeros((self.size, self.size)) # 创建一个全零的网格
x, y = self.state
grid[x, y] = 1 # 将当前状态的位置设为1
print(grid)
# 策略网络
class PolicyNetwork(nn.Module):
def __init__(self, input_dim, output_dim):
# 初始化策略网络
super(PolicyNetwork, self).__init__()
self.fc1 = nn.Linear(input_dim, 24) # 第一层全连接层
self.fc2 = nn.Linear(24, 128) # 第二层全连接层
self.fc3 = nn.Linear(128, output_dim) # 第三层全连接层
self.relu = nn.ReLU() # ReLU激活函数
self.softmax = nn.Softmax(dim=-1) # Softmax输出层
def forward(self, x):
# 前向传播
x = self.relu(self.fc1(x)) # 第一层激活
x = self.relu(self.fc2(x)) # 第二层激活
x = self.softmax(self.relu(self.fc3(x))) # 第三层激活
return x
# REINFORCE算法
class REINFORCE:
def __init__(self, state_space, action_space, learning_rate=0.01, gamma=0.99):
# 初始化REINFORCE算法
self.policy_network = PolicyNetwork(state_space, action_space) # 策略网络
self.optimizer = optim.Adam(self.policy_network.parameters(), lr=learning_rate) # Adam优化器
self.gamma = gamma # 折扣因子
def choose_action(self, state):
# 根据状态选择动作
state = torch.tensor(state, dtype=torch.float32).unsqueeze(0) # 将状态转换为张量
action_probs = self.policy_network(state).detach().numpy()[0] # 获取动作概率
action = np.random.choice(len(action_probs), p=action_probs) # 根据概率选择动作
return action
def compute_discounted_rewards(self, rewards):
# 计算折扣奖励
discounted_rewards = np.zeros_like(rewards, dtype=np.float32) # 初始化折扣奖励
cumulative = 0
for i in reversed(range(len(rewards))):
cumulative = cumulative * self.gamma + rewards[i] # 计算累计折扣奖励
discounted_rewards[i] = cumulative
return discounted_rewards
def train(self, states, actions, rewards):
# 训练策略网络
discounted_rewards = self.compute_discounted_rewards(rewards) # 计算折扣奖励
# 归一化奖励
discounted_rewards = (discounted_rewards - np.mean(discounted_rewards)) / (np.std(discounted_rewards) + 1e-8)
discounted_rewards = torch.tensor(discounted_rewards, dtype=torch.float32)
states = torch.tensor(states, dtype=torch.float32) # 将状态转换为张量
actions = torch.tensor(actions, dtype=torch.long) # 将动作转换为张量
# 计算损失
log_probs = torch.log(self.policy_network(states)) # 计算对数概率
selected_log_probs = log_probs[range(len(actions)), actions] # 选择执行动作的对数概率
loss = -torch.mean(selected_log_probs * discounted_rewards) # 计算损失
# 优化网络
self.optimizer.zero_grad() # 清零梯度
loss.backward() # 反向传播
self.optimizer.step() # 更新参数
# 主程序
def main():
env = GridWorldEnv(size=5) # 创建网格世界环境
agent = REINFORCE(state_space=10, action_space=4, learning_rate=0.01) # 创建REINFORCE智能体
episodes = 1000 # 训练回合数
reward_history = [] # 奖励历史
state = env.reset() # 重置环境
env.render() # 渲染初始状态
for episode in range(episodes):
state = env.reset() # 重置环境
states, actions, rewards = [], [], [] # 初始化状态、动作和奖励列表
done = False
while not done:
# 状态转换为独热向量
state_onehot = np.eye(env.size)[state[0]].tolist() + np.eye(env.size)[state[1]].tolist()
action = agent.choose_action(state_onehot) # 选择动作
next_state, reward, done = env.step(action) # 执行动作
states.append(state_onehot) # 记录状态
actions.append(action) # 记录动作
rewards.append(reward) # 记录奖励
state = next_state # 更新状态
agent.train(states, actions, rewards) # 训练智能体
reward_history.append(sum(rewards)) # 记录总奖励
if episode % 100 == 0:
print(f"Episode {episode}, Total Reward: {sum(rewards)}") # 每100回合打印一次总奖励
# 绘制奖励曲线
plt.plot(reward_history)
plt.xlabel("Episodes")
plt.ylabel("Total Reward")
plt.show()
if __name__ == "__main__":
main()
3. A2C算法
3.1 策略梯度的一些缺陷
策略梯度算法在理想情况下,在采样次数足够多的情况下效果是能很不错的,但是当采样不够时就会出现一些问题,例如 G t G_t Gt的取值是很不稳定的,下图可以形象说明:

由于 G t G_t Gt的取值不稳定,所以 ( s t , a t ) (s_t, a_t) (st,at)更新也不稳定。
3.2 DQN中的一些概念
由于 G G G的值有点太不稳定太玄学了,因此我们可以想办法去用一个神经网络去预测在 s s s状态下采取行动 a a a时对应的 G G G期望值,之后再训练中我们就直接用这个期望值去替代采样的值。为了完成这个目的,我们可以使用基于价值的方法深度Q网络,深度Q网络有两种函数,也就是两种评论员,他们分别是 V π ( s ) V_{\pi}(s) Vπ(s)和 Q π ( s , a ) Q_{\pi}(s,a) Qπ(s,a), V π ( s ) V_{\pi}(s) Vπ(s)是指假设演员的策略是 π {\pi} π,使用 π {\pi} π与环境交互,当智能体看到状态 s s s时,接下来累积奖励的期望值是多少。 Q π ( s , a ) Q_{\pi}(s,a) Qπ(s,a)是指把 s s s与 a a a当作输入,它表示在状态 s s s采取动作 a a a,接下来用策略 π {\pi} π与环境交互,累积奖励的期望值是多少。 V π ( s ) V_{\pi}(s) Vπ(s)接收输入 s s s,输出一个标量。 Q π ( s , a ) Q_{\pi}(s,a) Qπ(s,a)接收输入 s s s,它会给每一个 s s s都分配一个 Q Q Q值。

有了这两个函数,我们可以用这两个函数去替换原来的梯度公式:

又出现一个问题,神经网络本来就不稳定,现在还要估计两个神经网络,Q网络和V网络,那岂不是更不稳定,估不准的概率岂不是就翻倍了,那就得想点办法只估计一个网络,事实上,在演员-评论员算法中,我们可以只估计网络 V,并利用
V
V
V的值来表示
Q
Q
Q的值,
Q
π
(
s
t
n
,
a
t
n
)
Q_{\pi}(s_t^n, a_t^n)
Qπ(stn,atn)可以写成
r
t
n
+
V
π
(
s
t
+
1
n
)
r_t^n + V_{\pi}(s_{t+1}^n)
rtn+Vπ(st+1n)的期望值,即:
Q
π
(
s
t
n
,
a
t
n
)
=
E
[
r
t
n
+
V
π
(
s
t
+
1
n
)
]
Q_{\pi}(s_t^n, a_t^n) = \mathbb{E} \left[ r_t^n + V_{\pi}(s_{t+1}^n) \right]
Qπ(stn,atn)=E[rtn+Vπ(st+1n)]
在状态
s
s
s采取动作
a
a
a,我们会得到奖励
r
r
r,进入状态
s
t
+
1
s_{t+1}
st+1。但是我们会得到什么样的奖励
r
r
r,进入什么样的状态
s
t
+
1
s_{t+1}
st+1,这件事本身是有随机性的。所以要把
r
t
n
+
V
π
(
s
t
+
1
n
)
r_t^n + V_{\pi}(s_{t+1}^n)
rtn+Vπ(st+1n)取期望值才会等于Q函数的值。但我们现在把取期望值去掉,即:
Q
π
(
s
t
n
,
a
t
n
)
=
r
t
n
+
V
π
(
s
t
+
1
n
)
Q_{\pi}(s_t^n, a_t^n) = r_t^n + V_{\pi}(s_{t+1}^n)
Qπ(stn,atn)=rtn+Vπ(st+1n)
即:Q函数与V函数的差值可以变成
r
t
n
+
V
π
(
s
t
+
1
n
)
−
V
π
(
s
t
n
)
r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n)
rtn+Vπ(st+1n)−Vπ(stn)
至于究竟为什么可以去掉期望值,也没有什么很强有力的数学解释,只是研究人员发现这么做效果不错,其他方法效果没这个好,大家伙就都用这个方法了,都直接把期望去掉了。
推了半天,得到一个 r t n + V π ( s t + 1 n ) − V π ( s t n ) r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) rtn+Vπ(st+1n)−Vπ(stn),这个公式就是时序差分误差(TD-error),它代替了我们之前提到的优势函数,就是替代了 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)。
那么
∇
R
ˉ
θ
\nabla \bar{R}_{\theta}
∇Rˉθ就变成了:
∇
R
ˉ
θ
≈
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
r
t
n
+
V
π
(
s
t
+
1
n
)
−
V
π
(
s
t
n
)
)
∇
log
p
θ
(
a
t
n
∣
s
t
n
)
\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} (r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n)) \nabla \log p_{\theta}(a_t^n | s_t^n)
∇Rˉθ≈N1n=1∑Nt=1∑Tn(rtn+Vπ(st+1n)−Vπ(stn))∇logpθ(atn∣stn)
算法流程:

3.3 Actor网络和Critic网络的损失函数
讲了这么多,那么我们要训练两个网络,Actor网络和Critic网络,既然是训练这两个模型就需要损失函数,两个模型就需要两个损失函数,那他们分别是什么呢?
Actor网络的损失函数如下:
L
a
c
t
o
r
=
−
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
δ
t
n
⋅
log
π
θ
(
a
t
n
∣
s
t
n
)
)
L_{actor} = -\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} (\delta_t^n \cdot \log \pi_{\theta}(a_t^n | s_t^n))
Lactor=−N1n=1∑Nt=1∑Tn(δtn⋅logπθ(atn∣stn))
∙ δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) :时序差分误差 (TD-error)。 ∙ log π θ ( a t n ∣ s t n ) :策略的对数概率,表示 Actor 在状态 s t n 下选择动作 a t n 的对数概率。 ∙ N :是采样的轨迹数量。 ∙ T n :轨迹 n 的时间步数。 \begin{align*} & \bullet \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) \text{:时序差分误差 (TD-error)。} \\ & \bullet \log \pi_{\theta}(a_t^n|s_t^n) \text{:策略的对数概率,表示 Actor 在状态 } s_t^n \text{ 下选择动作 } a_t^n \text{ 的对数概率。} \\ & \bullet N \text{:是采样的轨迹数量。} \\ & \bullet T_n \text{:轨迹 } n \text{ 的时间步数。} \end{align*} ∙δtn=rtn+Vπ(st+1n)−Vπ(stn):时序差分误差 (TD-error)。∙logπθ(atn∣stn):策略的对数概率,表示 Actor 在状态 stn 下选择动作 atn 的对数概率。∙N:是采样的轨迹数量。∙Tn:轨迹 n 的时间步数。
Critic网络损失函数如下:
L
c
r
i
t
i
c
=
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
δ
t
n
)
2
L_{critic} = \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} \left( \delta_t^n\right)^2
Lcritic=N1n=1∑Nt=1∑Tn(δtn)2
δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) :时序差分误差 (TD-error)。 ∙ N :是采样的轨迹数量。 ∙ T n :轨迹 n 的时间步数。 \begin{align*} & \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) \text{:时序差分误差 (TD-error)。} \\ & \bullet N \text{:是采样的轨迹数量。} \\ & \bullet T_n \text{:轨迹 } n \text{ 的时间步数。} \end{align*} δtn=rtn+Vπ(st+1n)−Vπ(stn):时序差分误差 (TD-error)。∙N:是采样的轨迹数量。∙Tn:轨迹 n 的时间步数。
有了损失函数又改如何求梯度算参数呢:
首先需要明白有两个网络,Actor网络和Critic网络,他们在上述公式中的体现是什么,时序差分误差 δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) δtn=rtn+Vπ(st+1n)−Vπ(stn),既然是误差,涉及到评估工作,因此V网络就是Critic网络,其网络参数并未在公式中标注,可以将其标注为 ϕ {\phi} ϕ。至于Actor网络,公式中已经标明过了, π θ {\pi}_{\theta} πθ表示 π \pi π这个策略网络的参数就是 θ \theta θ,因此求梯度也就很轻松。
Actor网络的梯度公式如下:
∇
θ
L
a
c
t
o
r
=
−
1
N
∑
n
=
1
N
∑
t
=
1
T
n
δ
t
n
⋅
log
π
θ
(
a
t
n
∣
s
t
n
)
,
\nabla_{\theta} L_{actor} = -\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} \delta_t^n \cdot \log \pi_{\theta}(a_t^n|s_t^n),
∇θLactor=−N1n=1∑Nt=1∑Tnδtn⋅logπθ(atn∣stn),
∙ δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) :TD-error。 ∙ ∇ θ log π θ ( a t n ∣ s t n ) :策略关于参数 θ 的梯度。 \begin{align*} & \bullet \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) \text{:TD-error。} \\ & \bullet \nabla_{\theta} \log \pi_{\theta}(a_t^n|s_t^n) \text{:策略关于参数}\theta\text{的梯度。} \end{align*} ∙δtn=rtn+Vπ(st+1n)−Vπ(stn):TD-error。∙∇θlogπθ(atn∣stn):策略关于参数θ的梯度。
Critic网络的梯度公式如下:
∇
ϕ
L
c
r
i
t
i
c
=
1
N
∑
n
=
1
N
∑
t
=
1
T
n
2
⋅
δ
t
n
⋅
∇
ϕ
V
π
(
s
t
n
;
ϕ
)
\nabla_{\phi} L_{critic} = \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} 2 \cdot \delta_t^n \cdot \nabla_{\phi} V_{\pi}(s_t^n; \phi)
∇ϕLcritic=N1n=1∑Nt=1∑Tn2⋅δtn⋅∇ϕVπ(stn;ϕ)
∙ δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) :TD-error。 ∙ ∇ ϕ V π ( s t n ; ϕ ) :价值函数关于参数 ϕ 的梯度。 \begin{align*} & \bullet \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) \text{:TD-error。} \\ & \bullet \ \nabla_{\phi} V_{\pi}(s_t^n; \phi) \text{:价值函数关于参数}\phi\text{的梯度。} \\ \end{align*} ∙δtn=rtn+Vπ(st+1n)−Vπ(stn):TD-error。∙ ∇ϕVπ(stn;ϕ):价值函数关于参数ϕ的梯度。
3.4 A2C算法实现
实现优势演员-评论员算法的时候,有两个一定会用到的技巧。
第一个技巧是,我们需要估计两个网络: V V V网络和策略的网络(也就是演员)。评论员网络 V π ( s ) V_{\pi}(s) Vπ(s)接收一个状态,输出一个标量。演员的策略 π ( s ) {\pi(s)} π(s)接收一个状态,如果动作是离散的,输出就是一个动作的分布。如果动作是连续的,输出就是一个连续的向量。
离散动作的例子如下,连续动作的情况也是一样的。输入一个状态,网络决定现在要采取哪一个动作。演员网络和评论员网络的输入都是 s s s,所以它们前面几个层(layer)是可以共享的。

第二个技巧是我们需要探索的机制。在演员-评论员算法中,有一个常见的探索的方法是对 π {\pi} π输出的分布设置一个约束。这个约束用于使分布的熵(entropy)不要太小,也就是希望不同的动作被采用的概率平均一些。这样在测试的时候,智能体才会多尝试各种不同的动作,才会把环境探索得比较好,从而得到比较好的结果。
这里运用了两个技巧,第二个技巧在Actor网络的损失函数上加了个正则项,因此Actor损失函数变成了这样:
L
a
c
t
o
r
=
−
1
N
∑
n
=
1
N
∑
t
=
1
T
n
(
δ
t
n
⋅
log
π
θ
(
a
t
n
∣
s
t
n
)
)
−
λ
H
(
π
θ
)
L_{actor} = -\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} (\delta_t^n \cdot \log \pi_{\theta}(a_t^n|s_t^n)) - \lambda H(\pi_{\theta})
Lactor=−N1n=1∑Nt=1∑Tn(δtn⋅logπθ(atn∣stn))−λH(πθ)
∙ H ( π θ ) :策略的熵。 ∙ λ :控制探索的权重。 \begin{align*} & \bullet \ H(\pi_{\theta}) \text{:策略的熵。} \\ & \bullet \ \lambda \text{:控制探索的权重。} \end{align*} ∙ H(πθ):策略的熵。∙ λ:控制探索的权重。
代码中主要是以玩一个CartPole的游戏举例来实现的,游戏的具体玩法可以去网上搜到,大概就是一个平衡杆左右移动让杆子不倒的游戏。
import gym
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 配置类,集中管理所有超参数
class Config:
def __init__(self):
# 神经网络结构参数
self.hidden_dim = 128 # 共享网络隐藏层维度
self.actor_hidden_dim = 64 # Actor网络隐藏层维度
self.critic_hidden_dim = 64 # Critic网络隐藏层维度
# 算法训练参数
self.lr = 0.001 # 学习率
self.gamma = 0.99 # 折扣因子,用于计算未来奖励的现值
self.entropy_coef = 0.01 # 熵系数,用于鼓励探索
self.value_loss_coef = 0.5 # 价值损失系数,平衡actor和critic损失
# 训练过程参数
self.max_episodes = 5000 # 最大训练回合数
self.max_steps = 500 # 每个回合的最大步数
# 其他设置
self.seed = 42 # 随机种子,确保实验可重复性
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 设备选择
self.rewards_history = [] # 用于存储每个episode的reward
# Actor-Critic网络结构
class ActorCritic(nn.Module):
def __init__(self, state_dim, action_dim, config):
super(ActorCritic, self).__init__()
# 共享特征提取层
self.common = nn.Sequential(
nn.Linear(state_dim, config.hidden_dim), # 状态到特征的映射
nn.ReLU() # 非线性激活函数
)
# Actor网络:决策策略
self.actor = nn.Sequential(
nn.Linear(config.hidden_dim, action_dim), # 特征到动作概率的映射
nn.Softmax(dim=-1) # 输出动作概率分布
)
# Critic网络:状态价值评估
self.critic = nn.Sequential(
nn.Linear(config.hidden_dim, 1) # 特征到状态价值的映射
)
def forward(self, state):
common_out = self.common(state) # 提取共享特征
policy = self.actor(common_out) # 计算动作概率
value = self.critic(common_out) # 估计状态价值
return policy, value
# A2C算法实现
class A2C:
def __init__(self, state_dim, action_dim, config):
self.config = config
self.model = ActorCritic(state_dim, action_dim, config) # 创建神经网络
self.optimizer = optim.Adam(self.model.parameters(), lr=config.lr) # Adam优化器
self.gamma = config.gamma # 折扣因子
def compute_loss(self, states, actions, rewards, dones, next_states):
# 数据预处理:转换为PyTorch张量
states = torch.FloatTensor(states)
next_states = torch.FloatTensor(next_states)
actions = torch.LongTensor(actions)
rewards = torch.FloatTensor(rewards)
dones = torch.FloatTensor(dones)
# 获取当前状态的策略和价值估计
policy, values = self.model(states)
_, next_values = self.model(next_states)
# 计算TD目标和优势函数
# TD目标 = 即时奖励 + 折扣因子 * 下一状态的价值 * (1-终止标志)
td_targets = rewards + self.gamma * next_values.squeeze(1) * (1 - dones)
# 优势函数 = TD目标 - 当前状态的价值估计
advantages = td_targets - values.squeeze(1)
# 计算策略(Actor)损失
action_probs = policy.gather(1, actions.unsqueeze(1)).squeeze(1)
# 计算策略熵,用于鼓励探索
entropy = -(policy * torch.log(policy + 1e-10)).sum(1).mean()
# Actor损失 = -(log策略 * 优势函数) - 熵系数 * 熵
actor_loss = -(torch.log(action_probs) * advantages.detach()).mean() - \
self.config.entropy_coef * entropy
# Critic损失 = 优势函数的平方误差
critic_loss = self.config.value_loss_coef * advantages.pow(2).mean()
# 总损失 = Actor损失 + Critic损失
return actor_loss + critic_loss
def update(self, states, actions, rewards, dones, next_states):
loss = self.compute_loss(states, actions, rewards, dones, next_states)
self.optimizer.zero_grad() # 清除之前的梯度
loss.backward() # 反向传播计算梯度
self.optimizer.step() # 更新网络参数
# 训练主循环
def train_a2c():
config = Config() # 创建配置对象
# 设置随机种子确保可重复性
torch.manual_seed(config.seed)
np.random.seed(config.seed)
# 创建CartPole环境
env = gym.make("CartPole-v1")
state_dim = env.observation_space.shape[0] # 状态空间维度
action_dim = env.action_space.n # 动作空间维度
# 创建A2C智能体
agent = A2C(state_dim, action_dim, config)
# 训练循环
for episode in range(config.max_episodes):
state, _ = env.reset() # 重置环境
# 初始化回合数据存储
states, actions, rewards, next_states, dones = [], [], [], [], []
total_reward = 0 # 记录回合总奖励
episode_reward = 0 # 记录每个episode的reward
# 单回合交互循环
for step in range(config.max_steps):
# 将状态转换为张量并添加批次维度
state_tensor = torch.FloatTensor(state).unsqueeze(0)
# 获取动作概率分布
policy, _ = agent.model(state_tensor)
# 从概率分布中采样动作
action = torch.multinomial(policy, 1).item()
# 执行动作
next_state, reward, done, _, _ = env.step(action)
# 存储交互数据
states.append(state)
actions.append(action)
rewards.append(reward)
dones.append(done)
next_states.append(next_state)
total_reward += reward # 累积奖励
episode_reward += reward # 累积每个episode的reward
state = next_state # 更新状态
if done: # 如果回合结束,跳出循环
break
# 回合结束后更新模型
agent.update(states, actions, rewards, dones, next_states)
print(f"Episode {episode + 1}, Total Reward: {total_reward}")
# 记录每个episode的reward
config.rewards_history.append(episode_reward)
# 如果达到目标奖励,提前结束训练
if total_reward >= 500:
print("Solved!")
break
# 绘制训练奖励曲线
plot_rewards(config.rewards_history)
env.close() # 关闭环境
# 绘图函数
def plot_rewards(rewards):
plt.figure(figsize=(10,5))
plt.plot(rewards)
plt.title('Training Rewards')
plt.xlabel('Episode')
plt.ylabel('Reward')
plt.grid()
plt.show()
# 程序入口
if __name__ == "__main__":
train_a2c()
设置的reward到达500就提前结束训练,结果图:

由于平台字数限制,下文链接:https://blog.youkuaiyun.com/ooblack/article/details/144198573