返回代码索引

train.py

训练入口、环境与 runner 选择

原文件 · 551 行 · 21141 字节

train/train.py
SHA-256: 8e4e909d00f0866db14e6760596f56c1ff4b0876d10b8ce416b1e5953f79995c

1"""2# @Time    : 2021/6/30 10:07 下午3# @Author  : hezhiqiang4# @Email   : tinyzqh@163.com5# @File    : train.py6"""78# !/usr/bin/env python9import sys10import os11import re12import socket13import setproctitle14import numpy as np15from pathlib import Path16import torch17import yaml18from datetime import datetime1920# Get the parent directory of the current file21parent_dir = os.path.abspath(os.path.join(os.getcwd(), "."))2223# Append the parent directory to sys.path, otherwise the following import will fail24sys.path.append(parent_dir)2526from config import get_args, get_config27from envs.macf.env_wrappers import DummyVecEnv2829"""Train script for MPEs."""303132REPO_ROOT = Path(__file__).resolve().parents[1]333435def _safe_name(value):36    safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(value)).strip("._")37    if not safe:38        raise ValueError(f"Invalid path name: {value!r}")39    return safe404142def _resolve_repo_path(value):43    path = Path(value).expanduser()44    return path if path.is_absolute() else REPO_ROOT / path454647def _unique_path(path):48    if not path.exists():49        return path50    for index in range(2, 1000):51        candidate = path.with_name(f"{path.name}-{index}")52        if not candidate.exists():53            return candidate54    raise RuntimeError(f"Could not allocate a unique run directory for {path}")555657def _link_tensorboard_dir(tb_dir, link_dir):58    link_dir.parent.mkdir(parents=True, exist_ok=True)59    if link_dir.is_symlink():60        if link_dir.resolve() == tb_dir.resolve():61            return62        raise FileExistsError(f"TensorBoard link already exists: {link_dir}")63    if link_dir.exists():64        raise FileExistsError(f"TensorBoard link path already exists: {link_dir}")6566    relative_target = os.path.relpath(tb_dir, start=link_dir.parent)67    link_dir.symlink_to(relative_target, target_is_directory=True)686970def _prepare_run_dirs(config):71    configured_run_dir = getattr(config, "run_dir", None)72    if configured_run_dir:73        run_dir = _resolve_repo_path(configured_run_dir)74    else:75        results_root = _resolve_repo_path(getattr(config, "results_root", "runs"))76        group = _safe_name(77            getattr(config, "group_name", None)78            or getattr(config, "experiment_group", "single")79        )80        env_name = _safe_name(config.env_name)81        timestamp = datetime.now().strftime("%Y%m%d-%H%M")82        group_dir = _unique_path(results_root / f"{timestamp}_{env_name}_{group}")83        run_name = f"{_safe_name(config.algorithm_name)}_{_safe_name(config.experiment_name)}"84        run_dir = group_dir / run_name8586    tb_dir = run_dir / "tb"87    tb_dir.mkdir(parents=True, exist_ok=False)8889    log_root = _resolve_repo_path(getattr(config, "log_root", "logs"))90    tb_link_dir = log_root / run_dir.parent.name / run_dir.name91    _link_tensorboard_dir(tb_dir, tb_link_dir)9293    return run_dir, tb_dir, tb_link_dir949596def _write_config_snapshot(config, run_dir):97    snapshot = {}98    for key, value in vars(config).items():99        if key in {"envs", "eval_envs", "device"}:100            continue101        snapshot[key] = str(value) if isinstance(value, Path) else value102    with (run_dir / "config.yaml").open("w", encoding="utf-8") as file:103        yaml.safe_dump(snapshot, file, sort_keys=False, allow_unicode=True)104105106def _discrete_env_class(config):107    """按 env_name 选择具体环境包。108109    taskcf 是原任务能力联盟组建环境;coadel 是 coalition delivery 环境。110    macf/MyEnv/coalition_delivery 作为历史配置别名保留。111    """112113    env_name = str(getattr(config, "env_name", "taskcf"))114    if env_name in ("capcoal", "macf", "MyEnv", "taskcf"):115        from envs.capcoal.env_discrete import DiscreteActionEnv116        return DiscreteActionEnv117    if env_name in ("delivcoal", "coalition_delivery", "coadel"):118        from envs.delivcoal.env_discrete import DiscreteActionEnv119        return DiscreteActionEnv120    if env_name in ("gymrt2a", "mrt2a"):121        from envs.gymrt2a.env_discrete import DiscreteActionEnv122        return DiscreteActionEnv123    raise ValueError(124        f"Unknown env_name={env_name!r}; expected capcoal, delivcoal, or gymrt2a."125    )126127128def make_train_env(config):129    def get_env_fn(rank):130        def init_env():131            DiscreteActionEnv = _discrete_env_class(config)132            env = DiscreteActionEnv(config)133134            env.seed(config.seed + rank * 1000)135            return env136137        return init_env138139    return DummyVecEnv([get_env_fn(i) for i in range(config.n_rollout_threads)])140141142def make_eval_env(config):143    def get_env_fn(rank):144        def init_env():145            DiscreteActionEnv = _discrete_env_class(config)146            env = DiscreteActionEnv(config)147            eval_reset_mode = getattr(config, "eval_reset_mode", None)148            if eval_reset_mode is not None:149                env.set_reset_mode(eval_reset_mode)150            env.seed(config.eval_seed + rank * 1000)151            return env152153        return init_env154155    return DummyVecEnv([get_env_fn(i) for i in range(config.n_eval_rollout_threads)])156157158def parse_args(args, parser):159    parser.add_argument("--scenario_name", type=str, default="MyEnv", help="Which scenario to run on")160    parser.add_argument("--num_landmarks", type=int, default=3)161162    add_my_args(parser)163164    config = parser.parse_known_args(args)[0]165166    return config167168169def add_my_args(parser):170171    parser.add_argument("--config_file", type=str, default=None, help="path of config file (*.yaml)")172173    # GAT174    parser.add_argument("--attr_dim", type=int, default=4, help="")175    parser.add_argument("--gat_hidden_dim", type=int, default=64, help="")176    parser.add_argument("--gat_out_dim", type=int, default=32, help="")177    parser.add_argument("--gat_mode", type=str, default="gated", choices=["gat", "cat", "gated"], help="GAT fusion mode")178    parser.add_argument(179        "--graph_model",180        type=str,181        default="legacy_gat",182        choices=[183            "legacy_gat",184            "hatt",185            "hatt_v2_0",186            "hatt_v2_0_raw",187            "hatt_v2_0_physics",188            "hatt_v2_0_taskattn",189            "hatt_v2_0_noidle",190            "hatt_v2_0_resource",191            "hatt_v2_0_setctx",192            "hatt_v2_0_plain_noidle",193            "hatt_v2_0_detached",194            "hatt_v2_0_split",195            "hatt_v2_0_type_embed",196            "hatt_v2_0_type_gate",197            "hatt_v2_1",198            "hatt_v2",199            "hatt_v2_3a",200            "hatt_v2_3b",201            "hatt_v2_3c",202            "hatt_v2_4",203            "hatt_v2_5a",204            "hatt_v2_5b",205            "hatt_v2_5c",206            "hatt_v2_5",207            "hatt_v2_6a",208            "hatt_v2_6b",209            "hatt_v2_6c",210            "hatt_v2_6",211            "hatt_v2_7a",212            "hatt_v2_7b",213            "hatt_v2_7",214            "hatt_v3_0a",215            "hatt_v3_0b",216            "hatt_v3_0c",217            "hatt_v3_1a",218            "hatt_v3_1b",219            "hatt_v3_2a",220            "hatt_v3_2b",221            "hatt_v3_2c",222            "hatt_v3_2d",223            "hatt_v3_2r",224            "hatt_v3_2r_no_transition",225            "hatt_v3_2r_no_member",226            "hatt_v3_3aa",227            "hatt_v3_4",228            "hatt_gymrt2a",229        ],230    )231    parser.add_argument("--hatt_hidden_dim", type=int, default=128)232    parser.add_argument("--hatt_num_heads", type=int, default=4)233    parser.add_argument("--hatt_dropout", type=float, default=0.0)234    parser.add_argument(235        "--hatt_capability_scale",236        type=float,237        default=5.0,238        help="scale raw agent capabilities into the normalized task requirement space for HATT residuals",239    )240    parser.add_argument(241        "--hatt_v20_task_input_mode",242        choices=("native", "restore_req_scale", "compact_projection"),243        default="native",244        help="controlled HATT v2.0 task-schema intervention used for B/C attribution",245    )246    parser.add_argument(247        "--hatt_paired_init_seed",248        type=int,249        default=-1,250        help="local actor/critic initialization seed; -1 keeps historical RNG behavior",251    )252    parser.add_argument(253        "--hatt_v20_init_scheme",254        choices=("default", "compact_function_preserving"),255        default="default",256        help="optional exact compact6-to-full12 actor initialization for schema attribution",257    )258    parser.add_argument(259        "--hatt_shared_task_obs",260        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),261        default=True,262        help="whether obs['tasks'] is shared across all agents and can be encoded once",263    )264    parser.add_argument(265        "--hatt_use_aa_attention",266        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),267        default=True,268        help="enable HATT v2.0 agent-agent relation attention",269    )270    parser.add_argument(271        "--hatt_use_coalition_context",272        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),273        default=True,274        help="enable HATT v2.0 task-indexed coalition context",275    )276    parser.add_argument(277        "--hatt_candidate_aa_mode",278        type=str,279        default="learned",280        choices=["learned", "uniform", "zero"],281        help=(282            "HATT v3.3 A-A gate mode: learned for the method, uniform for the "283            "matched training control, zero for checkpoint intervention"284        ),285    )286    parser.add_argument(287        "--hatt_aa_task_context",288        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),289        default=True,290        help=(291            "HATT v3.4: feed the serving task's residual demand/priority into "292            "the A-A edge gate; false reproduces the v3.3AA 3-dim edge"293        ),294    )295    parser.add_argument(296        "--reconfiguration_mode",297        choices=["legacy", "always", "periodic", "event", "urgency", "learned"],298        default="legacy",299        help="coalition reconsideration schedule; learned enables the HATT v2.0 PPO gate",300    )301    parser.add_argument(302        "--allow_reconfiguration_mode_mismatch",303        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),304        default=False,305        help=(306            "allow an explicit evaluation-only intervention between non-learned "307            "reconfiguration schedules; learned-gate mismatches remain forbidden"308        ),309    )310    parser.add_argument("--reconfiguration_interval", type=int, default=10)311    parser.add_argument("--reconfiguration_urgency_threshold", type=float, default=0.7)312    parser.add_argument(313        "--reconfiguration_urgency_aggregation",314        choices=["mean", "sum"],315        default="mean",316    )317    parser.add_argument("--reconfiguration_switch_cost", type=float, default=0.0)318    parser.add_argument("--reconfiguration_gate_init", type=float, default=0.8)319    parser.add_argument("--reconfiguration_gate_floor", type=float, default=0.02)320    parser.add_argument(321        "--reconfiguration_gate_parameterization",322        choices=["mixture", "switch", "hierarchical"],323        default="mixture",324        help=(325            "mixture preserves the historical latent reconsideration gate; "326            "switch makes the gate the identifiable probability of leaving "327            "the current task"328        ),329    )330    parser.add_argument(331        "--reconfiguration_gate_override",332        choices=["none", "open", "closed", "event", "event_or_learned"],333        default="none",334        help=(335            "gate intervention; event_or_learned is a safe hybrid that forces "336            "event decisions open and learns only the residual periodic switches"337        ),338    )339    parser.add_argument(340        "--reconfiguration_decision_coef",341        type=float,342        default=0.0,343        help="regularization cost for learned (non-mandatory) hierarchical redecisions",344    )345    parser.add_argument(346        "--reconfiguration_context_features",347        type=lambda value: str(value).lower() in ("1", "true", "yes", "y"),348        default=False,349        help="expose explicit event/time context to the learned reconfiguration gate",350    )351    parser.add_argument("--reconfiguration_context_dim", type=int, default=9)352353    # Act-Crs354    parser.add_argument("--obs_shape", type=int, default=32, help="")355    parser.add_argument("--share_obs_shape", type=int, default=32, help="")356357    # Enviroment358    parser.add_argument("--map_size", type=float, default=10000, help="")359    parser.add_argument("--num_uav", type=int, default=8, help="")360    parser.add_argument("--num_ugv", type=int, default=4, help="")361    parser.add_argument("--num_agents", type=int, default=12, help="")362363    parser.add_argument("--uav_perception_rg", type=list, default=[50, 200], help="")364    parser.add_argument("--ugv_perception_rg", type=list, default=[10, 30], help="")365366    parser.add_argument("--target_num", type=int, default=20, help="任务数量")367    parser.add_argument("--gat_dim", type=int, default=4, help="输出维度")368    parser.add_argument("--env_reset_mode", type=str, default="jitter",369                        choices=["jitter", "full_random"],370                        help="'jitter': base env + task jitter; 'full_random': full reinit each episode")371    parser.add_argument("--eval_reset_mode", type=str, default=None,372                        choices=["jitter", "full_random"],373                        help="optional reset mode used only by evaluation envs")374    parser.add_argument("--curriculum_switch_ep", type=int, default=-1,375                        help="switch env_reset_mode to full_random after this env_episode; -1 = no switch")376    parser.add_argument("--max_steps", type=int, default=1000, help="最大步数")377    parser.add_argument(378        "--episode_time_limit",379        type=int,380        default=None,381        help="optional termination-only horizon; max_steps keeps time/deadline semantics",382    )383    parser.add_argument("--cap_dim", type=int, default=4, help="能力维度")384385    # Runner386    parser.add_argument("--use_gat", action="store_true", default=False, help="")387    parser.add_argument("--rule_based", action="store_true", default=False, help="")388    parser.add_argument("--log_info_interval", type=int, default=1, help="")389    parser.add_argument("--log_root", type=str, default='logs', help="")390    parser.add_argument("--results_root", type=str, default="runs", help="")391    parser.add_argument("--group_name", type=str, default=None, help="")392    parser.add_argument("--experiment_group", type=str, default="single", help="")393    parser.add_argument("--rewards_config_file", type=str, default='', help="")394    parser.add_argument("--gymrt2a_map_size", type=int, default=20)395    parser.add_argument("--gymrt2a_num_agents", type=int, default=10)396    parser.add_argument("--gymrt2a_objects", type=int, nargs="+", default=[4, 3, 3])397    parser.add_argument("--gymrt2a_view_range", type=int, default=5)398    parser.add_argument("--gymrt2a_comm_range", type=int, default=8)399    parser.add_argument("--gymrt2a_comm_dim", type=int, default=0)400    parser.add_argument("--gymrt2a_share_intention", type=str, default="path")401    parser.add_argument("--gymrt2a_reward_exponent", type=float, default=2.0)402    parser.add_argument("--gymrt2a_sectors", type=int, default=2)403    parser.add_argument("--gymrt2a_respawn", type=lambda v: str(v).lower() in ("1", "true", "yes"), default=True)404    parser.add_argument("--gymrt2a_ego_dim", type=int, default=3)405    parser.add_argument("--gymrt2a_other_dim", type=int, default=4)406407408def main(args):409    parser = get_args()410    config = parse_args(args, parser)411412    get_config(config)413414    if (415        (416            getattr(config, "explore_coverage_features", False)417            or getattr(config, "explore_observer_relative_geometry", False)418        )419        and getattr(config, "hatt_shared_task_obs", False)420        and getattr(config, "use_gat", False)421        and str(getattr(config, "graph_model", "")).startswith("hatt")422    ):423        raise ValueError(424            "Observer-relative exploration features require "425            "hatt_shared_task_obs=false; broadcasting agent 0 task features "426            "would discard the other agents' candidate geometry and coverage."427        )428429    if getattr(config, "reconfiguration_mode", "legacy") == "learned":430        if not getattr(config, "use_gat", False) or config.graph_model != "hatt_v2_0":431            raise ValueError(432                "reconfiguration_mode='learned' is frozen to use_gat=true and "433                "graph_model='hatt_v2_0'; use periodic/event/urgency for other policies."434            )435436    trainer_algorithm_name = (437        getattr(config, "trainer_algorithm_name", None) or config.algorithm_name438    )439    value_decomposition_algorithms = {"qmix", "vdn", "hatt_qmix"}440    if str(config.algorithm_name).lower() in value_decomposition_algorithms:441        trainer_algorithm_name = str(config.algorithm_name).lower()442    if not getattr(config, "use_pcfa", False):443        if trainer_algorithm_name not in ("rmappo", "mappo", *value_decomposition_algorithms):444            raise NotImplementedError(445                f"unsupported trainer_algorithm_name {trainer_algorithm_name!r}"446            )447        config.trainer_algorithm_name = trainer_algorithm_name448449        if trainer_algorithm_name == "rmappo":450            assert config.use_recurrent_policy or config.use_naive_recurrent_policy, "check recurrent policy!"451        elif trainer_algorithm_name == "mappo":452            assert (453                config.use_recurrent_policy == False and config.use_naive_recurrent_policy == False454            ), "check recurrent policy!"455    else:456        # PCFA does not use a neural trainer; keep the field for logging only.457        config.trainer_algorithm_name = trainer_algorithm_name or "pcfa"458459    assert (460        config.share_policy == True and config.scenario_name == "simple_speaker_listener"461    ) == False, "The simple_speaker_listener scenario can not use shared policy. Please check the config.py."462463    # cuda464    if config.cuda >= 0 and torch.cuda.is_available():465        print("choose to use gpu...")466        device = torch.device(f"cuda:{config.cuda}")467        torch.set_num_threads(config.n_training_threads)468        if config.cuda_deterministic:469            torch.backends.cudnn.benchmark = False470            torch.backends.cudnn.deterministic = True471    else:472        print("choose to use cpu...")473        device = torch.device("cpu")474        torch.set_num_threads(config.n_training_threads)475476    run_dir, tb_dir, tb_link_dir = _prepare_run_dirs(config)477478    setproctitle.setproctitle(479        str(config.algorithm_name)480        + "-"481        + str(config.env_name)482        + "-"483        + str(config.experiment_name)484        + "@"485        + str(config.user_name)486    )487488    # seed489    torch.manual_seed(config.seed)490    torch.cuda.manual_seed_all(config.seed)491    np.random.seed(config.seed)492493    if str(config.env_name) in ("gymrt2a", "mrt2a"):494        config.num_agents = int(config.gymrt2a_num_agents)495        config.eval_reset_mode = "full_random"496    else:497        config.num_agents = config.num_uav + config.num_ugv498499    # env init500    envs = make_train_env(config)501    eval_envs = make_eval_env(config) if config.use_eval else None502503    setattr(config, 'envs', envs)504    setattr(config, 'eval_envs', eval_envs)505    setattr(config, 'device', device)506    setattr(config, 'run_dir', run_dir)507    setattr(config, 'tb_dir', tb_dir)508    setattr(config, 'tb_link_dir', tb_link_dir)509    _write_config_snapshot(config, run_dir)510511    # run experiments512    # if config.share_policy:513    #     from runner.shared.env_runner import EnvRunner as Runner514515    # if config["use_gat"]:516    if config.rule_based:517        from runner.rule_based.macf_runner import MACFEnvRunner as Runner518    elif getattr(config, "use_pcfa", False):519        from runner.pcfa.macf_runner import MACFEnvRunner as Runner520    elif str(config.trainer_algorithm_name).lower() in value_decomposition_algorithms:521        if str(config.env_name) in ("gymrt2a", "mrt2a"):522            raise NotImplementedError("QMIX-family training currently targets capcoal/delivcoal Dict interfaces.")523        from runner.qmix.macf_runner import MACFRunner as Runner524    else:525        if str(config.env_name) in ("gymrt2a", "mrt2a"):526            from runner.shared.gymrt2a_runner import GymRT2ARunner as Runner527        else:528            from runner.shared.macf_runner import MACFEnvRunner as Runner529    # else:530        # from runner.separated_nogat.env_runner import EnvRunner as Runner531    # from runner.pcfa.macf_runner import MACFEnvRunner as Runner532533    # from runner.qmix.macf_runner import MACFRunner as Runner534535    runner = Runner(config)536    runner.run()537538    # post process539    envs.close()540    if config.use_eval and eval_envs is not envs:541        eval_envs.close()542543    runner.writter.export_scalars_to_json(str(run_dir / "summary.json"))544    runner.writter.close()545    if hasattr(runner, "writer") and runner.writer is not runner.writter:546        runner.writer.flush()547        runner.writer.close()548549550if __name__ == "__main__":551    main(sys.argv[1:])