MediumDenseNet

Transition Layer

DenseNet

Medium

Problem

Implement the transition between two DenseNet blocks. Apply evaluation-mode batch normalization, ReLU, a bias-free 1 by 1 convolution, and 2 by 2 average pooling with stride 2.

z=\operatorname{Conv}_{1\times1}\!\left(\operatorname{ReLU}(\operatorname{BN}(x))\right)

y_{n,c,i,j}=\frac{1}{4}\sum_{a=0}^{1}\sum_{b=0}^{1}z_{n,c,2i+a,2j+b}

Here, the supplied convolution weight determines the compressed output width C_{\mathrm{out}}. Return y as a float64 PyTorch tensor with shape (N,C_{\mathrm{out}},H/2,W/2).

Theory

The transition layer is the connective tissue between dense blocks in DenseNet (Huang et al., 2017). It performs two jobs at once: it compresses the number of feature-map channels with a 1 \times 1 convolution, and it halves the spatial resolution with 2 \times 2 average pooling. Without it, the channel count produced by dense connectivity would explode and the spatial maps would never shrink.


Why Transitions Exist

Inside a dense block, every layer receives the concatenation of all preceding feature maps. If a block has L layers and each layer adds k channels (the growth rate), the block input of C_0 channels grows to C_0 + L \cdot k channels by the end. Stacking several blocks back to back would compound this growth into thousands of channels, which is expensive in memory and compute.

Dense connectivity also requires that all feature maps inside a block share the same spatial size, otherwise concatenation along the channel axis is undefined. This means downsampling cannot happen inside a block. The network therefore needs a dedicated component, placed between blocks, that both reduces channels and reduces spatial resolution. That component is the transition layer.

The transition layer solves two problems with one module:


The Operation

A transition layer applies, in order: batch normalization, a ReLU nonlinearity, a 1 \times 1 convolution, and then 2 \times 2 average pooling. For an input x with C channels, the per-channel batch-normalized and activated tensor is:

\hat{x}_c = \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}}, \qquad y_c = \text{ReLU}\!\left(\gamma_c \, \hat{x}_c + \beta_c\right)

where \mu_c and \sigma_c^2 are the running mean and variance for channel c, \gamma_c and \beta_c are the learned scale and shift, and \epsilon is a small constant for numerical stability (typically 10^{-5}).

The 1 \times 1 convolution then mixes channels at each spatial location. With weight W \in \mathbb{R}^{C_{out} \times C \times 1 \times 1} and no bias, the output at position (h, w) for output channel o is:

z_{o,h,w} = \sum_{c=1}^{C} W_{o,c} \cdot y_{c,h,w}

Finally, non-overlapping 2 \times 2 average pooling with stride 2 reduces each spatial dimension by half:

\text{out}_{o,i,j} = \frac{1}{4}\sum_{a=0}^{1}\sum_{b=0}^{1} z_{o,\,2i+a,\,2j+b}

For an input of shape (N, C, H, W), the output has shape (N, C_{out}, H/2, W/2).


Compression and the Theta Hyperparameter

The number of output channels C_{out} produced by the 1 \times 1 convolution is the compression knob. The paper introduces a hyperparameter \theta \in (0, 1] called the compression factor. If a dense block emits m feature maps, the following transition layer produces \lfloor \theta m \rfloor output channels.

In an implementation, \theta is not passed explicitly. It is encoded by the shape of the convolution weight: a weight of shape (C_{out}, C, 1, 1) implies \theta = C_{out} / C. The forward pass simply reads C_{out} from the weight tensor and produces that many output channels.


Why a 1x1 Convolution

A 1 \times 1 convolution is the cheapest way to change channel count while preserving spatial structure. It has no spatial receptive field: each output pixel depends only on the same spatial location across input channels. This makes it a learned linear projection applied identically at every pixel.


Average Pooling vs Max Pooling

DenseNet uses average pooling in its transition layers, not max pooling. This is a deliberate choice that fits dense connectivity.

Using max pooling here changes the numerical output entirely (it selects the maximum of each window instead of the mean), so it is one of the most common implementation mistakes.


Where Transitions Sit in the Network

A DenseNet is a sequence of dense blocks separated by transition layers. A typical DenseNet for ImageNet has four dense blocks and therefore three transition layers, one between each adjacent pair of blocks.


Comparison with ResNet Downsampling

ResNet (He et al., 2016) handles downsampling differently. It uses strided convolutions: the first convolution of certain residual blocks has stride 2, which both reduces spatial size and changes channels in a single learned operation, and the skip connection uses a strided 1 \times 1 projection to match dimensions.


