zpenct's picture
feat: add sampling as decoding technique
e7b6295
Raw
History Blame Contribute Delete
5.68 kB
import torch
from typing import List, Dict, Any, Optional
from inspector import (
compute_probabilities,
compute_entropy,
get_top_k,
build_step_result,
)
DECODING_MODES = {
"greedy": "Greedy β€” selalu pilih token tertinggi (deterministik)",
"sampling": "Sampling β€” random sample dari distribusi (variatif)",
}
def greedy_decode_with_inspection(
model,
tokenizer,
device: str,
prompt: str,
max_new_tokens: int = 50,
top_k: int = 10,
temperature: float = 1.0,
stop_at_eos: bool = True,
decoding_mode: str = "greedy", # ← BARU
top_p: float = 0.9, # ← BARU
repetition_penalty: float = 1.0, # ← BARU
seed: Optional[int] = None, # ← BARU
progress_callback=None,
) -> List[Dict[str, Any]]:
"""
Run decoding step-by-step and extract full probability
inspection data at each step.
Args:
model : Loaded HuggingFace causal LM
tokenizer : Corresponding tokenizer
device : 'cpu' or 'cuda'
prompt : Input text string
max_new_tokens : Maximum number of tokens to generate
top_k : Number of top candidates to extract per step
temperature : Softmax temperature
stop_at_eos : Stop when EOS token is produced
decoding_mode : 'greedy' or 'sampling' ← BARU
top_p : Nucleus sampling threshold ← BARU
repetition_penalty: Penalize repeated tokens ← BARU
seed : Random seed (sampling mode only) ← BARU
progress_callback : Optional callable(step, total)
"""
if seed is not None:
torch.manual_seed(seed)
input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)
eos_token_id = tokenizer.eos_token_id
results = []
current_ids = input_ids.clone()
with torch.no_grad():
for step in range(1, max_new_tokens + 1):
outputs = model(input_ids=current_ids)
next_token_logits = outputs.logits[0, -1, :].clone()
# ── Repetition Penalty ──────────────────────────────────
if repetition_penalty != 1.0:
next_token_logits = _apply_repetition_penalty(
next_token_logits, current_ids[0], repetition_penalty
)
# ── Probability Distribution ────────────────────────────
probs = compute_probabilities(next_token_logits, temperature=temperature)
# ── Entropy & Top-K (selalu dari full distribution) ─────
entropy = compute_entropy(probs)
top_candidates = get_top_k(probs, tokenizer, k=top_k)
# ── Token Selection ─────────────────────────────────────
if decoding_mode == "greedy":
chosen_token_id = torch.argmax(probs).item()
elif decoding_mode == "sampling":
filtered_probs = _top_p_filter(probs.clone(), top_p=top_p)
chosen_token_id = torch.multinomial(filtered_probs, num_samples=1).item()
else:
raise ValueError(f"Unknown decoding_mode: '{decoding_mode}'. Use 'greedy' or 'sampling'.")
chosen_token_str = tokenizer.decode([chosen_token_id])
input_so_far = tokenizer.decode(current_ids[0], skip_special_tokens=True)
step_result = build_step_result(
step=step,
input_so_far=input_so_far,
chosen_token=chosen_token_str,
top_candidates=top_candidates,
entropy=entropy,
)
step_result["decoding_mode"] = decoding_mode # ← BARU
results.append(step_result)
if progress_callback:
progress_callback(step, max_new_tokens)
next_token_tensor = torch.tensor([[chosen_token_id]], device=device)
current_ids = torch.cat([current_ids, next_token_tensor], dim=1)
if stop_at_eos and chosen_token_id == eos_token_id:
break
return results
def _apply_repetition_penalty(
logits: torch.Tensor,
input_ids: torch.Tensor,
penalty: float,
) -> torch.Tensor:
"""
Turunkan probabilitas token yang sudah muncul di konteks.
Logits positif dibagi penalty, logits negatif dikali penalty.
"""
for token_id in set(input_ids.tolist()):
if logits[token_id] > 0:
logits[token_id] /= penalty
else:
logits[token_id] *= penalty
return logits
def _top_p_filter(
probs: torch.Tensor,
top_p: float = 0.9,
) -> torch.Tensor:
"""
Nucleus (top-p) filtering: zero out token di luar
top-p% cumulative probability mass.
"""
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
sorted_indices_to_remove = cumulative_probs - sorted_probs > top_p
sorted_probs[sorted_indices_to_remove] = 0.0
filtered = torch.zeros_like(probs)
filtered.scatter_(0, sorted_indices, sorted_probs)
total = filtered.sum()
if total > 0:
filtered = filtered / total
return filtered
def get_generated_text(prompt: str, results: List[Dict]) -> str:
"""Reconstruct the full generated text from step results."""
tokens = [r["chosen_token"] for r in results]
return prompt + "".join(tokens)