HardDenseNet

Full DenseNet Forward Pass

DenseNet

Hard

Problem

Implement a deterministic DenseNet forward pass. Apply the supplied 3 by 3 stem convolution, then each dense block in order with a transition between adjacent blocks. Finish with batch normalization, ReLU, global average pooling, and the supplied linear classifier.

x_{\ell}=H_{\ell}\!\left([x_0,x_1,\ldots,x_{\ell-1}]\right)

p_{n,c}=\frac{1}{HW}\sum_{h=1}^{H}\sum_{w=1}^{W}z_{n,c,h,w}

\operatorname{logits}=pW_{\mathrm{fc}}^{\mathsf T}+b_{\mathrm{fc}}

The weights dictionary contains stem_conv, blocks, transitions, the four final batch-normalization vectors, fc_weight, and fc_bias. Each block is an ordered list of composite-layer dictionaries, and each transition supplies its own batch-normalization vectors and 1 by 1 convolution. Return the class logits as a float64 PyTorch tensor with shape (N,C_{\mathrm{classes}}).

Theory

DenseNet (Huang et al., 2017) is a convolutional architecture in which every layer receives the feature maps of all preceding layers within a block as input. The full forward pass assembles a stem convolution, several dense blocks separated by transition layers, a final normalization, global average pooling, and a linear classifier into one end to end function from an image to class logits.


What the Full Network Computes

A DenseNet maps an input image x \in \mathbb{R}^{N \times C_{in} \times H \times W} to logits z \in \mathbb{R}^{N \times K} over K classes. The paper organizes the network into a small number of dense blocks (typically 3 or 4) joined by transition layers that downsample. The repeating motif is the dense block: a stack of composite layers where layer \ell sees the concatenation of all earlier feature maps.

The end to end pipeline is:


The Composite Layer

Each layer inside a block is a composite function H_\ell. In the plain (non bottleneck) form used here, H_\ell is batch normalization, then ReLU, then a 3 \times 3 convolution with padding 1 that outputs exactly $$ feature maps, where k is the growth rate:

H_\ell(x) = \text{Conv}_{3\times3}\big(\text{ReLU}(\text{BN}(x))\big)

The pre activation order (BN then ReLU then convolution) follows the identity mappings work of He et al. (2016). It matters: putting normalization before the convolution keeps the concatenated inputs on a comparable scale even though they originate from layers at very different depths.

Batch normalization in inference uses the stored running statistics, so for a channel c the normalized activation is:

\hat{x}_c = \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}}, \qquad y_c = \gamma_c \hat{x}_c + \beta_c

where \mu_c, \sigma_c^2 are the running mean and variance, and \gamma_c, \beta_c are the learned scale and shift. The padding of 1 on the $ \times 3$ convolution keeps the spatial size unchanged so that all feature maps inside a block remain concatenable.


Dense Connectivity Inside a Block

The defining idea of DenseNet is dense connectivity. Layer \ell receives the feature maps of all preceding layers as input, formed by concatenation along the channel axis:

x_\ell = H_\ell\big([x_0, x_1, \ldots, x_{\ell-1}]\big)

Here x_0 is the block input and [\cdot] is channel concatenation. A block with L composite layers and growth rate k that starts with C_0 channels ends with C_0 + L \cdot k channels. Because each layer adds only k maps, the network stays narrow even though connectivity is dense.

The implementation maintains a running list of feature maps, appends each layer's k new maps, and concatenates the accumulated list to feed the next layer:

This is the contrast with ResNet, where the shortcut is additive: x_\ell = H_\ell(x_{\ell-1}) + x_{\ell-1}. Addition combines features by summation, which can impede information flow; concatenation preserves every feature map intact and lets later layers selectively reuse them.


Transition Layers

Dense blocks keep spatial resolution fixed, so the network needs explicit downsampling between blocks. A transition layer does this and also compresses the channel count:

\text{Transition}(x) = \text{AvgPool}_{2\times2}\Big(\text{Conv}_{1\times1}\big(\text{ReLU}(\text{BN}(x))\big)\Big)

The 1 \times 1 convolution has no bias and outputs a reduced number of channels. The paper introduces a compression factor \theta \in (0, 1]: a transition that follows a block with m channels produces \lfloor \theta m \rfloor output channels. DenseNet-BC uses \theta = 0.5. The 2 \times 2 average pool with stride 2 then halves both height and width. Transitions appear between blocks only, never after the last block, so a network with $$ blocks has exactly B - 1 transitions.


Global Average Pooling and the Classifier

After the final dense block, the network applies one more batch norm and ReLU, then collapses the spatial dimensions with global average pooling. For a feature tensor of shape (N, C, H, W) the pooled vector is:

p_{n,c} = \frac{1}{H W} \sum_{i=1}^{H} \sum_{j=1}^{W} x_{n,c,i,j}

