强化学习 DQN Pytorch

本文是参考莫凡的代码,参考up主"Explorer2612",up主“EvilGeniusMR”以及自己的一些理解进行的DQN代码总结。

1.游戏环境

采用gym中的实例CartPole-v0环境,环境简介如下

2.强化学习伪代码

(1)原文

DQN的基本思想最开始在如下文章内提出

Human-level control through deep reinforcement learning

文中DQN的伪代码如下

在这里插入图片描述

(2)原文中提出的DQN步骤:

1. 初始化

  • 定义经验池的大小和形状,初始化经验池
  • 初始化评估网络 Q Network,该网络是较新的网络
  • 初始化目标网络 Q-Target Network,该网络较稳定,隔一段时间才与Q更新成一样

2. 循环各局游戏——循环1

  • 将agent初始化,得到原始状态
  • 在该局游戏内,反复进行操作,直到游戏结束(循环2)

3. 每一局游戏内的循环——循环2

  • 设置一个(0,1)之间的数ε,生成随机数,根据该随机数与ε的大小关系判断是随机选取一个动作a还是通过Q网络选取一个动作a
  • 执行动作a,与环境交互,得到奖励和下一个状态
  • 将学到的经验,即四元组<s,a,r,s_>作为一个transition放入经验池
  • 判断游戏是否结束
    • 如果游戏结束了,直接得到奖励
    • 如果游戏没结束,计算Q_value和Q_target value,根据TD error更新Q网络的参数
  • 经过指定回合后,将Q_Target网络的参数与Q网络的参数更新为一致

3.初始化环境等参数

(1)导入环境

import torch,gym
import torch.nn.functional as F
import torch.nn as nn
import numpy as np

(2)定义超参数(Hyper Parameters)

1)批量大小

根据MBGD(小批量梯度下降)的思想,每次学习选用一小批的样本进行学习,而不用单一样本,因为单一样本在计算梯度下降时非常不平稳。

BATCH_SIZE = 32

2)优化器的学习率 υ \upsilon υ

优化器optimizer能加速神经网络的训练,Adam优化器是最常用的优化器(融合了动量和自适应,采用修正避免了冷启动的问题),本质仍是梯度下降,计算梯度 g g g后通过学习率 υ \upsilon υ更新参数: θ k ⬅ θ k − 1 − υ g \theta_k⬅\theta_{k-1}-\upsilon g θkθk1υg

学习率 υ \upsilon υ一般设置为0.01

LR = 0.01

3)策略选择时的参考值 ϵ \epsilon ϵ

生成一个在[0, 1)内的随机数,根据随机数与 ϵ \epsilon ϵ的大小比较来判断是选择最优动作还是随机选择动作

EPSILON = 0.9

4)折扣因子 γ \gamma γ

用于计算TD Target: y j = r j + γ m a x Q ′ y_j = r_j +\gamma max Q' yj=rj+γmaxQ

GAMMA = 0.9

5)目标网络更新频率

学习100次以后,将目标网络的参数与评估网络的参数更新为一致(100)

TARGET_REPLACE_ITER = 100

6)经验池的大小

经验池最多存储1000条数据,此数可以更改

MEMORY_CAPACITY = 1000

(3)环境与输入输出

1)获取环境

env = gym.make('CartPole-v0').unwrapped

2)获得输入与输出的数量大小

N_ACTIONS = env.action_space.n
N_STATES = env.observation_space.shape[0]

(4)代码总结

import torch,gym
import torch.nn.functional as F
import torch.nn as nn
import numpy as np

# 超参数:Hyper Parameters
BATCH_SIZE = 32
LR = 0.01                    # 学习率 learning rate
EPSILON = 0.9                # greedy policy
GAMMA = 0.9                  # 折扣因子 reward discount
TARGET_REPLACE_ITER = 100    # target网络更新频率 Target Update Frequency
MEMORY_CAPACITY = 1000

# 环境与输入输出
env = gym.make('CartPole-v0').unwrapped
N_ACTIONS = env.action_space.n
N_STATES = env.observation_space.shape[0]
ENV_A_SHPAE = 0 if isinstance(env.action_space.sample(),int) else env.action_space.sample().shape

4.定义神经网络

