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.
- {simonbb-0.0.6 → simonbb-0.0.8}/PKG-INFO +3 -8
- {simonbb-0.0.6 → simonbb-0.0.8}/pyproject.toml +2 -7
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/experience.py +59 -59
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/ignite.py +11 -15
- simonbb-0.0.8/src/simonbb/cis433/math500.py +105 -0
- simonbb-0.0.8/src/simonbb/cis433/qwen3.py +639 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/utils.py +139 -0
- simonbb-0.0.6/src/simonbb/cis438/agentic.py → simonbb-0.0.8/src/simonbb/cis438/rag.py +28 -30
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/utils.py +213 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/LICENSE +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/README.md +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/__init__.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/__init__.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/actions.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/agent.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/bert.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/dqn.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/gpt.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/transformer.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis433/wrappers.py +0 -0
- {simonbb-0.0.6 → simonbb-0.0.8}/src/simonbb/cis438/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: simonbb
|
|
3
|
-
Version: 0.0.
|
|
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.
|
|
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
|
-
|
|
28
|
+
n-step experience source using single or multiple environments
|
|
29
29
|
|
|
30
|
-
Every experience contains
|
|
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
|
|
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
|
-
|
|
150
|
+
discounted_total_reward = 0.0
|
|
152
151
|
for e in reversed(elems):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action,
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|