Dense Block (Concatenative Connectivity)
DenseNet
Hard
Problem
Implement a DenseNet block with concatenative connectivity. Layer \ell receives the original block input and every preceding layer output concatenated along the channel axis.
x_{\ell}=H_{\ell}\!\left([x_0,x_1,\ldots,x_{\ell-1}]\right)
H_{\ell}(x)=\operatorname{Conv}_{3\times3}\!\left(\operatorname{ReLU}(\operatorname{BN}(x))\right)
Here, each layer dictionary supplies its batch-normalization parameters and convolution weight, while growth_rate is the number K of channels added by every layer. Return [x_0,x_1,\ldots,x_L] as one float64 PyTorch tensor with shape (N,C+LK,H,W).
Theory
A dense block is the core building unit of DenseNet (Huang et al., 2017). Inside a block, every layer receives the feature maps of all preceding layers as input, and passes its own feature maps to all subsequent layers. Connectivity is expressed by channel-wise concatenation, not by addition. This single design choice gives DenseNet strong feature reuse, very thin layers, efficient parameter usage, and excellent gradient flow.
What a Dense Block Computes
A dense block contains L composite layers. Let x_0 be the block input (a tensor with C channels). Each composite layer H_\ell is a small function (here BN-ReLU-Conv3x3) that produces a fixed number of new feature maps called the growth rate $$.
The defining recurrence is that layer \ell does not just look at the output of layer \ell-1. It looks at the concatenation of the block input and every earlier layer output:
x_\ell = H_\ell\big([\,x_0, x_1, x_2, \ldots, x_{\ell-1}\,]\big)
Here [\cdot] denotes concatenation along the channel dimension. This is Equation 2 of the DenseNet paper. The final output of the block is the concatenation of the input and all $$ layer outputs:
\text{out} = [\,x_0, x_1, \ldots, x_L\,] \in \mathbb{R}^{N \times (C + L \cdot k) \times H \times W}
Spatial size H \times W is preserved because each 3 \times 3 convolution uses padding 1 and stride 1. Only the channel count grows.
The motivation, stated in the paper's abstract, is direct: "to ensure maximum information flow between layers in the network, we connect all layers (with matching feature-map sizes) directly with each other." Whereas a traditional L-layer network has L connections (one between each layer and the next), a dense block has \frac{L(L+1)}{2} direct connections. Every layer is connected to every other layer that comes after it within the block.
What the Paper Says
DenseNet was introduced to address a recurring observation about very deep networks: "as information about the input or gradient passes through many layers, it can vanish and wash out by the time it reaches the end (or beginning) of the network." ResNets, Highway Networks, and stochastic depth all attack this by creating short paths from early layers to later ones. DenseNet distills that common theme into a simple connectivity pattern.
The paper lists several concrete advantages that follow from dense connectivity: it alleviates the vanishing-gradient problem, strengthens feature propagation, encourages feature reuse, and substantially reduces the number of parameters. Crucially, the authors note that DenseNets require fewer parameters than traditional convolutional networks because "there is no need to relearn redundant feature maps." Traditional architectures must carry information forward by copying it through each layer's weights; DenseNet layers can add a few feature maps while preserving access to all previously computed ones.
The Composite Layer H_\ell
In the paper, H_\ell is the composite function BN then ReLU then a 3 \times 3 convolution. Given an input u with c_\ell channels (where c_\ell = C + (\ell-1)k), the composite layer computes:
\hat{u} = \frac{u - \mu}{\sqrt{\sigma^2 + \epsilon}}, \quad z = \text{ReLU}\big(\gamma \odot \hat{u} + \beta\big), \quad x_\ell = \text{Conv}_{3\times 3}(z)
where:
- \mu, \sigma^2 \in \mathbb{R}^{c_\ell} are the per-channel batch-norm running mean and variance (here supplied as fixed inputs for inference)
- \gamma, \beta \in \mathbb{R}^{c_\ell} are the learned batch-norm scale and shift
- \epsilon is a small constant for numerical stability (default 10^{-5})
- the convolution kernel has shape (k, c_\ell, 3, 3): it maps c_\ell input channels to exactly k output channels
The key structural fact: because the input channel count c_\ell grows by k at every layer, the convolution kernel of each layer is wider on its input axis than the previous one. The output is always k channels regardless of \ell.
The order BN-ReLU-Conv is deliberate. The paper places batch normalization first so that normalization adapts to the statistics of the full concatenated input, which mixes feature maps from many different layers at very different scales. Applying BN before the convolution rescales each incoming channel to a comparable range, so the convolution sees a well-conditioned input. This "pre-activation" ordering follows the improved residual unit of He et al. (2016) and is one reason DenseNet trains stably even when the concatenated input is heterogeneous.
At inference time, the batch-norm statistics are fixed. Instead of recomputing per-batch mean and variance, the layer uses the running estimates \mu and \sigma^2 accumulated during training. That is why this implementation supplies \mu and \sigma^2 as explicit per-channel inputs rather than computing them from the batch: the forward pass is deterministic and independent of batch size.
Why Concatenation, Not Summation
ResNet (He et al., 2016) connects layers with an additive identity shortcut: x_\ell = H_\ell(x_{\ell-1}) + x_{\ell-1}. DenseNet replaces the sum with a concatenation. The paper argues this difference is fundamental:
- Summation can impede information flow. Adding tensors fuses old and new features into the same channels. If the identity and the residual carry conflicting signals, they partially cancel. Concatenation keeps every feature map intact and individually addressable by later layers.
- Concatenation makes feature reuse explicit. Each later layer can directly access the raw, unmodified output of any earlier layer, including the block input itself. The network never has to relearn features that an earlier layer already produced.
- Collective knowledge. The paper frames the concatenated stack as the block's "collective knowledge": layers add a small number of feature maps to a shared, growing state, rather than transforming a fixed-width hidden state in place.
The cost of concatenation is that the input width grows linearly with depth inside the block, so the per-layer convolution gets wider. DenseNet keeps this affordable by making k small (typically k = 12 or k = 32).
Growth Rate and Channel Bookkeeping
The growth rate k is the number of feature maps each composite layer contributes. After \ell layers, the running channel count is:
c_\ell = C + \ell \cdot k
So the input to layer \ell (1-indexed) has $ + (\ell - 1)k$ channels, and the block's final output has C + L \cdot k channels. A small k keeps each layer narrow: the paper notes that a relatively small growth rate is sufficient because every layer has direct access to the block's collective knowledge, so it only needs to add a little new information.
Parameter efficiency. Because layers are thin and reuse earlier features, DenseNet reaches the accuracy of much wider or deeper ResNets with fewer parameters. The paper reports DenseNet matching ResNet accuracy on ImageNet with roughly half the parameters and fewer FLOPs.
A useful way to see the efficiency: a single composite layer with growth rate k and input width c_\ell has about 9 \cdot c_\ell \cdot k convolution parameters (the 9 comes from the 3 \times 3 kernel). Even though c_\ell grows with depth, k stays small, so each layer remains cheap. Contrast this with a wide plain network where every layer might map hundreds of channels to hundreds of channels. DenseNet spends its parameter budget on a large number of narrow, highly-connected layers rather than a small number of very wide ones.
The paper also reports that DenseNet's intermediate feature maps tend to be used by many downstream layers, confirmed by inspecting the average absolute weight that each layer assigns to each source layer. Weights are spread across many source layers rather than concentrated on the immediately preceding one, which is empirical evidence that the concatenation is genuinely exploited for feature reuse.
Gradient Flow
Dense connectivity also improves training. Because the block output concatenates every layer output, each layer has a direct connection to the block output and, through the loss, to the gradient signal. There is a short path from the loss back to every layer, not just a long chain through all intermediate layers. The paper calls this implicit deep supervision: each layer receives gradient information almost as if it were directly supervised, which mitigates vanishing gradients and stabilizes training of very deep networks.
Worked Example (tracing channels)
Take C = 2, growth rate k = 2, and L = 2 composite layers, with a single sample of spatial size 4 \times 4. Start with the input x_0 (2 channels).
Layer 1. Input is [x_0] with 2 channels. BN-ReLU-Conv uses a kernel of shape (2, 2, 3, 3) and produces x_1 with 2 channels. Running stack is now [x_0, x_1] with 4 channels.
Layer 2. Input is the concatenation [x_0, x_1] with 4 channels. BN-ReLU-Conv uses a kernel of shape (2, 4, 3, 3) and produces x_2 with 2 channels. Running stack is now [x_0, x_1, x_2].
Block output. Concatenate everything: [x_0, x_1, x_2] has 2 + 2 + 2 = 6 channels, shape (1, 6, 4, 4). This matches C + L \cdot k = 2 + 2 \cdot 2 = 6.
Notice that layer 2's kernel input axis (4) equals the channel count of the concatenation it consumes, while layer 1's kernel input axis ($$) equals just C. Getting these widths right is the heart of the implementation.
A clean special case helps verify an implementation. If a layer's batch-norm parameters are the identity (\gamma = 1, \beta = 0, \mu = 0, \sigma^2 = 1), then for \epsilon \to 0 the normalization step is a no-op and the composite layer reduces to \text{ReLU} followed by the convolution. This makes it easy to hand-check that the BN step is wired correctly: the only transformation left is a rectified linear unit and a standard 3 \times 3 convolution. If the output of an identity-BN layer disagrees with a plain ReLU-then-conv, the batch-norm formula is being applied incorrectly (for example normalizing over the wrong axis or forgetting \epsilon).
Numerical Details
Two numerical points matter when implementing the composite layer. First, the \epsilon inside the square root prevents division by zero when a channel has near-zero variance; it lives inside the radical, \sqrt{\sigma^2 + \epsilon}, not added to the result. Placing it outside changes the value subtly and fails tight tolerance checks. Second, the convolution must use stride 1 and padding 1 for a $ \times 3$ kernel to preserve spatial dimensions; any other padding shrinks H and W and makes the per-layer concatenation impossible because the spatial sizes would no longer match.
Concatenation requires that all tensors agree on every dimension except the one being joined. Inside a dense block, that means every layer output must have the same N, H, and W as the block input. The padding-1, stride-1 convolution guarantees this, which is precisely why DenseNet restricts dense connectivity to layers "with matching feature-map sizes" and pushes all downsampling into the transition layers between blocks.
Transition Layers and the Bigger Picture
A full DenseNet alternates dense blocks with transition layers (BN, 1 \times 1 convolution, and 2 \times 2 average pooling) that reduce both spatial size and channel count between blocks. Without transitions the channel count would grow unbounded across the whole network. The dense block itself, however, only ever concatenates and never downsamples, which is why spatial dimensions are constant inside a block.
DenseNet-B adds a 1 \times 1 bottleneck convolution before each 3 \times 3 to cap the cost when c_\ell becomes large. DenseNet-C further compresses channels at transitions. The plain dense block here corresponds to the base DenseNet composite layer (H_\ell = BN-ReLU-Conv$ 3$) described in the paper before those refinements.
Pitfalls
- Feeding only the previous layer output instead of the full concatenation. Treating the block like a plain CNN (input to layer \ell is just x_{\ell-1}) breaks the defining property of DenseNet. Because each kernel expects the growing channel count C + (\ell-1)k, this usually crashes with a channel-mismatch error at layer 2, or silently produces the wrong block when $ = 1$.
- Concatenating on the wrong axis. Feature maps must be joined along the channel dimension (dim 1 for an (N, C, H, W) tensor). Concatenating along the batch dimension (dim 0) stacks samples instead of features and yields a tensor of shape (N \cdot (L+1), C, H, W) that is completely wrong.
- Dropping the input x_0 from the final output. The block output is [x_0, x_1, \ldots, x_L] with C + L \cdot k channels, not just the L layer outputs (L \cdot k channels). Omitting the original input both shrinks the channel count and discards the cheapest, most reusable features.
- Returning only the last layer output. A common mistake is to compute the dense inputs correctly but return x_L alone. The block must return the full concatenation; the whole point is that downstream layers see every intermediate feature map, not just the most recent one.
Examples
Example 1
- Input
x = [[[[1,0],[0,-1]]]], layers = [{"bn_gamma":[1],"bn_beta":[0],"bn_mean":[0],"bn_var":[1],"conv_weight":[[[[0,0.1,0],[0.1,0.5,0.1],[0,0.1,0]]]]}], growth_rate = 1, eps = 0.00001- Output
[[[[1,0],[0,-1]],[[0.499998,0.1],[0.1,0]]]]- Explanation
- The output retains the original channel and appends the new feature map produced from it.
Example 2
- Input
x.shape = (1, 1, 2, 3), layers = 2 supplied layer dictionaries, growth_rate = 1, eps = 0.00001- Output
[[[[-0.05157,-0.03165,-0.09038],[-0.48279,0.6395,-0.21491]],[[0.065917,0.072333,-0.073062],[0.020997,-0.038131,0.046715]],[[0.253784,0.352888,-0.253893],[-0.105019,0.094921,0.140759]]]]
Example 3
- Input
x.shape = (1, 2, 3, 2), layers = 1 supplied layer dictionaries, growth_rate = 2, eps = 0.00001- Output
[[[[-0.07919,0.83936],[0.46152,0.72792],[-0.4711,-0.12916]],[[0.68561,0.93799],[0.10141,0.88331],[0.70686,-0.21615]],[[-0.253079,-0.2722],[-0.073621,-0.315352],[-0.17462,0.106576]],[[-0.289511,0.0074],[0.108911,0.033796],[-0.009493,0.217671]]]]
Hints
- Keep a list initialized with x and concatenate it along dim=1 before each layer.
- Use padding=1 so every new feature map can be concatenated spatially.
Requirements
- Use PyTorch.
- Concatenate the block input and all preceding layer outputs before every composite transformation.
- Apply each supplied layer in order.
- Preserve the input before appending the new feature maps.
- Return the documented float64 tensor.
Constraints
- x has shape (N, C, H, W) and dtype torch.float64.
- layers contains L dictionaries in execution order.
- At layer l, each batch-normalization vector has shape (C + lK).
- At layer l, conv_weight has shape (K, C + lK, 3, 3).
- growth_rate equals K, and eps is positive.
- N, C, H, W, K, and L are positive.
Starter Code
import torch
import torch.nn.functional as F
def dense_block(x: torch.Tensor, layers: list, growth_rate: int,
eps: float = 1e-5) -> torch.Tensor:
"""
Returns the float64 concatenation of the block input and all layer outputs.
"""
passTest Cases
| Case | Matches | |
|---|---|---|
| One dense layer | — | public |
| Two dense layers | — | public |
| Two growth channels | — | public |