Kusupati, Ramanujan, Somani, Wortsman, Jain, Kakade, Farhadi (UW / AI2 / MSR), ICML 2020

Learning Sparsity Itself:
Soft Threshold Reparameterization

Stop hand-picking how many weights each layer keeps. Make the pruning threshold a parameter, push it down the gradient like everything else, and let the network discover its own per-layer sparsity budget — beating hand-tuned heuristics by up to 10% accuracy at 99% sparsity, with up to 50% fewer FLOPs.

Prerequisites: Gradient descent & weight decay + What pruning is
10
Chapters
6
Simulations

Chapter 0: The Problem

You have a trained ResNet50. It has 25.6 million weights and runs at 4.1 billion FLOPs per image. You want to ship it to a phone. The standard move is pruning: set most of the weights to zero, keep only the important ones, and the model still works. People routinely throw away 90% of the weights and lose only a point or two of accuracy.

But pruning forces a decision nobody likes to make: how sparse should each layer be? A ResNet50 has 54 weight tensors of wildly different sizes — a 3×3 first conv with a few thousand parameters, a final fully-connected layer with two million. Some layers are cheap to keep dense; some are expensive. The amount you can safely zero out differs from layer to layer, and you don't know the right split in advance.

The core question: Instead of a human guessing a sparsity budget for each layer (90% here, 80% there, keep the first layer dense), can the network learn the per-layer budget by gradient descent — choosing automatically how much to prune each layer to maximize accuracy at a given size?

What people did before: heuristics, everywhere

Every existing method picks the budget by hand or by rule of thumb. The paper groups them into two families, both unsatisfying:

Why the budget secretly controls FLOPs

Here is the subtlety the paper keeps hammering: parameter count and compute (FLOPs) are not the same thing. A 3×3 kernel in an early convolution has very few parameters, but it is applied to a huge feature map — so it costs a lot of FLOPs. If your budget rule keeps that layer dense "because it's small," you've kept the most expensive layer fully active.

Method (90% sparse ResNet50)Top-1 AccFLOPs
GMP (uniform)73.91%409M
RigL + ERK (heuristic budget)73.00%960M
STR (learnt budget)74.31%343M

Same target sparsity, but STR is more accurate and uses fewer FLOPs than every baseline. ERK is actually more expensive than uniform — its "keep small layers dense" rule keeps the FLOP-heavy early convs alive. A learnt budget sidesteps that trap.

Why can two 90%-sparse models have very different FLOPs?

Chapter 1: The Key Insight

The whole paper turns on one move. Pruning has always been an external operation: train the weights, then apply some rule to zero out the small ones. That rule has a knob — a threshold α, where any weight smaller than α in magnitude gets killed. Traditionally a human or a heuristic sets α.

STR's insight: make α a learnable parameter, one per layer, and write the forward pass so that the loss is a smooth function of α. Then α gets a gradient just like any weight, and SGD tunes it. Each layer discovers its own threshold — which means each layer discovers its own sparsity.

The insight: Don't prune after training. Bake the pruning operator into the forward pass as a differentiable function of a learnable threshold αl. The thresholds, and therefore the per-layer sparsity budget, are learned end-to-end by backpropagation — no heuristic, no hand-tuning.

The operator at the heart of it

The trick is to use the soft-threshold operator — a function statisticians have used for sparse regression since 1995. For a single weight w and threshold α:

S(w, α) = sign(w) · ReLU(|w| − α)

In words: if |w| is below α, the output is exactly zero (pruned). If |w| is above α, you keep its sign but shrink its magnitude by α. The forward pass uses S(W, α) instead of the raw weight W. We will dissect this operator in Chapter 3 — for now, notice it is continuous in α, which is exactly what makes α trainable.

reparameterize
Replace raw weight W in the forward pass with S(W,α) — the soft-thresholded weight. The "real" trainable weight W lives behind the operator.
make α learnable
α = g(s), a smooth function of a free parameter s. Loss flows back to s, so each layer's threshold is tuned by gradient descent.
weight decay drives sparsity
L2 decay on s slowly pushes thresholds up over training, so sparsity emerges and rises — you never set a budget, you set one decay knob λ.

