Day 65: Building a tiny U-Net from scratch (part 1): encoder
The contracting path, in PyTorch
Build U-Net yourself — it's the most concrete way to internalize an architecture, and it pays off directly in Stage 4. Today: the encoder. It's the canonical conv block from Day 35 (conv → BN → ReLU, ×2) followed by max-pooling to halve resolution, repeated. Crucially, you keep each block's pre-pool output — that's what the decoder's skip connections will consume.
import torch.nn as nn
def conv_block(in_c, out_c):
return nn.Sequential(
nn.Conv2d(in_c, out_c, 3, padding=1), nn.BatchNorm2d(out_c), nn.ReLU(),
nn.Conv2d(out_c, out_c, 3, padding=1), nn.BatchNorm2d(out_c), nn.ReLU(),
)
class Encoder(nn.Module):
def __init__(self):
super().__init__()
self.b1, self.b2, self.b3 = conv_block(3,64), conv_block(64,128), conv_block(128,256)
self.pool = nn.MaxPool2d(2)
def forward(self, x):
s1 = self.b1(x) # save for skip
s2 = self.b2(self.pool(s1)) # save for skip
s3 = self.b3(self.pool(s2)) # bottleneck-ish
return s3, [s1, s2] # deepest features + skipsYou already know every piece
There's nothing new here — it's Day-34 convolutions, Day-35 batchnorm and pooling, arranged in the Day-64 shape. Building U-Net is assembly, not new theory, precisely because Stage 1 front-loaded the components. That's the payoff of the roadmap's insistence on fundamentals: new architectures become recombinations of things you understand.
Key terms
- Contracting path
- U-Net's encoder: repeated conv blocks and pooling that shrink spatial size and deepen features.
- Skip output
- An encoder block's pre-pooling feature map, saved to be concatenated into the decoder later.
Why does the U-Net encoder save each block's output before pooling?