← All tutorials

Companion notebook

Pruning

On this page

1 Unstructured and structured pruning 2 How PyTorch prunes with masks 3 Pruning half of a ResNet-18 4 Recovering accuracy with fine-tuning 5 The prune and fine-tune loop 6 One-shot or iterative pruning 7 Pruning in practice 8 Pitfalls 9 Combining pruning with quantization 10 References

Pruning deletes the weights a trained network barely uses. Neural networks are overparameterized (they have far more weights than the task needs), so removing those weights gives a smaller, cheaper model that keeps what it learned. Two choices shape every pruning recipe: what to cut (single weights or whole filters) and how to recover the lost accuracy (fine-tune once, or prune and fine-tune in rounds).

Magnitude pruning runs once, after training, with no data: it ranks the trained weights by size and sets a keep-or-cut mask that every later forward pass (one trip of an input through the network) multiplies by until PyTorch bakes it in. Fine-tuning is then ordinary training for a few epochs (15 in the run below); iterative and gradual pruning repeat the cycle (Section 6).

The companion repo's run uses a ResNet-18 (an 18-layer residual network for images) trained on STL-10 (a dataset of 96×96 color images in 10 classes) (Figure 1).

60%70%80%90%92.58%trained66.25%prune halfthe weights92.74%fine-tune fc15 epochstest accuracy
Figure 1. The run's test accuracy through one cut and one fine-tune.

Pruning is the first stage of Deep Compression (Han et al., 2016), built because networks were too compute and memory hungry for embedded hardware. Huffman coding, its last stage, gives frequent values shorter codes.

Trained networkAlexNet 240MB, VGG-16 552MB1. Prune, then retrain9× to 13× fewer connections2. Quantize, then retrain32 bits to 5 per connection3. Huffman codingshort codes for frequent valuesCompressed, same accuracyAlexNet 6.9MB (35×), VGG-16 11.3MB (49×)this page
Figure 2. Deep Compression stage by stage on AlexNet and VGG-16.
AlexNet 35× VGG-16 49× times smaller (bars start at 0)
Figure 3. Both networks shrank with no accuracy loss.

The compressed models fit in on-chip SRAM (the fast memory inside the chip) instead of off-chip DRAM (the larger, slower memory outside it), and ran 3× to 4× faster per layer with 3× to 7× better energy efficiency.

1 Unstructured and structured pruning

Unstructured pruning removes individual weights wherever they are; the matrix keeps its shape but fills with zeros. Structured pruning removes whole filters or channels (a filter is one pattern detector in a convolutional layer, a channel the output sheet it produces), so the layer can be rebuilt smaller and runs faster on ordinary hardware.

The listing does unstructured pruning by L1 magnitude (absolute size): prune_smallest zeros the smallest share amount of every convolution and linear layer's weights, in place, and the last line cuts half of model, the trained ResNet-18.

import torch.nn as nn
import torch.nn.utils.prune as prune

KINDS = (nn.Conv2d, nn.Linear)

def prune_smallest(model, amount):
    for layer in model.modules():
        if isinstance(layer, KINDS):
            prune.l1_unstructured(layer, "weight", amount)

prune_smallest(model, amount=0.5)
  1. 1For every conv and linear layer, attach a mask that zeroes the smallest-magnitude weights.
  2. 2Prunes 50% of each layer's weights.

Each call attaches a keep-or-cut mask to the weights it touches.

Every weight is multiplied by its own 1 or 0, and sparsity is the share of zeros that leaves.
w′ = w ⊙ m
The weight the network uses, w′, is the trained weight w times a mask m of the same shape. ⊙ multiplies entry by entry, so each mask entry mi acts as a switch: 1 passes its weight through unchanged, 0 silences it. Pruning is a multiply rather than a delete, so the original weights stay underneath and the matrix keeps its shape.
s = Σi(1 − mi)N
Sparsity s is the share of weights the mask switched off. 1 − mi flips each switch, so it is 1 exactly for a cut weight; the sum Σi over every weight then counts the cuts, and dividing by the N weights turns the count into a fraction between 0 and 1. Cutting half the weights gives s = 0.5.
unstructured, 50%structured, 50%
Figure 4. X marks a cut weight; structured cuts delete whole rows.
small weights to large weights pruned

