Residual and Skip Connections
Chain enough shrinking layers and the gradient reaching the first one is indistinguishable from zero. A skip connection adds a path the gradient cannot shrink along.
Overview
What this is
Stack enough layers and, by the chain rule, the gradient reaching an early layer is the product of every local derivative between it and the loss. If each of those derivatives is reliably less than 1 — true of sigmoid and tanh almost everywhere — the product shrinks geometrically. Ten layers at a local derivative of 0.55 already leaves the input gradient at about 0.0025 of what it started as; twenty leaves it at six millionths. That is the vanishing gradient, and it is why very deep plain networks were nearly untrainable before residual connections.
The Stack
every layer squashes its input by the same factor (local derivative 0.55) — realistic for a deep sigmoid/tanh stack
Gradient Magnitude, Layer By Layer
—At The Input Layer
Residual Connections: A Practical Guide
A shortcut for the gradient, not just for the signal.
What a skip connection changes
plain: xi+1 = f(xi) → dxi+1/dxi = f'(xi)
residual: xi+1 = xi + f(xi) → dxi+1/dxi = 1 + f'(xi)
Adding the input back on gives the local derivative a constant +1 term. Whatever f' does — shrink toward zero, even go slightly negative — the identity path contributes exactly 1 at every layer, so the product across the whole stack can no longer collapse toward zero the way a chain of pure f' terms does. The gradient always has at least the unimpeded identity route back to the input.
Add the input back
A residual connection adds a layer's input to its output:
output = F(x) + x
F is whatever the block computes — typically two convolutions with normalisation and activation, or an attention sublayer in a transformer. The + x is the shortcut, and it costs nothing: no parameters, one addition.
That one addition is arguably the most important architectural idea in modern deep learning. Before it, networks beyond about 20 layers trained badly. After it, 152 layers became routine and 100-layer transformers followed.
The two reasons it works
Identity becomes easy to represent. Ask a block to produce the output directly and it must learn a mapping. Ask it to produce the difference and it can output nothing — drive the weights towards zero — and the block passes its input through unchanged.
That matters because deep plain networks were failing on training data, not just test data. A 56-layer network scored worse than a 20-layer one at fitting the training set, despite being able in principle to copy the shallower network and make the extra layers do nothing. It could not find that solution. Residual connections make it the default starting point.
Gradients get a clean path. Differentiating F(x) + x with respect to x gives F'(x) + 1. That constant term means the gradient reaching earlier layers can never be crushed to nothing by the product of many small factors — there is always a route back with a factor of exactly 1.
Where the addition goes
Two details matter in implementation.
The addition happens before the final activation in the original ResNet block:
x → Conv → BN → ReLU → Conv → BN → (+x) → ReLU
Pre-activation ordering trains deeper. Moving normalisation and activation to the start of the block — BN → ReLU → Conv → BN → ReLU → Conv → (+x) — leaves the identity path with nothing on it at all, and was shown to train 1,000-layer networks. Transformers use this arrangement: x + sublayer(norm(x)).
Shapes must match. F(x) + x requires both terms to have the same shape. When a block changes the channel count or downsamples, the shortcut needs a 1×1 convolution with matching stride to project x. Frameworks handle it; hand-written blocks frequently forget, and the error is immediate.
class ResidualBlock(nn.Module):
def __init__(self, cin, cout, stride=1):
super().__init__()
self.body = nn.Sequential(
nn.Conv2d(cin, cout, 3, stride, 1, bias=False), nn.BatchNorm2d(cout),
nn.ReLU(inplace=True),
nn.Conv2d(cout, cout, 3, 1, 1, bias=False), nn.BatchNorm2d(cout),
)
self.short = nn.Identity() if (stride == 1 and cin == cout) else nn.Sequential(
nn.Conv2d(cin, cout, 1, stride, bias=False), nn.BatchNorm2d(cout))
def forward(self, x):
return torch.relu(self.body(x) + self.short(x))
Exploration guide
- Read the plain stack at 10 layers. The gradient at the input is 0.5510 ≈ 0.0025 — over 99% of it is gone before it reaches the first layer.
- Turn on skip connections. The same ten layers, the same 0.55 local derivative, and the input gradient jumps to 1.5510 ≈ 80 — nowhere near zero.
- Push the layer count to 16. Without skips, the bars nearest the input are visually gone — the y-axis is logarithmic and they are still off the bottom. With skips, every bar stays a real, usable number.
- Note what does not change. The per-layer factor is identical in both cases — 0.55, the same squashing. Nothing about the layers themselves improved; only the path the gradient can take did.
Where that leaves you
A residual connection adds the block's input back onto its output, which adds a constant 1 to that layer's local derivative during backpropagation. A chain of derivatives all below 1 shrinks geometrically and vanishes; a chain that includes a guaranteed +1 at every step cannot collapse toward zero regardless of how small the learned part's derivative is. This is why residual connections, not just wider layers or better optimisers, were the change that made networks with over a hundred layers trainable at all.
Add or concatenate?
Two ways to reuse an earlier layer's output, with different trade-offs.
Addition (ResNet) keeps the channel count constant and costs nothing. The two signals are summed, so they must be comparable in scale.
Concatenation (DenseNet, U-Net) stacks them along the channel axis, preserving both separately and letting the next layer decide how to weigh them. It grows the width, so the following convolution is more expensive.
| Addition | Concatenation | |
|---|---|---|
| Channel count | Unchanged | Grows |
| Cost | Free | Wider next layer |
| Information | Merged | Both kept separately |
| Used by | ResNet, transformers | DenseNet, U-Net |
U-Net's choice is instructive. Its skip connections carry high-resolution boundary detail from the encoder to the decoder, where the deep path carries semantics. Concatenating keeps those two very different signals distinct rather than summing them into one, and the following convolution learns how to combine them.
Everywhere, in one form or another
The idea escaped computer vision within a year:
- Transformers. Every block is
x + attention(norm(x))thenx + ffn(norm(x)). Without these, deep transformers do not train. - U-Net. Encoder-to-decoder skips are what keep segmentation boundaries sharp.
- LSTM and GRU. The additive cell-state path is the recurrent equivalent, and it is why they handle far longer sequences than a plain RNN.
- Diffusion models. Built on residual U-Nets.
- DenseNet. Every layer receives the concatenated outputs of all previous ones.
- Highway networks preceded ResNet with a gated version, where a learned gate decided how much to pass through. ResNet showed the ungated version works better and is simpler.
If you take one design rule from this topic: any network deeper than about ten layers should have residual connections.
The ensemble view
A network with n residual blocks contains 2ⁿ possible paths from input to output, since each block can be traversed or effectively skipped.
The empirical evidence for this reading is striking: removing a single block from a trained ResNet barely changes its accuracy. Removing a layer from a plain deep network destroys it. A residual network behaves much more like an ensemble of many shallow networks than like one very deep one.
That also explains why residual networks are unusually robust to layer dropping and to architectural surgery, and why stochastic depth — randomly skipping whole blocks during training — works as a regulariser.
Add the input back, and depth stops hurting
A plain 40-layer network trains worse than a 10-layer one. The same network with skip connections does not, and the reason is visible in one line of the backward pass.
Questions people ask
Do residual connections have parameters? Not when the shapes match. A projection shortcut has a 1×1 convolution's worth.
Can I add them to any network? Yes, wherever the shapes match, and it usually helps beyond a few layers deep.
Why not just use fewer layers? Depth buys representational efficiency — composed features rather than enumerated ones. Residuals are what make depth usable.
Do they prevent overfitting? No, they address an optimisation problem. Regularisation is a separate concern.
What if I scale the shortcut? Multiplying it by a constant less than 1 reintroduces decay through depth. Keep it at exactly 1.
Is initialising the last normalisation to zero a real technique? Yes — setting γ = 0 in the final normalisation of each block makes the block start as an exact identity, which stabilises very deep and very large models.
Recap in one screen
output = F(x) + x: the block learns the difference, so doing nothing is trivially available.- The derivative gains a
+1, giving gradients a route back that cannot vanish. - Shapes must match; use a 1×1 projection when channels or stride change.
- Addition keeps width constant; concatenation keeps both signals separate and grows it.
- Present in transformers, U-Net, LSTMs and diffusion models — anything deep uses some version of it.