simonbb 0.0.6__tar.gz → 0.0.8__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.6
3
+ Version: 0.0.8
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>
@@ -10,14 +10,9 @@ Classifier: Operating System :: OS Independent
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.9
12
12
  Requires-Dist: gymnasium~=1.3.0
13
- Requires-Dist: langchain-chroma~=1.1.0
14
- Requires-Dist: langchain-community~=0.4.1
15
- Requires-Dist: langchain-core~=1.2.23
16
- Requires-Dist: langchain-huggingface~=1.2.1
17
- Requires-Dist: langchain~=1.2.13
18
- Requires-Dist: langgraph~=1.1.3
19
13
  Requires-Dist: matplotlib~=3.10.8
20
14
  Requires-Dist: numpy~=2.4.0
15
+ Requires-Dist: opencv-python~=5.0.0.93
21
16
  Requires-Dist: pytorch-ignite~=0.5.4
22
17
  Requires-Dist: pyyaml~=6.0.3
23
18
  Requires-Dist: ray~=2.56.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simonbb"
7
- version = "0.0.6"
7
+ version = "0.0.8"
8
8
  authors = [
9
9
  { name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
10
10
  ]
@@ -23,17 +23,12 @@ dependencies = [
23
23
  "torchvision~=0.24.1",
24
24
  "numpy~=2.4.0",
25
25
  "transformers~=4.57.3",
26
- "langchain-core~=1.2.23",
27
- "langchain~=1.2.13",
28
- "langgraph~=1.1.3",
29
- "langchain-community~=0.4.1",
30
- "langchain-chroma~=1.1.0",
31
- "langchain-huggingface~=1.2.1",
32
26
  "sentence-transformers~=5.3.0",
33
27
  "gymnasium~=1.3.0",
34
28
  "pytorch-ignite~=0.5.4",
35
29
  "stable_baselines3~=2.9.0",
36
30
  "ray~=2.56.0",
31
+ "opencv-python~=5.0.0.93",
37
32
  "pyyaml~=6.0.3"
38
33
  ]
39
34
  [project.urls]
@@ -25,28 +25,26 @@ 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
 
34
- def __init__(self, env: gym.Env | tt.Collection[gym.Env], agent: BaseAgent,
35
- steps_count: int = 2, steps_delta: int = 1,
36
- env_seed: tt.Optional[int] = None):
34
+ def __init__(self, env: gym.Env | tt.Collection[gym.Env], agent: BaseAgent, steps_count: int = 2, steps_delta: int = 1, env_seed: tt.Optional[int] = None):
37
35
  """
38
36
  Create simple experience source
39
37
  :param env: environment or list of environments to be used
40
38
  :param agent: callable to convert batch of states into actions to take
41
39
  :param steps_count: count of steps to track for every experience chain
42
- :param steps_delta: how many steps to do between experience items
40
+ :param steps_delta: how many steps to do between subjectory emissions
43
41
  :param env_seed: seed to be used in Env.reset() call
44
42
  """
45
43
  assert steps_count >= 1
46
44
  if isinstance(env, (list, tuple)):
47
45
  self.pool = env
48
46
  # do the check for the multiple copies passed
49
- ids = set(id(e) for e in env)
47
+ ids = set(id(e) for e in env) # id() returns a unique, constant integer identifier for a specified object during its lifetime
50
48
  if len(ids) < len(env):
51
49
  raise ValueError("You passed single environment instance multiple times")
52
50
  else:
@@ -54,7 +52,7 @@ class ExperienceSource:
54
52
  self.agent = agent
55
53
  self.steps_count = steps_count
56
54
  self.steps_delta = steps_delta
57
- self.total_rewards = []
55
+ self.total_rewards = [] # simple sum of immediate rewards, without discounting
58
56
  self.total_steps = []
59
57
  self.agent_states = [agent.initial_state() for _ in self.pool]
60
58
  self.env_seed = env_seed
@@ -67,7 +65,7 @@ class ExperienceSource:
67
65
  else:
68
66
  obs, _ = env.reset()
69
67
  states.append(obs)
70
- histories.append(deque(maxlen=self.steps_count))
68
+ histories.append( deque(maxlen=self.steps_count) ) # Note the max length of the double-ended queue
71
69
  cur_rewards.append(0.0)
72
70
  cur_steps.append(0)
73
71
 
@@ -81,17 +79,16 @@ class ExperienceSource:
81
79
  next_state, r, is_done, is_tr, _ = env.step(action)
82
80
  cur_rewards[idx] += r
83
81
  cur_steps[idx] += 1
84
- history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr))
82
+ 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
83
  if len(history) == self.steps_count and iter_idx % self.steps_delta == 0:
86
84
  yield tuple(history)
87
85
  states[idx] = next_state
88
86
  if is_done or is_tr:
89
- # generate tail of history
90
87
  if 0 < len(history) < self.steps_count:
91
88
  yield tuple(history)
92
89
  while len(history) > 1:
93
90
  history.popleft()
94
- yield tuple(history)
91
+ yield tuple(history) # a sub-trajectory of the history
95
92
  self.total_rewards.append(cur_rewards[idx])
96
93
  self.total_steps.append(cur_steps[idx])
97
94
  cur_rewards[idx] = 0.0
@@ -104,13 +101,15 @@ class ExperienceSource:
104
101
  history.clear()
105
102
  iter_idx += 1
106
103
 
104
+
105
+ # pop_total_rewards() returns a list of total rewards (without discounting), one per env, after an episode ends
107
106
  def pop_total_rewards(self) -> tt.List[float]:
108
107
  r = self.total_rewards
109
108
  if r:
110
109
  self.total_rewards = []
111
110
  self.total_steps = []
112
111
  return r
113
-
112
+ # pop_total_rewards() returns a list of total rewards (without discounting) and number of steps, one per env, after an episode ends
114
113
  def pop_rewards_steps(self) -> tt.List[tt.Tuple[float, int]]:
115
114
  res = list(zip(self.total_rewards, self.total_steps))
116
115
  if res:
@@ -148,12 +147,54 @@ class ExperienceSourceFirstLast(ExperienceSource):
148
147
  else:
149
148
  last_state = exp[-1].state
150
149
  elems = exp[:-1]
151
- total_reward = 0.0
150
+ discounted_total_reward = 0.0
152
151
  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)
152
+ discounted_total_reward *= self.gamma
153
+ discounted_total_reward += e.reward
154
+ yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action, reward=discounted_total_reward, last_state=last_state)
155
+
156
+
157
+
158
+ class ExperienceReplayBuffer:
159
+ def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
160
+ self.experience_source_iter = None if experience_source is None else iter(experience_source)
161
+ self.buffer: tt.List[ExperienceSource.Item] = []
162
+ self.capacity = buffer_size
163
+ self.pos = 0
164
+
165
+ def __len__(self):
166
+ return len(self.buffer)
167
+
168
+ def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
169
+ return iter(self.buffer)
170
+
171
+ def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
172
+ """
173
+ Get one random batch from experience replay
174
+ :param batch_size: size of the batch to sample
175
+ :return: list of experience entries
176
+ """
177
+ if len(self.buffer) <= batch_size:
178
+ return self.buffer
179
+ # Warning: replace=False makes random.choice O(n)
180
+ keys = np.random.choice(len(self.buffer), batch_size, replace=True)
181
+ return [self.buffer[key] for key in keys]
182
+
183
+ def _add(self, sample: ExperienceSource.Item):
184
+ if len(self.buffer) < self.capacity:
185
+ self.buffer.append(sample)
186
+ else:
187
+ self.buffer[self.pos] = sample
188
+ self.pos = (self.pos + 1) % self.capacity
189
+
190
+ def populate(self, samples: int):
191
+ """
192
+ Populates samples into the buffer
193
+ :param samples: how many samples to populate
194
+ """
195
+ for _ in range(samples):
196
+ entry = next(self.experience_source_iter)
197
+ self._add(entry)
157
198
 
158
199
 
159
200
  def vector_rewards(rewards: tt.Deque[np.ndarray], dones: tt.Deque[np.ndarray], gamma: float) -> np.ndarray:
@@ -406,47 +447,6 @@ class ExperienceSourceBuffer:
406
447
  yield self.buffer[episode][ofs:ofs+self.steps_count]
407
448
 
408
449
 
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
450
 
451
451
  class PrioReplayBufferNaive:
