Files
RL_TRPO/main.py
T

299 lines
11 KiB
Python

import argparse
import os
import random
from pathlib import Path
os.environ.setdefault("MKL_THREADING_LAYER", "GNU")
import gymnasium as gym
import matplotlib.pyplot as plt
import numpy as np
import torch
from tqdm.auto import tqdm
from agent.ppo import PPOAgent
from agent.trpo import TRPOAgent
def parse_args():
cpu_count = os.cpu_count() or 1
default_envs = max(1, min(4, cpu_count))
default_threads = max(1, min(8, cpu_count))
parser = argparse.ArgumentParser(description="使用 GPU / 多核并行训练 PPO 与 TRPO,并输出对比图。")
parser.add_argument("--env-name", type=str, default="Pendulum-v1", help="Gymnasium 环境名称。")
parser.add_argument("--num-episodes", type=int, default=500, help="每个算法训练的 episode 数。")
parser.add_argument("--batch-size", type=int, default=2000, help="每次策略更新前收集的环境步数。")
parser.add_argument("--num-envs", type=int, default=default_envs, help="并行采样环境数。大于 1 时可显著提高吞吐。")
parser.add_argument("--vector-mode", choices=("sync", "async"), default="async", help="向量环境模式。async 会启动多进程,更适合多核 CPU。")
parser.add_argument("--cpu-threads", type=int, default=default_threads, help="PyTorch CPU 线程数。GPU 训练时主要影响 CPU 侧数据准备。")
parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto", help="训练设备。auto 会优先选择 CUDA。")
parser.add_argument("--seed", type=int, default=42, help="全局随机种子。")
parser.add_argument("--hidden-dim", type=int, default=128, help="Actor/Critic 隐层宽度。")
parser.add_argument("--output-dir", type=str, default="outputs", help="图像与训练数据的输出目录。")
parser.add_argument("--no-show", action="store_true", help="仅保存图像,不弹出 matplotlib 窗口。")
return parser.parse_args()
def resolve_device(device_name):
if device_name == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
if device_name == "cuda" and not torch.cuda.is_available():
raise RuntimeError("请求使用 CUDA,但当前环境不可用。请检查 GPU 驱动和 PyTorch CUDA 版本。")
return torch.device(device_name)
def configure_runtime(cpu_threads, seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
if cpu_threads is not None:
torch.set_num_threads(max(1, cpu_threads))
if hasattr(torch, "set_num_interop_threads"):
torch.set_num_interop_threads(max(1, min(cpu_threads, 4)))
if hasattr(torch, "set_float32_matmul_precision"):
torch.set_float32_matmul_precision("high")
def make_env(env_name, seed, worker_idx):
def _thunk():
env = gym.make(env_name)
env.reset(seed=seed + worker_idx)
env.action_space.seed(seed + worker_idx)
return env
return _thunk
def create_vector_env(env_name, num_envs, seed, vector_mode):
env_fns = [make_env(env_name, seed, idx) for idx in range(num_envs)]
if num_envs == 1 or vector_mode == "sync":
return gym.vector.SyncVectorEnv(env_fns)
return gym.vector.AsyncVectorEnv(env_fns, context="spawn")
def format_postfix(episode_rewards, total_env_steps, updates, last_update_stats):
postfix = {
"avg20": f"{np.mean(episode_rewards[-20:]):.1f}" if episode_rewards else "n/a",
"steps": total_env_steps,
"updates": updates,
}
for key, value in last_update_stats.items():
if isinstance(value, (int, float, np.floating)):
postfix[key] = f"{float(value):.4f}"
return postfix
def train_agent(agent, env_name, num_episodes=500, batch_size=2000, num_envs=1, seed=42, vector_mode="async", desc=None):
"""通用训练函数,适用于 PPO 和 TRPO,支持 GPU 和向量化并行采样。"""
env = create_vector_env(env_name, num_envs, seed, vector_mode)
episode_rewards = []
memory = {
"states": [],
"actions": [],
"rewards": [],
"next_states": [],
"masks": [],
}
total_env_steps = 0
updates = 0
last_update_stats = {}
states, _ = env.reset(seed=seed)
running_rewards = np.zeros(num_envs, dtype=np.float64)
step_count = 0
progress = tqdm(total=num_episodes, desc=desc or agent.__class__.__name__, dynamic_ncols=True)
try:
while len(episode_rewards) < num_episodes:
actions = agent.get_action(states)
next_states, rewards, terminated, truncated, _ = env.step(actions)
done = np.logical_or(terminated, truncated)
masks = (~done).astype(np.float32)
reward_store = rewards.astype(np.float32).copy()
truncated_only = np.logical_and(truncated, ~terminated)
if np.any(truncated_only):
bootstrap_values = np.asarray(agent.get_value(next_states[truncated_only]), dtype=np.float32)
reward_store[truncated_only] += agent.gamma * bootstrap_values
memory["states"].append(np.asarray(states, dtype=np.float32).copy())
memory["actions"].append(np.asarray(actions, dtype=np.float32).copy())
memory["rewards"].append(np.asarray(reward_store, dtype=np.float32).copy())
memory["next_states"].append(np.asarray(next_states, dtype=np.float32).copy())
memory["masks"].append(np.asarray(masks, dtype=np.float32).copy())
running_rewards += rewards
step_count += num_envs
total_env_steps += num_envs
if np.any(done):
done_indices = np.flatnonzero(done)
new_episode_rewards = running_rewards[done_indices].tolist()
remaining = num_episodes - len(episode_rewards)
accepted = new_episode_rewards[:remaining]
if accepted:
episode_rewards.extend(accepted)
progress.update(len(accepted))
running_rewards[done_indices] = 0.0
progress.set_postfix(format_postfix(episode_rewards, total_env_steps, updates, last_update_stats))
if len(episode_rewards) >= num_episodes:
break
reset_mask = np.zeros(num_envs, dtype=bool)
reset_mask[done_indices] = True
reset_states, _ = env.reset(options={"reset_mask": reset_mask})
next_states[done_indices] = reset_states[done_indices]
states = next_states
if step_count >= batch_size:
last_update_stats = agent.update(memory)
for key in memory:
memory[key].clear()
step_count = 0
updates += 1
progress.set_postfix(format_postfix(episode_rewards, total_env_steps, updates, last_update_stats))
if memory["states"]:
last_update_stats = agent.update(memory)
updates += 1
progress.set_postfix(format_postfix(episode_rewards, total_env_steps, updates, last_update_stats))
finally:
progress.close()
env.close()
summary = {
"episodes": len(episode_rewards),
"total_env_steps": total_env_steps,
"updates": updates,
"final_avg20": float(np.mean(episode_rewards[-20:])) if episode_rewards else float("nan"),
}
summary.update(last_update_stats)
return episode_rewards, summary
def smooth(rewards, window=10):
"""滑动平均平滑曲线。"""
rewards = np.asarray(rewards, dtype=np.float32)
if rewards.size == 0:
return rewards
smoothed = []
for i in range(rewards.size):
start = max(0, i - window + 1)
smoothed.append(float(np.mean(rewards[start:i + 1])))
return np.asarray(smoothed, dtype=np.float32)
def save_artifacts(ppo_rewards, trpo_rewards, output_dir, env_name):
output_dir.mkdir(parents=True, exist_ok=True)
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
axes[0].plot(ppo_rewards, alpha=0.3, color="blue", label="PPO (raw)")
axes[0].plot(trpo_rewards, alpha=0.3, color="red", label="TRPO (raw)")
axes[0].plot(smooth(ppo_rewards, 20), color="blue", linewidth=2, label="PPO (smooth)")
axes[0].plot(smooth(trpo_rewards, 20), color="red", linewidth=2, label="TRPO (smooth)")
axes[0].set_title(f"PPO vs TRPO on {env_name}")
axes[0].set_xlabel("Episode")
axes[0].set_ylabel("Total Reward")
axes[0].legend()
axes[0].grid(True)
axes[1].plot(smooth(ppo_rewards, 20), color="blue", linewidth=2, label="PPO")
axes[1].plot(smooth(trpo_rewards, 20), color="red", linewidth=2, label="TRPO")
axes[1].set_title("PPO vs TRPO (Smoothed, window=20)")
axes[1].set_xlabel("Episode")
axes[1].set_ylabel("Total Reward")
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
figure_path = output_dir / "ppo_vs_trpo_comparison.png"
metrics_path = output_dir / "training_metrics.npz"
fig.savefig(figure_path, dpi=150)
np.savez(metrics_path, ppo_rewards=np.asarray(ppo_rewards), trpo_rewards=np.asarray(trpo_rewards))
return fig, figure_path, metrics_path
def main():
args = parse_args()
configure_runtime(args.cpu_threads, args.seed)
device = resolve_device(args.device)
output_dir = Path(args.output_dir)
env = gym.make(args.env_name)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_bound = float(env.action_space.high[0])
env.close()
tqdm.write(
f"配置: env={args.env_name}, device={device}, num_envs={args.num_envs}, "
f"vector_mode={args.vector_mode}, cpu_threads={args.cpu_threads}, batch_size={args.batch_size}"
)
ppo_agent = PPOAgent(
state_dim=state_dim,
action_dim=action_dim,
action_bound=action_bound,
hidden_dim=args.hidden_dim,
device=device,
)
ppo_rewards, ppo_summary = train_agent(
ppo_agent,
args.env_name,
num_episodes=args.num_episodes,
batch_size=args.batch_size,
num_envs=args.num_envs,
seed=args.seed,
vector_mode=args.vector_mode,
desc="PPO",
)
tqdm.write(f"PPO 完成: {ppo_summary}")
trpo_agent = TRPOAgent(
state_dim=state_dim,
action_dim=action_dim,
action_bound=action_bound,
hidden_dim=args.hidden_dim,
device=device,
)
trpo_rewards, trpo_summary = train_agent(
trpo_agent,
args.env_name,
num_episodes=args.num_episodes,
batch_size=args.batch_size,
num_envs=args.num_envs,
seed=args.seed + 10_000,
vector_mode=args.vector_mode,
desc="TRPO",
)
tqdm.write(f"TRPO 完成: {trpo_summary}")
fig, figure_path, metrics_path = save_artifacts(ppo_rewards, trpo_rewards, output_dir, args.env_name)
tqdm.write(f"对比图已保存至 {figure_path}")
tqdm.write(f"训练曲线原始数据已保存至 {metrics_path}")
if args.no_show:
plt.close(fig)
else:
plt.show()
if __name__ == "__main__":
main()