Dueling DQN的理论基础及其代码实现【Pytorch + Pendulum-v0】

Dueling DQN 理论基础

Dueling DQN是一种基于DQN的改进算法,它的主要突破点在于利用模型结构将值函数表示成更细致的形式,使得模型能够拥有更好的表现。

首先我们可以给出如下公式并定义一个新变量:
q ( s t , a t ) = v ( s t ) + A ( s t + a t ) q(s_t,a_t)=v(s_t)+A(s_t+a_t) q(st,at)=v(st)+A(st+at)
也就是说,基于状态和行动的值函数 q q q可以分解成基于状态的值函数 v v v和优势函数 A A A。由于存在
E a t [ q ( s t , a t ) ] = v ( s t ) E_{a_t}[q(s_t,a_t)]=v(s_t) Eat[q(st,at)]=v(st)
所以如果所有状态行动的值函数不相同,一些状态行动价值 q ( s , a ) q(s,a) q(s,a)必然高于状态的价值 v ( s ) v(s) v(s),当然也会有一些状态行动对低于价值,于是优势函数可以表示出当前行动和平均表现之间的区别:如果由于平均表现,那么优势函数为正,反之则为负

既然概念上又这样天然的分解,那么在设计模型时就可以考虑采用这样的结构:在保持网络主体结构不变的基础上,将原本网络中的单一输出变成两路输出,一个输出用于输出 v v v,它是一个一维标量;另一个输出用于输出 A A A,它的维度和行动数量相同,最后将两部分的输出加起来就是原本的 q q q值。
改变输出结构后,只需要对模型做很少的改变即可实现功能:模型前面部分可以保持不变,模型后面的部分从一路输出变为两路输出,最后合并为一个结果

仅仅做这样的分解并不能获得好的效果,因为当 q q q值一定时, v v v a a a有无穷种可行的组合(例如,对于同样的 Q Q Q值,如果将 V V V值加上任意大小的常数 C C C,再将所有 A A A值减去 C C C,则得到的值依然不变,这就导致了训练的不稳定性。),而实际上只有很小一部分的组合是合乎情理、接近真实数值的。为了解决 q q q值和 v v v值建模不唯一性的问题,就需要对优势函数 A A A做限定。显然 A A A函数的期望值为0:
E a [ A ( s t , a t ) ] = E a ( q ( s t , a t ) − v ( s t ) ) = v ( s t ) − v ( s t ) = 0 E_a[A(s_t,a_t)]=E_a(q(s_t,a_t)-v(s_t))=v(s_t)-v(s_t)=0 Ea[A(st,at)]=Ea(q(st,at)v(st))=v(st)v(st)=0
那么我们就可以对输出的 A A A值进行约束,例如将公式变成:
q ( s t , a t ) = v ( s t ) + ( A ( s t , a t ) − 1 ∣ A ∣ ∑ a ′ A ( s t , a t ′ ) ) q(s_t,a_t)=v(s_t)+(A(s_t,a_t)-\dfrac{1}{|A|}\sum\limits_{a'}A(s_t,a_t')) q(st,at)=v(st)+(A(st,at)A1aA(st,at))
让每一个 A A A值减去当前状态下所有 A A A值的平均数,就可以保证前面提到的期望值为0的约束,从而增加了 v v v A A A的输出稳定性。

另外一种约束是减去当前状态下的 A A A值的最大值。
q ( s t , a t ) = v ( s t ) + ( A ( s t , a t ) − max ⁡ a ′ A ( s t , a t ′ ) ) q(s_t, a_t)=v(s_t)+(A(s_t,a_t)-\max\limits_{a'}A(s_t,a_t')) q(st,at)=v(st)+(A(st,at)amaxA(st,at))

进行这样的分解有很多好处:

  • 通过这样的分解,不但可以得到给定状态和行动的 q q q值,还可以同时得到 v v v值和 A A A值。这样如果在某些场景需要使用 v v v值时,同样可以获得 v v v值而不用再训练一个网络。
  • 通过显式地给出 v v v函数地输出值,每一次更新时,都会显式地更新 v v v函数,这样 v v v函数的更新频率就会得到确定性的增加
  • 从网络训练的角度来看,原本需要训练 ∣ A ∣ |A| A个取值为 [ 0 , ∞ ] [0,\infty] [0,]的数值,变成了训练一个取值为 [ 0 , ∞ ] [0,\infty] [0,]的数值和 ∣ A ∣ |A| A个均值为0,实际取值为 [ − C , C ] [-C,C] [C,C]的数值,对网络训练来说,后者显然是更友好且容易的
  • 对于一些强化学习的问题来说, A A A值得取值范围远比 v v v值小,这样将两者分开训练更容易保持行动之间的排列顺序。由于 A A A值得数值范围比较小,因此它对模型更新更敏感,这样模型在更新时会更容易考量与其他行动得相对变化量,也就不会因为某一次得更新使得原本得行动排序被意外打破。比如在下面的驾驶车辆游戏中,智能体注意力集中的部位被显示为橙色,当智能体前面没有车时,车辆自身动作并没有太大差异,此时智能体更关注状态价值,而当智能体前面有车时(智能体需要超车),智能体开始关注不同动作优势值的差异。

