MathIsimple
CNNs & Computer Vision
15 min read

NIN and GoogLeNet: 1x1 Convolutions and the Inception Module

When you cannot know which filter size is best, run them all in parallel — then let the network decide.

NINGoogLeNetInception Module1x1 ConvolutionCNN Architecture

Network in Network and GoogLeNet are usually treated as separate landmark architectures, but they belong to the same line of thought. NIN argued that local feature extractors should themselves be small networks, introducing the 1×1 convolution and global average pooling. GoogLeNet built on those primitives to answer a harder question — given a fixed compute budget, how do you make a network both deep and wide at the same time?

NIN: stronger local extractors

In an ordinary CNN, a local image patch is processed by a linear convolutional kernel followed by a nonlinearity. That works well, but it also means each local receptive field is initially summarized by a fairly simple operator. The NIN perspective is that local appearance variation can itself be highly nonlinear. If so, asking a single linear filter to do all the local abstraction may be too weak. More depth later in the network helps, but the local patch encoder may already have thrown away useful structure.

NIN's answer is to replace the local linear filter with a shared multilayer perceptron applied at every receptive field location. The important word is shared — the model still preserves the core convolutional priors of locality and parameter sharing. In practice, an mlpconv block can be understood as:

k x k conv -> ReLU -> 1 x 1 conv -> ReLU -> 1 x 1 conv -> ReLU

The first k×kk \times k layer mixes information across local space and channels. The later 1×11 \times 1 layers no longer aggregate neighboring pixels. Instead, they perform nonlinear channel mixing at the same spatial position. That is the key insight behind the expressive power of 1×11 \times 1 convolutions.

A 1×11 \times 1 convolution is not a trivial kernel. It is a learned cross-channel transformation applied independently at every spatial location.

Global average pooling replaces the heavy classifier head

NIN's second major contribution is the use of global average pooling (GAP). Instead of flattening a large feature tensor and feeding it into several fully connected layers, NIN lets the final feature maps correspond directly to classes and averages each map spatially:

sc=1HWi=1Hj=1WFi,j,cs_c = \frac{1}{HW} \sum_{i=1}^{H} \sum_{j=1}^{W} F_{i,j,c}

Each class score scs_c becomes the mean activation of its corresponding final feature map F:,:,cF_{:,:,c}. This acts as a structural regularizer because it removes a large, high-capacity classifier that could otherwise overfit or compensate for messy upstream features. GAP effectively tells the convolutional body: if you want a high class score, produce a feature map whose activation meaningfully and consistently lights up when that class is present.

These two ideas are most powerful as a pair. Stronger local nonlinear modeling makes it more plausible that the final feature maps will carry clean category evidence. Once those feature maps are meaningful enough, a simple global average becomes a viable classifier instead of a blunt compression trick. NIN shifted the architectural question from "how many layers should we stack?" to "how expressive should each local computation be, and how simple can the prediction head become once the feature maps are strong enough?"

GoogLeNet's design question: deep and wide on a fixed budget

VGG settled one question: depth is a reliable lever for accuracy. NIN answered another: the local filter can itself be a small network, and 1×1 convolutions add nonlinearity for almost no cost. GoogLeNet then asked a harder question — given a fixed computational budget, how do you make a network both deep and wide at the same time?

The motivating argument starts from the structure of natural data. When recognizing a cat, the ear and the eye are highly correlated and tend to activate together. The ear and the background grass are nearly independent. That means the true statistical dependency structure is sparse — not every feature needs to be connected to every other.

If a dataset's probability distribution can be represented by a sparse network, the optimal topology can be constructed layer by layer by clustering neurons with highly correlated activations.

The theory is elegant but runs into two immediate obstacles. You cannot know the correlation structure before training, and you cannot train before you have the architecture — a genuine circular dependency. And GPU hardware is optimized for dense matrix operations; sparse tensor computations on current accelerators are often slower than their dense counterparts, not faster.

GoogLeNet's resolution is to stop trying to implement sparsity exactly. Instead, run several plausible operations in parallel and let the network learn which combinations matter:

If we do not know where the sparse structure is, run all plausible structures in parallel and let the network decide which to use.

The Inception module: multi-scale parallel convolution

Natural images contain objects at many scales. A distant bird and a close car both need to be recognized but occupy very different spatial footprints. VGG addresses this by stacking many layers so that later layers see larger effective receptive fields. The Inception module takes a more direct approach: apply multiple filter sizes at the same layer simultaneously.

Four parallel paths receive the same input tensor and produce outputs that are concatenated along the channel dimension:

