赞
踩
这一篇是DeepMind团队经过对2013版本的DQN改造后,在Nature上发表的,也就是业界称的Nature版本的DQN。这个模型和2013版本的区别是,该版本使用了两个网络,一个网络叫main network,另一个网络叫target network。
《Human-level Control Through Deep Reinforcement Learning》
读懂这篇论文还需要知道一些前置的知识,前置的知识我已经讲解过了,在我的博客上,我附一下链接。有一些基础的人,只需要看第4篇就可以了。
第1篇:(零基础可以看懂)强化学习中的动态规划(贝尔曼方程)(含代码)-《强化学习系列专栏第1篇》
第2篇:(零基础可以看懂)强化学习中的蒙特卡罗应用(贝尔曼方程)(含代码)-《强化学习系列专栏第2篇》
第3篇:(零基础可以看懂)强化学习中的时间差分算法(含代码)-《强化学习系列专栏第3篇》
第4篇:(零基础可以看懂)深度强化学习之DQN类算法之第1篇-2013年NeurIPS版本的DQN(含代码)-《强化学习系列专栏第4篇》
我们首先把算法的伪代码放上来,并且将2013版本的DQN的伪代码也放上来,来对比看下有什么区别。
仔细观察,可以看出以下区别:
①2015的DQN中的
o
t
h
e
r
w
i
s
e
s
e
l
e
c
t
a
t
=
a
r
g
m
a
x
a
Q
(
Φ
(
s
t
)
,
a
;
θ
)
otherwise \space select \space a_t=argmax_aQ(Φ(s_t),a;θ)
otherwise select at=argmaxaQ(Φ(st),a;θ)
这句话中的Q指的是action-value network,也就是main network。
②2015的DQN中的公式
y
i
=
{
r
j
r
j
+
γ
m
a
x
a
′
Q
^
(
Φ
j
+
1
,
a
′
;
θ
)
y_i=
中的
Q
^
\hat{Q}
Q^指的是target action-value network,也就是target network。这一点与2013的DQN中的公式不同。
③2015的DQN中还多了一个步骤
E
v
e
r
y
C
s
t
e
p
s
r
e
s
e
t
Q
^
=
Q
Every \space C \space steps \space reset \space \hat{Q}=Q
Every C steps reset Q^=Q
这其实就相当于是,我们在计算
y
i
y_i
yi的值的时候,用的是target network,然后实际上被训练的是main network,然后每过C steps,target network的参数就会被“赋值”为main network的参数,也就是会被update一次。论文中,C设置为10000。
④上面③这个改进也是2015版DQN相较于2013版DQN的主要改进,这样做的好处是,在训练时,模型更容易往稳定的方向去迭代。因为2013版的DQN就存在训练时不稳定的问题,本人在训练2013版的DQN时就存在这个问题,训练了好久,reward也没有增加,这导致一度我以为是我代码有问题。后来在训练2015版的DQN时,我发现也有这种问题,只不过2015版的DQN好一点。
这里,我是选择了玩Seaquest这个游戏,在gym里面,名称为“Seaquest-v0”。游戏过程的截图及视频如下。
视频-Seaquest-训练到一半
可以看到,大部分时间潜艇是在画面底部晃动来得分的。下面看一下reward的滚动平均的图
下面看一下loss的滚动平均的图
通过观察上面的reward变化以及loss变化,我们可以知道,loss是不会收敛的,因为训练的数据是不断在变化的,并不是像普通的监督学习那样样本是固定不变的。因此每一次前向传播的时候,数据极有可能是模型未曾见过的。
同时,reward随着训练不断的增加,其不同幕的平均值是在不断增加的。也证明了该模型确实是有效的。但从未做滚动平均的reward的图上看,可以知道,reward其实波动是大的。
总体来说,如果让计算机随机玩游戏,那么其reward的平均值大约在90/幕,但是经过训练一段时间后,reward的平均值可以达到590/幕,我训练到了590/幕,随着训练时间的增加,该数值会不断的增加。但由于模型本身鲁棒性就存在缺陷,所以也有可能reward会陷入不下降也不上升的情况。模型的这个缺陷我认为和exploration有关,从上面的视频可以看出,模型发现了只要在游戏下方不断的晃动开枪,就可以获得比较高的收益,所以大部分时间并没有跑到游戏上方去获取分数。这点可以通过增加exploration的rate来打破这种陷入“局部最优”的情况。也是由于这种问题,导致我在跑2013版的DQN模型的时候,玩“Pong”这个游戏时,reward一直升不上去。
#encoding=utf-8 ''' Author: Haitaifantuan Create Date: 2020-09-27 23:23:52 Author Email: 47970915@qq.com Description: Should you have any question, do not hesitate to contact me via E-mail. ''' import gym import torch.nn as nn import torch from torchvision import transforms import atari_py import random import time from PIL import Image import matplotlib.pyplot as plt from collections import deque import copy import os class Preprocessing(nn.Module): def __init__(self): super(Preprocessing, self).__init__() self.preprocessing = transforms.Compose([ # 按照论文步骤 # 先转换为灰度图 transforms.Grayscale(1), # 再下采样到110*84的大小 transforms.Resize((110, 84)), # 转换为Tensor()输入到网络 transforms.ToTensor() ] ) def forward(self, input): # 由于传进来是torch.Tensor() # 所以我们要将其转换为PIL.Image才能预处理 input = Image.fromarray(input) # 最后输出的就是论文所说的84*84的灰度图像了 output = self.preprocessing(input) # 这个时候output是[1, 84, 84] # 将多余的维度压缩掉,最后返回的是[84, 84]的形状 output = torch.squeeze(output) # 然后再裁剪到84*84的大小的游戏区域 output = output[17:101, :] # 这个区域是游戏的区域 # plt.imshow(output, cmap='gray') # plt.show() return output class Deep_Q_Network(nn.Module): def __init__(self, action_nums): super(Deep_Q_Network, self).__init__() self.features = nn.Sequential( nn.Conv2d(4, 16, (8, 8), 4), nn.ReLU(), # 论文中使用的不一定是这个激活函数,这里是为了简化使用ReLU nn.Conv2d(16, 32, (4, 4), 2), nn.ReLU() # 论文中使用的不一定是这个激活函数,这里是为了简化使用ReLU ) self.classifier = nn.Sequential( nn.Linear(2592, 256), nn.Linear(256, action_nums) ) def forward(self, input): output = self.features(input) output = output.view(-1, 2592) output = self.classifier(output) output = torch.squeeze(output) return output def initialization(self): for m in self.modules(): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight.data) if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight.data) class Agent(object): def __init__(self): # 模型保存的路径 self.model_path = './2015_Nature_DQN_cpu_trained_model_save_reward_loss' if not os.path.exists(self.model_path): os.mkdir(self.model_path) self.save_model_path = self.model_path + '/model' self.lr = 0.00001 # 我们玩Seaquest游戏,这里搭建下环境 self.env = gym.make('Seaquest-v0') self.env = self.env.unwrapped # 这个是游戏的valid的动作 self.action_space = self.env.action_space.n self.action_nums = self.env.action_space.n # 构建图像预处理对象 self.preprocessing = Preprocessing() # 构建action-value网络,和target action-value网络 self.a_v_net = Deep_Q_Network(self.action_nums) self.target_a_v_net = Deep_Q_Network(self.action_nums) # 初始化网络 if os.path.exists(self.save_model_path): state_dict = torch.load(self.save_model_path) self.a_v_net.load_state_dict(state_dict) self.target_a_v_net.load_state_dict(state_dict) print("从已训练好的模型中加载action-value网络和target action-value网络成功") else: self.a_v_net.initialization() self.target_a_v_net.initialization() print("初始化action-value网络和target action-value网络所有参数成功") # 构建action-value 网络的损失函数 self.loss_func = nn.MSELoss() # 构建优化器 self.opti = torch.optim.SGD(self.a_v_net.parameters(), lr=self.lr, momentum=0.9) # 每次训练的样本数量,论文中是32 self.batch_size = 32 # 创建一个缓存,超过大小后,最新的放进去,老的扔掉 # self.replay_memory_size = 1000000 self.replay_memory_size = 500000 # 30000的话,2080Ti显存11G不够 10万需要20个G内存左右 self.replay_memory = deque() # 当memory_size达到多少后,开始训练 self.begin_to_train_memory_size = 50000 self.alpha = 0.9 self.gamma = 0.99 self.init_epsilon = 0.1 # 论文为1 # self.init_epsilon = 0.9 self.final_epsilon = 0.1 # 论文为0.1 # self.final_epsilon = 0.01 self.epsilon_decay_frames = 1000000 # 论文1000000 # self.epsilon_decay_frames = 100000 self.train_times = 0 self.every_C_steps = 10000 # 论文中是每4帧,agent进行一次动作的选择。 self.select_action_every_k_time = 4 # 记录reward变化的变量 self.reward_change = [] # 记录loss变化的变量 self.loss_change = [] def four_img_list_to_Q_net_input(self, four_img_list): stacked = torch.stack(list(four_img_list)) return stacked def generate_initial_4_frames(self, current_state_single_frame): ''' 由于环境一开始,four_img_list的长度是小于4的 因此我们需要让其长度达到4后,再继续后面的记录操作 在前4步,我们都使用模型选择动作 :param current_state_single_frame: :return: 返回一个队列,里面存放了第1、2、3、4帧游戏画面的对应的Tensor数值 ''' four_img_list = deque() # 由于一开始并没有4张图片可以使用 # 因此,我们根据当前的状态,复制出另外3张图片 # 然后随着step的进行,我们一张图片一张图片的放进去 four_img_list.extend([current_state_single_frame, current_state_single_frame, current_state_single_frame, current_state_single_frame]) for _ in range(3): # 渲染环境 self.env.render() # 这里将4帧图片变成4个通道放到网络里 current_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list) # 放到网络里需要再添加一个Batch_size部分的维度 current_state_4_frames_stacked_result = torch.unsqueeze(current_state_4_frames_stacked_result,dim=0) action_value = self.a_v_net(current_state_4_frames_stacked_result) action = torch.argmax(action_value) next_state, reward, done, info = self.env.step(action) next_state_to_tensor = self.preprocessing(next_state) four_img_list.append(next_state_to_tensor) four_img_list.popleft() return four_img_list def train(self): # 原始论文:如果达到了replay_memory的最大值,那就开始从replay_memory中随机选取样本进行训练 # if len(self.replay_memory) > (self.replay_memory_size - 1): if len(self.replay_memory) > self.begin_to_train_memory_size: batch_data = random.choices(self.replay_memory, k=32) # 拿到训练数据后,将他们进行解包 current_state_4_frames_stacked_result_list = [each[0] for each in batch_data] current_state_action_list = torch.LongTensor([[each[1]] for each in batch_data]) reward_list = torch.FloatTensor([[each[2]] for each in batch_data]) next_state_4_frames_stacked_result_list = [each[3] for each in batch_data] done_list = [[each[4]] for each in batch_data] # 将训练数据放到模型里进行前向传播 y_pre = self.a_v_net(torch.stack(current_state_4_frames_stacked_result_list).squeeze()).gather(dim=1, index=current_state_action_list) # 根据公式,构建标签值 q_net_result = self.target_a_v_net(torch.stack(next_state_4_frames_stacked_result_list, dim=0)).detach() y_target = reward_list + self.gamma * torch.max(q_net_result, dim=1)[0].reshape(self.batch_size, -1) self.loss = self.loss_func(y_pre, y_target) # loss = loss.to('cpu') self.opti.zero_grad() self.loss.backward() self.opti.step() self.train_times += 1 def close_env(self): self.env.close() def save_model(self): torch.save(self.a_v_net.state_dict(), self.save_model_path) def fire_in_the_hole(self): self.steps_count = 0 self.current_epsilon = self.init_epsilon self.begin_time = time.time() for self.episode in range(100000): # 一个episode结束后,重新设置下环境,返回到随机的一个初始状态 current_state_single_frame = self.env.reset() # 将current_state()预处理一下然后转换为Tensor current_state = self.preprocessing(current_state_single_frame) # 这个方法返回的four_img_list里面就存放了第1、2、3、4帧画面的Tensor()形式 four_img_list = self.generate_initial_4_frames(current_state) current_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list) # 记录一下当前这一盘总的reward self.current_episode_reward = 0 self.select_action_count = 0 while True: # 渲染环境 self.env.render() # 论文每4帧才根据ε-greedy方法做一个动作 # 其他3帧时间的动作选取上一轮选择的动作 if self.select_action_count == 0 or self.select_action_count == self.select_action_every_k_time: # 根据ε-greedy方法,走一步,看看 if random.random() < self.current_epsilon: current_state_action = self.env.action_space.sample() else: # 根据Q函数找到最优的动作 # 放到网络里需要再添加一个Batch_size部分的维度 action_value = self.a_v_net(torch.unsqueeze(current_state_4_frames_stacked_result, dim=0)) current_state_action = torch.argmax(action_value) self.select_action_count = 0 next_state, reward, done, info = self.env.step(current_state_action) next_state_to_tensor = self.preprocessing(next_state) self.current_episode_reward += reward four_img_list.append(next_state_to_tensor) four_img_list.popleft() next_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list) # (将当前的状态以及前三幅图片组成的图片,当前的行为,当前获得的奖励,下一个状态,游戏是否结束)添加到replay_memory中 self.replay_memory.append((current_state_4_frames_stacked_result, current_state_action, reward, next_state_4_frames_stacked_result, done)) if len(self.replay_memory) > self.replay_memory_size: self.replay_memory.popleft() # 判断当前这一盘游戏是否结束 if done: self.end_time = time.time() self.total_seconds = self.end_time - self.begin_time self.day = int(self.total_seconds / (60 * 60 * 24)) self.hour = int(self.total_seconds / (60 * 60) - self.day * 24) self.minute = int(self.total_seconds/60 - self.day*24*60 - self.hour*60) if len(self.replay_memory) < self.begin_to_train_memory_size: self.loss = torch.tensor(0) break current_state_4_frames_stacked_result = next_state_4_frames_stacked_result self.select_action_count += 1 self.steps_count += 1 if self.steps_count <= self.epsilon_decay_frames and len(self.replay_memory) > self.begin_to_train_memory_size: self.current_epsilon = self.init_epsilon - (self.init_epsilon - self.final_epsilon) * (self.steps_count-self.begin_to_train_memory_size) / self.epsilon_decay_frames # 执行训练网络的操作,里面会判断reply_memory的长度是否达到最大值了 self.train() # 每self.every_C_steps步,更新将target_action_value网络的参数更新为action_value网络的参数 if self.steps_count % self.every_C_steps == 0: self.update_target_network() self.reward_change.append(self.current_episode_reward) self.loss_change.append(self.loss.data.item()) print( "当前已训练{}天-{}小时-{}分钟===当前为第{}个Episode===当前episode共获得{}reward===总共已训练{}次===当前的loss为:{}===当前的epsilon值为:{}===当前reply_memory的长度为:{}".format( self.day, self.hour, self.minute, self.episode+1, self.current_episode_reward, self.train_times, self.loss.data.item(), self.current_epsilon, len(self.replay_memory))) if (self.episode + 1) % 10 == 0: # 保存模型 self.save_model() # 将当前的self.reward_change列表保存下来,以覆盖的方式保存下来。 with open(self.model_path+'/reward_change.txt', 'w', encoding='utf-8') as file: file.write(str(self.reward_change)) # 将当前的self.loss_change保存下来 with open(self.model_path+'/loss_change.txt', 'w', encoding='utf-8') as file: file.write(str(self.loss_change)) # 打印出当前reward的图 # plt.clf() # plt.plot(self.reward_change) # plt.draw() # 关闭游戏环境 self.close_env() def update_target_network(self): self.target_a_v_net.load_state_dict(self.a_v_net.state_dict()) agent = Agent() agent.fire_in_the_hole()
完整代码地址:https://github.com/haitaifantuan/reinforcement_leanring
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。