Bottleneck Layer (DenseNet-B)
DenseNet
Medium
Problem
Implement the DenseNet-B bottleneck transformation. It applies batch normalization, ReLU, and a bias-free 1 by 1 convolution that produces 4K channels, followed by batch normalization, ReLU, and a bias-free 3 by 3 convolution that produces $$ new feature maps.
h=\operatorname{Conv}_{1\times1}\!\left(\operatorname{ReLU}(\operatorname{BN}_1(x))\right)
y=\operatorname{Conv}_{3\times3}\!\left(\operatorname{ReLU}(\operatorname{BN}_2(h))\right)
Each batch-normalization operation uses its supplied scale, shift, running mean, running variance, and eps. The first convolution uses no padding, while the second uses padding 1. Return y as a float64 PyTorch tensor with shape (N,K,H,W).
Theory
The DenseNet bottleneck layer (Huang et al., 2017) is the composite function used inside every dense block of the DenseNet-B variant. It places a 1 \times 1 convolution before the expensive 3 \times 3 convolution to cut the number of input feature maps, keeping dense connectivity affordable as the network deepens.
What It Is
A dense block connects every layer to every other layer in a feed-forward fashion. Layer \ell receives the concatenation of all preceding feature maps as input, so its input channel count grows linearly with depth: C_0 + (\ell - 1) \cdot k, where k is the growth rate (the number of feature maps each layer adds). Even with a small k, the input to a deep layer can have hundreds of channels.
Running a 3 \times 3 convolution directly on that many input channels is expensive. The bottleneck layer fixes this by inserting a 1 \times 1 convolution that reduces the wide input down to a fixed width of 4k channels before the 3 \times 3 convolution runs. The paper calls this the BN-ReLU-Conv(1x1)-BN-ReLU-Conv(3x3) version, abbreviated DenseNet-B.
The two-stage composite is:
- Stage 1 (reduce): BatchNorm, ReLU, then a 1 \times 1 convolution that maps the wide input C channels to 4k channels.
- Stage 2 (produce): BatchNorm, ReLU, then a 3 \times 3 convolution that maps 4k channels down to exactly k new feature maps.
Spatial resolution H \times W is preserved throughout because the 1 \times 1 convolution needs no padding and the 3 \times 3 convolution uses padding 1 with stride 1. Preserving the spatial size is essential: every layer in a dense block must emit feature maps of the same height and width so that concatenation along the channel dimension is well defined.
It helps to picture the data flow inside one dense block. The block starts with C_0 channels. The first bottleneck layer reads those C_0 channels and emits k new ones, which are concatenated to form C_0 + k channels. The second bottleneck layer reads all C_0 + k channels and emits another k, giving C_0 + 2k, and so on. After L layers the block holds C_0 + Lk channels. The bottleneck's 1 \times 1 reduction is what keeps each layer's 3 \times 3 convolution cheap even as this concatenated stack widens.
Key Equations
Each batch normalization stage, in evaluation mode, uses the stored running mean \mu, running variance \sigma^2, learned scale \gamma, and learned shift \beta. For a channel c:
\text{BN}(x)_c = \gamma_c \cdot \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}} + \beta_c
where \epsilon is a small constant for numerical stability (default 10^{-5}). The full bottleneck composite, with input x \in \mathbb{R}^{N \times C \times H \times W}, is:
y_1 = \text{Conv}_{1\times1}\!\left(\text{ReLU}\!\left(\text{BN}_1(x)\right)\right) \in \mathbb{R}^{N \times 4k \times H \times W}
y_2 = \text{Conv}_{3\times3}\!\left(\text{ReLU}\!\left(\text{BN}_2(y_1)\right)\right) \in \mathbb{R}^{N \times k \times H \times W}
Both convolutions are bias-free. \text{BN}_1 normalizes over the C input channels, and \text{BN}_2 normalizes over the 4k intermediate channels. The output y_2 holds the k new feature maps that get concatenated onto the dense block's running stack.
Why the 4k Convention
The paper fixes the 1 \times 1 output width at 4k feature maps: "we let each 1 \times 1 convolution produce 4k feature-maps." This number is deliberately tied to the growth rate k rather than to the (variable) input width C.
- It decouples cost from depth. No matter how wide the dense-block input grows, the 3 \times 3 convolution always sees exactly 4k input channels. The most expensive operation has a constant cost per layer.
- It keeps a comfortable working width. Reducing straight to k before the 3 \times 3 convolution would starve it of channels. The factor of 4 gives the spatial convolution enough intermediate features to mix before collapsing to k outputs.
- It is a fixed hyperparameter, not learned. The 4 \times multiplier was chosen empirically and used across all DenseNet-BC configurations in the paper.
A concrete example: a deep layer with C = 256 input channels and k = 32. The 1 \times 1 convolution maps 256 \to 128 channels, then the 3 \times 3 convolution maps 128 \to 32. Without the bottleneck, the 3 \times 3 convolution would operate on all 256 input channels. Note that here 4k = 128 happens to be smaller than C = 256, so the layer genuinely reduces width before the spatial convolution.
FLOPs and Parameter Savings
For a feature map of size H \times W, a 3 \times 3 convolution from C_{in} to C_{out} channels costs roughly 9 \cdot C_{in} \cdot C_{out} \cdot H \cdot W multiply-adds. Compare the plain layer against the bottleneck for an input width C producing k outputs.
Plain composite (single 3 \times 3, C \to k):
\text{cost}_{\text{plain}} = 9 \cdot C \cdot k \cdot H \cdot W
Bottleneck (1 \times 1 then 3 \times 3):
\text{cost}_{\text{bneck}} = \big(1 \cdot C \cdot 4k + 9 \cdot 4k \cdot k\big) \cdot H \cdot W
When C is large (deep in a block) the 9 \cdot C \cdot k term of the plain layer dominates. The bottleneck replaces it with the much cheaper 1 \cdot C \cdot 4k for the channel reduction plus a fixed 9 \cdot 4k \cdot k spatial cost that does not depend on C. For C = 256, k = 32, the plain 3 \times 3 costs about 9 \cdot 256 \cdot 32 \approx 73{,}728 per pixel, while the bottleneck costs about 256 \cdot 128 + 9 \cdot 128 \cdot 32 \approx 69{,}632 per pixel, and the gap widens sharply as C keeps growing with depth. The savings are what let DenseNet stack many layers without exploding the compute budget.
The parameter counts follow the same pattern. The plain 3 \times 3 layer holds 9 \cdot C \cdot k weights, which grows with the block depth through C. The bottleneck holds C \cdot 4k weights for the 1 \times 1 convolution plus 9 \cdot 4k \cdot k = 36 k^2 weights for the 3 \times 3 convolution. The second term is constant in C, so most of the bottleneck's parameter growth comes from the cheap 1 \times 1 reduction. The paper reports that DenseNet-BC reaches the same or better accuracy than plain DenseNet while using substantially fewer parameters: for example, a 250-layer DenseNet-BC with $ = 24$ achieves strong CIFAR results with roughly 15.3M parameters, far fewer than comparable wide ResNets of the era.
The intuition is that dense connectivity already gives each layer direct access to all earlier features through concatenation, so individual layers can be made thin (small k). The bottleneck protects that thinness from being undone by the linearly growing input width.
Comparison with Related Designs
- Plain DenseNet composite (no bottleneck): just BN-ReLU-Conv(3 \times 3) producing k maps. Simpler, but the 3 \times 3 convolution sees the full, growing input width, so cost scales with depth.
- ResNet bottleneck (He et al., 2016): a 1 \times 1 reduce, a 3 \times 3 process, and a 1 \times 1 expand wrapped in a residual addition. It reduces and then restores the channel count, and the block output is added to the input. DenseNet's bottleneck has only two convolutions and never expands back: it produces a small fixed k outputs that are concatenated, not added.
- DenseNet-BC: combines this bottleneck (the B) with compression at transition layers (the C), where a factor \theta < 1 shrinks channels between blocks. The bottleneck handles cost within a block; compression handles cost between blocks.
The defining difference from ResNet is concatenation versus summation. ResNet adds the transformed signal back, so input and output widths must match, motivating the expand step. DenseNet concatenates, so each layer only needs to emit its k new maps and the bottleneck never restores the original width. This is why the DenseNet bottleneck has two convolutions while the ResNet bottleneck has three: there is nothing to add back, hence no expansion to the original channel count.
Pre-Activation Order
DenseNet uses the pre-activation ordering BN-ReLU-Conv, following the improved residual unit of He et al. (2016). Normalization and activation come before the convolution, not after.
- Why pre-activation: with concatenation, every layer's output is fed (unchanged) into all later layers. Applying BN-ReLU before each convolution lets each consuming layer normalize the shared features in its own way, while the raw concatenated signal flows forward cleanly.
- Convolutions are bias-free: the BatchNorm shift \beta already supplies a per-channel additive term, so a separate convolution bias would be redundant. Both the 1 \times 1 and 3 \times 3 convolutions omit bias.
- Evaluation mode: at inference, BatchNorm uses the stored running statistics rather than per-batch statistics. This problem supplies \mu, \sigma^2, \gamma, \beta directly for each stage, so the computation is fully deterministic.
Modern Context
The two ideas at the heart of this layer, channel reduction with 1 \times 1 convolutions and feature reuse, became standard tools in efficient architecture design after DenseNet.
- 1 \times 1 as a cheap channel mixer: the 1 \times 1 convolution is a per-pixel linear projection across channels. It costs almost nothing spatially yet controls width, which is exactly why it appears as the reduce step here, in ResNet bottlenecks, and in the pointwise step of depthwise-separable convolutions (MobileNet, Xception).
- Feature reuse over feature transformation: by concatenating thin k-channel outputs rather than transforming and discarding features, DenseNet gets strong gradient flow and parameter efficiency. Later designs such as EfficientNet and the CSPNet family revisit the same trade-off between reusing and recomputing features.
- Bounded inner width: fixing the intermediate width at 4k keeps the most expensive operation independent of depth. This pattern, capping the width of the costly inner computation, recurs in inverted-residual blocks and many later efficient blocks.
Understanding the DenseNet bottleneck therefore transfers directly to reasoning about most modern convolutional building blocks: identify the cheap channel-mixing step, the expensive spatial step, and the rule that decides how wide the intermediate representation should be.
Worked Example
Take a tiny case: N = 1, C = 2, H = W = 4, growth rate k = 2 so the intermediate width is 4k = 8.
Stage 1 BN: normalize the 2 input channels with \gamma, \beta, \mu, \sigma^2 of length 2 using \hat{x}_c = \gamma_c (x_c - \mu_c)/\sqrt{\sigma_c^2 + \epsilon} + \beta_c.
Stage 1 ReLU: clamp negatives to zero element-wise.
Stage 1 Conv 1 \times 1: apply \text{conv1\_weight} of shape (8, 2, 1, 1) with padding 0, stride 1, no bias. Output has shape (1, 8, 4, 4). A 1 \times 1 convolution is a per-pixel linear mix across the 2 input channels into 8 output channels.
Stage 2 BN: normalize the 8 intermediate channels with length-8 statistics.
Stage 2 ReLU: clamp negatives to zero.
Stage 2 Conv 3 \times 3: apply \text{conv2\_weight} of shape (2, 8, 3, 3) with padding 1, stride 1, no bias. Output has shape (1, 2, 4, 4), the k = 2 new feature maps. Padding 1 preserves the 4 \times 4 spatial size.
The final tensor has shape (N, k, H, W) = (1, 2, 4, 4). These two maps would then be concatenated onto the dense block's feature stack.
Pitfalls
- Producing k feature maps from the 1 \times 1 layer instead of 4k. The 1 \times 1 convolution must output 4k channels; only the final 3 \times 3 convolution produces k. Collapsing to k too early starves the 3 \times 3 stage and mismatches the stage-2 BatchNorm channel count.
- Wrong padding on the convolutions. The 1 \times 1 convolution uses padding 0 and the 3 \times 3 uses padding 1. Swapping them changes the spatial output size: padding 1 on a 1 \times 1 grows the map, and padding 0 on a 3 \times 3 shrinks it by two rows and columns.
- Dropping a BatchNorm or ReLU. The composite is BN-ReLU-Conv twice. Forgetting the stage-2 BatchNorm or either ReLU silently produces wrong values without any shape error, which makes the bug hard to spot.
- Broadcasting BatchNorm statistics over the wrong axis. The per-channel vectors \gamma, \beta, \mu, \sigma^2 must broadcast over the channel dimension of an NCHW tensor, that is, reshaped to (1, C, 1, 1). Broadcasting over the batch or spatial axis applies the wrong statistic to every value.
Examples
Example 1
- Input
x = [[[[1,-1],[0.5,2]]]], bn1_gamma = [1], bn1_beta = [0], bn1_mean = [0], bn1_var = [1], conv1_weight = [[[[0.2]]],[[[0.1]]],[[[-0.1]]],[[[0.3]]]], bn2_gamma = [1,0.8,1.2,0.9], bn2_beta = [0,0.1,0,-0.1], bn2_mean = [0,0,0,0], bn2_var = [1,1,1,1], conv2_weight = [[[[0.06927,-0.19429,-0.06018],[0.11335,-0.02464,-0.00181],[-0.07164,0.17625,-0.06495]],[[0.10777,-0.14501,0.01642],[0.04517,-0.13683,-0.15729],[0.15147,-0.10728,-0.17704]],[[0.17439,0.00698,-0.0007],[0.09205,-0.1057,-0.14218],[-0.0223,0.09152,-0.01798]],[[-0.06253,0.03027,-0.06378],[0.19059,0.14828,-0.03954],[0.06622,-0.09118,-0.05218]]]], eps = 0.00001- Output
[[[[-0.115633,0.068365],[-0.133619,0.052261]]]]- Explanation
- The first projection creates four intermediate channels before the spatial convolution contributes one new feature map.
Example 2
- Input
x.shape = (1, 2, 3, 3), bn1_gamma.shape = (2), bn1_beta.shape = (2), bn1_mean.shape = (2), bn1_var.shape = (2), conv1_weight.shape = (4, 2, 1, 1), bn2_gamma.shape = (4), bn2_beta.shape = (4), bn2_mean.shape = (4), bn2_var.shape = (4), conv2_weight.shape = (1, 4, 3, 3), eps = 0.00001- Output
[[[[0.03558,0.016871,0.151669],[0.105243,0.09452,0.269204],[0.14974,0.16084,0.189147]]]]
Example 3
- Input
x.shape = (2, 1, 2, 3), bn1_gamma.shape = (1), bn1_beta.shape = (1), bn1_mean.shape = (1), bn1_var.shape = (1), conv1_weight.shape = (8, 1, 1, 1), bn2_gamma.shape = (8), bn2_beta.shape = (8), bn2_mean.shape = (8), bn2_var.shape = (8), conv2_weight.shape = (2, 8, 3, 3), eps = 0.00001- Output
[[[[0.026473,0.18821,0.14735],[0.070335,0.106878,0.153801]],[[0.099331,-0.084862,-0.041676],[-0.228215,-0.260064,-0.151901]]],[[[0.074625,0.093452,0.044857],[0.212937,0.230867,0.262375]],[[-0.087769,-0.231495,-0.030403],[-0.219811,-0.235509,-0.122065]]]]
Hints
- Broadcast each batch-normalization vector over the batch and spatial axes.
- Use padding=0 for the 1 by 1 convolution and padding=1 for the 3 by 3 convolution.
Requirements
- Use PyTorch.
- Apply both BN-ReLU-Conv stages in the documented order.
- Use the supplied running statistics and affine parameters.
- Use no convolution biases.
- Return the documented float64 tensor.
Constraints
- x has shape (N, C, H, W) and dtype torch.float64.
- The first batch-normalization vectors have shape (C).
- conv1_weight has shape (4K, C, 1, 1).
- The second batch-normalization vectors have shape (4K).
- conv2_weight has shape (K, 4K, 3, 3).
- eps is positive.
Starter Code
import torch
import torch.nn.functional as F
def bottleneck_layer(x: torch.Tensor, bn1_gamma: torch.Tensor, bn1_beta: torch.Tensor,
bn1_mean: torch.Tensor, bn1_var: torch.Tensor,
conv1_weight: torch.Tensor, bn2_gamma: torch.Tensor,
bn2_beta: torch.Tensor, bn2_mean: torch.Tensor,
bn2_var: torch.Tensor, conv2_weight: torch.Tensor,
eps: float = 1e-5) -> torch.Tensor:
"""
Returns the float64 output of the DenseNet-B bottleneck transformation.
"""
passTest Cases
| Case | Matches | |
|---|---|---|
| One input channel | — | public |
| Two input channels | — | public |
| Two growth channels | — | public |