Input: 28 x 28 x 192
 ├── 1x1 conv  -> 28x28x64     (cross-channel compression)
 ├── 3x3 conv  -> 28x28x128    (small neighborhood)
 ├── 5x5 conv  -> 28x28x32     (large neighborhood)
 └── 3x3 pool  -> 28x28x192    (local max response)
          ↓
 Channel concat -> 28x28x416

The four outputs share the same spatial dimensions and are stacked into a single tensor. The next layer receives a joint representation that mixes single-point, small-scale, large-scale, and pooled information. Learning which combination is useful is left entirely to the subsequent layers.

The channel explosion problem and 1×1 bottlenecks

There is an immediate problem with the naive version. The max-pooling branch passes its full input directly through: 192 channels in, 192 channels out. Stack a second Inception module on the 416-channel output and the pooling path now outputs 416 channels. Each successive module compounds this growth, and convolutional cost scales with the number of input channels, so the computation budget escalates rapidly.

The fix comes directly from NIN. A 1×11 \times 1 convolution applies a learned linear projection to the channel dimension at every spatial position while leaving spatial resolution unchanged. Inserting 1×11 \times 1 reductions before the 3×33 \times 3 and 5×55 \times 5 convolutions, and after the pooling branch, gives the full Inception module:

Input: 28 x 28 x 192
 ├── 1x1 conv (64)                       -> 28x28x64
 ├── 1x1 conv (96)  -> 3x3 conv (128)   -> 28x28x128
 ├── 1x1 conv (16)  -> 5x5 conv (32)    -> 28x28x32
 └── 3x3 pool       -> 1x1 conv (32)    -> 28x28x32
          ↓
 Channel concat -> 28x28x256

The computation reduction is substantial. Consider the 5×55 \times 5 branch. Applied directly with 192 input channels to produce 32 output channels on a 28×28 map, the cost is:

direct: 28×28×(5×5×192)×32120,000,000 ops\text{direct: } 28 \times 28 \times (5 \times 5 \times 192) \times 32 \approx 120{,}000{,}000 \text{ ops}

With a 1×11 \times 1 reduction to 16 channels first, the two-step cost is:

28×28×(1×1×192)×162,400,000+28×28×(5×5×16)×3210,000,00012,400,000 ops\underbrace{28 \times 28 \times (1 \times 1 \times 192) \times 16}_{\approx 2{,}400{,}000} + \underbrace{28 \times 28 \times (5 \times 5 \times 16) \times 32}_{\approx 10{,}000{,}000} \approx 12{,}400{,}000 \text{ ops}

The bottleneck reduces computation on the 5×55 \times 5 path by roughly a factor of ten. The 1×11 \times 1 layer also introduces an extra ReLU nonlinearity, increasing representational capacity as a side effect — exactly the argument NIN made for the mlpconv block.

GoogLeNet's three-stage architecture

With the Inception module defined, GoogLeNet assembles 22 layers in three stages.

Stage one: standard convolutions

Input: 224 x 224 x 3
-> 7x7 conv (64), stride 2   -> 112 x 112 x 64
-> 3x3 max pool, stride 2    -> 56 x 56 x 64
-> 1x1 conv (64)             -> 56 x 56 x 64
-> 3x3 conv (192)            -> 56 x 56 x 192
-> 3x3 max pool, stride 2    -> 28 x 28 x 192

Early convolutional layers detect low-level features — edges, color gradients, simple textures — whose spatial correlations are highly localized. Multi-scale parallel convolution provides little advantage when features correlate only within a very small neighborhood. Plain spatial convolutions are more efficient here.

Stage two: stacked Inception modules

28 x 28 x 192
-> Inception 3a -> Inception 3b
-> 3x3 max pool, stride 2       -> 14 x 14
-> Inception 4a -> 4b -> 4c -> 4d -> 4e
-> 3x3 max pool, stride 2       -> 7 x 7
-> Inception 5a -> Inception 5b

A pattern emerges across the depth: higher-stage Inception modules progressively increase the relative weight given to 3×33 \times 3 and 5×55 \times 5 paths. As features become more abstract, the spatial range of relevant correlations expands. The architecture adapts by allocating more capacity to larger receptive fields at higher levels.

Stage three: GAP classifier

7 x 7 x 1024
-> Global average pool  -> 1 x 1 x 1024
-> Dropout (40%)
-> Linear               -> 1000 classes
-> Softmax

GAP eliminates the tens of millions of parameters that VGG needed in its fully connected tail and acts as a structural regularizer: to produce a high class score, the feature map must carry consistent and spatially distributed evidence for that class.

Auxiliary classifiers: a direct fix for vanishing gradients

