DQN实现控制倒立摆
完整代码在文章结尾。
DQN算法内容
主要思想:用神经网络近似最优动作价值函数 Q ∗ ( s , a ) Q^*(s,a) Q∗(s,a)。该神经网络也称为深度Q网络(DQN)。
训练算法:时间差分算法(TD)。
最优动作价值函数用最大化消除策略:
Q
∗
(
s
t
,
a
t
)
=
max
π
Q
π
(
s
t
,
a
t
)
Q_*(s_t,a_t)=\max_πQ_π(s_t,a_t)
Q∗(st,at)=πmaxQπ(st,at)
当得知了最优动作价值函数后,可以使用其进行控制,选择价值大的动作进行执行。
神经网络结构:输入层为状态向量,输出层为动作个数的向量,每个元素代表对应动作的Q值。
训练流程
- 将一条轨迹划分成n个 ( s t , a t , r t , s t + 1 ) (s_t,a_t,r_t,s_{t+1}) (st,at,rt,st+1)四元组,存入经验回放数组。
- 随机从经验回放数组中取出一个四元组,对DQN正向传播,得到 Q Q Q值: q ^ j = Q ( s j , a j ; w n o w ) q ^ j + 1 = max a Q ( s j + 1 , a ; w n o w ) \hat{q}_j=Q(s_j,a_j;w_{now})\qquad\hat{q}_{j+1}=\max_aQ(s_{j+1},a;w_{now}) q^j=Q(sj,aj;wnow)q^j+1=amaxQ(sj+1,a;wnow)
- 计算TD目标和TD误差: y ^ j = r j + γ q ^ j + 1 δ j = q ^ j − y ^ j \hat{y}_j=r_j+\gamma\hat{q}_{j+1}\qquad \delta_j=\hat{q}_j-\hat{y}_j y^j=rj+γq^j+1δj=q^j−y^j
- 对DQN反向传播,得到梯度: g j = ∇ w Q ( s j , a j ; w n o w ) g_j=\nabla_wQ(s_j,a_j;w_{now}) gj=∇wQ(sj,aj;wnow)
- 做梯度下降更新DQN参数: w n e w = w n o w − α ⋅ δ j ⋅ g j w_{new}=w_{now}-\alpha \cdot \delta_j \cdot g_j wnew=wnow−α⋅δj⋅gj
实现流程
实现内容:
- 经验回放数组;
- 神经网络,用来近似最优动作价值函数。
- 智能体,即DQN算法;
经验回放数组类
需要实现的内容
由训练流程可知,经验回放数组应具备:
- 收集四元组,即能以元组形式存入 ( s t , a t , r t , s t + 1 ) (s_t,a_t,r_t,s_{t+1}) (st,at,rt,st+1)——可使用列表实现;
- 能从数组中随机抽取成批四元组,用于计算 Q Q Q值——可使用random模块的sample()方法实现。
具体代码
class ReplayBuffer:
"""经验回放"""
def __init__(self,batch_size=64):
self.data = [] # 用列表实现四元组的储存
self.batch_size = batch_size # 设置抽取的批量数
def add_data(self,state,action,reward,next_state,done):
"""
向数组中添加数据,可再添加是否完成done这一参数,可用于之后判断下一状态是否需要动作。
"""
self.data.append((state,action,reward,next_state,done))
def sample(self):
"""
抽取四(五)元组用于更新网络参数。
使用*sample_data,将列表解包;再用zip()方法,将各元组相同位置的元素,打包成一个元组。
"""
sample_data = random.sample(self.data,self.batch_size)
state, action, reward, next_state, done = zip(*sample_data)
return np.array(state), np.array(action), np.array(reward), np.array(next_state), np.array(done)
def length(self):
"""
返回经验回放数组的长度。
"""
return len(self.data)
神经网络类
需要实现的内容
- 输入维度;
- 隐藏层大小;
- 输入维度。
这里只使用一层隐藏层,实现简单的神经网络。
具体代码
class model(nn.Module):
"""定义Q神经网络"""
def __init__(self,state_dim,hidden_dim,action_dim):
super(model,self).__init__()
self.net = nn.Sequential(
nn.Linear(state_dim,hidden_dim),nn.ReLU(),
nn.Linear(hidden_dim,action_dim)
)
def forward(self,x):
return self.net(x)
智能体(DQN)类
需要实现的内容
- 行为策略,即做出动作与环境交互,这里使用 ϵ \epsilon ϵ-贪婪算法;
- 更新网络,根据经验回放数组中的数据,进行参数更新;
- 目标网络,使用目标网络缓解自举产生的偏差。
具体代码
class DQN():
"""DQN智能体"""
def __init__(self,state_dim,hidden_dim,action_dim,learning_rate,gamma,epsilon,target_update,device):
"""
神经网络需要输入维度,隐藏层维度,输出维度。
更新网络需要学习率,使用设备(cpu or gpu)
行为策略需要epsilon概率大小
计算回报需要gamma衰减因子
目标网络更新频率
"""
self.action_dim = action_dim
self.net = model(state_dim,hidden_dim,action_dim).to(device)
self.load() # 用来加载训练过的网络参数,没有也没关系
self.target_net = model(state_dim,hidden_dim,action_dim).to(device) # 建立目标网络,用来计算下一状态时,对动作价值的估计。
self.optimizer = torch.optim.Adam(self.net.parameters(),lr=learning_rate)
self.gamma = gamma # 奖励折扣因子
self.epsilon = epsilon #贪婪行为策略的参数
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 记录更新次数
self.device = device
self.loss = nn.MSELoss(reduction='none')
def take_action(self,state):
"""
行为策略:实现epsilon-贪婪算法,用来决定动作。
"""
if random.random() < self.epsilon:
action = random.randint(0,self.action_dim-1)
else:
state = torch.tensor(state,dtype=torch.float).to(self.device)
action = self.net(state).argmax().item() # 得到最大价值动作对应的索引值
return action
def update(self,states,actions,rewards,next_states,dones):
"""
对神经网络进行更新。
输入参数来自经验回放数组的批量抽取。
"""
states = torch.tensor(states,dtype=torch.float).to(device=self.device)
actions = torch.tensor(actions,dtype=torch.int64).reshape(-1,1).to(device=self.device) # 转换成2维多行一列,是为了之后提取对应动作的价值方便
rewards = torch.tensor(rewards,dtype=torch.float).to(device=self.device)
next_states = torch.tensor(next_states,dtype=torch.float).to(device=self.device)
dones = torch.tensor(dones,dtype=torch.float).to(device=self.device)
q_values = self.net(states).gather(-1,actions) # gather第二参数索引必须为tensor类型
max_q_values = self.target_net(next_states).max(dim=-1)[0] # 指定维度后,返回值:最大值 + 索引
q_target = rewards + self.gamma * max_q_values * (1-dones) # 当回合结束,即dones=1时,只有rewards,没有下一状态后的动作价值。下一状态存在,但没有动作了,所以直接舍去。
q_target = q_target.unsqueeze(dim=-1) # 为了下面计算loss维度一样,注释该行也没事,因为有广播机制,会自动广播到相同维度。
l = self.loss(q_values,q_target)
self.optimizer.zero_grad()
l.mean().backward()
self.optimizer.step()
# 当网络更新到一定次数,更新一次目标网络,减缓目标网络的更新频率
if self.count % self.target_update == 0:
self.target_net.load_state_dict(self.net.state_dict())
self.count += 1
def save(self):
"""
用来保存网络的参数
"""
torch.save(self.net.state_dict(),'DQN_CartPole.pth')
def load(self):
"""
加载训练好的网络参数
"""
try:
self.net.load_state_dict(torch.load('DQN_CartPole.pth'))
except:
pass
完整代码
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import random
import os
import gym
class ReplayBuffer:
"""经验回放"""
def __init__(self,batch_size=64):
self.data = []
self.batch_size = batch_size
def add_data(self,state,action,reward,next_state,done):
self.data.append((state,action,reward,next_state,done))
def sample(self):
sample_data = random.sample(self.data,self.batch_size)
state, action, reward, next_state, done = zip(*sample_data)
return np.array(state), np.array(action), np.array(reward), np.array(next_state), np.array(done)
def length(self):
return len(self.data)
class model(nn.Module):
"""定义Q神经网络"""
def __init__(self,state_dim,hidden_dim,action_dim):
super(model,self).__init__()
self.net = nn.Sequential(
nn.Linear(state_dim,hidden_dim),nn.ReLU(),
nn.Linear(hidden_dim,action_dim)
)
def forward(self,x):
return self.net(x)
class DQN():
"""DQN智能体"""
def __init__(self,state_dim,hidden_dim,action_dim,learning_rate,gamma,epsilon,target_update,device):
self.action_dim = action_dim
self.net = model(state_dim,hidden_dim,action_dim).to(device)
self.load()
self.target_net = model(state_dim,hidden_dim,action_dim).to(device)
self.optimizer = torch.optim.Adam(self.net.parameters(),lr=learning_rate)
self.gamma = gamma # 奖励折扣因子
self.epsilon = epsilon #贪婪行为策略的参数
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 记录更新次数
self.device = device
self.loss = nn.MSELoss(reduction='none')
def take_action(self,state):
if random.random() < self.epsilon:
action = random.randint(0,self.action_dim-1)
else:
state = torch.tensor(state,dtype=torch.float).to(self.device)
action = self.net(state).argmax().item()
return action
def update(self,states,actions,rewards,next_states,dones):
states = torch.tensor(states,dtype=torch.float).to(device=self.device)
actions = torch.tensor(actions,dtype=torch.int64).reshape(-1,1).to(device=self.device) # 转换成2维多行一列,是为了之后提取对应动作的价值方便
rewards = torch.tensor(rewards,dtype=torch.float).to(device=self.device)
next_states = torch.tensor(next_states,dtype=torch.float).to(device=self.device)
dones = torch.tensor(dones,dtype=torch.float).to(device=self.device)
q_values = self.net(states).gather(-1,actions) # gather第二参数索引必须为tensor类型
max_q_values = self.target_net(next_states).max(dim=-1)[0] # 指定维度后,返回值:最大值 + 索引
q_target = rewards + self.gamma * max_q_values * (1-dones)
q_target = q_target.unsqueeze(dim=-1)
l = self.loss(q_values,q_target)
self.optimizer.zero_grad()
l.mean().backward()
self.optimizer.step()
if self.count % self.target_update == 0:
self.target_net.load_state_dict(self.net.state_dict())
self.count += 1
def save(self):
torch.save(self.net.state_dict(),'DQN_CartPole.pth')
def load(self):
try:
self.net.load_state_dict(torch.load('DQN_CartPole.pth'))
except:
pass
if __name__ == '__main__':
os.system('cls' if os.name == 'nt' else 'clear')
lr = 2e-3
num_episodes = 20
gamma = 0.9
hidden_dim = 128
epsilon = 0.05
target_update = 10
buffer_size = 10000
minimal_size = 500
batch_size = 64
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
env = gym.make('CartPole-v1',render_mode='human')
# env = gym.make('CartPole-v1')
replay_buffer = ReplayBuffer(batch_size)
state_dim = env.observation_space.shape[0]
print(state_dim)
action_dim = env.action_space.n
agent = DQN(state_dim,hidden_dim,action_dim,lr,gamma,epsilon,target_update,device)
return_list = []
for i in range(10):
for i_episode in range(num_episodes):
episode_return = 0
state, info = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done, _, __ = env.step(action)
replay_buffer.add_data(state,action,reward,next_state,done)
state = next_state
episode_return += reward
if replay_buffer.length() > minimal_size:
b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample()
agent.update(b_s, b_a, b_r, b_ns, b_d)
print(episode_return)
return_list.append(episode_return)
agent.save()