simonbb 0.0.4__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.4 → simonbb-0.0.6}/PKG-INFO +4 -2
- {simonbb-0.0.4 → simonbb-0.0.6}/pyproject.toml +4 -2
- {simonbb-0.0.4 → 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.6/src/simonbb/cis433/wrappers.py +100 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/utils.py +112 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/LICENSE +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/README.md +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/__init__.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/__init__.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/agent.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/bert.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/experience.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/gpt.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/ignite.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/transformer.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis433/utils.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis438/__init__.py +0 -0
- {simonbb-0.0.4 → simonbb-0.0.6}/src/simonbb/cis438/agentic.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simonbb
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: A small package to facilitate teaching AI courses
|
|
3
|
+
Version: 0.0.6
|
|
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>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -20,7 +20,9 @@ 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
|
|
25
|
+
Requires-Dist: stable-baselines3~=2.9.0
|
|
24
26
|
Requires-Dist: torchvision~=0.24.1
|
|
25
27
|
Requires-Dist: torch~=2.9.1
|
|
26
28
|
Requires-Dist: transformers~=4.57.3
|
|
@@ -4,11 +4,11 @@ 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
|
]
|
|
11
|
-
description = "A small package to facilitate teaching AI courses"
|
|
11
|
+
description = "A small package to facilitate teaching AI courses at business schools"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
classifiers = [
|
|
@@ -32,6 +32,8 @@ dependencies = [
|
|
|
32
32
|
"sentence-transformers~=5.3.0",
|
|
33
33
|
"gymnasium~=1.3.0",
|
|
34
34
|
"pytorch-ignite~=0.5.4",
|
|
35
|
+
"stable_baselines3~=2.9.0",
|
|
36
|
+
"ray~=2.56.0",
|
|
35
37
|
"pyyaml~=6.0.3"
|
|
36
38
|
]
|
|
37
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
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import typing as tt
|
|
2
|
+
import gymnasium as gym
|
|
3
|
+
from gymnasium import spaces
|
|
4
|
+
import collections
|
|
5
|
+
import numpy as np
|
|
6
|
+
from stable_baselines3.common import atari_wrappers
|
|
7
|
+
|
|
8
|
+
class ImageToPyTorch(gym.ObservationWrapper):
|
|
9
|
+
def __init__(self, env):
|
|
10
|
+
super(ImageToPyTorch, self).__init__(env)
|
|
11
|
+
obs = self.observation_space
|
|
12
|
+
assert isinstance(obs, gym.spaces.Box)
|
|
13
|
+
assert len(obs.shape) == 3
|
|
14
|
+
new_shape = (obs.shape[-1], obs.shape[0], obs.shape[1])
|
|
15
|
+
self.observation_space = gym.spaces.Box(
|
|
16
|
+
low=obs.low.min(), high=obs.high.max(),
|
|
17
|
+
shape=new_shape, dtype=obs.dtype)
|
|
18
|
+
|
|
19
|
+
def observation(self, observation):
|
|
20
|
+
return np.moveaxis(observation, 2, 0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FrameStack1D(gym.Wrapper):
|
|
24
|
+
"""
|
|
25
|
+
Stacks observations into 1D array
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, env, k):
|
|
28
|
+
assert isinstance(env, gym.Env)
|
|
29
|
+
assert isinstance(env.observation_space, gym.spaces.Box)
|
|
30
|
+
assert len(env.observation_space.shape) == 1
|
|
31
|
+
super(FrameStack1D, self).__init__(env)
|
|
32
|
+
self.k = k
|
|
33
|
+
self.frames = collections.deque(maxlen=k)
|
|
34
|
+
shp = env.observation_space.shape
|
|
35
|
+
self.observation_space = gym.spaces.Box(
|
|
36
|
+
low=np.min(env.observation_space.low),
|
|
37
|
+
high=np.max(env.observation_space.high),
|
|
38
|
+
shape=(env.observation_space.shape[0]*k,),
|
|
39
|
+
dtype=env.observation_space.dtype)
|
|
40
|
+
|
|
41
|
+
def reset(self):
|
|
42
|
+
ob, extra = self.env.reset()
|
|
43
|
+
for _ in range(self.k):
|
|
44
|
+
self.frames.append(ob)
|
|
45
|
+
return self._get_ob(), extra
|
|
46
|
+
|
|
47
|
+
def step(self, action):
|
|
48
|
+
ob, reward, done, trunc, info = self.env.step(action)
|
|
49
|
+
self.frames.append(ob)
|
|
50
|
+
return self._get_ob(), reward, done, trunc, info
|
|
51
|
+
|
|
52
|
+
def _get_ob(self):
|
|
53
|
+
assert len(self.frames) == self.k
|
|
54
|
+
return np.concatenate(self.frames)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BufferWrapper(gym.ObservationWrapper):
|
|
59
|
+
def __init__(self, env, n_steps):
|
|
60
|
+
super(BufferWrapper, self).__init__(env)
|
|
61
|
+
obs = env.observation_space
|
|
62
|
+
assert isinstance(obs, spaces.Box)
|
|
63
|
+
new_obs = gym.spaces.Box(
|
|
64
|
+
obs.low.repeat(n_steps, axis=0),
|
|
65
|
+
obs.high.repeat(n_steps, axis=0), dtype=obs.dtype)
|
|
66
|
+
self.observation_space = new_obs
|
|
67
|
+
self.buffer = collections.deque(maxlen=n_steps)
|
|
68
|
+
|
|
69
|
+
def reset(self, *, seed: tt.Optional[int] = None,
|
|
70
|
+
options: tt.Optional[dict[str, tt.Any]] = None):
|
|
71
|
+
for _ in range(self.buffer.maxlen-1):
|
|
72
|
+
self.buffer.append(self.env.observation_space.low)
|
|
73
|
+
obs, extra = self.env.reset()
|
|
74
|
+
return self.observation(obs), extra
|
|
75
|
+
|
|
76
|
+
def observation(self, observation: np.ndarray) -> np.ndarray:
|
|
77
|
+
self.buffer.append(observation)
|
|
78
|
+
return np.concatenate(self.buffer)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def wrap_dqn(env: gym.Env, stack_frames: int = 4,
|
|
82
|
+
episodic_life: bool = True, clip_reward: bool = True,
|
|
83
|
+
noop_max: int = 0) -> gym.Env:
|
|
84
|
+
"""
|
|
85
|
+
Apply a common set of wrappers for Atari games.
|
|
86
|
+
:param env: Environment to wrap
|
|
87
|
+
:param stack_frames: count of frames to stack, default=4
|
|
88
|
+
:param episodic_life: convert life to end of episode
|
|
89
|
+
:param clip_reward: reward clipping
|
|
90
|
+
:param noop_max: how many NOOP actions to execute
|
|
91
|
+
:return: wrapped environment
|
|
92
|
+
"""
|
|
93
|
+
assert 'NoFrameskip' in env.spec.id
|
|
94
|
+
env = atari_wrappers.AtariWrapper(
|
|
95
|
+
env, clip_reward=clip_reward, noop_max=noop_max,
|
|
96
|
+
terminal_on_life_loss=episodic_life)
|
|
97
|
+
env = ImageToPyTorch(env)
|
|
98
|
+
env = BufferWrapper(env, stack_frames)
|
|
99
|
+
return env
|
|
100
|
+
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
import time
|
|
1
2
|
import re
|
|
3
|
+
import collections
|
|
4
|
+
import sys
|
|
2
5
|
import pickle
|
|
3
6
|
import json
|
|
4
7
|
from collections import Counter
|
|
5
8
|
import urllib.request
|
|
6
9
|
import numpy as np
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
7
12
|
|
|
13
|
+
class WeightedMSELoss(nn.Module):
|
|
14
|
+
def __init__(self, size_average=True):
|
|
15
|
+
super(WeightedMSELoss, self).__init__()
|
|
16
|
+
self.size_average = size_average
|
|
8
17
|
|
|
18
|
+
def forward(self, input, target, weights=None):
|
|
19
|
+
if weights is None:
|
|
20
|
+
return nn.MSELoss(self.size_average)(input, target)
|
|
21
|
+
|
|
22
|
+
loss_rows = (input - target) ** 2
|
|
23
|
+
if len(loss_rows.size()) != 1:
|
|
24
|
+
loss_rows = torch.sum(loss_rows, dim=1)
|
|
25
|
+
res = (weights * loss_rows).sum()
|
|
26
|
+
if self.size_average:
|
|
27
|
+
res /= len(weights)
|
|
28
|
+
return res
|
|
29
|
+
|
|
9
30
|
class TextVectorizer:
|
|
10
31
|
def __init__(self, max_tokens=20000, output_mode="int", output_sequence_length=None, ngrams=None, standardize=None):
|
|
11
32
|
self.max_tokens = max_tokens
|
|
@@ -123,6 +144,97 @@ class TextVectorizer:
|
|
|
123
144
|
return vectorizer
|
|
124
145
|
|
|
125
146
|
|
|
147
|
+
|
|
148
|
+
class TBMeanTracker:
|
|
149
|
+
"""
|
|
150
|
+
TensorBoard value tracker: allows to batch fixed amount of historical values and write their mean into TB
|
|
151
|
+
|
|
152
|
+
Designed and tested with pytorch-tensorboard in mind
|
|
153
|
+
"""
|
|
154
|
+
def __init__(self, writer, batch_size):
|
|
155
|
+
"""
|
|
156
|
+
:param writer: writer with close() and add_scalar() methods
|
|
157
|
+
:param batch_size: integer size of batch to track
|
|
158
|
+
"""
|
|
159
|
+
assert isinstance(batch_size, int)
|
|
160
|
+
assert writer is not None
|
|
161
|
+
self.writer = writer
|
|
162
|
+
self.batch_size = batch_size
|
|
163
|
+
|
|
164
|
+
def __enter__(self):
|
|
165
|
+
self._batches = collections.defaultdict(list)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
169
|
+
self.writer.close()
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _as_float(value):
|
|
173
|
+
assert isinstance(value, (float, int, np.ndarray, np.generic, torch.autograd.Variable)) or torch.is_tensor(value)
|
|
174
|
+
tensor_val = None
|
|
175
|
+
if isinstance(value, torch.autograd.Variable):
|
|
176
|
+
tensor_val = value.data
|
|
177
|
+
elif torch.is_tensor(value):
|
|
178
|
+
tensor_val = value
|
|
179
|
+
|
|
180
|
+
if tensor_val is not None:
|
|
181
|
+
return tensor_val.float().mean().item()
|
|
182
|
+
elif isinstance(value, np.ndarray):
|
|
183
|
+
return float(np.mean(value))
|
|
184
|
+
else:
|
|
185
|
+
return float(value)
|
|
186
|
+
|
|
187
|
+
def track(self, param_name, value, iter_index):
|
|
188
|
+
assert isinstance(param_name, str)
|
|
189
|
+
assert isinstance(iter_index, int)
|
|
190
|
+
|
|
191
|
+
data = self._batches[param_name]
|
|
192
|
+
data.append(self._as_float(value))
|
|
193
|
+
|
|
194
|
+
if len(data) >= self.batch_size:
|
|
195
|
+
self.writer.add_scalar(param_name, np.mean(data), iter_index)
|
|
196
|
+
data.clear()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class RewardTracker:
|
|
200
|
+
def __init__(self, writer, min_ts_diff=1.0):
|
|
201
|
+
"""
|
|
202
|
+
Constructs RewardTracker
|
|
203
|
+
:param writer: writer to use for writing stats
|
|
204
|
+
:param min_ts_diff: minimal time difference to track speed
|
|
205
|
+
"""
|
|
206
|
+
self.writer = writer
|
|
207
|
+
self.min_ts_diff = min_ts_diff
|
|
208
|
+
|
|
209
|
+
def __enter__(self):
|
|
210
|
+
self.ts = time.time()
|
|
211
|
+
self.ts_frame = 0
|
|
212
|
+
self.total_rewards = []
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def __exit__(self, *args):
|
|
216
|
+
self.writer.close()
|
|
217
|
+
|
|
218
|
+
def reward(self, reward, frame, epsilon=None):
|
|
219
|
+
self.total_rewards.append(reward)
|
|
220
|
+
mean_reward = np.mean(self.total_rewards[-100:])
|
|
221
|
+
ts_diff = time.time() - self.ts
|
|
222
|
+
if ts_diff > self.min_ts_diff:
|
|
223
|
+
speed = (frame - self.ts_frame) / ts_diff
|
|
224
|
+
self.ts_frame = frame
|
|
225
|
+
self.ts = time.time()
|
|
226
|
+
epsilon_str = "" if epsilon is None else ", eps %.2f" % epsilon
|
|
227
|
+
print("%d: done %d episodes, mean reward %.3f, speed %.2f f/s%s" % (
|
|
228
|
+
frame, len(self.total_rewards), mean_reward, speed, epsilon_str
|
|
229
|
+
))
|
|
230
|
+
sys.stdout.flush()
|
|
231
|
+
self.writer.add_scalar("speed", speed, frame)
|
|
232
|
+
if epsilon is not None:
|
|
233
|
+
self.writer.add_scalar("epsilon", epsilon, frame)
|
|
234
|
+
self.writer.add_scalar("reward_100", mean_reward, frame)
|
|
235
|
+
self.writer.add_scalar("reward", reward, frame)
|
|
236
|
+
return mean_reward if len(self.total_rewards) > 30 else None
|
|
237
|
+
|
|
126
238
|
def query_ollama(prompt, model="llama3", url="http://localhost:11434/api/chat"):
|
|
127
239
|
# Create the data payload as a dictionary
|
|
128
240
|
data = {
|
|
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
|