import math import gradio as gr from transformers import AutoTokenizer, AutoModelForCausalLM import torch # ---- Model config ---- MODEL_NAME = "gpt2" # e.g., "distilgpt2", "gpt2", "gpt2-medium" device = "cuda" if torch.cuda.is_available() else "cpu" tok = AutoTokenizer.from_pretrained(MODEL_NAME) model = AutoModelForCausalLM.from_pretrained(MODEL_NAME).to(device) model.eval() def seq_logprob(context: str, candidate: str, assume_leading_space: bool, show_topk: int): """ Return (total_logprob, step_detail_text, token_list) Computes P(candidate | context) via chain rule over the candidate tokens. """ if not context.strip(): return None, "Please enter context.", [] # Helpful for GPT-2 BPE “word starts” cand_text = (" " + candidate) if assume_leading_space else candidate with torch.no_grad(): ctx_ids = tok.encode(context, return_tensors="pt").to(device) cand_ids = tok.encode(cand_text, add_special_tokens=False) if len(cand_ids) == 0: return None, "Candidate tokenized to empty sequence (check spacing).", [] total_logprob = 0.0 step_lines = [] input_ids = ctx_ids token_texts = [] for i, t_id in enumerate(cand_ids): outputs = model(input_ids=input_ids) logits = outputs.logits[:, -1, :] logprobs = torch.log_softmax(logits, dim=-1) token_logprob = logprobs[0, t_id].item() total_logprob += token_logprob tok_str = tok.decode([t_id]) token_texts.append(tok_str) if show_topk > 0: topk_vals, topk_idx = torch.topk(logprobs, k=min(show_topk, logprobs.shape[-1]), dim=-1) tops = ", ".join([f"{repr(tok.decode([int(idx)]))}:{val.item():.2f}" for idx, val in zip(topk_idx[0], topk_vals[0])]) step_lines.append( f"Step {i+1}: token={repr(tok_str)} logprob={token_logprob:.6f} " f"prob={math.exp(token_logprob):.6e}\n top-{show_topk}: {tops}" ) else: step_lines.append( f"Step {i+1}: token={repr(tok_str)} logprob={token_logprob:.6f} " f"prob={math.exp(token_logprob):.6e}" ) # teacher-forcing: append the true token to continue conditioning input_ids = torch.cat([input_ids, torch.tensor([[t_id]], device=device)], dim=1) detail_text = "\n".join(step_lines) return total_logprob, detail_text, token_texts def compare_candidates(context, cand1, cand2, assume_space, topk): # Basic input checks errs = [] if not context.strip(): errs.append("Please enter a context.") if not cand1.strip(): errs.append("Please enter Candidate A.") if not cand2.strip(): errs.append("Please enter Candidate B.") if errs: return ( f"
{' '.join(errs)}
", "", "", "", "", "" ) # Compute log-probs logp1, details1, toks1 = seq_logprob(context, cand1, assume_space, topk) logp2, details2, toks2 = seq_logprob(context, cand2, assume_space, topk) if logp1 is None or logp2 is None: return ( "
Tokenization error. Check inputs.
", details1, details2, "", "", "" ) # Summaries for each candidate def make_summary(label, cand, logp, toks): seq_prob = math.exp(logp) return ( f"**{label}**: {cand}\n\n" f"Tokenization: {toks}\n" f"Total logprob: {logp:.6f}\n" f"Sequence probability: {seq_prob:.6e}" ) summary1 = make_summary("Candidate A", cand1, logp1, toks1) summary2 = make_summary("Candidate B", cand2, logp2, toks2) # Ratio, odds, and winner log_odds = logp1 - logp2 # log(P(A)/P(B)) # Cap extreme ratios for display; still show exact log-odds try: ratio = math.exp(log_odds) ratio_str = f"{ratio:.6e}" except OverflowError: ratio_str = "∞ (overflow)" winner = "Candidate A" if logp1 > logp2 else ("Tie" if abs(log_odds) < 1e-12 else "Candidate B") if winner == "Candidate A": win_color = "#166534" # green elif winner == "Candidate B": win_color = "#1d4ed8" # blue else: win_color = "#92400e" # amber (tie) headline = ( f"
" f"
Winner: {winner}
" f"
" f"Odds (A/B) = {ratio_str}  |  " f"log-odds = {log_odds:.6f}" f"
" f"
" f"(Odds > 1 means A is more probable; < 1 means B is more probable.)" f"
" ) return headline, summary1, details1, summary2, details2, "" with gr.Blocks(title="Two-Candidate Next-Token Probability Comparator") as demo: gr.Markdown( "# Two-Candidate Next-Word/Token Probability\n" "Given a **context**, compare the conditional probabilities of **two candidate continuations**.\n" "- Uses a pretrained causal LM (default: GPT-2). No fine-tuning.\n" "- Works at the **token** level; multi-token “words” are handled via the chain rule.\n" "- The **Winner** is the higher-probability candidate; we also show the **odds ratio (A/B)** and log-odds." ) with gr.Row(): context = gr.Textbox(label="Context (prompt)", lines=6, placeholder="Paste your prior text here...") with gr.Row(): cand1 = gr.Textbox(label="Candidate A (follow-up)") cand2 = gr.Textbox(label="Candidate B (follow-up)") with gr.Row(): assume_space = gr.Checkbox( value=True, label="Assume leading space before candidates (helps align with word starts in GPT-2 tokenization)" ) topk = gr.Slider(0, 20, value=5, step=1, label="Show top-k alternatives (per token step)") btn = gr.Button("Compare") winner_html = gr.HTML() summary1 = gr.Markdown() details1 = gr.Textbox(label="Candidate A — step-by-step", lines=10) summary2 = gr.Markdown() details2 = gr.Textbox(label="Candidate B — step-by-step", lines=10) _hidden = gr.Textbox(visible=False) btn.click( fn=compare_candidates, inputs=[context, cand1, cand2, assume_space, topk], outputs=[winner_html, summary1, details1, summary2, details2, _hidden] ) demo.launch()