RLlib中的example有一个代码是action_masking,很感兴趣,所以学习了一下
主要功能是:
博客原文
当我开始深度强化学习时,我工作的环境中的每个时间步都无法执行特定操作。
让我们具体说明不可能或不可用动作的概念:假设您想开发一个代理来玩马里奥赛车。接下来,假设代理有空库存(没有香蕉或任何东西)。代理无法执行“使用库存中的对象”操作。将代理限制为有意义的操作选择将使其能够以更智能的方式进行探索并输出更好的策略。
现在您了解了不可能或不可用操作的概念,自然的问题是:“我如何管理不可能的操作?” 我实施的第一个解决方案是,如果智能体采取不可能的操作,则分配负奖励。它的表现比不限制动作的选择,但我对这种方法不满意,因为它不能阻止代理选择不可能的动作。
然后我决定使用动作屏蔽(Action Masking)。这种方法实现起来简单且优雅,因为它限制代理只采取“有意义”的动作。
在我的深度强化学习实践中,我了解到有很多方法可以使用Masking。Masking可用于神经网络中的任何级别并用于不同的任务。不幸的是,除了 Costa Huang 的这篇精彩文章 [7] 之外,很少有强化学习的Masking实现可用。
这篇博文的范围是解释Masking的概念并通过图形和代码进行说明。事实上,这些Masking可以对我们在阅读这篇博文时看到的许多约束进行建模。请注意,整个过程是完全可微的。简而言之,Masking是为了简化您的生活。
如果您想了解这个概念是什么,我邀请您阅读这篇解释 Transformer 的精彩文章 [6]
深度强化学习中Masking的主要功能是过滤掉不可能或不可用的动作。例如,在《星际争霸 II》和《Dota 2》中,每个时间步的动作总数分别为 1 0 26 10^{26} 1026 和 1 , 837 , 080 1,837,080 1,837,080 。然而,每个时间步的可能操作空间仅占可用操作空间的一小部分。因此,使用Masking有两个优点:
图1说明了动作屏蔽的原理。其背后的想法很简单,它包括替换不可能的操作相关的 logits为 − ∞ -∞ −∞。
那么,为什么应用这个掩码可以防止选择不可能的动作呢?
1. 基于价值的算法(Q-Learning):
在基于价值的方法中,我们选择动作价值函数的最高估计值 Q ( s , a ) Q(s,a) Q(s,a):
a = arg max a ∈ A Q ( s , . ) a=\argmax\limits_{a\in A}Q(s,.) a=a∈AargmaxQ(s,.)
通过应用掩码,与不可能动作相关的 Q 值将等于 − ∞ -∞ −∞ ,因此它们永远不会是最高值,因此永远不会被选择。
2. 基于策略的算法(策略梯度):
在基于策略的方法中,我们根据模型输出的概率分布对动作进行采样:
a = π θ ( . ∣ s ) a=~\pi_\theta(.|s) a= πθ(.∣s)
因此,有必要将与不可能动作相关的概率设置为0。在我们使用Masking的时候不可能的动作是 − ∞ -∞ −∞。我们使用 softmax 函数从 logits 转移到概率域:
Softmax ( z ⃗ ) i = e z i ∑ j = 1 K e z j for i = 1 , … , K and z = ( z 1 , … , z K ) ∈ R K . \text{Softmax}(\vec{z})_i = \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}} \quad \text{for} \ i = 1, \ldots, K \ \text{and} \ z = (z_1, \ldots, z_K) \in \mathbb{R}^K. Softmax(z)i=∑j=1Kezjezifor i=1,…,K and z=(z1,…,zK)∈RK.
考虑到我们已将与不可能动作相关的 logits 值设置为 − ∞ -∞ −∞ ,对这些动作进行采样的概率等于0。
现在让我们练习并实现离散动作空间和基于策略的算法的动作屏蔽。我使用 Costa Huang 的论文和动作屏蔽代码 [7] 作为起点。想法很简单,我们继承 PyTorch 的 Categorical 类并添加一个可选的 mask 参数。
当我们应用Masking时,我们会替换不可能动作的 logits。
然而,由于我们使用 float32,因此我们需要以 32 位表示的最小值。在 PyTorch 中,我们通过运行 torch.finfo(torch.float.dtype).min 来获取它,即 -3.40e+38。
最后,对于一些基于策略的方法,例如近端策略优化(PPO)[12],有必要计算模型输出的概率分布熵。在我们的例子中,我们将仅计算可用操作的熵。
from typing import Optional
import torch
from torch.distributions.categorical import Categorical
from torch import einsum
from einops import reduce
class CategoricalMasked(Categorical):
def __init__(self, logits: torch.Tensor, mask: Optional[torch.Tensor] = None):
self.mask = mask
self.batch, self.nb_action = logits.size()
if mask is None:
super(CategoricalMasked, self).__init__(logits=logits)
else:
self.mask_value = torch.tensor(
torch.finfo(logits.dtype).min, dtype=logits.dtype
)
logits = torch.where(self.mask, logits, self.mask_value)
super(CategoricalMasked, self).__init__(logits=logits)
def entropy(self):
if self.mask is None:
return super().entropy()
# Elementwise multiplication
p_log_p = einsum("ij,ij->ij", self.logits, self.probs)
# Compute the entropy with possible action only
p_log_p = torch.where(
self.mask,
p_log_p,
torch.tensor(0, dtype=p_log_p.dtype, device=p_log_p.device),
)
return -reduce(p_log_p, "b a -> b", "sum", b=self.batch, a=self.nb_action)
以下代码块的目的是向您展示如何使用操作掩码。首先,我们创建虚拟逻辑和具有相同形状的虚拟蒙版。
logits_or_qvalues = torch.randn((2, 3), requires_grad=True) # batch size, nb action
print(logits_or_qvalues)
# tensor([[-1.8222, 1.0769, -0.6567],
# [-0.6729, 0.1665, -1.7856]])
mask = torch.zeros((2, 3), dtype=torch.bool) # batch size, nb action
mask[0][2] = True
mask[1][0] = True
mask[1][1] = True
print(mask) # False -> mask action
# tensor([[False, False, True],
# [ True, True, False]])
然后我们比较有和没有遮蔽的动作。
head = CategoricalMasked(logits=logits_or_qvalues)
print(head.probs) # Impossible action are not masked
# tensor([[0.0447, 0.8119, 0.1434], There remain 3 actions available
# [0.2745, 0.6353, 0.0902]]) There remain 3 actions available
head_masked = CategoricalMasked(logits=logits_or_qvalues, mask=mask)
print(head_masked.probs) # Impossible action are masked
# tensor([[0.0000, 0.0000, 1.0000], There remain 1 actions available
# [0.3017, 0.6983, 0.0000]]) There remain 2 actions available
print(head.entropy())
# tensor([0.5867, 0.8601])
print(head_masked.entropy())
# tensor([-0.0000, 0.6123])
我们可以观察到,当我们应用掩码时,与不可能的动作相关的概率等于0 。因此,我们的智能体永远不会选择不可能的动作。
最后,当我们在熵计算中不包括不可能的动作时,我们就得到了一致的值。这种校正后的熵计算使代理能够仅在有效动作上最大化其探索。
这么酷的把戏!