The Price List, in JAX/Flax NNX

· 10 min read

#ml#kernels#rkhs#representer-theorem#regularization#generalization#eigenvalues#jax#flax#nnx#implementation

Part 5 of 5Weights in Kernel Space
  1. 1The Readout is a Convex Combination of Prototypes
  2. 2Where Does a Weight Live?
  3. 3What Can a Weight Be?
  4. 4The MLP Block Is a Representer Theorem
  5. 5Why Regularization Is a Price Listthis post's explainer
Explainer companionWhy Regularization Is a Price ListWant the full intuition first? This is the runnable companion to the explainer.Read the explainer

The explainer argued that the RKHS norm is a price list: every kernel splits into modes priced by eigenvalues, a weight pays for the modes it uses, and regularization is just tightening the budget. This is the implementation. A Gaussian kernel in a few lines of JAX, the representer solve (K+λI)α=y(K + \lambda I)\,\alpha = y, the bill αKα\alpha^\top K \alpha, the effective dimension deff=kλk/(λk+λ)d_{\text{eff}} = \sum_k \lambda_k/(\lambda_k + \lambda) read off the Gram spectrum, and a sweep over λ\lambda that draws the generalization U-curve. There is no gradient descent anywhere: kernel ridge on a small dataset is one linear solve plus one eigendecomposition. Every number below is from that run; the script is scripts/regularization_pricelist.py and the GIFs are scripts/render_reg_gifs.py.

The kernel and the solve

Where is the budget in the code? It is a single scalar in a single solve. The whole machine is a Gaussian kernel, its Gram matrix on the training points, and the ridge system (K+λI)α=y(K + \lambda I)\,\alpha = y. That system is the representer weight f(x)=iαik(xi,x)f(x) = \sum_i \alpha_i\, k(x_i, x) solved in one shot, and the only thing λ\lambda does is inflate the diagonal, which shrinks every coefficient at once.

import jax, jax.numpy as jnp

SIG = 0.45

@jax.jit
def gram(A, B):                                       # k(xᵢ, yⱼ) for every pair
    d2 = ((A[:, None, :] - B[None, :, :]) ** 2).sum(-1)
    return jnp.exp(-d2 / (2 * SIG ** 2))

def kernel_ridge(K, y, lam):                          # the representer solve, no descent
    return jnp.linalg.solve(K + lam * jnp.eye(len(K)), y)

The vector alpha that comes back is the weight, one charge per training point. Turn λ\lambda up and the whole vector deflates. On a 1-D regression toy (a noisy damped sine, 40 training points, a clean test grid of 400) that deflation is the fit going from ferocious wiggle to smooth, while two readouts fall with it: the RKHS bill and the effective dimension.

A 1-D kernel ridge fit stiffening from a wiggly interpolation to a smooth curve as lambda grows, with the RKHS bill and effective dimension readouts falling
Turn the budget knob. As λ\lambda grows the ridge fit (orange) stops chasing every noisy point and settles onto the true signal (grey). The two bars are the real readouts of the solve: the RKHS bill fH2=αKα\lVert f\rVert_{\mathcal H}^2 = \alpha^\top K \alpha and the effective dimension deffd_{\text{eff}}. The bill falls from 63.463.4 at the loosest budget to under 33 near the sweet spot; the effective dimension falls from 18.818.8 toward 0.40.4. Same points, same sum, only the spending limit changes. Rendered by scripts/render_reg_gifs.py.

The bill you pay for the weight

So what is the bill, exactly? The explainer’s whole argument is that “expensive” is a real number you can compute from nothing but the kernel: the squared RKHS norm of the solution is αKα\alpha^\top K \alpha. It is one line, and it is the quantity λ\lambda is capping.

def rkhs_norm_sq(K, alpha):                           # ‖f‖²_H, the bill
    return float(alpha @ (K @ alpha))

That single number falls monotonically as λ\lambda tightens: the ferocious fit spends 63.463.4, the fit at the generalization sweet spot spends 2.52.5, and the over-tight fit spends almost nothing, 0.0030.003. The bill is the budget spent, and the knob is literally the spending limit.

The price list: eigenvalues and the effective dimension

But a single cap does not tell you which modes get cut. That is set by the eigenvalues of the Gram matrix, the posted prices, and one symmetric eigendecomposition hands you the whole list.

eigs = jnp.linalg.eigvalsh(K)[::-1]                   # the price list, descending

