添加 PPO 算法实现及相关配置,更新训练入口以支持 SAC 和 PPO 模式

This commit is contained in:
2026-04-04 17:50:02 +08:00
parent f3d2d1a85f
commit a2ce5073c5
10 changed files with 675 additions and 73 deletions
+236 -54
View File
@@ -1,86 +1,268 @@
import gymnasium as gym
"""
训练入口 — 支持 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
import yaml
import os
from utils.logger import Logger
# 读取 YAML 配置文件
config_path = os.path.join(os.path.dirname(__file__), 'configs', 'pendulum_config.yaml')
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
def main():
# ---------------------------------------------------------
# 2. 实例化环境与获取维度信息
# ---------------------------------------------------------
env = gym.make('Pendulum-v1')
# 动态获取环境的维度信息,确保算法通用性
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
max_action = float(env.action_space.high[0])
print(f"环境已加载: 状态维度 {state_dim}, 动作维度 {action_dim}, 最大动作界限 {max_action}")
# ===========================================================================
# 配置加载
# ===========================================================================
# ---------------------------------------------------------
# 3. 实例化 SAC 代理与经验回放池
# ---------------------------------------------------------
agent = SAC(state_dim, action_dim, max_action, config)
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", "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 # 记录与环境交互的总步数
total_steps = 0
# ---------------------------------------------------------
# 4. 训练大循环
# ---------------------------------------------------------
for episode in range(config['max_episodes']):
# 重置环境,获取初始状态 (Gymnasium 返回 state 和 info)
state, _ = env.reset()
episode_reward = 0
for step in range(config['max_steps']):
# --- a. 动作选择策略 ---
# 强化学习工程技巧:在训练初期使用纯随机动作,收集高多样性的初始数据
episode_reward: float = 0.0
for _ in range(config['max_steps']):
# 动作选择:预热期使用纯随机,之后用策略
if total_steps < config['start_steps']:
action = env.action_space.sample()
else:
# 预热结束后,交由 SAC 的策略网络进行带噪声的采样
action = agent.select_action(state, evaluate=False)
# --- b. 与环境交互 ---
next_state, reward, terminated, truncated, _ = env.step(action)
# 判断回合是否真正结束 (超时截断 truncated 不算做环境动力学意义上的 done)
done = float(terminated)
# --- c. 存入经验池 ---
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
episode_reward += reward
episode_reward += float(reward)
total_steps += 1
# --- d. 核心学习逻辑 ---
# 只有当经验池里的数据量足够凑齐一个 Batch 时,才开始更新网络
if replay_buffer.size > config['batch_size']:
agent.update(replay_buffer, config['batch_size'])
# 如果提前倒地或撞毁,结束当前回合
if terminated or truncated:
break
# 打印当前回合的训练结果
print(f"Episode: {episode+1:03d} | Total Steps: {total_steps:06d} | Reward: {episode_reward:.2f}")
# --- 阶段性保存模型 (可选) ---
logger.record(episode_reward)
print(f"[SAC] Episode: {episode+1:03d} | Steps: {total_steps:06d} | Reward: {episode_reward:.2f}")
if (episode + 1) % 50 == 0:
torch.save(agent.actor.state_dict(), f"sac_actor_pendulum_ep{episode+1}.pth")
print(f"[*] 已保存第 {episode+1} 回合的模型权重。")
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()
print("训练结束!")
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()