What this buys you

Because every layer learns its own α, the per-layer sparsity is non-uniform and optimized. Layers that can afford to lose weights become very sparse; layers that can't stay denser. And because the gradient update is multiplied by an indicator that the weight survived, the gradient itself is sparse — STR fixes the two classic pains of threshold-based pruning at once. We'll prove both claims.

What is the single trick that makes the pruning threshold trainable?

Chapter 2: Background — Hard vs Soft Thresholding

Pruning is, mathematically, an old friend: sparse optimization. You want to minimize a loss while keeping the number of non-zero parameters small. Formally:

minW L(W; D)   s.t.  ‖W‖0 ≤ k

where ‖W‖0 is the L0 "norm" — literally the count of non-zero entries — and k is your parameter budget. This is the cleanest way to write "keep at most k weights." The trouble: the L0 constraint is combinatorial and non-convex, so you can't just run gradient descent on it.

The classical fix: projected gradient descent

The decades-old workaround is projected gradient descent (PGD): take a normal gradient step, then project back onto the feasible set (snap the iterate to something sparse). Two projections dominate the literature, depending on which sparsity-encouraging set you project onto:

Why L1 is the relaxation of L0: minimizing the count of non-zeros (L0) is intractable, but minimizing the sum of absolute values (L1) is convex and provably yields sparse solutions — the L1 ball has sharp corners on the axes, so the optimum lands where coordinates are exactly zero. Soft thresholding is the operator that performs this L1 projection. This is the same logic behind LASSO regression.

Two things break when you bring PGD to deep nets

The paper is blunt about why naive PGD pruning is painful at the scale of modern networks:

  1. Dense gradients and dense intermediate iterates. PGD takes a full dense gradient step then projects. So between steps the weights are dense again — you're computing and storing billion-element dense updates even though the final model is sparse.
  2. The budget k is global and indivisible. You're told "keep k weights total" but not how to split k across layers. That's the exact heuristic-budget problem from Chapter 0.

STR's reparameterization will dissolve both. The first one disappears because the gradient update gets multiplied by a survival indicator (Chapter 5). The second disappears because each layer learns its own α (Chapter 6).

What is the difference between hard and soft thresholding on the survivors (weights above the threshold)?

Chapter 3: The Soft-Threshold Operator, Dissected

Everything rides on this one function, so let's make it concrete. For a scalar weight w and threshold α ≥ 0:

S(w, α) = sign(w) · ReLU(|w| − α) = sign(w) · max(|w| − α, 0)

Decompose it symbol by symbol, each with a plain-language role:

The two-zone behavior: Soft thresholding has a flat dead zone [−α, +α] where everything maps to exactly 0, and outside it a line of slope 1 that is shifted toward zero by α. Compare to hard thresholding, which also has a dead zone but then jumps straight to the identity line — no shrinkage. The shrinkage is not a bug; it's a built-in regularizer.

Worked example with real numbers

Set α = 0.3. Push a few weights through:

w|w||w| − αReLUS(w, 0.3)fate
0.90.90.60.6+0.6kept, shrunk by 0.3
−0.50.50.20.2−0.2kept, shrunk by 0.3
0.20.2−0.100pruned
−0.050.05−0.2500pruned

Notice the regime: anything within ±0.3 vanishes; everything else loses 0.3 of its size. Raise α and the dead zone widens, more weights vanish, survivors shrink harder. That single dial controls the layer's sparsity. Now drag it yourself.

Interactive: the soft-threshold transfer function

Drag the slider to change α. Watch the dead zone (gray) widen and the input→output curve (teal) shift. Toggle to compare against hard thresholding (the magnitude-pruning baseline).


With α = 0.4, what is S(−0.7, 0.4)?

Chapter 4: The Reparameterization — SHOWCASE

We now assemble the actual method. The naive idea — "set α and apply S" — still needs a human to set α. STR closes the loop by writing α as a smooth function of a free, trainable parameter s:

αl = g(sl),    forward pass uses  Sg(Wl, sl) = sign(Wl) · ReLU(|Wl| − g(sl))

where g is any continuous, increasing function (the paper uses the sigmoid for unstructured sparsity). Why route α through g rather than train α directly? Two reasons:

You never set a sparsity budget. The overall sparsity is controlled by the L2 weight-decay λ applied to s (and the rest of the weights). Bigger λ → s decays toward more negative → ... wait, that would shrink α. The actual dynamics are subtler and beautiful: weight decay shrinks the surviving weights, which makes more of them fall under α, which makes the loss tolerate a larger α, which decays the budget upward. Sparsity emerges. You tune one global λ (and optionally sinit) to hit a target size.

The full training loop, in pseudocode

# STR: a 2-D conv whose weights are soft-thresholded by a LEARNED threshold
import torch, torch.nn as nn, torch.nn.functional as F

class STRConv(nn.Conv2d):
    def __init__(self, *a, s_init=-5.0, **k):
        super().__init__(*a, **k)
        # s is a SINGLE learnable scalar per layer -> one threshold per layer
        self.s = nn.Parameter(torch.tensor(s_init))   # weight-decayed like any param

    def sparse_weight(self):
        alpha = torch.sigmoid(self.s)               # g(s): threshold in (0,1), starts ~0
        w = self.weight
        return torch.sign(w) * F.relu(torch.abs(w) - alpha)  # S_g(W, s)

    def forward(self, x):
        return self._conv_forward(x, self.sparse_weight(), self.bias)

# Training is ORDINARY SGD. Sparsity is a side effect of weight decay on s + W.
opt = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9,
                      weight_decay=1e-4)   # lambda controls final sparsity

That is the entire method. No mask buffers, no per-step top-k, no pruning schedule. Just an extra scalar per layer and standard SGD. Now watch it run.

SHOWCASE: STR training dynamics on a weight tensor

A layer's 64 weights are shown as bars (height = magnitude, gray bars are pruned to zero by the current α). The teal line is the learned threshold α = sigmoid(s). Press Train to run SGD: weight decay shrinks weights and slowly raises α, so sparsity climbs. Adjust λ to see how the final budget changes — exactly the paper's mechanism.


Press Train. Watch α rise and bars go gray as sparsity emerges.
In STR, what actually decides the final sparsity of a layer?

Chapter 5: The Gradients — Why the Update Is Sparse

Time to earn the claim that STR fixes the "dense gradient" pain of classical PGD. Write the loss with the reparameterization, add standard L2 decay, and take a gradient-descent step on each weight tensor. The update equation from the paper is:

Wl(t+1) ← (1 − ηλ) Wl(t) − η ∇SL · 1{ Sg(Wl, sl) ≠ 0 }

Read every piece:

Where the indicator comes from

By the chain rule, the gradient w.r.t. the raw weight W is SL · ∂S/∂W. And what is ∂S/∂W? Differentiate S(w) = sign(w)·ReLU(|w| − α):

