BCQ —— Batch-Constrained Deep Reinforcement Learning
论文:https://arxiv.org/pdf/1812.02900.pdf
代码:https://github.com/sfujim/BCQ
提出背景
如果以标准的off-policy的方式(例如DQN和DDPG)在一个与要训练的agent毫不相关的数据集上训练的话,往往效果不好,会产生Extrapolation Error(外推误差)。意思就是,如果你用要训练的agent去在一个这个agent完全没接触过的固定数据集上,用传统的off-policy算法训练的话,效果会不好。
Extrapolation Error
论文提出了一种名为Extrapolation Error的误差。
产生这种误差的原因可能有一下几点:
1.Absent Data
固定的数据集往往无法包含所有的<state,action>对,这样就不好估计未在数据集中出现过的<state,action>的Q(s,a)。之后在使用训练好的agent去实操时,对于这种Q(s,a)的评估会有较大偏差。比如要训练一个AI去玩QQ飞车,而提供的固定数据集中没有Q(弯道,漂移)这种pair。在训练完后,agent可能永远不会在弯道做出漂移这种操作,因为它会错误地估计在弯道进行漂移操作之后的累计reward。
2.Model Bias
这个不是很理解,模型当然有偏差啊。
3.Training Mismatch
即使有充足的数据,由于用的数据不是用要训练的agent采集的,用数据集里的<state,action>pair预估Q_value时会很不准确。是因为采样的这些数据,肯定不是随机采样的,是由一个agent按照它的Q_value function去采样的,它选择Q_value大的action。而这些<state,action>对于要训练的agent来说,可能是十分陌生的,于是会造成Q_value预估不准确,而且是经常预估不准确,导致训练效果不好。
验证实验
1.Final Buffer
用DDPG算法训练一个agent边采样边学100w步,加入高斯噪声保证充分探索。这样就得到了一个很大的数据集,几乎包含了所有<state,action>pair。再用这个数据集以off-policy的方法去训练agent。
结果发现即使数据集充分大,off-policy训练出的agent的Average情况也没behavior agent好。Q_value的估计也不是很稳定。
2.Concurrent
用DDPG算法训练一个agent边采样边学100w步,加入高斯噪声保证充分探索。同时有另一个agent和它一起用采样的数据学,也就是用相同的数据。相当于一个是用自己采样出的buffer里的数据去学,一个是用别人采样的数据去学。两个agent唯一的区别就是初始参数不一样,用的数据完全一样。
发现即使是用的一样的数据,用自己采样出来的数据学也好一些。
3.Imitation
用训练好的DDPG agent去采样,不探索,全选最佳动作。这样就得到了一个全是专家轨迹的数据集,几乎包含了所有<state,action>pair。再用这个数据集去训练off-policy agent。
用专家数据学的话,由于采样出每一步得到的期望都很高,所以模型会过分高估Q_value,认为怎么都会得到很多reward。我觉得这就像监督学习二分类问题里,如果数据正类负类数量要均衡。
BCQ大致思路
用VAE去根据state来生成action,由于VAE看到的都是出现过的<state,action>pair,所以VAE根据state去生成的action会和之前VAE看见过的相似性很高。当然只用VAE来生成action当然不行,这样会缺乏探索性,BCQ里的Actor是一个扰动网络,将输入的state和action加入扰动,提高action的多样性。
算法流程
看伪代码各种符号特别复杂,还是看代码来的直接一些。
Actor
这里的Actor就是perturbation network ,它的参数是
Actor的输入是state和action,输出扰动后的action。
扰动网络的目的在于提供action的多样性
class Actor(nn.Module):
def __init__(self, state_dim, action_dim, max_action, phi=0.05):
super(Actor, self).__init__()
self.l1 = nn.Linear(state_dim + action_dim, 400)
self.l2 = nn.Linear(400, 300)
self.l3 = nn.Linear(300, action_dim)
self.max_action = max_action
self.phi = phi
def forward(self, state, action): #在action基础上做扰动
a = F.relu(self.l1(torch.cat([state, action], 1)))
a = F.relu(self.l2(a))
a = self.phi * self.max_action * torch.tanh(self.l3(a)) #使用tanh激活函数将值映射到(-1,1)
#再乘上action最大值和扰动因子得到扰动大小a
#用a+action得到扰动后的action
return (a + action).clamp(-self.max_action, self.max_action) #使用裁剪,控制范围在(-max_action,max_action)
Critic
这里的Critic就是 网络,有、 两套参数。
就是常用一个双Q trick,让Q_value的预测更准。
Critic的输入是state和action,输出两个Q值。
class Critic(nn.Module):
def __init__(self, state_dim, action_dim):
super(Critic, self).__init__()
self.l1 = nn.Linear(state_dim + action_dim, 400)
self.l2 = nn.Linear(400, 300)
self.l3 = nn.Linear(300, 1)
self.l4 = nn.Linear(state_dim + action_dim, 400)
self.l5 = nn.Linear(400, 300)
self.l6 = nn.Linear(300, 1)
def forward(self, state, action):
q1 = F.relu(self.l1(torch.cat([state, action], 1)))
q1 = F.relu(self.l2(q1))
q1 = self.l3(q1)
q2 = F.relu(self.l4(torch.cat([state, action], 1)))
q2 = F.relu(self.l5(q2))
q2 = self.l6(q2)
return q1, q2 #两个Q网络
def q1(self, state, action):
q1 = F.relu(self.l1(torch.cat([state, action], 1)))
q1 = F.relu(self.l2(q1))
q1 = self.l3(q1)
return q1
VAE
VAE在使用时,只会用到decode方法,输入是state,让z随机,然后输出action。
class VAE(nn.Module):
def __init__(self, state_dim, action_dim, latent_dim, max_action, device):
super(VAE, self).__init__()
self.e1 = nn.Linear(state_dim + action_dim, 750) #encoder第一层
self.e2 = nn.Linear(750, 750) #encoder第二层
self.mean = nn.Linear(750, latent_dim)
self.log_std = nn.Linear(750, latent_dim)
self.d1 = nn.Linear(state_dim + latent_dim, 750) #decoder第一层
self.d2 = nn.Linear(750, 750) #decoder第二层
self.d3 = nn.Linear(750, action_dim) #decoder第三层
self.max_action = max_action
self.latent_dim = latent_dim
self.device = device
def decode(self, state, z=None): #根据state与z生成action
# 如果从VAE采样,就把z裁剪到(-0.5,0.5)
if z is None:# 如果z未指定,则通过mean、std当场采样
z = torch.randn((state.shape[0], self.latent_dim)).to(self.device).clamp(-0.5,0.5) #(batch_size,latent_dim)
a = F.relu(self.d1(torch.cat([state, z], 1)))
a = F.relu(self.d2(a))
return self.max_action * torch.tanh(self.d3(a)) #(batch_size,action_dim)
def forward(self, state, action):
# 输入state、真实样本action
z = F.relu(self.e1(torch.cat([state, action], 1)))
z = F.relu(self.e2(z))
#z用来算mean和log_std
mean = self.mean(z) #mean,后面用来算KL loss
# Clamped for numerical stability
log_std = self.log_std(z).clamp(-4, 15) #为什么是[-4,15]?
std = torch.exp(log_std) #后面用来算KL loss
z = mean + std * torch.randn_like(std)
#torch.randn_like是产生和输入tensor形状一样的满足N~(0,1)分布的随机数
#z ~ N(mean,std)
#z.shape (batch,latent_dim)
u = self.decode(state, z) # (batch_size,action_dim)
# 根据state和z重建action
return u, mean, std
Train
def train(self, replay_buffer, iterations, batch_size=100):
for it in range(iterations):
# 从replay_buffer中随机采样出batch_size个时间步的数据
state, action, next_state, reward, not_done = replay_buffer.sample(batch_size)
# 训练VAE
recon, mean, std = self.vae(state, action) #
recon_loss = F.mse_loss(recon, action) # VAE重建loss
KL_loss = -0.5 * (1 + torch.log(std.pow(2)) - mean.pow(2) - std.pow(2)).mean() #KL loss
vae_loss = recon_loss + 0.5 * KL_loss # VAE loss
self.vae_optimizer.zero_grad()
vae_loss.backward()
self.vae_optimizer.step()
用VAE的encoder算出mean和std,然后用mean和std在正太分布中采样出z,再用z和state去重建action。
VAE的loss由重建loss和KL loss组成。
# 训练Critic
with torch.no_grad():
# Duplicate next state 10 times
next_state = torch.repeat_interleave(next_state, 10, 0) #在第0维重复10次
# 输入的next_state维度(batch_size,state_dim),重复10遍变成(batch_size*10,state_dim)
# Compute value of perturbed actions sampled from the VAE
target_Q1, target_Q2 = self.critic_target(next_state, self.actor_target(next_state, self.vae.decode(next_state)))
# 维度(batch_size*10,)
# Soft Clipped Double Q-learning
target_Q = self.lmbda * torch.min(target_Q1, target_Q2) + (1. - self.lmbda) * torch.max(target_Q1, target_Q2)
# Take max over each action sampled from the VAE
target_Q = target_Q.reshape(batch_size, -1).max(1)[0].reshape(-1, 1)
# 取10个生成动作样本中最大的,最终维度(batch_size,)
#根据bellman公式,算出target_Q
target_Q = reward + not_done * self.discount * target_Q
current_Q1, current_Q2 = self.critic(state, action) #算当前Q_value
# current_Q和这个tartget_Q越接近越好
critic_loss = F.mse_loss(current_Q1, target_Q) + F.mse_loss(current_Q2, target_Q)
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
1.用VAE根据next_state生成n(代码里是10)个的action
2.然后把next_state和这些action输入到Target_Actor里,生成扰动后的action。
3.在把扰动后的action和next_state输入到Target_Critic去算出target Q_value,并在n个Q_value里选最大的。
如果n足够大,就有点像Q-learning了。这里用了采样的方式来近似 ,n越大,采样越多就越能模拟整个的action space,最后选最大的就越接近真正的argmax。
当n=1,时,就像behavioral cloning了,每次选择的动作就是VAE采样出来的动作,而vae采样出的动作又很大概率是由之前出现过的<state,action>pair,所以agent会偏向于模仿数据集里的<state,action>pair。
4.当前Q_value就用state和action来计算。
5.之后更新当前的Critic
# Pertubation Model / Action Training
#获得扰动后的action 训练扰动网络 这里的actor是做扰动作用
sampled_actions = self.vae.decode(state) #用的是vae中随机的z
perturbed_actions = self.actor(state, sampled_actions) #扰动后action
# 用DPG的方式update Actor
actor_loss = -self.critic.q1(state, perturbed_actions).mean()
#这个<state,action>pair在critic里的评分越高越好
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
# Update Target Networks
for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
用vae根据state采样出action
把state和action输入到扰动网络里输出扰动后的action
用critic评估state扰动后的action的Q值,越大越好
最后再用移动平均法更新actor和critic的target network