DRL基础(十)——深度Q网络 (DQN)完全教程【附代码】

本文详细介绍了深度Q网络(DQN)的基本原理,包括Q-learning的不足、神经网络拟合Q函数(NFQ)的概念以及DQN的改进策略。通过使用经验重放池和目标网络,DQN解决了连续状态空间的问题。文章还提供了基于PyTorch的DQN智能体实现,用于控制月球车着陆,并提供了完整的代码下载链接。
摘要由CSDN通过智能技术生成

主要内容

  • 讲解DQN基本理论,
  • 基于Pytorch实现DQN算法
  • 开发智能体控制月球车着陆
  • DQN完整代码下载链接见文末

1 DQN基本原理

DQN算法是David Silver等人2015年在《Nature》发表的一个工作,报告了他们训练的DQN智能体在几十款Atari视频游戏中超越了人类的平均水平。而且他们的智能体是基于游戏画面做决策的,即和人类一样,这令很多人都非常的震惊。并且他们使用同一个智能体玩几十款游戏,因此该智能体具有一定的通用型,让人们看到了通用人工智能AGI的可能性。

1.1 Q-learnig等表格型RL算法的不足

Q-learning是一种表格型强化学习算法,可以解决规模不大的具有离散状态和动作空间的问题。但是,自然界更多的问题具有连续的状态和动作空间。或者,状态和动作的规模过于庞大,用表进行处理的存储空间和时间都无法实现。

在这里插入图片描述

1.2 神经网络拟合Q函数(NFQ)

为了应对连续装状态的问题,使用神经网络来拟合Q函数,可以估计任意状态下执行各个动作的未来奖励和的期望。这种想法其实很早就有了,且被称为Neural Fitted Q iteration (NFQ, Riedmiller, 2005)。

将神经网络的参数记为 w w w,则价值函数 Q ( s , a ; w ) Q(s,a;w) Q(s,a;w)的特性是由参数 w w w唯一确定的。

回顾Q-learning的更新:
Q ( s , a ) ← Q ( s , a ) + α [ r + γ Q ( s ′ , a ′ ) − Q ( s , a ) ] Q(s,a)\leftarrow Q(s,a)+\alpha\left[r+\gamma Q(s^{\prime},a^{\prime})-Q(s,a)\right] Q(s,a)Q(s,a)+α[r+γQ(s,a)Q(s,a)]

现在没有表格了,Q函数的输入输出是由神经网络的参数 w w w决定的,需要更新参 w w w来更新Q函数。那么怎么调整神经网络的参数 w w w呢?在NFQ中使用了一个叫做target的概念,表达式是这样的:
t a r g e t = r + γ Q ( s ′ , a ′ ) target=r+\gamma Q(s^\prime,a^\prime) target=r+γQ(s,a)
这个target是根据下个状态和即使奖励获得的,如果Q函数对当前的状态估计的足够准确,那么二者因该是相等的:
Q ( s , a ) = t a r g e t Q(s,a)=target Q(s,a)=target
因此,构造误差函数
e r r o r = [ Q ( s , a ) − [ r + γ Q ( s ′ , a ′ ) ] ] 2 error=\left[Q(s,a)-\left[r+\gamma Q(s^{\prime},a^{\prime})\right]\right]^2 error=[Q(s,a)[r+γQ(s,a)]]2
要想办法减小该误差。方法就是梯度下降。

怎么组织数据呢?按照 ⟨ s , a , r , s ′ ⟩ \langle s,a,r,s^\prime\rangle s,a,r,s这样的格式组织数据,形成样本集合 D \mathcal D D。这样就可以off-line的方式进行学习。

1.3 DQN算法

DQN与NFQ一脉相承,Riedmiller也是DQN的作者之一。

DQN作了这些方面的改进:

  • 使用经验重放池 D \mathcal D D,每次从经验池中随机采样样本进行学习,打破样本之间的相关性;
  • 使用带延迟的target网络 w − w^- w,稳定训练过程。每隔一定时间用 w w w去更新 w − w^- w

DQN算法的工作流程,可以用下面的图表示
在这里插入图片描述