452
452
  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,
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import re
5
+ import time
6
+ import json
7
+ from pathlib import Path
8
+ from ..utils import grade_math_answer, eta_progress_message
9
+
10
+ RE_NUMBER = re.compile( r"-?(?:\d+/\d+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)" )
11
+ def get_last_boxed(text):
12
+ # Find the last occurrence of "\boxed"
13
+ boxed_start_idx = text.rfind(r"\boxed")
14
+ if boxed_start_idx == -1:
15
+ return None
16
+ # Get position after "\boxed"
17
+ current_idx = boxed_start_idx + len(r"\boxed")
18
+ # Skip any whitespace after "\boxed"
19
+ while current_idx < len(text) and text[current_idx].isspace():
20
+ current_idx += 1
21
+ # Expect an opening brace "{"
22
+ if current_idx >= len(text) or text[current_idx] != "{":
23
+ return None
24
+ # Parse the braces with nesting
25
+ current_idx += 1
26
+ brace_depth = 1
27
+ content_start_idx = current_idx
28
+ while current_idx < len(text) and brace_depth > 0:
29
+ char = text[current_idx]
30
+ if char == "{":
31
+ brace_depth += 1
32
+ elif char == "}":
33
+ brace_depth -= 1
34
+ current_idx += 1
35
+ # Account for unbalanced braces
36
+ if brace_depth != 0:
37
+ return None
38
+ # Extract content inside the outermost braces
39
+ return text[content_start_idx:current_idx-1]
40
+
41
+ def extract_final_candidate(text, fallback="number_then_full"):
42
+ # Default return value if nothing matches
43
+ result = ""
44
+ if text:
45
+ boxed = get_last_boxed(text.strip()) # Prefer the last boxed expression if present
46
+ if boxed:
47
+ result = boxed.strip().strip("$ ")
48
+ # If no boxed expression, try fallback
49
+ elif fallback in ("number_then_full", "number_only"):
50
+ m = RE_NUMBER.findall(text)
51
+ if m: # Use last number
52
+ result = m[-1]
53
+ elif fallback == "number_then_full":
54
+ # Else return full text if no number found
55
+ result = text
56
+ return result
57
+
58
+ def render_prompt(prompt):
59
+ template = (
60
+ "You are a helpful math assistant.\n"
61
+ "Answer the question and write the final result on a new line as:\n"
62
+ "\\boxed{ANSWER}\n\n"
63
+ f"Question:\n{prompt}\n\nAnswer:"
64
+ )
65
+ return template
66
+
67
+
68
+ def evaluate_math500_stream( model, tokenizer, device, generate_func, math_data, out_path=None, max_new_tokens=512, verbose=False ):
69
+ num_examples = len(math_data)
70
+ num_correct = 0
71
+ start_time = time.time()
72
+ with open(Path( out_path ), "w", encoding="utf-8") as f: # Save results for inspection
73
+ for i, row in enumerate(math_data, start=1):
74
+ prompt = render_prompt(row["problem"]) # 1. Apply prompt template
75
+ gen_text = generate_func( model=model, tokenizer=tokenizer, prompt=prompt, device=device, max_new_tokens=max_new_tokens, verbose=verbose )
76
+ extracted = extract_final_candidate( gen_text )
77
+ is_correct = grade_math_answer( extracted, row["answer"] )
78
+ num_correct += int(is_correct)
79
+ record = { # Record to be saved for inspection
80
+ "index": i,
81
+ "problem": row["problem"],
82
+ "gtruth_answer": row["answer"],
83
+ "generated_text": gen_text,
84
+ "extracted": extracted,
85
+ "correct": bool(is_correct),
86
+ }
87
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
88
+
89
+ progress_msg = eta_progress_message( processed=i, total=num_examples, start_time=start_time, show_eta=True, label="MATH-500" )
90
+ print(progress_msg, end="\r", flush=True)
91
+ if verbose: # Print responses during the generation process
92
+ print(
93
+ f"\n\n{'='*50}\n{progress_msg}\n"
94
+ f"{'='*50}\nExtracted: {extracted}\n"
95
+ f"Expected: {row['answer']}\n"
96
+ f"Correct so far: {num_correct}\n{'-'*50}"
97
+ )
98
+
99
+ # Print summary information
100
+ seconds_elapsed = time.time() - start_time
101
+ acc = num_correct / num_examples if num_examples else 0.0
102
+ print(f"\nAccuracy: {acc*100:.1f}% ({num_correct}/{num_examples})")
103
+ print(f"Total time: {seconds_elapsed/60:.1f} min")
104
+ print(f"Logs written to: {out_path}")
105
+ return num_correct, num_examples, acc