Q-learning算法及其衍生算法Dyna-Q、DQN
- 时序差分原理
- Q-learning理论以及算法
-
- Q-Learning 收敛性证明
-
- 从贝尔曼期望角度的理论证明
- DQN解析收敛的理论证明
-
- 前置知识
-
E
[
F
t
(
x
,
a
)
∣
F
t
]
和
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
\\mathbb{E}[F_t(x,a) \\mid \\mathcal{F}_t]和var[F_t(x,a) \\mid \\mathcal{F}_t]
E[Ft(x,a)∣Ft]和var[Ft(x,a)∣Ft]的推理 -
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
var[F_t(x,a)|\\mathcal{F}_t]
var[Ft(x,a)∣Ft]的边界
- Q-Learning 算法
-
- 表格型Q-Learning 算法
-
- 无模型的表格型Q-Learning 算法
- 有模型的表格型Dyna-Q算法
- DQN算法
-
- CartPole车杆环境说明
- 神经网络Q的构建
- 代码实现
- 改进DQN算法一:Double DQN
- 改进DQN算法二:Dueling DQN
时序差分原理
当环境的奖励函数和状态转移函数未知时,我们需要直接使用和环境交互的过程中采样到的数据来学习。 根据状态价值函数的定义。我们可以分别列出当状态s被访问k次以及被访问k+1次时的状态价值函数。
V
k
(
s
)
=
1
k
∑
i
=
1
k
G
i
(1.1)
V_k(s)=\\frac{1}{k}\\sum_{i=1}^{k}G_i\\text{(1.1)}
Vk(s)=k1i=1∑kGi(1.1)
V
k
+
1
(
s
)
=
1
k
+
1
∑
i
=
1
k
+
1
G
i
(1.2)
V_{k+1}(s)=\\frac{1}{k+1}\\sum_{i=1}^{k+1}G_i\\text{(1.2)}
Vk+1(s)=k+11i=1∑k+1Gi(1.2) 由(1.1)(1.2)可知
V
k
+
1
(
s
)
=
1
k
+
1
(
∑
i
=
1
k
G
i
+
G
k
+
1
)
V_{k+1}(s)=\\frac{1}{k+1}(\\sum_{i=1}^{k}G_i+G_{k+1})
Vk+1(s)=k+11(i=1∑kGi+Gk+1)
=
1
k
+
1
(
k
V
k
(
s
)
+
G
k
+
1
)
=\\frac{1}{k+1}(kV_{k}(s)+G_{k+1})
=k+11(kVk(s)+Gk+1)
=
k
k
+
1
V
k
(
s
)
+
1
k
+
1
G
k
+
1
=\\frac{k}{k+1}V_{k}(s)+\\frac{1}{k+1}G_{k+1}
=k+1kVk(s)+k+11Gk+1
=
V
k
(
s
)
−
1
k
+
1
V
k
(
s
)
+
1
k
+
1
G
k
+
1
=V_k(s)-\\frac{1}{k+1}V_{k}(s)+\\frac{1}{k+1}G_{k+1}
=Vk(s)−k+11Vk(s)+k+11Gk+1
=
V
k
(
s
)
+
1
k
+
1
(
G
k
+
1
−
V
k
(
s
)
)
=V_k(s)+\\frac{1}{k+1}(G_{k+1}-V_{k}(s))
=Vk(s)+k+11(Gk+1−Vk(s)) 即:
V
k
+
1
(
s
)
=
V
k
(
s
)
+
1
k
+
1
(
G
k
+
1
−
V
k
(
s
)
)
V_{k+1}(s)=V_k(s)+\\frac{1}{k+1}(G_{k+1}-V_{k}(s))
Vk+1(s)=Vk(s)+k+11(Gk+1−Vk(s)) 这就是蒙特卡洛方法的原理,但在强化学习的实际应用中,为了让价值估计能适应非平稳的环境(比如策略在变化),我们通常不会严格地使用平均值(1/访问次数),而是用一个固定的步长
α
\\alpha
α,因此,蒙特卡洛方法更新公式表示如下:
V
k
+
1
(
s
)
=
V
k
(
s
)
+
α
(
G
k
+
1
−
V
k
(
s
)
)
(1.3)
V_{k+1}(s)=V_k(s)+\\alpha(G_{k+1}-V_{k}(s))\\text{(1.3)}
Vk+1(s)=Vk(s)+α(Gk+1−Vk(s))(1.3) 这本质上是用真实发生的完整回报,来逐步修正当前的价值估计。然而真实发生的完整回报需要等待整个序列结束之后才能计算得到。如何才能逐步更新价值函数呢?这就是时序差分方法。
V
π
(
s
)
=
E
π
[
G
t
∣
S
t
=
s
]
=
E
π
[
r
t
+
γ
V
π
(
S
t
+
1
)
∣
S
t
=
s
]
V_{\\pi}(s)=\\mathbb{E}_{\\pi}[G_t|S_t=s]=\\mathbb{E}_{\\pi}[r_t+\\gamma V_{\\pi}(S_{t+1})|S_t=s]
Vπ(s)=Eπ[Gt∣St=s]=Eπ[rt+γVπ(St+1)∣St=s] 因此,在状态s下,
V
k
+
1
(
s
)
=
V
k
(
s
)
+
α
(
r
t
+
γ
V
(
S
t
+
1
)
−
V
k
(
s
)
)
(1.4)
V_{k+1}(s)=V_k(s)+\\alpha(r_t+\\gamma V(S_{t+1})-V_{k}(s))\\text{(1.4)}
Vk+1(s)=Vk(s)+α(rt+γV(St+1)−Vk(s))(1.4) 这种只需利用该状态的奖励和下一状态价值估计与该状态价值之差来决定增量的该状态价值的更新,即称为时序差分。
Q-learning理论以及算法
由状态价值函数与动作-价值函数的关系,我们可将状态的价值函数扩展至动作-状态价值函数。我们将选取最大动作-价值函数值为下一个状态的动作为行为策略,则可得到一个动作-价值函数更新策略,称之为Q-learning更新,数学表示如下:
Q
t
+
1
(
s
t
,
a
t
)
=
Q
t
(
s
t
,
a
t
)
+
α
t
(
x
t
,
a
t
)
[
r
t
+
γ
max
a
Q
(
s
t
+
1
,
a
)
−
Q
(
s
t
,
a
t
)
]
(2.1)
Q_{t+1}(s_t,a_t)=Q_t(s_t,a_t)+\\alpha_t(x_t,a_t) [r_t+\\gamma \\max_{a}Q(s_{t+1},a)-Q(s_t,a_t)]\\text{(2.1)}
Qt+1(st,at)=Qt(st,at)+αt(xt,at)[rt+γamaxQ(st+1,a)−Q(st,at)](2.1)
Q-Learning 收敛性证明
从贝尔曼期望角度的理论证明
一、预备知识 1.1(最优)价值函数 我们将马尔可夫决策过程表示为一个元组
(
X
,
A
,
P
,
r
)
(X,A,P,r)
(X,A,P,r)。其中,X 是(有限的)状态空间;A是(有限的)动作空间;P表示转移概率;r表示奖励函数。 我们用 x 和 y 表示 X 中的元素,用 a 和 b 表示 A中的元素。我们考虑一般情形,即奖励定义在三元组 (x,a,y) 上,也就是说,r 是一个函数
r
:
X
×
A
×
X
→
R
r:X\\times A\\times X\\rightarrow \\mathbb{R}
r:X×A×X→R 每次因动作 a 从 x 转移到 y 时,赋予奖励 r(x,a,y)。我们允许r 是有界确定性函数。 对于一组控制序列
A
t
{A_t}
At,状态x的价值定义为:
J
(
x
,
A
t
)
=
E
[
∑
t
=
0
∞
y
t
R
(
X
t
,
A
t
)
∣
X
0
=
x
]
(2.2)
J(x,{A_t})=\\mathbb{E}[\\sum_{t=0}^{\\infty}y^{t}R(X_t,A_t)|X_0=x]\\text{(2.2)}
J(x,At)=E[t=0∑∞ytR(Xt,At)∣X0=x](2.2) 最优值函数定义为:
∀
x
∈
X
,
V
∗
(
x
)
=
max
A
t
J
\\forall x\\in X,V^{*}(x)=\\max_{A_t}J
∀x∈X,V∗(x)=maxAtJ(
x
x
x,{
A
t
A_t
At})
(2.3)
\\text{(2.3)}
(2.3) 并且满足每个状态都取到最优动作:
V
∗
(
x
)
=
max
a
∈
A
∑
y
∈
X
P
a
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
V
∗
(
y
)
]
(2.4)
V^{*}(x)=\\max_{a\\in A}\\sum_{y\\in X}P_a(x,y)[r(x,a,y)+\\gamma V^{*}(y)]\\text{(2.4)}
V∗(x)=a∈Amaxy∈X∑Pa(x,y)[r(x,a,y)+γV∗(y)](2.4) 由(2.3)我们可以定义最优Q函数
Q
∗
Q^{*}
Q∗
Q
∗
(
x
,
a
)
=
∑
y
∈
X
P
a
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
V
∗
(
y
)
]
(2.5)
Q^{*}(x,a)=\\sum_{y\\in X}P_a(x,y)[r(x,a,y)+\\gamma V^{*}(y)]\\text{(2.5)}
Q∗(x,a)=y∈X∑Pa(x,y)[r(x,a,y)+γV∗(y)](2.5)
1.2无穷范数的定义 对于任意一个函数f,其无穷范数
(
∞
−
范式
)
(\\infty-范式)
(∞−范式)定义为:在所有可能的输入下,函数值的绝对值的最大值。即:
∣
∣
f
∣
∣
∞
=
max
z
∈
定义域
∣
f
(
z
)
∣
||f||_{\\infty}=\\max_{z\\in 定义域}|f(z)|
∣∣f∣∣∞=z∈定义域max∣f(z)∣ 这里的 z 是函数f 的输入变量,遍历函数的整个定义域。
二、(最优)动作-状态函数Q 根据Q的贝尔曼期望方程,我们定义一个H算子在一般函数
q
:
X
×
A
→
R
q:X\\times A\\rightarrow \\mathbb{R}
q:X×A→R,形式为:
(
H
q
)
(
x
,
a
)
=
∑
y
∈
X
P
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
max
b
∈
A
q
(
y
,
b
)
]
(2.6)
(Hq)(x,a)=\\sum_{y\\in X}P(x,y)[r(x,a,y)+\\gamma \\max_{b\\in A} q(y,b)]\\text{(2.6)}
(Hq)(x,a)=y∈X∑P(x,y)[r(x,a,y)+γb∈Amaxq(y,b)](2.6) 根据无穷范式的定义可得:
∣
∣
H
q
1
(
x
,
a
)
−
H
q
2
(
x
,
a
)
∣
∣
∞
=
m
a
x
x
,
a
∣
∑
y
∈
X
P
a
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
max
b
∈
A
q
1
(
y
,
b
)
−
r
(
x
,
a
,
y
)
−
γ
max
b
∈
A
q
2
(
y
,
b
)
]
∣
||Hq_1(x,a)-Hq_2(x,a)||_{\\infty}=max_{x,a}|\\sum_{y\\in X}P_a(x,y)[r(x,a,y)+\\gamma \\max_{b\\in A} q_1(y,b)-r(x,a,y)-\\gamma \\max_{b\\in A} q_2(y,b)] |
∣∣Hq1(x,a)−Hq2(x,a)∣∣∞=maxx,a∣y∈X∑Pa(x,y)[r(x,a,y)+γb∈Amaxq1(y,b)−r(x,a,y)−γb∈Amaxq2(y,b)]∣ 消去
r
(
x
,
a
,
y
)
r(x,a,y)
r(x,a,y)可得:
=
m
a
x
x
,
a
γ
∣
∑
y
∈
X
P
a
(
x
,
y
)
[
max
b
∈
A
q
1
(
y
,
b
)
−
max
b
∈
A
q
2
(
y
,
b
)
]
∣
=max_{x,a}\\gamma |\\sum_{y\\in X}P_a(x,y)[\\max_{b\\in A} q_1(y,b)- \\max_{b\\in A} q_2(y,b)] |
=maxx,aγ∣y∈X∑Pa(x,y)[b∈Amaxq1(y,b)−b∈Amaxq2(y,b)]∣ 根据性质
∣
x
1
+
x
2
+
.
.
.
+
x
n
∣
≤
∣
x
1
∣
+
∣
x
2
∣
+
.
.
.
+
∣
x
n
∣
|x_1+x_2+…+x_n|\\le |x_1|+|x_2|+…+|x_n|
∣x1+x2+…+xn∣≤∣x1∣+∣x2∣+…+∣xn∣易得:
≤
m
a
x
x
,
a
γ
∑
y
∈
X
P
a
(
x
,
y
)
∣
max
b
∈
A
q
1
(
y
,
b
)
−
max
b
∈
A
q
2
(
y
,
b
)
∣
\\le max_{x,a}\\gamma \\sum_{y\\in X}P_a(x,y)|\\max_{b\\in A} q_1(y,b)- \\max_{b\\in A} q_2(y,b)|
≤maxx,aγy∈X∑Pa(x,y)∣b∈Amaxq1(y,b)−b∈Amaxq2(y,b)∣ 由于两个量各自的最大值之差,不会大于这两个量在所有点上的最大差异。,因此:
≤
m
a
x
x
,
a
γ
∑
y
∈
X
P
a
(
x
,
y
)
max
b
∈
A
∣
(
q
1
(
y
,
b
)
−
q
2
(
y
,
b
)
∣
\\le max_{x,a}\\gamma \\sum_{y\\in X}P_a(x,y)\\max_{b\\in A} |(q_1(y,b)- q_2(y,b)|
≤maxx,aγy∈X∑Pa(x,y)b∈Amax∣(q1(y,b)−q2(y,b)∣ 根据无穷范式的定义可以识别为:
≤
m
a
x
x
,
a
γ
∑
y
∈
X
P
a
(
x
,
y
)
∣
∣
q
1
−
q
2
∣
∣
∞
\\le max_{x,a}\\gamma \\sum_{y\\in X}P_a(x,y) ||q_1- q_2||_{\\infty}
≤maxx,aγy∈X∑Pa(x,y)∣∣q1−q2∣∣∞
由于
m
a
x
x
,
a
主要控制
P
a
(
x
,
y
)
中的a与x为使
γ
∑
y
∈
X
P
a
(
x
,
y
)
∣
∣
q
1
−
q
2
∣
∣
∞
最大的特定动作
a
和状态
x
,且在这个条件下,
∑
y
∈
X
P
a
(
x
,
y
)
=
1
,则:
\\text{由于}max_{x,a}\\text{主要控制}P_a(x,y)\\text{中的a与x为使}\\gamma \\sum_{y\\in X}P_a(x,y) ||q_1- q_2||_{\\infty}最大的特定动作a和状态x,且在这个条件下,\\sum_{y\\in X}P_a(x,y)=1,则:
由于maxx,a主要控制Pa(x,y)中的a与x为使γ∑y∈XPa(x,y)∣∣q1−q2∣∣∞最大的特定动作a和状态x,且在这个条件下,∑y∈XPa(x,y)=1,则:
=
γ
∣
∣
q
1
−
q
2
∣
∣
∞
=\\gamma ||q_1- q_2||_{\\infty}
=γ∣∣q1−q2∣∣∞
综上所述:
∣
∣
H
q
1
(
x
,
a
)
−
H
q
2
(
x
,
a
)
∣
∣
∞
≤
γ
∣
∣
q
1
−
q
2
∣
∣
∞
(2.7)
||Hq_1(x,a)-Hq_2(x,a)||_{\\infty}\\le \\gamma ||q_1- q_2||_{\\infty}\\text{(2.7)}
∣∣Hq1(x,a)−Hq2(x,a)∣∣∞≤γ∣∣q1−q2∣∣∞(2.7) 由此可知,贝尔曼算子
(
H
q
)
(
x
,
a
)
(Hq)(x,a)
(Hq)(x,a)是压缩算子。根据巴拿赫定理可知,Q的贝尔曼算子有且仅有一个不动点(不可迭代点),该点则是最优的Q函数。 在表格型 Q-learning 中,只要满足充分探索和适当学习率,Q-learning 几乎必然收敛到最优 Q 函数
Q
∗
Q^{*}
Q∗,收敛概率为 1。但在深度强化学习中,由于函数逼近、非凸优化等复杂因素,严格的收敛保证不再成立——这也是 DQN 需要众多稳定技巧的根源。接下来我们来探讨在深度 Q-learning(DQN)下的收敛情况。
DQN解析收敛的理论证明
前置知识
(1)对于任意随机变量Z,恒有:
V
a
r
(
Z
)
=
E
[
Z
2
]
−
(
E
[
Z
]
)
2
≤
E
[
Z
2
]
Var(Z)=\\mathbb{E}[Z^2]-(\\mathbb{E}[Z])^2\\le \\mathbb{E}[Z^2]
Var(Z)=E[Z2]−(E[Z])2≤E[Z2] 更进一步,由于我们是在有限状态和有限动作的MDP中,所有随机变量的取值都是有界的,所以方差不会超过该随机变量最大绝对值的平方:
V
a
r
[
Z
]
≤
m
a
x
[
Z
]
2
Var[Z]\\le max [Z]^2
Var[Z]≤max[Z]2 (2) 取值于
(
R
n
)
(\\mathbb{R}^n)
(Rn) 的随机过程
(
{
Δ
t
}
)
(\\{\\Delta_t\\})
({Δt}),定义为
Δ
t
+
1
(
x
)
=
(
1
−
α
t
(
x
)
)
Δ
t
(
x
)
+
α
t
(
x
)
F
t
(
x
)
\\Delta_{t+1}(x) = (1 – \\alpha_t(x))\\Delta_t(x) + \\alpha_t(x)F_t(x)
Δt+1(x)=(1−αt(x))Δt(x)+αt(x)Ft(x) 在以假设下以概率 1 收敛到零:
(
∙
)
(
0
≤
α
t
≤
1
,
∑
t
∞
α
t
(
x
)
=
∞
)
且
(
∑
t
∞
α
t
2
(
x
)
<
∞
)
;
(\\bullet) (0 \\le \\alpha_t \\le 1, \\sum_t^{\\infty} \\alpha_t(x) = \\infty) 且 (\\sum_t^{\\infty} \\alpha_t^2(x) < \\infty);
(∙)(0≤αt≤1,∑t∞αt(x)=∞)且(∑t∞αt2(x)<∞); 其中,
∑
t
∞
α
t
(
x
)
=
∞
)
且
(
∑
t
∞
α
t
2
(
x
)
<
∞
)
\\sum_t^{\\infty} \\alpha_t(x) = \\infty) 且 (\\sum_t^{\\infty} \\alpha_t^2(x) < \\infty)
∑t∞αt(x)=∞)且(∑t∞αt2(x)<∞)意味着增量步长之和不收敛,而增量步长的平方之和收敛。在数学上,若
α
t
(
x
)
=
1
t
,则满足要求
\\alpha_t(x)=\\frac{1}{t},则满足要求
αt(x)=t1,则满足要求;若
α
t
(
x
)
=
1
t
2
,则不满足要求。
\\alpha_t(x)=\\frac{1}{t^2},则不满足要求。
αt(x)=t21,则不满足要求。。在算法上,步长之和发散保证了步长不会衰减得太快。而衰减过快则导致增量趋近0;步长平方之和收敛确保了随机噪声(方差)带来的累积波动是有限的。如果步长平方和无穷大,即使收敛了也会因为噪声而永远大幅震荡,无法稳定。
(
∙
)
(
∣
E
[
F
t
(
x
)
∣
F
t
]
∣
W
≤
γ
∣
Δ
t
∥
W
)
,其中
(
γ
<
1
)
(\\bullet) (|\\mathbb{E}[F_t(x) \\mid \\mathcal{F}_t]|_W \\le \\gamma |\\Delta_t\\|_W),其中 (\\gamma < 1)
(∙)(∣E[Ft(x)∣Ft]∣W≤γ∣Δt∥W),其中(γ<1);
(
∙
)
(
var
[
F
t
(
x
)
∣
F
t
]
≤
C
(
1
+
∣
Δ
t
∥
W
2
)
)
,其中
(
C
>
0
)
(\\bullet) (\\text{var}[F_t(x) \\mid \\mathcal{F}_t] \\le C(1 + |\\Delta_t\\|_W^2)),其中 (C > 0)
(∙)(var[Ft(x)∣Ft]≤C(1+∣Δt∥W2)),其中(C>0) (注:
F
t
\\mathcal{F}_t
Ft为过往事数据)
E
[
F
t
(
x
,
a
)
∣
F
t
]
和
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
\\mathbb{E}[F_t(x,a) \\mid \\mathcal{F}_t]和var[F_t(x,a) \\mid \\mathcal{F}_t]
E[Ft(x,a)∣Ft]和var[Ft(x,a)∣Ft]的推理
深度 Q-learning(DQN)即是用函数拟合的方法来估计
Q
Q
Q值。 我们先将(2.1)重写成
Q
t
+
1
(
x
t
,
a
t
)
=
(
1
−
α
t
(
x
t
,
a
t
)
)
Q
t
(
x
t
,
a
t
)
+
α
t
(
x
t
,
a
t
)
[
r
t
+
γ
max
b
∈
A
Q
t
(
x
t
+
1
,
b
)
]
(2.8)
Q_{t+1}(x_t,a_t)=(1-\\alpha_t(x_t,a_t))Q_t(x_t,a_t)+\\alpha_t(x_t,a_t)[r_t+\\gamma \\max_{b\\in A}Q_t(x_{t+1},b)]\\text{(2.8)}
Qt+1(xt,at)=(1−αt(xt,at))Qt(xt,at)+αt(xt,at)[rt+γb∈AmaxQt(xt+1,b)](2.8)
从等式两边减去
Q
∗
(
x
t
,
a
t
)
Q^{*}(x_t,a_t)
Q∗(xt,at),并令
△
t
(
x
,
a
)
=
Q
t
(
x
,
a
)
−
Q
∗
(
x
,
a
)
\\triangle_t(x,a)=Q_t(x,a)-Q^{*}(x,a)
△t(x,a)=Qt(x,a)−Q∗(x,a) 得到
△
t
+
1
(
x
t
,
a
t
)
=
(
1
−
α
t
(
x
t
,
a
t
)
)
△
t
(
x
t
,
a
t
)
+
α
t
(
x
t
,
a
t
)
[
r
t
+
γ
max
b
∈
A
Q
t
(
x
t
+
1
,
b
)
−
Q
∗
(
x
t
,
a
t
)
]
(2.9)
\\triangle_{t+1}(x_t,a_t)=(1-\\alpha_t(x_t,a_t))\\triangle_t(x_t,a_t)+\\alpha_t(x_t,a_t)[r_t+\\gamma \\max_{b\\in A}Q_t(x_{t+1},b)-Q^{*}(x_t,a_t)]\\text{(2.9)}
△t+1(xt,at)=(1−αt(xt,at))△t(xt,at)+αt(xt,at)[rt+γb∈AmaxQt(xt+1,b)−Q∗(xt,at)](2.9) 我们另
F
t
(
x
,
a
)
=
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
−
Q
∗
(
x
,
a
)
F_t(x,a)=r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)-Q^{*}(x,a)
Ft(x,a)=r(x,a,X(x,a))+γb∈AmaxQ(x,b)−Q∗(x,a) 其中
X
(
x
,
a
)
X(x,a)
X(x,a)是从马尔可夫链
(
X
,
P
a
)
(X,P_a)
(X,Pa) 中获得的随机样本状态,那么我们有
E
[
F
t
(
x
,
a
)
]
=
∑
y
∈
X
P
a
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
−
Q
∗
(
x
,
a
)
]
\\mathbb{E}[F_t(x,a)]=\\sum_{y\\in X}P_a(x,y)[r(x,a,y)+\\gamma \\max_{b\\in A}Q(x,b)-Q^{*}(x,a)]
E[Ft(x,a)]=y∈X∑Pa(x,y)[r(x,a,y)+γb∈AmaxQ(x,b)−Q∗(x,a)] 由于
Q
∗
Q^{*}
Q∗是不动点,因此
Q
∗
=
H
Q
∗
Q^{*}=HQ^{*}
Q∗=HQ∗,得到:
E
[
F
t
(
x
,
a
)
∣
F
t
]
=
(
H
Q
t
)
(
x
,
a
)
−
H
Q
∗
(
x
,
a
)
\\mathbb{E}[F_t(x,a)|\\mathcal{F}_t]=(HQ_t)(x,a)-HQ^{*}(x,a)
E[Ft(x,a)∣Ft]=(HQt)(x,a)−HQ∗(x,a) 由(2.7)可推出:
∣
∣
E
[
F
t
(
x
,
a
)
∣
F
t
]
∣
∞
≤
γ
∣
∣
Q
t
−
Q
∗
∣
∣
∞
=
γ
∣
∣
△
t
∣
∣
∞
(2.8)
||\\mathbb{E}[F_t(x,a)|\\mathcal{F}_t]|_{\\infty}\\le \\gamma ||Q_t-Q^{*}||_{\\infty}=\\gamma ||\\triangle_t||_{\\infty}\\text{(2.8)}
∣∣E[Ft(x,a)∣Ft]∣∞≤γ∣∣Qt−Q∗∣∣∞=γ∣∣△t∣∣∞(2.8) 最后,根据
v
a
r
(
x
)
=
E
[
(
X
−
E
X
)
2
]
var(x)=E[(X-EX)^2]
var(x)=E[(X−EX)2]
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
=
E
[
(
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
−
Q
∗
(
x
,
a
)
−
(
H
Q
t
)
(
x
,
a
)
+
Q
∗
(
x
,
a
)
)
2
∣
F
t
]
var[F_t(x,a)|\\mathcal{F}_t]=\\mathbb{E}[(r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)-Q^{*}(x,a)-(HQ_t)(x,a)+Q^{*}(x,a))^{2}|\\mathcal{F}_t]
var[Ft(x,a)∣Ft]=E[(r(x,a,X(x,a))+γb∈AmaxQ(x,b)−Q∗(x,a)−(HQt)(x,a)+Q∗(x,a))2∣Ft] 化简可得:
=
E
[
(
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
−
(
H
Q
t
)
(
x
,
a
)
)
2
∣
F
t
]
=\\mathbb{E}[(r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)-(HQ_t)(x,a))^2|\\mathcal{F}_t]
=E[(r(x,a,X(x,a))+γb∈AmaxQ(x,b)−(HQt)(x,a))2∣Ft] 由于
E
[
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
F
t
]
=
∑
y
∈
X
P
a
(
x
,
y
)
[
r
(
x
,
a
,
y
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
]
=
H
Q
(
x
,
a
)
\\mathbb{E}[r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|\\mathcal{F}_t]=\\sum_{y\\in X}P_a(x,y)[r(x,a,y)+\\gamma \\max_{b\\in A}Q(x,b)]=HQ(x,a)
E[r(x,a,X(x,a))+γmaxb∈AQ(x,b)∣Ft]=∑y∈XPa(x,y)[r(x,a,y)+γmaxb∈AQ(x,b)]=HQ(x,a),因此进一步可得:
v
a
r
[
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
F
t
]
var[r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|\\mathcal{F}_t]
var[r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣Ft] 综上,
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
=
v
a
r
[
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
F
t
]
(2.10)
var[F_t(x,a)|\\mathcal{F}_t]=var[r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|\\mathcal{F}_t]\\text{(2.10)}
var[Ft(x,a)∣Ft]=var[r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣Ft](2.10)
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
var[F_t(x,a)|\\mathcal{F}_t]
var[Ft(x,a)∣Ft]的边界
已知: (1)奖励函数r有界,设
∣
r
∣
<
R
|r|<R
∣r∣<R(R是某个正常数) (2)最优Q函数
Q
∗
Q^*
Q∗有界。因为奖励有界且折扣因子
γ
\\gamma
γ小于1,所以
∣
Q
∗
∣
≤
R
1
−
γ
|Q^*|\\le \\frac{R}{1-\\gamma}
∣Q∗∣≤1−γR证明如下: 由于
γ
<
1
且
∣
r
∣
≤
R
\\gamma<1且|r|\\le R
γ<1且∣r∣≤R,则:
∣
Q
∗
≤
R
+
γ
R
+
γ
2
R
+
γ
3
R
+
.
.
.
=
R
⋅
∑
i
=
1
∞
1
−
γ
i
1
−
γ
=
R
⋅
1
1
−
γ
=
R
1
−
γ
|Q^*\\le R+\\gamma R+\\gamma^2 R+\\gamma^3 R+…=R\\cdot \\sum_{i=1}^{\\infty}\\frac{1-\\gamma^i}{1-\\gamma}=R\\cdot\\frac{1}{1-\\gamma}=\\frac{R}{1-\\gamma}
∣Q∗≤R+γR+γ2R+γ3R+…=R⋅i=1∑∞1−γ1−γi=R⋅1−γ1=1−γR (3)当前的估计误差为
△
t
=
Q
t
−
Q
∗
\\triangle_t=Q_t-Q^{*}
△t=Qt−Q∗,所以
Q
t
(
x
,
a
)
=
△
t
(
x
,
a
)
+
Q
∗
(
x
,
a
)
Q_t(x,a)=\\triangle_t(x,a)+Q^{*}(x,a)
Qt(x,a)=△t(x,a)+Q∗(x,a) 于是我们可以估计:
∣
max
b
Q
t
(
y
,
b
)
∣
≤
∣
max
b
Q
∗
(
y
,
b
)
∣
≤
max
b
∣
(
Q
∗
(
y
,
b
)
∣
+
∣
△
t
(
y
,
b
)
∣
)
|\\max_{b}Q_t(y,b)|\\le |\\max_{b}Q^*(y,b)|\\le \\max_{b}|(Q^*(y,b)|+|\\triangle_t(y,b)|)
∣bmaxQt(y,b)∣≤∣bmaxQ∗(y,b)∣≤bmax∣(Q∗(y,b)∣+∣△t(y,b)∣) 由于
∣
∣
△
t
∣
∣
||\\triangle_t||
∣∣△t∣∣表示所有状态-动作对中误差的最大值(或加权范数),所以:
∣
max
b
Q
t
(
y
,
b
)
∣
≤
R
1
−
γ
+
∣
∣
△
t
∣
∣
|\\max_{b}Q_t(y,b)|\\le \\frac{R}{1-\\gamma}+||\\triangle_t||
∣bmaxQt(y,b)∣≤1−γR+∣∣△t∣∣ 进而,
∣
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
≤
R
+
γ
(
R
1
−
γ
+
∣
∣
△
t
∣
∣
)
|r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|\\le R+\\gamma (\\frac{R}{1-\\gamma}+||\\triangle_t||)
∣r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣≤R+γ(1−γR+∣∣△t∣∣) 由于r 有界,上式显然满足
v
a
r
[
F
t
(
x
,
a
)
∣
F
t
]
≤
C
(
1
+
∣
∣
△
t
∣
∣
W
2
)
var[F_t(x,a)|\\mathcal{F}_t]\\le C(1+||\\triangle_t||^2_W)
var[Ft(x,a)∣Ft]≤C(1+∣∣△t∣∣W2) 整理常数项:
R
+
γ
R
1
−
γ
=
R
1
−
γ
R+\\frac{\\gamma R}{1-\\gamma}=\\frac{ R}{1-\\gamma}
R+1−γγR=1−γR 所以:
∣
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
≤
R
1
−
γ
+
γ
∣
∣
△
t
∣
∣
|r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|\\le \\frac{ R}{1-\\gamma}+\\gamma ||\\triangle_t||
∣r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣≤1−γR+γ∣∣△t∣∣ 则:
∣
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
2
≤
(
R
1
−
γ
+
γ
∣
∣
△
t
∣
∣
)
2
|r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|^2 \\le (\\frac{ R}{1-\\gamma}+\\gamma ||\\triangle_t||)^2
∣r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣2≤(1−γR+γ∣∣△t∣∣)2 由柯西-施瓦茨的推论
(
u
+
v
)
2
≤
2
u
2
+
2
v
2
(u+v)^2\\le 2u^2+2v^2
(u+v)2≤2u2+2v2得:
∣
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
2
≤
2
(
R
1
−
γ
)
2
+
2
γ
2
(
∣
∣
△
t
∣
∣
)
2
|r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|^2 \\le 2(\\frac{ R}{1-\\gamma})^2+2\\gamma^2( ||\\triangle_t||)^2
∣r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣2≤2(1−γR)2+2γ2(∣∣△t∣∣)2 令:
C
=
2
(
R
1
−
γ
)
2
+
2
γ
2
C= 2(\\frac{ R}{1-\\gamma})^2+2\\gamma^2
C=2(1−γR)2+2γ2 则显然有:
∣
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
∣
2
≤
C
(
1
+
∣
∣
△
t
∣
∣
2
)
|r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)|^2 \\le C(1+||\\triangle_t||^2)
∣r(x,a,X(x,a))+γb∈AmaxQ(x,b)∣2≤C(1+∣∣△t∣∣2) 又由前置知识可得:
V
a
r
[
F
t
]
=
V
a
r
[
r
(
x
,
a
,
X
(
x
,
a
)
)
+
γ
max
b
∈
A
Q
(
x
,
b
)
]
≤
C
(
1
+
∣
∣
△
t
∣
∣
2
)
(2.9)
Var[F_t]=Var[r(x,a,X(x,a))+\\gamma \\max_{b\\in A}Q(x,b)]\\le C(1+||\\triangle_t||^2)\\text{(2.9)}
Var[Ft]=Var[r(x,a,X(x,a))+γb∈AmaxQ(x,b)]≤C(1+∣∣△t∣∣2)(2.9)
基于前置知识2,式子(2.8)(2.9)可知,在算法上若能依据前置知识2中对
α
\\alpha
α要求进行设置,则网络
△
t
=
Q
t
−
Q
∗
\\triangle_t=Q_t-Q^{*}
△t=Qt−Q∗在拟合过程中以概率1收敛至0。也就是说:在利用Q-learning规则构造Q网络在拟合过程中以概率1收敛到最优
Q
∗
Q^*
Q∗。
Q-Learning 算法
表格型Q-Learning 算法
无模型的表格型Q-Learning 算法
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm # tqdm是显示循环进度条的库
class CliffWalkingEnv:
def __init__(self, ncol, nrow):
self.nrow = nrow
self.ncol = ncol
self.x = 0 # 记录当前智能体位置的横坐标
self.y = self.nrow – 1 # 记录当前智能体位置的纵坐标
def step(self, action): # 外部调用这个函数来改变当前位置
# 4种动作, change[0]:上, change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
# 定义在左上角
change = [[0, –1], [0, 1], [–1, 0], [1, 0]]
self.x = min(self.ncol – 1, max(0, self.x + change[action][0]))
self.y = min(self.nrow – 1, max(0, self.y + change[action][1]))
next_state = self.y * self.ncol + self.x
reward = –1
done = False
if self.y == self.nrow – 1 and self.x > 0: # 下一个位置在悬崖或者目标
done = True
if self.x != self.ncol – 1:
reward = –100
return next_state, reward, done
def reset(self): # 回归初始状态,坐标轴原点在左上角
self.x = 0
self.y = self.nrow – 1
return self.y * self.ncol + self.x
class QLearning:
""" Q-learning算法 """
def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
self.Q_table = np.zeros([nrow * ncol, n_action]) # 初始化Q(s,a)表格
self.n_action = n_action # 动作个数
self.alpha = alpha # 学习率
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略中的参数
def take_action(self, state): #选取下一步的操作
if np.random.random() < self.epsilon:
action = np.random.randint(self.n_action)
else:
action = np.argmax(self.Q_table[state])
return action
def best_action(self, state): # 用于打印策略
Q_max = np.max(self.Q_table[state])
a = [0 for _ in range(self.n_action)]
for i in range(self.n_action):
if self.Q_table[state, i] == Q_max:
a[i] = 1
return a
def update(self, s0, a0, r, s1):
td_error = r + self.gamma * self.Q_table[s1].max(
) – self.Q_table[s0, a0]
self.Q_table[s0, a0] += self.alpha * td_error
np.random.seed(0)
epsilon = 0.1
alpha = 0.1
gamma = 0.9
agent = QLearning(ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500 # 智能体在环境中运行的序列的数量
return_list = [] # 记录每一条序列的回报
for i in range(10): # 显示10个进度条
# tqdm的进度条功能
#
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)): # 每个进度条的序列数
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done = env.step(action)
episode_return += reward # 这里回报的计算不进行折扣因子衰减
agent.update(state, action, reward, next_state)
state = next_state
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0: # 每10条序列打印一下这10条序列的平均回报
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[–10:])
})
pbar.update(1)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Q-learning on {}'.format('Cliff Walking'))
plt.show()
action_meaning = ['^', 'v', '<', '>']
print('Q-learning算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
最终结果如下:

可以看出无模型的表格型Q-Learning 算法确实可以在宏观上得到收敛,但是我们也不难发现,其收敛步数较大且在收敛过程中震荡很大。仔细研究算法可以发现,在Q更新的增长量中,我们较大程度在依赖已知Q表格中取得的最大的动作。但已知Q表格本身就是探索不足的,因此,max 操作选出来的“最优动作”可能只是“已知范围内最好的”,而不是“真正最好的”。当智能体偶然发现一个更好的动作时,Q 值会剧烈修正,导致曲线震荡。这种震荡不是因为算法有 bug,而是因为 Q-Learning 在信息不足的情况下被迫做出贪婪选择。有什么办法可以尽可能减少这种震荡呢?我们观察局部图不难看出,在两点符合更新更新趋势中间,都是处于盲目探索的点(后面称之为废点集),是否可以根据两点中前面一点所得到的Q值模拟出环境,在已知的环境上进行Q-learning更新后,于第二点模拟出更符合实际环境的环境模型。这就是接下来的Dyna-Q。由于无模型的表格型Q-Learning 算法总是取一个在take_action确定的动作,因此,我们可以利用字典记录(s, a) → (r, s’)来作为环境模型。
有模型的表格型Dyna-Q算法

class DynaQ:
""" Dyna-Q算法 """
def __init__(self,
ncol,
nrow,
epsilon,
alpha,
gamma,
n_planning,
n_action=4):
self.Q_table = np.zeros([nrow * ncol, n_action]) # 初始化Q(s,a)表格
self.n_action = n_action # 动作个数
self.alpha = alpha # 学习率
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略中的参数
self.n_planning = n_planning #执行Q-planning的次数, 对应1次Q-learning
self.model = dict() # 环境模型
def take_action(self, state): # 选取下一步的操作
if np.random.random() < self.epsilon:
action = np.random.randint(self.n_action)
else:
action = np.argmax(self.Q_table[state])
return action
def q_learning(self, s0, a0, r, s1):
td_error = r + self.gamma * self.Q_table[s1].max(
) – self.Q_table[s0, a0]
self.Q_table[s0, a0] += self.alpha * td_error
def update(self, s0, a0, r, s1):
self.q_learning(s0, a0, r, s1)
self.model[(s0, a0)] = r, s1 # 将数据添加到模型中
for _ in range(self.n_planning): # Q-planning循环
# 随机选择曾经遇到过的状态动作对
(s, a), (r, s_) = random.choice(list(self.model.items()))
self.q_learning(s, a, r, s_)
def DynaQ_CliffWalking(n_planning):
ncol = 12
nrow = 4
env = CliffWalkingEnv(ncol, nrow)
epsilon = 0.01
alpha = 0.1
gamma = 0.9
agent = DynaQ(ncol, nrow, epsilon, alpha, gamma, n_planning)
num_episodes = 300 # 智能体在环境中运行多少条序列
return_list = [] # 记录每一条序列的回报
for i in range(10): # 显示10个进度条
# tqdm的进度条功能
with tqdm(total=int(num_episodes / 10),
desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)): # 每个进度条的序列数
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done = env.step(action)
episode_return += reward # 这里回报的计算不进行折扣因子衰减
agent.update(state, action, reward, next_state)
state = next_state
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0: # 每10条序列打印一下这10条序列的平均回报
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[–10:])
})
pbar.update(1)
return return_list
np.random.seed(0)
random.seed(0)
n_planning_list = [0, 2, 20]
for n_planning in n_planning_list:
print('Q-planning步数为:%d' % n_planning)
time.sleep(0.5)
return_list = DynaQ_CliffWalking(n_planning)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list,
return_list,
label=str(n_planning) + ' planning steps')
plt.legend()
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Dyna-Q on {}'.format('Cliff Walking'))
plt.show()
由算法不难看出,Dyna-Q 的精髓在于:把真实环境交互中获得的经验存储为模型,然后用这个模型生成模拟数据,在后台反复做 Q-learning(Q-planning),从而在不需要额外真实交互(真的的交互必然导致return的负增加)的情况下加速学习。换种说法,Dyna-Q贮存先验知识并以此构建一个已知的虚拟环境,在虚拟环境中利用先验知识先进行采样来更新Q值(本质是复制一套物理规则),在虚拟环境中并不增加return(因此,episode_return += reward 必须在agent.update(state, action, reward, next_state)之前)。这本质上是在节省在无模型的表格型Q-Learning 算法中两点间的废点集。 关注DynaQ.update中(s, a), (r, s_) = random.choice(list(self.model.items()))的在虚拟环境中的随机更新设计。在MDP中除第一点Q外,任意一点的Q值都包含前面序列的信息,因此字典存贮的相邻信息间的时间相关性较强。则如果按存储顺序更新,等于在重复一遍带“废点”的探索老路,价值信息沿着轨迹缓慢传播,收敛很慢。 假设智能体先验顺序为: 起点A → B → C → D → E → F → G → H → I → 终点J 如果按存储顺序进行 Q-planning,更新顺序是: 第1次:更新 (A,右) → 用到 (B) 的 Q 值,此时 (B) 的 Q 值还是 0 第2次:更新 (B,右) → 用到 © 的 Q 值,此时 © 的 Q 值还是 0 第3次:更新 (C,右) → … … 第9次:更新 (I,右) → 用到 (J) 的 Q 值,此时 (J) 的 Q 值已经是高奖励 只有当更新进行到第 9 次时,终点的高奖励才开始被反向传播。前面 8 次更新都在“废点集”上做无用功——这些状态-动作对本身没有高奖励,它们的 Q 值也不会因为这次更新而有实质提升。 如果随机抽取,情况完全不同: 第1次:随机抽到 (I, 右) → 更新 Q(I, 右),此时已经用到了 J 的高奖励 第2次:随机抽到 (H, 右) → 更新 Q(H, 右),此时 I 的 Q 值已经被更新过了 第3次:随机抽到 (G, 右) → 更新 Q(G, 右),此时 H 的 Q 值已经被更新过了 … 总而言之,按顺序更新之所以慢,是因为 TD 误差的反向传播被束缚在轨迹的时间顺序上——高奖励必须一步步“爬过”废点集才能传递到远处的状态。随机抽取打破了这种时序依赖,让价值信息可以在状态间直接跳跃传播,从而大幅减少废点集带来的传播延迟。这正是 Dyna-Q 用随机采样加速收敛的根本原因。
这里还存在一个问题:对字典逆序取高奖励可以沿着轨迹直接反向传播,一轮就传到底。但为什么在实际算法中我们选择随机取,而不是逆序取?原因在于逆序取依赖于一个在实际中无法满足的假设。(1)轨迹是完整的:从起点到终点,每一步的顺序都已知。(2)轨迹是单一的:只有一条轨迹,不存在多条轨迹的交错。(3)终点是明确的:高奖励只在终点,且终点位置已知。 在 Dyna-Q 的 Q-planning 中,模型存储的是 (s, a) → (r, s’) 的映射,它不保留轨迹信息。我们无法知道哪些 (s, a) 属于同一条轨迹,也无法知道哪条经验是“终点”。 简要说明如下: 智能体通常会生成多条不同轨迹,如: 轨迹1:起点A → B → C → 终点D(高奖励) 轨迹2:起点A → E → F → 终点D(高奖励) 轨迹3:起点A → B → G → 撞墙(负奖励) 这些轨迹在字典中交织在一起: 字典 = { (A,→): (B, 0), # 来自轨迹1和3 (B,→): (C, 0), # 来自轨迹1 (F,↓):(D, +100), # 来自轨迹3 (C,→): (D, +100), # 来自轨迹1 (A,↑): (E, 0), # 来自轨迹2 (E,→): (F, 0), # 来自轨迹2 (B,→):(G, -10) # 来自轨迹2 } 若逆序取则开始就是负奖励,只会对Q更新帮倒忙。 而随机取不仅可以避免这些问题,而且所有经验都被公平对待,高奖励和惩罚都能快速传播,不会因为轨迹顺序而产生偏见。从随机逼近理论的角度看,随机采样使得更新序列满足马尔可夫链的遍历性——每个状态-动作对都有非零概率被无限次访问。这是 Q-learning 收敛到最优解的必要条件。
结果如下:

可以注意到Dyna-Q相较于无模型的表格型Q-Learning而言,最终收敛后的return值相近,前置明显收敛更快且震荡更小,说明对Dyna-Q和无模型的表格型Q-Learning分析正确。
这种用表格存储动作价值的做法只在环境的状态和动作都是离散的,并且空间都比较小的情况下适用,我们之前进行代码实战的几个环境都是如此(如悬崖漫步)。当状态或者动作数量非常大的时候,这种做法就不适用了。 对于这种情况,我们需要用函数拟合的方法来估计值,即将这个复杂的值表格视作数据,使用一个参数化的函数来拟合这些数据。很显然,这种函数拟合的方法存在一定的精度损失,因此被称为近似方法。
DQN算法
CartPole车杆环境说明
状态值就是连续的,动作值是离散的。在车杆环境中,有一辆小车,智能体的任务是通过左右移动保持车上的杆竖直,若杆的倾斜度数过大,或者车子离初始位置左右的偏离程度过大,或者坚持时间到达 200 帧,则游戏结束。智能体的状态是一个维数为 4 的向量,每一维都是连续的,其动作是离散的,动作空间大小为 2,详情参见表 7-1 和表 7-2。在游戏中每坚持一帧,智能体能获得分数为 1 的奖励,坚持时间越长,则最后的分数越高,坚持 200 帧即可获得最高的分数。 在标准环境中,Pendulum(倒立摆)是一个稀疏且为负值的奖励环境,最优解为0。你的DQN图像中Q值围绕0附近震荡是正常的。
神经网络Q的构建

由图像显示,我们最终需要构建的是S-A的Q函数。 一、 Q 网络的损失函数
我们先来回顾一下 Q-learning 的更新规则
Q
(
s
,
a
)
←
Q
(
s
,
a
)
+
α
[
r
+
γ
max
a
′
∈
A
Q
(
s
′
,
a
′
)
−
Q
(
s
,
a
)
]
Q(s, a) \\leftarrow Q(s, a) + \\alpha \\left[ r + \\gamma \\max_{a' \\in \\mathcal{A}} Q(s', a') – Q(s, a) \\right]
Q(s,a)←Q(s,a)+α[r+γa′∈AmaxQ(s′,a′)−Q(s,a)] 其中更新差量取决于
r
+
γ
max
a
′
∈
A
Q
(
s
′
,
a
′
)
−
Q
(
s
,
a
)
r + \\gamma \\max_{a' \\in \\mathcal{A}} Q(s', a') – Q(s, a)
r+γmaxa′∈AQ(s′,a′)−Q(s,a)也就是要使得
Q
(
s
,
a
)
→
r
+
γ
max
a
′
∈
A
Q
(
s
′
,
a
′
)
Q(s,a)\\rightarrow r + \\gamma \\max_{a' \\in \\mathcal{A}} Q(s', a')
Q(s,a)→r+γmaxa′∈AQ(s′,a′),但二者相等时,Q不会有增量,此时处于唯一不动点
Q
∗
Q^*
Q∗,我们可以很自然地将 Q 网络的损失函数构造为均方误差的形式:
ω
∗
=
arg
min
ω
1
2
N
∑
i
=
1
N
[
Q
ω
(
s
i
,
a
i
)
−
(
r
i
+
γ
max
a
′
Q
ω
(
s
i
′
,
a
′
)
)
]
2
\\omega^* = \\arg \\min_{\\omega} \\frac{1}{2N} \\sum_{i=1}^{N} \\left[ Q_{\\omega}(s_i, a_i) – \\left( r_i + \\gamma \\max_{a'} Q_{\\omega}(s'_i, a') \\right) \\right]^2
ω∗=argωmin2N1i=1∑N[Qω(si,ai)−(ri+γa′maxQω(si′,a′))]2 至此,我们就可以将 Q-learning 扩展到神经网络形式——深度 Q 网络(deep Q network,DQN)算法。由于 DQN 是离线策略算法,因此我们在收集数据的时候可以使用一个-贪婪策略来平衡探索与利用,将收集到的数据存储起来,在后续的训练中使用。DQN 中还有两个非常重要的模块——经验回放和目标网络,它们能够帮助 DQN 取得稳定、出色的性能。
二、经验回放 在一般的有监督学习中,假设训练数据是独立同分布的(由前面分析我们可知具有相关性数据在数据拟合中的危害),我们每次训练神经网络的时候从训练数据中随机采样一个或若干个数据来进行梯度下降,随着学习的不断进行,探索的数据越来越准确且每一个训练数据会被使用多次。这是否有些熟悉?没错这正和前文所提的Dyna-Q算法中随机更新策略相似。但二者还是有区别的。 Dyna-Q字典就是一个确定性的环境模型。它不是在记录某一次具体的经历,而是在归纳一条普适的物理规则:只要我在这个状态下执行这个动作,就一定会得到这个奖励,并转移到那个状态。所以,Dyna-Q 的“模拟”是在后台凭空运行另一套物理世界。它可以随意抽取一个状态-动作对,让这个“物理规则”生成下一步的数据来更新自己,而不需要智能体真的去做这个动作。这和确定性策略、随机策略无关,只和环境本身的确定性有关。换句话说,每一次的Q-planing相当于在局部环境下进行Q-learning更新Q,这种策略得以实现是因为最优Q是唯一的不动点。 回放缓冲区里存的,是曾经真实发生过的、带有各种偶然性的历史片段。它不是一个可以查询的规则。因此,DQN 的“模拟”不是在设计新规则,而是在反复复习旧历史。它只是在重放和打乱智能体过去真实的行为轨迹和结果。 三、目标网络与训练网络 由于 TD 误差目标本身就包含神经网络的输出,因此在更新网络参数的同时目标也在不断地改变,这非常容易造成神经网络训练的不稳定性。为了解决这一问题,DQN 便使用了目标网络(target network)的思想:既然训练过程中 Q 网络的不断更新会导致目标不断发生改变,不如暂时先将 TD 目标中的 Q 网络固定住。为了实现这一思想,我们需要利用两套 Q 网络。 (1)原来的训练网络
Q
w
(
s
,
a
)
Q_w(s,a)
Qw(s,a),用于计算原来的损失函数中
arg
min
ω
1
2
N
∑
i
=
1
N
[
Q
ω
(
s
i
,
a
i
)
−
(
r
i
+
γ
max
a
′
Q
ω
−
(
s
i
′
,
a
′
)
)
]
2
\\arg \\min_{\\omega} \\frac{1}{2N} \\sum_{i=1}^{N} \\left[ Q_{\\omega}(s_i, a_i) – \\left( r_i + \\gamma \\max_{a'} Q_{\\omega^-}(s'_i, a') \\right) \\right]^2
argminω2N1∑i=1N[Qω(si,ai)−(ri+γmaxa′Qω−(si′,a′))]2的项
Q
w
(
s
,
a
)
Q_w(s,a)
Qw(s,a),并且使用正常梯度下降方法来进行更新。 (2)目标网络
Q
w
−
(
s
,
a
)
Q_{w^{-}}(s,a)
Qw−(s,a),用于计算原先损失函数中
arg
min
ω
1
2
N
∑
i
=
1
N
[
Q
ω
(
s
i
,
a
i
)
−
(
r
i
+
γ
max
a
′
Q
ω
−
(
s
i
′
,
a
′
)
)
]
2
\\arg \\min_{\\omega} \\frac{1}{2N} \\sum_{i=1}^{N} \\left[ Q_{\\omega}(s_i, a_i) – \\left( r_i + \\gamma \\max_{a'} Q_{\\omega^-}(s'_i, a') \\right) \\right]^2
argminω2N1∑i=1N[Qω(si,ai)−(ri+γmaxa′Qω−(si′,a′))]2的项
r
i
+
γ
max
a
′
Q
ω
−
(
s
i
′
,
a
′
)
r_i + \\gamma \\max_{a'} Q_{\\omega^-}(s'_i, a')
ri+γmaxa′Qω−(si′,a′),其中
w
−
w^-
w−表示目标网络中的参数。 如果两套网络的参数随时保持一致,则仍为原先不够稳定的算法。为了让更新目标更稳定,目标网络并不会每一步都更新。具体而言,目标网络使用训练网络的一套较旧的参数,训练网络在训练中的每一步都会更新,而目标网络的参数每隔步才会与训练网络同步一次,即。这样做使得目标网络相对于训练网络更加稳定。
代码实现
from tqdm import tqdm
import numpy as np
import torch
import collections
import random
class ReplayBuffer:
def __init__(self, capacity):
self.buffer = collections.deque(maxlen=capacity)
def add(self, state, action, reward, next_state, terminated, truncated):
self.buffer.append((state, action, reward, next_state, terminated, truncated))
def sample(self, batch_size):
transitions = random.sample(self.buffer, batch_size)
state, action, reward, next_state, terminated, truncated = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), terminated, truncated
def size(self):
return len(self.buffer)
def moving_average(a, window_size):
cumulative_sum = np.cumsum(np.insert(a, 0, 0))
middle = (cumulative_sum[window_size:] – cumulative_sum[:–window_size]) / window_size
r = np.arange(1, window_size – 1, 2)
begin = np.cumsum(a[:window_size – 1])[::2] / r
end = (np.cumsum(a[:–window_size:–1])[::2] / r)[::–1]
return np.concatenate((begin, middle, end))
def train_on_policy_agent(env, agent, num_episodes):
return_list = []
for i in range(10):
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
transition_dict = {'states': [], 'actions': [], 'next_states': [], 'rewards': [], 'dones': []}
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
state, action, reward, next_state, terminated, truncated = env.step(action)
transition_dict['states'].append(state)
transition_dict['actions'].append(action)
transition_dict['next_states'].append(next_state)
transition_dict['rewards'].append(reward)
transition_dict['terminated'].append(terminated)
transition_dict['truncated'].append(truncated)
state = next_state
episode_return += reward
return_list.append(episode_return)
agent.update(transition_dict)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[–10:])})
pbar.update(1)
return return_list
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
return_list = []
for i in range(10):
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
state, action, reward, next_state, terminated, truncated= env.step(action)
replay_buffer.add(state, action, reward, next_state, terminated, truncated)
state = next_state
episode_return += reward
if replay_buffer.size() > minimal_size:
b_s, b_a, b_r, b_ns,t_e, t_r= replay_buffer.sample(batch_size)
transition_dict = {'states': b_s, 'actions': b_a, 'next_states': b_ns, 'rewards': b_r,
'terminated': t_e, 'truncated': t_r}
agent.update(transition_dict)
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
'return': '%.3f' % np.mean(return_list[–10:])})
pbar.update(1)
return return_list
def compute_advantage(gamma, lmbda, td_delta):
td_delta = td_delta.detach().numpy()
advantage_list = []
advantage = 0.0
for delta in td_delta[::–1]:
advantage = gamma * lmbda * advantage + delta
advantage_list.append(advantage)
advantage_list.reverse()
return torch.tensor(advantage_list, dtype=torch.float)
import random
#Gymnasium 是强化学习领域最通用的开源标准环境库。它提供了一整套统一的 API,让你能方便地调用各种预置的游戏或控制任务来训练智能体,比如经典的“CartPole(倒立摆)”和“MountainCar(山地车)”。
import gymnasium as gym
import numpy as np
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
import rl_utils
from tqdm import tqdm
class Qnet(torch.nn.Module):
def __init__(self,state_dim,hidden_dim,action_dim):
super(Qnet,self).__init__()
self.fc1 = torch.nn.Linear(state_dim,hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim,action_dim)
def forward(self,x):
x=F.relu(self.fc1(x))
return self.fc2(x)
class DNQ:
def __init__(self,
state_dim,
hidden_dim,
action_dim,
gamma,
epsilon,
target_update,
learning_rate,
device,
dqn_type='VanillaDQN'):
self.action_dim=action_dim
#训练网络
self.q_net=Qnet(state_dim,hidden_dim,action_dim).to(device)
#目标网络
self.target_net=Qnet(state_dim,hidden_dim,action_dim).to(device)
self.optimizer = torch.optim.Adam(self.q_net.parameters(),lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
self.target_update = target_update
self.count = 0
self.dqn_type = dqn_type
self.device = device
#取Q最大的动作
def take_action(self,state):
if np.random.random()<self.epsilon:
action = np.random.randint(self.action_dim)
else:
#将输入的state格式化为tensor变量才能放进神经网络中
#[state]:在gymnasium返回的环境一般是列表/数组,大小为(n,)一维。而神经网络需要输入tensor变量,且输入大小为:(批次大小,n)。[(n,)]大小为:(1,4)
state=torch.tensor([state], dtype=torch.float).to(self.device)
#.item():转换成列表
action=self.q_net(state).argmax().item()
return action
def max_q_value(self,state):
state=torch.tensor([state], dtype=torch.float).to(self.device)
return self.q_net(state).max().item()#返回最优状态
def update(self,transition_dict):
states=torch.tensor(transition_dict['states'],dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(–1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(–1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
terminated = torch.tensor(transition_dict['terminated'],
dtype=torch.float).view(–1, 1).to(self.device)
#.gather(1,actions):从第二维度观察按actions选取
q_values=self.q_net(states).gather(1,actions)
if self.dqn_type=='Dueling':
max_action=self.q_net(next_states).max(1)[1].view(–1,1)
max_next_q_values=self.target_net(next_states).gather(1,max_action)
else:
max_next_q_values=self.target_net(next_states).max(1)[0].view(–1,1)
#TD误差
q_targets = rewards + self.gamma * (1.0 – terminated) * max_next_q_values
dnq_loss=torch.mean(F.mse_loss(q_values,q_targets))
self.optimizer.zero_grad()
dnq_loss.backward()
self.optimizer.step()
if self.count % self.target_update==0:
#.state_dict() 是一个 PyTorch 模型的方法,它返回一个字典(OrderedDict),里面包含了这个模型所有可学习参数(权重和偏置)的当前值。.load_state_dict(某个字典) 是另一个模型的方法。它接收一个参数字典,然后用这个字典里的值,去覆盖自己模型内部的参数。
self.target_net.load_state_dict(self.q_net.state_dict())
self.count+=1
lr = 1e-2
num_episodes = 200
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
target_update = 50
buffer_size = 5000
minimal_size = 1000
batch_size = 64
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
"cpu")
#Pendulum(倒立摆)
env_name = 'Pendulum-v1'
evn=gym.make(env_name)
#state_dim = env.observation_space.shape[0] 可以直接作为网络第一层 nn.Linear(state_dim, hidden_dim) 的输入维度
state_dim=evn.observation_space.shape[0]
action_dim=11
#%%
# 离散动作转回连续的函数
def dis_to_con(discrete_action,env,action_dim):
# 连续动作的最小值
action_lowbound=env.action_space.low[0]
# 连续动作的最大值
action_upbound=env.action_space.high[0]
return action_lowbound + (discrete_action /
(action_dim – 1)) * (action_upbound –
action_lowbound)
#%%
def train_DNQ(agent, env, num_episodes, replay_buffer, minimal_size,
batch_size):
return_list=[]
max_q_value_list = []
max_q_value = 0
for i in range(10):
with tqdm(total=int(num_episodes / 10),
desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)):
episode_return = 0
state,_ = env.reset(seed=42)
done = False
while not done:
action = agent.take_action(state)
#训练中的原始 Q 值(agent.max_q_value(state))受网络随机初始化、经验回放采样等因素影响,通常在短期内剧烈震荡。直接观察它们就像看一团噪音,难以判断模型是否在真正进步。为了过滤短期噪声,凸显长期变化趋势,就需要对数据进行平滑。
#max_q_value = 新值 * 0.005 + 旧值 * 0.995这是一个指数移动平均(EMA)。它像一个拥有“超长记忆”的过滤器,新数据只贡献 0.5% 的权重,而 99.5% 来自过去无数轮数据的累积。这个操作纯粹是为了数据可视化,帮你画出一条平滑的曲线来诊断模型是否在收敛,绝对不影响模型本身的训练
max_q_value = agent.max_q_value(
state) * 0.005 + max_q_value * 0.995 # 平滑处理
# 保存每个状态的最大Q值
max_q_value_list.append(max_q_value)
action_continuous =dis_to_con(action,env,action_dim)
# [action_continuous] 用方括号包裹动作,是 Gymnasium 环境接口的一个硬性要求。它和“动作空间是连续还是离散”无关,而是为了统一接口格式。
# env.step(action) 中的 action 必须是一个可迭代对象(如列表、元组或 NumPy 数组)。
# 新版 Gymnasium将原来的 done 拆分成了两个信号:terminated (终结):布尔值,代表任务在逻辑上已真正完成(成功或失败)。例如,倒立摆倒下、小车到达山顶,这些是 MDP 定义中的终止状态。此时,算法应当停止当前序列,且之后不再计算未来的价值。;truncated (截断):布尔值,代表回合因为非任务逻辑的原因被强制结束。最常见的原因是超时(达到了 max_episode_steps)。此时,智能体并没有“死掉”,只是环境不再提供下一步的数据。这在理论上不是一个真正的终止状态,算法如果错误地将其当作终止状态处理,可能会导致高价值区域被低估。
next_state, reward, terminated, truncated, _ =env.step([action_continuous])
done=terminated or truncated
replay_buffer.add(state, action, reward, next_state, terminated, truncated)
state=next_state
episode_return += reward
return_list.append(episode_return)
if replay_buffer.size() > minimal_size:
#随机取样的组成取决于前面代码next_state, reward,terminated, truncated, _ =env.step([action_continuous]) done = terminated or truncated
b_s, b_a, b_r, b_ns, b_te,b_tr = replay_buffer.sample(
batch_size)
transition_dict = {
'states': b_s,
'actions': b_a,
'next_states': b_ns,
'rewards': b_r,
'terminated': b_te,
'truncated': b_tr,
}
agent.update(transition_dict)
if (i_episode + 1) % 10 == 0:
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[–10:])
})
pbar.update(1)
return return_list, max_q_value_list
#%%
random.seed(0)
np.random.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DNQ(state_dim, hidden_dim, action_dim, gamma, epsilon, target_update, lr, device,dqn_type='Dueling')
#%%
return_list, max_q_value_list = train_DNQ(agent, evn, num_episodes,
replay_buffer, minimal_size,
batch_size)

由图像可以看出:在 DQN 的性能得到提升后,它会持续出现一定程度的震荡。回归Q-Learning更新方式,其总会选取历史上某状态下的最大动作方向进行更新。由于神经网络拟合带有噪声,因此,选取的方向可能被高估了。max 操作会毫不犹豫地选中这个被高估的动作,把它的价值作为更新目标。当这个带有高估的目标反向传播,更新上游状态的 Q 值时,上游状态的价值也就被连带高估了。在后续的更新中,这种错误又会被叠加,就像滚雪球一样,最终导致严重的价值高估。高估的Q值造成了Q在错误方向上的估计,因此,需要更多步数拟合才能拉回正确轨道上。 基于此分析,我们可以考虑: (1)解耦动作选择与动作评估。在动作选择上可以更偏向信用已知的经验Q;在动作评估上需要相对保守,也即拟合的Q网络相对于动作选择的Q网络有更少的经验(高估值累计较少) (2)尽可能让动作-价值函数的更新是在一个基于状态的大基准下对最优动作的小范围拔高调整。
改进DQN算法一:Double DQN
耦动作选择与动作评估即为用不同的Q网络来拟合,由于DQN算法中本身就有两种Q网路。我么可以尝试用训练网路和目标网路完成解耦。
- 动作选择:由一直更新的 Q 主网络负责。它使用最新的参数,决定了选择哪个动作
α
m
a
x
\\alpha_{max}
αmax。如果它因为误差而错误地选择了一个被高估的动作,那也没关系。 - 动作评估:由冻结参数的目标网络负责。它作为独立的裁判,来评估主网络选定的动作
α
m
a
x
\\alpha_{max}
αmax的真实价值。由于目标网络参数没有和主网络同步更新,它对动作的价值评估相对客观,不会和主网络“串通”起来放大同一个误差。这样就切断了“选错动作”和“高估此动作”之间的循环,使得价值更新目标变得更加真实、稳定。
代码如下:
import random
import gym
import numpy as np
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
import rl_utils
from tqdm import tqdm
class Qnet(torch.nn.Module):
''' 只有一层隐藏层的Q网络 '''
def __init__(self, state_dim, hidden_dim, action_dim):
super(Qnet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
return self.fc2(x)
class DQN:
''' DQN算法,包括Double DQN '''
def __init__(self,
state_dim,
hidden_dim,
action_dim,
learning_rate,
gamma,
epsilon,
target_update,
device,
dqn_type='VanillaDQN'):
self.action_dim = action_dim
self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
self.target_q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
self.target_update = target_update
self.count = 0
self.dqn_type = dqn_type
self.device = device
def take_action(self, state):
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = torch.tensor([state], dtype=torch.float).to(self.device)
action = self.q_net(state).argmax().item()
return action
def max_q_value(self, state):
state = torch.tensor([state], dtype=torch.float).to(self.device)
return self.q_net(state).max().item()
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(–1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(–1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(–1, 1).to(self.device)
q_values = self.q_net(states).gather(1, actions) # Q值
# 下个状态的最大Q值
if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别
max_action = self.q_net(next_states).max(1)[1].view(–1, 1)
max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
else: # DQN的情况
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(–1, 1)
q_targets = rewards + self.gamma * max_next_q_values * (1 – dones) # TD误差目标
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets)) # 均方误差损失函数
self.optimizer.zero_grad() # PyTorch中默认梯度会累积,这里需要显式将梯度置为0
dqn_loss.backward() # 反向传播更新参数
self.optimizer.step()
if self.count % self.target_update == 0:
self.target_q_net.load_state_dict(
self.q_net.state_dict()) # 更新目标网络
self.count += 1
lr = 1e-2
num_episodes = 200
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
target_update = 50
buffer_size = 5000
minimal_size = 1000
batch_size = 64
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
"cpu")
env_name = 'Pendulum-v1'
env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = 11 # 将连续动作分成11个离散动作
def dis_to_con(discrete_action, env, action_dim): # 离散动作转回连续的函数
action_lowbound = env.action_space.low[0] # 连续动作的最小值
action_upbound = env.action_space.high[0] # 连续动作的最大值
return action_lowbound + (discrete_action /
(action_dim – 1)) * (action_upbound –
action_lowbound)
random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
target_update, device)
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
replay_buffer, minimal_size,
batch_size)
episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN on {}'.format(env_name))
plt.show()
frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='–')
plt.axhline(10, c='red', ls='–')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('DQN on {}'.format(env_name))
plt.show()
random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
target_update, device, 'DoubleDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
replay_buffer, minimal_size,
batch_size)
episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Double DQN on {}'.format(env_name))
plt.show()
frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='–')
plt.axhline(10, c='red', ls='–')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('Double DQN on {}'.format(env_name))
plt.show()
结果如下:
我们可以发现,与普通的 DQN 相比,Double DQN 比较少出现Q值大于 0 的情况,说明Q值过高估计的问题得到了很大缓解。
改进DQN算法二:Dueling DQN
在强化学习中,我们将状态动作价值函数Q减去状态价值函数V的结果定义为优势函数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)。在同一个状态下,强制所有动作的优势值之和为 0(这是符合基本规律的,因为所有动作的动作价值的期望就是这个状态的状态价值。)因此,我么构建的更新规则为:
Q
(
s
,
a
)
=
V
(
s
)
+
A
(
s
,
a
)
Q(s,a)=V(s)+A(s,a)
Q(s,a)=V(s)+A(s,a) 之所以强制所有动作的优势值之和为 0,是因为这样状态价值 V 与动作优势 A”之间才可以唯一识别。想象一下,我们可以给 V(s) 加上一个常数 c,同时给所有 A(s,a) 减去同一个常数 c,最终的 Q(s,a) 结果完全不变。这就是没有做到唯一识别。可以看出若是没有唯一识别,Q的更新可能不是原始的构造要求。 强制所有动作的优势值之和为 0的方法是让每个动作的优势值减去它们所有动作优势值的平均值。即:
Q
(
s
,
a
)
=
V
(
s
)
+
(
A
(
s
,
a
)
−
1
∣
A
∣
∑
a
′
A
(
s
,
a
′
)
)
Q(s,a)=V(s)+(A(s,a) – \\frac{1}{|A|}\\sum_{a'}A(s,a'))
Q(s,a)=V(s)+(A(s,a)−∣A∣1∑a′A(s,a′)) 综上,Dueling DQN 通过强制 A 均值为 0,避免了 Q 值在某个动作上的“结构性失衡”。单个动作的Q值,就变成了这个稳定的基准值,加上该动作相对于平均水平的“偏差”。这个偏差值本身就是归一化的,它直接反映了“选这个动作比选其他动作好多少(或差多少)”,其增量被自然地限制在一个合理的范围内。
class VAnet(torch.nn.Module):
''' 只有一层隐藏层的A网络和V网络 '''
def __init__(self, state_dim, hidden_dim, action_dim):
super(VAnet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim) # 共享网络部分
self.fc_A = torch.nn.Linear(hidden_dim, action_dim)
self.fc_V = torch.nn.Linear(hidden_dim, 1)
def forward(self, x):
A = self.fc_A(F.relu(self.fc1(x)))
V = self.fc_V(F.relu(self.fc1(x)))
Q = V + A – A.mean(1).view(–1, 1) # Q值由V值和A值计算得到
return Q
class DQN:
''' DQN算法,包括Double DQN和Dueling DQN '''
def __init__(self,
state_dim,
hidden_dim,
action_dim,
learning_rate,
gamma,
epsilon,
target_update,
device,
dqn_type='VanillaDQN'):
self.action_dim = action_dim
if dqn_type == 'DuelingDQN': # Dueling DQN采取不一样的网络框架
self.q_net = VAnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.target_q_net = VAnet(state_dim, hidden_dim,
self.action_dim).to(device)
else:
self.q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.target_q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
self.target_update = target_update
self.count = 0
self.dqn_type = dqn_type
self.device = device
def take_action(self, state):
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = torch.tensor([state], dtype=torch.float).to(self.device)
action = self.q_net(state).argmax().item()
return action
def max_q_value(self, state):
state = torch.tensor([state], dtype=torch.float).to(self.device)
return self.q_net(state).max().item()
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(–1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(–1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(–1, 1).to(self.device)
q_values = self.q_net(states).gather(1, actions)
if self.dqn_type == 'DoubleDQN':
max_action = self.q_net(next_states).max(1)[1].view(–1, 1)
max_next_q_values = self.target_q_net(next_states).gather(
1, max_action)
else:
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(
–1, 1)
q_targets = rewards + self.gamma * max_next_q_values * (1 – dones)
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
self.optimizer.zero_grad()
dqn_loss.backward()
self.optimizer.step()
if self.count % self.target_update == 0:
self.target_q_net.load_state_dict(self.q_net.state_dict())
self.count += 1
random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
target_update, device, 'DuelingDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
replay_buffer, minimal_size,
batch_size)
episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Dueling DQN on {}'.format(env_name))
plt.show()
frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='–')
plt.axhline(10, c='red', ls='–')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('Dueling DQN on {}'.format(env_name))
plt.show()
结果如下: 
不难看出,Dueling DQN 网络更加稳定。





