MediumDenseNet

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:

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.

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.


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.


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.

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.

  1. 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.

  2. Stage 1 ReLU: clamp negatives to zero element-wise.

  3. 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.

  4. Stage 2 BN: normalize the 8 intermediate channels with length-8 statistics.

  5. Stage 2 ReLU: clamp negatives to zero.

  6. 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


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

  1. Broadcast each batch-normalization vector over the batch and spatial axes.
  2. Use padding=0 for the 1 by 1 convolution and padding=1 for the 3 by 3 convolution.

Requirements

Constraints

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.
    """
    pass

Test Cases

CaseMatches
One input channelpublic
Two input channelspublic
Two growth channelspublic