DQN算法是强化学习中最经典的算法之一,是DeepMind发表在nature上的第一篇论文,论文的链接如下:
Human-level control through deep reinforcement learning | Nature
算法的大体框架是Qlearning,首先回顾一下Qlearning算法:
Qlearning是异策略时间差分法,伪代码如下:
这里有两个概念:异策略和时间差分
异策略:指行动策略(产生数据的策略)和要评估的策略不是一个策略。在上述伪代码中,行动策略(产生数据的策略)是第5行的策略,而要评估和改进的策略是第6行的贪婪策略(每个状态取值函数最大的那个动作)
时间差分方法:指利用时间差分目标来更新当前行为值函数,在上述伪代码中,时间差分目标为
DQN算法
DQN是在Qlearning基础上修改得到的,主要体现在以下三个方面:
(1)DQN利用深度卷积神经网络逼近值函数;
(2)DQN 利用经验回放训练强化学习的学习过程;
(3)DQN独立设置了目标网络来单独处理时间差分算法中的TD偏差;
具体介绍如下:
(1)DQN利用深度卷积神经网络逼近值函数
DQN的⾏为值函数利⽤神经⽹络逼近,属于⾮线性逼近。此处的值函数对应着⼀组参数,在神经⽹络⾥参数是每层⽹络的权重,用表示。用公式表示的话,值函数,此时更新值函数其实是更新参数,当网络结构确定时,就代表值函数。DQN所用的网络结构是三个卷积层加两个全连接层,整体框架如下图:
DeepMind将神经科学的成果应用到了深度神经网络的训练之中
(2)DQN利用经验回放训练强化学习过程
通过经验回放为什么可以令神经网络的训练收敛且稳定?
原因是:训练神经⽹络时,存在的假设是训练数据是独⽴同分布的, 但是通过强化学习采集的数据之间存在着关联性,利⽤这些数据进⾏顺序训练,神经⽹络当然不稳定。经验回放可以打破数据间的关联,如下图:
在强化学习过程中,智能体将数据存储到⼀个数据库中,再利⽤均匀随机采样的⽅法从数据库中抽取数据,然后利⽤抽取的数据训练神经⽹络。 这种经验回放的技巧可以打破数据之间的关联性。
(3)DQN独立设置了目标网络来单独处理时间差分算法中的TD偏差
与表格型的Qlearning算法不同的是,利⽤神经⽹络对值函数进⾏逼近时,值函数的更新步更新的是参数,如下:
DQN 利⽤了卷积神经⽹络。其更新⽅法是梯度下降法。因此图Qlearning中伪代码第6⾏值函数更新实际上变成了监督学习的⼀次更新过程,其梯度下降法为:
其中,为TD目标,在计算值时用到的网络参数为。
在DQN算法出现之前,利用神经网络逼近值函数时,计算TD目标的动作值函数所用的网络参数,与梯度计算中要逼近的值函数所用的网络参数相同,这样就容易导致数据间存在关联性,从而使训练不稳定。为了解决此问题,DeepMind提出计算TD目标的网络表示为;计算值函数逼近的网络表示为;用于动作值函数逼近的网络每一步都更新,而用于计算TD目标的网络则是每个固定的步数更新一次。
因此值函数的更新变为:
DQN伪代码如下:
下面对DQN伪代码逐行说明:
[1] 初始化回放记忆D,可容纳的数据条数为N;
[2] 利用随机权值 初始化动作-行为值函数Q;
[3] 令 初始化,计算TD目标的动作行为值Q;
[4] 循环每次事件;
[5] 初始化事件的第一个状态,通过预处理得到状态对应的特征输入;
[6] 循环每个事件的第一步;
[7] 利用概率 选一个随机动作 ;
[8] 若小概率事件没发生,则用贪婪策略选择当前值函数最大的那个动作;
[9] 在仿真器中执行动作,观测回报以及图像;
[10] 设置,预处理;
[11] 将转换储存在回放记忆D中;
[12] 从回放记忆D中均匀随机采样一个转换样本数据,用表示;
[13] 判断是否是一个事件的终止状态,若是,则TD目标为 ,否则利用TD目标网络计算TD目标;
[14] 执行一次梯度下降算法
[15] 更新动作值函数逼近的网络参数;
[16] 每隔C步更新一次TD网络权值,即令;
[17] 结束每次事件内循环;
[18] 结束事件间循环。
可以看出:在第[12]行利用了经验回放;[13]利用了独立的目标网络;第[15]行更新动作值函数逼近网络参数;[17]行更新目标网络参数。
PyTorch实现DQN算法
首先定义了一个名为DQN的神经网络模型,它有一个输入层、两个隐藏层和一个输出层;
接着定义了一个Agent类,它包含了DQN模型、一个经验池、一个优化器和其他一些变量;Agent类中还定义了一系列方法,包括选择动作、存储经验、训练网络和更新目标网络等;
最后,定义了一个train_dqn函数,用于训练DQN模型。
在训练过程中,使用CartPole-v1环境作为示例,该环境是一个倒立摆平衡问题。它由一个可以倒置的杆和一个可以在杆的一端移动的小车组成。任务是使小车在杆倒置之前保持杆的平衡。环境的状态由小车的位置、速度、杆的角度和角速度组成。行动空间只有两个,向左或向右移动小车。奖励是每个时间步骤的1,目标是使奖励的总和最大化。
在train_dqn函数中,首先重置环境,然后在每个时间步中选择一个action并观察environment返回的下一个state和reward。将这些信息存储在经验池中,并使用随机选择的一批经验来训练DQN模型。在每个训练周期结束时,使用目标网络更新策略网络,以便更好地平衡估计误差和方差。
安装相关库:
pip install gym==0.25.2 torch tqdm tensorboard pygame
完整代码如下:
import gym
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
import numpy as np
import random
from collections import deque
from tqdm import tqdm
class DQN(nn.Module):
def __init__(self, state_dim, action_dim):
super(DQN, self).__init__()
self.fc1 = nn.Linear(state_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.fc3 = nn.Linear(64, action_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
class Agent():
def __init__(self, state_dim, action_dim, memory_size=10000, batch_size=64, gamma=0.99, lr=1e-3):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.state_dim = state_dim
self.action_dim = action_dim
self.memory = deque(
maxlen=memory_size) # deque是一个双端队列,可以在队首或队尾插入或删除元素。在DQN算法中,我们使用deque实现经验池来存储之前的经验,因为它可以在队尾插入新的经验,并在队首删除最老的经验,从而保持经验池的大小不变。
self.batch_size = batch_size
self.gamma = gamma
self.lr = lr
self.policy_net = DQN(state_dim, action_dim).to(self.device)
self.target_net = DQN(state_dim, action_dim).to(self.device)
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=self.lr)
self.loss_fn = nn.MSELoss()
self.steps = 0
self.writer = SummaryWriter()
def select_action(self, state, eps):
if random.random() < eps:
return random.randint(0, self.action_dim - 1)
else:
state = torch.FloatTensor(state).to(self.device)
with torch.no_grad():
action = self.policy_net(state).argmax().item()
return action
def store_transition(self, state, action, reward, next_state, done):
self.memory.append((state, action, reward, next_state, done))
def train(self):
if len(self.memory) < self.batch_size:
return
transitions = random.sample(self.memory, self.batch_size)
batch = list(zip(*transitions))
state_batch = torch.FloatTensor(batch[0]).to(self.device)
action_batch = torch.LongTensor(batch[1]).to(self.device)
reward_batch = torch.FloatTensor(batch[2]).to(self.device)
next_state_batch = torch.FloatTensor(batch[3]).to(self.device)
done_batch = torch.FloatTensor(batch[4]).to(self.device)
q_values = self.policy_net(state_batch).gather(1, action_batch.unsqueeze(1)).squeeze(1)
next_q_values = self.target_net(next_state_batch).max(1)[0]
expected_q_values = reward_batch + self.gamma * next_q_values * (1 - done_batch)
loss = self.loss_fn(q_values, expected_q_values.detach())
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
self.steps += 1
self.writer.add_scalar("Loss", loss.item(), self.steps)
def update_target(self):
self.target_net.load_state_dict(self.policy_net.state_dict())
def train_dqn(env, agent, eps_start=1, eps_end=0.1, eps_decay=0.995, max_episodes=1000, max_steps=1000):
eps = eps_start
for episode in tqdm(range(max_episodes)):
state = env.reset()
for step in range(max_steps):
action = agent.select_action(state, eps)
next_state, reward, done, _ = env.step(action)
agent.store_transition(state, action, reward, next_state, done)
state = next_state
agent.train()
if episode % 20 == 0:
env.render()
if done:
break
agent.update_target()
eps = max(eps * eps_decay, eps_end)
if __name__ == "__main__":
env = gym.make("CartPole-v1")
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
agent = Agent(state_dim, action_dim)
train_dqn(env, agent)