在Dueling DQN中,我们引入优势函数,从而Q网络被建模为
,其中V为状态价值函数,A则为该状态采取不同动作的优势函数,表示采取不同动作的差异性,我们利用神经网络分别输出状态价值函数和优势函数,再求和得到Q值。
将状态价值函数和优势函数分别建模的好处是:某些环境下智能体只会关注状态价值而并不关心不同动作导致的差异,将二者分开建模可以更好地处理与动作关联较小的状态,因为每次更新时,函数状态价值函数V都会被更新,这样子的同时也会影响到其他动作的Q值,而传统的DQN算法只会更新某个动作的Q值,因此采用Dueling DQN算法能够更加准确地估计Q值。
然而对于上述的Q值公式,存在对于V值和A值建模不唯一性的问题,如果加V之加上任意大小的常数C,再将所有A值减去C,则Q值不变,也就是说神经网络中的V和A是在上下波动的,如果波动的幅度相同,方向相反,那么神经网络的Q值输出相同,而V值和A值的上下波动会使得神经网络不稳定,没有很好的训练效果。
为了解决这一问题,我们可以强制取到最优动作时的优势函数输出为0,即:
当取最优动作a*时,A(s,a*)=maxA(s,a'),从而maxQ(s,a)=V(s),此时最优的Q有唯一的V,确保了V值建模的唯一性。对于该式子的证明,我们可以对优势函数A*(s,a)=Q*(s,a)-V*(s),两边同时取最优动作a*,则有maxA*(s,a*)=maxQ*(s,a*)-V*(s),又根据最优策略与Q值的关系(贝尔曼最优方程),有V*(s)=maxQ*(s,a),可以得到maxA*(s,a)=0。
那么就可以得到Q*(s,a)=V*(s)+A*(s,a)-maxA*(s,a)。
在实现过程中,我们还可以用平均操作替代max操作,即:
这个操作其实并没有什么科学依据,只是实践经验证明这样子的效果更加稳定
下面我们采用倒立摆环境
导入库
import random
import gym
import numpy as np
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
import rl_utils
from tqdm import tqdm
定义经验回放池
class ReplayBuffer:
"""经验回放池"""
def __init__(self,capacity):
#创建一个双向队列
self.buffer=collections.deque(maxlen=capacity) #队列,先进先出
def add(self,state,action,reward,next_state,done): #将数据加入buffer
self.buffer.append((state,action,reward,next_state,done))
def sample(self,batch_size): #从buffer中采样数据,数量为batch_size
#采样之后,transitions是一个列表,每一个元素是一个聚合体
transitions=random.sample(self.buffer,batch_size)
#zip是将列表分离,然后将所有聚合体的第一个元素给state,第二个给action,以此类推
state,action,reward,next_state,done=zip(*transitions)
#所以分配之后,state,action等等都是一个列表
return np.array(state),action,reward,np.array(next_state),done
def size(self): #目前buffer中数据的数量
return len(self.buffer)
定义VA网络
class VAnet(torch.nn.Module):
"""只有一层隐藏层的A网络和V网络"""
def __init__(self,state_dim,hidden_dim,action_dim):
super(VAnet,self).__init__()
self.fc1=torch.nn.Linear(state_dim,hidden_dim) #共享网络部分
#A值是状态-动作对的优势值,而V是状态的价值,不需要考虑动作,所以A网络输出层的感知机是action_dim个,而V网络是一个
self.fc_A=torch.nn.Linear(hidden_dim,action_dim)
self.fc_V=torch.nn.Linear(hidden_dim,1)
def forward(self,x):
A=self.fc_A(F.relu(self.fc1(x)))
#print(A.shape)
V=self.fc_V(F.relu(self.fc1(x)))
#print(V.shape)
#这里计算时,V以及A.mean(1).veiw(-1,1)会自动广播到与A的形状相同的张量再进行计算
#A.mean(1)就是按列索引求平均值,也就是求每一行的平均值,所以将A变成[1,]的张量,然后再通过view增加他的维度,变成[1,1],其中维度1的1是根据view的1直接添加的,而view的-1表示自动计算
#因为我们需要得到的是Q,也是状态-动作对的价值,也需要考虑到action,因此[1,11]的形状是没有问题的
Q=V+A-A.mean(1).view(-1,1) #Q值由V值和A值计算得到
#print(Q.shape)
return Q
在DQN代码的基础上进行修改,使用Dueling DQN
class DQN:
''' DQN算法,包括Double DQN和Dueling DQN '''
def __init__(self,
state_dim,
hidden_dim,
action_dim,
learning_rate,
gamma,
epsilon,
target_update,
device,
dqn_type='VanillaDQN'):
self.action_dim = action_dim
if dqn_type == 'DuelingDQN': # Dueling DQN采取不一样的网络框架
self.q_net = VAnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.target_q_net = VAnet(state_dim, hidden_dim,
self.action_dim).to(device)
else:
self.q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.target_q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
self.target_update = target_update
self.count = 0
self.dqn_type = dqn_type
self.device = device
def take_action(self, state):
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = torch.tensor([state], dtype=torch.float).to(self.device)
action = self.q_net(state).argmax().item()
return action
def max_q_value(self, state):
state = torch.tensor([state], dtype=torch.float).to(self.device)
return self.q_net(state).max().item()
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(-1, 1).to(self.device)
q_values = self.q_net(states).gather(1, actions)
if self.dqn_type == 'DoubleDQN':
#max(1)得到的是最大值及其索引,最大值是[0],索引是[1],所以这里采用max(1)[1]是得到最大值的动作
max_action = self.q_net(next_states).max(1)[1].view(-1, 1)
#得到动作之后,再利用目标网络计算最大动作的Q值
max_next_q_values = self.target_q_net(next_states).gather(
1, max_action)
else:
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(
-1, 1)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
self.optimizer.zero_grad()
dqn_loss.backward()
self.optimizer.step()
if self.count % self.target_update == 0:
self.target_q_net.load_state_dict(self.q_net.state_dict())
self.count += 1
定义离线策略训练函数
def train_DQN(agent,env,num_episodes,replay_buffer,minimal_size,batch_size):
return_list=[]
max_q_value_list=[]
max_q_value=0
for i in range(10):
with tqdm(total=int(num_episodes/10),desc='Iteration %d'% i) as pbar:
for i_episode in range(int(num_episodes/10)):
episode_return=0
state=env.reset()
done=False
while not done:
action=agent.take_action(state)
#指数加权移动平均法,对历史数据的逐步遗忘和当前数据的重要性更新
#加权处理,实现对历史数据的逐步遗忘和重要性更新
max_q_value=agent.max_q_value(state)*0.005+max_q_value*0.995 #平滑处理
max_q_value_list.append(max_q_value) #保存每个状态的最大Q值
action_continuous=dis_to_con(action,env,agent.action_dim)
#训练神经网络时是将离散化的动作传进去的,然后让环境做动作时应该将动作还原回连续的状态
next_state,reward,done,_=env.step([action_continuous])
replay_buffer.add(state,action,reward,next_state,done)
state=next_state
episode_return+=reward
if replay_buffer.size()>minimal_size:
b_s,b_a,b_r,b_ns,b_d=replay_buffer.sample(batch_size)
transition_dict={'states':b_s,'actions':b_a,'rewards':b_r,'next_states':b_ns,'dones':b_d}
agent.update(transition_dict)
return_list.append(episode_return)
if(i_episode+1) %10==0:
pbar.set_postfix({'episode':'%d' %(num_episodes/10*i+i_episode+1),'return':'%.3f'% np.mean(return_list[-10:])})
pbar.update(1)
return return_list,max_q_value_list
进行训练,并将结果可视化
random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
target_update, device, 'DuelingDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes,
replay_buffer, minimal_size,
batch_size)
episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Dueling DQN on {}'.format(env_name))
plt.show()
frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='--')
plt.axhline(10, c='red', ls='--')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('Dueling DQN on {}'.format(env_name))
plt.show()
相比于传统的DQN,Dueling DQN在多个动作选择下的学习更加稳定,得到的回报最大值也更大。