simonbb 0.0.5__tar.gz → 0.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simonbb
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: A small package to facilitate teaching AI courses at business schools
5
5
  Project-URL: Homepage, https://simonbb.com
6
6
  Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
@@ -20,6 +20,7 @@ Requires-Dist: matplotlib~=3.10.8
20
20
  Requires-Dist: numpy~=2.4.0
21
21
  Requires-Dist: pytorch-ignite~=0.5.4
22
22
  Requires-Dist: pyyaml~=6.0.3
23
+ Requires-Dist: ray~=2.56.0
23
24
  Requires-Dist: sentence-transformers~=5.3.0
24
25
  Requires-Dist: stable-baselines3~=2.9.0
25
26
  Requires-Dist: torchvision~=0.24.1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simonbb"
7
- version = "0.0.5"
7
+ version = "0.0.6"
8
8
  authors = [
9
9
  { name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
10
10
  ]
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "gymnasium~=1.3.0",
34
34
  "pytorch-ignite~=0.5.4",
35
35
  "stable_baselines3~=2.9.0",
36
+ "ray~=2.56.0",
36
37
  "pyyaml~=6.0.3"
37
38
  ]
38
39
  [project.urls]
@@ -61,10 +61,7 @@ class EpsilonTracker:
61
61
  """
62
62
  Updates epsilon according to linear schedule
63
63
  """
64
- def __init__(self, selector: EpsilonGreedyActionSelector,
65
- eps_start: tt.Union[int, float],
66
- eps_final: tt.Union[int, float],
67
- eps_frames: int):
64
+ def __init__(self, selector: EpsilonGreedyActionSelector, eps_start: tt.Union[int, float], eps_final: tt.Union[int, float], eps_frames: int):
68
65
  self.selector = selector
69
66
  self.eps_start = eps_start
70
67
  self.eps_final = eps_final
