当前位置:   article > 正文

第0-(1)章-DRL的细碎笔记-计算图、训练以及基本逻辑

第0-(1)章-DRL的细碎笔记-计算图、训练以及基本逻辑

文章目录

第0-(1)章-DRL的细碎笔记-计算图、训练以及基本逻辑

作者:想要飞的猪

工作地点:北京科技大学

   这次主要讲解一下学习深度强化学习的一点心得体会,不足之处希望大家指正!
   我个人看来深度学习既简单又复杂。简单在于深度学习理论入门很简单,神经网络的训练主要就是用到了运筹学中最简单的梯度下降。复杂在于真正研究深度学习需要根据问题本身设计网络结构并选择激活函数等等,这些深入的内容需要很好的数学功底。“黑盒不黑”这句话还是非常有道理的。下面的讲解都是基于python语言中pytorch架构。

1. 计算图

  我第一次接触计算图这个概念的时候很费解。感觉计算图是一个很高深莫测的东西,毕竟现在主流的深度学习框架(tensorflow、pytorch、keras等)都是基于计算图的概念。现在对于深度学习稍微入门之后,其实计算图可以理解成算法流程的逻辑图。其实构建计算图的过程就是构建算法逻辑的过程,计算图包含神经网络的架构以及相应的计算。在代码中计算图的构建是网络加外层的损失函数。
  在构建计算图的过程中部署计算图节点时,有的节点并不参与梯度运算,这种节点不参与计算图的构建,梯度在此不会传递。这种节点在声明的时候会用到如下语句:

with torch.no_grad():
    dist = Categorical(probs=self.actor(s))
    a = dist.sample()
    a_logprob = dist.log_prob(a)
  • 1
  • 2
  • 3
  • 4

torch.no_grad()作用域下的节点不会被加入计算图,虽然不影响forward的计算,但是在backward的时候不参与梯度更新,所以在torch.no_grad()作用域节点之前的节点的梯度对应的也不会更新。这种节点的作用是当节点的值通过计算输出,但是在损失函数中又只是作为参数而不作为优化变量或者优化变量的外层函数。具体的理解可以借助以下的伪代码理解:
θ k + 1 = arg ⁡ max ⁡ θ 1 ∣ D k ∣ T ∑ τ ∈ D k ∑ t = 0 T min ⁡ ( π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) A π θ k ( s t , a t ) , g ( ϵ , A π θ k ( s t , a t ) ) ) \theta_{k+1}=\arg \max _\theta \frac{1}{\left|\mathcal{D}_k\right| T} \sum_{\tau \in \mathcal{D}_k} \sum_{t=0}^T \min \left(\frac{\pi_\theta\left(a_t \mid s_t\right)}{\pi_{\theta_k}\left(a_t \mid s_t\right)} A^{\pi_{\theta_k}}\left(s_t, a_t\right), \quad g\left(\epsilon, A^{\pi_{\theta_k}}\left(s_t, a_t\right)\right)\right) θk+1=argθmaxDkT1τDkt=0Tmin(πθk(atst)πθ(atst)Aπθk(st,at),g(ϵ,Aπθk(st,at)))
这是PPO算法中一段关于actor网络更新的公式,其中需要更新的参数是 θ \theta θ,在表达式中仅有 π θ ( a t ∣ s t ) \pi_{\theta}\left(a_t|s_t\right) πθ(atst) θ \theta θ相关,而其他参数都可以看作常数,但是其他参数的运算也用到了与参数 π θ ( a t ∣ s t ) \pi_{\theta}\left(a_t|s_t\right) πθ(atst)相同的神经网络,如果不声明这部分不参与计算图,在backward的时候会造成错误的梯度更新。其实只要明确算法的逻辑以及求梯度时需要更新哪些参数就容易理解这块内容。
   当然,如果这个地方用不同的神经网络产生其他的参数(例如在代码中用到了target 网络)并且在优化器时声明了参数范围仅涉及 θ \theta θ相关的eval网络,那么可以不需要声明(个人的理解,如有问题希望大家指正!)。我在阅读别人开源DDPG代码时有下面两个版本:
–使用torch.no_grad()的版本–