(1)神经网络的简要解释

基于神经网络的机器学习是自己会学习的算法。

  • 机器学习的目标:找到神经网络的系数
  • 机器学习的模型:系数
  • 别人训练好的模型:已经找好的系数
  • 不断试错,不断改正,找到正解
    • 初始化权重
    • 按照权重计算结果
    • 计算误差
    • 根据误差调整权重
    • 重复

参考教学视频:大话神经网络

(2)DQN的神经网络

1)初始化网络

①执行父类构造函数

nn.Module的子类函数必须在构造函数中执行父类的构造函数

super(Net, self).__init__()
②定义第一个全连接层并初始化参数

设置第一个全连接层,即输入层到隐藏层

注:

  • 该实例里,输入为状态数,即N_STATES = 4,隐藏层神经元设置为20个
  • 初始化可以让神经网络在学习的过程中更容易收敛,这里将权重初始化为<均值为0,方差为0.1的正态分布>
self.fc1 = nn.Linear(N_STATES,20)
self.fc1.weight.data.normal_(0,0.1)
③定义第二个全连接层并初始化参数

设置第二个全连接层,即隐藏层到输出层

注:

  • 该实例里,隐藏神经元设置为20个,输出为动作数,即N_ACTIONS = 2
  • 权重初始化为均值为0,方差为0.1的正态分布
self.out = nn.Linear(20,N_ACTIONS)
self.out.weight.data.normal_(0,0.1)

2)前向传播

输入到第一层隐藏层进行计算,对结果使用激活函数,输入到输出层进行计算,得到的结果即为该神经网络的最后输出结果,最后一层的结果无需使用激活函数,因为此时输出结果为各动作的得分而不是概率

x = self.fc1(x)
x = F.relu(x)
action_value = self.out(x)
return action_value

3)总结:神经网络类的代码

# 定义Net类 (定义网络)
class Net(nn.Module):
    def __init__(self):     # 定义Net的一系列属性
        super(Net, self).__init__()             # nn.Module的子类函数必须在构造函数中执行父类的构造函数
        self.fc1 = nn.Linear(N_STATES,20)       # 设置第一个全连接层(输入层到隐藏层): 状态数个(4个)神经元到20个神经元
        self.fc1.weight.data.normal_(0,0.1)     # 初始化,为了让神经网络在学习过程中更加容易收敛
        self.out = nn.Linear(20,N_ACTIONS)      # 设置第二个全连接层(隐藏层到输出层): 20个神经元到动作数个(2个)神经元
        self.out.weight.data.normal_(0,0.1)     # 权重初始化(均值为0,方差为0.1的正态分布)

    def forward(self,x):    # 定义forward函数 (x为状态)
        x = self.fc1(x)                         # 连接输入层到隐藏层
        x = F.relu(x)                           # 使用激励函数ReLU来处理经过隐藏层后的值
        action_value = self.out(x)              # 连接隐藏层到输出层,获得最终的输出值 (即动作值)
        return action_value                     # 返回动作值

5.定义DQN类

(1)初始化DQN并定义一系列属性

考虑到DQN的实现流程,在初始化类的操作里,需要定义两个神经网络、定义学习的次数、定义经验池的相关属性、定义学习率的大小、并定义优化器相关属性。

1)创建两个神经网络

DQN的核心是采用两个神经网络,即评估网络和目标网络。评估网络是DQN选择动作的核心,每次选择Q值最大的动作,每次都根据TD error和梯度计算更新评估网络的参数;而目标网络定期更新,较为稳定。

self.eval_net = Net()
self.target_net = Net()

3)定义学习的次数

由于目标网络是定期更新,需要定义学习了多少次,以便将目标网络的参数与评估网络的参数更新为一致

self.learn_step_counter = 0 

4)定义经验池的相关属性

经验池用于存储已经学过的经验,经验池的大小可以人为设置,根据场景的不同设置为不同值,在CartPole实例中一般设置为1000
每次学习后都将学习到的经验转化为一条transition存入经验池,如果经验池已满就覆盖之前的经验,保证经验池的数据最多为1000条
存储的经验即四元组<s,a,r,s_>,当前状态,选择动作,与环境交互得到的奖励和下一个状态,数量为N_STATES + 1 + 1 + N_STATES = 10