def eff_dim(eigs, lam):                               # modes that survive the budget
    return float(jnp.sum(eigs / (eigs + lam)))

Each mode contributes λk/(λk+λ)\lambda_k/(\lambda_k + \lambda), a survival weight that is near 11 when the mode is cheap relative to the budget and near 00 when it is not. Summed, that is the effective dimension: the number of directions the weight is actually allowed to use. Watch the budget evict the tail first, the expensive high-frequency modes going dark before the cheap smooth ones even dim.

Eigenvalue bars of the Gram matrix, each tinted by its survival weight, with the small tail eigenvalues fading first as lambda grows
The spectrum eviction. Each bar is an eigenvalue λk\lambda_k, the price of one mode; the bright orange bars survive the budget and the fading ones are being evicted, brightness set by the real survival weight λk/(λk+λ)\lambda_k/(\lambda_k + \lambda). As λ\lambda grows the small tail eigenvalues, the expensive wiggly modes, go dark first, and deff=kλk/(λk+λ)d_{\text{eff}} = \sum_k \lambda_k/(\lambda_k+\lambda) counts what is left. Rendered by scripts/render_reg_gifs.py.

The points-to-modes bridge

The representer weight is written one point at a time; the price list charges one mode at a time. The two currencies exchange through the Gram eigenvectors. Writing K=kλkukukK = \sum_k \lambda_k\, u_k u_k^\top, the discrete Mercer modes are ϕk(xi)=uk[i]\phi_k(x_i) = u_k[i], a bump’s shopping list is k(xi,)=kλkϕk(xi)ϕkk(x_i, \cdot) = \sum_k \lambda_k\, \phi_k(x_i)\, \phi_k, and buying the combination f=iαik(xi,)f = \sum_i \alpha_i\, k(x_i, \cdot) purchases mode kk in amount ck=λkiαiϕk(xi)c_k = \lambda_k \sum_i \alpha_i\, \phi_k(x_i).

evals, U = jnp.linalg.eigh(K)                         # K = Σ λₖ uₖ uₖᵀ
c = evals * (U.T @ alpha)                             # cₖ = λₖ (uₖ · α), the mode purchase
bill_modes  = float(jnp.sum(c ** 2 / evals))          # Σ cₖ²/λₖ  ...
bill_direct = float(alpha @ (K @ alpha))              # ... equals αᵀKα, exactly

The two bills agree to machine precision in the run: 4.30514.3051 priced by modes, 4.30514.3051 priced by points, a relative error of 2×1092 \times 10^{-9}. It is one weight, quoted in two currencies. A bump does not walk in empty-handed: it carries a fixed shopping list of modes, and α\alpha only chooses how many copies of each list to buy.

Two bar panels: a single kernel bump's fixed list of mode amounts, and the mode coefficients the whole solved weight buys
The points-to-modes bridge, in two panels. The top bars are one bump’s fixed shopping list, λkϕk(x1)\lambda_k\, \phi_k(x_1), a little of every mode in proportions pinned by where its point sits. The bottom bars are ck=λkiαiϕk(xi)c_k = \lambda_k \sum_i \alpha_i\, \phi_k(x_i), what the whole solved weight actually buys once α\alpha chooses the copies. Same weight, priced by modes instead of by points. Rendered by scripts/render_reg_gifs.py.

Once the weight is priced by modes, the bill breaks down line by line: each mode adds ck2/λkc_k^2/\lambda_k to the RKHS norm, the expensive lines dominating, up to the same real total αKα\alpha^\top K \alpha.

Bars of each mode's cost c_k squared over lambda_k, ordered largest first, with a cumulative curve reaching the real total
Where the RKHS bill comes from. Each bar is a mode’s line on the bill, ck2/λkc_k^2/\lambda_k, ordered largest first; the curve is the running total, which reaches the real αKα=4.31\alpha^\top K \alpha = 4.31, the same number the direct one-line formula returns. A handful of modes carry almost the whole bill: the top mode alone is 29%29\% of it, the top five are 73%73\%. Rendered by scripts/render_reg_gifs.py.

The shrink is graded, never zero

The explainer drew one distinction and this companion keeps it: kernel ridge is not a support-vector machine. It solves (K+λI)α=y(K + \lambda I)\,\alpha = y, and unlike a hinge loss it never sets a coefficient exactly to zero. Tightening λ\lambda shrinks every αi\alpha_i smoothly, just at very different rates, so what you get is a graded hierarchy, not a hard roster of survivors. On the six-point, two-class miniature from the explainer, the coefficients start at αi{1.27,0.66,1.16,1.11,0.46,1.10}|\alpha_i| \in \{1.27, 0.66, 1.16, 1.11, 0.46, 1.10\} and, at the tightest budget, all sink toward a common floor with a minimum of 0.010.01, still strictly positive.

