强化学习之入门笔记(三)

强化学习

强化学习之入门笔记(一)
强化学习之入门笔记(二)

DDPG算法

深度策略性梯度算法(Deep Deterministic Policy Gradient,DDPG):适用于连续动作空间
DDPG算法采用Actor-Critic框架,利用深度神经网络近似策略和动作价值函数,利用随机梯度法训练策略网络和价值网络模型中的参数,并通过经验回放和双重网络结构提高学习稳定性
在这里插入图片描述
在Actor-Critic(AC)算法中,Critic网络的任务是评估当前策略并估计价值函数,并通过最小化时序差分误差来优化这个估计 ⇒ \Rightarrow δ t = R t + 1 + γ V ( s t + 1 ) − V ( s t ) \delta_t=R_{t+1}+\gamma V(s_{t+1})-V(s_t) δt=Rt+1+γV(st+1)V(st)TD-Error δ t \delta_t δt衡量了状态值估计与实际获得的回报之间的差异 ⇒ \Rightarrow 如果价值函数估计与实际回报之间的TD-Error较大,减少Critic网络的误差,降低Actor网络的输出概率;如果价值函数估计与实际回报之间的TD-Error较小,减少Critic网络的误差,提高Actor网络的输出概率

DDPG算法使用双重神经网络架构,对于策略函数和价值函数均使用双重神经网络架构(即Online网络和Target网络)引入经验回放机制,Actor与环境交互产生的数据样本存储到经验池中,抽取批量数据样本进行训练

比喻:学生和老师的“错题本”​ → \rightarrow 假设你是一个学生(主Actor网络),每天做题(生成动作),老师(Critic网络)会批改你的答案并打分(Q值)。但老师打分标准经常变,今天说答案A好,明天又说答案B好,导致你学得很混乱。于是,老师决定做一个​“错题本”​​(目标网络),记录过去稳定的评分标准,用来更温和地指导你学习

