本章介绍DQN改进的一些算法,改进角度略有不同,例如Double DQN 及Dueling DQN主要从网络模型层面改进,而PER DQN则从经验回放的角度改进。算法各有不同,但本质都是从提高预测精度和控制过程中的探索度来改善DQN算法性能。
8.1 Double DQN
其主要贡献是通过引入两个网络用于解决Q值过估计的,这两个网络跟前述目标网络类似。回顾一下DQN算法跟新公式,
是估计值,Qθbar是指目标网络。意思是之间用目标网络中各个动作对应得最大Q值为估计值,所以存在过估计问题。为此,Double DQN 先在当前网络中找出最大Q值对应得动作,再将这个动作带入到目标网络中去计算Q值。
相当于将动作选择和评估两个过程分离开,减轻过估计问题。Double DQN并不是每隔C步复制参数到目标网络,而是随机选择其中一个网络选择动作进行更新。假设有两个网络Qa和Qb,那么在更新Qa的时候就把Qb当作目标网络估计动作值,同时Qa也是用来选择动作的。
8.2 Dueling DQN算法
前述算法通过改进目标Q值来计算优化算法,而在此算法则是通过优化神经网络的结构来优化算法的。
输入层维度为状态的维度,输出层维度为动作的维度。
而Dueling DQN算法中则是在输出层之前分流(dueling)了俩个层,一个是优势层,用来估计每个动作带来的优势,输出维度为动作数,一个是价值层,用于估计每个状态的价值,输出维度为1.
去掉价值层就是普通的Q网络了,我们会对优势层做一个中心化处理,即减掉均值。
8.3 Noisy DQN算法
这个算法也是通过优化网络结构来提升DQN算法性能,但不同的是,目的不是为了提高Q值的估计,而是增强网络的探索能力。在DQN的基础上再神经网络中引入噪声层来提高网络性能,即将随机性应用到神经网络中的参数或者说权重,增加了对状态和动作空间的探索能力,从而提高收敛速度和稳定性。实践上就是添加随机性参数到神经网络的线性层。
8.4 PER DQN
此算法进一步优化了经验回放的设计从而提高了模型的收敛能力和鲁棒性。PER可以翻译为优先经验回放,在采样过程中赋予经验回放样本的优先集。而不同优先级的依据便是TD误差,广义的定义便是值函数(状态价值函数和动作价值函数)的估计值和实际值之差。TD误差越大,损失函数的值也越大,对反向传播的作用也越大。TD误差越大的样本更容易被采到的话算法更容易收敛实践中通过SumTree等二叉树结构来实现。
通过根节点的值来计算出每个样本的TD误差占所有样本TD误差的比例,根据比例采集样本。引入随机优先采样,即每次更新时,不是直接采样TD误差,而是定义采样概率,
重要性采样:是用于是用于估计某一分布性质的方法,基本思想是,通过与待估计分布不同的另一种分布中采样,然后通过采样样本的权重来估计分布的性质。
p(x)是带估计分布,q(x)是采样分布,f(x)是待估计分布的性质。
实战 Double DQN算法
# 计算当前网络的Q值,即Q(s_t+1|a)
next_q_value_batch = self.policy_net(next_state_batch)
# 计算目标网络的Q值,即Q'(s_t+1|a)
next_target_value_batch = self.target_net(next_state_batch)
# 计算 Q'(s_t+1|a=argmax Q(s_t+1|a))
next_target_q_value_batch = next_target_value_batch.gather(1, torch.max(next_q_value_batch, 1)[1].unsqueeze(1))
Dueling DQN
class DuelingQNetwork(nn.Module):
def __init__(self, state_dim, action_dim,hidden_dim=128):
super(DuelingQNetwork, self).__init__()
# 隐藏层
self.hidden_layer = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU()
)
# 优势层
self.advantage_layer = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, action_dim)
)
# 价值层
self.value_layer = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
def forward(self, state):
x = self.hidden_layer(state)
advantage = self.advantage_layer(x)
value = self.value_layer(x)
return value + advantage - advantage.mean() # Q(s,a) = V(s) + A(s,a) - mean(A(s,a))
Noisy DQN
class NoisyLinear(nn.Module):
'''在Noisy DQN中用NoisyLinear层替换普通的nn.Linear层
'''
def __init__(self, input_dim, output_dim, std_init=0.4):
super(NoisyLinear, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.std_init = std_init
self.weight_mu = nn.Parameter(torch.empty(output_dim, input_dim))
self.weight_sigma = nn.Parameter(torch.empty(output_dim, input_dim))
# 将一个 tensor 注册成 buffer,使得这个 tensor 不被当做模型参数进行优化。
self.register_buffer('weight_epsilon', torch.empty(output_dim, input_dim))
self.bias_mu = nn.Parameter(torch.empty(output_dim))
self.bias_sigma = nn.Parameter(torch.empty(output_dim))
self.register_buffer('bias_epsilon', torch.empty(output_dim))
self.reset_parameters() # 初始化参数
self.reset_noise() # 重置噪声
def forward(self, x):
if self.training:
weight = self.weight_mu + self.weight_sigma * self.weight_epsilon
bias = self.bias_mu + self.bias_sigma * self.bias_epsilon
else:
weight = self.weight_mu
bias = self.bias_mu
return F.linear(x, weight, bias)
def reset_parameters(self):
mu_range = 1 / self.input_dim ** 0.5
self.weight_mu.data.uniform_(-mu_range, mu_range)
self.weight_sigma.data.fill_(self.std_init / self.input_dim ** 0.5)
self.bias_mu.data.uniform_(-mu_range, mu_range)
self.bias_sigma.data.fill_(self.std_init / self.output_dim ** 0.5)
def reset_noise(self):
epsilon_in = self._scale_noise(self.input_dim)
epsilon_out = self._scale_noise(self.output_dim)
self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))
self.bias_epsilon.copy_(self._scale_noise(self.output_dim))
def _scale_noise(self, size):
x = torch.randn(size)
x = x.sign().mul(x.abs().sqrt())
return x
class NoisyQNetwork(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=128):
super(NoisyQNetwork, self).__init__()
self.fc1 = nn.Linear(state_dim, hidden_dim)
self.noisy_fc2 = NoisyLinear(hidden_dim, hidden_dim)
self.noisy_fc3 = NoisyLinear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.noisy_fc2(x))
x = self.noisy_fc3(x)
return x
def reset_noise(self):
self.noisy_fc2.reset_noise()
self.noisy_fc3.reset_noise()