for lam in lambdas:                                   # the sweep, one solve each
    alpha = kernel_ridge(K, y, lam)
    abs_alpha = jnp.abs(alpha)                         # all shrink, none hit zero
Six coefficient bars all sinking smoothly as lambda tightens, the two redundant points slipping under a dashed reading-aid line first, none reaching zero
The graded shrink. Every αi|\alpha_i| bar sinks smoothly as λ\lambda tightens, and the two redundant points (whose lists a neighbor can substitute) slip under the dashed line first. The line is a reading aid, not a mechanism: nothing is ever set to zero, the running minimum stays strictly positive. This is graded shrinkage, not the hard sparsity of an SVM. Rendered by scripts/render_reg_gifs.py.

Why smooth generalizes: the sweep

None of this matters unless the tight budget actually generalizes better, so the last thing the run does is sweep λ\lambda across five orders of magnitude and measure train and test error at each. That sweep draws the U-curve, and it is the whole point of the post.

for lam in lambdas:
    alpha = kernel_ridge(K_train, y_train, lam)
    train_mse = jnp.mean((K_train @ alpha - y_train) ** 2)
    test_mse  = jnp.mean((K_test  @ alpha - y_test)  ** 2)   # K_test is test-to-train

At a loose budget the fit chases the noise and test error sits above train (test MSE 0.0280.028); at an over-tight budget it underfits and test error explodes (test MSE 0.230.23); in between, at λ=0.42\lambda^\star = 0.42, test error bottoms out at 0.0110.011, where the effective dimension is 9.89.8 of the 4040 available modes. The sweet spot is not where the training error is lowest; it is where the price list has capped the capacity to just what the signal needs.

A generalization U-curve: train and test MSE versus log lambda, with the sweet spot at the test minimum marked
The generalization U-curve, from the real sweep. Train MSE (blue) rises steadily as the budget tightens; test MSE (orange) dips to a minimum then climbs sharply into underfitting. The marked sweet spot is λ=0.42\lambda^\star = 0.42, where test error bottoms at 0.0110.011 and deff=9.8d_{\text{eff}} = 9.8. Tightening λ\lambda lowers the effective dimension, which lowers the capacity, which shrinks the gap: the price list is the mechanism, not a metaphor for it. Rendered by scripts/render_reg_gifs.py.

That is the price list made runnable. One kernel, one solve, one scalar budget. The eigenvalues post the prices, the effective dimension counts what the budget can afford, the bill is αKα\alpha^\top K \alpha, and the U-curve is the payoff: the weight generalizes not because it is a free combination of the data but because it is a combination the price list can afford.


The representer theorem is Schölkopf, Herbrich and Smola (2001); the eigenvalue-price-list framing is standard RKHS theory in Steinwart and Christmann (2008); the effective-dimension complexity bound is Bartlett and Mendelson (2002); the input-space-center kernel this series builds on is Bouhsine (2026). The conceptual companion is Why Regularization Is a Price List.

Cite as

Bouhsine, T. (). The Price List, in JAX/Flax NNX. Records of the !mmortal Data Scientist. https://tahabouhsine.com/blog/regularization-is-a-price-list-jax-flax-nnx/

BibTeX
@misc{bouhsine2026regularizationisapricelistjaxflaxnnx,
  author       = {Bouhsine, Taha},
  title        = {The Price List, in JAX/Flax NNX},
  year         = {2026},
  month        = {jul},
  howpublished = {\url{https://tahabouhsine.com/blog/regularization-is-a-price-list-jax-flax-nnx/}},
  note         = {Blog post, Records of the !mmortal Data Scientist}
}

References

  1. Schölkopf, B., Herbrich, R., Smola, A. J. (2001). A Generalized Representer Theorem. COLT 2001.
  2. Steinwart, I., Christmann, A. (2008). Support Vector Machines. Springer.
  3. Bartlett, P. L., Mendelson, S. (2002). Rademacher and Gaussian Complexities: Risk Bounds and Structural Results. JMLR.
  4. Bouhsine, T. (2026). A Universal Reproducing Kernel Hilbert Space from Polynomial Alignment and IMQ Distance. arXiv:2605.03262