经验池的构建:智能体与环境交互,并将经验数据按照 ⟨ s , a , r , s ′ ⟩ \langle s,a,r,s^\prime\rangle s,a,r,s的形式存储到经验重放池中。假设经验重放池中有 N N N条样本,则经验池可以表示为 D = { s t , a t , r t + 1 , s t + 1 } t = 0 N − 1 D=\left\{s_t,a_t,r_{t+1},s_{t+1}\right\}_{t=0}^{N-1} D={st,at,rt+1,st+1}t=0N1

智能体训练:从经验池 D \mathcal D D中随机采样一个mini-batch,计算损失函数,即单步时间差分误差的平方:

L ( w ) = ( y − Q ( s , a ; w ) ) 2 \mathcal{L}(w)=\left(y-Q(s,a;w)\right)^2 L(w)=(yQ(s,a;w))2

其中
y = { r , 如果 s ′ 是终止状态 r + γ max ⁡ a ′ Q ( s ′ , a ′ ; w − ) , 其他情况 y=\begin{cases}r,\quad\text{如果}s^\prime\text{是终止状态}\\r+\gamma\max_{a^\prime}Q(s^\prime,a^\prime;w^-),\quad\text{其他情况}\end{cases} y={r,如果s是终止状态r+γmaxaQ(s,a;w),其他情况

考虑非终止状态,
L ( w ) = ( r + γ max ⁡ a ′ Q ( s ′ , a ′ ; w − ) − Q ( s , a ; w ) ) 2 \mathcal{L}(w)=\left(r+\gamma\max_{a^\prime}Q\left(s^\prime,a^\prime;{\color{red}{w^{-}} }\right) - Q(s,a;w)\right)^2 L(w)=(r+γamaxQ(s,a;w)Q(s,a;w))2

进行梯度下降调整神经网络参数时,针对的是 w w w,而 w − {\color{red}{w^-}} w此时是固定住的。那么 w − {\color{red}{w^-}} w是怎么更新的呢?他是通过与 w w w进行同步来更新的。这里具体来说,使用的是一种软更新的技术,即
w − ← ( 1 − ξ ) w − + ξ w {\color{red}{w^-}}\leftarrow(1-\xi){\color{red}{w^-}}+\xi w w(1ξ)w+ξw
其中 ξ > 0 \xi>0 ξ>0是一个很小的正数,称为更新率。

2 编程实现

下面介绍如何使用Pytorch开发一个DQN智能体,并训练它完月球车着陆任务。
在这里插入图片描述

2.1 引入需要用到的库

import gym
import random
import torch
import numpy as np
from collections import deque
import matplotlib.pyplot as plt
%matplotlib inline

2.2 实例化环境和智能体

按照下列代码创建环境

env = gym.make('LunarLander-v2')
env.seed(0)
print('State shape: ', env.observation_space.shape)
print('Number of actions: ', env.action_space.n)
State shape:  (8,)
Number of actions:  4

先来看看这是个什么样的任务。

from dqn_agent import Agent

agent = Agent(state_size=8, action_size=4, seed=0)

# 看看随机选择动作的智能体与环境交互是什么样子
state = env.reset()
for j in range(200):
    action = agent.act(state)
    env.render()
    state, reward, done, _ = env.step(action)
    if done:
        break 
        
env.close()

2.3 训练DQN智能体

下面是实现训练过程的主要程序

def dqn(n_episodes=2000, max_t=1000, eps_start=1.0, eps_end=0.01, eps_decay=0.995):
    """Deep Q-Learning.
    
    Params
    ======
        n_episodes (int): maximum number of training episodes
        max_t (int): maximum number of timesteps per episode
        eps_start (float): starting value of epsilon, for epsilon-greedy action selection
        eps_end (float): minimum value of epsilon
        eps_decay (float): multiplicative factor (per episode) for decreasing epsilon
    """
    scores = []                        # 用于记录智能体的得分
    scores_window = deque(maxlen=100)  # 用于记录最近100幕得分
    eps = eps_start                    # 初始探索率
    for i_episode in range(1, n_episodes+1):
        state = env.reset()
        score = 0
        for t in range(max_t):
            action = agent.act(state, eps)
            next_state, reward, done, _ = env.step(action)
            agent.step(state, action, reward, next_state, done)
            state = next_state
            score += reward
            if done:
                break 
        scores_window.append(score)       # 保存当前幕得分
        scores.append(score)              # 保存当前幕得分
        eps = max(eps_end, eps_decay*eps) # 减小探索率
        print('\rEpisode {}\tAverage Score: {:.2f}'.format(i_episode, np.mean(scores_window)), end="")
        if i_episode % 100 == 0:
            print('\rEpisode {}\tAverage Score: {:.2f}'.format(i_episode, np.mean(scores_window)))
        if np.mean(scores_window)>=200.0:
            print('\nEnvironment solved in {:d} episodes!\tAverage Score: {:.2f}'.format(i_episode-100, np.mean(scores_window)))
            torch.save(agent.qnetwork_local.state_dict(), 'checkpoint.pth')
            break
    return scores

scores = dqn()

# plot the scores
fig = plt.figure()
ax = fig.add_subplot(111)
plt.plot(np.arange(len(scores)), scores)
plt.ylabel('Score')
plt.xlabel('Episode #')
plt.show()
Episode 100	Average Score: -135.01
Episode 200	Average Score: -106.86
Episode 274	Average Score: -91.379

2.4 DQN智能体实现文件dqn_agent.py

文件dqn_agent.py实现了DQN智能体的经验重放池、Q网络、学习方法等。

import numpy as np
import random
from collections import namedtuple, deque

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")


class Agent():
    """DQN智能体,实现能够与环境交互并学习."""

    def __init__(self, state_size, action_size, seed, buffer_size=int(1e5),
                 batch_size=64, gamma=0.99, tau=1e-3, lr=1e-3):
        """初始化智能体.

        参数
        ======
            state_size (int): (观测)状态维度大小
            action_size (int): 动作数量
            seed (int): 随机种子
        """
        self.state_size = state_size
        self.action_size = action_size
        self.seed = random.seed(seed)

        self.BUFFER_SIZE = buffer_size  # 经验池大小
        self.BATCH_SIZE = batch_size  # 训练用的批大小
        self.GAMMA = gamma  # 折扣因子
        self.TAU = tau  # 目标网络更新参数
        self.LR = lr  # 优化器学习参数

        # Q-Network
        self.qnetwork_local = QNetwork(state_size, action_size, seed).to(device)  # 用于和环境交互的神经网络
        self.qnetwork_target = QNetwork(state_size, action_size, seed).to(device)  # 目标神经网络
        self.optimizer = optim.Adam(self.qnetwork_local.parameters(), lr=self.LR)

        # 经验池
        self.buffer = ReplayBuffer(action_size, self.BUFFER_SIZE, self.BATCH_SIZE, seed)
        # 初始化交互次数计数器
        self.t_step = 0

    def add_traj(self, traj):
        """将一条轨迹存入经验池中
        """
        for transition in traj:
            self.buffer.memory.append(transition)

    def act(self, state, eps=0., action_mask=None):
        """完成从状态到动作的映射.
        Params
        ======
            state (array_like): (观测)状态
            eps (float): 即epsilon, 贪婪策略的探索率
        """
        state = torch.from_numpy(state).float().unsqueeze(0).to(device)
        self.qnetwork_local.eval()
        with torch.no_grad():
            action_values = self.qnetwork_local(state)
        self.qnetwork_local.train()

        if action_mask is not None:
            action_values = action_values * torch.tensor(action_mask, dtype=torch.float32).to(device)

        # 按照贪婪策略选择动作
        if random.random() > eps:
            return np.argmax(action_values.cpu().data.numpy())
        elif action_mask is not None:
            return np.random.choice(np.arange(self.action_size), p=action_mask / (np.sum(action_mask)) + 1e-9)
        else:
            return random.choice(np.arange(self.action_size))

    def learn(self):
        """使用经验数据更新两个价值网络的参数。
           这是DQN的核心部分。

        Params
        ======
            experiences (Tuple[torch.Tensor]): 按照(s, a, r, s', done) 这样的格式组织的经验数据
            gamma (float): 折扣因子
        """
        if len(self.buffer) < 10 * self.BATCH_SIZE:
            return 0
        experiences = self.buffer.sample()
        states, actions, rewards, next_states, dones = experiences
        # 使用目标网络估计下个状态的价值
        Q_next = self.qnetwork_target(next_states).detach().max(1)[0].unsqueeze(1)
        y = rewards + (self.GAMMA * Q_next * (1 - dones))  # 使用单步TD估计当前状态的价值
        Q = self.qnetwork_local(states).gather(1, actions)  # 使用局部网络直接估计当前状态的价值
        loss = F.mse_loss(Q, y)  # 计算TD误差,作为损失函数
        # 用优化器优化神经网络
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        # ------------------- 更新目标网络参数 ------------------- #
        self.soft_update(self.qnetwork_local, self.qnetwork_target, self.TAU)

    def soft_update(self, local_model, target_model, tau):
        """两个神经网络参数之间的软更新:
        θ_target = τ*θ_local + (1 - τ)*θ_target

        Params
        ======
            local_model (PyTorch model): 用于和环境交互的网络模型
            target_model (PyTorch model): 目标网络模型
            tau (float): 更新步长
        """
        for target_param, local_param in zip(target_model.parameters(), local_model.parameters()):
            target_param.data.copy_(tau * local_param.data + (1.0 - tau) * target_param.data)


class ReplayBuffer:
    """用于存储智能体与环境交互的经验"""

    def __init__(self, action_size, buffer_size, batch_size, seed):
        """Initialize a ReplayBuffer object.

        Params
        ======
            action_size (int): 动作数量
            buffer_size (int): 经验池大小
            batch_size (int): 训练
            seed (int): 随机种子
        """
        self.action_size = action_size
        self.memory = deque(maxlen=buffer_size)
        self.batch_size = batch_size
        self.transition = namedtuple("Transition", field_names=["state", "action", "reward", "next_state", "done"])
        self.seed = random.seed(seed)

    def sample(self):
        """从经验池中随机采样一定数量的样本."""
        experiences = random.sample(self.memory, k=self.batch_size)

        states = torch.from_numpy(np.vstack([e.state for e in experiences if e is not None])).float().to(device)
        actions = torch.from_numpy(np.vstack([e.action for e in experiences if e is not None])).long().to(device)
        rewards = torch.from_numpy(np.vstack([e.reward for e in experiences if e is not None])).float().to(device)
        next_states = torch.from_numpy(np.vstack([e.next_state for e in experiences if e is not None])).float().to(
            device)
        dones = torch.from_numpy(np.vstack([e.done for e in experiences if e is not None]).astype(np.uint8)).float().to(
            device)

        return (states, actions, rewards, next_states, dones)

    def __len__(self):  # 方便别的地方使用len()获取经验池汇中transition的数量
        return len(self.memory)


class QNetwork(nn.Module):
    """价值估计网络."""

    def __init__(self, state_size, action_size, seed, hidden_size_1=256, hidden_size_2=128):
        """初始化价值网络.
        Params
        ======
            state_size (int): 状态的维度大小
            action_size (int): 动作空维度数量
            seed (int): 随机种子
            hidden_size_1 (int): 第隐藏曾1的神经元数量
            hidden_size_2 (int): 第隐藏曾2的神经元数量
        """
        super(QNetwork, self).__init__()
        self.seed = torch.manual_seed(seed)
        self.input_layer = nn.Linear(state_size, hidden_size_1)
        self.hidden_layer = nn.Linear(hidden_size_1, hidden_size_2)
        self.output_layer = nn.Linear(hidden_size_2, action_size)

    def forward(self, state):
        """数据的前向传播"""
        x = F.relu(self.input_layer(state))
        x = F.relu(self.hidden_layer(x))
        return self.output_layer(x)

2.5 观看训练好的智能体

# 加载训练好的神经网络模型
agent.qnetwork_local.load_state_dict(torch.load('checkpoint.pth'))

for i in range(3):
    state = env.reset()
    for j in range(200):
        action = agent.act(state)
        env.render()
        state, reward, done, _ = env.step(action)
        if done:
            break 
            
env.close()

LunarLander-V2

3 代码下载

DQN源码下载地址:
链接:https://pan.baidu.com/s/1JMA46DvE7JuqVpKLUnTCtA
提取码:bb4m

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二向箔不会思考

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值