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.
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.
Every existing method picks the budget by hand or by rule of thumb. The paper groups them into two families, both unsatisfying:
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 Acc | FLOPs |
|---|---|---|
| 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.
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 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 α:
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.
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.
Pruning is, mathematically, an old friend: sparse optimization. You want to minimize a loss while keeping the number of non-zero parameters small. Formally:
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 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:
The paper is blunt about why naive PGD pruning is painful at the scale of modern networks:
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).
Everything rides on this one function, so let's make it concrete. For a scalar weight w and threshold α ≥ 0:
Decompose it symbol by symbol, each with a plain-language role:
Set α = 0.3. Push a few weights through:
| w | |w| | |w| − α | ReLU | S(w, 0.3) | fate |
|---|---|---|---|---|---|
| 0.9 | 0.9 | 0.6 | 0.6 | +0.6 | kept, shrunk by 0.3 |
| −0.5 | 0.5 | 0.2 | 0.2 | −0.2 | kept, shrunk by 0.3 |
| 0.2 | 0.2 | −0.1 | 0 | 0 | pruned |
| −0.05 | 0.05 | −0.25 | 0 | 0 | pruned |
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.
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).
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:
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:
# 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.
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.
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:
Read every piece:
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.)
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 α.
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."
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.
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.
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.
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.
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).
For an RNN weight matrix W, the paper does the following each forward pass:
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.
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.
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.
| Sparsity | Method | Top-1 Acc | FLOPs |
|---|---|---|---|
| 90% | GMP | 73.91% | 409M |
| 90% | RigL+ERK | 73.00% | 960M |
| 90% | STR | 74.31% | 343M |
| 95% | GMP | 70.59% | 204M |
| 95% | STR | 70.97% | 182M |
| 98% | GMP | 57.90% | 82M |
| 98% | STR | 62.84% | 80M |
| 99% | GMP | 44.78% | 41M |
| 99% | STR | 54.79% | 54M |
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.
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.
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.
| Symbol | Meaning | Plain analogy |
|---|---|---|
| S(w, α) | Soft-threshold operator | A moat of width α: drowns small weights, shrinks the rest by α |
| αl = g(sl) | Per-layer learned threshold | Each layer's own pruning dial, set by SGD not by you |
| sl | Free trainable scalar (one per layer) | The thing gradients actually move; weight-decayed like a weight |
| g | Sigmoid (unstructured) / exp (structured) | Keeps α ≥ 0 and shapes how fast it can grow |
| 1{S ≠ 0} | Survival indicator = ∂S/∂w | Gradient gate: pruned weights get zero gradient → sparse update |
| λ | L2 weight decay | The single global knob that controls final sparsity |
| sinit | Initialization of s | Set so α starts near 0 (begin dense, grow sparse) |
# 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.
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.