@@ -0,0 +1,312 @@
1
+ import random
2
+ import argparse
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import dataclasses
7
+ from datetime import timedelta
8
+ import typing as tt
9
+ import os
10
+ import glob
11
+
12
+ from ignite.engine import Engine
13
+ from ignite.metrics import RunningAverage
14
+ from ignite.contrib.handlers import tensorboard_logger as tb_logger
15
+ from ray import tune
16
+
17
+ from .experience import ExperienceFirstLast, ExperienceSourceFirstLast, ExperienceReplayBuffer
18
+ from .ignite import EndOfEpisodeHandler, EpisodeEvents, EpisodeFPSHandler, PeriodicEvents, PeriodEvents
19
+
20
+ #%%
21
+
22
+ SEED = 123
23
+
24
+ @dataclasses.dataclass
25
+ class Hyperparams:
26
+ env_name: str
27
+ stop_reward: float
28
+ run_name: str
29
+ replay_size: int
30
+ replay_initial: int
31
+ target_net_sync: int
32
+ epsilon_frames: int
33
+
34
+ learning_rate: float = 0.0001
35
+ batch_size: int = 32
36
+ gamma: float = 0.99
37
+ epsilon_start: float = 1.0
38
+ epsilon_final: float = 0.1
39
+
40
+ tuner_mode: bool = False
41
+ episodes_to_solve: int = 500
42
+
43
+
44
+ GAME_PARAMS = {
45
+ 'pong': Hyperparams(
46
+ env_name="PongNoFrameskip-v4",
47
+ stop_reward=18.0,
48
+ run_name="pong",
49
+ replay_size=100_000,
50
+ replay_initial=10_000,
51
+ target_net_sync=1000,
52
+ epsilon_frames=100_000,
53
+ epsilon_final=0.02,
54
+ ),
55
+ 'breakout-small': Hyperparams(
56
+ env_name="BreakoutNoFrameskip-v4",
57
+ stop_reward=500.0,
58
+ run_name="breakout-small",
59
+ replay_size=300_000,
60
+ replay_initial=20_000,
61
+ target_net_sync=1000,
62
+ epsilon_frames=1_000_000,
63
+ batch_size=64,
64
+ ),
65
+ 'breakout': Hyperparams(
66
+ env_name="BreakoutNoFrameskip-v4",
67
+ stop_reward=500.0,
68
+ run_name='breakout',
69
+ replay_size=1_000_000,
70
+ replay_initial=50_000,
71
+ target_net_sync=10_000,
72
+ epsilon_frames=10_000_000,
73
+ learning_rate=0.00025,
74
+ ),
75
+ 'invaders': Hyperparams(
76
+ env_name="SpaceInvadersNoFrameskip-v4",
77
+ stop_reward=500.0,
78
+ run_name='invaders',
79
+ replay_size=10_000_000,
80
+ replay_initial=50_000,
81
+ target_net_sync=10_000,
82
+ epsilon_frames=10_000_000,
83
+ learning_rate=0.00025,
84
+ ),
85
+ }
86
+
87
+
88
+ def unpack_batch( batch: tt.List[ExperienceFirstLast] ):
89
+ """
90
+ Convert a batch of transitions to Numpy arrays suitable for training
91
+ """
92
+ states, actions, rewards, dones, last_states = [],[],[],[],[]
93
+ for exp in batch:
94
+ states.append(exp.state)
95
+ actions.append(exp.action)
96
+ rewards.append(exp.reward)
97
+ dones.append(exp.last_state is None)
98
+ if exp.last_state is None: # end of episode
99
+ lstate = exp.state # the result will be masked anyway, but this avoid special handling of None
100
+ else:
101
+ lstate = exp.last_state
102
+ last_states.append(lstate)
103
+ return np.asarray(states), np.array(actions), np.array(rewards, dtype=np.float32), np.array(dones, dtype=bool), np.asarray(last_states)
104
+
105
+
106
+ def calc_loss_dqn( batch: tt.List[ExperienceFirstLast], net: nn.Module, tgt_net: nn.Module, gamma: float, device: torch.device) -> torch.Tensor:
107
+ states, actions, rewards, dones, next_states = unpack_batch(batch)
108
+ states_v = torch.as_tensor(states).to(device)
109
+ next_states_v = torch.as_tensor(next_states).to(device)
110
+ actions_v = torch.LongTensor(actions).to(device)
111
+ rewards_v = torch.FloatTensor(rewards).to(device)
112
+ done_mask = torch.BoolTensor(dones).to(device)
113
+
114
+ actions_v = actions_v.unsqueeze(-1)
115
+ state_action_vals = net(states_v).gather(1, actions_v).squeeze(-1)
116
+ with torch.no_grad():
117
+ next_state_vals = tgt_net(next_states_v).max(1)[0]
118
+ next_state_vals[done_mask] = 0.0
119
+
120
+ bellman_vals = next_state_vals.detach() * gamma + rewards_v
121
+ return nn.MSELoss()(state_action_vals, bellman_vals)
122
+
123
+
124
+ def calc_loss_double_dqn( batch: tt.List[ExperienceFirstLast], net: nn.Module, tgt_net: nn.Module, gamma: float, device: torch.device):
125
+ states, actions, rewards, dones, next_states = unpack_batch(batch)
126
+ states_v = torch.as_tensor(states).to(device)
127
+ actions_v = torch.tensor(actions).to(device)
128
+ rewards_v = torch.tensor(rewards).to(device)
129
+ done_mask = torch.BoolTensor(dones).to(device)
130
+
131
+ actions_v = actions_v.unsqueeze(-1)
132
+ state_action_vals = net(states_v).gather(1, actions_v).squeeze(-1)
133
+ with torch.no_grad():
134
+ next_states_v = torch.as_tensor(next_states).to(device)
135
+ next_state_acts = net(next_states_v).max(1)[1] # use net() to get the best action for the next state
136
+ next_state_acts = next_state_acts.unsqueeze(-1)
137
+ next_state_vals = tgt_net(next_states_v).gather(1, next_state_acts).squeeze(-1) # use tgt_net to get the best action value corresponding to the selected action
138
+ next_state_vals[done_mask] = 0.0
139
+ exp_sa_vals = next_state_vals.detach() * gamma + rewards_v
140
+ return nn.MSELoss()(state_action_vals, exp_sa_vals)
141
+
142
+
143
+ def batch_generator(buffer: ExperienceReplayBuffer, initial: int, batch_size: int) -> tt.Generator[tt.List[ExperienceFirstLast], None, None]:
144
+ buffer.populate(initial) # get initial fresh samples
145
+ while True:
146
+ buffer.populate(1)
147
+ yield buffer.sample(batch_size)
148
+
149
+
150
+ @torch.no_grad()
151
+ def calc_values_of_states(states: np.ndarray, net: nn.Module, device: torch.device):
152
+ mean_vals = []
153
+ for batch in np.array_split(states, 64):
154
+ states_v = torch.tensor(batch).to(device)
155
+ action_values_v = net(states_v)
156
+ best_action_values_v = action_values_v.max(1)[0]
157
+ mean_vals.append(best_action_values_v.mean().item())
158
+ return np.mean(mean_vals)
159
+
160
+
161
+ def setup_ignite(
162
+ engine: Engine, params: Hyperparams, exp_source: ExperienceSourceFirstLast,
163
+ run_name: str, log_dir: str, extra_metrics: tt.Iterable[str] = (),
164
+ tuner_reward_episode: int = 100, tuner_reward_min: float = -19,
165
+ ):
166
+ handler = EndOfEpisodeHandler( exp_source, bound_avg_reward=params.stop_reward )
167
+ handler.attach(engine)
168
+ EpisodeFPSHandler().attach(engine)
169
+
170
+ @engine.on(EpisodeEvents.EPISODE_COMPLETED)
171
+ def episode_completed(trainer: Engine):
172
+ passed = trainer.state.metrics.get('time_passed', 0)
173
+ print("Episode %d: reward=%.0f, steps=%s, speed=%.1f f/s, elapsed=%s" % (
174
+ trainer.state.episode, trainer.state.episode_reward,
175
+ trainer.state.episode_steps, trainer.state.metrics.get('avg_fps', 0),
176
+ timedelta(seconds=int(passed))))
177
+
178
+ @engine.on(EpisodeEvents.BOUND_REWARD_REACHED)
179
+ def game_solved(trainer: Engine):
180
+ passed = trainer.state.metrics['time_passed']
181
+ print("Game solved in %s, after %d episodes and %d iterations!" % (
182
+ timedelta(seconds=int(passed)), trainer.state.episode,
183
+ trainer.state.iteration))
184
+ trainer.should_terminate = True
185
+ trainer.state.solved = True
186
+
187
+ tb = tb_logger.TensorboardLogger(log_dir=log_dir)
188
+ run_avg = RunningAverage(output_transform=lambda v: v['loss'])
189
+ run_avg.attach(engine, "avg_loss")
190
+
191
+ metrics = ['reward', 'steps', 'avg_reward']
192
+ handler = tb_logger.OutputHandler(tag="episodes", metric_names=metrics)
193
+ event = EpisodeEvents.EPISODE_COMPLETED
194
+ tb.attach(engine, log_handler=handler, event_name=event)
195
+
196
+ # write to tensorboard every 100 iterations
197
+ PeriodicEvents().attach(engine)
198
+ metrics = ['avg_loss', 'avg_fps']
199
+ metrics.extend(extra_metrics)
200
+ handler = tb_logger.OutputHandler(tag="train", metric_names=metrics, output_transform=lambda a: a)
201
+ event = PeriodEvents.ITERS_100_COMPLETED
202
+ tb.attach(engine, log_handler=handler, event_name=event)
203
+
204
+ if params.tuner_mode:
205
+ @engine.on(EpisodeEvents.EPISODE_COMPLETED)
206
+ def episode_completed(trainer: Engine):
207
+ avg_reward = trainer.state.metrics.get('avg_reward')
208
+ max_episodes = params.episodes_to_solve * 1.1
209
+ if trainer.state.episode > tuner_reward_episode and \
210
+ avg_reward < tuner_reward_min:
211
+ trainer.should_terminate = True
212
+ trainer.state.solved = False
213
+ elif trainer.state.episode > max_episodes:
214
+ trainer.should_terminate = True
215
+ trainer.state.solved = False
216
+ if trainer.should_terminate:
217
+ print(f"Episode {trainer.state.episode}, "
218
+ f"avg_reward {avg_reward:.2f}, terminating")
219
+
220
+
221
+ # hyperparams tuner
222
+ TrainFunc = tt.Callable[ [Hyperparams, torch.device, dict], tt.Optional[int] ]
223
+
224
+ BASE_SPACE = {
225
+ "learning_rate": tune.loguniform(1e-5, 1e-4),
226
+ "gamma": tune.choice([0.9, 0.92, 0.95, 0.98, 0.99, 0.995]),
227
+ }
228
+
229
+ def tune_params(
230
+ base_params: Hyperparams, train_func: TrainFunc, device: torch.device,
231
+ samples: int = 10, extra_space: tt.Optional[tt.Dict[str, tt.Any]] = None,
232
+ ):
233
+ """
234
+ Perform hyperparameters tune.
235
+ :param train_func: Train function, has to return "episodes" key with metric
236
+ :param device: torch device
237
+ :param samples: count of samples to perform
238
+ :param extra_space: additional search space
239
+ """
240
+ search_space = dict(BASE_SPACE)
241
+ if extra_space is not None:
242
+ search_space.update(extra_space)
243
+ config = tune.TuneConfig(num_samples=samples)
244
+
245
+ def objective(config: dict, device: torch.device) -> dict:
246
+ keys = dataclasses.asdict(base_params).keys()
247
+ upd = {"tuner_mode": True}
248
+ for k, v in config.items():
249
+ if k in keys:
250
+ upd[k] = v
251
+ params = dataclasses.replace(base_params, **upd)
252
+ res = train_func(params, device, config)
253
+ return {"episodes": res if res is not None else 10**6}
254
+
255
+ obj = tune.with_parameters(objective, device=device)
256
+ if device.type == "cuda":
257
+ obj = tune.with_resources(obj, {"gpu": 1})
258
+ tuner = tune.Tuner(obj, param_space=search_space, tune_config=config)
259
+ results = tuner.fit()
260
+ best = results.get_best_result(metric="episodes", mode="min")
261
+ print(best.config)
262
+ print(best.metrics)
263
+
264
+
265
+ def train_or_tune(
266
+ args: argparse.Namespace,
267
+ train_func: TrainFunc,
268
+ device: torch.device,
269
+ best_params: Hyperparams,
270
+ extra_params: tt.Optional[dict] = None,
271
+ extra_space: tt.Optional[dict] = None,
272
+ ):
273
+ random.seed(SEED)
274
+ torch.manual_seed(SEED)
275
+ if args.params == "common":
276
+ params = GAME_PARAMS['pong']
277
+ else:
278
+ params = best_params
279
+
280
+ if extra_params is None:
281
+ extra_params = {}
282
+ if args.tune is None:
283
+ train_func(params, device, extra_params)
284
+ else:
285
+ tune_params(params, train_func, device, samples=args.tune, extra_space=extra_space)
286
+
287
+
288
+ def save_model(model, path, global_step, start_iteration):
289
+ torch.save({
290
+ 'model_state_dict': model.state_dict(),
291
+ 'global_step': global_step,
292
+ 'start_iteration': start_iteration
293
+ }, path)
294
+
295
+
296
+ def load_model(model, path):
297
+ if not os.path.exists(path):
298
+ raise FileNotFoundError(f"No checkpoint found at {path}")
299
+ checkpoint = torch.load(path) # Load the checkpoint
300
+ if 'model_state_dict' in checkpoint:
301
+ model.load_state_dict(checkpoint['model_state_dict'])
302
+ else:
303
+ raise KeyError(f"'model_state_dict' not found in the checkpoint at {path}")
304
+ model.eval() # Set the model to evaluation mode
305
+ return checkpoint # Return the checkpoint to access global_step
306
+
307
+ def get_latest_checkpoint(dir_path):
308
+ checkpoint_files = glob.glob(os.path.join(dir_path, '*.pth'))
309
+ if not checkpoint_files:
310
+ return None
311
+ latest_checkpoint = max(checkpoint_files, key=os.path.getmtime)
312
+ return latest_checkpoint
File without changes
File without changes
File without changes
File without changes