返回代码索引

ppo_learner.py

MRT2A 上游 PPO learner

原文件 · 266 行 · 12641 字节

third_party/marl_mrt2a/marl_mrt2a/marl/src/learners/ppo_learner.py
SHA-256: afe1562bba86741f97e4145748fd29f6a1b10e347a448eec342c7c1c68b9fc12

1import copy2from components.episode_buffer import EpisodeBatch3from modules.critics.coma import COMACritic4from modules.critics.centralV import CentralVCritic5from utils.rl_utils import build_td_lambda_targets6import torch as th7from torch.optim import Adam8from modules.critics import REGISTRY as critic_registry9from components.standarize_stream import RunningMeanStd101112# 一个完整 episode 的栅格观测无法一次放入显存,因此按时间段前向计算并累积梯度。13# 当前策略没有使用 RNN,各时间步的损失可以分别计算后求和。14_TIME_CHUNK = 32151617class PPOLearner:18    """使用集中式 critic 更新共享的 MARL-MRT2A 策略。"""19    def __init__(self, mac, scheme, logger, args):20        self.args = args21        self.n_agents = args.n_agents22        self.n_actions = args.n_actions23        self.logger = logger2425        self.mac = mac26        # old_mac 保存本轮 PPO 更新开始前的策略,用于计算概率比值。27        self.old_mac = copy.deepcopy(mac)28        self.agent_params = list(mac.parameters())29        self.agent_optimiser = Adam(params=self.agent_params, lr=args.lr)3031        self.critic = critic_registry[args.critic_type](scheme, args)32        # target_critic 提供计算回报目标时使用的延迟参数。33        self.target_critic = copy.deepcopy(self.critic)3435        self.critic_params = list(self.critic.parameters())36        self.critic_optimiser = Adam(params=self.critic_params, lr=args.lr)3738        self.last_target_update_step = 039        self.critic_training_steps = 040        self.log_stats_t = -self.args.learner_log_interval - 14142        device = "cuda" if args.use_cuda else "cpu"43        if self.args.standardise_returns:44            self.ret_ms = RunningMeanStd(shape=(self.n_agents, ), device=device)45        if self.args.standardise_rewards:46            self.rew_ms = RunningMeanStd(shape=(1,), device=device)4748    def train(self, batch: EpisodeBatch, t_env: int, episode_num: int):49        # filled 去掉补齐的时间步,terminated 去掉 episode 结束后的数据。5051        rewards = batch["reward"][:, :-1]52        actions = batch["actions"][:, :]53        terminated = batch["terminated"][:, :-1].float()54        mask = batch["filled"][:, :-1].float()55        mask[:, 1:] = mask[:, 1:] * (1 - terminated[:, :-1])56        target_updates = batch["target_update"][:, :-1][...,1]57        action_exec = batch["action_exec"][:, :-1][...,0]58        # target_updates 表示本步重新选过目标,action_exec 表示环境实际执行了动作。59        actions = actions[:, :-1]60        if self.args.standardise_rewards:61            self.rew_ms.update(rewards)62            rewards = (rewards - self.rew_ms.mean) / th.sqrt(self.rew_ms.var)6364        mask = mask.repeat(1, 1, self.n_agents)65        mask = th.logical_and( mask, action_exec ).float()66        # critic 使用实际执行过动作的时间步;actor 还要求本步重新选择过目标。67        critic_mask = mask.clone()68        mask = th.logical_and( mask, target_updates ).float()69        # mask = th.logical_and( mask, action_exec ).float()7071        # 当前 batch 没有发生目标选择时,actor 没有可用样本。72        if mask.sum() == 0:73            self.logger.log_stat("Mask_Sum_Zero", 1, t_env)74            self.logger.console_logger.error("Actor Critic Learner: mask.sum() == 0 at t_env {}".format(t_env))75            return7677        with th.no_grad():78            # 整条序列先用 old_mac 计算一次,PPO 多轮更新期间保持不变。79            old_mac_out = []80            self.old_mac.init_hidden(batch.batch_size)81            for t in range(batch.max_seq_length - 1):82                agent_outs, _, _ = self.old_mac.forward(batch, t=t)83                old_mac_out.append(agent_outs)84            try:85                old_mac_out = th.stack(old_mac_out, dim=1)  # Concat over time86            except:87                pass88            old_pi = old_mac_out89        old_pi[mask == 0] = 1.09091        old_pi_taken = th.gather(old_pi, dim=3, index=actions).squeeze(3)92        # gather 只取轨迹中实际选择动作的概率。93        old_log_pi_taken = th.log(old_pi_taken + 1e-10)9495        mask_sum = mask.sum()96        for k in range(self.args.epochs):97            # 每个 epoch 先更新 critic,并用最新 critic 重新计算 advantage。98            advantages, critic_train_stats = self.train_critic_sequential(self.critic, self.target_critic, batch, rewards,99                                                                          critic_mask)100            advantages = advantages.detach()101102            # 分段计算 PPO 损失,每段 backward 后保留梯度,最后统一更新参数。103            self.mac.init_hidden(batch.batch_size)104            self.agent_optimiser.zero_grad()105            pg_loss = 0.0106            pi_max_num = 0.0107            for t0 in range(0, batch.max_seq_length - 1, _TIME_CHUNK):108                t1 = min(t0 + _TIME_CHUNK, batch.max_seq_length - 1)109                pi_parts = []110                for t in range(t0, t1):111                    agent_outs, _, _ = self.mac.forward(batch, t=t)112                    pi_parts.append(agent_outs)113                pi = th.stack(pi_parts, dim=1)  # Concat over time (chunk)114115                pi[mask[:, t0:t1] == 0] = 1.0116117                pi_taken = th.gather(pi, dim=3, index=actions[:, t0:t1]).squeeze(3)118                log_pi_taken = th.log(pi_taken + 1e-10)119120                ratios = th.exp(log_pi_taken - old_log_pi_taken[:, t0:t1].detach())121                # ratios 衡量当前策略相对更新前策略改变了多少。122                surr1 = ratios * advantages[:, t0:t1]123                surr2 = th.clamp(ratios, 1 - self.args.eps_clip, 1 + self.args.eps_clip) * advantages[:, t0:t1]124125                entropy = -th.sum(pi * th.log(pi + 1e-10), dim=-1)126                chunk_loss = -((th.min(surr1, surr2) + self.args.entropy_coef * entropy) * mask[:, t0:t1]).sum() / mask_sum127                if th.isnan(chunk_loss).any():128                    raise ValueError("NaN in loss")129                chunk_loss.backward()130                pg_loss += chunk_loss.detach().item()131                pi_max_num += (pi.max(dim=-1)[0] * mask[:, t0:t1]).sum().item()132133            grad_norm = th.nn.utils.clip_grad_norm_(self.agent_params, self.args.grad_norm_clip)134            self.agent_optimiser.step()135136        self.old_mac.load_state(self.mac)137138        self.critic_training_steps += 1139        if self.args.target_update_interval_or_tau > 1 and (140                self.critic_training_steps - self.last_target_update_step) / self.args.target_update_interval_or_tau >= 1.0:141            self._update_targets_hard()142            self.last_target_update_step = self.critic_training_steps143        elif self.args.target_update_interval_or_tau <= 1.0:144            self._update_targets_soft(self.args.target_update_interval_or_tau)145146        if t_env - self.log_stats_t >= self.args.learner_log_interval:147            ts_logged = len(critic_train_stats["critic_loss"])148            for key in ["critic_loss", "critic_grad_norm", "td_error_abs", "q_taken_mean", "target_mean"]:149                self.logger.log_stat(key, sum(critic_train_stats[key]) / ts_logged, t_env)150151            self.logger.log_stat("advantage_mean", (advantages * mask).sum().item() / mask_sum.item(), t_env)152            self.logger.log_stat("pg_loss", pg_loss, t_env)153            self.logger.log_stat("agent_grad_norm", grad_norm.item(), t_env)154            self.logger.log_stat("pi_max", pi_max_num / mask_sum.item(), t_env)155            self.log_stats_t = t_env156157    def train_critic_sequential(self, critic, target_critic, batch, rewards, mask):158        # Optimise critic159        target_vals = []160        with th.no_grad():161            for t in range(batch.max_seq_length):162                target_vals.append(target_critic(batch, t=t).squeeze(3))163        target_vals = th.cat(target_vals, dim=1)164165        if self.args.standardise_returns:166            target_vals = target_vals * th.sqrt(self.ret_ms.var) + self.ret_ms.mean167168        target_returns = self.nstep_returns(rewards, mask, target_vals, self.args.q_nstep)169        if self.args.standardise_returns:170            self.ret_ms.update(target_returns)171            target_returns = (target_returns - self.ret_ms.mean) / (th.sqrt(self.ret_ms.var)+1e-5)172173        running_log = {174            "critic_loss": [],175            "critic_grad_norm": [],176            "td_error_abs": [],177            "target_mean": [],178            "q_taken_mean": [],179        }180181        # Chunked value forward with gradient accumulation so the full182        # 750-step sequence never has to be materialised on the device at once.183        mask_sum = mask.sum()184        critic_loss = 0.0185        v_parts = []186        self.critic_optimiser.zero_grad()187        for t0 in range(0, batch.max_seq_length - 1, _TIME_CHUNK):188            t1 = min(t0 + _TIME_CHUNK, batch.max_seq_length - 1)189            v = []190            for t in range(t0, t1):191                v.append(critic(batch, t=t).squeeze(3))192            v = th.cat(v, dim=1)193            masked_td_error = (target_returns[:, t0:t1].detach() - v) * mask[:, t0:t1]194            loss = (masked_td_error ** 2).sum() / mask_sum195            loss.backward()196            critic_loss += loss.detach().item()197            v_parts.append(v.detach())198199        grad_norm = th.nn.utils.clip_grad_norm_(self.critic_params, self.args.grad_norm_clip)200        self.critic_optimiser.step()201202        v = th.cat(v_parts, dim=1)203        masked_td_error = (target_returns.detach() - v) * mask204205        running_log["critic_loss"].append(critic_loss)206        running_log["critic_grad_norm"].append(grad_norm.item())207        mask_elems = mask_sum.item()208        running_log["td_error_abs"].append((masked_td_error.abs().sum().item() / mask_elems))209        running_log["q_taken_mean"].append((v * mask).sum().item() / mask_elems)210        running_log["target_mean"].append((target_returns * mask).sum().item() / mask_elems)211212        if th.isnan(masked_td_error).any():213            raise ValueError("NaN in TD error")214215        return masked_td_error, running_log216217    def nstep_returns(self, rewards, mask, values, nsteps):218        nstep_values = th.zeros_like(values[:, :-1])219        for t_start in range(rewards.size(1)):220            nstep_return_t = th.zeros_like(values[:, 0])221            for step in range(nsteps + 1):222                t = t_start + step223                if t >= rewards.size(1):224                    break225                elif step == nsteps:226                    nstep_return_t += self.args.gamma ** (step) * values[:, t] * mask[:, t]227                elif t == rewards.size(1) - 1 and self.args.add_value_last_step:228                    nstep_return_t += self.args.gamma ** (step) * rewards[:, t] * mask[:, t]229                    nstep_return_t += self.args.gamma ** (step + 1) * values[:, t + 1]230                else:231                    nstep_return_t += self.args.gamma ** (step) * rewards[:, t] * mask[:, t]232            nstep_values[:, t_start, :] = nstep_return_t233        return nstep_values234235    def _update_targets(self):236        self.target_critic.load_state_dict(self.critic.state_dict())237238    def _update_targets_hard(self):239        self.target_critic.load_state_dict(self.critic.state_dict())240241    def _update_targets_soft(self, tau):242        for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()):243            target_param.data.copy_(target_param.data * (1.0 - tau) + param.data * tau)244245    def cuda(self):246        self.old_mac.cuda()247        self.mac.cuda()248        self.critic.cuda()249        self.target_critic.cuda()250251    def save_models(self, path):252        self.mac.save_models(path)253        th.save(self.critic.state_dict(), "{}/critic.th".format(path))254        th.save(self.agent_optimiser.state_dict(), "{}/agent_opt.th".format(path))255        th.save(self.critic_optimiser.state_dict(), "{}/critic_opt.th".format(path))256257    def load_models(self, path, eval=False):258        self.mac.load_models(path)259        if not eval:260            self.critic.load_state_dict(th.load("{}/critic.th".format(path), map_location=lambda storage, loc: storage))261            # Not quite right but I don't want to save target networks262            self.target_critic.load_state_dict(self.critic.state_dict())263            self.critic_optimiser.load_state_dict(264                th.load("{}/critic_opt.th".format(path), map_location=lambda storage, loc: storage))265        self.agent_optimiser.load_state_dict(266            th.load("{}/agent_opt.th".format(path), map_location=lambda storage, loc: storage))