EasyVGG

Complete VGG Network

Very Deep Convolutional Networks

Easy

Problem

Implement a compact numerical VGG forward pass. Execute the supplied convolution and pooling configuration, flatten the resulting features, apply two affine layers with ReLU, and apply the final affine classifier without activation. The classifier dictionary contains W1, b1, W2, b2, W3, and b3. Return raw class logits as a float64 NumPy array.

Theory

The complete VGG-16 network is an end-to-end image classification model. It takes a raw image tensor and produces class logits by composing two stages: a feature extractor built from convolutional blocks and a classifier built from fully connected layers. A config list drives the architecture, making the same code reusable across VGG-11 through VGG-19.

This is the capstone of the VGGNet architecture. Every component studied in isolation -- conv blocks, max pooling, feature extraction, and the classifier head -- is assembled here into a single coherent model that maps pixels to predictions.


What It Is / What It Does

VGG-16 is a convolutional neural network for image classification. Given an input image tensor of shape (B, 3, 224, 224), it outputs a logits tensor of shape (B, 1000) representing scores for 1000 ImageNet classes. The pipeline has two stages:

The output is raw logits -- no softmax is applied. Cross-entropy loss applies log-softmax internally for numerical stability during training.


Key Equations

Stage 1 -- Feature extraction:

\text{features} = \text{vgg\_features}(x, \text{config})

where x \in \mathbb{R}^{B \times 3 \times 224 \times 224} is the input image batch, config is the layer specification list, and \text{features} \in \mathbb{R}^{B \times 512 \times 7 \times 7}.

Stage 2 -- Classification:

\text{logits} = \text{vgg\_classifier}(\text{flatten}(\text{features}))

where \text{flatten}(\text{features}) \in \mathbb{R}^{B \times 25088} and \text{logits} \in \mathbb{R}^{B \times 1000}.

The full forward pass in one line:

\text{logits} = \text{vgg\_classifier}(\text{flatten}(\text{vgg\_features}(x, \text{config})))

The flatten operation reshapes the 4D feature tensor (B, 512, 7, 7) into a 2D matrix (B, 25088) where 25088 = 512 \times 7 \times 7. This bridges the convolutional and fully connected stages.


The Two-Stage Design

VGG cleanly separates feature extraction from classification. This reflects a fundamental design principle that proved enormously influential:

Feature Extraction (convolutional blocks):

Classification (fully connected layers):

This clean separation means you can replace the classifier for transfer learning while keeping the feature extractor frozen. You can also discard the classifier entirely and use the feature maps for detection, segmentation, or style transfer.


VGG-16 Architecture

The "16" in VGG-16 counts the number of weight layers: 13 convolutional layers plus 3 fully connected layers. Activation functions, pooling layers, and dropout do not count because they have no learned weights.

The 5 convolutional blocks:

The 3 fully connected layers:

The naming convention -- VGG-11, VGG-13, VGG-16, VGG-19 -- always counts weight layers only. All variants share the same classifier; they differ only in how many conv layers appear within each block.


The Config-Driven Approach

A key insight in the VGG paper is that the entire family of networks can be built from a single config list. For VGG-16 (configuration D in the paper), the config is:

\text{config} = [64, 64, \text{M}, 128, 128, \text{M}, 256, 256, 256, \text{M}, 512, 512, 512, \text{M}, 512, 512, 512, \text{M}]

The rules are simple:

The same vgg_features function handles every variant:

The classifier is identical for all variants because all configs produce the same 512 \times 7 \times 7 spatial output.


Parameter Count

VGG-16 has approximately 138 million parameters. The distribution between the two stages is dramatically uneven:

Convolutional layers (~14.7M parameters):

Fully connected layers (~123.6M parameters):

The FC layers contain about 89% of all parameters, with FC1 alone holding 74% of the total. This is why later architectures (GoogLeNet, ResNet) replaced FC layers with global average pooling -- it eliminates the massive FC1 bottleneck entirely. VGG's parameter inefficiency was a key motivation for subsequent architectural innovations.


Paper Context

VGGNet was introduced by Karen Simonyan and Andrew Zisserman of the Visual Geometry Group at Oxford in their 2014 paper "Very Deep Convolutional Networks for Large-Scale Image Recognition." The network was the runner-up in ILSVRC-2014 classification (behind GoogLeNet) but won the localization task.

The key insight: depth matters. The paper systematically evaluated networks from 11 to 19 layers, all using only 3 \times 3 convolutions. This departed from AlexNet (2012), which used larger $ \times 11$ and 5 \times 5 filters. Simonyan and Zisserman showed that stacking two 3 \times 3 convolutions achieves the same receptive field as one 5 \times 5 but with fewer parameters (2 \times 9C^2 = 18C^2 vs. 25C^2) and an extra nonlinearity.

