forked from rasbt/reasoning-from-scratch
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathch05.py
More file actions
268 lines (223 loc) · 8.13 KB
/
Copy pathch05.py
File metadata and controls
268 lines (223 loc) · 8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# Copyright (c) Sebastian Raschka under Apache License 2.0 (see LICENSE.txt)
# Source for "Build a Reasoning Model (From Scratch)": https://mng.bz/lZ5B
# Code repository: https://github.com/rasbt/reasoning-from-scratch
from .ch03 import extract_final_candidate, render_prompt
from .ch04 import (
generate_text_stream_concat_flex,
generate_text_top_p_stream_cache
)
import math
import torch
def heuristic_score(
answer,
prompt=None, # Placeholder that is ignored
brevity_bonus=500.0,
boxed_bonus=2.0,
extract_bonus=1.0,
fulltext_bonus=0.0,
):
score = 0.0
# Reward answers that have a final boxed value
cand = extract_final_candidate(answer, fallback="none")
if cand:
score += boxed_bonus
# Give weaker rewards if answer doesn't have a boxed value
else:
cand = extract_final_candidate(answer, fallback="number_only")
if cand:
score += extract_bonus
else:
cand = extract_final_candidate(
answer, fallback="number_then_full"
)
if cand:
score += fulltext_bonus
# Add a brevity reward that decays with text length
score += 1.5 * math.exp(-len(answer) / brevity_bonus)
return score
@torch.inference_mode()
def calc_next_token_probas(model, tokenizer, prompt, device):
token_ids = torch.tensor(tokenizer.encode(prompt), device=device)
# Get logits and probabilities similar to text generation functions
logits = model(token_ids.unsqueeze(0)).squeeze(0)
all_probas = torch.softmax(logits, dim=-1)
# Positions we score (here: all)
t_idx = torch.arange(0, token_ids.shape[0] - 1, device=device)
# Since we have the text, we know the true next tokens
next_ids = token_ids[1:]
# Get probabilities for each next token
next_token_probas = all_probas[t_idx, next_ids]
print(
"Next-token probabilities:",
[p.item() for p in next_token_probas]
)
# Likelihood of the sequence is the product of the probability scores
print(
"Joint probability:",
torch.prod(next_token_probas)
)
@torch.inference_mode()
def calc_next_token_logprobas(model, tokenizer, prompt, device, show=True):
token_ids = torch.tensor(tokenizer.encode(prompt), device=device)
logits = model(token_ids.unsqueeze(0)).squeeze(0)
# We now use log_softmax
all_logprobas = torch.log_softmax(logits, dim=-1)
t_idx = torch.arange(0, token_ids.shape[0] - 1, device=device)
next_ids = token_ids[1:]
next_token_logprobas = all_logprobas[t_idx, next_ids]
# We replace the product with a sum
sum_next_token_logprobas = torch.sum(next_token_logprobas)
if show:
print("Next-token log-probabilities:", next_token_logprobas)
print("Joint log-probability:", sum_next_token_logprobas)
else:
return next_token_logprobas, sum_next_token_logprobas
@torch.inference_mode()
def avg_logprob_answer(model, tokenizer, prompt, answer, device="cpu"):
# Encode prompt and answer tokens separately to get the prompt length later
prompt_ids = tokenizer.encode(prompt)
answer_ids = tokenizer.encode(answer)
full_ids = torch.tensor(prompt_ids + answer_ids, device=device)
# Same as in calc_next_token_logprobas before
logits = model(full_ids.unsqueeze(0)).squeeze(0)
logprobs = torch.log_softmax(logits, dim=-1)
# Index range for positions corresponding to answer tokens
start = len(prompt_ids) - 1
end = full_ids.shape[0] - 1
# Same as before, except for using start and end
t_idx = torch.arange(start, end, device=device)
next_tokens = full_ids[start + 1 : end + 1]
next_token_logps = logprobs[t_idx, next_tokens]
# Average over the answer token scores
return torch.mean(next_token_logps).item()
def make_critique_prompt(raw_prompt, draft):
return (
"You are a meticulous reviewer. Identify logical errors, missing "
"steps, or arithmetic mistakes. If the answer seems correct, "
"say so briefly. Then propose a concise plan to fix issues.\n\n"
f"Question:\n{raw_prompt}\n\n"
f"Draft answer:\n{draft}\n\n"
"Write a short critique and bullet-point fix plan "
"(under ~120 words).\n"
"Critique:"
)
def make_refine_prompt(raw_prompt, draft, critique):
return (
"Revise the answer using the critique. Keep it concise and "
"end with a final boxed result: \\boxed{ANSWER}\n\n"
f"Question:\n{raw_prompt}\n\n"
f"Previous answer:\n{draft}\n\n"
f"Critique:\n{critique}\n\n"
"Revised answer:"
)
def self_refinement_loop(
model,
tokenizer,
raw_prompt,
device,
iterations=2,
max_response_tokens=2048,
max_critique_tokens=256,
score_fn=None,
prompt_renderer=render_prompt,
prompt_suffix="",
verbose=False,
temperature=0.7,
top_p=0.9,
):
steps = []
# Initial response (draft)
prompt = prompt_renderer(raw_prompt) + prompt_suffix
current_full = generate_text_stream_concat_flex(
model=model,
tokenizer=tokenizer,
prompt=prompt,
device=device,
max_new_tokens=max_response_tokens,
verbose=False,
generate_func=generate_text_top_p_stream_cache,
temperature=temperature,
top_p=top_p,
)
current_extracted = extract_final_candidate(
current_full, fallback="number_then_full"
)
if score_fn:
current_score = score_fn(answer=current_full, prompt=prompt)
else:
current_score = 0.0
# Run for one or more iterations
for it in range(iterations):
draft_before_full = current_full
draft_before_extracted = current_extracted
score_before = current_score
# Critique the response
critique_prompt = make_critique_prompt(
raw_prompt, draft_before_full
)
critique_full = generate_text_stream_concat_flex(
model=model,
tokenizer=tokenizer,
prompt=critique_prompt,
device=device,
max_new_tokens=max_critique_tokens,
verbose=False,
generate_func=generate_text_top_p_stream_cache,
temperature=temperature,
top_p=top_p,
)
# Refine the response
refine_prompt = make_refine_prompt(
raw_prompt, draft_before_full, critique_full
)
revised_full = generate_text_stream_concat_flex(
model=model,
tokenizer=tokenizer,
prompt=refine_prompt,
device=device,
max_new_tokens=max_response_tokens,
verbose=False,
generate_func=generate_text_top_p_stream_cache,
temperature=temperature,
top_p=top_p,
)
revised_extracted = extract_final_candidate(
revised_full, fallback="number_then_full"
)
if score_fn:
revised_score = score_fn(
answer=revised_full, prompt=prompt # Still use original prompt here
)
else:
revised_score = 0.0
# Log the results
step = {
"iteration": it + 1,
"draft_full": draft_before_full,
"draft_extracted": draft_before_extracted,
"critique": critique_full,
"revised_full": revised_full,
"revised_extracted": revised_extracted,
"score_before": score_before,
"score_after": revised_score,
}
steps.append(step)
if verbose:
print(
f"[Refinement {it+1}/{iterations}]"
f"\nCurrent: {draft_before_extracted}"
f"\nRevised: {revised_extracted}"
f"\nScore before: {score_before:.3f}"
f"\nScore after: {revised_score:.3f}"
f"\n{'=' * 25}"
)
# Accept revised response if it's not worse
if revised_score >= current_score:
current_full = revised_full
current_extracted = revised_extracted
current_score = revised_score
return {
"final_full": current_full,
"final_extracted": current_extracted,
"steps": steps,
}