Spaces:
Paused
Paused
Add learned VQ tokenizer option
Browse files- README.md +6 -1
- app.py +172 -6
- requirements.txt +6 -1
README.md
CHANGED
|
@@ -15,7 +15,12 @@ license: mit
|
|
| 15 |
|
| 16 |
A CPU-friendly Gradio app for teaching image tokenization and image generation as sequence problems.
|
| 17 |
|
| 18 |
-
The first tab takes a real image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
The app also includes a deliberately small, transparent image-token sampler. It does not call a proprietary image model. Instead, it shows the mechanics that matter for a workshop:
|
| 21 |
|
|
|
|
| 15 |
|
| 16 |
A CPU-friendly Gradio app for teaching image tokenization and image generation as sequence problems.
|
| 17 |
|
| 18 |
+
The first tab takes a real image and shows image tokenization two ways:
|
| 19 |
+
|
| 20 |
+
- a pretrained learned VQ tokenizer from `CompVis/ldm-celebahq-256/vqvae`
|
| 21 |
+
- a transparent k-means patch tokenizer for comparison
|
| 22 |
+
|
| 23 |
+
The learned tokenizer shows real learned codebook IDs, reconstruction from the VQ decoder, token usage, and representative image regions for the most-used codes. The k-means option can learn a tiny codebook from one image or from all loaded MoMA images.
|
| 24 |
|
| 25 |
The app also includes a deliberately small, transparent image-token sampler. It does not call a proprietary image model. Instead, it shows the mechanics that matter for a workshop:
|
| 26 |
|
app.py
CHANGED
|
@@ -7,10 +7,13 @@ from functools import lru_cache
|
|
| 7 |
from io import BytesIO
|
| 8 |
|
| 9 |
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
|
|
|
|
|
|
| 10 |
|
| 11 |
import gradio as gr
|
| 12 |
import numpy as np
|
| 13 |
import requests
|
|
|
|
| 14 |
from PIL import Image, ImageDraw, ImageFont
|
| 15 |
|
| 16 |
try:
|
|
@@ -42,6 +45,8 @@ HEADERS = {
|
|
| 42 |
}
|
| 43 |
MOMA_SAMPLE_SIZE = 24
|
| 44 |
MOMA_CANDIDATES = 120
|
|
|
|
|
|
|
| 45 |
|
| 46 |
TOKENS = [
|
| 47 |
{"id": "sky", "label": "Sky", "base": (91, 169, 230), "initial": "S"},
|
|
@@ -255,6 +260,27 @@ def randomize_seed():
|
|
| 255 |
return int(rng.integers(0, MAX_SEED))
|
| 256 |
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
def prompt_bias(prompt):
|
| 259 |
prompt = (prompt or "").lower()
|
| 260 |
bias = np.zeros(TOKEN_COUNT, dtype=np.float32)
|
|
@@ -775,8 +801,140 @@ def learned_codebook_rows(centroids, assignments, errors):
|
|
| 775 |
return rows
|
| 776 |
|
| 777 |
|
| 778 |
-
|
| 779 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
grid_size = max(4, min(32, int(grid_size)))
|
| 781 |
patch_size = max(4, min(32, int(patch_size)))
|
| 782 |
codebook_size = max(2, min(64, int(codebook_size)))
|
|
@@ -818,18 +976,18 @@ def tokenize_image(image, grid_size, patch_size, codebook_size, iterations, seed
|
|
| 818 |
return prepared, token_grid, reconstruction, error_map, gallery, rows, summary
|
| 819 |
|
| 820 |
|
| 821 |
-
def initial_tokenizer(grid_size, patch_size, codebook_size, iterations, seed, codebook_source):
|
| 822 |
items, message = load_moma_items()
|
| 823 |
-
outputs = tokenize_image(items[0]["img"], grid_size, patch_size, codebook_size, iterations, seed, codebook_source)
|
| 824 |
summary = f"{message}\n\n{outputs[-1]}"
|
| 825 |
return (moma_gallery_items(),) + outputs[:-1] + (summary,)
|
| 826 |
|
| 827 |
|
| 828 |
-
def tokenize_moma_selection(grid_size, patch_size, codebook_size, iterations, seed, codebook_source, evt: gr.SelectData):
|
| 829 |
items, _ = load_moma_items()
|
| 830 |
index = evt.index if isinstance(evt.index, int) else 0
|
| 831 |
index = max(0, min(index, len(items) - 1))
|
| 832 |
-
return tokenize_image(items[index]["img"], grid_size, patch_size, codebook_size, iterations, seed, codebook_source)
|
| 833 |
|
| 834 |
|
| 835 |
def probability_rows(record):
|
|
@@ -1073,6 +1231,11 @@ def build_app():
|
|
| 1073 |
)
|
| 1074 |
tokenizer_upload = gr.Image(label="Upload image", type="pil", sources=["upload", "clipboard"])
|
| 1075 |
with gr.Accordion("Tokenizer settings", open=True):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
with gr.Row():
|
| 1077 |
tokenizer_grid_size = gr.Slider(6, 28, value=16, step=1, label="Token grid")
|
| 1078 |
tokenizer_patch_size = gr.Slider(6, 24, value=14, step=1, label="Patch pixels")
|
|
@@ -1264,6 +1427,7 @@ def build_app():
|
|
| 1264 |
tokenizer_codebook_size,
|
| 1265 |
tokenizer_iterations,
|
| 1266 |
tokenizer_seed,
|
|
|
|
| 1267 |
tokenizer_codebook_source,
|
| 1268 |
]
|
| 1269 |
tokenizer_outputs = [
|
|
@@ -1291,6 +1455,7 @@ def build_app():
|
|
| 1291 |
tokenizer_codebook_size,
|
| 1292 |
tokenizer_iterations,
|
| 1293 |
tokenizer_seed,
|
|
|
|
| 1294 |
tokenizer_codebook_source,
|
| 1295 |
],
|
| 1296 |
outputs=tokenizer_outputs,
|
|
@@ -1305,6 +1470,7 @@ def build_app():
|
|
| 1305 |
tokenizer_codebook_size,
|
| 1306 |
tokenizer_iterations,
|
| 1307 |
tokenizer_seed,
|
|
|
|
| 1308 |
tokenizer_codebook_source,
|
| 1309 |
],
|
| 1310 |
outputs=tokenizer_outputs,
|
|
|
|
| 7 |
from io import BytesIO
|
| 8 |
|
| 9 |
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
| 10 |
+
os.environ.setdefault("USE_TF", "0")
|
| 11 |
+
os.environ.setdefault("TRANSFORMERS_NO_TF", "1")
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
import numpy as np
|
| 15 |
import requests
|
| 16 |
+
import torch
|
| 17 |
from PIL import Image, ImageDraw, ImageFont
|
| 18 |
|
| 19 |
try:
|
|
|
|
| 45 |
}
|
| 46 |
MOMA_SAMPLE_SIZE = 24
|
| 47 |
MOMA_CANDIDATES = 120
|
| 48 |
+
DEFAULT_VQ_MODEL = "CompVis/ldm-celebahq-256"
|
| 49 |
+
DEFAULT_VQ_SUBFOLDER = "vqvae"
|
| 50 |
|
| 51 |
TOKENS = [
|
| 52 |
{"id": "sky", "label": "Sky", "base": (91, 169, 230), "initial": "S"},
|
|
|
|
| 260 |
return int(rng.integers(0, MAX_SEED))
|
| 261 |
|
| 262 |
|
| 263 |
+
def current_device():
|
| 264 |
+
if torch.cuda.is_available():
|
| 265 |
+
return torch.device("cuda")
|
| 266 |
+
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
| 267 |
+
return torch.device("mps")
|
| 268 |
+
return torch.device("cpu")
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@lru_cache(maxsize=1)
|
| 272 |
+
def load_vq_tokenizer(model_id=DEFAULT_VQ_MODEL, subfolder=DEFAULT_VQ_SUBFOLDER):
|
| 273 |
+
from diffusers import VQModel
|
| 274 |
+
|
| 275 |
+
device = current_device()
|
| 276 |
+
model = VQModel.from_pretrained(model_id, subfolder=subfolder, use_safetensors=False)
|
| 277 |
+
model.to(device)
|
| 278 |
+
model.eval()
|
| 279 |
+
for param in model.parameters():
|
| 280 |
+
param.requires_grad_(False)
|
| 281 |
+
return model, device
|
| 282 |
+
|
| 283 |
+
|
| 284 |
def prompt_bias(prompt):
|
| 285 |
prompt = (prompt or "").lower()
|
| 286 |
bias = np.zeros(TOKEN_COUNT, dtype=np.float32)
|
|
|
|
| 801 |
return rows
|
| 802 |
|
| 803 |
|
| 804 |
+
def prepare_vq_image(image, size=256):
|
| 805 |
+
return center_crop_square(image).resize((size, size), Image.Resampling.BICUBIC)
|
| 806 |
+
|
| 807 |
+
|
| 808 |
+
def pil_to_vq_tensor(image, device):
|
| 809 |
+
arr = np.asarray(image).astype(np.float32) / 255.0
|
| 810 |
+
arr = arr * 2.0 - 1.0
|
| 811 |
+
tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).to(device=device, dtype=torch.float32)
|
| 812 |
+
return tensor
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def vq_tensor_to_pil(tensor):
|
| 816 |
+
arr = tensor.detach().float().cpu().clamp(-1, 1).squeeze(0).permute(1, 2, 0).numpy()
|
| 817 |
+
arr = ((arr + 1.0) * 127.5).round().clip(0, 255).astype(np.uint8)
|
| 818 |
+
return Image.fromarray(arr, mode="RGB")
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
def draw_learned_token_grid(indices, height, width, cell=None):
|
| 822 |
+
if cell is None:
|
| 823 |
+
cell = max(8, min(18, 720 // max(1, width)))
|
| 824 |
+
grid = indices.reshape(height, width)
|
| 825 |
+
unique = sorted(int(value) for value in np.unique(grid))
|
| 826 |
+
palette = {}
|
| 827 |
+
for order, token_id_value in enumerate(unique):
|
| 828 |
+
hue = order / max(1, len(unique))
|
| 829 |
+
r = int(80 + 135 * abs(math.sin(math.tau * hue)))
|
| 830 |
+
g = int(80 + 135 * abs(math.sin(math.tau * (hue + 0.33))))
|
| 831 |
+
b = int(80 + 135 * abs(math.sin(math.tau * (hue + 0.66))))
|
| 832 |
+
palette[token_id_value] = (r, g, b)
|
| 833 |
+
|
| 834 |
+
image = Image.new("RGB", (width * cell, height * cell), (31, 36, 44))
|
| 835 |
+
draw = ImageDraw.Draw(image)
|
| 836 |
+
font = ImageFont.load_default()
|
| 837 |
+
for row in range(height):
|
| 838 |
+
for col in range(width):
|
| 839 |
+
token_id_value = int(grid[row, col])
|
| 840 |
+
x0 = col * cell
|
| 841 |
+
y0 = row * cell
|
| 842 |
+
draw.rectangle((x0, y0, x0 + cell, y0 + cell), fill=palette[token_id_value], outline=(24, 28, 34))
|
| 843 |
+
if cell >= 18:
|
| 844 |
+
text = str(token_id_value % 100)
|
| 845 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 846 |
+
draw.text(
|
| 847 |
+
(x0 + (cell - (bbox[2] - bbox[0])) / 2, y0 + (cell - (bbox[3] - bbox[1])) / 2),
|
| 848 |
+
text,
|
| 849 |
+
fill=(10, 14, 20),
|
| 850 |
+
font=font,
|
| 851 |
+
)
|
| 852 |
+
return image
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def learned_vq_gallery(image, indices, latent_height, latent_width, distances, max_items=48):
|
| 856 |
+
grid = indices.reshape(latent_height, latent_width)
|
| 857 |
+
counts = np.bincount(indices)
|
| 858 |
+
used_ids = [int(index) for index in np.argsort(counts)[::-1] if counts[index] > 0][:max_items]
|
| 859 |
+
cell_w = image.width / latent_width
|
| 860 |
+
cell_h = image.height / latent_height
|
| 861 |
+
gallery = []
|
| 862 |
+
|
| 863 |
+
for token_id_value in used_ids:
|
| 864 |
+
positions = np.argwhere(grid == token_id_value)
|
| 865 |
+
flat_positions = positions[:, 0] * latent_width + positions[:, 1]
|
| 866 |
+
best_flat = int(flat_positions[np.argmin(distances[flat_positions])])
|
| 867 |
+
row = best_flat // latent_width
|
| 868 |
+
col = best_flat % latent_width
|
| 869 |
+
left = int(round(col * cell_w))
|
| 870 |
+
top = int(round(row * cell_h))
|
| 871 |
+
right = int(round((col + 1) * cell_w))
|
| 872 |
+
bottom = int(round((row + 1) * cell_h))
|
| 873 |
+
crop = image.crop((left, top, right, bottom)).resize((96, 96), Image.Resampling.NEAREST)
|
| 874 |
+
gallery.append((crop, f"Token {token_id_value}\nused {int(counts[token_id_value])} positions"))
|
| 875 |
+
return gallery
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def learned_vq_rows(indices, distances):
|
| 879 |
+
counts = np.bincount(indices)
|
| 880 |
+
total = max(1, int(counts.sum()))
|
| 881 |
+
rows = []
|
| 882 |
+
for token_id_value in np.argsort(counts)[::-1]:
|
| 883 |
+
count = int(counts[token_id_value])
|
| 884 |
+
if count == 0:
|
| 885 |
+
continue
|
| 886 |
+
members = distances[indices == token_id_value]
|
| 887 |
+
rows.append(
|
| 888 |
+
[
|
| 889 |
+
f"Token {int(token_id_value)}",
|
| 890 |
+
count,
|
| 891 |
+
round(count / total, 3),
|
| 892 |
+
"learned VQ embedding",
|
| 893 |
+
round(float(members.mean()) if len(members) else 0.0, 6),
|
| 894 |
+
]
|
| 895 |
+
)
|
| 896 |
+
return rows
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
def learned_vq_tokenize(image, model_id=DEFAULT_VQ_MODEL, subfolder=DEFAULT_VQ_SUBFOLDER):
|
| 900 |
+
model, device = load_vq_tokenizer(model_id, subfolder)
|
| 901 |
+
sample_size = int(getattr(model.config, "sample_size", 256) or 256)
|
| 902 |
+
prepared = prepare_vq_image(image, sample_size)
|
| 903 |
+
tensor = pil_to_vq_tensor(prepared, device)
|
| 904 |
+
|
| 905 |
+
with torch.inference_mode():
|
| 906 |
+
latents = model.encode(tensor).latents
|
| 907 |
+
quantized, _, info = model.quantize(latents)
|
| 908 |
+
indices = info[2].detach().cpu().numpy().astype(np.int64).reshape(-1)
|
| 909 |
+
reconstruction = model.decode(latents).sample
|
| 910 |
+
|
| 911 |
+
z = latents.permute(0, 2, 3, 1).contiguous().view(-1, model.quantize.vq_embed_dim)
|
| 912 |
+
embeddings = model.quantize.embedding.weight
|
| 913 |
+
chosen = embeddings[torch.from_numpy(indices).to(device)]
|
| 914 |
+
distances = torch.mean((z - chosen) ** 2, dim=1).detach().cpu().numpy()
|
| 915 |
+
|
| 916 |
+
latent_height, latent_width = int(latents.shape[2]), int(latents.shape[3])
|
| 917 |
+
learned_cell = max(8, min(18, 720 // max(1, latent_width)))
|
| 918 |
+
token_grid = draw_learned_token_grid(indices, latent_height, latent_width, cell=learned_cell)
|
| 919 |
+
reconstruction_image = vq_tensor_to_pil(reconstruction)
|
| 920 |
+
error_map = draw_error_heatmap(distances, latent_height, cell=learned_cell)
|
| 921 |
+
gallery = learned_vq_gallery(prepared, indices, latent_height, latent_width, distances)
|
| 922 |
+
rows = learned_vq_rows(indices, distances)
|
| 923 |
+
summary = (
|
| 924 |
+
f"Encoded the image with the pretrained learned tokenizer `{model_id}/{subfolder}`.\n"
|
| 925 |
+
f"The VQ model produced a {latent_height}x{latent_width} grid: {latent_height * latent_width} learned token IDs. "
|
| 926 |
+
f"It used {len(rows)} unique codebook entries from a vocabulary of {int(model.config.num_vq_embeddings)}.\n"
|
| 927 |
+
f"Mean latent-to-codebook distance: {float(distances.mean()):.6f}. "
|
| 928 |
+
"The gallery shows representative image regions for the most-used learned token IDs."
|
| 929 |
+
)
|
| 930 |
+
return prepared, token_grid, reconstruction_image, error_map, gallery, rows, summary
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
@spaces.GPU(duration=120)
|
| 934 |
+
def tokenize_image(image, grid_size, patch_size, codebook_size, iterations, seed, tokenizer_method, codebook_source):
|
| 935 |
+
if tokenizer_method == "Learned VQ tokenizer":
|
| 936 |
+
return learned_vq_tokenize(image)
|
| 937 |
+
|
| 938 |
grid_size = max(4, min(32, int(grid_size)))
|
| 939 |
patch_size = max(4, min(32, int(patch_size)))
|
| 940 |
codebook_size = max(2, min(64, int(codebook_size)))
|
|
|
|
| 976 |
return prepared, token_grid, reconstruction, error_map, gallery, rows, summary
|
| 977 |
|
| 978 |
|
| 979 |
+
def initial_tokenizer(grid_size, patch_size, codebook_size, iterations, seed, tokenizer_method, codebook_source):
|
| 980 |
items, message = load_moma_items()
|
| 981 |
+
outputs = tokenize_image(items[0]["img"], grid_size, patch_size, codebook_size, iterations, seed, tokenizer_method, codebook_source)
|
| 982 |
summary = f"{message}\n\n{outputs[-1]}"
|
| 983 |
return (moma_gallery_items(),) + outputs[:-1] + (summary,)
|
| 984 |
|
| 985 |
|
| 986 |
+
def tokenize_moma_selection(grid_size, patch_size, codebook_size, iterations, seed, tokenizer_method, codebook_source, evt: gr.SelectData):
|
| 987 |
items, _ = load_moma_items()
|
| 988 |
index = evt.index if isinstance(evt.index, int) else 0
|
| 989 |
index = max(0, min(index, len(items) - 1))
|
| 990 |
+
return tokenize_image(items[index]["img"], grid_size, patch_size, codebook_size, iterations, seed, tokenizer_method, codebook_source)
|
| 991 |
|
| 992 |
|
| 993 |
def probability_rows(record):
|
|
|
|
| 1231 |
)
|
| 1232 |
tokenizer_upload = gr.Image(label="Upload image", type="pil", sources=["upload", "clipboard"])
|
| 1233 |
with gr.Accordion("Tokenizer settings", open=True):
|
| 1234 |
+
tokenizer_method = gr.Radio(
|
| 1235 |
+
["K-means patch tokenizer", "Learned VQ tokenizer"],
|
| 1236 |
+
value="Learned VQ tokenizer",
|
| 1237 |
+
label="Tokenizer",
|
| 1238 |
+
)
|
| 1239 |
with gr.Row():
|
| 1240 |
tokenizer_grid_size = gr.Slider(6, 28, value=16, step=1, label="Token grid")
|
| 1241 |
tokenizer_patch_size = gr.Slider(6, 24, value=14, step=1, label="Patch pixels")
|
|
|
|
| 1427 |
tokenizer_codebook_size,
|
| 1428 |
tokenizer_iterations,
|
| 1429 |
tokenizer_seed,
|
| 1430 |
+
tokenizer_method,
|
| 1431 |
tokenizer_codebook_source,
|
| 1432 |
]
|
| 1433 |
tokenizer_outputs = [
|
|
|
|
| 1455 |
tokenizer_codebook_size,
|
| 1456 |
tokenizer_iterations,
|
| 1457 |
tokenizer_seed,
|
| 1458 |
+
tokenizer_method,
|
| 1459 |
tokenizer_codebook_source,
|
| 1460 |
],
|
| 1461 |
outputs=tokenizer_outputs,
|
|
|
|
| 1470 |
tokenizer_codebook_size,
|
| 1471 |
tokenizer_iterations,
|
| 1472 |
tokenizer_seed,
|
| 1473 |
+
tokenizer_method,
|
| 1474 |
tokenizer_codebook_source,
|
| 1475 |
],
|
| 1476 |
outputs=tokenizer_outputs,
|
requirements.txt
CHANGED
|
@@ -1,5 +1,10 @@
|
|
| 1 |
gradio>=5.22.0
|
| 2 |
spaces
|
| 3 |
-
numpy
|
| 4 |
pillow
|
| 5 |
requests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
gradio>=5.22.0
|
| 2 |
spaces
|
| 3 |
+
numpy<2
|
| 4 |
pillow
|
| 5 |
requests
|
| 6 |
+
torch>=2.8.0
|
| 7 |
+
diffusers>=0.35.0
|
| 8 |
+
accelerate
|
| 9 |
+
safetensors
|
| 10 |
+
huggingface_hub>=0.34.0,<1.0
|