Skip to content

Commit 25f0ba4

Browse files
authored
Add run.py CLI runner for BipedalWalker PPO and update env_utils/README (OpenHUTB#7368)
1 parent 6204aab commit 25f0ba4

3 files changed

Lines changed: 200 additions & 2 deletions

File tree

src/bipedal_walker_rl/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,36 @@ The `make_env()` function prepares the environment for training and evaluation w
7272

7373
- **Clip Observations**: You can clip observations to avoid outliers during training by setting `clip_obs` to a certain value (default: 10.0).
7474

75+
- **CLI Runner**: Use `run.py` to train or evaluate models from the command line with support for normal/hardcore mode, video recording, and custom timesteps.
76+
7577
### Example Usage:
7678

7779
```python
7880
env = make_env(env_name="BipedalWalker-v3", hardcore=True, record_video=True, use_monitor=True)
7981
```
8082

83+
### Command Line Usage
84+
85+
Train a normal model:
86+
```bash
87+
python run.py --task train --mode normal --timesteps 100000 --model-name ppo_bipedalwalker
88+
```
89+
90+
Train a hardcore model with video recording:
91+
```bash
92+
python run.py --task train --mode hardcore --timesteps 200000 --model-name ppo_bipedalwalker_hardcore --record-video
93+
```
94+
95+
Evaluate a saved model:
96+
```bash
97+
python run.py --task eval --mode normal --model-path models/ppo_bipedalwalker.zip --eval-episodes 5
98+
```
99+
100+
Evaluate and record video:
101+
```bash
102+
python run.py --task eval --mode normal --model-path models/ppo_bipedalwalker.zip --eval-episodes 3 --record-video
103+
```
104+
81105
### 3.2 observe_model()
82106

83107
The observe_model() function loads a trained PPO model and evaluates it in the specified environment. It automatically checks if VecNormalize and VecFrameStack were used during training and applies them accordingly.

src/bipedal_walker_rl/env_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from stable_baselines3 import PPO
77
from stable_baselines3.common.evaluation import evaluate_policy
88

9-
def make_env(env_name="BipedalWalker-v3", hardcore=None, n_stack=4, clip_obs=10.0, render_mode=None, record_video=False, video_folder='videos', use_monitor=False, logs_dir='logs'):
9+
def make_env(env_name="BipedalWalker-v3", hardcore=None, n_stack=4, clip_obs=10.0, render_mode=None, record_video=False, video_folder='videos', use_monitor=False, logs_dir='logs', norm_obs=True, norm_reward=True):
1010
"""
1111
Create and wrap the environment for BipedalWalker with optional hardcore mode,
1212
vectorized operations, normalization, frame stacking, rendering options, video recording, and monitoring.
@@ -20,6 +20,8 @@ def make_env(env_name="BipedalWalker-v3", hardcore=None, n_stack=4, clip_obs=10.
2020
- record_video (bool): Whether to record video during the environment execution. Default is False.
2121
- video_folder (str): Directory where video recordings will be saved. Default is 'videos'.
2222
- use_monitor (bool): Whether to wrap the environment with Monitor for logging. Default is False.
23+
- norm_obs (bool): Whether to normalize observations. Default is True.
24+
- norm_reward (bool): Whether to normalize rewards. Default is True.
2325
- logs_dir (str): Directory where monitor logs will be saved. Default is 'logs'.
2426
2527
Returns:
@@ -57,7 +59,7 @@ def make_env(env_name="BipedalWalker-v3", hardcore=None, n_stack=4, clip_obs=10.
5759
env = DummyVecEnv([lambda: env])
5860

5961
# Normalize observations and rewards in the environment
60-
env = VecNormalize(env, norm_obs=True, norm_reward=True, clip_obs=clip_obs)
62+
env = VecNormalize(env, norm_obs=norm_obs, norm_reward=norm_reward, clip_obs=clip_obs)
6163

6264
# Stack the last n_stack observations
6365
env = VecFrameStack(env, n_stack=n_stack)

src/bipedal_walker_rl/run.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import argparse
2+
import os
3+
from stable_baselines3 import PPO
4+
from stable_baselines3.common.evaluation import evaluate_policy
5+
from env_utils import make_env
6+
7+
MODEL_DIR = "models"
8+
LOGS_DIR = "logs"
9+
VIDEO_DIR = "videos"
10+
11+
12+
def parse_args():
13+
parser = argparse.ArgumentParser(description="Bipedal Walker PPO runner")
14+
15+
parser.add_argument(
16+
"--task",
17+
choices=["train", "eval"],
18+
required=True,
19+
help="Task to run: train a PPO model or evaluate a saved model",
20+
)
21+
parser.add_argument(
22+
"--mode",
23+
choices=["normal", "hardcore"],
24+
default="normal",
25+
help="Environment mode",
26+
)
27+
parser.add_argument(
28+
"--timesteps",
29+
type=int,
30+
default=100000,
31+
help="Total training timesteps",
32+
)
33+
parser.add_argument(
34+
"--model-name",
35+
default="ppo_bipedalwalker",
36+
help="Model save name for training (without extension)",
37+
)
38+
parser.add_argument(
39+
"--model-path",
40+
default=None,
41+
help="Path to a saved model for evaluation",
42+
)
43+
parser.add_argument(
44+
"--eval-episodes",
45+
type=int,
46+
default=5,
47+
help="Number of episodes for evaluation",
48+
)
49+
parser.add_argument(
50+
"--record-video",
51+
action="store_true",
52+
help="Record a video during training or evaluation",
53+
)
54+
parser.add_argument(
55+
"--video-folder",
56+
default=VIDEO_DIR,
57+
help="Video folder for recording output",
58+
)
59+
parser.add_argument(
60+
"--learning-rate",
61+
type=float,
62+
default=3e-4,
63+
help="Learning rate for PPO training",
64+
)
65+
parser.add_argument(
66+
"--n-steps",
67+
type=int,
68+
default=2048,
69+
help="Number of steps to run for each environment update",
70+
)
71+
parser.add_argument(
72+
"--batch-size",
73+
type=int,
74+
default=64,
75+
help="Batch size for PPO",
76+
)
77+
parser.add_argument(
78+
"--gamma",
79+
type=float,
80+
default=0.99,
81+
help="Discount factor",
82+
)
83+
return parser.parse_args()
84+
85+
86+
def get_env_name(mode: str) -> str:
87+
return "BipedalWalkerHardcore-v3" if mode == "hardcore" else "BipedalWalker-v3"
88+
89+
90+
def train(args):
91+
os.makedirs(MODEL_DIR, exist_ok=True)
92+
os.makedirs(LOGS_DIR, exist_ok=True)
93+
os.makedirs(args.video_folder, exist_ok=True)
94+
95+
env = make_env(
96+
env_name=get_env_name(args.mode),
97+
hardcore=(args.mode == "hardcore"),
98+
render_mode="rgb_array" if args.record_video else None,
99+
record_video=args.record_video,
100+
video_folder=args.video_folder,
101+
use_monitor=True,
102+
logs_dir=LOGS_DIR,
103+
norm_obs=True,
104+
norm_reward=True,
105+
)
106+
107+
model = PPO(
108+
"MlpPolicy",
109+
env,
110+
verbose=1,
111+
learning_rate=args.learning_rate,
112+
n_steps=args.n_steps,
113+
batch_size=args.batch_size,
114+
gamma=args.gamma,
115+
)
116+
117+
print(f"Starting training: mode={args.mode}, timesteps={args.timesteps}")
118+
model.learn(total_timesteps=args.timesteps)
119+
120+
model_path = os.path.join(MODEL_DIR, f"{args.model_name}")
121+
model.save(model_path)
122+
print(f"Saved model to: {model_path}")
123+
124+
env.close()
125+
126+
if args.record_video:
127+
print(f"Recorded videos saved in: {args.video_folder}")
128+
129+
130+
def evaluate(args):
131+
if args.model_path is None:
132+
raise ValueError("--model-path is required for evaluation")
133+
134+
model = PPO.load(args.model_path)
135+
env = make_env(
136+
env_name=get_env_name(args.mode),
137+
hardcore=(args.mode == "hardcore"),
138+
render_mode="rgb_array" if args.record_video else "human",
139+
record_video=args.record_video,
140+
video_folder=args.video_folder,
141+
use_monitor=False,
142+
norm_obs=False,
143+
norm_reward=False,
144+
)
145+
146+
mean_reward, std_reward = evaluate_policy(
147+
model,
148+
env,
149+
n_eval_episodes=args.eval_episodes,
150+
return_episode_rewards=False,
151+
)
152+
153+
env.close()
154+
155+
print(f"Evaluation results: mean_reward={mean_reward:.2f}, std_reward={std_reward:.2f}")
156+
if args.record_video:
157+
print(f"Recorded evaluation videos saved in: {args.video_folder}")
158+
159+
160+
def main():
161+
args = parse_args()
162+
163+
if args.task == "train":
164+
train(args)
165+
elif args.task == "eval":
166+
evaluate(args)
167+
else:
168+
raise ValueError(f"Unsupported task: {args.task}")
169+
170+
171+
if __name__ == "__main__":
172+
main()

0 commit comments

Comments
 (0)