One codebook, and the wall
If you had to compress vectors with no prior reading, you would probably invent this. Here is exactly why it cannot reach 16 bytes.
1Replace each vector by its nearest representative
Pick representative vectors, the codebook . Replace each data point by the index of its nearest representative.
- Store the index, the code: bits.
- Decode by looking the index up.
- Train with k-means, which finds the representatives that minimise squared error.
This is vector quantization (VQ).
Slide the bits Each extra bit doubles . On the right, error on a log scale. In two dimensions each bit roughly halves the error once is large; the dashed line is that slope.
def nearest(X, C):
# ||x-c||² = ||x||² - 2x·c + ||c||²; drop ||x||², it doesn't move the argmin
return (-2 * X @ C.T + (C ** 2).sum(1)).argmin(1)
codes = nearest(X, C) # encode: log2(K) bits per row
X_hat = C[codes] # decode: a table lookup
2The wall: bits per dimension
The right-hand plot hides a law. With a good codebook, distortion falls exponentially with the rate in bits per vector, but the exponent is divided by the dimension:
In two dimensions, one bit halves the error, which is what you saw. Now put in Marginalia’s :
- one bit shrinks the error by ;
- getting anywhere useful takes dozens of bits per vector;
- dozens of bits means or more.
A codebook that size:
- cannot be stored (18 quintillion vectors);
- cannot be trained (k-means needs many more points than centroids);
- cannot be searched when encoding (finding the nearest of entries is the very problem we were trying to make cheap).
Can we have a gigantic codebook without storing it, and still find good entries in it?
The question that drives the rest of this part.