5.1 深度强化学习DQN
在表格表示的Q学习中,行号表示智能体的状态,列号对应智能体的动作,表格存储的时动作价值Q(s,a),这种表示的问题是,随着状态变量类型数量增加,每个变量被精细的离散化的话,表格中的行数会变得很大,如果以图像作为状态,每个像素对应于状态变量,50像素的方哥具有2500多个状态变量,用表格表示的强化学习解决大量状态的任务是不显示的。
采用神经网络表示动作价值函数,神经网络的输入时每个状态变量的值。输入层的神经元数和状态变量的数量相同。CartPole有位置、速度、角度、角速度四个变量,因此有四个输入神经元,输入神经网络时不需要离散化,输出层中的神经元数是动作类型的数量。CartPole有两种,左推和右推。输出层中神经元输出的值是动作价值函数 Q ( s t , a t ) Q(s_t,a_t) Q(st,at)的值,也就是说,它输出在采用对应于该神经元的动作之后所获得的折扣奖励总和。通过比较输出层各神经元输出的折扣奖励来确定行动,这种情况下,深度学习不是一个分类问题,而是一个回归问题,需要求解具体的数值。
更新Q学习动作价值函数Q的公式为:
Q
(
s
t
,
a
t
)
=
Q
(
s
t
,
a
t
)
+
η
∗
(
R
t
+
1
+
γ
max
a
∗
Q
(
s
t
+
1
,
a
)
−
Q
(
s
t
,
a
t
)
)
Q(s_t,a_t)=Q(s_t,a_t)+\eta*(R_{t+1}+\gamma\max\limits_a *Q(s_{t+1},a)-Q(s_t,a_t))
Q(st,at)=Q(st,at)+η∗(Rt+1+γamax∗Q(st+1,a)−Q(st,at))
Q
(
s
t
,
a
t
)
=
R
t
+
1
+
γ
max
a
Q
(
s
t
+
1
,
a
)
Q(s_t,a_t)=R_{t+1}+\gamma\max\limits_a Q(s_{t+1},a)
Q(st,at)=Rt+1+γamaxQ(st+1,a)
输出层输出的值是
Q
(
s
t
,
a
t
)
Q(s_t,a_t)
Q(st,at),学习使得输出值和
R
t
+
1
+
γ
max
a
Q
(
s
t
+
1
,
a
)
R_{t+1}+\gamma\max\limits_a Q(s_{t+1},a)
Rt+1+γamaxQ(st+1,a)接近。误差函数写为:
E
(
s
t
,
a
t
)
=
(
R
t
+
1
+
γ
max
a
Q
(
s
t
+
1
,
a
)
−
Q
(
s
t
,
a
t
)
)
2
E(s_t,a_t)=(R_{t+1}+\gamma\max\limits_a Q(s_{t+1},a)-Q(s_t,a_t))^2
E(st,at)=(Rt+1+γamaxQ(st+1,a)−Q(st,at))2
用图表示为:
5.2实现DQN的4个要点
为了让DQN能够稳定学习,第一个要点是经验回放(experience replay),DQN不像表格表示的Q学习意义,每一步都学习该步的内容,而是将每个步骤的内容储在经验池中并随机从经验池中提取内容(replay)让神经网络学习。每个步骤的内容也称为转换(transition),神经网络连续地学习时间上相关度高的内容,从而出现连接参数难以稳定的问题。借助经验回放,可以使用经验池多个步骤的经验,就可以使用小批量学习来训练神经网络。
第二个要点是固定目标网络(fixed target Q-Network)。有两种类型神经网络:确定动作的主网络(main network)和计算误差函数时确定动作价值的目标网络(target-network)。DQN在更新 Q ( s t , a ) Q(s_t,a) Q(st,a)时,必须要下一个状态的价值函数 Q ( s t + 1 , a ) Q(s_{t+1},a) Q(st+1,a),如果这两个值相同,则会出现Q函数的学习趋于不稳定。因此,当需要更新所需的 max a Q ( s t + 1 , a ) \max\limits_a Q(s_{t+1},a) amaxQ(st+1,a),使用一段时间之前的另一个Q函数(固定目标Q网络),目标网络被主网络周期性覆盖。
第三个要点时奖励的裁剪(clipping)。这里采用将在每个步骤中获得的奖励固定为-1、0或1的方法。它具有以下优点:无论任务如何,都可以使用相同的超参数来执行DQN。
第四个要点损失函数是使用Huber函数,而不是平方误差函数,误差在-1~1之间取误差的平方,其他取绝对值
5.3实现DQN
- 实现小批量学习以实现经验回放和模拟固定目标Q网络。每个步骤的转换(状态,动作,下一个状态,奖励)存储在经验池中。小批量指的是从该经验池中随机地取出多个步骤的转换数据块。当杆掉落或连续站立200步时游戏结束,此时不存在下一个状态,因此需要针对下一个状态是否存在来设计实现方法。
- Pytorch可以有效处理小批量数据,熟悉方法
- 注意变量的类型。CartPole处理NumPy的变量,pytorch使用Tensor类型处理变量
- 注意变量的大小。
- 注意使用namedtuple。可以命名并保存CartPole观察到的状态变量值
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML
import gym
import random
import numpy as np
# 动画显示函数
def display_frames_as_gif(frames):
plt.figure(figsize=(frames[0].shape[1] / 72.0, frames[0].shape[0] / 72.0), dpi=72)
patch = plt.imshow(frames[0])
plt.axis('off')
def animate(i):
patch.set_data(frames[i])
anim = animation.FuncAnimation(plt.gcf(), animate, frames=len(frames), interval=50)
anim.save('./image/movie_cartpole_DQN.mp4')
plt.close() # 防止显示两个输出
return HTML(anim.to_jshtml())
from collections import namedtuple
#namedtuple就是有名字的元组,使得元组有键名,以便在DQN访问状态和动作值
Tr = namedtuple('tr',('name_a','value_b'))
Tr_object = Tr('名称为A',100)
print(Tr_object)
print(Tr_object.value_b)
tr(name_a='名称为A', value_b=100)
100
from collections import namedtuple
#namedtuple就是有名字的元组,使得元组有键名,以便在DQN访问状态和动作值
Transition = namedtuple('Transition', ('state', 'action', 'next_state', 'reward'))
ENV = 'CartPole-v0'
GAMMA = 0.99
MAX_STEPS = 200
NUM_EPISODES = 500
#为了实现小批量学习,实现了内存类ReplayMemory来存储经验数据。ReplayMemory
class ReplayMemory:
'''push函数保存步骤中的transition,随机选择的sample函数'''
def __init__(self,CAPACITY) -> None:
self.capacity = CAPACITY
self.memory = []
self.index = 0
def push(self,state,action,state_next,reward):
'''将transition(state,action,state_next,reward)保存在存储器中'''
if len(self.memory) < self.capacity:
self.memory.append(None)
self.memory[self.index] = Transition(state,action,state_next,reward)
self.index = (self.index + 1) % self.capacity #保存的index移动一位
def sample(self,batch_size):
return random.sample(self.memory,batch_size)
def __len__(self):
'''返回当前memory长度'''
return len(self.memory)
第3章解释的表格表示的Q学习中,Brain类有一个表,但这里有一个神经网络,使用函数replay和函数decision_action。函数replay从内存类中获取小批量数据,学习神经网络连接参数,并更新Q函数。函数decision_action遵循ε-贪婪法,返回随机选取的动作或在当前状态下具有最高Q值的动作的索引index。
import random
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
BATCH_SIZE = 32
CAPACITY = 10000
# 它是成为智能体的大脑的类,它执行DQN
# 将Q函数定义为深度学习网络
import random
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
BATCH_SIZE = 32
CAPACITY = 10000
class Brain:
def __init__(self, num_states, num_actions):
self.num_actions = num_actions # 获取CartPole的两个动作(向左或向右)
# 创建存储经验的对象
self.memory = ReplayMemory(CAPACITY)
# 建立神经网络
self.model = nn.Sequential()
self.model.add_module('fc1', nn.Linear(num_states, 32))
self.model.add_module('relu1', nn.ReLU())
self.model.add_module('fc2', nn.Linear(32, 32))
self.model.add_module('relu2', nn.ReLU())
self.model.add_module('fc3', nn.Linear(32, num_actions))
print(self.model) # 输出网络形状
# 设置优化方法
self.optimizer = optim.Adam(self.model.parameters(), lr=0.0001)
def replay(self):
'''通过Experience Replay学习神经网络的连接参数'''
# -----------------------------------------
# 1. 检查经验池的大小
# -----------------------------------------
# 1.1 当内存大小小于小批量数据时不执行任何操作
if len(self.memory) < BATCH_SIZE:
return
# -----------------------------------------
# 2. 创建小批量数据
# -----------------------------------------
# 2.1 从经验池中获取小批量数据
transitions = self.memory.sample(BATCH_SIZE)
# 2.2 将每个变量转换为与小批量数据对应的形式
# 得到的transitions存储了一个BATCH_SIZE的(state,action,state_next,reward)
#即(state,action,state_next,reward)×BATCH_SIZE
#想把它变成小批量数据。换句话说
#设为(state×BATCH_SIZE,action×BATCH_SIZE,state_next×BATCH_SIZE,reward×BATCH_SIZE)
batch = Transition(*zip(*transitions))
# 2.3 将每个变量的元素转换为与小批量数据相对应的形式,并将其设置为变量,以便可以在网络中进行处理
# 例如,对于state,形状为[torch.FloatTensor of size 1×4]
# 将其转换为 torch.FloatTensor of size BATCH_SIZEx4
# 为状态,动作,奖励,非最终状态小批量创建变量
# cat代表Concatenates(级联)
state_batch = torch.cat(batch.state)
action_batch = torch.cat(batch.action)
reward_batch = torch.cat(batch.reward)
non_final_next_states = torch.cat([s for s in batch.next_state
if s is not None])
# -----------------------------------------
# 3. 求取Q(s_t,a_t)值作为监督信号
# -----------------------------------------
# 3.1 将网络切换到推理模式
self.model.eval()
# 3.2 求取网络输出的Q(s_t,a_t)
# self.model(state_batch)输出左右两个Q值
# [torch.FloatTensor of size BATCH_SIZEx2]になっている。
# 为了求得与从此处执行的动作a_t对应的Q值,找到由action_batch执行的动作a_t是右还是左的index
# 用gather获得相应的Q值。
state_action_values = self.model(state_batch).gather(1, action_batch)
# 3.3 求取max{Q(s_t+1, a)}値。但是,请注意以下状态
# 创建一个索引掩码以检查cartpole是否未完成,且具有next_state
non_final_mask = torch.ByteTensor(tuple(map(lambda s: s is not None,
batch.next_state)))
# 首先全部设置为0
next_state_values = torch.zeros(BATCH_SIZE)
# 求取具有下一状态的index的最大Q值
# 访问输出并通过max(1)查找列方向上的最大值[value,index]。
# 并输出其Q值(index=0)
# 用detach取出该值
next_state_values[non_final_mask] = self.model(
non_final_next_states).max(1)[0].detach()
# 3.4 从Q公式中求取Q(s_t,a_t)值作为监督信息
expected_state_action_values = reward_batch + GAMMA * next_state_values
# -----------------------------------------
# 4. 更新连接参数
# -----------------------------------------
# 4.1 将网络切换到训练模式
self.model.train()
# 4.2 计算损失函数(smooth_l1_loss是Huberloss)
# expected_state_action_values的
# size是[minbatch],通过unsqueeze得到[minibatch x 1]
loss = F.smooth_l1_loss(state_action_values,
expected_state_action_values.unsqueeze(1))
# 4.3 更新联接参数
self.optimizer.zero_grad() # 重置渐变
loss.backward() # 计算反向传播
self.optimizer.step() # 更新连接参数
def decide_action(self, state, episode):
'''根据当前状态确定动作'''
# 采用ε-greedy法逐步采用最佳动作
epsilon = 0.5 * (1 / (episode + 1))
if epsilon <= np.random.uniform(0, 1):
self.model.eval() # 将网络切换到推理模式
with torch.no_grad():
action = self.model(state).max(1)[1].view(1, 1)
# 获取网络最大值的索引index= max(1)[1]
# .view(1,1)将[torch.LongTensor of size 1] 转换为 size 1x1 大小
else:
# 随机返回0,1的动作
action = torch.LongTensor(
[[random.randrange(self.num_actions)]]) # 随机返回0,1的动作
# action的形式为[torch.LongTensor of size 1x1]
return action
函数replay执行了以下四个步骤。1)检查经验池大小。2)创建小批量数据。3)求取将成为监督信息的Q(st,at)值。4)更新连接参数。
定义一个Agent类,它是一个带有杆的小车(Cart)对象。实现内容与第3章表格表示的Q学习几乎相同。与第3章中Q学习的不同之处在于存在函数memorize。使用此功能可将已经学过的数据(transition)存储在经验池中。其他函数与第3章中的表格表示的实现示例相同,但请注意参数略有不同。
class Agent:
'''CartPole智能体,带有杆的小车'''
def __init__(self,num_states,num_actions) -> None:
self.brain = Brain(num_states,num_actions)
#为智能体创建大脑以做出决策
def update_q_function(self):
'''Q函数的更新'''
self.brain.replay
def get_action(self,state,episode):
'''动作的确定'''
action = self.brain.decide_action(state,episode)
return action
def memorize(self,state,action,state_next,reward):
self.brain.memory.push(state,action,state_next,reward)
接下来,定义运行CartPole的环境类。基本上与第3章中的Q学习相同,这里有一点变化。第3章与深度Q学习的主要区别在于CartPole的观察结果observation是按原样用于状态state的。不像表格表示那样进行离散化。此外,准备一个列表,该列表存储过去10轮内连续站立的步数,并通过查看其平均值使学习进度易于表示。
# 这是一个执行CartPole的环境类
class Environment:
def __init__(self):
self.env = gym.make(ENV) # 设定要执行的任务
num_states = self.env.observation_space.shape[0] # 设定任务状态和动作的数量
num_actions = self.env.action_space.n # CartPole的动作(向做或向右)数量为2
self.agent = Agent(num_states, num_actions) # 创建Agent在环境中执行的动作
def run(self):
'''执行'''
episode_10_list = np.zeros(10) # 存储10个试验的连续站立步骤数,并使用平均步骤数进行输出
complete_episodes = 0 # 持续站立195步或更多的试验次数
episode_final = False # 最终尝试目标
frames = [] # 用于存储图像的变量,以使最后一轮成为动画
for episode in range(NUM_EPISODES): # 重复试验次数
observation = self.env.reset() # 环境初始化
state = observation # 直接使用观测作为状态state使用
state = torch.from_numpy(state).type(
torch.FloatTensor) # 将NumPy变量转换为PyTorch Tensor
state = torch.unsqueeze(state, 0) # size 4转换为size 1x4
for step in range(MAX_STEPS): # 1 episode(轮)循环
if episode_final is True: # 在最终试验中,将各时刻图像添加到帧中
frames.append(self.env.render(mode='rgb_array'))
action = self.agent.get_action(state, episode) # 求取动作
# 通过执行动作a_t求s_{t+1}和done标志
# 从acttion中指定.item()并获取内容
observation_next, _, done, _ = self.env.step(
action.item()) # 使用'_'是因为在面的流程中不适用reward和info
# 给予奖励。另外,设置episode和state_next的结束评估
if done: # 如果step不超过200,或者如果倾斜超过某个角度,则done为true
state_next = None # 没有下一个状态,因此存储为None
# 添加到最近的10轮的站立步数列表中
episode_10_list = np.hstack(
(episode_10_list[1:], step + 1))
if step < 195:
reward = torch.FloatTensor(
[-1.0]) # 如果您在途中倒下,给予奖励-1作为惩罚
complete_episodes = 0 # 重置连续成功记录
else:
reward = torch.FloatTensor([1.0]) # 一直站立直到结束时奖励为1
complete_episodes = complete_episodes + 1 # 更新连续记录
else:
reward = torch.FloatTensor([0.0]) # 普通奖励为0
state_next = observation_next # 保持观察不变
state_next = torch.from_numpy(state_next).type(
torch.FloatTensor) # 将numpy变量转换为PyTorch Tensor
state_next = torch.unsqueeze(state_next, 0) # size 4转换为size 1x4
# 向经验池中添加经验
self.agent.memorize(state, action, state_next, reward)
# Experience Replay中更新Q函数
self.agent.update_q_function()
# 更新观测值
state = state_next
# 结束处理
if done:
print('%d Episode: Finished after %d steps:10次试验的平均step数 = %.1lf' % (
episode, step + 1, episode_10_list.mean()))
break
if episode_final is True:
# 保存并绘制动画
display_frames_as_gif(frames)
break
# 连续十轮成功
if complete_episodes >= 10:
print('10轮连续成功')
episode_final = True # 使下一次尝试成为最终绘制的动画
cartpole_env = Environment()
cartpole_env.run()
Sequential(
(fc1): Linear(in_features=4, out_features=32, bias=True)
(relu1): ReLU()
(fc2): Linear(in_features=32, out_features=32, bias=True)
(relu2): ReLU()
(fc3): Linear(in_features=32, out_features=2, bias=True)
)
0 Episode: Finished after 17 steps:10次试验的平均step数 = 1.7
1 Episode: Finished after 11 steps:10次试验的平均step数 = 2.8
2 Episode: Finished after 10 steps:10次试验的平均step数 = 3.8
3 Episode: Finished after 9 steps:10次试验的平均step数 = 4.7
4 Episode: Finished after 8 steps:10次试验的平均step数 = 5.5
5 Episode: Finished after 8 steps:10次试验的平均step数 = 6.3
6 Episode: Finished after 10 steps:10次试验的平均step数 = 7.3
7 Episode: Finished after 11 steps:10次试验的平均step数 = 8.4
8 Episode: Finished after 11 steps:10次试验的平均step数 = 9.5
9 Episode: Finished after 9 steps:10次试验的平均step数 = 10.4
10 Episode: Finished after 10 steps:10次试验的平均step数 = 9.7
11 Episode: Finished after 10 steps:10次试验的平均step数 = 9.6
12 Episode: Finished after 9 steps:10次试验的平均step数 = 9.5
13 Episode: Finished after 10 steps:10次试验的平均step数 = 9.6
14 Episode: Finished after 10 steps:10次试验的平均step数 = 9.8
15 Episode: Finished after 10 steps:10次试验的平均step数 = 10.0
16 Episode: Finished after 10 steps:10次试验的平均step数 = 10.0
17 Episode: Finished after 9 steps:10次试验的平均step数 = 9.8
18 Episode: Finished after 10 steps:10次试验的平均step数 = 9.7
19 Episode: Finished after 10 steps:10次试验的平均step数 = 9.8
20 Episode: Finished after 11 steps:10次试验的平均step数 = 9.9
……
495 Episode: Finished after 9 steps:10次试验的平均step数 = 9.2
496 Episode: Finished after 10 steps:10次试验的平均step数 = 9.3
497 Episode: Finished after 10 steps:10次试验的平均step数 = 9.3
498 Episode: Finished after 9 steps:10次试验的平均step数 = 9.3
499 Episode: Finished after 9 steps:10次试验的平均step数 = 9.2