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.
- {simonbb-0.0.5 → simonbb-0.0.6}/PKG-INFO +2 -1
- {simonbb-0.0.5 → simonbb-0.0.6}/pyproject.toml +2 -1
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/actions.py +1 -4
- simonbb-0.0.6/src/simonbb/cis433/dqn.py +312 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/LICENSE +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/README.md +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/__init__.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/__init__.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/agent.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/bert.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/experience.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/gpt.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/ignite.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/transformer.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/utils.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis433/wrappers.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis438/__init__.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/cis438/agentic.py +0 -0
- {simonbb-0.0.5 → simonbb-0.0.6}/src/simonbb/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simonbb
|
|
3
|
-
Version: 0.0.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|