self.memory_counter = 0   # 用于计数,已经存储了多少条经验
self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2))	# 经验池的大小为1000×10

5)定义优化器相关属性

使用Adam优化器和均方损失函数

self.optimizer = torch.optim.Adam(self.eval_net.parameters(),lr = LR)
self.loss_func = nn.MSELoss()

6)总结:初始化代码

def __init__(self):     # 定义DQN的一系列属性
    self.eval_net,self.target_net = Net(),Net()     # 利用Net创建两个神经网络: 评估网络和目标网络
    self.learn_step_counter = 0         # for target updating
    self.memory_counter = 0             # for storing memory
    self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2))              # 初始化记忆库,一行代表一个transition,1000×10
    self.optimizer = torch.optim.Adam(self.eval_net.parameters(),lr = LR)   # 使用Adam优化器 (输入为评估网络的参数和学习率)
    self.loss_func = nn.MSELoss()       # 使用均方损失函数(loss(xi, yi)=(xi-yi)^2)

(2)定义动作选择函数

1)输入状态处理

输入状态xnumpy.ndarray形式,大小为4的一维数组。为了便于计算,需要转变为tensor形式,大小为torch.Size([1, 4])。因此,需要在dim=0处增加维数为1的维度。

x = torch.unsqueeze(torch.FloatTensor(x),0)

unsqueeze()函数
函数功能:升维,参数表示在哪个地方加一个维度,简单理解如下:

  • 0表示在张量最外侧加一个中括号变成第一维
  • 1表示在每个数外面都加一个中括号

举例

import torch  
input = torch.arange(0,3)  
print(input)          # --→ tensor([0, 1, 2])  
print(input.shape)    # 大小为3 --→ torch.Size([3])  
print(input.unsqueeze(0))   # --→ tensor([[0, 1, 2]])  
print(input.unsqueeze(0).shape)     # 大小为1x3 --→ torch.Size([1, 3])  
print(input.unsqueeze(1))   # --→ tensor([[0],[1],[2]])  
print(input.unsqueeze(1).shape)     # 大小为3x1 --→ torch.Size([3, 1])
t = torch.as_tensor(np.array([1,0,0])).unsqueeze(-1)      # -> tensor([[1],[0],[0]], dtype=torch.int32)

2)动作选择

生成一个在[0, 1)内的随机数,如果该随机数小于 ϵ \epsilon ϵ,则选择最优动作,否则选择随机动作

①选择最优动作

将一个batch内的状态输入评估网络,通过前向传播,获得各动作对应的价值,找到各状态的动作最大价值所对应的动作

if np.random.uniform() < EPSILON:
	actions_value = self.eval_net.forward(x)                # 把状态x输入评估网络,前向传播获得动作值
	action = torch.argmax(actions_value,1).data.numpy()     # 输出每一行最大值的索引,转化为numpy ndarray的形式
	action = action[0]                                      # 输出动作最大的索引
②选择随机动作

动作只有两个值,0和1

else:
	action = np.random.randint(0,N_ACTIONS)                 # 这里action随机等于0或1 (N_ACTIONS = 2)
③动作选择改进

上述示例中, ϵ \epsilon ϵ是个定值,但是在训练初始阶段,希望尽可能的选择随机动作,在训练的后期,更多的选择已有的经验,因此可以在动作选择时加入学习次数的考虑。即学习次数较少时,随机数小一点

decline = 0.6  # 衰减系数
if random.randint(0, 100) < 100 * (decline ** learn_time):
    action = np.random.randint(0,N_ACTIONS)
else:
	actions_value = self.eval_net.forward(x)                # 把状态x输入评估网络,前向传播获得动作值
	action = torch.argmax(actions_value,1).data.numpy()     # 输出每一行最大值的索引,转化为numpy ndarray的形式
	action = action[0]                                      # 输出动作最大的索引

