DQN及其变种
Deep Q Network
Q learning 回顾
Q学习是一种表格型学习、off-policy的TD算法,去学习一个最优的动作-状态值函数。
- 表格型学习:策略最简单的表示,离散状态动作空间,查找表(look-up table),表格型策略,即使用查找表的强化学习法方法。最终我们会得到一个学习好的策略表格。
- off-policy:与on-policy对立,指其存在两种不同的策略,包括目标策略和行为策略,目标策略指我们需要去学习的策略,而行为策略是探索环境采样的策略。
- TD算法:指时序差分预测方法,更新目标。
$Q(S_t,A_t) \gets Q(S_t,A_t) + \alpha[{R_{t+1}} + \gamma {Q(S_{t+1},A_{t+1})}-Q(S_t,A_t)]$
- 行为策略:epsion贪心(软性)
- 目标策略:$a^*=\pi(S_{t+1})=\underset{a}{\arg\max}{Q(S_{t+1},a)}$(确定性策略)
- 更新公式:
$Q(S_t,A_t) \gets Q(S_t,A_t) + \alpha[{R_{t+1}} + \gamma \underset{a}\max{Q(S_{t+1},a)}-Q(S_t,A_t)]$
背景与简介
- Q学习的维度灾难:在现实中基本不可行,因为状态太多了,使用表格的方法根本无法存储。
- 比如说Atari游戏:输入是原始的图像数据,210x160 pixels的图片+几个离散的按键动作,每个pixel有256个可能的值,那么总共的状态空间达到了256的210x160次方这么大,这显然不可能由表格来存储
- Q学习针对的是低维离散的状态和动作空间。
- 解决方法:价值函数近似,通过矩阵运算降维输出单值Q。本质上用一个函数来近似Q的分布
$Q(s,a) \approx f(s,a,w) = w_1s+w_2a +b$
- 最终:只把状态s作为输入,输出每一个动作的Q值$[Q(s,a_1),Q(s,a_2),…,Q(s,a_n)]$
$Q(s) \approx f(s,w)$
DQN
DQN算法流程
- 关键:使用深度神经网络来表示价值函数近似函数f,也就是说,Q值变成用Q网络来表示。
- DQN的全称是deep Q-network,顾名思义,是一种深层神经网络的算法,用来预测Q值的大小。Q值可以理解为状态动作价值,即智能体在某一状态下执行该动作所带来的预期收益。
- 深度网络与强化学习之间的融合:
- 深度网络:需要大量的标签数据,其每个样本之间相互独立
- 强化学习:仅通过环境返回奖励,存在样本稀疏的问题,当前状态值依赖后面的状态返回值
- 通过Q-Learning的原理来构造标签数据(目标数据):
$Target = {R_{t+1}} + \gamma \underset{a}\max{Q(S_{t+1},a)}$
由此我们可以得到Q网络训练的损失函数:
$L(w) = \mathbb{E}[(r + \gamma\max_{a’}Q(s’,a’,w) - Q(s,a,w))^2]$
- DQN算法(NIPS 2013):
DQN Tips
- Experience Replay(经验回放):Q-learning是一种off-policy的离线学习法,可以学习当前经历的,也能学习过去经历过的,在学习过程中随机加入之前的经验会让神经网络更有效率,且解决了样本间相关性的问题。
- 通过缓存每一步状态、动作、奖励、下一状态四元组,在一回合结束后批量训练多次,实现维护一个指定大小的数组,每回合用新产生的N个状态、动作、奖励、下一状态元组随机替换掉缓存池中现有的N个,然后再回合结束后做数次训练。
- Fixed Q-targets:复制一个和原本Q网络一模一样的Target-Q网络用于计算target值,使得target-Q网络的参数在一定时间段内保持固定不变,在这段时间内用梯度下降训练Q网络。然后阶段性地根据Q网络学习完后的参数更新这个target-Q网络(就是把Q网络的参数再复制过去)。
https://zhuanlan.zhihu.com/p/21421729
https://blog.csdn.net/qq_30615903/article/details/80744083
Double DQN
Overestimate
指对一系列数先求最大值再求平均,通常比先求平均再求最大值要大或相等
$E(\max(X1,X2,…))\geq \max(E(X1),E(X2),…)$
DQN的更新:
$L(w) = \mathbb{E}[(r + \gamma\max_{a’}Q(s’,a’,w) - Q(s,a,w))^2]$
可以看到,最优值函数的更新依赖于maxQ(s,…),而从损失函数可以看出,是将N个值先取max操作后,再进行更新的,因此会比县计算出N个Q取期望后再max要大。也就是说,更新中的Q会比真实的Q值大。
Double Q-learning
- Q-learning: 使用单估计器去估计下一个状态
- Double Q-learning:使用两个估计器去估计,每个Q估计器会使用另一个Q估计器的值来更新下一个状态,也就是从不同的经验子集中学习,且执行的动作可以同时使用两个值函数。
Double DQN的更新
继承Double Q-learning双Q估计器的思想,而DQN的构架中本来就存在online network和target network,正好适用于这个方法。
- DQN:在使用贪心算法预估下一个action时,更新价值网络取最大也是使用的同一套参数,这使之更容易过估计。
- DDQN:分别使用两个网络来分别进行选择动作和更新价值网络这两个工作,其中$\theta_t^-$为target network参数;$\theta_t$为online network参数
$Target_{DDQN} = R_{t+1} + \gamma Q(S_{t+1}, \argmax_aQ(S_{t+1},a;\theta_t);\theta_t^-)$
(D)DQN in Atari Games
env: Breakout -v4
- Observation space:RGB图像
- Action space: Reduced action space
- Reward:
DQN Agent
- 网络构架:三层CNN + 2层全链接
- new_height = (input_height - filter_height + 2*padding)/stride + 1
- new_width = (input_width - filter_width + 2*padding)/stride + 1
- new_depth = filter_num
- 输入:前4帧预处理过的84x84图像。4(D) 84(H) 84(W)
- 输出:4维的Q值,对应四个动作
- Replay buffer
- Epsilon-greedy
- 软更新:θ_target = τθ_local + (1 - τ)θ_target
Dueling DQN
在传统 DQN 的基础上只进行了微小的改动,但却能大幅提升 DQN 的表现。在强化学习中,我们将状态动作价值函数减去状态价值函数的结果定义为优势函数A
$A(s,a)=Q(s,a)-V(s)$
在同一个状态下,所有动作的优势值之和为 0,因为所有动作的动作价值的期望就是这个状态的状态价值。据此,在 Dueling DQN 中,Q 网络被建模为:
$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)$
其中,$V_{\eta,\alpha}$为状态价值函数,而$A_{\eta,\beta}(s,a)$则为该状态下采取不同动作的优势函数,表示采取不同动作的差异性;$\eta$是状态价值函数和优势函数共享的网络参数,一般用在神经网络中,用来提取特征的前几层;而$\alpha$和$\beta$分别为状态价值函数和优势函数的参数。在这样的模型下,我们不再让神经网络直接输出$Q$值,而是训练神经网络的最后几层的两个分支,分别输出状态价值函数和优势函数,再求和得到$Q$值
将状态价值函数和优势函数分别建模的好处在于:某些情境下智能体只会关注状态的价值,而并不关心不同动作导致的差异,此时将二者分开建模能够使智能体更好地处理与动作关联较小的状态。
如图所示,这是Atari game中的一个赛车游戏,Value表示状态价值,Advantage表示动作优势值,图中红色部分表示注意力。当前方没有车辆时,智能体左右移动并没有影响,说明动作对Q值没有影响,但是状态对Q值很有影响。从上面两幅图可以看出,Value更加关注远方道路,因为开的越远,对应的状态值越大;Advantage没有特别注意的地方,说明动作没有影响。当前方存在车辆阻挡时,智能体的动作选择至关重要,说明动作对Q值存在影响,同样状态对Q值也会存在影响。从下面两幅图可以看出,Value同样更加关注远方道路,但是Advantge此时会关注前方车辆,因为如果不采取相应动作,智能体很有可能会发生碰撞,分数就会很低。
对于 Dueling DQN 中的公式,存在对于$V$值和$A$值建模不唯一性的问题。例如,对于同样的$Q$值,如果将$V$值加上任意大小的常数$C$,再将所有$A$值减去$C$,则得到的$Q$值依然不变,这就导致了训练的不稳定性。为了解决这一问题,Dueling DQN 强制最优动作的优势函数的实际输出为 0
$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)-max_{a’}A_{\eta,\beta}(s,a’)$
可以确保$V$值建模的唯一性,在实现时用平均代替最大化
$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)-\frac{1}{|A|}\sum _{a’}A_{\eta,\beta}(s,a’)$
虽然它不再满足贝尔曼最优方程,但实际应用时更加稳定。
代码
网络构造
def __init__(self, action_dim, device):
super(DQN, self).__init__()
self.__conv1 = nn.Conv2d(4, 32, kernel_size=8, stride=4, bias=False)
self.__conv2 = nn.Conv2d(32, 64, kernel_size=4, stride=2, bias=False)
self.__conv3 = nn.Conv2d(64, 64, kernel_size=3, stride=1, bias=False)
self.__fc1_a = nn.Linear(64*7*7, 512)
self.__fc1_v = nn.Linear(64*7*7, 512)
self.__fc2_a = nn.Linear(512, action_dim)
self.__fc2_v = nn.Linear(512, 1)
# self.__fc3 = nn.Linear(512, 1)
self.__act_dim = action_dim
self.__device = device
def forward(self, x):
x = x / 255.
x = F.relu(self.__conv1(x))
x = F.relu(self.__conv2(x))
x = F.relu(self.__conv3(x))
xv = x.view(x.size(0), -1)
vs = F.relu(self.__fc1_v(xv))
vs = self.__fc2_v(vs).expand(x.size(0), self.__act_dim)#V
asa = F.relu(self.__fc1_a(xv))
asa = self.__fc2_a(asa)#A
# return vs + asa - torch.mean(asa,-1,True)
return vs + asa - asa.mean(1).unsqueeze(1).expand(x.size(0),self.__act_dim)#Q
def learn(self, memory: ReplayMemory, batch_size: int) -> float:
"""learn trains the value network via TD-learning."""
state_batch, action_batch, reward_batch, next_batch, done_batch = \
memory.sample(batch_size)
values = self.__policy(state_batch.float()).gather(1, action_batch)
values_next = self.__target(next_batch.float()).max(1).values.detach()
expected = (self.__gamma * values_next.unsqueeze(1)) * \
(1. - done_batch) + reward_batch
loss = F.smooth_l1_loss(values, expected)
self.__optimizer.zero_grad()
loss.backward()
for param in self.__policy.parameters():
param.grad.data.clamp_(-1, 1)
self.__optimizer.step()
return loss.item()
Hindsight Experience Replay(HER)
开发了解决奖励稀疏的另一个途径,大致思想是奖励稀疏的时候可能绝大多数的探索都是失败的,这里考虑把这些失败的探索都利用起来。比如智能体想去食堂,某一次探索失败了,没去到食堂,但是走到操场了,与其认为这一次失败的探索,不如记下来这样走可以到操场,这样更加充分地利用了经历的失败。
函数$f_g:S\rightarrow {0,1}$表示在目标$g$下,任意一个状态是否达到该目标
奖励函数:$r_g(s,a)$
经验池:相比于普通的经验池,该经验池中附加存放了相应的目标$(s_t||g,a_t,r_{g,t},s_{t+1}||g)$,此时行为策略也与目标相关,输入为当前状态和所需完成的目标,写为$\pi _b(s_t||g)$
经验池的构建:
1.基于实际目标g采样完整序列
要采样M个完整的序列。对于任意一个序列,我们首先采样它的初始状态和目标状态,因为此时每个序列的目标是不同的,我们要根据不同的目标来选择动作,所以动作的采样同时基于当前的状态s和目标g:
$a_t \leftarrow \pi_b(s_t||g)$
同时,基于状态s、动作a以及目标g来计算奖励r:
$r_t=r(s_t,a_t,g)$
因此保存的每一条经验可以由五部分组成:当前状态s,采取的动作a,即时奖励r,下一个状态s’,当前的目标g
2.通过变换目标得到新的经验
对于每一个序列,可以得到一些额外的经验,我们通过一些方法来得到一个新的目标g’
根据g’,以及t时刻的状态s,t时刻的动作a,来计算新的奖励$r’$:
$r’=r(s_t,a_t,g’)$
并将$(s_t||g,a_t,r’,s_{t+1}||g)$存入经验池中
3.新目标的获取方法
主要有4种新目标的获取方式
- 未来模式——future:在一个序列$s_0,s_1,s_2,⋯,s_T$中,如果遍历到状态$s_2$,则在$s_3,⋯,s_T$之间随机抽取$k$个状态作为目标状态$g’$,并依此向经验池中存入($s_2||g’,a_2,r_2’,s_3||g’$),特点:一个episode的后续部分
- 回合模式——episode:在一个序列$s_0,s_1,s_2,⋯,s_T$中,如果遍历到状态$s_2$,则在整个序列中随机抽取$k$个状态作为目标状态$g’$,并依此向经验池中存入$s_2||g’,a_2,r_2’,s_3||g’$,特点:一个episode
- 随机模式——random:在一个序列$s_0,s_1,s_2,⋯,s_T$中,如果遍历到状态$s_2$,则在多个序列$τ_0,τ_1,τ_2,⋯$中随机抽取$k$个状态作为目标状态$g’$,并依此向经验池中存入$s_2||g’,a_2,r_2’,s_3||g’$,特点:多个episode
- 最终模式——final:在一个序列$s_0,s_1,s_2,⋯,s_T$中,如果遍历到状态$s_2$ ,则之间令$g’=s_T$,并向经验池中存入$s_2||g’,a_2,r_2’,s_3||g’$,特点:一个episode的最后一个状态,如果设置k,则存入k个相同的经验
环境
- 名称:bit-flipping environment
- 状态空间$S={0,1}^n$
- 动作空间$A={0,1,⋯,n−1}$
- 规则:对于每个episode,均匀采样长度为$n$的初始状态$s0$(如n=5,s0=10101)和目标状态$sg$,每一步从动作空间中选取一个动作$a$,翻转$s_0$第$a$个位置的值,如$a=1⇒s1=11101$,直到回合结束或者翻转后的状态与$s_g$相同
- 奖励函数:$rg(s,a)=−[s≠g]$,即达到目标状态则为0,未达到目标状态则为-1。这个很容易理解,$s≠g⇒true≐1,s=g⇒false≐0$
代码
class nbit_flip:
def __init__(self,stringlength=8,goal=[]):
self.goalstring = np.random.choice([0,1],stringlength)
self.startstring = np.random.choice([0,1],stringlength)
while (self.startstring == self.goalstring).all():
self.startstring = np.random.choice([0,1],stringlength)
self.statestring = self.startstring
def random_action(self):
return np.random.randint(0,self.goalstring.shape[0])
def reset(self):
self.goalstring = np.random.choice([0,1],self.goalstring.shape[0])
self.startstring = np.random.choice([0,1],self.goalstring.shape[0])
while (self.startstring == self.goalstring).all():
self.startstring = np.random.choice([0,1],self.goalstring.shape[0])
self.statestring = self.startstring
return np.concatenate((self.statestring,self.goalstring))#将状态与目标结合
def step(self,action):
self.statestring[action] = np.logical_not(self.statestring[action]).astype(int)
if (self.statestring == self.goalstring).all():
done = True
reward = 0#成功时奖励为0,否则为-1
else:
done = False
reward = -1
return np.concatenate((self.statestring,self.goalstring)), reward, done#将状态与目标组合输出
bits = 20 # number of bit for bitflipping game
A1 = nn.layer(bits*2,256)
A1.f = nn.f_relu
AOUT = nn.layer(256,bits)
AOUT.f = nn.f_iden
L1 = nn.layer(bits*2,256)
L1.f = nn.f_relu
LOUT = nn.layer(256,bits)
LOUT.f = nn.f_iden
onlineNet = nn.mlp([A1,AOUT]) #if u r numpy_net instead of mlp_framework it's nn.module([...])
for L in onlineNet.Layerlist:
try:
L.eta = 1e-3
except:
pass
targetNet = nn.mlp([L1,LOUT]) #if u r numpy_net instead of mlp_framework it's nn.module([...])
for L in targetNet.Layerlist:
try:
L.eta = 1e-3
except:
pass
HER = True ## toggle Hindsight Experience Replay
loss_plot = []
rew_plot = []
ringB = DF.ringbuffer(1e5)
MAX_EPOCHS = 6
MAX_CYCLES = 50
MAX_EPISODES = 16
MAX_STEPS = bits
UPDATE_STEPS = 40
EXPLORATION = 500
BATCHSIZE = 128
GAMMA = 0.98
E_MIN = 0.02
game = nbit_flip(bits)
step_count = 0
success_rate = []
for E in range(MAX_EPOCHS):
#run cycle
for c in range(MAX_CYCLES):
successes = []
#run episodes
for e in range(MAX_EPISODES):
eps_reward = 0
episode_buffer = []
current_state = game.reset()
#run game
for p in range(MAX_STEPS):
e = 1. / (((step_count)/EXPLORATION)+1)
if np.random.uniform(0,1) < max(E_MIN,e):
action = game.random_action()
else:
action = np.argmax(onlineNet.infer(current_state[True,:]))
next_state, reward, done = game.step(action)
eps_reward += reward
step_count += 1
episode_buffer += [(current_state, action, reward, next_state,done)]
current_state = next_state
if done or (p+1)==MAX_STEPS:
successes += [done]
rew_plot += [eps_reward]
break
#run replay
for r in range(p):
# add each episode step to RPB
s = np.array(episode_buffer[r][0])
a = np.array([episode_buffer[r][1]])
rew = np.array([episode_buffer[r][2]])
s_ = np.array(episode_buffer[r][3])
d = np.array([episode_buffer[r][4]])
ringB.add([s,a,rew,s_,d])
#run hindsight experience replay
if HER:
# take last state as goal and add (whole) episode to RPB
g = np.array(episode_buffer[p-1][3][:bits])#g'
s = np.concatenate((s[:bits],g))
if (s_[:bits] == g).all():
rew = np.array([0])
d = np.array([True])
else:
rew = np.array([-1])
d = np.array([False])
s_ = np.concatenate((s_[:bits],g))
ringB.add([s,a,rew,s_,d])
if (c+1) % 10 == 0: print(np.mean(successes),' cycle success rate')
success_rate += [np.mean(successes)]
#run training
if step_count > BATCHSIZE:
for u in range(UPDATE_STEPS):
BATCH = ringB.sample(BATCHSIZE)
DF.train_bellman(onlineNet, targetNet, BATCH, GAMMA)
loss_plot += [onlineNet.loss]
#update target net
DF.update_target(onlineNet,targetNet)
print(np.mean(success_rate[-MAX_CYCLES:]),' epoch {}/{} success rate'.format(E+1,MAX_EPOCHS))
参考:
https://blog.csdn.net/weixin_46133643/article/details/121880738
https://hrl.boyuai.com/chapter/2/dqn%E6%94%B9%E8%BF%9B%E7%AE%97%E6%B3%95/
https://github.com/boyu-ai/Hands-on-RL/
https://zhuanlan.zhihu.com/p/100431847
https://zhuanlan.zhihu.com/p/51357496
https://cloud.tencent.com/developer/article/1541893