DDPG共包含4个神经网络,用于近似表示Q值函数和策略

  1. Critic目标网络用于近似估计下一个时刻的状态—动作的Q值函数 Q w ′ ( S t + 1 , π θ ′ ( S t + 1 ) ) Q_{w^{\prime}}(S_{t+1},\pi_{\theta^{\prime}}(S_{t+1})) Qw(St+1,πθ(St+1)),其中下一个时刻的动作值 π θ ′ ( S t + 1 ) \pi_{\theta^{\prime}}(S_{t+1}) πθ(St+1)是通过Actor目标网络近似估计得到的 ⇒ \Rightarrow 当前状态下Q值函数的目标值 y i = r i + γ Q w ′ ( S i + 1 , π θ ′ ( S i + 1 ) ) y_{i}=r_{i}+\gamma Q_{w^{\prime}}(S_{i+1},\pi_{\theta^{\prime}}(S_{i+1})) yi=ri+γQw(Si+1,πθ(Si+1))
  2. Critic训练网络输出当前时刻状态—动作的Q值函数 Q w ( S t , a t ) Q_w(S_t,a_t) Qw(St,at),用于对当前策略进行评价。通过最小化损失值(均方误差损失)来更新Critic网络的参数,Critic网络更新时的损失函数为 loss = 1 N ∑ i ( y i − Q w ( S i , a i ) ) 2 \text{loss}=\frac{1}{N}\sum_i(y_i-Q_w(S_i,a_i))^2 loss=N1i(yiQw(Si,ai))2,其中 a i = π θ ( S i ) + ε a_i=\pi_\theta(S_i)+\varepsilon ai=πθ(Si)+ε ε \varepsilon ε表示行为策略上的探索噪声
  3. Actor目标网络用于提供下一个状态的策略,Actor训练网络则是提供当前状态的策略,结合Critic训练网络的Q值函数可以得到Actor在参数更新时的策略梯度 ∇ π θ J = 1 N ∑ i ∇ a Q w ′ ( s , a ) ∣ s = s i , a = π θ ( s i ) ∇ θ π θ ( s ) ∣ s i \nabla_{\pi_\theta}J=\frac{1}{N}\sum_i\nabla_aQ_{w'}(s,a)|_{s=s_i,a=\pi_\theta(s_i)}\nabla_\theta\pi_\theta(s)|_{s_i} πθJ=N1iaQw(s,a)s=si,a=πθ(si)θπθ(s)si,其中 ∇ a Q w ′ \nabla_aQ_{w'} aQw是Critic提供的“目标信号”(“应该把动作往这个方向改”)、 ∇ θ π θ ( s ) \nabla_\theta\pi_\theta(s) θπθ(s)是Actor内部的“传导机制”(“为了把动作往那个方向改,需要这样调整权重 θ \theta θ”)、 1 N ∑ i \frac{1}{N}\sum_i N1i表示对从经验回放池中获取的 N N N个样本的梯度取平均值
    演员Actor/策略网络 π θ ( s ) \pi_{\theta}(s) πθ(s):给Actor一个状态 s s s,Actor会确定地输出一个具体的动作 a a a,即 a = π θ ( s ) a=\pi_{\theta}(s) a=πθ(s)
    评论家Critic/Q值网络 Q w ′ ( s , a ) Q_{w'}(s,a) Qw(s,a):给Critic一个状态 s s s和一个动作 a a a,Critic会告诉 ( s , a ) (s,a) (s,a)组合的“价值”或“分数”
    演员(Actor) π θ \pi_{\theta} πθ的目标是调整自己的参数 θ \theta θ,使得它在任意状态 s s s时,它所输出的动作 a = π θ ( s ) a=\pi_{\theta}(s) a=πθ(s)能让 Q w ′ ( s , a ) Q_{w'}(s,a) Qw(s,a)的分数尽可能高 ⇒ \Rightarrow Actor的目标函数 J ( θ ) J(\theta) J(θ)是所选动作的期望 Q Q Q值,即 J ( θ ) = E s ∼ ρ [ Q w ′ ( s , a ) ] J(\theta)=\mathbb{E}_{s\sim\rho}[Q_{w'}(s,a)] J(θ)=Esρ[Qw(s,a)],其中 a = π θ ( s ) a=\pi_{\theta}(s) a=πθ(s) ρ \rho ρ表示从经验回放池中采样的状态分布
    Actor通过梯度上升最大化 J ( θ ) J(\theta) J(θ),需要计算 J ( θ ) J(\theta) J(θ)相对于Actor参数 θ \theta θ的梯度 ∇ θ J \nabla_{\theta}J θJ
    链式法则:Actor的参数 θ \theta θ决定了策略 π θ \pi_{\theta} πθ → \to 策略 π θ ( s ) \pi_{\theta}(s) πθ(s)决定了输出的动作 a a a → \to 动作 a a a和状态 s s s决定了Critic的 Q Q Q Q w ′ ( s , a ) Q_{w'}(s,a) Qw(s,a) ⇒ \Rightarrow 如果稍微改变 θ \theta θ,对最终分数 Q Q Q的影响是 ∂ Q ∂ θ = ∂ Q ∂ a ⏟ Q 对 a 的梯度 ⋅ ∂ a ∂ θ ⏟ a 对  θ  的梯度 \frac{\partial Q}{\partial\theta}=\underbrace{\frac{\partial Q}{\partial a}}_{\text{Q 对 a 的梯度}}\cdot\underbrace{\frac{\partial a}{\partial\theta}}_{\text{a 对 }\theta\text{ 的梯度}} θQ= a 的梯度 aQ θ 的梯度 θa

对于Critic目标网络参数 w ′ w' w θ ′ \theta' θ的更新,DDPG通过软更新机制保证参数可以缓慢更新,从而提高学习的稳定性 ⇒ \Rightarrow w ′ ← ξ w + ( 1 − ξ ) w ′ θ ′ = ← ξ θ + ( 1 − ξ ) θ ′ \begin{aligned} w^{\prime} & \leftarrow\xi w+(1-\xi)w^{\prime} \\ \theta^{\prime} & =\leftarrow\xi\theta+(1-\xi)\theta^{\prime} \end{aligned} wθξw+(1ξ)w=←ξθ+(1ξ)θ,其中 w ′ w' w θ ′ \theta' θ是Critic目标网络的参数、 w w w θ \theta θ是Critic训练网络的参数、 ξ \xi ξ表示Critic目标网络更新的速度

Critic目标网络的策略梯度,就是用“过去的你”生成的稳定动作,指导“现在的你”改进策略,防止被Critic的善变评价带跑偏
在这里插入图片描述
DDPG算法的伪代码
在这里插入图片描述

# -*- coding: utf-8 -*-
# @Author  : 楚楚
# @File    : 01DDPG.py
# @Software: PyCharm

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import collections
import random


# 经验回放
class ReplayBuffer(object):
    def __init__(self, capacity):
        """
        :param capacity: 最大容量
        """
        self.buffer = collections.deque(maxlen=capacity)

    def add(self, state, action, reward, next_state, done):
        """
        在队列中添加数据
        :param state: 当前状态
        :param action: 动作
        :param reward: 奖励
        :param next_state: 下一个状态
        :param done: 是否完成
        :return:
        """
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):
        """
        在队列中随机取样
        :param batch_size: batch size
        :return:
        """
        transitions = random.sample(self.buffer, batch_size)

        state, action, reward, next_state, done = zip(*transitions)

        return np.array(state), action, reward, np.array(next_state), done

    def size(self):
        return len(self.buffer)