3)总结:动作选择函数代码

   def choose_action(self,x):              # 定义动作选择函数 (x为状态)
       x = torch.unsqueeze(torch.FloatTensor(x),0)     # 将x转换成32-bit floating point形式,并在dim=0增加维数为1的维度
       if np.random.uniform() < EPSILON:   # 生成一个在[0, 1)内的随机数
           # 随机数如果小于EPSILON,选择最优动作
           actions_value = self.eval_net.forward(x)                # 把状态x输入评估网络,前向传播获得动作值
           action = torch.argmax(actions_value,1).data.numpy()     # 输出每一行最大值的索引,转化为numpy ndarray的形式
           action = action[0]  # 输出动作最大的索引
       else:
           # 随机数如果大于等于EPSILON,随机选择动作
           action = np.random.randint(0,N_ACTIONS)                 # 这里action随机等于0或1 (N_ACTIONS = 2)
       return action     # 返回选择的动作 (0或1)

(3)定义记忆存储函数

1)拼接数组

根据上文所述,存储的经验为四元组<s,a,r,s_>,经验池的每条transition长度为10,即我们希望每一行的transition是一个大小为10的数组,因此使用np.hstack()函数进行拼接

注:
这里a和r都单独的数值,因此加[]把它们变成一个数组再做拼接

transition = np.hstack((s,[a,r],s_))

np.hstack()
函数功能:将参数元组的元素数组按水平方向进行叠加
举例:

import numpy as np  
arr1 = [1, 2, 3]  
arr2 = [4, 5]  
arr3 = [6, 7]  
res = np.hstack((arr1, arr2, arr3))     # -> [1 2 3 4 5 6 7]

2)向记忆库添加新的数据

如果记忆库满了,就覆盖旧的数据

index = self.memory_counter % MEMORY_CAPACITY   # 获取transition要置入的行数
self.memory[index,:] = transition               # 置入transition
self.memory_counter += 1                        # 计数加1

3)总结:记忆存储函数代码

  def store_transition(self,s,a,r,s_):        # 定义记忆存储函数 (这里输入为一个transition),每次将四个量存入并更新下标
      transition = np.hstack((s,[a,r],s_))    # 在水平方向上拼接数组(拼接的所有数组外面必须加括号,a和r是动作不是数组,需要转变为数组)
      # 如果记忆库满了,覆盖旧的数据
      index = self.memory_counter % MEMORY_CAPACITY   # 获取transition要置入的行数
      self.memory[index,:] = transition               # 置入transition
      self.memory_counter += 1                        # 计数加1

(4)定义学习函数

1)目标网络参数更新

每次训练都进行计数,当训练到一定次数后,更新目标网络的参数

if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
    self.target_net.load_state_dict(self.eval_net.state_dict())     # 将评估网络的参数赋给目标网络
self.learn_step_counter += 1    # 学习步数加1

2)抽取批数据并存储到新数组

从经验池中随机抽取一定数量的数据,本实例中设置为抽取32个数,这些数不一定相邻,完全随机抽取,且有可能重复。

sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)     # 在[0, 2000)内随机抽取32个数
b_memory = self.memory[sample_index,:]                          # 将32个索引对应的那一行transition存入b_memory
 # 分别将这32个transition中的s,a,r,s_取出存储到如下新的数组
 b_s = torch.FloatTensor(b_memory[:, : N_STATES])    # 32×4
 b_a = torch.LongTensor(b_memory[:,N_STATES:N_STATES+1].astype(int)) # 32×1
 b_r = torch.FloatTensor(b_memory[:,N_STATES+1: N_STATES+2]) # 32×1
 b_s_ = torch.FloatTensor(b_memory[:, -N_STATES: ])  # 32×4

3)获取批数据的评估值

利用评估网络得到各动作对应的值,这里不是选择值最大的动作,而是根据经验池里取出的批数据来选择动作,原来存储的是什么动作,现在就选择什么动作

action_values_b = self.eval_net(b_s)          # 利用前向网络得到各动作的值(默认会调用forward函数)
q_eval = action_values_b.gather(1,b_a)        # 原来选择什么动作,这次依然选择什么动作,得到该动作对应的值

4)获得批数据的目标值

对下一个状态,使用目标网络求取动作代价值,并预测动作。
注:

  • 这时不进行反向传递(使用detach()隔绝梯度)
  • 直接求取的索引是32×1的张量,为了后续计算,需要变成1×32的张量
