,策略函数
会算出动作空间中每个动作
的概率值。智能体agent执行的动作是随机抽样的结果,所以带有随机性。
和动作
都被确定下来,下一个状态仍然有随机性。我们的环境用状态转移函数
计算所有可能状态的概率,然后做随机抽样,得到新的状态。
,在当前状态
下根据
-greedy策略选择一个动作
,然后执行我们选择的动作
与环境交互,获取下一个状态
,并得到
的即时回报
,这时我们会更新我们初始状态行为对的Q值
,我们更新公式中的
即使用下一个状态
中对应价值最大的动作的Q值进行更新,注意这里只是更新,并不会真的执行这个价值最大的动作。这里的更新策略(评估策略)与我们的行为策略(
-greedy)不同,这种算法我们也称为off-policy算法。
,两项的差值显然是在计算误差,那这是在计算什么误差?为什么这么计算误差就是对的?
称为TD-Target,把
称为TD-Error。
是我们估计的在状态
下采取动作
的价值,这个价值是我们从表格近似得到的,而
是我们执行了动作后实际获得的奖励
加上后续状态的估计
,前者是纯估计,后者是实际得到的奖励+估计,可以把他称为部分估计,部分估计因为有事实的部分,所以一定会比纯估计要更准确,所以我们训练算法的时候让TD-Error变小的过程就是让我们估计的
逼近TD-Target的过程,也就是让我们不准确的“全估计”逼近更准确的“部分估计”的过程。
-greedy策略选择了一个动作进入了下一个状态后,Sarsa继续根据
-greedy策略选择下一个动作执行并用这个动作更新,这里我们的更新策略(评估策略)与我们的行为策略都是
-greedy策略,这种算法我们也称为on-policy策略。
,然后通过梯度下降更新
,使得
越来越接近TD-Target。想要切断bootstrap带来的偏差,可以用另一个网络计算TD-Target,而不是用DQN自己计算TD-Target。另一个网络就叫做目标网络(Target Network),把他记做
,它的神经网络结构与 DQN 完全相同,但是参数和原始参数不同。
class AgentDQN(AgentBase):
def __init__(self):
super().__init__()
self.explore_rate = 0.1 # the probability of choosing action randomly in epsilon-greedy
self.action_dim = None # chose discrete action randomly in epsilon-greedy
def init(self, net_dim, state_dim, action_dim): # explict call self.init() for multiprocessing
self.action_dim = action_dim
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.cri = QNet(net_dim, state_dim, action_dim).to(self.device)
self.cri_target = deepcopy(self.cri)
self.act = self.cri # to keep the same from Actor-Critic framework
self.criterion = torch.torch.nn.MSELoss()
self.cri_optimizer = torch.optim.Adam(self.cri.parameters(), lr=self.learning_rate)
def select_action(self, state) -> int: # for discrete action space
if rd.rand() < self.explore_rate: # epsilon-greedy
a_int = rd.randint(self.action_dim)
else:
#截断梯度加速运算.
states = torch.as_tensor((state,), dtype=torch.float32, device=self.device).detach_()
action = self.act(states)[0]
a_int = action.argmax().cpu().numpy()
return a_int
def explore_env(self, env, buffer, target_step, reward_scale, gamma) -> int:
for _ in range(target_step):
action = self.select_action(self.state)
next_s, reward, done, _ = env.step(action)
other = (reward * reward_scale, 0.0 if done else gamma, action) # action is an int
buffer.append_buffer(self.state, other)
self.state = env.reset() if done else next_s
return target_step
def update_net(self, buffer, target_step, batch_size, repeat_times) -> (float, float):
buffer.update_now_len_before_sample()
next_q = obj_critic = None
for _ in range(int(target_step * repeat_times)):
with torch.no_grad():
reward, mask, action, state, next_s = buffer.sample_batch(batch_size) # next_state
next_q = self.cri_target(next_s).max(dim=1, keepdim=True)[0]
q_label = reward + mask * next_q
q_eval = self.cri(state).gather(1, action.type(torch.long))
obj_critic = self.criterion(q_eval, q_label)
self.cri_optimizer.zero_grad()
obj_critic.backward()
self.cri_optimizer.step()
self.soft_update(self.cri_target, self.cri, self.soft_update_tau)
return next_q.mean().item(), obj_critic.item()
class QNet(nn.Module): # nn.Module is a standard PyTorch Network
def __init__(self, mid_dim, state_dim, action_dim):
super().__init__()
self.net = nn.Sequential(nn.Linear(state_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, action_dim))
def forward(self, state):
return self.net(state) # q value
离TD-Target越远(TD-Error越大),即你预测的Q值越不准我自然越要拿你出来更新让你变准。
,它的抽样概率取决于TD-Error。有两种方法设置抽样概率,一种是:
是个很小的数,防止抽样概率接近零,用于保证所有样本都以非零的概率被抽到。第二种是先将
做降序排列,然后计算:
的序号,大的
序号小,小的
序号大。
是一个范围在0到1的超参数,需要调整。
=1时,如果抽样概率
变大10倍,从公式来看那学习率就减少10倍,看起来似乎两者的变化抵消了,但其实并不是。两种情况并不等价:
的网络不为同一个。那这种思路是否能用来缓解最大化造成的高估呢?显然是可以的。
,第二步则是求值
,沿用之前的思路,我们只要把选择动作的网络和求值的网络区分开,一个用原本的网络,一个用目标网络,自然就可以缓解最大化造成的高估。
class AgentDoubleDQN(AgentDQN):
def __init__(self):
super().__init__()
self.explore_rate = 0.25 # the probability of choosing action randomly in epsilon-greedy
self.softmax = torch.nn.Softmax(dim=1)
def init(self, net_dim, state_dim, action_dim):
self.action_dim = action_dim
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.cri = QNetTwin(net_dim, state_dim, action_dim).to(self.device)
self.cri_target = deepcopy(self.cri)
self.act = self.cri
self.criterion = torch.nn.SmoothL1Loss()
self.cri_optimizer = torch.optim.Adam(self.act.parameters(), lr=self.learning_rate)
def select_action(self, state) -> np.ndarray: # for discrete action space
states = torch.as_tensor((state,), dtype=torch.float32, device=self.device).detach_()
actions = self.act(states)
if rd.rand() < self.explore_rate: # epsilon-greedy
action = self.softmax(actions)[0]
a_prob = action.detach().cpu().numpy() # choose action according to Q value
a_int = rd.choice(self.action_dim, p=a_prob)
else:
action = actions[0]
a_int = action.argmax(dim=0).cpu().numpy()
return a_int
def update_net(self, buffer, target_step, batch_size, repeat_times) -> (float, float):
buffer.update_now_len_before_sample()
next_q = obj_critic = None
for _ in range(int(target_step * repeat_times)):
with torch.no_grad():
reward, mask, action, state, next_s = buffer.sample_batch(batch_size)
next_q = torch.min(*self.cri_target.get_q1_q2(next_s))
next_q = next_q.max(dim=1, keepdim=True)[0]
q_label = reward + mask * next_q
act_int = action.type(torch.long)
q1, q2 = [qs.gather(1, act_int) for qs in self.cri.get_q1_q2(state)]
obj_critic = self.criterion(q1, q_label) + self.criterion(q2, q_label)
self.cri_optimizer.zero_grad()
obj_critic.backward()
self.cri_optimizer.step()
self.soft_update(self.cri_target, self.cri, self.soft_update_tau)
return next_q.mean().item(), obj_critic.item() / 2
进行学习训练,会发现训练过程不稳定,因为这样A和V可以随意上下波动而不影响Q,比如A增加100,V减少100,对于Q值没有影响,这就意味着 V 和 D 的参数可以很随意地变化,却不会影响输出,所以我们需要约束这种情况。
,这样就可以让训练稳定:
class AgentD3QN(AgentDoubleDQN): # D3QN: Dueling Double DQN
def __init__(self):
super().__init__()
def init(self, net_dim, state_dim, action_dim):
"""Contribution of D3QN (Dueling Double DQN)
There are not contribution of D3QN.
Obviously, DoubleDQN is compatible with DuelingDQN.
Any beginner can come up with this idea (D3QN) independently.
"""
self.action_dim = action_dim
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.cri = QNetTwinDuel(net_dim, state_dim, action_dim).to(self.device)
self.cri_target = deepcopy(self.cri)
self.act = self.cri
self.criterion = torch.nn.SmoothL1Loss()
self.cri_optimizer = torch.optim.Adam(self.act.parameters(), lr=self.learning_rate)
class QNetTwinDuel(nn.Module): # D3QN: Dueling Double DQN
def __init__(self, mid_dim, state_dim, action_dim):
super().__init__()
self.net_state = nn.Sequential(nn.Linear(state_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, mid_dim), nn.ReLU())
self.net_val1 = nn.Sequential(nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, 1)) # q1 value
self.net_val2 = nn.Sequential(nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, 1)) # q2 value
self.net_adv1 = nn.Sequential(nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, action_dim)) # advantage function value 1
self.net_adv2 = nn.Sequential(nn.Linear(mid_dim, mid_dim), nn.ReLU(),
nn.Linear(mid_dim, action_dim)) # advantage function value 1
def forward(self, state):
t_tmp = self.net_state(state)
q_val = self.net_val1(t_tmp)
q_adv = self.net_adv1(t_tmp)
return q_val + q_adv - q_adv.mean(dim=1, keepdim=True) # one dueling Q value
def get_q1_q2(self, state):
tmp = self.net_state(state)
val1 = self.net_val1(tmp)
adv1 = self.net_adv1(tmp)
q1 = val1 + adv1 - adv1.mean(dim=1, keepdim=True)
val2 = self.net_val2(tmp)
adv2 = self.net_adv2(tmp)
q2 = val2 + adv2 - adv2.mean(dim=1, keepdim=True)
return q1, q2 # two dueling Q values
,现在就变成
,
是
的均值,
是
的标准差,前两个参数是可学习的参数,
是随机生成的噪声,生成的方式分为独立高斯噪音和分解高斯噪音。
)。第二,噪音的标准差也是学习的参数,网络通过学习可以调整噪音的大小。