Files
RL_SAC/train.py
T

269 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
训练入口 — 支持 SAC / PPO / 对比(compare) 三种模式
用法:
python train.py --algo sac # 仅训练 SAC(行为与原始版本一致)
python train.py --algo ppo # 仅训练 PPO
python train.py --algo compare # 依次训练 SAC 和 PPO,结束后输出对比曲线
可选参数:
--env Gymnasium 环境 ID(默认 Pendulum-v1
--seed 随机种子(默认 0)
"""
import argparse
import os
import yaml
import numpy as np
import torch
import gymnasium as gym
from typing import cast
from gymnasium.spaces import Box
from algorithms.sac import SAC
from algorithms.ppo import PPO
from utils.replay_buffer import ReplayBuffer
from utils.logger import Logger
# ===========================================================================
# 配置加载
# ===========================================================================
def load_config(path: str) -> dict:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
ROOT = os.path.dirname(os.path.abspath(__file__))
SAC_CFG_PATH = os.path.join(ROOT, "configs", "sac_pendulum_config.yaml")
PPO_CFG_PATH = os.path.join(ROOT, "configs", "ppo_pendulum_config.yaml")
# ===========================================================================
# 工具函数
# ===========================================================================
def set_seed(seed: int, env: gym.Env):
np.random.seed(seed)
torch.manual_seed(seed)
env.reset(seed=seed)
def make_env(env_id: str):
env = gym.make(env_id)
obs_space = cast(Box, env.observation_space)
act_space = cast(Box, env.action_space)
if obs_space.shape is None or act_space.shape is None:
raise ValueError("环境的 observation/action space 不支持 shape 维度读取。")
state_dim = obs_space.shape[0]
action_dim = act_space.shape[0]
max_action = float(act_space.high[0])
return env, state_dim, action_dim, max_action
# ===========================================================================
# SAC 训练循环 (off-policy)
# ===========================================================================
def train_sac(env_id: str, seed: int) -> list:
"""
训练 SAC 并返回每个 episode 的总奖励列表。
"""
config = load_config(SAC_CFG_PATH)
env, state_dim, action_dim, max_action = make_env(env_id)
set_seed(seed, env)
print(f"\n{'='*60}")
print(f" Training SAC on {env_id}")
print(f" state_dim={state_dim}, action_dim={action_dim}, max_action={max_action}")
print(f"{'='*60}\n")
agent = SAC(state_dim, action_dim, max_action, config)
replay_buffer = ReplayBuffer(state_dim, action_dim, max_size=config['buffer_size'])
logger = Logger()
total_steps = 0
for episode in range(config['max_episodes']):
state, _ = env.reset()
episode_reward: float = 0.0
for _ in range(config['max_steps']):
# 动作选择:预热期使用纯随机,之后用策略
if total_steps < config['start_steps']:
action = env.action_space.sample()
else:
action = agent.select_action(state, evaluate=False)
next_state, reward, terminated, truncated, _ = env.step(action)
done = float(terminated)
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
episode_reward += float(reward)
total_steps += 1
if replay_buffer.size > config['batch_size']:
agent.update(replay_buffer, config['batch_size'])
if terminated or truncated:
break
logger.record(episode_reward)
print(f"[SAC] Episode: {episode+1:03d} | Steps: {total_steps:06d} | Reward: {episode_reward:.2f}")
if (episode + 1) % 50 == 0:
path = os.path.join(ROOT, f"sac_actor_pendulum_ep{episode+1}.pth")
torch.save(agent.actor.state_dict(), path)
print(f" [*] 模型已保存: {path}")
env.close()
logger.plot_learning_curve(save_dir=ROOT)
print("\n[SAC] 训练完成!\n")
return logger.episode_rewards
# ===========================================================================
# PPO 训练循环 (on-policy)
# ===========================================================================
def train_ppo(env_id: str, seed: int) -> list:
"""
训练 PPO 并返回每个 episode 的总奖励列表。
PPO 是 on-policy 的:先收集固定 T 步数据(steps_per_update),
然后用这批数据做 K 轮 epoch 更新,再继续收集。
"""
config = load_config(PPO_CFG_PATH)
env, state_dim, action_dim, max_action = make_env(env_id)
set_seed(seed, env)
print(f"\n{'='*60}")
print(f" Training PPO on {env_id}")
print(f" state_dim={state_dim}, action_dim={action_dim}, max_action={max_action}")
print(f"{'='*60}\n")
agent = PPO(state_dim, action_dim, max_action, config)
logger = Logger()
steps_per_update = config.get('steps_per_update', 2048)
max_episodes = config['max_episodes']
max_steps = config['max_steps']
total_steps = 0
buffer_steps = 0
episode = 0
episode_reward: float = 0.0
state, _ = env.reset()
while episode < max_episodes:
action, action_raw, log_prob, value = agent.select_action(state)
next_state, reward, terminated, truncated, _ = env.step(action)
done = float(terminated)
agent.rollout.add(state, action_raw, log_prob, reward, done, value)
state = next_state
episode_reward += float(reward)
total_steps += 1
buffer_steps += 1
# ---- episode 结束 ----
if terminated or truncated:
logger.record(episode_reward)
print(f"[PPO] Episode: {episode+1:03d} | Steps: {total_steps:06d} | Reward: {episode_reward:.2f}")
# 阶段性保存(避免重复保存)
if (episode + 1) % 50 == 0:
path = os.path.join(ROOT, f"ppo_actor_pendulum_ep{episode+1}.pth")
torch.save(agent.actor.state_dict(), path)
print(f" [*] 模型已保存: {path}")
episode += 1
episode_reward = 0.0
state, _ = env.reset()
if episode >= max_episodes:
break
# ---- 收集够 T 步 → 触发 PPO 更新 ----
if buffer_steps >= steps_per_update:
last_value = 0.0 if done else agent.get_value(state)
agent.rollout.compute_returns_and_advantages(last_value)
info = agent.update()
buffer_steps = 0
print(f" [PPO update] actor_loss={info['actor_loss']:.4f} | "
f"value_loss={info['value_loss']:.4f} | entropy={info['entropy']:.4f}")
env.close()
# 若缓冲区中还有剩余数据,做最后一次更新
if buffer_steps > 0:
agent.rollout.compute_returns_and_advantages(0.0)
agent.update()
logger.plot_learning_curve(save_dir=ROOT)
print("\n[PPO] 训练完成!\n")
return logger.episode_rewards
# ===========================================================================
# 对比模式:依次训练两者,输出对比曲线
# ===========================================================================
def train_compare(env_id: str, seed: int):
print("\n" + "="*60)
print(" Compare Mode: SAC vs PPO")
print("="*60)
rewards_sac = train_sac(env_id, seed)
rewards_ppo = train_ppo(env_id, seed)
Logger.plot_comparison(
rewards_dict={'SAC': rewards_sac, 'PPO': rewards_ppo},
window=10,
save_dir=ROOT,
)
print("\n[Compare] 对比训练完成!已生成 3 张图:")
print(" - comparison_curve.png (同 episode 范围对比)")
print(" - sac_learning_curve.png (SAC 独立完整曲线)")
print(" - ppo_learning_curve.png (PPO 独立完整曲线)")
# ===========================================================================
# 命令行入口
# ===========================================================================
def parse_args():
parser = argparse.ArgumentParser(
description="SAC / PPO 强化学习训练脚本 (Pendulum-v1)"
)
parser.add_argument(
"--algo", type=str, default="sac",
choices=["sac", "ppo", "compare"],
help="选择训练的算法: sac | ppo | compare (默认: sac)"
)
parser.add_argument(
"--env", type=str, default="Pendulum-v1",
help="Gymnasium 环境 ID (默认: Pendulum-v1)"
)
parser.add_argument(
"--seed", type=int, default=0,
help="随机种子 (默认: 0)"
)
return parser.parse_args()
def main():
args = parse_args()
if args.algo == "sac":
train_sac(args.env, args.seed)
elif args.algo == "ppo":
train_ppo(args.env, args.seed)
elif args.algo == "compare":
train_compare(args.env, args.seed)
if __name__ == "__main__":
main()