Parameter and Compute Cost

The transition layer is intentionally lightweight relative to the dense blocks around it. Its only learned parameters are the batch-norm affine terms (\gamma, \beta, two values per input channel) and the 1 \times 1 convolution weights.

This is a large part of why DenseNet-BC reaches strong accuracy with far fewer parameters than comparable ResNets: compression at each transition keeps the per-block channel counts from ballooning, and the 1 \times 1 projection is cheap.


Worked Example (N=1, C=2, H=W=4, C_{out}=1, \epsilon=0)

Suppose channel 0 of x is all ones and channel 1 is all twos, with \gamma = [1, 1], \beta = [0, 0], \mu = [1, 2], \sigma^2 = [1, 1]. The input map is (1, 2, 4, 4), so after the convolution to one channel and the 2 \times 2 pool we expect a (1, 1, 2, 2) output. Walking through the four stages by hand confirms both the values and the shape.

  1. Batch norm: \hat{x}_0 = (1 - 1)/\sqrt{1} = 0 for every pixel of channel 0, and \hat{x}_1 = (2 - 2)/\sqrt{1} = 0 for channel 1. So \hat{x} is all zeros.

  2. ReLU: \gamma \hat{x} + \beta = 0, and \text{ReLU}(0) = 0. The activated tensor y is all zeros.

  3. 1 \times 1 convolution: with weight W = [[0.5], [0.5]] (shape (1, 2, 1, 1)), each output pixel is 0.5 \cdot 0 + 0.5 \cdot 0 = 0. The 4 \times 4 output map is all zeros.

  4. Average pool 2 \times 2 stride 2: each $ \times 2$ window averages to 0, giving a 2 \times 2 output of zeros. Final shape is (1, 1, 2, 2).

Now change channel 0 of x to all twos (mean still 1). Then \hat{x}_0 = (2 - 1)/1 = 1, ReLU keeps it at 1, the convolution gives $ \cdot 1 + 0.5 \cdot 0 = 0.5$ per pixel, and average pooling preserves 0.5. The output is a 2 \times 2 map filled with 0.5. This shows how each stage transforms the values.


Modern Context and Variants

The transition layer pattern of normalize, project, downsample shows up across many later architectures, though the exact components vary.

The enduring lesson from the DenseNet transition is that downsampling and channel control can be cleanly decoupled from feature extraction, and that average pooling is a reasonable default when the architecture relies on reusing features rather than selecting the strongest activations.


Pitfalls


Examples

Example 1

Input
x = [[[[1,2],[3,4]],[[-1,0],[1,2]]]], bn_gamma = [1,0.5], bn_beta = [0,0.1], bn_mean = [0,0], bn_var = [1,1], conv_weight = [[[[0.5]],[[-0.25]]]], eps = 0.00001
Output
[[[[1.137494]]]]
Explanation
The 1 by 1 convolution compresses channels before average pooling halves both spatial dimensions.

Example 2

Input
x.shape = (1, 3, 4, 4), bn_gamma.shape = (3), bn_beta.shape = (3), bn_mean.shape = (3), bn_var.shape = (3), conv_weight.shape = (2, 3, 1, 1), eps = 0.00001
Output
[[[[0.003041,0.042495],[0.043089,0.028172]],[[-0.157591,0.026717],[-0.047066,-0.01502]]]]

Example 3

Input
x.shape = (2, 2, 2, 4), bn_gamma.shape = (2), bn_beta.shape = (2), bn_mean.shape = (2), bn_var.shape = (2), conv_weight.shape = (1, 2, 1, 1), eps = 0.00001
Output
[[[[-0.218039,-0.293813]]],[[[-0.231119,-0.523215]]]]

Hints

  1. Broadcast each batch-normalization vector to (1, C, 1, 1).
  2. Use F.avg_pool2d with kernel_size=2 and stride=2.

Requirements

Constraints

Starter Code

import torch
import torch.nn.functional as F

def transition_layer(x: torch.Tensor, bn_gamma: torch.Tensor, bn_beta: torch.Tensor,
                     bn_mean: torch.Tensor, bn_var: torch.Tensor,
                     conv_weight: torch.Tensor, eps: float = 1e-5) -> torch.Tensor:
    """
    Returns the float64 output of the DenseNet transition layer.
    """
    pass

Test Cases

CaseMatches
Two channelspublic
Larger spatial mappublic
Batch of twopublic