# 策略网络
class PolicyNet(nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim, action_bound):
        """
        :param state_dim: 状态数量
        :param hidden_dim: 隐层数据的维度
        :param action_dim: 动作数量
        :param action_bound: 环境可以接收的动作最大值
        """
        super(PolicyNet, self).__init__()

        super(PolicyNet, self).__init__()
        self.action_bound = action_bound

        self.fc1 = nn.Linear(state_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, action_dim)

    def forward(self, x):
        x = self.fc1(x)  # [b,state_dim]-->[b,hidden_dim]
        x = F.relu(x)
        x = self.fc2(x)  # [b,hidden_dim]-->[b,action_dim]
        x = torch.tanh(x)  # 缩放到 [-1,1]
        x = x * self.action_bound  # 缩放到 [-action_bound, action_bound]

        return x


# 价值网络
class QValueNet(nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim):
        """
        :param state_dim: 状态数量
        :param hidden_dim: 隐层数据的维度
        :param action_dim: 动作数量
        """
        super(QValueNet, self).__init__()

        self.fc1 = nn.Linear(state_dim + action_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, hidden_dim)
        self.fc3 = nn.Linear(hidden_dim, 1)

    def forward(self, state, action):
        """
        :param state: 状态
        :param action: 动作
        :return:
        """

        """
        拼接动作和状态: Q值函数是动作和价值的联合函数
        """
        cat = torch.cat([state, action], dim=1)  # [b, state_dim + action_dim]

        x = self.fc1(cat)  # [b, hidden_dim]
        x = F.relu(x)
        x = self.fc2(x)  # [b, hidden_dim]
        x = F.relu(x)
        x = self.fc3(x)  # [b, 1]
        return x


