Source code for mixle.represent.quantize
"""``VectorQuantizer`` -- learn a discrete vocabulary IN the shared embedding space, don't guess it upstream.
Discrete tokens, when you want them (compression, transfer, a fixed vocabulary), come *after* embedding, not
before segmentation: fit a codebook to the continuous vectors and each vector's nearest code is its token id. The
codebook is a *learned* model (k-means / a mixture), so the vocabulary is inferred from data rather than assumed
-- and because every modality is embedded into the same space, one codebook is a **cross-modal vocabulary**
(an image patch and a word can share a token id when they land near the same centroid).
``fit``/``quantize``/``dequantize`` are the codec; ``straight_through`` gives the VQ-VAE gradient so the codebook
and the encoders can be trained end to end under a generative or downstream objective. This is the *only* place
discreteness lives -- the segmenter and embedding stay vocabulary-free.
"""
from __future__ import annotations
from typing import Any
import numpy as np
[docs]
class VectorQuantizer:
"""A learned codebook over ``R^dim``: nearest-centroid quantization of embedding vectors into discrete ids."""
def __init__(self, num_codes: int, dim: int, *, seed: int = 0) -> None:
self.num_codes = int(num_codes)
self.dim = int(dim)
self.seed = int(seed)
self.codebook: np.ndarray | None = None # (num_codes, dim)
[docs]
def fit(self, vectors: np.ndarray, *, iters: int = 25) -> VectorQuantizer:
"""Fit the codebook by k-means (Lloyd) on ``vectors`` ``(n, dim)`` -- the vocabulary is learned, not assumed."""
x = np.asarray(vectors, dtype=np.float64)
rng = np.random.RandomState(self.seed)
k = min(self.num_codes, len(x))
centers = x[rng.choice(len(x), size=k, replace=False)].copy()
for _ in range(int(iters)):
ids = self._assign(x, centers)
new = np.stack([x[ids == j].mean(axis=0) if np.any(ids == j) else centers[j] for j in range(len(centers))])
if np.allclose(new, centers):
centers = new
break
centers = new
self.codebook = centers
return self
@staticmethod
def _assign(x: np.ndarray, centers: np.ndarray) -> np.ndarray:
# ||x - c||^2 = ||x||^2 - 2 x·c + ||c||^2 ; the data terms drop out of the argmin
d = -2.0 * x @ centers.T + np.sum(centers**2, axis=1)[None, :]
return d.argmin(axis=1)
[docs]
def quantize(self, vectors: np.ndarray) -> np.ndarray:
"""Nearest-code id for each vector -- the discrete token stream ``(n,)``."""
if self.codebook is None:
raise RuntimeError("call fit(...) before quantize(...)")
return self._assign(np.asarray(vectors, dtype=np.float64), self.codebook)
[docs]
def dequantize(self, ids: np.ndarray) -> np.ndarray:
"""Codebook vectors for token ids ``(n,)`` -> ``(n, dim)`` (the reconstruction / de-tokenization)."""
if self.codebook is None:
raise RuntimeError("call fit(...) before dequantize(...)")
return self.codebook[np.asarray(ids, dtype=np.int64)]
[docs]
def reconstruction_error(self, vectors: np.ndarray) -> float:
"""Mean squared quantization error -- the codebook's fidelity (a codebook-size / bitrate knob)."""
v = np.asarray(vectors, dtype=np.float64)
return float(np.mean(np.sum((v - self.dequantize(self.quantize(v))) ** 2, axis=1)))
[docs]
def straight_through(self, vectors: Any) -> Any:
"""VQ-VAE straight-through estimator: return quantized vectors but pass gradients to ``vectors`` unchanged.
Lets the encoders and (with a codebook-commitment loss) the codebook train end to end through the discrete
bottleneck. ``vectors`` is a torch tensor ``(n, dim)``.
"""
import torch
if self.codebook is None:
raise RuntimeError("call fit(...) before straight_through(...)")
cb = torch.as_tensor(self.codebook, dtype=vectors.dtype, device=vectors.device)
d = torch.cdist(vectors, cb)
ids = d.argmin(dim=1)
q = cb[ids]
return vectors + (q - vectors).detach() # identity in the backward pass