q_next = self.target_net(b_s_).detach()           # q_next不进行反向传递误差,所以detach
q_next_max = q_next.max(1)[0]                     # 返回最大值,不返回索引,32×1
q_next_max_shape = q_next_max.view(BATCH_SIZE,1)  # 将32×1的张量变成1×32
q_target = b_r + GAMMA * q_next_max_shape

5)更新的常用步骤

将32个评估值和目标值依次输入优化器,使用均方损失函数

self.optimizer.zero_grad()    # 清空上一步的残余更新参数值
loss.backward()               # 误差反向传播, 计算参数更新值
self.optimizer.step()         # 更新评估网络的所有参数

6)总结:学习函数代码

def learn(self):        # 定义学习函数(记忆库已满后便开始学习)
    # 目标网络参数更新
    if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
        self.target_net.load_state_dict(self.eval_net.state_dict())     # 将评估网络的参数赋给目标网络
    self.learn_step_counter += 1    # 学习步数加1

    # 抽取记忆库中的批数据
    sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)     # 在[0, 2000)内随机抽取32个数,可能会重复
    b_memory = self.memory[sample_index,:]                          # 将32个索引对应的那一行transition存入b_memory
    # 分别将这32个transition中的s,a,r,s_取出存储到如下新的数组
    b_s = torch.FloatTensor(b_memory[:, : N_STATES])    # 32×4
    b_a = torch.LongTensor(b_memory[:,N_STATES:N_STATES+1].astype(int)) # 32×1
    b_r = torch.FloatTensor(b_memory[:,N_STATES+1: N_STATES+2]) # 32×1
    b_s_ = torch.FloatTensor(b_memory[:, -N_STATES: ])  # 32×4

    # 获取批数据的评估值和目标值,利用损失函数和优化器对评估网络进行参数更新
    # 评估值
    action_values_b = self.eval_net(b_s)          # 利用前向网络得到各动作的值(默认会调用forward函数)
    q_eval = action_values_b.gather(1,b_a)        # 原来选择什么动作,这次依然选择什么动作,得到该动作对应的值
    # 目标值
    q_next = self.target_net(b_s_).detach()          # q_next不进行反向传递误差,所以detach
    q_next_max = q_next.max(1)[0]                    # 返回最大值,不返回索引,32×1
    q_next_max_shape = q_next_max.view(BATCH_SIZE,1)  # 将32×1的张量变成1×32
    q_target = b_r + GAMMA * q_next_max_shape
    # 输入32个评估值和32个目标值,使用均方损失函数
    loss = self.loss_func(q_eval, q_target)
    # 更新的常用步骤
    self.optimizer.zero_grad()                                      # 清空上一步的残余更新参数值
    loss.backward()                                                 # 误差反向传播, 计算参数更新值
    self.optimizer.step()                                           # 更新评估网络的所有参数

5.训练

(1)各局游戏的循环

需要进行N多局游戏,游戏开始时重置环境为初始状态,每局游戏一直玩到结束为止,不断训练两个网络的参数,并记录得分情况

for i in range(400):
    print('<<<<<<<<<Episode: %s' % i)	# 记录是第i局游戏
    s = env.reset()  					# 重置环境
    episode_reward_sum = 0				# 该局游戏的得分初始化为0
    while True:
    	# 一局游戏内的循环
env.close()   

(2)一局游戏内的循环

1)选择动作与显示

可以选择每走一步都显示动画,也可以隔断时间显示一次。本实例中每走一步都会更新显示。“走一步”即使用DQN的《动作选择函数》确定动作,然后执行该动作,与环境交互,获得奖励和下一个状态。

env.render()  # 显示实验动画
a = dqn.choose_action(s)  # 输入该步对应的状态s,选择动作
s_, r, done, info = env.step(a)  # 执行动作,获得反馈

2)修改奖励

为了更好的训练摆杆,修改奖励,不修改也可以进行训练。
这里结合摆杆移动的位置和摆杆偏移的角度作为新的奖励函数

x, x_dot, theta, theta_dot = s_
r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
new_r = r1 + r2

3) 存储样本并记录得分

将现有状态、选择的动作、与环境交互获得的下一状态以及上述重新定义的奖励作为一个新的transition存储到经验池中。并记录该步获得的新奖励,更新这一局的总得分。

