返回代码索引

HATTGyMRT2ANetwork.py

GymRT2A 空间候选 HATT actor

原文件 · 135 行 · 6795 字节

algorithms/algorithm/HATTGyMRT2ANetwork.py
SHA-256: dd9e27c9f677dee4686c5af0180d7a5cc227f05b516f2e8d7309c85b5736da2f

1"""用于 GyMRT2A 空间动作网格的 HATT actor。23动作网格中的每个格子都作为一个候选位置,并共用同一个 decoder。当前可见的其他4智能体先通过 A-A attention 汇总,再参与候选位置打分。5"""67import torch8import torch.nn as nn910from algorithms.algorithm.HATTV20Network import FeatureEncoder, RelationAttention11from algorithms.utils.distributions import FixedCategorical12from algorithms.utils.rnn import RNNLayer13from algorithms.utils.util import check141516class HATTGyMRT2AActor(nn.Module):17    """将空间观测编码成网格中每个位置的动作分数。"""18    def __init__(self, args, ego_dim, other_dim, task_dim, num_agents, num_tasks, device):19        super().__init__()20        self.num_agents = num_agents21        self.num_tasks = num_tasks22        self.hidden_size = args.hidden_size23        self.use_policy_active_masks = args.use_policy_active_masks24        self.use_recurrent = args.use_naive_recurrent_policy or args.use_recurrent_policy25        self.tpdv = dict(dtype=torch.float32, device=device)26        hidden = args.hatt_hidden_dim2728        self.ego_encoder = FeatureEncoder(ego_dim, hidden)29        # 当前智能体、其他智能体和候选位置的字段分别编码。30        self.other_encoder = FeatureEncoder(other_dim, hidden)31        self.candidate_encoder = FeatureEncoder(task_dim, hidden)32        self.aa_attention = RelationAttention(hidden, edge_dim=3, num_heads=args.hatt_num_heads)33        # A-A 输出与当前智能体表示合并后,再转换到 MAPPO 的 hidden_size。34        self.agent_fusion = nn.Sequential(nn.Linear(hidden * 2, hidden), nn.ReLU(), nn.Linear(hidden, hidden))35        self.agent_norm = nn.LayerNorm(hidden)36        self.agent_projection = nn.Sequential(37            nn.Linear(hidden, self.hidden_size), nn.ReLU(), nn.LayerNorm(self.hidden_size)38        )39        if self.use_recurrent:40            self.rnn = RNNLayer(41                self.hidden_size, self.hidden_size, args.recurrent_N, args.use_orthogonal42            )43        self.decoder = nn.Sequential(44            # 每个格子的输入包含智能体表示、候选位置表示和二维相对坐标。45            nn.Linear(self.hidden_size + hidden + 2, self.hidden_size),46            nn.ReLU(),47            nn.Linear(self.hidden_size, self.hidden_size),48            nn.ReLU(),49            nn.Linear(self.hidden_size, 1),50        )51        nn.init.orthogonal_(self.decoder[-1].weight, gain=0.01)52        nn.init.zeros_(self.decoder[-1].bias)53        self.last_diagnostics = {}54        self.to(device)5556    def _prepare_obs(self, obs):57        """整理观测张量,并记录调用方是否只传入了一个 batch。"""58        if not isinstance(obs, dict):59            raise TypeError("HATTGyMRT2AActor requires Dict observations.")60        result = {key: check(value).to(**self.tpdv) for key, value in obs.items()}61        if result["ego"].ndim == 2:62            # buffer 会把 batch 和智能体维压平,这里恢复成两维。63            result = {64                key: value.reshape(value.shape[0] // self.num_agents, self.num_agents, *value.shape[1:])65                for key, value in result.items()66            }67        return result6869    def _distribution(self, obs, rnn_states, masks, available_actions):70        # HATT 先输出任务槽分数,环境适配层已将任务槽排成空间动作网格。71        # RNN 保存跨时间步的信息,动作掩码负责去掉当前不可达的位置。72        obs = self._prepare_obs(obs)73        batch, agents, _ = obs["ego"].shape74        available = check(available_actions).to(**self.tpdv).reshape(batch, agents, self.num_tasks)7576        ego_h = self.ego_encoder(obs["ego"])77        other_h = self.other_encoder(obs["others"])78        candidate_h = self.candidate_encoder(obs["tasks"])79        # others 的前两项是相对位置,第 3 项表示该智能体是否可见。80        relative = obs["others"][..., :2]81        visible = obs["others"][..., 2] > 0.582        edge = torch.cat([relative, visible.float().unsqueeze(-1)], dim=-1)83        aa_context, aa_weights = self.aa_attention(ego_h, other_h, edge, visible)84        # 残差保留当前智能体自己的编码。85        agent_h = self.agent_norm(ego_h + self.agent_fusion(torch.cat([ego_h, aa_context], dim=-1)))86        agent_h = self.agent_projection(agent_h).reshape(batch * agents, self.hidden_size)8788        rnn_states = check(rnn_states).to(**self.tpdv)89        masks = check(masks).to(**self.tpdv)90        if self.use_recurrent:91            # RNN 的输入按 batch × 智能体展开。92            agent_h, rnn_states = self.rnn(agent_h, rnn_states, masks)93        agent_h = agent_h.reshape(batch, agents, self.hidden_size)9495        expanded = agent_h.unsqueeze(2).expand(-1, -1, self.num_tasks, -1)96        # 将当前智能体表示复制到每个网格位置,再逐格计算 logit。97        candidate_xy = obs["tasks"][..., :2]98        logits = self.decoder(torch.cat([expanded, candidate_h, candidate_xy], dim=-1)).squeeze(-1)99        logits = logits.reshape(batch * agents, self.num_tasks)100        logits = logits.masked_fill(available.reshape(batch * agents, self.num_tasks) <= 0, -1e10)101        # 诊断项只用于日志,全部从反向传播图中分离。102        self.last_diagnostics = {103            "aa_attention": aa_weights.detach(),104            "visible_agents": visible.float().sum(dim=-1).detach(),105            "candidate_context_norm": candidate_h.norm(dim=-1).detach(),106        }107        return FixedCategorical(logits=logits), rnn_states108109    def forward(self, obs, rnn_states, masks, available_actions=None, deterministic=False):110        dist, rnn_states = self._distribution(obs, rnn_states, masks, available_actions)111        actions = dist.mode() if deterministic else dist.sample()112        return actions, dist.log_probs(actions), rnn_states113114    def evaluate_actions(115        self,116        obs,117        rnn_states,118        action,119        masks,120        available_actions=None,121        active_masks=None,122        gate_actions=None,123    ):124        del gate_actions  # GyMRT2A 只有一个离散动作输出,不使用额外 gate 动作。125        dist, _ = self._distribution(obs, rnn_states, masks, available_actions)126        action = check(action).to(device=self.tpdv["device"])127        log_probs = dist.log_probs(action)128        entropy = dist.entropy()129        # 未激活的智能体不参与平均熵计算。130        if active_masks is not None and self.use_policy_active_masks:131            active_masks = check(active_masks).to(**self.tpdv).squeeze(-1)132            entropy = (entropy * active_masks).sum() / active_masks.sum().clamp_min(1.0)133        else:134            entropy = entropy.mean()135        return log_probs, entropy