class DDPG(object):
    def __init__(self, state_dim, action_dim, max_action):
        ...
        self.actor = Actor(state_dim, action_dim, self.hidden_width, max_action)
        self.actor_target = copy.deepcopy(self.actor)
        self.critic = Critic(state_dim, action_dim, self.hidden_width)
        self.critic_target = copy.deepcopy(self.critic)
		...

    def choose_action(self, s):
        ...

    def learn(self, relay_buffer):
		...
        # Compute the target Q
        with torch.no_grad():  # target_Q has no gradient
            Q_ = self.critic_target(batch_s_, self.actor_target(batch_s_))
            target_Q = batch_r + self.GAMMA * (1 - batch_dw) * Q_

        # Compute the current Q and the critic loss
        current_Q = self.critic(batch_s, batch_a)
        critic_loss = self.MseLoss(target_Q, current_Q)
        # Optimize the critic
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()

        # Freeze critic networks so you don't waste computational effort
        for params in self.critic.parameters():
            params.requires_grad = False

        # Compute the actor loss
        actor_loss = -self.critic(batch_s, self.actor(batch_s)).mean()
        # Optimize the actor
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()

        # Unfreeze critic networks
        for params in self.critic.parameters():
            params.requires_grad = True

        ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

这段代码中使用了torch.no_grad()声明target网络不做梯度更新,这样做能够减少计算开销。上述代码需要注意的地方:

# Freeze critic networks so you don't waste computational effort
for params in self.critic.parameters():
	params.requires_grad = False
  • 1
  • 2
  • 3

这一段代码是必要的,因为critic网络在之前已经做过梯度更新,所以这里将critic网络中参数的梯度更新冻结既能减少计算开销又能有效避免梯度积累的错误(如果不声明冻结critic网络的梯度,则会导致critic梯度重复计算累加)。当然如果不怕计算开销也可以不声明梯度冻结,但是需要在self.actor_optimizer.zero_grad()下一行加上self.critic_optimizer.zero_grad()以避免梯度积累。
–没有使用torch.no_grad()的版本–

class DDPG(object):
    def __init__(self, a_dim, s_dim, a_bound,):
        ...
        self.Actor_eval = ANet(s_dim,a_dim) # 创建Actor_eval对象
        self.Actor_target = ANet(s_dim,a_dim) # 创建Actor_target对象
        self.Critic_eval = CNet(s_dim,a_dim) # 创建Critic_eval对象
        self.Critic_target = CNet(s_dim,a_dim) # 创建Critic_target对象
        self.ctrain = torch.optim.Adam(self.Critic_eval.parameters(),lr=LR_C) # 使用Adam优化器创建Critic_eval对象的优化器ctrian,使用MSE作为损失函数
        self.atrain = torch.optim.Adam(self.Actor_eval.parameters(),lr=LR_A) # 使用Adam优化器创建Actor_eval对象的优化器atrain
        self.loss_td = nn.MSELoss() # 创建均方误差损失函数对象

    def choose_action(self, s):
        ...
    def learn(self):
        ...
        # 计算 Actor 网络的 loss_a 并更新参数
        a = self.Actor_eval(bs)
        q = self.Critic_eval(bs, a)  # loss=-q=-ce(s,ae(s))更新ae   ae(s)=a   ae(s_)=a_
        # 如果 a 是一个正确的行为的话,那么它的 Q 应该更贴近0
        loss_a = -torch.mean(q)
        self.atrain.zero_grad()  # 清除上一次的梯度
        loss_a.backward()  # 反向传播求导数
        self.atrain.step()  # 更新参数

        # 计算 Critic 网络的 loss_c 并更新参数
        a_ = self.Actor_target(bs_)  # 用于预测 Critic 的 Q_target 中的 action
        q_ = self.Critic_target(bs_, a_)  # 用于给出 Actor 更新参数时的 Gradient ascent 强度
        q_target = br + GAMMA * q_  # q_target = 负的
        q_v = self.Critic_eval(bs, ba)
        td_error = self.loss_td(q_target, q_v)  # 计算 temporal-difference error
        self.ctrain.zero_grad()
        td_error.backward()
        self.ctrain.step()

    def store_transition(self, s, a, r, s_):
        ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

尽管这段代码不使用torch.no_grad()声明target网络不参与梯度更新不会造成梯度更新错误,但是会造成额外的计算负担,同时使得代码不容易阅读。因此,从效率以及规范两个方面考虑,使用torch.no_grad()是必要的。同时这段代码中没有像前面的代码一样冻结critic的梯度,这是因为在这段代码中先更新了actor_eval的参数,然后又更新了critic_eval的参数,在更新critic_eval的参数时,清零了更新actor_eval的参数时对critic_eval的参数求的梯度,所以这里的代码没有问题,但是计算开销要比前面的代码大,因为这里对critic_eval的参数的梯度求了两次。

