The main application of tensor factorizations is compression: images and video, or the parameter tensors inside a neural net — a VGG conv kernel, a transformer dense map.
NumPy for CP, Tucker, and truncated HOSVD is first. Then the two compressions, then an unmixing cube: flattening loses the dyes.
1 Tensor data
Film clip. Five seconds of the 1962 Lawrence of Arabia theatrical trailer (public domain, Wikimedia Commons). Colour, picture and sound. Cut at 00:00:55 (train explosion, then the cut that follows). Files: media/clip.mp4, still.png, frames.npy, clip.wav.
Conv kernel. Synthetic \(3\times 3\times 64\times 64\) CP rank-16 weight plus noise, seed 7. Stands in for one VGG-16 conv5 layer. The 512-channel counts are closed-form, not a downloaded VGG.
Dense map. Synthetic \(256\times 256\) TT-rank-4 matrix plus noise. Stands in for a \(4096\times 4096\) transformer projection. The \(4096\) counts use the same formula.
Mixing cube. Synthetic \(20\times 24\times 18\) table, three dyes plus noise: sample × emission colour × excitation colour. Seed 7. Stands in for a fluorescence excitation–emission stack.
Figures and the slider read widget-data/curves.json, written from the same seed.
This section uses a synthetic \(8\times 7\times 6\) array.
Rank-2 CP: two outer products of Gaussian bumps, plus i.i.d. Gaussian noise at \(0.08\) times the clean scale, seed 7.
Stands in for a small 3-way table (sample × feature × condition). The size is so the factors fit on one screen.
Objective: recover the two components. Report \(\|X-\hat X\|_F/\|X\|_F\).
A flattened SVD mixes the two bumps. An overspecified CP rank invents a third. Either error mis-assigns which condition drives which feature.
Tensor methods fit because Kruskal uniqueness (CP) and per-mode truncation (HOSVD / Tucker) are properties of this layout, not of any matrix obtained from it.
Mode-\(n\) unfolding \(X_{(n)}\) puts mode \(n\) on the rows. SVD of that matrix is Eckart–Young for the unfolding, not for \(X\). Mode-\(n\) product \(X\times_n M\) is \(M\) times that unfolding, folded back.
Code
def unfold(X, n):return np.moveaxis(X, n, 0).reshape(X.shape[n], -1)def fold(mat, n, shape): full = [shape[n]] + [s for i, s inenumerate(shape) if i != n]return np.moveaxis(mat.reshape(full), 0, n)def mode_prod(X, M, n): shape =list(X.shape) shape[n] = M.shape[0]return fold(M @ unfold(X, n), n, tuple(shape))
2.3 Truncated HOSVD
Higher-order SVD (De Lathauwer, De Moor, Vandewalle 2000): one truncated SVD per unfolding; the core is \(X\) projected onto those bases. The three truncations are not jointly optimal for the Tucker loss.
Code
def truncated_hosvd(X, ranks): factors = []for n, r inenumerate(ranks): u, _, _ = np.linalg.svd(unfold(X, n), full_matrices=False) factors.append(u[:, :r]) core = Xfor n, U inenumerate(factors): core = mode_prod(core, U.T, n) recon = corefor n, U inenumerate(factors): recon = mode_prod(recon, U, n)return recon, core, factorsdef hosvd_approx(tensor, ranks): recon, _, _ = truncated_hosvd(tensor, ranks) n_params =int( np.prod(ranks) +sum(s * r for s, r inzip(tensor.shape, ranks)) ) err =float(np.linalg.norm(tensor - recon) / np.linalg.norm(tensor))return recon, n_params, errX_hosvd, G_hosvd, _ = truncated_hosvd(X, (2, 2, 2))print("HOSVD core", G_hosvd.shape)print(f"HOSVD relative error {T.rel_fro(X, X_hosvd):.4f}")
HOSVD core (2, 2, 2)
HOSVD relative error 0.0619
2.4 Tucker
HOOI starts from that HOSVD and cycles modes: contract the others, replace the factor by the leading left singular vectors (Tucker 1966). TensorLy’s tucker(..., init="svd") is this pipeline.
Code
def tucker_als(X, ranks, n_iter=15): _, _, factors = truncated_hosvd(X, ranks)for _ inrange(n_iter):for n, r inenumerate(ranks): Y = Xfor m, U inenumerate(factors):if m != n: Y = mode_prod(Y, U.T, m) u, _, _ = np.linalg.svd(unfold(Y, n), full_matrices=False) factors[n] = u[:, :r] core = Xfor n, U inenumerate(factors): core = mode_prod(core, U.T, n) recon = corefor n, U inenumerate(factors): recon = mode_prod(recon, U, n)return recon, core, factorsX_tucker, G_tucker, _ = tucker_als(X, (2, 2, 2))print("Tucker core", G_tucker.shape)print(f"Tucker relative error {T.rel_fro(X, X_tucker):.4f}")
CANDECOMP/PARAFAC writes \(X\) as a sum of rank-1 outer products. ALS: fix every factor but one, solve a Khatri–Rao least-squares problem, cycle (Harshman 1970; Kolda and Bader 2009). CP rank can exceed a mode size; extra columns are random if the unfolding SVD runs out of vectors.
Code
def khatri_rao_except(factors, skip): mats = [F for i, F inenumerate(factors) if i != skip] out = mats[0]for F in mats[1:]: out = np.einsum("ir,jr->ijr", out, F).reshape(-1, out.shape[1])return outdef cp_reconstruct(factors): subs =",".join(f"{chr(105+ n)}r"for n inrange(len(factors))) out ="".join(chr(105+ n) for n inrange(len(factors)))return np.einsum(f"{subs}->{out}", *factors)def cp_als(X, rank, n_iter=50, seed=7): rng = np.random.default_rng(seed) factors = []for n inrange(X.ndim): u, _, _ = np.linalg.svd(unfold(X, n), full_matrices=False) U = rng.normal(size=(X.shape[n], rank)) keep =min(rank, u.shape[1]) U[:, :keep] = u[:, :keep] factors.append(U)for _ inrange(n_iter):for n inrange(X.ndim): gram = np.ones((rank, rank))for m, F inenumerate(factors):if m != n: gram *= F.T @ F factors[n] = ( unfold(X, n) @ khatri_rao_except(factors, n) @ np.linalg.pinv(gram) )for n inrange(X.ndim -1): scale = np.linalg.norm(factors[n], axis=0, keepdims=True) +1e-12 factors[n] /= scale factors[-1] *= scalereturn factorscp_factors = cp_als(X, rank=2, n_iter=40)X_cp = cp_reconstruct(cp_factors)print("CP factor shapes", [F.shape for F in cp_factors])print(f"CP relative error {T.rel_fro(X, X_cp):.4f}")
Noise was added at \(0.08\) times the clean scale. A residual near \(0.06\) is that noise, not a missed component. The three fits land on the same floor because the cube is rank-2 CP.
Figure 1: One slice of the \(8\times 7\times 6\) cube (third mode, index 3). Left to right: data, truncated HOSVD, Tucker (HOOI), CP-ALS. Rank \((2,2,2)\) / CP rank \(2\).
3 CP convolution
VGG-16 (Simonyan and Zisserman 2015) uses \(3\times 3\) convolutions throughout. After the fourth pool the feature map is \(14\times 14\). Each of conv5_1, conv5_2, conv5_3 maps 512 channels to 512 channels.
Before. That kernel is a 4-way array \(3\times 3\times 512\times 512\): one \(3\times 3\) patch for every input–output pair. Storage \(3\cdot 3\cdot 512\cdot 512=2{,}359{,}296\) weights. Every spatial site on the \(14\times 14\) map pays that cost, so one forward pass is \(9\cdot 512\cdot 512\cdot 14\cdot 14\approx 462\) million multiply-adds.
What CP changes. Canonical polyadic decomposition writes that 4-D stack as a short sum of separable pieces. In the network that becomes four skinny convolutions in a row (Lebedev et al. 2015):
\(1\times 1\) squeeze: 512 channels down to \(R\).
Depthwise \(3\times 1\): smear vertically, one channel at a time.
Depthwise \(1\times 3\): smear horizontally.
\(1\times 1\) expand: \(R\) channels back to 512.
Same \(3\times 3\) receptive field as VGG. Far fewer weights. Too small an \(R\) mixes input channels that should stay separate — that shows up as relative error, not as a storage bug. The usual workflow is compress a trained net, then fine-tune.
After. Rank \(64\) stores \(65{,}920\) weights — \(35.8\times\) fewer. Multiply-adds drop by the same factor, because the spatial size of the map cancels. Formula: \(R(2d+C_{\mathrm{in}}+C_{\mathrm{out}})\) against \(d^{2}C_{\mathrm{in}}C_{\mathrm{out}}\).
Storage is exact. Fit quality is relative error \(\|W-\hat W\|_F/\|W\|_F\): the leftover fraction of the kernel. A trained VGG layer is not exact CP, so some relative error remains even at rank \(64\); that is why Lebedev et al. fine-tune.
The 512-channel drop is a closed-form count (no VGG is fitted). Toy relative error on a \(3\times 3\times 64\times 64\) kernel (true rank 16, noise \(0.08\)): rank 4 is \(0.62\); rank 16 is \(0.077\) (the noise); rank 64 is \(0.066\). Rank 16 on the toy stores \(2{,}144\) weights instead of \(36{,}864\) (\(17.2\times\)).
A transformer block at model dimension \(4096\) stores square maps of that width: the output projection of multi-head attention \(W_O\in\mathbb{R}^{4096\times 4096}\) (Vaswani et al. 2017). Novikov et al. (2015) write a dense matrix of this kind as a tensor-train.
Before. Mapping 4096 numbers to 4096 numbers stores \(4096^{2}=16{,}777{,}216\) weights. Multiplying a residual-stream vector by \(W_O\) costs \(O(N^{2})\).
What a TT-matrix changes. Factor \(4096=8\times 8\times 8\times 8\). Fold the rectangle into a higher-order array and write it as a chain of small cores \(G_k\in\mathbb{R}^{r_{k-1}\times 8\times 8\times r_k}\) (Novikov et al. 2015; Oseledets 2011). Multiplying a vector is a sweep along that chain, not one huge matmul. Storage at equal mode size \(n\) and internal rank \(r\) is \(O(d n^{2} r^{2})\), not \(O(N^{2})\).
After. Rank \(16\) stores \(34{,}816\) weights — \(481.9\times\) fewer. Apply cost drops to \(O(d r^{2} n N)\). Ordinary SVD of the unfolded rectangle cannot see that chain: a map that is low-rank after folding still looks high-rank as a matrix, so its relative error stays large.
The 4096-wide drop is closed-form (no transformer is fitted). Figure 2 is both layers at those ranks. Toy relative error on a \(256\times 256\) map built at TT-rank 4 plus noise \(0.08\): TT-rank 4 is \(0.079\) on \(640\) weights; SVD rank 4 is \(0.932\) on \(2{,}052\) weights; SVD rank 64 is \(0.436\) on \(32{,}832\) weights.
Code
ms = ns = [T.TT_MODE] * T.TT_ORDERM, _ = T.make_tt_matrix(np.random.default_rng(T.SEED), ms, ns)cores = T.tt_matrix_svd(M, ms, ns, max_rank=4)M_tt = T.tt_matrix_to_dense(cores)print("matrix", M.shape, "TT cores", [c.shape for c in cores])print(f"TT-rank 4 relative error {T.rel_fro(M, M_tt):.3f}")
Figure 2: Closed-form storage at the ranks in the two sections. Left: weights, dense vs kept. Middle: compression (dense / kept). Right: fraction of the dense layer that remains. No network is fitted.
The slider below varies rank. Relative error there is the toys.
Seven ranks — weights and relative errorruns in the browser
Seven ranks. Defaults are the ranks the toys were built at (CP 16, TT 4) — the gold band on the error plot. Weight bars are VGG-16 conv5 / transformer \(W_O\) closed-form counts, not the toys.
5 Complexity
The table answers two questions: how many numbers you store, and how much arithmetic one forward pass costs. \(H,W\) are the spatial size of a feature map. \(N=n^{d}\) is one side of a square TT-matrix with \(d\) equal modes. Apply is the thing you run at inference — a convolution, or \(Wx\).
Method
Storage
Apply
Dense conv
\(O(d^{2}C_{\mathrm{in}}C_{\mathrm{out}})\)
\(O(d^{2}C_{\mathrm{in}}C_{\mathrm{out}}HW)\)
CP-conv rank \(R\)
\(O(R(2d+C_{\mathrm{in}}+C_{\mathrm{out}}))\)
\(O(R(C_{\mathrm{in}}+2d+C_{\mathrm{out}})HW)\)
Dense \(M\times N\)
\(O(MN)\)
\(O(MN)\)
SVD rank \(k\)
\(O(k(M+N))\)
\(O(k(M+N))\)
TT-matrix rank \(r\), \(d\) modes of size \(n\)
\(O(d n^{2} r^{2})\)
\(O(d r^{2} n N)\)
The VGG-16 conv5 counts and the \(4096\) transformer map are the closed-form counts from the two sections above. The clock below is NumPy on the toy 64-channel kernel and a \(16\times 16\) map — not a cuDNN GEMM. Factorized is still fewer multiply-adds; wall-clock can go the other way on a GPU once the chain of small contractions becomes memory-bound.
A pixel is three numbers: red, green, blue. Height and width stack those triples into an image. Colour is a third mode of the array; time is a fourth. The clip is those four modes in one tensor.
Figure 3: Pixel triples stacked into tensor containers. Height and width make an image; colour is a third mode; time stacks frames into a 4-tensor.
The source clip is in Tensor data: 5 s, picture and sound. Truncated HOSVD (one SVD per mode, no HOOI) compresses the still, the RGB video, and the soundtrack STFT. Ranks target relative error about \(0.05\). Reconstructed picture and sound are muxed into media/clip-hosvd.mp4. It is the same truncated_hosvd as in Factorizations.
still (240, 320, 3) → core (20, 28, 3)
still relative error 0.049
5.2 Video
Still \(240\times 320\times 3\), ranks \((20,28,3)\). Clip \(120\times 160\times 3\times 60\), ranks \((60,80,3,30)\). Time is its own mode, so motion is not smeared into space.
Figure 6: Three frames from the 5 s clip. Top: original. Bottom: compressed. Time is kept as its own axis, so motion is not smeared into space.
5.3 Audio
A microphone stores air pressure as one number per sample. That list is a 1-tensor: one mode, time. Factorizing it is a 1-D SVD — there is no second mode to separate.
The short-time Fourier transform (STFT) cuts the list into overlapping windows and writes each window as a spectrum. Stack the spectra along time. The array is frequency \(\times\) time, a 2-tensor — the same layout as a greyscale image. Each bin is complex, so split real and imaginary into a third mode. Frequency \(\times\) time \(\times\) {real, imag} is a 3-tensor, the same layout as an RGB still.
This clip: 5 s mono at 8 kHz. Window \(n=256\), hop \(128\). Truncated HOSVD ranks \((88,140,2)\).
samples — each cell is one pressure number.
1-tensor — the waveform. Overlapping windows are the STFT cuts.
2-tensor — those spectra stacked: frequency \(\times\) time.
3-tensor — add {real, imag}, the way colour is added to an image.
Figure 7: Pressure samples stacked into tensor containers. The waveform is a 1-tensor. The STFT makes a 2-tensor (frequency × time). Real and imaginary parts are a third mode, as colour is for an image.
Figure 8: Soundtrack STFT after HOSVD. Left: numbers stored, dense vs kept. Middle: compression (dense / kept). Right: relative error on the STFT tensor, the complex STFT, and the waveform.
/var/folders/p9/vwq0gfs15vb07tg6xw1r14180000gn/T/ipykernel_12694/2602618714.py:22: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
fig.tight_layout()
Figure 9: Original vs compressed. Top: STFT magnitude. Bottom: waveform after inverting the compressed STFT.
You want how much of each dye is in each well. The dyes are already mixed; you cannot pipette them apart.
The instrument shines one excitation colour into one well and records how bright that well is at one emission colour. That number is intensity. Sweep both colours and one well becomes a map (emission colour × excitation colour). Twenty wells become a stack of twenty maps.
That stack is a 3-way tensor \(\mathcal{X}\in\mathbb{R}^{20\times 24\times 18}\).
Sample (20). Which well.
Emission colour (24). Colour coming out.
Excitation colour (18). Colour shone in.
Intensity. The value in each cell, not a fourth axis.
Dye is not an axis of \(\mathcal{X}\). Each dye is one rank-1 tensor: amount × emission spectrum × excitation spectrum. The observed cube is the sum of three of those, plus noise.
The maps do not give the amounts. The three fingerprints overlap, so a bright spot is a mix. Flatten the two colour axes into one long row and the cube becomes a \(20\times 432\) matrix: SVD then returns mixed dyes. CP keeps the three axes and recovers the amounts.
This cube is synthetic (seed 7, noise \(0.08\)). It stands in for a fluorescence excitation–emission stack. No wet-lab data.
Figure 10 is the whole assay: settings in, one observation out, that tensor, and what the two splits return.
Figure 10: You want dye amounts; you measure intensity. Top, left to right: someone makes up twenty wells of mixed dye; three settings go in — which well, which excitation colour, which emission colour; the instrument returns one observation. Sweeping the two colour settings gives one map per well, and stacking the wells gives \(\mathcal{X}\in\mathbb{R}^{20\times 24\times 18}\), 8,640 readings. Middle: the three rank-1 dyes the cube is made of; their amounts overlap. Bottom: CP returns the dyes, flatten-then-SVD returns mixes. Drawn; the cube is synthetic, seed 7.
6.2 Recoveries
Figure 11 is CP versus flatten-then-SVD on that cube.
Figure 11: It opens on the instrument, then builds the cube dye by dye. Each sample is an emission × excitation map; the needle marks that sample. CP recovers the three dyes. Flatten-then-SVD returns mixed dyes.
cube (20, 24, 18)
CP relative error 0.071, mean |corr| 1.00
flatten-SVD mean |corr| 0.536
What you measure (the sweep). One map per sample. Bright spots move as the mix changes. The methods see only those maps.
CP. Write the cube as three outer products, one per dye. Amount correlation \(1.00\). Leftover error \(0.071\) is the noise that was added. Kruskal’s condition holds (\(k_A+k_B+k_C\ge 2R+2\) at rank \(3\)), so this split is unique up to renaming and scaling the dyes.
Flatten, then SVD. Stack each map into one long row. The cube becomes a \(20\times 432\) matrix. SVD finds three directions among the samples; they are mixes of the dyes (correlation \(0.536\)). Amounts go negative. The maps are not the dyes.
An unfolding SVD can reconstruct the matrix well and still not return the sources.
7 Constraints
GEMM vs contractions. One large matrix multiply is replaced by a chain of small tensor contractions. On GPUs that chain is often memory-bound; theoretical MAC drop is not wall-clock. cuTENSOR and TensorLy-Torch exist to close part of that gap (Kossaifi et al. 2019).
Lossy fit. Truncation discards higher-order mass. Lebedev et al. compress then fine-tune; a raw CP or TT drop-in lowers accuracy.
Rank search. Exact CP rank is NP-hard. ALS can split a component or stall. The synthetic residuals above flatten at the noise floor only because the generating rank is known.
Håstad, J. (1990). Tensor rank is NP-complete. Journal of Algorithms 11(4), 644–654.
Harshman, R. A. (1970). Foundations of the PARAFAC procedure. UCLA Working Papers in Phonetics 16, 1–84.
Tucker, L. R. (1966). Some mathematical notes on three-mode factor analysis. Psychometrika 31(3), 279–311.
De Lathauwer, L., De Moor, B., and Vandewalle, J. (2000). A multilinear singular value decomposition. SIAM Journal on Matrix Analysis and Applications 21(4), 1253–1278.
Kolda, T. G., and Bader, B. W. (2009). Tensor decompositions and applications. SIAM Review 51(3), 455–500.
Oseledets, I. V. (2011). Tensor-train decomposition. SIAM Journal on Scientific Computing 33(5), 2295–2317.
Novikov, A., Podoprikhin, D., Osokin, A., and Vetrov, D. (2015). Tensorizing neural networks. NeurIPS.
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., and Polosukhin, I. (2017). Attention is all you need. NeurIPS.