A 22-layer network creates a new training problem. Gradients backpropagated from the output must traverse the entire depth before reaching early layers. Each layer attenuates the signal slightly, and by layer five or six from the bottom, the useful gradient can be negligibly small.

GoogLeNet's solution is to attach two lightweight auxiliary classifiers to intermediate points in the network — specifically after Inception modules 4a and 4d. During training, all three loss signals are combined:

Ltotal=Lmain+0.3Laux1+0.3Laux2\mathcal{L}_{\text{total}} = \mathcal{L}_{\text{main}} + 0.3 \cdot \mathcal{L}_{\text{aux}_1} + 0.3 \cdot \mathcal{L}_{\text{aux}_2}

The auxiliary gradients inject a direct learning signal into the middle of the network. An intermediate layer no longer needs to wait for information to flow back from layer 22; it receives an additional gradient from the nearby auxiliary head. The auxiliary classifiers are discarded at inference time. Their only role is to strengthen gradient signals during training. ResNet would later replace this workaround with a more permanent solution: residual connections that let gradients flow directly through skip paths.

A PyTorch implementation

class InceptionBlock(nn.Module):
    def __init__(self, in_channels, c1, c2, c3, c4):
        super().__init__()
        self.path1 = nn.Sequential(
            nn.Conv2d(in_channels, c1, kernel_size=1), nn.ReLU()
        )
        self.path2 = nn.Sequential(
            nn.Conv2d(in_channels, c2[0], kernel_size=1), nn.ReLU(),
            nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1), nn.ReLU()
        )
        self.path3 = nn.Sequential(
            nn.Conv2d(in_channels, c3[0], kernel_size=1), nn.ReLU(),
            nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2), nn.ReLU()
        )
        self.path4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, c4, kernel_size=1), nn.ReLU()
        )

    def forward(self, x):
        return torch.cat([self.path1(x), self.path2(x),
                          self.path3(x), self.path4(x)], dim=1)


class GoogLeNet(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
            nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(),
            nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
        )
        self.inception3a = InceptionBlock(192,  64, (96, 128),  (16, 32),  32)
        self.inception3b = InceptionBlock(256, 128, (128, 192), (32, 96),  64)
        self.pool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.inception4a = InceptionBlock(480, 192, (96, 208),  (16, 48),  64)
        self.inception4b = InceptionBlock(512, 160, (112, 224), (24, 64),  64)
        self.inception4c = InceptionBlock(512, 128, (128, 256), (24, 64),  64)
        self.inception4d = InceptionBlock(512, 112, (144, 288), (32, 64),  64)
        self.inception4e = InceptionBlock(528, 256, (160, 320), (32, 128), 128)
        self.pool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.inception5a = InceptionBlock(832, 256, (160, 320), (32, 128), 128)
        self.inception5b = InceptionBlock(832, 384, (192, 384), (48, 128), 128)

        self.classifier = nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Dropout(0.4),
            nn.Linear(1024, num_classes),
        )

    def forward(self, x):
        x = self.stem(x)
        x = self.pool3(self.inception3b(self.inception3a(x)))
        x = self.pool4(self.inception4e(
            self.inception4d(self.inception4c(self.inception4b(self.inception4a(x))))))
        return self.classifier(self.inception5b(self.inception5a(x)))

Why the numbers matter

GoogLeNet's parameter count is approximately 5 million. AlexNet uses roughly 60 million, and VGG-16 uses around 138 million. Despite having twelve times fewer parameters than AlexNet, GoogLeNet achieved a lower Top-5 error rate on ImageNet. The structural efficiency comes directly from two compounding choices: the bottleneck reductions that keep each Inception module affordable, and global average pooling that removes the heavy classifier tail entirely.

This is not just a benchmark result. It is a design principle: accuracy does not have to come from scaling parameter count. It can come from smarter structure.

The main takeaway

NIN argued that the local feature extractor itself should be more expressive, introducing 1×1 convolutions for cross-channel mixing and global average pooling as a structural classifier. GoogLeNet built on those primitives to ask a different question: given a fixed budget, how should a layer allocate its capacity across different spatial scales?

The Inception module is a disciplined answer. It runs multiple filter sizes in parallel, uses 1×1 bottlenecks to control compute, and lets the network learn which combination of scales matters for a given task. Auxiliary classifiers solve the gradient delivery problem that depth creates. Global average pooling removes the largest source of unnecessary parameters.

Each design decision addresses a concrete problem with a clean mechanism. That is the real reason this lineage remains worth understanding: not because Inception modules are still state-of-the-art, but because the reasoning behind each choice is unusually explicit and reusable.

Related reading

Continue the cluster — these articles build directly on the ideas above.

Ask AI ✨