Dueling DQN 代码实现

Dueling DQN 与 DQN 相比的差异只是在网络结构上,大部分代码依然可以继续沿用。我们定义状态价值函数和优势函数的复合神经网络VAnet

class Qnet(nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim):
        super(Qnet, self).__init__()
        self.layer = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)
        )

    def forward(self, s):
        s = self.layer(s)
        return s


class VAnet(nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim):
        super(VAnet, self).__init__()
        self.fc1 = nn.Linear(state_dim, hidden_dim)   # 共享网络部分
        self.fc_A = nn.Linear(hidden_dim, action_dim)
        self.fc_V = nn.Linear(hidden_dim, 1)

    def forward(self, x):
        A = self.fc_A(F.relu(self.fc1(x)))
        V = self.fc_V(F.relu(self.fc1(x)))
        Q = V + A - A.mean(1).view(-1, 1)  # Q值由V值和A值计算得到
        return Q

DQN算法包括Double DQN和Dueling DQN

class DQN:
    def __init__(self, args):
        self.args = args
        self.hidden_dim = args.hidden_size
        self.batch_size = args.batch_size
        self.lr = args.lr
        self.gamma = args.gamma  # 折扣因子
        self.epsilon = args.epsilon  # epsilon-贪婪策略
        self.target_update = args.target_update  # 目标网络更新频率
        self.count = 0  # 计数器,记录更新次数
        self.num_episodes = args.num_episodes
        self.minimal_size = args.minimal_size
        self.dqn_type = args.dqn_type

        self.env = gym.make(args.env_name)

        random.seed(args.seed)
        np.random.seed(args.seed)
        self.env.seed(args.seed)
        torch.manual_seed(args.seed)

        self.replay_buffer = ReplayBuffer(args.buffer_size)

        self.state_dim = self.env.observation_space.shape[0]
        self.action_dim = 11  # 将连续动作分成11个离散动作

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

        #########################################################################################################
        if self.dqn_type == "DuelingDQN":  # Dueling DQN采取不一样的网络框架
            self.q_net = VAnet(self.state_dim, self.hidden_dim, self.action_dim).to(self.device)
            self.target_q_net = VAnet(self.state_dim, self.hidden_dim, self.action_dim).to(self.device)
        else:
            self.q_net = Qnet(self.state_dim, self.hidden_dim, self.action_dim).to(self.device)
            self.target_q_net = Qnet(self.state_dim, self.hidden_dim, self.action_dim).to(self.device)
        #########################################################################################################

        self.optimizer = Adam(self.q_net.parameters(), lr=self.lr)

    def select_action(self, state):  # epsilon-贪婪策略采取动作
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.action_dim)
        else:
            state = torch.tensor([state], dtype=torch.float).to(self.device)
            action = self.q_net(state).argmax().item()
        return action

    def max_q_value(self, state):
        state = torch.tensor([state], dtype=torch.float).to(self.device)
        return self.q_net(state).max().item()

    def update(self, transition):
        states = torch.tensor(transition["states"], dtype=torch.float).to(self.device)
        actions = torch.tensor(transition["actions"]).view(-1, 1).to(self.device)
        rewards = torch.tensor(transition["rewards"], dtype=torch.float).view(-1, 1).to(self.device)
        next_states = torch.tensor(transition["next_states"], dtype=torch.float).to(self.device)
        dones = torch.tensor(transition["dones"], dtype=torch.float).view(-1, 1).to(self.device)

        q_values = self.q_net(states).gather(1, actions)  # Q value

        # 下个状态的最大Q值
        #########################################################################################################
        if self.dqn_type == 'DoubleDQN':
            max_action = self.q_net(next_states).max(1)[1].view(-1, 1)
            max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
        else:  # DQN
            max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)
        #########################################################################################################

        q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)  # TD error

        loss = torch.mean(F.mse_loss(q_values, q_targets))  # 均方误差损失函数
        self.optimizer.zero_grad()  # PyTorch中默认梯度会累积,这里需要显式将梯度置为0
        loss.backward()  # 反向传播更新参数
        self.optimizer.step()

        if self.count % self.target_update == 0:
            self.target_q_net.load_state_dict(self.q_net.state_dict())  # 更新目标网络

        self.count += 1

    def train_DQN(self):
        return_list = []
        max_q_value_list = []
        max_q_value = 0
        for i in range(10):
            with tqdm(total=int(self.num_episodes / 10), desc=f'Iteration {i}') as pbar:
                for episode in range(self.num_episodes // 10):
                    episode_return = 0
                    state = self.env.reset()
                    while True:
                        action = self.select_action(state)
                        max_q_value = self.max_q_value(state) * 0.005 + max_q_value * 0.995  # 平滑处理
                        max_q_value_list.append(max_q_value)  # 保存每个状态的最大Q值

                        action_continuous = dis_to_con(action, self.env, self.action_dim)
                        next_state, reward, done, _ = self.env.step([action_continuous])

                        self.replay_buffer.add(state, action, reward, next_state, done)

                        if self.replay_buffer.size() > self.minimal_size:
                            s, a, r, s_, d = self.replay_buffer.sample(self.batch_size)
                            transitions = {"states": s, "actions": a, "rewards": r, "next_states": s_, "dones": d}
                            self.update(transitions)

                        state = next_state
                        episode_return += reward

                        if done: break

                    return_list.append(episode_return)
                    if (episode + 1) % 10 == 0:
                        pbar.set_postfix(
                            {
                                "episode": f"{self.num_episodes / 10 * i + episode + 1}",
                                "return": f"{np.mean(return_list[-10:]):3f}"
                            }
                        )
                    pbar.update(1)
        return return_list, max_q_value_list

代码运行结果
image.png
image.png
根据代码运行结果我们可以发现,相比于传统的 DQN,Dueling DQN 在多个动作选择下的学习更加稳定,得到的回报最大值也更大。由 Dueling DQN 的原理可知,随着动作空间的增大,Dueling DQN 相比于 DQN 的优势更为明显。

总的来说,Dueling DQN 能够很好地学习到不同动作的差异性,在动作空间较大的环境下非常有效

\quad
\quad
\quad

参考:

  • 《动手学强化学习》
  • 《强化学习精要》

\quad
\quad
\quad
持续更新~有错误的话敬请指正!

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Dueling DQN是一种基于DQN算法的改进算法,它能够更高效地学习状态价值函数。与传统的DQN只会更新某个动作的Q值不同,Dueling DQN会同时更新状态价值函数和动作优势函数,从而更加频繁、准确地学习状态价值函数。具体来说,Dueling DQN将Q值分解为状态价值函数和动作优势函数两部分,这样就可以在学习状态价值函数时避免重复计算动作优势函数,从而提高学习效率。 下面是Dueling DQN代码实现: ```python import torch import torch.nn as nn import torch.optim as optim import numpy as np class DuelingDQN(nn.Module): def __init__(self, input_size, output_size): super(DuelingDQN, self).__init__() self.input_size = input_size self.output_size = output_size self.fc1 = nn.Linear(input_size, 128) self.fc2 = nn.Linear(128, 128) self.fc3_adv = nn.Linear(128, output_size) self.fc3_val = nn.Linear(128, 1) def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) adv = self.fc3_adv(x) val = self.fc3_val(x).expand(x.size(0), self.output_size) x = val + adv - adv.mean(1, keepdim=True).expand(x.size(0), self.output_size) return x class Agent(): def __init__(self, input_size, output_size, gamma=0.99, lr=0.001): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.gamma = gamma self.model = DuelingDQN(input_size, output_size).to(self.device) self.optimizer = optim.Adam(self.model.parameters(), lr=lr) self.loss_fn = nn.MSELoss() def act(self, state, epsilon=0.0): if np.random.random() < epsilon: return np.random.choice(self.model.output_size) state = torch.FloatTensor(state).unsqueeze(0).to(self.device) q_values = self.model(state) return q_values.argmax().item() def learn(self, state, action, reward, next_state, done): state = torch.FloatTensor(state).unsqueeze(0).to(self.device) next_state = torch.FloatTensor(next_state).unsqueeze(0).to(self.device) action = torch.LongTensor([action]).to(self.device) reward = torch.FloatTensor([reward]).to(self.device) q_value = self.model(state).gather(1, action.unsqueeze(1)) next_q_value = self.model(next_state).max(1)[0].unsqueeze(1) expected_q_value = reward + self.gamma * next_q_value * (1 - done) loss = self.loss_fn(q_value, expected_q_value.detach()) self.optimizer.zero_grad() loss.backward() self.optimizer.step() ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AlphaGuaGua

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

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

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

打赏作者

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

抵扣说明:

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

余额充值