import streamlit as st
from transformers import GPT2LMHeadModel, GPT2Tokenizer
import os
import time
st.set_page_config(
page_title="LLM Visualizer",
page_icon="",
layout="wide"
)
st.title(" LLM Visualizer")
st.caption("See exactly what happens inside GPT-2")
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"Embedding Space",
"Tokenizer Explorer",
"Next Token Probability",
"Generation Walkthrough",
"Attention Heatmap"
])
@st.cache_resource
def load_model():
from transformers import AutoTokenizer, AutoModelForCausalLM
if not os.path.exists(".model/"):
AutoTokenizer.from_pretrained("distilgpt2").save_pretrained(".model/")
AutoModelForCausalLM.from_pretrained("distilgpt2").save_pretrained(".model/")
tokenizer = GPT2Tokenizer.from_pretrained(".model/")
model = GPT2LMHeadModel.from_pretrained(".model/", output_attentions=True)
model.eval()
return tokenizer, model
if "model_loaded" not in st.session_state:
st.markdown("""
LLM Visualizer
Loading GPT-2 into memory...
This only happens once per session
""", unsafe_allow_html=True)
tokenizer, model = load_model()
st.session_state.model_loaded = True
st.session_state.show_success = True
st.rerun()
else:
tokenizer, model = load_model()
if st.session_state.get("show_success"):
success = st.success("Model ready!")
success.empty()
st.session_state.show_success = False
with tab1:
st.header("Embedding Space Visualizer")
from modules.embedding_viz import get_word_embeddings, reduce_dimensions
import plotly.express as px
import pandas as pd
method = st.radio("Reduction method", ["UMAP", "PCA"], horizontal=True)
dimensions = st.radio("View", ["3D", "2D"], horizontal=True)
if st.button("Generate", key="embed_generate"):
with st.spinner("Computing embeddings..."):
words, categories, vectors = get_word_embeddings(model, tokenizer)
n = 3 if dimensions == "3D" else 2
coords = reduce_dimensions(vectors, method, n_components=n)
if dimensions == "3D":
df = pd.DataFrame({
"word": words, "category": categories,
"x": coords[:, 0], "y": coords[:, 1], "z": coords[:, 2]
})
fig = px.scatter_3d(df, x="x", y="y", z="z",
color="category", text="word", title="Word Embeddings in 3D Space")
else:
df = pd.DataFrame({
"word": words, "category": categories,
"x": coords[:, 0], "y": coords[:, 1]
})
fig = px.scatter(df, x="x", y="y",
color="category", text="word", title="Word Embeddings in 2D Space")
fig.update_traces(textposition="top center", marker=dict(size=6))
fig.update_layout(height=700)
st.plotly_chart(fig, use_container_width=True)
with tab2:
from modules.tokenizer_explorer import tokenize_text
text = st.text_input("Type anything", "What are pre-trained models with respect to Transformers?",key="tok_input")
if text:
tokens, ids, types = tokenize_text(text, tokenizer)
color_map = {
"full word": "🟦",
"subword": "🟨",
"punctuation": "🟥",
"number": "🟩"
}
cols = st.columns(len(tokens))
for i, col in enumerate(cols):
col.markdown(f"{color_map[types[i]]} **{tokens[i]}**")
col.caption(str(ids[i]))
st.markdown("🟦 Full word 🟨 Subword 🟥 Punctuation 🟩 Number")
st.write(f"Total tokens: {len(tokens)} | Characters: {len(text)} | Ratio: {len(text)/len(tokens):.1f} chars/token")
with tab3:
st.header("Next Token Probability")
from modules.next_token import get_next_token_probs
import plotly.express as px
text = st.text_input("Enter prompt", "Once upon a time",key="prob_input")
temperature = st.slider("Temperature", 0.1, 2.0, 1.0, 0.1, key="prob_temperature")
rep_penalty = st.slider("Repetition Penalty", 1.0, 2.0, 1.0, 0.1, key="prob_rep_penalty")
st.caption("1.0 = no penalty, 2.0 = strongly avoid repeating tokens")
if text:
with st.spinner("Running forward pass..."):
tokens, probs, entropy = get_next_token_probs(
text, model, tokenizer, temperature, top_k=15, rep_penalty=rep_penalty
)
col1, col2 = st.columns([3, 1])
with col1:
fig = px.bar(
x=probs, y=tokens,
orientation="h",
title="Top 15 Next Token Probabilities",
labels={"x": "Probability", "y": "Token"}
)
fig.update_layout(yaxis=dict(autorange="reversed"), height=500)
st.plotly_chart(fig, use_container_width=True)
with col2:
st.metric("Entropy", f"{entropy:.2f}")
st.caption("Low = confident\nHigh = uncertain")
st.metric("Top token", tokens[0])
st.metric("Top prob", f"{probs[0]:.2%}")
with tab4:
st.header("Generation Walkthrough")
from modules.generation import get_next_token
import time
import pandas as pd
def token_color(prob):
if prob > 0.3:
return "green"
elif prob > 0.1:
return "orange"
else:
return "red"
prompt = st.text_input("Enter prompt", "Once upon a time", key="gen_prompt")
st.markdown("""
How Generation Works
At each step the model:
- Reads your entire prompt so far
- Calculates probability for all 50,257 tokens
- Picks the next token based on temperature
- Adds it to the prompt and repeats
🟢 Green = model was confident (p > 0.3)
🟠Orange = model was uncertain (p 0.1-0.3)
🔴 Red = model was guessing (p < 0.1)
Try low temperature (0.1) for focused output vs high temperature (1.5) for creative/chaotic output
""", unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
max_tokens = st.slider("Max tokens", 5,100, 20, 1, key="gen_max_tokens")
with col2:
temperature = st.slider("Temperature", 0.1, 2.0, 1.0, 0.1, key="gen_temperature")
with col3:
rep_penalty = st.slider("Repetition Penalty", 1.0, 2.0, 1.3, 0.1, key="gen_rep_penalty")
if st.button("Generate", key="gen_generate"):
sentence = prompt
sentence_tokens = [prompt]
sentence_probs = [1.0]
history_data = []
sentence_placeholder = st.empty()
table_placeholder = st.empty()
for i in range(int(max_tokens)):
token, alternatives, entropy = get_next_token(
sentence, model, tokenizer, temperature, rep_penalty
)
sentence += token[0]
sentence_tokens.append(token[0])
sentence_probs.append(float(token[1]))
history_data.append({
"Step": i + 1,
"Chosen Token": token[0].strip(),
"Probability": f"{token[1]:.2f}",
"Alternatives": " | ".join([f"{t.strip()}({p:.2f})" for t, p in alternatives])
})
colored_tokens = []
for j, (tok, prob) in enumerate(zip(sentence_tokens, sentence_probs)):
if j == len(sentence_tokens) - 1:
color = token_color(prob)
colored_tokens.append(f'{tok}')
else:
colored_tokens.append(tok)
sentence_placeholder.markdown(
f'{" ".join(colored_tokens)}
',
unsafe_allow_html=True
)
df = pd.DataFrame(history_data)
table_placeholder.dataframe(
df,
use_container_width=True,
height=(len(history_data) + 1) * 35 + 3
)
time.sleep(1)
with tab5:
st.header("Attention Heatmap")
from modules.attention import get_attention
import plotly.express as px
import numpy as np
text = st.text_input("Enter sentence (max 20 words)", "The cat sat on the mat", key="att_input")
st.markdown("""
How Attention Works
When GPT-2 processes a sentence, every token "looks at" every other token with different levels of focus. This is called self-attention.
Darker red = stronger attention between two tokens
White = token is ignoring that position
Upper triangle is always empty — GPT-2 can only look backwards, never at future tokens
First column dark — most tokens attend strongly to the first word
What to try:
🔹 Layer 1-2 — basic syntax, nearby word relationships
🔹 Layer 3-4 — grammar, subject-verb patterns
🔹 Layer 5-6 — semantics, meaning level patterns
🔹 Average all heads — see the overall picture across all 12 perspectives
Try "The cat sat on the mat" and see how "cat" gets attended to by everything after it
""", unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
layer = st.slider("Layer", 1, 6, 1, 1, key="att_layer")
with col2:
head = st.slider("Head", 1, 12, 1, 1, key="att_head")
avg_heads = st.checkbox("Average all heads")
if st.button("Show Attention", key="att_button"):
with st.spinner("Computing attention..."):
tokens, attentions = get_attention(text, model, tokenizer)
# attentions[layer] shape = (12, seq_len, seq_len)
layer_attention = attentions[layer - 1] # select layer
if avg_heads:
matrix = layer_attention.mean(axis=0) # average across heads
else:
matrix = layer_attention[head - 1] # select specific head
fig = px.imshow(
matrix,
x=tokens,
y=tokens,
color_continuous_scale="Reds",
title=f"Attention - Layer {layer} {'(avg heads)' if avg_heads else f'Head {head}'}",
labels={"x": "Attends To", "y": "Token"}
)
fig.update_layout(height=600)
st.plotly_chart(fig, use_container_width=True)
# interesting pattern callouts
st.subheader("What to look for")
col1, col2, col3 = st.columns(3)
with col1:
st.info(" Diagonal = each token attending to itself")
with col2:
st.info("Last row = final token attends to everything")
with col3:
st.info(" Try different layers — early=syntax, late=semantics")