As the paper states: "In spite of a large number of parameters (144 million for VGG-19), these networks require few epochs to converge." The authors attributed this to the implicit regularization provided by greater depth and smaller filter sizes.

Pre-trained VGG weights became a standard tool in computer vision. Before large-scale pre-training with ImageNet-21k or self-supervised methods, VGG-16 pre-trained on ImageNet was the default feature extractor for transfer learning.


Spatial Dimension Trace

Trace an input of shape (1, 3, 224, 224) through the full VGG-16:

Feature extraction stage:

Flatten: (1, 512, 7, 7) \to (1, 25088)

Classifier stage:

The spatial dimensions follow a clean halving pattern: 224 \to 112 \to 56 \to 28 \to 14 \to 7. Each max pool divides by 2. The final $ \times 7$ comes from 224 / 2^5 = 7. This is why VGG requires 224 \times 224 input -- the 5 pooling layers produce a $ \times 7$ grid that yields 512 \times 7 \times 7 = 25{,}088 features matching FC1's expected input.


VGG's Legacy

Despite being superseded by ResNet (2015) for classification accuracy, VGG remains one of the most influential networks in deep learning. Its impact extends well beyond classification:

VGG was superseded because its massive FC layers waste parameters and without skip connections, training beyond 19 layers suffers from vanishing gradients. ResNet solved both problems. But VGG's design philosophy -- simple building blocks, systematic depth scaling, small filters everywhere -- directly informed the architectures that followed.


Pitfalls

1. Using the wrong config for the variant

VGG-16 uses config D: [64, 64, \text{M}, 128, 128, \text{M}, 256, 256, 256, \text{M}, 512, 512, 512, \text{M}, 512, 512, 512, \text{M}]. A common mistake is using config E (VGG-19) which has 4 conv layers in blocks 3-5 instead of 3. If you load VGG-16 pre-trained weights into a VGG-19 architecture, the weight shapes will mismatch and produce wrong results.

2. Forgetting to flatten between features and classifier

The feature extractor outputs a 4D tensor (B, 512, 7, 7) but the classifier expects a 2D tensor (B, 25088). You must explicitly flatten between the two stages. Passing the 4D tensor directly to FC1 causes a shape mismatch error. The flatten operation is $ -1)$ or x.\text{flatten}(1).

3. Wrong weight initialization order

When loading pre-trained weights, the state dict keys must match exactly. A frequent bug is building the feature extractor with layers in a different order than the reference implementation. If your config parsing adds extra layers (like BatchNorm, which original VGG does not use), all subsequent weights will be misaligned. The model runs without errors but produces random predictions.

4. Applying softmax in the forward pass

The model should output raw logits, not probabilities. If you apply softmax in the forward pass and then use nn.CrossEntropyLoss (which applies log-softmax internally), you get double-softmax. The gradients become near-zero and the model fails to learn. Apply softmax outside the model at inference if needed.

5. Incorrect input dimensions

VGG-16 expects 224 \times 224 input. With 5 max-pool layers of stride 2, any input not divisible by $ = 32$ produces non-integer spatial dimensions. Using 256 \times 256 images produces 8 \times 8 \times 512 = 32{,}768 features instead of 25,088, causing a dimension mismatch at FC1.


Examples

Example 1

Input
x = [[[[1],[2]],[[3],[4]]]], config = [1,"M"], kernels = [[[[[1]]]]], biases = [[0]], classifier = {"W1":[[0.5]],"b1":[0],"W2":[[2]],"b2":[-1],"W3":[[0.3,-0.2]],"b3":[0.1,0]}
Output
[[1,-0.6]]
Explanation
The configuration produces spatial features that are flattened and passed through the complete classifier.

Example 2

Input
x = [[[[1,-1]]]], config = [2], kernels = [[[[[1,0],[0.5,1]]]]], biases = [[0,0.1]], classifier = {"W1":[[1],[0.5]],"b1":[0],"W2":[[1]],"b2":[0.2],"W3":[[1]],"b3":[-0.1]}
Output
[[0.6]]

Example 3

Input
x = [[[[1]]],[[[-1]]]], config = [1], kernels = [[[[[2]]]]], biases = [[0.5]], classifier = {"W1":[[1,-1]],"b1":[0,0.2],"W2":[[1],[0.5]],"b2":[0],"W3":[[1,-1]],"b3":[0.1,0.2]}
Output
[[2.6,-2.3],[0.2,0.1]]

Hints

  1. Finish all config entries before flattening.
  2. Use the next kernel only for an integer config entry.
  3. Do not apply ReLU after the final classifier projection.

Requirements

Constraints

Starter Code

import numpy as np

def vgg_forward(x: np.ndarray, config: list, kernels: list,
                biases: list, classifier: dict) -> np.ndarray:
    """
    Returns float64 class logits with shape (B, C_classes).
    """
    pass

Test Cases

CaseMatches
Compact classifierpublic
Two convolution channelspublic
Batch logitspublic