Training a Vision Model (III): Pooling and Dense Layers, From Pixels to Decision

Training a Vision Model (III): Pooling and Dense Layers, From Pixels to Decision

In the first article of this series we built the skeleton of the neural network. In the second one we opened the black box of `conv1` and `conv2`: what a convolution is and how a filter learns to detect visual patterns.

We have two pieces left to explain, and they are precisely the ones that tend to generate the most questions because they can no longer be "seen" as easily as filters: pooling (`self.pool`) and dense layers (`fc1`, `fc2`). We close the series with them.


1. What is Pooling?

Pooling is an operation applied right after a convolution. Its job is simple: reduce the size of the activation map, keeping only the most relevant information from each zone.

Think of pooling as taking a high-resolution photo and purposely compressing it, keeping only the essentials of each block of pixels, instead of trying to preserve everything.

How It Works: A Sliding Window

Like convolution, pooling moves across the image with a window, but without complex multiplications or filters: it simply takes the most representative value from each zone.

Visual comparison between MaxPooling and Average Pooling
TypeWhat It Does
MaxPoolingTakes the maximum value in the window
AvgPoolingTakes the average value in the window

Numerical Example

Imagine this section of an activation map (2×2 values):


[1, 3]
[4, 2]

A 2×2 MaxPooling on this block returns a single value: `4` — the highest of the four.

A 2×2 AvgPooling on the same block returns `2.5` — the average of (1+3+4+2)/4.

In our model, we use MaxPooling because, for image classification, we are usually more interested in "where is there a strong activation of this pattern?" rather than "what is the average of this zone?" — a sharp edge in a corner of the window is more informative than diluting it with the average of the rest.

In Our Code


# We define the pooling only once in __init__, and reuse it
# both after conv1 and after conv2.
self.pool = nn.MaxPool2d(2, 2)   # 2x2 window, keeps the highest value from each

# In forward, pooling is always applied right after the convolution + ReLU:
x = self.pool(torch.relu(self.conv1(x)))

Concrete example with real numbers: if the input image is 32×32 pixels, after this line the result is 16×16. Passing through `conv2` and `self.pool` again, we go from 16×16 to 8×8. That is why in the first article the dense layer expected `32 * 8 * 8` input values — the `8×8` comes exactly from applying pooling twice to a 32×32 image.


