๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Pruning

Pruning sets the smallest weights in a layer to zero. Sparsity is the share of a layer's weights that sit at zero.

Pruning one small layer

Half of this layer's twelve weights go to zero. The nonzero count drops from twelve to six.

import torch
from torch.nn.utils import prune

torch.manual_seed(0)
layer = torch.nn.Linear(4, 3)
print(int((layer.weight != 0).sum().item()))
prune.l1_unstructured(layer, name="weight", amount=0.5)
print(int((layer.weight != 0).sum().item()))

The smallest go first

Four hand-set weights make the choice visible. The two smallest magnitudes become zero, and the two biggest stay.

import torch
from torch.nn.utils import prune

layer = torch.nn.Linear(2, 2, bias=False)
with torch.no_grad():
    layer.weight.copy_(torch.tensor([[0.01, 0.5], [-2.0, 0.2]]))
prune.l1_unstructured(layer, name="weight", amount=0.5)
print(layer.weight.detach().tolist())

Making the zeros permanent

prune.remove folds the mask into the weight and drops the extra names. The zeros stay after the removal.

import torch
from torch.nn.utils import prune

torch.manual_seed(0)
layer = torch.nn.Linear(4, 3)
prune.l1_unstructured(layer, name="weight", amount=0.5)
prune.remove(layer, "weight")
print(int((layer.weight != 0).sum().item()))
print(hasattr(layer, "weight_orig"))

Example

import torch
from torch.nn.utils import prune

torch.manual_seed(0)
centers = torch.tensor([[-1.5] * 20, [1.5] * 20])
def make(rows, seed):
    g = torch.Generator().manual_seed(seed)
    labels = torch.randint(0, 2, (rows,), generator=g)
    data = centers[labels] + torch.randn(rows, 20, generator=g) * 0.6
    return data, labels

train_x, train_y = make(300, 1)
test_x, test_y = make(200, 2)

torch.manual_seed(3)
model = torch.nn.Sequential(
    torch.nn.Linear(20, 16), torch.nn.ReLU(), torch.nn.Linear(16, 2))
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
loss_fn = torch.nn.CrossEntropyLoss()
for step in range(200):
    optimizer.zero_grad()
    loss = loss_fn(model(train_x), train_y)
    loss.backward()
    optimizer.step()

def accuracy(m):
    with torch.no_grad():
        pred = m(test_x).argmax(dim=1)
    return (pred == test_y).float().mean().item()

before = accuracy(model)
layer = model[0]
total = layer.weight.numel()
prune.l1_unstructured(layer, name="weight", amount=0.5)
effective = layer.weight.detach()
nonzero = int((effective != 0).sum().item())
after = accuracy(model)
print(f"Accuracy before pruning: {before:.2f}")
print(f"First layer weights: {total}")
print(f"Nonzero after pruning: {nonzero}")
print(f"Sparsity: {1 - nonzero / total:.2f}")
print(f"Accuracy after pruning: {after:.2f}")

Key points

  • Mask: a same-shaped tensor of ones and zeros that selects entries
  • Effective weight: the weight a layer actually uses, with its mask applied
  • Use prune.l1_unstructured(layer, name='weight', amount=0.5) to zero half of one weight
  • L1 picks by absolute value, and the smallest weights go first
  • The prune helpers live in torch.nn.utils.prune, and the name needs its own import
  • Sparsity counts the zeros in the effective weight after pruning

In this example, a small classifier trains on two seeded blob classes with 20 features. The first layer holds 320 weights into 16 hidden units. Accuracy prints before pruning, and the run reaches 1.00 on the test batch. Half of the first layer's weights are then pruned by size. The effective weight keeps 160 nonzero entries, and sparsity prints as 0.50. Accuracy after pruning prints as 1.00. The accuracy does not move here, because these blobs are easy to separate.

Note

Zeros alone do not shrink a saved file, and smaller files need extra steps

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

What does amount=0.5 mean in l1_unstructured?