simonbb 0.0.5__tar.gz → 0.0.7__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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simonbb
3
- Version: 0.0.5
3
+ Version: 0.0.7
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>
@@ -18,8 +18,10 @@ Requires-Dist: langchain~=1.2.13
18
18
  Requires-Dist: langgraph~=1.1.3
19
19
  Requires-Dist: matplotlib~=3.10.8
20
20
  Requires-Dist: numpy~=2.4.0
21
+ Requires-Dist: opencv-python~=5.0.0.93
21
22
  Requires-Dist: pytorch-ignite~=0.5.4
22
23
  Requires-Dist: pyyaml~=6.0.3
24
+ Requires-Dist: ray~=2.56.0
23
25
  Requires-Dist: sentence-transformers~=5.3.0
24
26
  Requires-Dist: stable-baselines3~=2.9.0
25
27
  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.7"
8
8
  authors = [
9
9
  { name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
10
10
  ]
@@ -33,6 +33,8 @@ 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",
37
+ "opencv-python~=5.0.0.93",
36
38
  "pyyaml~=6.0.3"
37
39
  ]
38
40
  [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
@@ -25,9 +25,9 @@ class Experience:
25
25
 
26
26
  class ExperienceSource:
27
27
  """
28
- Simple n-step experience source using single or multiple environments
28
+ n-step experience source using single or multiple environments
29
29
 
30
- Every experience contains n list of Experience entries
30
+ Every experience contains a list of n Experience entries
31
31
  """
32
32
  Item = tt.Tuple[Experience, ...]
33
33
 
@@ -39,7 +39,7 @@ class ExperienceSource:
39
39
  :param env: environment or list of environments to be used
40
40
  :param agent: callable to convert batch of states into actions to take
41
41
  :param steps_count: count of steps to track for every experience chain
42
- :param steps_delta: how many steps to do between experience items
42
+ :param steps_delta: how many steps to do between subjectory emissions
43
43
  :param env_seed: seed to be used in Env.reset() call
44
44
  """
45
45
  assert steps_count >= 1
@@ -54,7 +54,7 @@ class ExperienceSource:
54
54
  self.agent = agent
55
55
  self.steps_count = steps_count
56
56
  self.steps_delta = steps_delta
57
- self.total_rewards = []
57
+ self.total_rewards = [] # simple sum of immediate rewards, without discounting
58
58
  self.total_steps = []
59
59
  self.agent_states = [agent.initial_state() for _ in self.pool]
60
60
  self.env_seed = env_seed
@@ -67,7 +67,7 @@ class ExperienceSource:
67
67
  else:
68
68
  obs, _ = env.reset()
69
69
  states.append(obs)
70
- histories.append(deque(maxlen=self.steps_count))
70
+ histories.append( deque(maxlen=self.steps_count) ) # Note the max length of the double-ended queue
71
71
  cur_rewards.append(0.0)
72
72
  cur_steps.append(0)
73
73
 
@@ -81,17 +81,16 @@ class ExperienceSource:
81
81
  next_state, r, is_done, is_tr, _ = env.step(action)
82
82
  cur_rewards[idx] += r
83
83
  cur_steps[idx] += 1
84
- history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr))
84
+ history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr)) # push out the oldest step once deque is full (i.e., steps_count reached)
85
85
  if len(history) == self.steps_count and iter_idx % self.steps_delta == 0:
86
86
  yield tuple(history)
87
87
  states[idx] = next_state
88
88
  if is_done or is_tr:
89
- # generate tail of history
90
89
  if 0 < len(history) < self.steps_count:
91
90
  yield tuple(history)
92
91
  while len(history) > 1:
93
92
  history.popleft()
94
- yield tuple(history)
93
+ yield tuple(history) # a sub-trajectory of the history
95
94
  self.total_rewards.append(cur_rewards[idx])
96
95
  self.total_steps.append(cur_steps[idx])
97
96
  cur_rewards[idx] = 0.0
@@ -104,13 +103,15 @@ class ExperienceSource:
104
103
  history.clear()
105
104
  iter_idx += 1
106
105
 
106
+
107
+ # pop_total_rewards() returns a list of total rewards (without discounting), one per env, after an episode ends
107
108
  def pop_total_rewards(self) -> tt.List[float]:
108
109
  r = self.total_rewards
109
110
  if r:
110
111
  self.total_rewards = []
111
112
  self.total_steps = []
112
113
  return r
113
-
114
+ # pop_total_rewards() returns a list of total rewards (without discounting) and number of steps, one per env, after an episode ends
114
115
  def pop_rewards_steps(self) -> tt.List[tt.Tuple[float, int]]:
115
116
  res = list(zip(self.total_rewards, self.total_steps))
116
117
  if res:
@@ -148,12 +149,54 @@ class ExperienceSourceFirstLast(ExperienceSource):
148
149
  else:
149
150
  last_state = exp[-1].state
150
151
  elems = exp[:-1]
151
- total_reward = 0.0
152
+ discounted_total_reward = 0.0
152
153
  for e in reversed(elems):
153
- total_reward *= self.gamma
154
- total_reward += e.reward
155
- yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action,
156
- reward=total_reward, last_state=last_state)
154
+ discounted_total_reward *= self.gamma
155
+ discounted_total_reward += e.reward
156
+ yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action, reward=discounted_total_reward, last_state=last_state)
157
+
158
+
159
+
160
+ class ExperienceReplayBuffer:
161
+ def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
162
+ self.experience_source_iter = None if experience_source is None else iter(experience_source)
163
+ self.buffer: tt.List[ExperienceSource.Item] = []
164
+ self.capacity = buffer_size
165
+ self.pos = 0
166
+
167
+ def __len__(self):
168
+ return len(self.buffer)
169
+
170
+ def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
171
+ return iter(self.buffer)
172
+
173
+ def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
174
+ """
175
+ Get one random batch from experience replay
176
+ :param batch_size: size of the batch to sample
177
+ :return: list of experience entries
178
+ """
179
+ if len(self.buffer) <= batch_size:
180
+ return self.buffer
181
+ # Warning: replace=False makes random.choice O(n)
182
+ keys = np.random.choice(len(self.buffer), batch_size, replace=True)
183
+ return [self.buffer[key] for key in keys]
184
+
185
+ def _add(self, sample: ExperienceSource.Item):
186
+ if len(self.buffer) < self.capacity:
187
+ self.buffer.append(sample)
188
+ else:
189
+ self.buffer[self.pos] = sample
190
+ self.pos = (self.pos + 1) % self.capacity
191
+
192
+ def populate(self, samples: int):
193
+ """
194
+ Populates samples into the buffer
195
+ :param samples: how many samples to populate
196
+ """
197
+ for _ in range(samples):
198
+ entry = next(self.experience_source_iter)
199
+ self._add(entry)
157
200
 
158
201
 
159
202
  def vector_rewards(rewards: tt.Deque[np.ndarray], dones: tt.Deque[np.ndarray], gamma: float) -> np.ndarray:
@@ -406,47 +449,6 @@ class ExperienceSourceBuffer:
406
449
  yield self.buffer[episode][ofs:ofs+self.steps_count]
407
450
 
408
451
 
409
- class ExperienceReplayBuffer:
410
- def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
411
- self.experience_source_iter = None if experience_source is None else iter(experience_source)
412
- self.buffer: tt.List[ExperienceSource.Item] = []
413
- self.capacity = buffer_size
414
- self.pos = 0
415
-
416
- def __len__(self):
417
- return len(self.buffer)
418
-
419
- def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
420
- return iter(self.buffer)
421
-
422
- def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
423
- """
424
- Get one random batch from experience replay
425
- :param batch_size: size of the batch to sample
426
- :return: list of experience entries
427
- """
428
- if len(self.buffer) <= batch_size:
429
- return self.buffer
430
- # Warning: replace=False makes random.choice O(n)
431
- keys = np.random.choice(len(self.buffer), batch_size, replace=True)
432
- return [self.buffer[key] for key in keys]
433
-
434
- def _add(self, sample: ExperienceSource.Item):
435
- if len(self.buffer) < self.capacity:
436
- self.buffer.append(sample)
437
- else:
438
- self.buffer[self.pos] = sample
439
- self.pos = (self.pos + 1) % self.capacity
440
-
441
- def populate(self, samples: int):
442
- """
443
- Populates samples into the buffer
444
- :param samples: how many samples to populate
445
- """
446
- for _ in range(samples):
447
- entry = next(self.experience_source_iter)
448
- self._add(entry)
449
-
450
452
 
451
453
  class PrioReplayBufferNaive:
452
454
  def __init__(self, exp_source, buf_size, prob_alpha=0.6):
@@ -13,39 +13,38 @@ class EpisodeEvents(EventEnum):
13
13
 
14
14
 
15
15
  class EndOfEpisodeHandler:
16
- def __init__(self, exp_source: ExperienceSource, alpha: float = 0.98,
17
- bound_avg_reward: Optional[float] = None,
18
- subsample_end_of_episode: Optional[int] = None):
16
+ def __init__(self, exp_source: ExperienceSource, alpha: float = 0.98, bound_avg_reward: Optional[float] = None, period: Optional[int] = None):
19
17
  """
20
- Construct end-of-episode event handler
18
+ This end-of-episode event handler, triggered by Events.ITERATION_COMPLETED, fires an EPISODE_COMPLETED event for every episode, or every {period} number of episodes
21
19
  :param exp_source: experience source to use
22
20
  :param alpha: smoothing alpha param
23
21
  :param bound_avg_reward: optional boundary for average reward
24
- :param subsample_end_of_episode: if given, end of episode event will be subsampled by this amount
22
+ :param period: if given, end of episode event fire once every {period} episodes
25
23
  """
26
24
  self._exp_source = exp_source
27
25
  self._alpha = alpha
28
26
  self._bound_avg_reward = bound_avg_reward
29
27
  self._best_avg_reward = None
30
- self._subsample_end_of_episode = subsample_end_of_episode
28
+ self._period = period
31
29
 
32
30
  def attach(self, engine: Engine):
33
- engine.add_event_handler(Events.ITERATION_COMPLETED, self)
34
- engine.register_events(*EpisodeEvents)
31
+ engine.add_event_handler( Events.ITERATION_COMPLETED, self ) # invoke EndOfEpisodeHandler() instance whenever an iteration (consisting of multiple episodes) is completed
32
+ engine.register_events( *EpisodeEvents )
35
33
  State.event_to_attr[EpisodeEvents.EPISODE_COMPLETED] = "episode"
36
34
  State.event_to_attr[EpisodeEvents.BOUND_REWARD_REACHED] = "episode"
37
35
  State.event_to_attr[EpisodeEvents.BEST_REWARD_REACHED] = "episode"
38
36
 
39
37
  def __call__(self, engine: Engine):
38
+ # pop_rewards_steps() returns a list of (total rewards, steps) of newly completed episodes since the last call of pop_rewards_steps().
40
39
  for reward, steps in self._exp_source.pop_rewards_steps():
41
40
  engine.state.episode = getattr(engine.state, "episode", 0) + 1
42
- engine.state.episode_reward = reward
41
+ engine.state.episode_reward = reward # sum of immediate rewards of all steps during the episode, without discounting
43
42
  engine.state.episode_steps = steps
44
43
  engine.state.metrics['reward'] = reward
45
44
  engine.state.metrics['steps'] = steps
46
45
  self._update_smoothed_metrics(engine, reward, steps)
47
- if self._subsample_end_of_episode is None or engine.state.episode % self._subsample_end_of_episode == 0:
48
- engine.fire_event(EpisodeEvents.EPISODE_COMPLETED)
46
+ if self._period is None or engine.state.episode % self._period == 0:
47
+ engine.fire_event( EpisodeEvents.EPISODE_COMPLETED )
49
48
  if self._bound_avg_reward is not None and engine.state.metrics['avg_reward'] >= self._bound_avg_reward:
50
49
  engine.fire_event(EpisodeEvents.BOUND_REWARD_REACHED)
51
50
  if self._best_avg_reward is None:
@@ -110,13 +109,10 @@ class PeriodEvents(EventEnum):
110
109
  ITERS_10000_COMPLETED = "iterations_10000_completed"
111
110
  ITERS_100000_COMPLETED = "iterations_100000_completed"
112
111
 
113
-
114
112
  class PeriodicEvents:
115
113
  """
116
- The same as CustomPeriodicEvent from ignite.contrib, but use true amount of iterations,
117
- which is good for TensorBoard
114
+ The same as CustomPeriodicEvent from ignite.contrib, but use true amount of iterations, which is good for TensorBoard
118
115
  """
119
-
120
116
  INTERVAL_TO_EVENT = {
121
117
  10: PeriodEvents.ITERS_10_COMPLETED,
122
118
  100: PeriodEvents.ITERS_100_COMPLETED,
File without changes
File without changes
File without changes
File without changes