Accuracy vs sparsity (a curve through the run's two measured points)
slider setting   measured on ResNet-18, unstructured

Figure 5. Accuracy falls with sparsity, never below 10% chance.

2 How PyTorch prunes with masks

PyTorch deletes nothing at first: the weight a forward pass uses is the mask line of Section 1, built from two stored tensors (Figure 6).

weight_origoriginal valuesweight_mask1 keep, 0 cut⊙weightused by every forward passprune.removeweightone plain parameter, mask gone
Figure 6. Nothing is deleted until prune.remove.

The listing reads both stored tensors of one pruned convolution, checks their product, then bakes the mask in. Its w and m are the mask line's.

conv = model.layer1[0].conv1
w = conv.weight_orig
m = conv.weight_mask
# what forward uses
w_prime = conv.weight
assert torch.equal(w_prime, w * m)
prune.remove(conv, "weight")
  1. 1The original weights and the 0/1 mask PyTorch stores beside them.
  2. 2The weight forward uses is their elementwise product.
  3. 3Makes the pruning permanent: drops the mask and keeps the product.

3 Pruning half of a ResNet-18

The companion repo's run prunes its ResNet-18 with prune_smallest at amount=0.5. The listing computes the sparsity line: it adds up the weights into N, counts the exact zeros, and divides to get s (Figure 7).

amount is the fraction of each layer's weights set to zero. Raising it costs more accuracy before fine-tuning; lowering it keeps more weights and saves less, amount = 0 changes nothing and amount = 1 zeroes every weight. PyTorch has no default (amount is a required argument of L1Unstructured).

N = cut = 0
for layer in model.modules():
    if isinstance(layer, KINDS):
        w = layer.weight
        N += w.numel()
        cut += (w == 0).sum().item()
s = cut / N   # 0.5 after the cut
  1. 1Counts all weights and the zeros among them, layer by layer.
  2. 2Sparsity is the zero fraction.
11,172,032 prunable weights (Conv2d, Linear)5,586,016 zeroedkept50.00%STL-10 test accuracybefore92.58%after66.25%−26 points: the survivors were trainedto lean on the weights now cut
Figure 7. Pruning half of each layer, before any fine-tuning.

4 Recovering accuracy with fine-tuning

The fix is to let the survivors adjust. The run trains only the final classifier layer for 15 epochs (an epoch is one full pass over the training data) with SGD (stochastic gradient descent) with momentum. Batch-norm statistics also refresh during those epochs, because the whole model is in train mode, so the recovery is not the classifier's work alone.

Each step follows a decaying sum of past gradients instead of the latest gradient alone.
v ← μ v + g
The velocity v is a decaying sum of gradients: each step keeps the share μ = 0.9 (the momentum) of its old value and adds the new gradient g of the loss. Directions that repeat step after step build up, while noisy back-and-forth directions cancel, so the steps move in a steadier direction. A larger μ remembers more of the past; a smaller μ follows the newest gradient more closely, and μ = 0 is plain gradient descent.
w ← w − η v
Each final-layer weight w moves against the velocity, downhill on the loss, by a step the learning rate η = 0.001 sets. A small η keeps the fine-tuning steps small, so the surviving weights shift without forgetting what they learned; a larger η recovers faster but can overwrite what the survivors learned, and η = 0 freezes the layer.

The listing wraps that recipe as fine_tune: SGD with η = 1e-3 and μ = 0.9 on the final layer fc only, then the standard training loop. train_loader yields STL-10 batches, criterion is nn.CrossEntropyLoss() (averaged over the batch), and y holds the true labels.

def fine_tune(model, epochs=15):
    params = model.fc.parameters()
    opt = torch.optim.SGD(params, lr=1e-3, momentum=0.9)
    # BN statistics refresh too
    model.train()
    for epoch in range(epochs):
        for x, y in train_loader:
            opt.zero_grad()
            out = model(x)
            loss = criterion(out, y)
            loss.backward()
            opt.step()
  1. 1Optimizer over the final layer only; train mode lets BatchNorm statistics update.
  2. 2A normal training loop over the pruned model.

The loss falls from 0.40 to 0.23 over the 15 epochs and accuracy ends at 92.74%, above the unpruned 92.58% (Figure 1).

5 The prune and fine-tune loop

The recipe runs on a 6 by 6 grid that stands for the final layer, in five steps with the run's real accuracy numbers.

cell shade is weight magnitude weakest half, the cut pruned

Figure 8. Cut, measure, retrain: accuracy comes back.

6 One-shot or iterative pruning

One-shot pruning prunes once, then fine-tunes. Iterative pruning prunes a little, fine-tunes, and repeats, so the survivors adapt before the next cut, and goes further for the same accuracy (Figure 9).

AlexNet, no accuracy lossiterative9× fewerone-shot5× fewerconnections cut before accuracy drops
Figure 9. Iterative pruning reaches 9×, one-shot 5× (Han 2015).

The listing is iterative pruning in five rounds. A repeated l1_unstructured call ranks only the weights still alive, so each round cuts 20% of what is left (about 67% gone after five rounds), and fine_tune retrains in between.

for _ in range(5):
    prune_smallest(model, amount=0.2)
    fine_tune(model)

Gradual pruning folds the same idea into training itself: the mask is updated on a schedule while the network keeps learning.

The gradual schedule of Zhu and Gupta (2017) sets how much of the network should be cut by training step t.
st = sf
+ (si − sf)(1 − t − t0nΔt)3
Sparsity st rises from the start value si (usually 0) to the final value sf over n mask updates, one every Δt steps from step t0. The fraction inside the bracket is the share of the schedule already used: 0 at the start, 1 at the end, so the bracket falls from 1 to 0 and st slides from si to sf. The cube makes that slide steep early and flat late: big cuts while many weights are redundant, small cuts once every survivor matters. A larger sf leaves a smaller model and costs more accuracy; a longer Δt gives the network more steps to recover between cuts, and a larger n spreads the cuts over more, smaller updates.
00.50.9 one-shot gradual training step t sparsity 0 to 0.9 in 10 mask updates sparsity
Figure 10. Gradual (10 updates) against one-shot, both to 90%.

7 Pruning in practice

In a real project pruning is a short stage between training and export: prune, fine-tune (The prune and fine-tune loop), then bake the masks in with prune.remove, which stores the masked weight as a plain parameter again (Figure 6).

import torch
import torch.nn as nn
import torch.nn.utils.prune as prune

KINDS = (nn.Conv2d, nn.Linear)
for layer in model.modules():
    if isinstance(layer, KINDS):
        # bake the mask in
        prune.remove(layer, "weight")
sd = model.state_dict()
torch.save(sd, "pruned.pt")
  1. 1Bakes each mask into its weight and removes the mask buffers.
  2. 2Saves the plain pruned weights.

8 Pitfalls

9 Combining pruning with quantization

Deep Compression (Figure 2) combines the two: prune, then store the survivors in fewer bits (Post-Training Quantization and Quantization-Aware Training). Pruning starts from a trained network; Knowledge Distillation instead trains a small network from a large one, and neural architecture search looks for a small one from the start (Neural Architecture Search Basics).

10 References

  1. Han, S., Pool, J., Tran, J., Dally, W. J. (2015). Learning both Weights and Connections for Efficient Neural Networks. NeurIPS. arXiv:1506.02626.
  2. Zhu, M., Gupta, S. (2017). To prune, or not to prune: exploring the efficacy of pruning for model compression. arXiv:1710.01878.
  3. Han, S., et al. (2016). Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding. ICLR. arXiv:1510.00149.