这篇博文要讲解的是利用DQN来做CartPole任务
我们知道,给定一个状态 s s s,agent根据策略 π ( a ∣ s ) \pi(a|s) π(a∣s)做出行为 a a a,得到的奖励是 r r r,然后环境根据状态转移概率 P ( s ′ ∣ s ) P(s'|s) P(s′∣s)转移到新的状态 s ′ s' s′.
强化学习中更多时候关注的是给定某一个状态 S t S_t St,它的累计奖励,也叫“回报”或者“收获”,用英文return表示。
定义给定状态 S t S_t St下的回报:
G t = R t + γ R t + 1 + γ 2 R t + 2 + ⋯ G_t=R_t+\gamma R_{t+1}+\gamma^2 R_{t+2}+\cdots Gt=Rt+γRt+1+γ2Rt+2+⋯
它的含义就是在状态 S t S_t St下开始,根据策略做出行为,获得奖励,累计奖励,更新状态,直到终止状态下所有奖励的累计。加入折扣因子 γ \gamma γ是因为未来的奖励对当前的状态具有不确定性,避免在计算回报时陷入循环。
我们可以看出,一个状态的回报反映了该状态在一个状态序列下的重要程度。但是当给定一个状态时,它可能产生无数多个状态序列。
具体的:
所以简单地用回报 G t G_t Gt是不能准确的定义一个状态的好坏的,因为 G t G_t Gt仅仅针对某一个状态序列。
那么一种自然的想法就是状态 s s s下,根据策略 π ( a ∣ s ) \pi(a|s) π(a∣s)和状态转移矩阵 P ( s ′ ∣ s ) P(s'|s) P(s′∣s),穷举所有可能产生的状态序列下的回报 ∑ t = 1 ∞ G t \sum_{t=1}^{\infty}G_t ∑t=1∞Gt,作为一个指标,衡量状态 s s s的好坏.
这也就是对给定状态 s s s下所有可能产生的状态序列下的回报的期望。
E [ G t ∣ S t = s ] \mathbb{E}[G_t|S_t=s] E[Gt∣St=s]
这就是强化学习中“价值”的概念,也就是状态 s s s的价值就是 v π ( s ) = E [ G t ∣ S t = s ] v_\pi(s)=\mathbb{E}[G_t|S_t=s] vπ(s)=E[Gt∣St=s]. value的含义就是状态回报的期望.
如果存在一个函数,给定一个状态,就能得到该状态对应的价值,那么这个函数就叫做价值函数(value function).
价值函数是状态到价值的映射 s t a t e → v a l u e f u n c t i o n v a l u e state \xrightarrow[]{value function} value statevaluefunctionvalue
注意:价值函数是基于策略 π \pi π的。
我们知道在不同的策略 π \pi π下,给定状态 s s s, v π ( s ) v_\pi(s) vπ(s)显然是不同的。
最优价值函数就是对所有的策略 π \pi π下, v π ( s ) v_\pi(s) vπ(s)值最大的价值函数:
v ∗ ( s ) = max π v π ( s ) v_*(s)=\underset{\pi}{\text{max}} v_{\pi}(s) v∗(s)=πmaxvπ(s)
价值函数评估的是根据策略 π \pi π当前状态的好坏,动作价值函数评估的是当前状态下根据策略 π \pi π做出某一个行为的好坏。所以它引入了动作(也就是行为)的概念。
q π ( s , a ) = E [ G t ∣ S t = s , A t = a ] q_{\pi}(s,a)=\mathbb{E}[G_t|S_t=s,A_t=a] qπ(s,a)=E[Gt∣St=s,At=a]
再啰嗦一遍:
我们知道在不同的策略 π \pi π下,给定状态 s s s, q π ( s , a ) q_\pi(s,a) qπ(s,a)显然是不同的。
最优动作价值函数就是对所有的策略 π \pi π下, q π ( s , a ) q_\pi(s,a) qπ(s,a)值最大时对应的动作价值函数:
q ∗ ( s , a ) = max π q π ( s , a ) q_*(s,a)=\underset{\pi}{\text{max}} q_{\pi}(s,a) q∗(s,a)=πmaxqπ(s,a)
我们来推导动作价值函数的Bellman equation(贝尔曼等式):
q π ( s , a ) = E [ G t ∣ S t = s , A t = a ] = E [ R t + γ R t + 1 + γ 2 R t + 2 + ⋯ ∣ S t = s , A t = a ] = E [ R t + γ ( R t + 1 + γ R t + 2 + γ 2 R t + 3 + ⋯ ) ∣ S t = s , A t = a ] = E [ R t + γ G t + 1 ∣ S t = s , A t = a ] = E [ R t ∣ S t = s , A t = a ] + γ E [ G t + 1 ∣ S t = s , A t = a ] = E [ R t ∣ S t = s , A t = a ] + γ q π ( S t + 1 , A t + 1 ) = E [ R t ∣ S t = s , A t = a ] + γ E [ q π ( S t + 1 , A t + 1 ) ∣ S t = s , A t = a ] = E [ R t + γ q π ( S t + 1 , A t + 1 ) ∣ S t = s , A t = a ] \begin{aligned} q_\pi(s,a) &= \mathbb{E}[G_t|S_t=s,A_t=a]\\ &= \mathbb{E}[R_t+\gamma R_{t+1}+\gamma^2 R_{t+2}+\cdots|S_t=s,A_t=a]\\ &= \mathbb{E}[R_t+\gamma(R_{t+1}+\gamma R_{t+2}+\gamma^2 R_{t+3}+\cdots)|S_t=s,A_t=a]\\ &= \mathbb{E}[R_t+\gamma G_{t+1}|S_t=s,A_t=a]\\ &= \mathbb{E}[R_t|S_t=s,A_t=a]+\gamma\mathbb{E}[G_{t+1}|S_t=s,A_t=a] \\ &= \mathbb{E}[R_t|S_t=s,A_t=a]+\gamma q_\pi(S_{t+1},A_{t+1}) \\ &= \mathbb{E}[R_t|S_t=s,A_t=a]+\gamma\mathbb{E}[q_\pi(S_{t+1},A_{t+1})|S_t=s,A_t=a] \\ &= \mathbb{E}[R_t+\gamma q_\pi(S_{t+1},A_{t+1})|S_t=s,A_t=a] \end{aligned} qπ(s,a)=E[Gt∣St=s,At=a]=E[Rt+γRt+1+γ2Rt+2+⋯∣St=s,At=a]=E[Rt+γ(Rt+1+γRt+2+γ2Rt+3+⋯)∣St=s,At=a]=E[Rt+γGt+1∣St=s,At=a]=E[Rt∣St=s,At=a]+γE[Gt+1∣St=s,At=a]=E[Rt∣St=s,At=a]+γqπ(St+1,At+1)=E[Rt∣St=s,At=a]+γE[qπ(St+1,At+1)∣St=s,At=a]=E[Rt+γqπ(St+1,At+1)∣St=s,At=a]
整个推导过程中需要注意的几点:
上面的公式中由于有期望,所以我们一般是通过采样的方法近似这个期望值,也就是用观测得到的奖励 r t r_t rt近似 R t R_t Rt,观测得到的下一时刻的状态 s t + 1 , a t + 1 s_{t+1},a_{t+1} st+1,at+1代替 S t + 1 , A t + 1 S_{t+1},A_{t+1} St+1,At+1:
q π ( s , a ) ≈ r t + γ q π ( s t + 1 , a t + 1 ) q_\pi(s,a)\approx r_t+\gamma q_\pi(s_{t+1},a_{t+1}) qπ(s,a)≈rt+γqπ(st+1,at+1)
所以不难得出价值函数的Bellman equation就是:
v π ( s ) = E [ R t + γ v π ( S t + 1 ) ∣ S t = s ] v_\pi(s)=\mathbb{E}[R_t+\gamma v_\pi(S_{t+1})|S_t=s] vπ(s)=E[Rt+γvπ(St+1)∣St=s]
近似值就是:
v π ( s ) ≈ r t + γ v π ( s t + 1 ) v_\pi(s)\approx r_t+\gamma v_\pi(s_{t+1}) vπ(s)≈rt+γvπ(st+1)
前面我们已经提到,价值函数评估的是一个状态的价值,动作价值函数评估的是该状态下做出某一个动作的价值。那么怎么评估这个价值呢?
v π ( s t ) ← v π ( s t ) − [ α ( r t + γ v π ( s t + 1 ) − v π ( s t ) ) ] v_\pi(s_t)\leftarrow v_\pi(s_t)-[\alpha(r_t+\gamma v_\pi(s_{t+1})-v_\pi(s_t))] vπ(st)←vπ(st)−[α(rt+γvπ(st+1)−vπ(st))]
其中的 r t + γ v π ( s t + 1 ) r_t+\gamma v_{\pi}(s_{t+1}) rt+γvπ(st+1)称为TD target, r t + γ v π ( s t + 1 ) − v π ( s t ) r_t+\gamma v_\pi(s_{t+1})-v_\pi(s_t) rt+γvπ(st+1)−vπ(st)称为TD error。
TD学习有两种算法:SARSA和Q-learning.
我们主要来看如何用两种算法来学习动作价值函数和最优动作价值函数,按照时序差分的思想,我们可以推导出:
q π ( s t , a t ) ← q π ( s t , a t ) − [ α ( r t + γ q π ( s t + 1 , a t + 1 ) − q π ( s t , a t ) ) ] q_\pi(s_t,a_t)\leftarrow q_\pi(s_t,a_t)-[\alpha(r_t+\gamma q_\pi(s_{t+1},a_{t+1})-q_\pi(s_t,a_t))] qπ(st,at)←qπ(st,at)−[α(rt+γqπ(st+1,at+1)−qπ(st,at))]
前面我们已经推导出动作价值函数的Bellman equation是:
q π ( s , a ) = E [ R t + γ q π ( S t + 1 , A t + 1 ) ∣ S t = s , A t = a ] q_\pi(s,a)= \mathbb{E}[R_t+\gamma q_\pi(S_{t+1},A_{t+1})|S_t=s,A_t=a] qπ(s,a)=E[Rt+γqπ(St+1,At+1)∣St=s,At=a]
最优动作价值函数定义为:
q ∗ ( s , a ) = max π q π ( s , a ) q_*(s,a)=\underset{\pi}{\text{max}}q_\pi(s,a) q∗(s,a)=πmaxqπ(s,a)
所以最优动作价值函数的Bellman equation为:
q ∗ ( s , a ) = E [ R t + γ q ∗ ( S t + 1 , A t + 1 ) ∣ S t = s , A t = a ] q_*(s,a)= \mathbb{E}[R_t+\gamma q_*(S_{t+1},A_{t+1})|S_t=s,A_t=a] q∗(s,a)=E[Rt+γq∗(St+1,At+1)∣St=s,At=a]
q ∗ ( s , a ) q_*(s,a) q∗(s,a)可评估给定状态下所有动作的好坏,那么当状态到达 S t + 1 S_{t+1} St+1时,最优动作自然是 A t + 1 = argmax a ′ ∈ A q ∗ ( S t + 1 , a ′ ) A_{t+1}=\underset{a'\in A}{\text{argmax}}q_*(S_{t+1},a') At+1=a′∈Aargmaxq∗(St+1,a′)
所以我们可以推出来:
q ∗ ( S t + 1 , A t + 1 ) = max a ′ ∈ A q ∗ ( S t + 1 , a ′ ) q_*(S_{t+1},A_{t+1})=\underset{a'\in A}{\text{max}} q_*(S_{t+1},a') q∗(St+1,At+1)=a′∈Amaxq∗(St+1,a′)
所以最优动作价值函数的Bellman equation还可以写成:
q ∗ ( s , a ) = E [ R t + γ max a ′ ∈ A q ∗ ( S t + 1 , a ′ ) ∣ S t = s , A t = a ] q_*(s,a)= \mathbb{E}[R_t+\gamma \underset{a'\in A}{\text{max}} q_*(S_{t+1},a')|S_t=s,A_t=a] q∗(s,a)=E[Rt+γa′∈Amaxq∗(St+1,a′)∣St=s,At=a]
仍然用采样的方式去掉期望,也就是用观测得到的奖励 r t r_t rt,以及观测得到的 s t + 1 s_{t+1} st+1代替上式的 S t + 1 S_{t+1} St+1。
所以我们得到最优动作价值函数 q ∗ ( s , a ) q_*(s,a) q∗(s,a)的近似值:
q ∗ ( s , a ) ≈ r t + γ max a ′ ∈ A q ∗ ( s t + 1 , a ′ ) q_*(s,a)\approx r_t+\gamma \underset{a'\in A}{\text{max}} q_*(s_{t+1},a') q∗(s,a)≈rt+γa′∈Amaxq∗(st+1,a′)
Q-learning算法的更新过程就是:
q ∗ ( s , a ) ← q ∗ ( s , a ) − α ( r t + γ max a ′ ∈ A q ∗ ( s t + 1 , a ′ ) − q ∗ ( s , a ) ) q_*(s,a)\leftarrow q_*(s,a)-\alpha(r_t+\gamma\underset{a'\in A}{\text{max}}q_*(s_{t+1},a')-q_*(s,a)) q∗(s,a)←q∗(s,a)−α(rt+γa′∈Amaxq∗(st+1,a′)−q∗(s,a))
其中 r t + γ max a ′ ∈ A q ∗ ( s t + 1 , a ′ ) r_t+\gamma\underset{a'\in A}{\text{max}}q_*(s_{t+1},a') rt+γa′∈Amaxq∗(st+1,a′)称为TD target, r t + γ max a ′ ∈ A q ∗ ( s t + 1 , a ′ ) − q ∗ ( s , a ) r_t+\gamma\underset{a'\in A}{\text{max}}q_*(s_{t+1},a')-q_*(s,a) rt+γa′∈Amaxq∗(st+1,a′)−q∗(s,a)称为TD error。
DQN就是利用神经网络近似最优动作价值函数 q ∗ ( s , a ) q_*(s,a) q∗(s,a)。我们写成 q ( s , a ; θ ) q(s,a;\theta) q(s,a;θ), θ \theta θ就是网络的参数。我们的目的现在变为学习参数 θ \theta θ,使得其更好的近似最优动作价值函数。
定义神经网络的损失函数是均方误差。也就是
l o s s = 1 2 ∑ t ( q ( s t , a t ; θ ) − y t ) 2 loss=\frac{1}{2}\sum_t(q(s_t,a_t;\theta)-y_t)^2 loss=21t∑(q(st,at;θ)−yt)2
算法流程:
此外DQN的训练过程有许多的技巧,比如:
我们把一个 ( s t , a t , r t , s t + 1 ) (s_t,a_t,r_t,s_{t+1}) (st,at,rt,st+1)称为一个transition.按照上面的算法流程我们可以看到,我们是拿一个transition,去更新一次参数 θ \theta θ,然后这个transition就丢弃不用了。然后根据新的transition ( s t + 1 , a t + 1 , r t + 1 , s t + 2 ) (s_{t+1},a_{t+1},r_{t+1},s_{t+2}) (st+1,at+1,rt+1,st+2)再重复这个过程。
我们把从初始状态 s 0 s_{0} s0到终止状态 s n s_{n} sn之间的所有transition称为经验experience
这种做法有两点问题:
所以就有了经验回放这种做法:
有一个replay buffer池,收集很多的transition, ( s t , a t , r t , s t + 1 ) , ( s t + 1 , a t + 1 , r t + 1 , s t + 2 ) , ( s t + 2 , a t + 2 , r t + 2 , s t + 2 ) , ⋯ , ( s t + n , a t + n , r t + n , s t + n + 1 ) (s_t,a_t,r_t,s_{t+1}),(s_{t+1},a_{t+1},r_{t+1},s_{t+2}),(s_{t+2},a_{t+2},r_{t+2},s_{t+2}),\cdots,(s_{t+n},a_{t+n},r_{t+n},s_{t+n+1}) (st,at,rt,st+1),(st+1,at+1,rt+1,st+2),(st+2,at+2,rt+2,st+2),⋯,(st+n,at+n,rt+n,st+n+1),这里的n就是池的容量,将这些transition放到replay buffer中,注意这里我是顺序写的,但实际上不是的,各个transition之间是打乱了顺序的。然后从replay buffer中取出batch_size个transition更新参数。
在模型刚开始训练的时候,网络的参数都是随机初始化的,所以不会很好的近似最优价值函数。所以在刚开始我们有一定的概率随机做出一个行为。在训练了很多步骤后,网络已经能很好的近似最优价值函数,那么我们此时倾向于做出神经网络输出的行为,而不再是随机做出行为。
ϵ \epsilon ϵ-greedy策略是探索(exploration)和利用(exploitation)之间的折中。
if random.random()>epsilon_threshold:
return Q_network(state)
else:
return random.randint(a=0,b=action_space-1)#随机做出行为
Double DQN是解决DQN高估问题的一个非常有效的方法。
q π ( s t , a t ) = r t + γ max a ′ q π ( s t + 1 , a ′ ) q_\pi(s_t,a_t)=r_t+\gamma\underset{a'}{\max}q_\pi(s_{t+1},a') qπ(st,at)=rt+γa′maxqπ(st+1,a′)
上式中有 max a ′ q π ( s t + 1 , a ′ ) \underset{a'}{\max}q_\pi(s_{t+1},a') a′maxqπ(st+1,a′)这一项,这一项是对状态 s t + 1 s_{t+1} st+1所有可能的动作价值的估计中最大的,这就会造成高估问题。举个例子:
x 1 , x 2 , ⋯ , x n x_1,x_2,\cdots,x_n x1,x2,⋯,xn是n个数据点,我们向其中添加均值为0的噪声,此时的数据表示为 δ 1 , δ 2 , ⋯ , δ n \delta_1,\delta_2,\cdots,\delta_n δ1,δ2,⋯,δn
那么我们可以证明如下式成立:
m e a n i ( x i ) = m e a n i ( δ i ) m a x i ( x i ) ≤ m a x i ( δ i ) \underset{i}{mean}(x_i)=\underset{i}{mean}(\delta_i) \\ \underset{i}{max}(x_i)\leq\underset{i}{max}(\delta_i) imean(xi)=imean(δi)imax(xi)≤imax(δi)
这是因为噪声的均值是0,所以添加噪声后的数据的均值不变,但是既然均值为0,那么说明噪声中肯定既有负值又有正值,所以导致
我们回到DQN, max a ′ q π ( s t + 1 , a ′ ) \underset{a'}{\max}q_\pi(s_{t+1},a') a′maxqπ(st+1,a′)这一项就是对所有的动作进行估计,估计它的价值。我们可以将 Q Q Q函数中的参数视为均值是0的噪声,那么我们就知道了 max a ′ q π ( s t + 1 , a ′ ) \underset{a'}{\max}q_\pi(s_{t+1},a') a′maxqπ(st+1,a′)就会导致估计值大于真实值。从而使得 q π ( s t , a t ) = r t + max a ′ q π ( s t + 1 , a ′ ) q_\pi(s_t,a_t)=r_t+\underset{a'}{\max}q_\pi(s_{t+1},a') qπ(st,at)=rt+a′maxqπ(st+1,a′)变大。
我们从上面的公式看到,DQN在估计 t t t时刻的 Q Q Q值的时候,利用了 t + 1 t+1 t+1时刻 Q Q Q值的估计,这就是自举(booststrapping),我们已经知道 t + 1 t+1 t+1时刻由于最大化操作造成了高估,那么就会使得DQN对 t t t时刻的估计同样变大,造成高估问题。以此下去,使得高估问题越来越严重。
通过上面的理论说明我们知道造成DQN高估的问题有两个,一个是最大化;另一个是自举。
Double DQN就是有两个Q-Network,记为 Q Q Q和 Q ′ Q' Q′,
我们对比一下DQN和DDQN:
可以看出DDQN减轻了最大化和自举带来的高估问题。
其中 Q Q Q和 Q ′ Q' Q′的网络结构完全相同,对于 Q Q Q我们是实时更新的。对于 Q ′ Q' Q′,我们每隔一定时间步,将 Q Q Q的参数复制给 Q ′ Q' Q′,用pytorch实现就是:
Q'.load_state_dict(Q.state_sict())
在进行代码之前,我们写一下伪代码再熟悉一遍流程:
env=gym.make("CartPole-v")#创建环境
while True:
state=env.reset()#每一个序列episode结束后重置环境,也就是让小车回到原点
for t in range(T):
action=Q_network(state)#我们利用神经网络作为Q_network近似最优价值函数,它的输出就是最优动作
next_state,reward,done,info=env.step(action)#我们执行这个动作,获得了奖励,并且更新到下一个状态
#done表示的就是是否达到了终止状态,info不用考虑
if done:
break
在正常的CartPole游戏中,状态是由四个数值组成的,分别代表(车的位置,车的加速度,杆的角度,杆的角加速度)。
但是我们这里有另一种做法,就是把当前画面和前一帧画面之间的差值作为状态输入给神经网络。 神经网络的输出是对行为的打分,这里只有两个行为(左和右)
import gym
import math
import random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple
from itertools import count
from PIL import Image
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as T
env = gym.make('CartPole-v0').unwrapped#创建环境
is_ipython = 'inline' in matplotlib.get_backend()
if is_ipython:
from IPython import display
plt.ion()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
resize = T.Compose([T.ToPILImage(),
T.Resize(40, interpolation=Image.CUBIC),
T.ToTensor()])
# This is based on the code from gym.
screen_width = 600
def get_cart_location():
world_width = env.x_threshold * 2
scale = screen_width / world_width
return int(env.state[0] * scale + screen_width / 2.0) # MIDDLE OF CART
def get_screen():
screen = env.render(mode='rgb_array').transpose(
(2, 0, 1)) # transpose into torch order (CHW)
# Strip off the top and bottom of the screen
screen = screen[:, 160:320]
view_width = 320
cart_location = get_cart_location()
if cart_location < view_width // 2:
slice_range = slice(view_width)
elif cart_location > (screen_width - view_width // 2):
slice_range = slice(-view_width, None)
else:
slice_range = slice(cart_location - view_width // 2,
cart_location + view_width // 2)
# Strip off the edges, so that we have a square image centered on a cart
screen = screen[:, :, slice_range]
# Convert to float, rescare, convert to torch tensor
# (this doesn't require a copy)
screen = np.ascontiguousarray(screen, dtype=np.float32) / 255
screen = torch.from_numpy(screen)
# Resize, and add a batch dimension (BCHW)
return resize(screen).unsqueeze(0).to(device)
env.reset()
plt.figure()
plt.imshow(get_screen().cpu().squeeze(0).permute(1, 2, 0).numpy(),
interpolation='none')
plt.title('Example extracted screen')
plt.show()
也就是说调用get_screen()函数会返回如下图片
我们将状态定义为两个相邻图片之间的差值,具体的:
last_screen=get_screen()
current_screen=get_screen()
state=current_screen-last_screen
print(state.size())
我们可以得到状态的形状是(1,3,40,80)。1代表的一个样本,3是通道数,40是高,80是宽。
我们定义一个由三层卷积构成的神经网络近似最优动作价值函数。我们把这个网络称为Q network.
它的输入就是状态,形状是(batch_size,3,40,80)
输出就是给每一个状态对应的动作打的分数,形状是(batch_size,2)
class DQN(nn.Module):
def __init__(self):
super(DQN, self).__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=5, stride=2)
self.bn1 = nn.BatchNorm2d(16)
self.conv2 = nn.Conv2d(16, 32, kernel_size=5, stride=2)
self.bn2 = nn.BatchNorm2d(32)
self.conv3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
self.bn3 = nn.BatchNorm2d(32)
self.head = nn.Linear(448, 2)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
return self.head(x.view(x.size(0), -1))
Transition = namedtuple('Transition',
('state', 'action', 'next_state', 'reward'))
class ReplayMemory(object):
def __init__(self, capacity):
self.capacity = capacity
self.memory = []
self.position = 0
def push(self, *args):
"""Saves a transition."""
if len(self.memory) < self.capacity:
self.memory.append(None)
self.memory[self.position] = Transition(*args)
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size):
return random.sample(self.memory, batch_size)
def __len__(self):
return len(self.memory)
选择epsilon贪婪策略,一开始时尽可能的选取随机策略,因为此时的网络还不能很好的近似最优价值函数。所以此时我们需要尽可能的探索每一个状态下可能出现的每一个行为。
当一定步骤之后,我们的网络已经可以给出最优动作的时候,我们倾向于使用这个最优动作,而不再随机做出行为。
ϵ \epsilon ϵ-greedy策略的目的就是再探索和利用中取得平衡
BATCH_SIZE = 128
GAMMA = 0.999
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
policy_net = DQN().to(device)
target_net = DQN().to(device)
target_net.load_state_dict(policy_net.state_dict())
target_net.eval()
optimizer = optim.RMSprop(policy_net.parameters())
memory = ReplayMemory(10000)
steps_done = 0
def select_action(state):
global steps_done
sample = random.random()
eps_threshold = EPS_END + (EPS_START - EPS_END) * \
math.exp(-1. * steps_done / EPS_DECAY)
steps_done += 1
if sample > eps_threshold:
with torch.no_grad():
return policy_net(state).max(1)[1].view(1, 1)
else:
return torch.tensor([[random.randrange(2)]], device=device, dtype=torch.long)
episode_durations = []
def plot_durations():
plt.figure(2)
plt.clf()
durations_t = torch.tensor(episode_durations, dtype=torch.float)
plt.title('Training...')
plt.xlabel('Episode')
plt.ylabel('Duration')
plt.plot(durations_t.numpy())
# Take 100 episode averages and plot them too
if len(durations_t) >= 100:
means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
means = torch.cat((torch.zeros(99), means))
plt.plot(means.numpy())
plt.pause(0.001) # pause a bit so that plots are updated
if is_ipython:
display.clear_output(wait=True)
display.display(plt.gcf())
def optimize_model():
if len(memory) < BATCH_SIZE:
return#当存储区中的transition不足batch_size的时候,我们暂时不更新参数
transitions = memory.sample(BATCH_SIZE)#采样出batch_size个transition
batch = Transition(*zip(*transitions))
#需要注意的是,当某个状态下做出行为后,很有可能就导致游戏结束了,所以next_state此时应该是None,对应的done是1
non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,
batch.next_state)), device=device, dtype=torch.bool)
non_final_next_states = torch.cat([s for s in batch.next_state
if s is not None])
state_batch = torch.cat(batch.state)
action_batch = torch.cat(batch.action)
reward_batch = torch.cat(batch.reward)
# Compute Q(s_t, a) - the model computes Q(s_t), then we select the
# columns of actions taken
state_action_values = policy_net(state_batch).gather(1, action_batch)
# Compute V(s_{t+1}) for all next states.
next_state_values = torch.zeros(BATCH_SIZE, device=device)
next_state_values[non_final_mask] = target_net(non_final_next_states).max(1)[0].detach()
#对于那些next_state是游戏结束的state,我们将它们的next_state_values设置为0
# Compute the expected Q values
expected_state_action_values = (next_state_values * GAMMA) + reward_batch
# Compute Huber loss
loss = F.smooth_l1_loss(state_action_values, expected_state_action_values.unsqueeze(1))
# Optimize the model
optimizer.zero_grad()
loss.backward()
for param in policy_net.parameters():
param.grad.data.clamp_(-1, 1)
optimizer.step()
num_episodes = 5000
for i_episode in range(num_episodes):
# Initialize the environment and state
env.reset()
last_screen = get_screen()
current_screen = get_screen()
state = current_screen - last_screen
for t in count():
# Select and perform an action
action = select_action(state)
_, reward, done, _ = env.step(action.item())
reward = torch.tensor([reward], device=device)
# Observe new state
last_screen = current_screen
current_screen = get_screen()
if not done:
next_state = current_screen - last_screen
else:
next_state = None
# Store the transition in memory
memory.push(state, action, next_state, reward)
# Move to the next state
state = next_state
# Perform one step of the optimization (on the target network)
optimize_model()
if done:
episode_durations.append(t + 1)
plot_durations()
break
# Update the target network
if i_episode % TARGET_UPDATE == 0:
target_net.load_state_dict(policy_net.state_dict())
print('Complete')
env.render()
env.close()
plt.ioff()
plt.show()