dqn.store_transition(s, a, new_r, s_)  # 存储样本
episode_reward_sum += new_r  # 逐步加上一个episode内每个step的reward

4)开始学习

最开始只探索,不学习,当经验池满了以后再开始学习

if dqn.memory_counter > MEMORY_CAPACITY:  # 如果累计的transition数量超过了记忆库的固定容量2000
    # 开始学习 (抽取记忆,即32个transition,并对评估网络参数进行更新,并在开始学习后每隔100次将评估网络的参数赋给目标网络)
    dqn.learn()

5)判断是否结束该局游戏

与环境交互得到的doneTrue则游戏结束,结束该局游戏(while True循环)

if done:
    print('episode%s---reward_sum: %s' % (i_episode, round(episode_reward_sum, 2)))
    break

s = s_  # 更新状态

6.代码总览

'''
 @Project: pythonProject1 
 @Author: xby
 @File:   try_dqn_2.py
 @Date    2022/10/17 11:05 
'''

import torch,gym
import torch.nn.functional as F
import torch.nn as nn
import numpy as np

# 超参数:Hyper Parameters
BATCH_SIZE = 32
LR = 0.01                    # 学习率 learning rate
EPSILON = 0.9                # greedy policy
GAMMA = 0.9                  # 折扣因子 reward discount
TARGET_REPLACE_ITER = 100    # target网络更新频率 Target Update Frequency
MEMORY_CAPACITY = 1000

# 环境与输入输出
env = gym.make('CartPole-v0').unwrapped
N_ACTIONS = env.action_space.n
N_STATES = env.observation_space.shape[0]

# 定义Net类 (定义网络)
class Net(nn.Module):
    def __init__(self):     # 定义Net的一系列属性
        super(Net, self).__init__()             # nn.Module的子类函数必须在构造函数中执行父类的构造函数
        self.fc1 = nn.Linear(N_STATES,20)       # 设置第一个全连接层(输入层到隐藏层): 状态数个(4个)神经元到20个神经元
        self.fc1.weight.data.normal_(0,0.1)     # 初始化,为了让神经网络在学习过程中更加容易收敛
        self.out = nn.Linear(20,N_ACTIONS)      # 设置第二个全连接层(隐藏层到输出层): 20个神经元到动作数个(2个)神经元
        self.out.weight.data.normal_(0,0.1)     # 权重初始化(均值为0,方差为0.1的正态分布)

    def forward(self,x):    # 定义forward函数 (x为状态)
        x = self.fc1(x)                         # 连接输入层到隐藏层
        x = F.relu(x)                           # 使用激励函数ReLU来处理经过隐藏层后的值
        action_value = self.out(x)              # 连接隐藏层到输出层,获得最终的输出值 (即动作值)
        return action_value                     # 返回动作值