# DDPG
class DDPG(object):
    def __init__(self, state_dim, hidden_dim, action_dim, action_bound, sigma, action_lr, critic_lr, tau, gamma,
                 device, weight_decay=5e-3):
        """
        :param state_dim: 状态数量
        :param hidden_dim: 隐层数据的维度
        :param action_dim: 动作数量
        :param action_bound: 环境可以接收的动作最大值
        :param sigma: 高斯噪声的标准差
        :param action_lr: 策略网络的learning rate
        :param critic_lr: 价值网络的learning rate
        :param tau: 目标网络的软更新参数
        :param gamma: 折扣因子
        :param device: 设备 cuda or cpu
        :param weight_decay: 权重衰减系数
        """

        # 策略网络 -- 训练
        self.actor = PolicyNet(state_dim=state_dim, hidden_dim=hidden_dim, action_dim=action_dim,
                               action_bound=action_bound).to(device)
        # 价值网络 -- 训练
        self.critic = QValueNet(state_dim=state_dim, hidden_dim=hidden_dim, action_dim=action_dim).to(device)

        # 策略网络 -- 目标
        self.target_actor = PolicyNet(state_dim=state_dim, hidden_dim=hidden_dim, action_dim=action_dim,
                                      action_bound=action_bound).to(device)
        # 价值网络 -- 目标
        self.target_critic = QValueNet(state_dim=state_dim, hidden_dim=hidden_dim, action_dim=action_dim).to(device)

        # 初始化价值网络的参数,两个价值网络的参数相同
        self.target_actor.load_state_dict(self.actor.state_dict())
        # 初始化策略网络的参数,两个策略网络的参数相同
        self.target_critic.load_state_dict(self.critic.state_dict())

        # 策略网络的优化器
        self.actor_optimizer = optim.AdamW(self.actor.parameters(), lr=action_lr, weight_decay=weight_decay)
        # 价值网络的优化器
        self.critic_optimizer = optim.AdamW(self.critic.parameters(), lr=critic_lr, weight_decay=weight_decay)

        self.gamma = gamma  # 折扣因子
        self.sigma = sigma  # 高斯噪声的标准差,均值设为0
        self.tau = tau  # 目标网络的软更新参数
        self.action_dim = action_dim
        self.device = device

    # 动作选择
    def take_action(self, state):
        """
        :param state: 当前状态
        :return:
        """
        # list[state_dim]-->tensor[1,state_dim]
        state = torch.tensor(state, dtype=torch.float32).view(1, -1).to(self.device)

        # 策略网络计算出当前状态下的动作价值 [1,state_dim]-->[1,1]
        action = self.actor(state).item()

        # 给动作添加噪声,增加搜索
        action = action + self.sigma * np.random.randn(self.action_dim)

        return action

    # 软更新,每次learn的时候更新部分参数
    def soft_update(self, net, target_net):
        # 获取训练网络和目标网络需要更新的参数
        for target_parameter, parameter in zip(target_net.parameters(), net.parameters()):
            # 训练网络的参数更新要综合考虑目标网络和训练网络
            """
            调用 target_parameter.data.copy_(source) 时,将 source 张量的数据复制到 target_parameter 张量的 .data 属性中
            可以更新 target_parameter 的值,而不会影响它的梯度信息,因为梯度信息存储在 .grad 属性中,而不是 .data 属性
            """
            target_parameter.data.copy_(target_parameter.data * (1 - self.tau) + parameter.data * self.tau)

    # 训练
    def update(self, transition_dict):
        """
        :param transition_dict: 训练集
        :return:
        """
        states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)  # [b,state_dim]
        actions = torch.tensor(transition_dict['actions'], dtype=torch.float).view(-1, 1).to(self.device)  # [b,1]
        rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device)  # [b,1]
        next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)  # [b,state_dim]
        dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device)  # [b,1]

        # 策略目标网络获取下一时刻的动作
        next_actions = self.target_actor(next_states)
        # 价值目标网络获取下一个时刻动作的价值
        next_q_values = self.target_critic(next_states, next_actions)

        # 当前时刻动作价值的目标值
        q_targets = rewards + self.gamma * next_q_values * (1 - dones)

        # 当前时刻动作价值的预测值
        q_values = self.critic(states, actions)

        # 预测值和目标值之间的均方差损失
        critic_loss = torch.mean(F.mse_loss(q_values, q_targets))
        # 价值网络的梯度
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()

        # 当前状态下选择的动作
        current_actions = self.actor(states)
        # 当前状态下动作的价值
        scores = self.critic(states, current_actions)
        # 计算损失
        # 最小化负的期望回报,即最大化期望回报
        actor_loss = -torch.mean(scores)

        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()

        # 软更新策略网络的梯度
        self.soft_update(self.actor, self.target_actor)
        # 软更新价值网络的梯度
        self.soft_update(self.critic, self.target_critic)