2. 神经网络的训练

   神经网络的训练通过基于梯度下降的误差逆传播算法进行。在代码中要明确两块内容:
(1)在梯度下降过程中需要依据梯度迭代更新的参数及其更新法则;

optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9)
  • 1

其中,model.parameter()制定了了优化器optim.SGD优化参数的范围,其他对应的参数大家可以搜一下具体的文档这里不赘述了。
(2)求对应的梯度

loss.backward()
  • 1

注意:这里的loss的定义我没有给出具体的内容,这个loss可以看作是计算图的最后一个节点,即咱们神经网络最后的损失函数。这段代码会对应的逆向(backward)求解loss计算图中所有节点的梯度,并存储在节点中的grad属性中。
  梯度更新完整的代码为:

optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9)
... # 代码其他部分
loss = ... # loss构建
optimizer.zero_grad()
loss.backward()
optimizer.step()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3. 深度强化学习的基本逻辑

  深度强化学习的重要组成可以看作3部分:环境、算法、网络。
(1)环境:使用深度强化学习解决运筹问题,很大部分的工作就是在编写环境上。环境输出的state就是运筹问题中的一些模型参数,reward就是运筹问题中的目标函数;
(2)算法:指的是PPO、A2C、DDPG、TD3等算法,为什么要把算法跟网络分开呢,因为这里算法主要规定了网络更新的方式,例如DDPG中critic网络更新用到的损失函数是TD error,actor网络更新所用到的损失函数是值函数的相反数;
(3)网络:具体就是里面actor网络与critic网络的构建方法,再复杂一点会加一个特征提取层。
  这里拿DDPG算法作为例子来讲解深度强化学习的逻辑,本部分主要讲解算法及网络,环境的搭建放到后面来讲。
-算法-:示例代码如下:

class DDPG(object):
    def __init__(self, a_dim, s_dim, a_bound,):
        self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound,
        self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)
        self.pointer = 0
        self.Actor_eval = ANet(s_dim,a_dim)
        self.Actor_target = ANet(s_dim,a_dim)
        self.Critic_eval = CNet(s_dim,a_dim)
        self.Critic_target = CNet(s_dim,a_dim)
        self.ctrain = torch.optim.Adam(self.Critic_eval.parameters(),lr=LR_C)
        self.atrain = torch.optim.Adam(self.Actor_eval.parameters(),lr=LR_A)
        self.loss_td = nn.MSELoss()

    def choose_action(self, s):
        s = torch.unsqueeze(torch.FloatTensor(s), 0)
        return self.Actor_eval(s)[0].detach() # ae(s)

    def learn(self):

        # eval()讲字符串解析为可执行的表达式, x直接连接到state_dict中最底层的地址,所以修改x也会相应修改模型的值
        for x in self.Actor_target.state_dict().keys():
            eval('self.Actor_target.' + x + '.data.mul_((1-TAU))')
            eval('self.Actor_target.' + x + '.data.add_(TAU*self.Actor_eval.' + x + '.data)')
        for x in self.Critic_target.state_dict().keys():
            eval('self.Critic_target.' + x + '.data.mul_((1-TAU))')
            eval('self.Critic_target.' + x + '.data.add_(TAU*self.Critic_eval.' + x + '.data)')

        # soft target replacement
        indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE)
        bt = self.memory[indices, :]
        bs = torch.FloatTensor(bt[:, :self.s_dim])
        print('BATCH_SIZE:', BATCH_SIZE)
        print('bs_shape:', bs.shape)
        ba = torch.FloatTensor(bt[:, self.s_dim: self.s_dim + self.a_dim])
        br = torch.FloatTensor(bt[:, -self.s_dim - 1: -self.s_dim])
        bs_ = torch.FloatTensor(bt[:, -self.s_dim:])

        # 此处tensor是一个batch的形式输入到torch.nn模块中,所以不需要用torch.unsqueeze()拓展维度
        # tensor需要是一个batch才能输入到torch.nn模块中
        a = self.Actor_eval(bs)
        q = self.Critic_eval(bs,a)  # loss=-q=-ce(s,ae(s))更新ae   ae(s)=a   ae(s_)=a_
        # 如果 a是一个正确的行为的话,那么它的Q应该更贴近0
        loss_a = -torch.mean(q)
        print('type_a', loss_a.dtype)
        self.atrain.zero_grad()
        loss_a.backward()
        self.atrain.step()

        a_ = self.Actor_target(bs_)  # 这个网络不及时更新参数, 用于预测 Critic 的 Q_target 中的 action
        q_ = self.Critic_target(bs_,a_)  # 这个网络不及时更新参数, 用于给出 Actor 更新参数时的 Gradient ascent 强度
        q_target = br+GAMMA*q_  # q_target = 负的

        q_v = self.Critic_eval(bs,ba)

        td_error = self.loss_td(q_target,q_v)
        self.ctrain.zero_grad()
        td_error.backward()
        self.ctrain.step()

    def store_transition(self, s, a, r, s_):
        transition = np.hstack((s, a, [r], s_))
        index = self.pointer % MEMORY_CAPACITY  # replace the old memory with new memory
        self.memory[index, :] = transition
        self.pointer += 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

