Leeps commited on
Commit
ed8852d
·
1 Parent(s): 85db9e9

Add learned VQ tokenizer option

Browse files
Files changed (3) hide show
  1. README.md +6 -1
  2. app.py +172 -6
  3. 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, splits it into patches, learns a tiny vector-quantized codebook from those patches, and shows the resulting token ID grid and reconstruction. It can use MoMA collection images or an uploaded 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
- @spaces.GPU(duration=30)
779
- def tokenize_image(image, grid_size, patch_size, codebook_size, iterations, seed, codebook_source):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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