producing (N, C). Global average pooling, popularized by Network in Network (Lin et al., 2014), replaces large fully connected layers with a single spatial average per channel. It removes a huge number of parameters and imposes a useful structural prior: each channel of the final feature map acts as a confidence map for a concept, and its spatial average is the evidence for that concept.

The classifier is a single linear layer applied to the pooled vector:

z = p\, W_{fc}^\top + b_{fc}

with W_{fc} \in \mathbb{R}^{K \times C} and b_{fc} \in \mathbb{R}^{K}, giving logits of shape (N, K).


Standard DenseNet Configurations

The paper defines a family of networks (Table 1) that all share four dense blocks with growth rate k = 32 and the BC variant (1 \times 1 bottleneck plus \theta = 0.5 compression). They differ only in the number of composite layers per block:

The number in the name counts layers with learnable weights: convolutions in the composite layers and transitions, plus the stem and the classifier. The depth grows but the parameter count stays modest because the per layer growth k is small and transitions repeatedly compress the width.


Bottleneck and Compression in Full Models

In the full DenseNet-BC, the composite layer is augmented with a bottleneck: a 1 \times 1 convolution produces 4k feature maps before the 3 \times 3 convolution. The bottleneck caps the cost of the 3 \times 3 convolution, whose input width grows with every layer. Combined with transition compression \theta = 0.5, BC models reach the best accuracy per parameter. This problem deliberately uses the plain composite layer (BN-ReLU-3 \times 3Conv only) so the forward logic stays tractable; the dense connectivity, transitions, pooling, and classifier are identical to the full model.


Why Dense Connectivity Helps at Network Scale

The benefits of concatenative connectivity are clearest when reasoning about the whole network rather than a single block.

A subtle cost is memory. Naive concatenation stores every intermediate feature map for the backward pass, so memory grows quadratically with block depth. The paper and follow up work address this with shared memory allocations and recomputation, but the forward computation itself is exactly the concatenation described here.


Implementation Order and Numerical Notes

The forward pass is sensitive to the order of operations, and several conventions must be matched exactly to reproduce reference logits:


Comparison with ResNet Forward

Both DenseNet and ResNet build very deep networks by giving gradients short paths back to early layers, but the mechanism differs:


Worked Example (tiny network)

Take N = 1, C_{in} = 2, H = W = 8, stem width C_0 = 4, growth rate k = 2, two blocks of two layers each, and one transition, with K = 3 classes.

The key invariant to track at every step is the channel count: it grows by k per composite layer and is reset by each transition.


Pitfalls


Examples

Example 1

Input
x = [[[[-0.15441,0.38403],[-0.03968,0.04415]]]], weights = {"stem_conv":[[[[-0.19179,-0.20879,-0.10259],[-0.19144,-0.02615,-0.15921],[-0.07057,0.06929,0.08283]]]],"blocks":[[{"bn_gamma":[0.69251],"bn_beta":[-0.1198],"bn_mean":[0.10282],"bn_var":[0.95959],"conv_weight":[[[[-0.13171,-0.19187,0.19535],[-0.04574,0.13067,0.15458],[-0.15993,-0.19898,0.09224]]]]}]],"transitions":[],"final_bn_gamma":[0.89042,0.59945],"final_bn_beta":[0.11602,0.29186],"final_bn_mean":[0.28338,-0.11992],"final_bn_var":[1.27184,0.65231],"fc_weight":[[0.04251,-0.0161],[-0.0053,0.0332]],"fc_bias":[-0.00133,0.11938]}, growth_rate = 1, eps = 0.00001
Output
[[-0.007462,0.132025]]
Explanation
The supplied stem, dense layer, final normalization, pooling, and classifier are applied in order.

Example 2

Input
x.shape = (2, 1, 4, 4), weights = supplied DenseNet weights, growth_rate = 1, eps = 0.00001
Output
[[-0.110528,-0.069652],[-0.11093,-0.069368]]

Example 3

Input
x.shape = (1, 2, 3, 3), weights = supplied DenseNet weights, growth_rate = 2, eps = 0.00001
Output
[[0.250917,-0.325549,0.058388]]

Hints

  1. Implement one helper for the supplied evaluation-mode BN-ReLU operation.
  2. Keep a feature list inside each block and concatenate it before every layer.
  3. Average the final feature map over dimensions 2 and 3 before the classifier.

Requirements

Constraints

Starter Code

import torch
import torch.nn.functional as F

def densenet_forward(x: torch.Tensor, weights: dict, growth_rate: int,
                     eps: float = 1e-5) -> torch.Tensor:
    """
    Returns the float64 class logits from the complete DenseNet forward pass.
    """
    pass

Test Cases

CaseMatches
Single blockpublic
Two blockspublic
Two-layer blockpublic