在DDPG算法中必要的两个函数是choose_action以及learn函数。这两个函数choose_action是对环境的环境输入的state进行反馈action;learn的功能是网络的训练。choose_action函数的编写较为简单,learn函数稍微复杂一下,但是learn函数的核心就是在定义网络的损失函数。在上面的代码中actor网络的损失函数是值函数的相反数:

loss_a = -torch.mean(q)
  • 1

critic网络的损失函数是TD error:

td_error = self.loss_td(q_target,q_v)
  • 1

定义完了损失函数就梯度更新,梯度更新的方法在第2子节中已经做了表述。
   在上面的代码中还有一个store_transition的函数,这个函数是负责填充buffer的,buffer的编写可以单独使用类来编写,这个示例中只是把buffer定义成了一个np数组,所以store_transition不是算法中必要的函数。
-网络-:示例代码如下:

class ANet(nn.Module):
    def __init__(self,s_dim,a_dim):
        super(ANet,self).__init__()
        self.fc1 = nn.Linear(s_dim,30)
        self.fc1.weight.data.normal_(0,0.1)
        self.out = nn.Linear(30,a_dim)
        self.out.weight.data.normal_(0,0.1)
    def forward(self,x):
        x = self.fc1(x)
        x = F.relu(x)
        x = self.out(x)
        x = F.tanh(x)
        actions_value = x*2
        return actions_value

class CNet(nn.Module):
    def __init__(self,s_dim,a_dim):
        super(CNet,self).__init__()
        self.fcs = nn.Linear(s_dim,30)
        self.fcs.weight.data.normal_(0,0.1)
        self.fca = nn.Linear(a_dim,30)
        self.fca.weight.data.normal_(0,0.1)
        self.out = nn.Linear(30,1)
        self.out.weight.data.normal_(0, 0.1)
    def forward(self,s,a):
        x = self.fcs(s)
        y = self.fca(a)
        net = F.relu(x+y)
        actions_value = self.out(net)
        return actions_value
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

在上述代码中,分别搭建了actor网络-ANet与critic网络CNet的类,如果大家接触过深度学习那这一块就没有什么特别的难度,仅仅需要注意网络输入输出维度以及对应的激活函数就好。

4. 数据类型及结构

   数据类型与数据结构也是深度强化学习中需要注意的问题,神经网络需要tensor类型的数据输入,同时在输入时需要按照batch的形式输入到网络中。智能体与环境互动时,环境反馈的数据类型一般时numpy的数据类型,numpy的数据在输入到神经网络时,需要转换为tensor的数据类型,同时数据结构需要转换为按照batch输入的形式。示例代码如下:

def choose_action(self, s):
	s = torch.unsqueeze(torch.FloatTensor(s), 0) # 将s变为FloatTensor类型,并将维度拓展为1,与模型输入维度一致
	return self.Actor_eval(s)[0].detach() # 用Actor_eval模型预测s对应的动作a,返回a的Tensor
  • 1
  • 2
  • 3

上述代码定义了智能体依据state选取action的函数。其中,“torch.FloatTensor(s)”就是将s(state)由numpy的数据类型转换为tensor的数据类型。其中,“unsqueeze”函数将数据shape由(state_dim)转换为(1,state_dim),为什么这里是1而不是batch_size,因为在choose_action的时候,算法是按照单个state来选取action的,这与训练时按照batch训练不同。但是神经网络只能接受batch形式的数据,所以这里要将数据的维度拓展。
   最后,我觉得学习深度强化学习特别重要的一点是要去读代码,其实很多理论上的东西需要到实际当中理解的才会更加深刻。我在学习深度强化学习的过程中接受了许多人的帮助,有面对面交流的帮助,也有我自己阅读别人开源资料获得的启发。我很感恩大家!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/741231
推荐阅读
相关标签
  

闽ICP备14008679号