iT邦幫忙

0

PPO 訓練Flappy Bird:為什麼高維Lidar(180+ 維)反而比12 維狀態表現好?

  • 分享至 

  • xImage

我在使用Stable-Baselines3 的PPO訓練flappy_bird_gymnasium (FlappyBird-v0),遇到一個狀態設計上的困惑。

一、實驗背景

同一個遊戲環境,我測試了兩種observation 設計:

(A)高維Lidar 類型(180~2000 維)

每個方向一條ray,回傳與障礙物距離

有用Gym 內置lidar,也有用OpenCV 擷取畫面後自行模擬

Observation 維度高,但不做CNN,仍是MLP

結果:

訓練很快收斂

分數可達數百甚至上千

但行為偏投機,泛化能力差

(B)低維手作狀態(12 維)

包含:

bird y、bird vertical velocity

前兩個pipe 的 x

gap 中心、gap 高度

bird 與gap 的相對距離

全部都有正規化

理論上是「乾淨、接近Markov state」的設計。

二、問題現象

使用12 維狀態時:

PPO 訓練穩定、不發散

但分數幾乎都卡在80~120 左右

調過:

gamma(到0.999)

n_steps(到2048)

reward shaping

網絡層數/ 神經元數
→ 改善有限

使用Lidar 高維時:

分數明顯較高

但策略依賴視覺幾何,實戰易崩

三、想請教的問題

為什麼PPO 在高維觀測下反而比較「好學」?
即使理論上12 維已包含完整資訊。

這是否其實是:

隱性POMDP?

時間尺度資訊不足(例如「多久會到下一根水管」)?

高維Lidar 是否「隱式編碼了時間與未來碰撞資訊」,
讓PPO 比較容易學?

若想讓低維狀態表現接近高維:

是否需要加入:

time-to-collision

pipe speed

或history / frame stacking?

四、目標

不是單純刷高分,而是想理解:

狀態維度vs 可學性(learnability)

PPO 在連續時間控制問題中,對observation 設計的實際限制

希望有做過RL / PPO / 遊戲環境的前輩能分享經驗,感謝🙏

import gymnasium as gym
import flappy_bird_gymnasium
import numpy as np
import torch
import random
import os
from gymnasium import spaces
from stable_baselines3 import PPO
from stable_baselines3.common.env_util import make_vec_env
from stable_baselines3.common.callbacks import EvalCallback, CheckpointCallback

# =========================================================
# 1. 全域隨機種子設定 (確保實驗可重複性)
# =========================================================
def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    # 讓 cuDNN 運算結果確定化 (會稍微犧牲一點點速度,但對比最準)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

SEED = 42
set_seed(SEED)

# =========================================================
# 2. 環境 Wrapper (修正 Reset 漏洞與獎勵縮放)
# =========================================================
class CV12FlappyEnv(gym.Wrapper):
    def __init__(self, env):
        super().__init__(env)
        self.observation_space = spaces.Box(
            low=-2.0, high=2.0, shape=(12,), dtype=np.float32
        )
        self.last_score = 0

    def reset(self, seed=None, options=None):
        # 核心修正:必須將 seed 傳遞給底層環境
        obs, info = self.env.reset(seed=seed, options=options)
        self.last_score = 0
        return self._build_obs(obs), info

    def step(self, action):
        obs, _, terminated, truncated, info = self.env.step(action)

        # --- 獎勵設計區 (建議將數值縮小 10 倍以提升穩定性) ---
        reward = 0.01  # 生存獎勵 (原 0.05 -> 0.01)
        
        # 死亡懲罰
        if terminated or truncated:
            reward = -2.0  # 原 -20.0 -> -2.0

        # 過關獎勵
        score = info.get("score", 0)
        if score > self.last_score:
            reward += 1.5  # 原 15.0 -> 1.5
            self.last_score = score

        # 引導獎勵:鼓勵鳥靠近水管缺口中心 (可選,有助於突破 50 步)
        # bird_y = obs[9]
        # gap1_cy = (obs[4] + obs[5]) / 2.0
        # reward -= 0.001 * abs(bird_y - gap1_cy) 

        return self._build_obs(obs), reward, terminated, truncated, info

    def _build_obs(self, obs):
        # 官方索引對齊
        p1_x, p1_y_t, p1_y_b = obs[3], obs[4], obs[5]
        p2_x, p2_y_t, p2_y_b = obs[6], obs[7], obs[8]
        bird_y, bird_v, bird_r = obs[9], obs[10], obs[11]

        gap1_cy = (p1_y_t + p1_y_b) / 2.0
        gap2_cy = (p2_y_t + p2_y_b) / 2.0
        
        return np.array([
            bird_y / 512.0,
            bird_v / 10.0,
            p1_x / 288.0,
            gap1_cy / 512.0,
            (p1_y_b - p1_y_t) / 512.0,
            p2_x / 288.0,
            gap2_cy / 512.0,
            (p2_y_b - p2_y_t) / 512.0,
            (bird_y - gap1_cy) / 512.0,
            (bird_y - gap2_cy) / 512.0,
            bird_r / 90.0,
            (p1_x - p2_x) / 288.0
        ], dtype=np.float32)

# =========================================================
# 3. 訓練主程式
# =========================================================
if __name__ == "__main__":
    # 建立目錄
    os.makedirs("./models/best/", exist_ok=True)
    os.makedirs("./tb_logs/", exist_ok=True)

    env_kwargs = {"render_mode": None, "use_lidar": False}
    policy_kwargs = dict(net_arch=[128, 128, 128]) 

    # 訓練環境 (加入 Seed)
    train_env = make_vec_env(
        lambda: CV12FlappyEnv(gym.make("FlappyBird-v0", **env_kwargs)),
        n_envs=4,
        seed=SEED
    )

    # 評估環境
    eval_env = CV12FlappyEnv(gym.make("FlappyBird-v0", **env_kwargs))
    eval_env.reset(seed=SEED)

    # PPO 模型設定
    model = PPO(
        "MlpPolicy",
        train_env,
        policy_kwargs=policy_kwargs,
        learning_rate=2e-3,   # 建議降至 3e-4,2e-3 在 RL 中極容易跑飛
        n_steps=2048,
        batch_size=1024,
        gamma=0.99,
        ent_coef=0.01,        # 稍微提高探索,防止太快變成 PPO_19 的死腦筋
        clip_range=0.3,       # 標準 PPO 常用 0.2
        verbose=1,
        seed=SEED,            # 核心:固定模型種子
        tensorboard_log="./tb_logs/"
    )

    # Callbacks
    eval_callback = EvalCallback(
        eval_env, 
        best_model_save_path="./models/best/", 
        log_path="./tb_logs/",
        eval_freq=10000, 
        deterministic=True, 
        render=False
    )
    checkpoint_callback = CheckpointCallback(save_freq=50000, save_path="./models/")

    print(f"🚀 實驗啟動 (Seed: {SEED})")
    model.learn(
        total_timesteps=500_000, 
        callback=[eval_callback, checkpoint_callback],
        tb_log_name="PPO"
    )
    
    model.save("ppo_flappy_final_v1")
    print("✅ 訓練完成!")
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友回答

立即登入回答