That derivative is the indicator 1{|w| > α} = 1{S ≠ 0}. (At the kink S is non-differentiable, so the paper uses a sub-gradient — pick 0 or 1; it doesn't matter in practice.)

Why this matters at scale: The gradient for a pruned weight is multiplied by 0 — it receives no update from the loss, only the weight-decay shrink. As training proceeds and the layer gets sparser, more and more of the gradient is exactly zero. So the effective gradient becomes sparse for free, growing sparser over iterations. This is the opposite of PGD, which always computes a fully dense gradient before projecting.

The gradient on the threshold itself

And s is trainable because L is continuous in s: as α = g(s) increases, more weights cross into the dead zone and the survivors shrink, all smoothly. The sub-gradient ∂L/∂s exists and SGD descends it, while weight decay on s nudges the threshold upward. The tug-of-war between "loss wants accuracy (low α)" and "decay wants small s" settles each layer at its own α.

Interactive: dense gradient (PGD) vs sparse gradient (STR)

Each cell is one weight's gradient. Slide α up: in STR, gradient cells inside the dead zone go dark (exactly 0, killed by the survival indicator). PGD keeps every cell live. Watch the live "fraction of gradient that is zero."


Why is STR's gradient on the weights sparse?

Chapter 6: The Learnt Budget

This is the payoff. Because every one of ResNet50's 54 layers learns its own α, STR produces a non-uniform sparsity budget that no human designed. The paper's Figure 3 plots the final per-layer thresholds; Figure 6 plots the resulting per-layer sparsity. The shape is striking and consistent.

What STR discovers: The early layers (especially the FLOP-heavy initial convolutions on big feature maps) get pruned hard — STR is willing to gut them because they're cheap in parameters but expensive in compute. Later layers, dense in parameters, are kept relatively denser. This is precisely the budget that minimizes FLOPs for a given accuracy — and it's the opposite of the ERK heuristic that keeps small/early layers dense.

Why the learnt budget transfers

A second surprise: the sparsity budget STR learns on one task is a good backbone for other tasks. Take the per-layer sparsity STR discovered, freeze those budgets, and retrain — you get an efficient sparse backbone that generalizes. The budget encodes which layers structurally matter, not just task-specific quirks.

The training trajectory of sparsity

During training the overall sparsity follows a smooth S-curve (the paper's Figure 2): near-dense early, accelerating in the middle, leveling off. The authors note this looks like the hand-crafted GMP pruning schedule — except STR was never told to follow it. It learned the same shape that practitioners had been hand-engineering for years.

Interactive: the learnt non-uniform budget across layers

Each bar is a layer of a ResNet-style net (left = early/high-FLOP, right = late/parameter-heavy). Toggle between Uniform (every layer 90%), ERK heuristic (keep small layers dense), and STR's learnt budget. Watch the total FLOPs and accuracy proxy update — STR's profile prunes the expensive early layers to win on FLOPs.

What kind of budget does STR tend to learn for the FLOP-heavy early convolutional layers?

Chapter 7: From Unstructured to Structured Sparsity

So far every weight is pruned individually — unstructured sparsity. It gives the highest compression but the resulting scattered zeros are hard for standard hardware to exploit. The paper shows STR generalizes cleanly to structured sparsity, which removes whole structures (filters, channels, or — for RNNs — entire dimensions of rank).

One operator, two regimes: Apply soft thresholding to individual weight entries → unstructured sparsity (scattered zeros). Apply the very same soft thresholding to the singular values of a weight matrix → structured low-rank sparsity. Shrinking small singular values to zero literally lowers the matrix rank — a structured form that commodity hardware can accelerate.

The low-rank version, step by step

For an RNN weight matrix W, the paper does the following each forward pass:

1. SVD
Factor W = U Σ Vᵀ, where Σ holds the singular values σ1 ≥ σ2 ≥ ... (the matrix's "energy" per direction).
2. soft-threshold Σ
Apply S(σi, α): zero out small singular values, shrink the rest. Now several σi = 0.
3. reconstruct
W' = U S(Σ) Vᵀ is now exactly low-rank — rank = number of surviving singular values. Store/run it as two thin matrices.

The threshold α on the singular values is, once again, learned via the same reparameterization. So the network learns its own rank per layer. For structured sparsity the paper uses the exponential as g (instead of sigmoid) — it allows the threshold to grow large enough to aggressively cut singular values.

Why low-rank is "hardware-friendly"

A rank-r matrix W' ∈ Rm×n is stored as A ∈ Rm×r and B ∈ Rr×n with W' = AB. A matrix-vector product drops from mn multiplies to r(m+n) — a dense, regular computation that GPUs love, unlike scattered unstructured zeros. The paper reports STR matching or beating prior low-rank RNN methods (e.g. on Google-12 keyword spotting) while learning the rank automatically.

How does STR produce a low-rank (structured) weight matrix?

Chapter 8: The Experiments

STR is benchmarked on ImageNet-1K with ResNet50 and MobileNetV1, against the strongest pruning baselines of the day: GMP, DNW, DSR, SNFS, RigL (with and without the ERK budget), and DPF. The headline: across every sparsity regime from 80% to 99%, STR sits on the accuracy-vs-sparsity frontier — most accurate at each size — while using the fewest FLOPs.

ResNet50 on ImageNet (dense baseline: 77.01% top-1)

SparsityMethodTop-1 AccFLOPs
90%GMP73.91%409M
90%RigL+ERK73.00%960M
90%STR74.31%343M
95%GMP70.59%204M
95%STR70.97%182M
98%GMP57.90%82M
98%STR62.84%80M
99%GMP44.78%41M
99%STR54.79%54M
The ultra-sparse regime is where STR shines hardest: at 99% sparsity STR scores 54.79% vs GMP's 44.78% — a ~10 point jump. When almost everything is being thrown away, where you spend your tiny remaining budget matters enormously, and a learned budget vastly outperforms a uniform one. At 98%, STR is +4.9 points over GMP at essentially equal FLOPs.

MobileNetV1: efficiency on an already-efficient net

MobileNetV1 is designed to be small, so it's a harder pruning target. STR still wins: at 90% sparsity it boosts accuracy by ~0.3% over baselines while cutting FLOPs by up to 50% — the clearest demonstration that the learnt budget is buying real compute savings, not just parameter count.

Reading the frontier yourself

Interactive: the accuracy-vs-sparsity frontier (ResNet50/ImageNet)

Real numbers from the paper. Drag the sparsity slider; the dots are STR vs GMP at that level. The gap widens dramatically as you approach 99% — STR forms the upper frontier curve.


Honest limitations the paper does NOT hide: (1) Unstructured sparsity, STR's strongest result, is hard to accelerate on commodity hardware — the FLOP savings are theoretical until you have a sparse kernel. (2) STR doesn't let you set an exact target sparsity; you tune λ and land near a budget (e.g. "90%" is really 90.23%). (3) Like all dense-to-sparse methods, training still touches dense weights, so it doesn't reduce training cost the way sparse-to-sparse methods aim to.
In which regime does STR most dramatically beat uniform pruning (GMP)?

Chapter 9: Connections & Cheat Sheet

STR's lasting idea isn't a particular accuracy number — it's the pattern: take a discrete, hand-tuned design choice (the sparsity budget) and make it a smooth, learnable parameter folded into the forward pass. That pattern recurs everywhere in modern model compression.

Where it sits in the landscape

The cheat sheet

SymbolMeaningPlain analogy
S(w, α)Soft-threshold operatorA moat of width α: drowns small weights, shrinks the rest by α
αl = g(sl)Per-layer learned thresholdEach layer's own pruning dial, set by SGD not by you
slFree trainable scalar (one per layer)The thing gradients actually move; weight-decayed like a weight
gSigmoid (unstructured) / exp (structured)Keeps α ≥ 0 and shapes how fast it can grow
1{S ≠ 0}Survival indicator = ∂S/∂wGradient gate: pruned weights get zero gradient → sparse update
λL2 weight decayThe single global knob that controls final sparsity
sinitInitialization of sSet so α starts near 0 (begin dense, grow sparse)

The whole method in five lines

# STR in five lines. That is genuinely all of it.
alpha = torch.sigmoid(s)                              # learned threshold, one s per layer
W_sparse = torch.sign(W) * F.relu(W.abs() - alpha) # soft-threshold the weights
y = conv(x, W_sparse)                              # forward pass uses the SPARSE weight
loss = criterion(y, target)
# loss.backward() + SGD(weight_decay=lambda) does the rest. Sparsity emerges.
The Feynman test for this lesson: Could you, on a whiteboard, (1) write S(w,α) and explain its dead zone and shrinkage, (2) explain why α=g(s) is trainable while a hard threshold is not, (3) derive why the weight gradient is sparse from ∂S/∂w, and (4) explain why a learnt non-uniform budget beats uniform on FLOPs? If yes, you own the paper.

Related on Engineermaxxing

Pair this with lessons on magnitude pruning, the Lottery Ticket Hypothesis, low-rank factorization (SVD), and quantization to see the full model-compression toolbox. STR is the "learn the budget" corner of that map.

What is STR's transferable big idea, beyond pruning?