2. Why Pooling Matters (It's Not Just "To Save Space")

  • Reduces size → fewer numbers to process in subsequent layers, faster training.
  • Keeps what's important → discards fine, less relevant details and preserves the main patterns of each zone.
  • Makes the model more robust → if in one photo the chicken is slightly further to the right than in another, pooling helps the model recognize it just the same, because it doesn't depend on the exact position of each pixel.
  • Helps prevent overfitting → by simplifying the information, it is harder for the model to "memorize" irrelevant details from the training photos instead of learning the general pattern.
Analogy: imagine describing a photo to someone over the phone. You don't tell them the exact color of every pixel — you say "there is a brown chicken in the center, looking left." That is, in essence, what pooling does: keeping the useful summary and discarding the noise.

3. Dense Layers: Where the Final Decision Is Made

After convolutions and pooling, we no longer have an image in the usual sense — we have a vector of numbers, a long list summarizing what the network has detected. Something like:


[0.1, 2.3, 0.0, 5.4, 1.2, 0.7, ...]

This is where the dense layers (also called *fully connected*, hence the names `fc1`, `fc2`) come in:

Visualization of the embedding vector passing from convolutions to dense layers

self.fc1 = nn.Linear(32 * 8 * 8, 128)   # from 2048 input values to a 128-dimensional vector
self.fc2 = nn.Linear(128, num_classes)  # from 128 values to one neuron per class

`nn.Linear(input, output)` connects every input number to every output neuron — hence "dense": it does not skip any possible connection, unlike convolution, which only looks at a small window at a time.

Why Are Dense Layers Talked About Less Than Filters?

It's a very reasonable question, and has a clear answer: because they cannot be "seen" as easily. A convolution filter is a small image — you can draw it and look at it. A dense layer, on the other hand, works with an abstract vector of numbers, without any spatial structure (there is no "up," "down," or "neighbor" in a list of 2048 numbers). There is no direct way to draw "what this layer is thinking."

Still, it is precisely here that the decision is made. The process in short:

  1. Convolutional layers detect isolated patterns: edges, curves, textures.
  2. Dense layers combine those patterns. Simply put: *"if there is a round shape with two vertical lines next to it, it is probably an eye."*
  3. If we also combine activations related to head, beak, and feathers: *"with all this together, it is very likely a chicken."*
Analogy: imagine a team of detectives. Convolutional filters are the ones scanning the scene looking for individual clues (a footprint, a hair, a stain). Pooling is the one that discards unclear clues and keeps only the most obvious ones. And dense layers are the lead detective, who puts all those loose clues together and says: *"with all this, I think it was the butler"* — or, in our case, *"with all this, I think it's a chicken."*

4. The Embedding Vector: The "Digital Fingerprint" of the Image

The result of `fc1` — that vector of 128 numbers — has its own name: it is called an embedding. It is a numerical, abstract representation of the image, calculated by the network.

If you are already familiar with Qdrant (a vector database we will use in other projects on this website), this will sound familiar: it is exactly the same concept. When you train the model, similar images (two photos of different chickens, for example) end up generating embedding vectors that are close to each other — just like we store vectors in Qdrant to compare meanings, here the model generates vectors to compare images. The last layer (`fc2`) uses that vector to decide the final class, almost as if it were internally querying a small vector database.


# outputs is the final result of passing the image through the ENTIRE model (including fc2).
# outputs.shape would be something like [1, num_classes] -> one score per class.
# torch.max gives us the index of the class with the highest score:
# the "1" indicates that we search for the maximum along the class dimension.
_, predicted = torch.max(outputs, 1)

An important nuance about the size of this vector: the number of dimensions of the embedding is not fixed, you decide it when defining the layer. If in your model you set `nn.Linear(32 * 8 * 8, 128)`, the embedding would have 128 dimensions. If in another project you used `nn.Linear(16 * 32 * 32, 64)`, it would have 64. There is no universal "correct" number — it's just another design parameter, like deciding how many figures to use to summarize something: more dimensions can capture finer details, but also make the model heavier to train.


5. The Complete Pipeline, at a Glance

The general processing pipeline of our neural network can be summarized in the following stages:

Visual diagram of the convolutional neural network pipeline
StageWhat It DoesAnalogy
Convolution (`conv1`, `conv2`)Detects isolated visual patterns: edges, textures, curvesA detective looking for individual clues at the scene
Pooling (`pool`)Reduces size, keeps the most relevant part of each zoneSummarizing the scene over the phone, without describing every pixel
Flattening (`view`)Converts the result into a list of numbersUnravelling a folded blanket into a single thread
Dense layers (`fc1`, `fc2`)Combine the detected patterns and decide the final classThe lead detective joining all the clues and giving a verdict

6. Closing the Series

This completes our three-part journey:

  1. First part: the project structure and the complete network architecture.
  2. Second part: what a convolution is and what filters look like, both hand-designed and learned by the model.
  3. Third part: how pooling simplifies information, and how dense layers combine it to make the final decision.

From here to a real trained model working in production, the difference is mainly: repeating this same flow thousands of times, with real, labeled data, letting the optimizer (`Adam`, from the first article) gradually adjust each filter and each weight in the dense layers until the predictions match reality. We leave that in-depth training process — how long it takes, how to know when to stop, what typical errors appear — for a future series.


Article prepared from real tests on Girtual's own infrastructure (Girona).

About the author: David Otero Verdaguer, Graduate in Multimedia from the UOC, with training in Network Computer Systems Administration (ASIX/ASIR). If you are interested in this type of content, I write more frequently about networks, systems, and the technical foundations supporting projects like this on the ReparamiPC blog (www.reparamipc.com).

Comments