案例:基于 OpenAI 的 gym 环境完成一个推车游戏,目标是将小车推到山顶旗子处。动作维度为1,属于连续值;状态维度为 2,分别是 x 坐标和小车速度
在这里插入图片描述

# -*- coding: utf-8 -*-
# @Author  : 楚楚
# @File    : 02DDPG_OpenAI.py
# @Software: PyCharm

import torch
import numpy as np
import gym
import matplotlib.pyplot as plt
from RL_DDPG import ReplayBuffer, DDPG

# 环境加载
env_name = "MountainCarContinuous-v0"  # 连续型动作
env = gym.make(env_name, render_mode="human")
state_dim = env.observation_space.shape[0]  # 状态数 2
action_dim = env.action_space.shape[0]  # 动作数 1
action_bound = env.action_space.high[0]  # 动作的最大值 1.0

hidden_dim = 32

# 经验回放池最大尺寸
buffer_size = 1024
# 经验回放池的最小尺寸
min_size = 32
# batch size
batch_size = 16
# 高斯噪声的标准差
sigma = 1.0
# 策略网络的学习率
actor_lr = 5e-4
# 价值网络的学习率
critic_lr = 5e-4
# 软更新系数
tau = 0.01
# 折扣因子
gamma = 0.8

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

replay_buffer = ReplayBuffer(capacity=buffer_size)

agent = DDPG(state_dim=state_dim,
             hidden_dim=hidden_dim,
             action_dim=action_dim,
             action_bound=action_bound,
             sigma=sigma,
             tau=tau,
             gamma=gamma,
             action_lr=actor_lr,
             critic_lr=critic_lr,
             device=device)

# -------------------------------------- #
# 模型训练
# -------------------------------------- #

# 记录每个回合的return
return_list = []
# 记录每个回合的return均值
mean_return_list = []

for i in range(10):
    # 每个episode的累计奖励值
    episode_return = 0
    # 初始时的状态
    state = env.reset()[0]
    # 回合结束标记
    done = False

    while not done:
        # 获取当前状态的动作
        action = agent.take_action(state)

        # 环境更新
        next_state, reward, done, _, _ = env.step(action)

        # 更新经验回放池
        replay_buffer.add(state, action, reward, next_state, done)

        # 状态更新
        state = next_state
        # 累计每一步的reward
        episode_return += reward

        # 如果经验池超过容量,开始训练
        if replay_buffer.size() > min_size:
            states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size=batch_size)

            # 构造数据集
            transition_dict = {
                'states': states,
                'actions': actions,
                'rewards': rewards,
                'next_states': next_states,
                'dones': dones,
            }
            # 模型训练
            agent.update(transition_dict)

    # 保存每一个回合的回报
    return_list.append(episode_return)
    mean_return_list.append(np.mean(return_list[-10:]))

    # 打印回合信息
    print(f'iter:{i}, return:{episode_return}, mean_return:{mean_return_list}')

# 关闭动画窗格
env.close()

# -------------------------------------- #
# 绘图
# -------------------------------------- #

x_range = list(range(len(return_list)))

"""
plt.subplot(121):
    第一个参数 1 表示图形窗口中子图的行数(rows)
    第二个参数 2 表示图形窗口中子图的列数(columns)
    第三个参数 1(可选)表示当前激活的子图编号,编号从1开始
"""
plt.subplot(121)
plt.plot(x_range, return_list)  # 每个回合return
plt.xlabel('episode')
plt.ylabel('return')

plt.subplot(122)
plt.plot(x_range, mean_return_list)  # 每回合return均值
plt.xlabel('episode')
plt.ylabel('mean_return')

参考文献

  1. DDPG 模型解析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值