# DQN:有两个网络——eval_net是更加靠前的新网络,不停试探进行新游戏;target_net更加稳定
class DQN(object):
    def __init__(self):     # 定义DQN的一系列属性
        self.eval_net,self.target_net = Net(),Net()     # 利用Net创建两个神经网络: 评估网络和目标网络
        self.learn_step_counter = 0         # for target updating
        self.memory_counter = 0             # for storing memory
        self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2))              # 初始化记忆库,一行代表一个transition,1000×10
        self.optimizer = torch.optim.Adam(self.eval_net.parameters(),lr = LR)   # 使用Adam优化器 (输入为评估网络的参数和学习率)
        self.loss_func = nn.MSELoss()       # 使用均方损失函数(loss(xi, yi)=(xi-yi)^2)

    def choose_action(self,x):              # 定义动作选择函数 (x为状态)
        x = torch.unsqueeze(torch.FloatTensor(x),0)     # # 将x转换成32-bit floating point形式,并在dim=0增加维数为1的维度
        if np.random.uniform() < EPSILON:   # 生成一个在[0, 1)内的随机数
            # 随机数如果小于EPSILON,选择最优动作
            actions_value = self.eval_net.forward(x)                # 把状态x输入评估网络,前向传播获得动作值
            action = torch.argmax(actions_value,1).data.numpy()     # 输出每一行最大值的索引,转化为numpy ndarray的形式
            action = action[0]    # 输出动作最大的索引
        else:
            # 随机数如果大于等于EPSILON,随机选择动作
            action = np.random.randint(0,N_ACTIONS)                 # 这里action随机等于0或1 (N_ACTIONS = 2)
        return action     # 返回选择的动作 (0或1)

    def store_transition(self,s,a,r,s_):        # 定义记忆存储函数 (这里输入为一个transition),每次将四个量存入并更新下标
        transition = np.hstack((s,[a,r],s_))    # 在水平方向上拼接数组(拼接的所有数组外面必须加括号,a和r是动作不是数组,需要转变为数组)
        # 如果记忆库满了,覆盖旧的数据
        index = self.memory_counter % MEMORY_CAPACITY   # 获取transition要置入的行数
        self.memory[index,:] = transition               # 置入transition
        self.memory_counter += 1                        # 计数加1

    def learn(self):        # 定义学习函数(记忆库已满后便开始学习)
        # 目标网络参数更新
        if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
            self.target_net.load_state_dict(self.eval_net.state_dict())     # 将评估网络的参数赋给目标网络
        self.learn_step_counter += 1    # 学习步数加1

        # 抽取记忆库中的批数据
        sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)     # 在[0, 2000)内随机抽取32个数,可能会重复
        b_memory = self.memory[sample_index,:]                          # 将32个索引对应的那一行transition存入b_memory
        # 分别将这32个transition中的s,a,r,s_取出存储到如下新的数组
        b_s = torch.FloatTensor(b_memory[:, : N_STATES])    # 32×4
        b_a = torch.LongTensor(b_memory[:,N_STATES:N_STATES+1].astype(int)) # 32×1
        b_r = torch.FloatTensor(b_memory[:,N_STATES+1: N_STATES+2]) # 32×1
        b_s_ = torch.FloatTensor(b_memory[:, -N_STATES: ])  # 32×4

        # 获取批数据的评估值和目标值,利用损失函数和优化器对评估网络进行参数更新
        # 评估值
        action_values_b = self.eval_net(b_s)          # 利用前向网络得到各动作的值(默认会调用forward函数)
        q_eval = action_values_b.gather(1,b_a)        # 原来选择什么动作,这次依然选择什么动作,得到该动作对应的值
        # 目标值
        q_next = self.target_net(b_s_).detach()          # q_next不进行反向传递误差,所以detach
        q_next_max = q_next.max(1)[0]                    # 返回最大值,不返回索引,32×1
        q_next_max_shape = q_next_max.view(BATCH_SIZE,1)  # 将32×1的张量变成1×32
        q_target = b_r + GAMMA * q_next_max_shape
        # 输入32个评估值和32个目标值,使用均方损失函数
        loss = self.loss_func(q_eval, q_target)
        # 更新的常用步骤
        self.optimizer.zero_grad()                                      # 清空上一步的残余更新参数值
        loss.backward()                                                 # 误差反向传播, 计算参数更新值
        self.optimizer.step()                                           # 更新评估网络的所有参数

dqn = DQN()

print('\nCollecting experience...')

for i_episode in range(400):
    print('<<<<<<<<<Episode: %s' % i_episode)
    s = env.reset()     # 初始化游戏状态
    episode_reward_sum = 0

    while True:
        env.render()  # 显示实验动画
        a = dqn.choose_action(s)  # 输入该步对应的状态s,选择动作
        s_, r, done, info = env.step(a)  # 执行动作,获得反馈

        # 修改奖励 (不修改也可以,修改奖励只是为了更快地得到训练好的摆杆)
        x, x_dot, theta, theta_dot = s_
        r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
        r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
        new_r = r1 + r2

        dqn.store_transition(s, a, new_r, s_)  # 存储样本
        episode_reward_sum += new_r  # 逐步加上一个episode内每个step的reward

        if dqn.memory_counter > MEMORY_CAPACITY:  # 如果累计的transition数量超过了记忆库的固定容量2000
            # 开始学习 (抽取记忆,即32个transition,并对评估网络参数进行更新,并在开始学习后每隔100次将评估网络的参数赋给目标网络)
            dqn.learn()

        if done:
            print('episode%s---reward_sum: %s' % (i_episode, round(episode_reward_sum, 2)))
            break

        s = s_  # 更新状态
  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值