PyTorch Cheat Sheet
A dense reference for everyday PyTorch 2: tensors, autograd, modules, layers, losses, optimizers, data loading, the training loop, checkpoints, devices and mixed precision, and debugging.
Related chapters: PyTorch fundamentals · The training loop · CNNs · Sequence models · Embeddings · Transfer learning
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader , Dataset , TensorDataset
torch . manual_seed ( 0 )
Tensors
Code
Result / note
torch.tensor([[1., 2.], [3., 4.]])
copies data; dtype inferred (float32 for floats, int64 for ints)
torch.zeros(2, 3), torch.ones(3), torch.full((2, 2), 7.)
constant tensors
torch.arange(0, 10, 2), torch.linspace(0, 1, 5)
ranges (stop excluded / included)
torch.rand(2, 3), torch.randn(2, 3), torch.randint(0, 10, (5,))
uniform \([0, 1)\) , standard normal, integers
torch.zeros_like(x), torch.randn_like(x)
same shape, dtype, device as x
x.shape, x.dtype, x.device, x.ndim, x.numel()
attributes
x.float(), x.long(), x.to(torch.float16)
change dtype
x.reshape(3, -1), x.view(-1)
reshape (view needs compatible memory; reshape copies if needed)
x.unsqueeze(0), x.squeeze(1), x.flatten(1)
add / remove size-1 dims; flatten all but the batch dim
x.permute(0, 2, 3, 1), x.transpose(0, 1), x.T
reorder dims (views, not contiguous)
torch.cat([a, b], dim=0), torch.stack([a, b])
join along an existing / a new dim
x.chunk(4, dim=1), x.split(16, dim=1)
split into pieces
x[:, 0], x[1:, ::2]
basic slicing: views that share memory
x[[0, 2]], x[x > 0]
fancy / boolean indexing: copies
x.clone(), x.contiguous()
explicit copy; compact copy
x.add_(1), x.zero_()
in-place ops end in _ (avoid on tensors autograd needs)
x.sum(dim=1, keepdim=True), x.mean(0), x.max(1).values, x.argmax(1)
reductions (dim = NumPy's axis)
a @ b, torch.einsum("bij,bjk->bik", a, b)
matrix product, batched via einsum
torch.where(c, a, b), x.clamp(0, 1)
elementwise select, clip
Dtypes: float32 (default), float64 (gradient checks), float16/bfloat16 (mixed precision), int64 (labels, indices), uint8 (raw pixels), bool (masks). Python scalars don't promote a tensor's float dtype.
Broadcasting: NumPy's rules. Align shapes from the right; sizes must match or be 1. Watch (n, 1) - (n,), which silently becomes (n, n).
NumPy interop
Code
Shares memory?
torch.from_numpy(a)
yes (keeps float64; add .float())
torch.as_tensor(a, dtype=torch.float32)
when possible
torch.tensor(a)
no, always copies
t.numpy()
yes (CPU, no grad only)
t.detach().cpu().numpy()
the universal way back
t.item()
a one-element tensor as a Python number
Autograd
Code
Meaning
w = torch.randn(3, requires_grad=True)
a leaf you want gradients for
loss.backward()
fills .grad of leaves; adds to existing grads; frees the graph
w.grad, w.grad = None
the gradient; clear it
y.backward(gradient=v)
non-scalar output: computes \(J^\top\mathbf{v}\)
torch.autograd.grad(loss, [w])
returns grads without touching .grad
x.grad_fn, x.is_leaf
the backward node; is it a leaf
with torch.no_grad():
don't record (evaluation, manual updates)
with torch.inference_mode():
stricter, faster no_grad for inference
t.detach()
same data, cut from the graph
p.requires_grad_(False)
freeze a parameter
h.retain_grad()
keep .grad for a non-leaf
torch.autograd.gradcheck(f, (x64,))
compare with finite differences (use float64)
Modules
class MLP ( nn . Module ):
def __init__ ( self , d_in , d_hidden , d_out , p = 0.2 ):
super () . __init__ ()
self . net = nn . Sequential ( nn . Linear ( d_in , d_hidden ), nn . ReLU (), nn . Dropout ( p ), nn . Linear ( d_hidden , d_out ))
def forward ( self , x ):
return self . net ( x )
model = MLP ( 20 , 64 , 3 )
n_params = sum ( p . numel () for p in model . parameters () if p . requires_grad )
Code
Use
model(x)
call (runs hooks); never model.forward(x)
model.parameters(), model.named_parameters()
learnable tensors (registered automatically)
model.named_buffers()
state that isn't learned (BN running stats)
self.register_buffer("mask", t)
add a buffer
nn.ModuleList([...]), nn.ModuleDict({...})
containers that register submodules (a plain list doesn't)
model.train(), model.eval()
dropout / batch norm behavior (not gradients)
model.to(device)
move all parameters and buffers
model.apply(init_fn)
apply a function to every submodule
nn.init.kaiming_normal_(m.weight, nonlinearity="relu"), nn.init.xavier_uniform_, nn.init.zeros_
initialization
Common layers
Layer
Input → output shape
Parameters
nn.Linear(d_in, d_out)
(*, d_in) → (*, d_out)
\(d_{\text{out}}(d_{\text{in}} + 1)\) ; weight is (d_out, d_in)
nn.Conv2d(c_in, c_out, k, stride, padding)
(N, c_in, H, W) → (N, c_out, H', W')
\(c_{\text{out}}(c_{\text{in}}k^2 + 1)\)
nn.MaxPool2d(2), nn.AvgPool2d(2)
halves H, W
0
nn.AdaptiveAvgPool2d(1)
(N, C, H, W) → (N, C, 1, 1)
0
nn.ConvTranspose2d(c_in, c_out, 2, stride=2)
doubles H, W
\(c_{\text{in}}c_{\text{out}}k^2 + c_{\text{out}}\)
nn.BatchNorm1d(d), nn.BatchNorm2d(c)
same shape
\(2d\) (+ \(2d\) buffers)
nn.LayerNorm(d)
same shape
\(2d\)
nn.Dropout(p)
same shape
0
nn.Embedding(V, d)
(*) int64 → (*, d)
\(Vd\)
nn.EmbeddingBag(V, d, mode="mean")
flat ids + offsets → (N, d)
\(Vd\)
nn.RNN/GRU/LSTM(d_in, h, batch_first=True)
(N, T, d_in) → (N, T, h), h_n (layers·dirs, N, h)
\(\{1,3,4\} \times (h\,d_{\text{in}} + h^2 + 2h)\)
nn.MultiheadAttention(d, heads, batch_first=True)
(N, T, d) → (N, T, d)
\(4d^2 + 4d\)
nn.Flatten(), nn.Identity()
flatten from dim 1; pass-through
0
Conv/pool output size: \(\lfloor (I + 2P - K)/S \rfloor + 1\) . Activations: nn.ReLU, nn.GELU, nn.SiLU, nn.Tanh, nn.Sigmoid, nn.LeakyReLU(0.01), nn.Softmax(dim=-1).
Losses
Task
Loss
Model output
Target
Multi-class
F.cross_entropy(logits, y)
raw logits (N, C)
int64 (N,) (or probabilities (N, C))
Multi-class, imbalanced
F.cross_entropy(logits, y, weight=w, label_smoothing=0.1)
logits
int64
Binary / multi-label
F.binary_cross_entropy_with_logits(z, y, pos_weight=pw)
raw logits
float, same shape
Regression
F.mse_loss(pred, y), F.l1_loss, F.smooth_l1_loss
values
float, same shape
Sequences with padding
F.cross_entropy(logits.reshape(-1, C), y.reshape(-1), ignore_index=PAD)
logits
int64
Distributions
F.kl_div(log_q, p, reduction="batchmean")
log-probabilities
probabilities
Never apply softmax or sigmoid before the ..._with_logits / cross_entropy losses. reduction="sum" or "none" for custom weighting.
Optimizers and schedulers
Code
Notes
torch.optim.SGD(params, lr=0.1, momentum=0.9, nesterov=True, weight_decay=5e-4)
classic for CNNs
torch.optim.AdamW(params, lr=1e-3, weight_decay=0.01)
the default choice
torch.optim.Adam(params, lr=1e-3)
L2 inside the gradient (prefer AdamW)
[{"params": backbone.parameters(), "lr": 1e-4}, {"params": head.parameters(), "lr": 1e-3}]
parameter groups (discriminative LRs)
lr_scheduler.OneCycleLR(opt, max_lr, epochs=E, steps_per_epoch=S)
step per batch
lr_scheduler.CosineAnnealingLR(opt, T_max=E)
step per epoch
lr_scheduler.StepLR(opt, step_size=10, gamma=0.1)
step per epoch
lr_scheduler.ReduceLROnPlateau(opt, factor=0.5, patience=2)
sched.step(val_loss)
lr_scheduler.LambdaLR(opt, lambda s: min(1, (s + 1) / warmup))
linear warmup
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
after backward, before step; returns the norm
opt.param_groups[0]["lr"]
read the current learning rate
Data loading
Code
Use
TensorDataset(X, y)
in-memory tensors
custom Dataset with __len__ and __getitem__(i)
anything else; apply transforms in __getitem__
DataLoader(ds, batch_size=64, shuffle=True, generator=torch.Generator().manual_seed(0))
reproducible shuffling
num_workers=4, pin_memory=True, persistent_workers=True
parallel loading for GPU training
drop_last=True
avoid a tiny last batch (batch norm)
collate_fn=my_collate
custom batching (padding, offsets)
torch.utils.data.Subset(ds, idx), random_split(ds, [0.9, 0.1], generator=g)
splits
nn.utils.rnn.pad_sequence(seqs, batch_first=True)
pad variable-length tensors
pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)
RNNs skip padding
torchvision.transforms.v2.Compose([v2.RandomCrop(32, padding=4), v2.RandomHorizontalFlip()])
augmentation (training only)
The training-loop template
X = torch . randn ( 1000 , 20 )
y = ( X [:, 0 ] + X [:, 1 ] > 0 ) . long () + ( X [:, 2 ] > 1 ) . long () # 3 classes
train_dl = DataLoader ( TensorDataset ( X [: 800 ], y [: 800 ]), batch_size = 64 , shuffle = True ,
generator = torch . Generator () . manual_seed ( 0 ))
val_dl = DataLoader ( TensorDataset ( X [ 800 :], y [ 800 :]), batch_size = 256 )
def train_one_epoch ( model , loader , opt , scheduler = None , clip = 1.0 ):
model . train ()
total , n = 0.0 , 0
for xb , yb in loader :
loss = F . cross_entropy ( model ( xb ), yb )
opt . zero_grad ( set_to_none = True )
loss . backward ()
nn . utils . clip_grad_norm_ ( model . parameters (), clip )
opt . step ()
if scheduler is not None :
scheduler . step () # per-batch schedulers
total , n = total + loss . item () * len ( yb ), n + len ( yb )
return total / n
@torch . no_grad ()
def evaluate ( model , loader ):
model . eval ()
loss , correct , n = 0.0 , 0 , 0
for xb , yb in loader :
logits = model ( xb )
loss += F . cross_entropy ( logits , yb , reduction = "sum" ) . item ()
correct += ( logits . argmax ( 1 ) == yb ) . sum () . item ()
n += len ( yb )
return loss / n , correct / n
model = MLP ( 20 , 64 , 3 )
opt = torch . optim . AdamW ( model . parameters (), lr = 3e-3 , weight_decay = 1e-2 )
best , patience , bad = float ( "inf" ), 5 , 0
for epoch in range ( 1 , 51 ):
train_loss = train_one_epoch ( model , train_dl , opt )
val_loss , val_acc = evaluate ( model , val_dl )
if val_loss < best - 1e-4 :
best , bad = val_loss , 0
torch . save ({ "epoch" : epoch , "model" : model . state_dict (), "optimizer" : opt . state_dict ()}, "best.pt" )
else :
bad += 1
if bad >= patience :
break
model . load_state_dict ( torch . load ( "best.pt" )[ "model" ])
Before a long run: loss at init ≈ \(\ln C\) ; overfit one batch to ~0 loss; LR range test; then train with validation, early stopping, and checkpoints; evaluate on test once.
Saving and loading
Code
Use
torch.save(model.state_dict(), "model.pt")
weights only (inference)
model = MLP(...); model.load_state_dict(torch.load("model.pt"))
rebuild from code, then load
torch.save({"model": ..., "optimizer": ..., "scheduler": ..., "epoch": e, "torch_rng": torch.get_rng_state()}, p)
resumable checkpoint
torch.load(p, map_location="cpu")
load a GPU checkpoint on a CPU
torch.load(p) (weights_only=True default)
safe: tensors and plain containers only
model.load_state_dict(sd, strict=False)
partial loads (returns missing / unexpected keys)
torch.jit.script(model), torch.export.export(model, (x,))
export for deployment without Python class code
Never torch.save(model) for long-term storage (it pickles the class by reference), and never load pickles you didn't create with weights_only=False.
Devices and mixed precision
device = "cuda" if torch . cuda . is_available () else "mps" if torch . backends . mps . is_available () else "cpu"
model = model . to ( device )
xb = X [: 8 ] . to ( device , non_blocking = True )
with torch . autocast ( device_type = "cpu" , dtype = torch . bfloat16 ): # bf16: no scaler needed
logits = model ( xb . cpu () if device != "cpu" else xb )
scaler = torch . amp . GradScaler ( "cuda" ) # fp16 on CUDA needs loss scaling
for xb , yb in train_dl :
xb , yb = xb . to ( device ), yb . to ( device )
with torch . autocast ( device_type = "cuda" , dtype = torch . float16 ):
loss = F . cross_entropy ( model ( xb ), yb )
opt . zero_grad ( set_to_none = True )
scaler . scale ( loss ) . backward ()
scaler . unscale_ ( opt ) # before clipping
nn . utils . clip_grad_norm_ ( model . parameters (), 1.0 )
scaler . step ( opt ) # skips the step on inf/NaN
scaler . update ()
Fact
Detail
All operands on one device
no implicit transfers; move batches to the model's device
GPU calls are async
.item(), print, .cpu() synchronize; time with torch.cuda.synchronize()
Memory
params + grads + optimizer state (2× for Adam) + activations; shrink batch, use AMP
Autocast
matmul/conv in 16-bit; softmax, losses, norms in fp32; weights stay fp32
fp16 vs bf16
fp16: 5 exponent bits, needs GradScaler; bf16: fp32's range, less precision
Reproducibility
torch.manual_seed, seeded loader generator; on GPU torch.use_deterministic_algorithms(True)
Threads
torch.set_num_threads(n) on shared CPUs
Debugging
Symptom
Likely causes
Loss doesn't move
optimizer got the wrong params; LR ~0; no_grad/detach in the forward; frozen layers
Loss stuck above 0 on one batch
softmax before cross_entropy; labels misaligned; bottleneck
Loss NaN/inf
LR too high; log(0); division by zero; fp16 overflow (use a scaler or bf16)
Initial loss far from \(\ln C\)
unnormalized inputs; bad init; leakage if much lower
Great train, bad val
overfitting: augmentation, weight decay, dropout, early stopping, more data
Val worse than expected, noisy
forgot model.eval(); augmentation applied to val
Training worse after eval
forgot model.train()
expected ... same device
move inputs (and new tensors) to device
mat1 and mat2 must have the same dtype
float64 from NumPy: .float()
expected scalar type Long
class labels must be int64
view size is not compatible
use reshape or .contiguous()
Trying to backward through the graph a second time
a fresh forward per step; or detach carried state (RNN hidden)
modified by an inplace operation
replace x += ... / relu_ with out-of-place versions
Gradient is None
not a leaf (retain_grad), not used in the loss, or requires_grad=False
Shapes wrong at Linear after convs
compute \(C \cdot H \cdot W\) , or use AdaptiveAvgPool2d(1)
Useful checks: print(x.shape) everywhere; torch.autograd.set_detect_anomaly(True) (slow) to find the op that made a NaN; log clip_grad_norm_ return values; torch.isfinite(t).all(); compare against a tiny hand-computed case.