Danh sách bài viết

Bài 29: Pooling Layer — Max Pooling và Average Pooling

Pooling là phép downsample feature map trong CNN: chia feature map thành các cửa sổ nhỏ rồi lấy giá trị đại diện (max hoặc mean) cho mỗi cửa sổ. Mục tiêu: giảm spatial dimension để tiết kiệm tham số ở layer sau, tăng receptive field hiệu dụng và tạo translation invariance nhẹ. Bài này đi qua Max Pool, Avg Pool, công thức output size, backward, Global Average Pooling, Adaptive Pooling, so sánh với stride convolution và phê bình từ Hinton.

24/05/2026
13 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Viết được công thức Max Pooling và Average Pooling.
  • Tính được output size sau pooling với kernel / stride / padding bất kỳ.
  • Phân biệt khi dùng MaxPool, AvgPool, GlobalAvgPool và AdaptivePool.
  • Mô tả được gradient backward khác nhau giữa max pool và avg pool.
  • Hiểu vì sao kiến trúc modern (ResNet, EfficientNet) thay nhiều pool bằng stride convolution và dùng GAP ở cuối.
  • Implement Max Pool 2x2 bằng NumPy.

Bài này nối Bài 28 — Convolutional Layer với Bài 30 — Flatten và Fully Connected.

2

Pooling Layer là gì

Pooling là phép downsample feature map: nhận input dạng \( (B, C, H, W) \) và trả về output \( (B, C, H', W') \) với \( H' < H, W' < W \) (số channel \( C \) giữ nguyên).

Cơ chế: trượt một cửa sổ kích thước nhỏ (thường 2x2) trên feature map, mỗi cửa sổ rút gọn thành 1 giá trị duy nhất. Hai phép rút gọn phổ biến nhất là lấy maxlấy mean, ứng với Max Pooling và Average Pooling.

Khác với Conv layer, pooling thực hiện độc lập trên từng channel: không mix thông tin giữa các channel.

3

Ba mục tiêu của pooling

  • Giảm tham số ở layer sau: feature map nhỏ hơn → input cho Conv hoặc FC kế tiếp nhỏ hơn → ít param và ít FLOPs. Ví dụ: pool 2x2 stride 2 giảm chi phí khoảng 4 lần.
  • Tăng receptive field hiệu dụng: sau pooling, một pixel ở feature map mới "nhìn" được vùng rộng hơn ở input ban đầu. Conv layer phía sau có thể bắt được pattern lớn hơn mà không cần kernel to.
  • Translation invariance nhẹ: nếu một feature dịch chuyển vài pixel trong vùng pooling, max / mean của vùng đó thay đổi rất ít. Mô hình bớt nhạy với shift nhỏ trong ảnh đầu vào.

Translation invariance ở đây là yếu — chỉ trong phạm vi vài pixel của cửa sổ pool. Để invariance mạnh hơn (kiểu xoay, scale), CNN dựa vào data augmentation chứ không chỉ pooling.

4

Max Pooling — công thức

Với input \( X \in \mathbb{R}^{H \times W} \) (xét 1 channel), kernel \( k \times k \) và stride \( s \), cửa sổ ứng với vị trí output \( (i, j) \) là tập chỉ số:

\[ W_{ij} = \{(u, v) \mid i \cdot s \le u < i \cdot s + k,\; j \cdot s \le v < j \cdot s + k\} \]

Max Pool chọn giá trị lớn nhất trong cửa sổ:

\[ Y_{ij} = \max_{(u, v) \in W_{ij}} X_{u, v} \]

Trực giác: max pool giữ lại tín hiệu mạnh nhất (peak feature). Nếu Conv phía trước đã phát hiện được "có cạnh ở đâu đó trong vùng này", max pool chuyển thông tin đó thành 1 giá trị đại diện. Đây là phép pooling phổ biến nhất trong CNN cho classification.

5

Average Pooling — công thức

Avg Pool lấy trung bình trong cửa sổ:

\[ Y_{ij} = \frac{1}{|W_{ij}|} \sum_{(u, v) \in W_{ij}} X_{u, v} \]

Với cửa sổ \( k \times k \) thông thường thì \( |W_{ij}| = k^2 \).

Trực giác: avg pool "làm mượt" feature map. Thay vì chọn peak, nó giữ context tổng thể của cả vùng — kể cả tín hiệu nền (background). LeNet-5 (LeCun et al., 1998) dùng avg pool; các CNN sau đó (AlexNet, VGG) chuyển sang max pool vì cho accuracy tốt hơn trên classification.

6

Tham số: kernel_size, stride, padding

  • kernel_size: kích thước cửa sổ pool. Thường 2x2 (đôi khi 3x3 — như AlexNet với MaxPool2d(3, stride=2) overlapping).
  • stride: bước trượt. PyTorch default stride = kernel_size → cửa sổ không chồng nhau (non-overlapping). Với kernel=2, stride=2: output \( H/2 \times W/2 \).
  • padding: ít dùng với pooling. Đôi khi đặt 1 với kernel 3 để giữ kích thước chẵn.
  • ceil_mode (PyTorch): khi kích thước không chia hết, dùng ceil thay floor. Default False (dùng floor).
  • dilation: hiếm dùng với pool, để tăng khoảng cách giữa các phần tử trong cửa sổ.

Pattern thực dụng nhất: MaxPool2d(kernel_size=2, stride=2) — gọn, không overlap, giảm spatial đúng nửa.

7

Công thức output size

Với input \( H_{\text{in}} \), kernel \( k \), stride \( s \), padding \( p \):

\[ H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor + 1 \]

Khi ceil_mode=True, thay floor bằng ceil.

Bốn case hay gặp:

  • \( k = 2, s = 2, p = 0 \): \( H_{\text{out}} = \lfloor H_{\text{in}} / 2 \rfloor \). Giảm đúng nửa, áp dụng cho input chẵn.
  • \( k = 3, s = 2, p = 1 \): \( H_{\text{out}} = \lceil H_{\text{in}} / 2 \rceil \). Overlapping pool, dùng trong ResNet đầu vào.
  • \( k = 2, s = 1, p = 0 \): \( H_{\text{out}} = H_{\text{in}} - 1 \). Hiếm — chỉ giảm 1 pixel.
  • Global pool (kernel = toàn bộ feature map): \( H_{\text{out}} = 1 \).

Ví dụ tính nhanh: input (1, 32, 32) qua MaxPool2d(2, 2) → output (1, 16, 16). Tiếp tục qua MaxPool2d(2, 2)(1, 8, 8). Sau 5 lần pool 2x2 từ ảnh 224x224 → \( 224 / 2^5 = 7 \) → (C, 7, 7) (đúng kích thước feature map trước FC trong VGG / ResNet).

8

PyTorch API — MaxPool2d và AvgPool2d

import torch
import torch.nn as nn

x = torch.randn(1, 3, 32, 32)   # (B, C, H, W)

max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)

y_max = max_pool(x)             # (1, 3, 16, 16)
y_avg = avg_pool(x)             # (1, 3, 16, 16)

Vài tham số khác có sẵn:

nn.MaxPool2d(
    kernel_size=2,
    stride=None,        # None -> mặc định = kernel_size
    padding=0,
    dilation=1,
    return_indices=False,  # True trả thêm vị trí max (dùng cho MaxUnpool2d)
    ceil_mode=False,
)

nn.AvgPool2d(
    kernel_size=2,
    stride=None,
    padding=0,
    ceil_mode=False,
    count_include_pad=True,  # có tính padding khi lấy mean hay không
    divisor_override=None,   # thay mẫu số mặc định k*k
)

Functional version cho phép gọi inline không tạo module:

import torch.nn.functional as F
y = F.max_pool2d(x, kernel_size=2, stride=2)
9

Pooling không có parameter học

Khác Conv layer (có weightbias học được), pooling là một phép toán cố định: max và mean không có tham số. Số param của MaxPool2d hay AvgPool2d luôn là 0.

Hệ quả thực hành:

  • Không cần init, không cần regularize.
  • Không xuất hiện trong model.parameters() — optimizer bỏ qua.
  • Behavior train và eval giống nhau (khác DropoutBatchNorm).
  • Forward / backward nhanh, ít tốn bộ nhớ.

Có thể coi pooling là một "feature engineering layer" cứng — không học, chỉ thực hiện downsample theo quy tắc đã định.

10

Backward của pooling

Dù không có parameter, pooling vẫn cần backward để gradient chảy ngược về Conv layer phía trước.

Max Pool: gradient từ output \( Y_{ij} \) chỉ đi ngược về đúng vị trí đạt max trong cửa sổ forward. Các vị trí còn lại nhận gradient 0:

\[ \frac{\partial L}{\partial X_{u, v}} = \begin{cases} \dfrac{\partial L}{\partial Y_{ij}} & \text{nếu } (u, v) = \arg\max_{W_{ij}} X \\ 0 & \text{ngược lại} \end{cases} \]

PyTorch lưu lại "argmax index" của mỗi cửa sổ trong forward để dùng cho backward. Nếu hai phần tử cùng giá trị max, tie-break tùy implementation (thường chọn phần tử đầu).

Avg Pool: gradient được chia đều cho mọi vị trí trong cửa sổ:

\[ \frac{\partial L}{\partial X_{u, v}} = \frac{1}{|W_{ij}|} \cdot \frac{\partial L}{\partial Y_{ij}} \quad \text{với mọi } (u, v) \in W_{ij} \]

Tính chất so sánh:

  • Max pool gradient sparse: chỉ 1 phần tử / cửa sổ có gradient → cập nhật tập trung vào neuron mạnh.
  • Avg pool gradient dense: mọi phần tử nhận gradient đều → smooth hơn, nhưng tín hiệu yếu hơn.
11

Position trong CNN

Vị trí tiêu chuẩn của pooling trong một block CNN:

Conv  →  Activation (ReLU)  →  Pool  →  Conv  →  Activation  →  Pool  →  ...

Lý do đặt pool sau activation: ReLU đã làm sạch phần âm; max của output ReLU vẫn có ý nghĩa "feature strength". Nếu đảo thứ tự (pool trước activation), max của pre-activation có thể là số âm — sau ReLU sẽ thành 0, giảm hiệu quả.

Nếu block có BatchNorm, thứ tự đầy đủ là: Conv → BN → ReLU → Pool. BN nằm ngay sau Conv vì pre-activation \( z = Wx + b \) gần Gaussian, dễ chuẩn hóa.

Modern CNN không pool ở mọi block. Phổ biến: 2–3 conv liên tiếp (giữ nguyên spatial), sau đó 1 pool (giảm nửa). Lặp 4–5 lần, đến lúc feature map còn 7x7 hoặc 1x1.

12

Global Average Pooling (GAP)

Global Average Pooling lấy mean qua toàn bộ spatial dimension. Input \( (B, C, H, W) \) → output \( (B, C, 1, 1) \) (hoặc squeeze thành \( (B, C) \)):

\[ Y_c = \frac{1}{H \cdot W} \sum_{u=1}^{H} \sum_{v=1}^{W} X_{c, u, v} \]

Đề xuất lần đầu trong "Network in Network" (Lin, Chen & Yan, 2014 — arXiv:1312.4400) như cách thay Flatten → FC → Softmax ở cuối CNN bằng GAP → Softmax.

Lợi ích:

  • Giảm tham số đáng kể: FC từ \( C \times H \times W \) chiều xuống 1000 class (VGG) có khoảng 100 triệu param chỉ riêng lớp FC cuối. GAP thay thế FC bằng phép mean — 0 param.
  • Chịu được input size khác nhau: không phụ thuộc \( H, W \) cụ thể.
  • Regularization tự nhiên: không có param FC để overfit.
  • Có ý nghĩa diễn giải: mỗi channel đại diện một class / concept; mean spatial là "độ mạnh" của concept đó trong ảnh.

API PyTorch:

gap = nn.AdaptiveAvgPool2d((1, 1))
# hoặc
y = F.adaptive_avg_pool2d(x, output_size=(1, 1))

Hầu như mọi CNN modern (ResNet, Inception, EfficientNet, ConvNeXt) đều có GAP trước FC cuối cùng.

Global Max Pooling tồn tại như alternative (AdaptiveMaxPool2d), nhưng GAP phổ biến hơn nhiều.

13

Adaptive Pooling

Adaptive pooling đảo bài toán: thay vì truyền kernelstride, ta chỉ định output size mong muốn; PyTorch tự tính kernel và stride.

pool = nn.AdaptiveAvgPool2d(output_size=(7, 7))
y = pool(torch.randn(1, 512, 14, 14))    # (1, 512, 7, 7)

# Cũng hoạt động với input shape khác
y = pool(torch.randn(1, 512, 28, 28))    # (1, 512, 7, 7)

Use case chính:

  • Ghép feature map có spatial khác nhau (nhiều scale) về cùng size trước khi concat.
  • Cho phép input ảnh kích thước thay đổi mà phần FC cuối vẫn nhận shape cố định.
  • Thay thế hard-code kernel / stride khi không biết chính xác feature map đầu vào.

Lưu ý: với output size không chia hết input size, kernel / stride mỗi vị trí có thể khác nhau (kernel "biến thiên") — không hoàn toàn equivalent với pool thông thường.

Hai dòng API tương ứng: AdaptiveAvgPool2dAdaptiveMaxPool2d. Cũng có version 1d và 3d.

14

Max vs Avg — khi dùng cái nào

Task / vị tríKhuyên dùngLý do
Classification, downsample giữa blockMax Poolgiữ peak feature, gradient sparse cập nhật neuron mạnh
Object detection (peak feature)Max Pooledge / corner cần được giữ rõ
Segmentation, super-resolutionAvg Poolsmooth, giữ thông tin nền
Style transferAvg Poolthông tin texture phân bố đều, max làm mất
Cuối CNN modern (trước FC / softmax)GAPgiảm param, regularize
Cần output size cố định cho input đa kích thướcAdaptivelinh hoạt

Khi không chắc và làm classification — chọn Max Pool 2x2 stride 2 ở giữa block + GAP ở cuối. Hai lựa chọn này phủ phần lớn use case CNN cho ảnh.

15

Striding vs Pooling

Có hai cách downsample feature map:

  • Pool: phép cố định (max / mean), không param.
  • Stride convolution: dùng Conv với stride=2 — kernel học được vừa rút trích feature vừa downsample.

So sánh:

Tiêu chíPoolStride Conv
Có parameter họckhông
FLOPs khi downsamplethấpcao hơn
Linh hoạtcố địnhhọc theo data
Translation invariancecó (nhẹ)không có sẵn
Đại diện kiến trúcVGG, AlexNetResNet, EfficientNet

Springenberg et al. (2014) — "Striving for Simplicity: The All Convolutional Net" (arXiv:1412.6806) — thay toàn bộ pool bằng stride conv, kết quả gần ngang VGG nhưng kiến trúc gọn hơn.

Modern: ResNet giữ 1 MaxPool ở đầu1 GAP ở cuối; phần giữa dùng Conv2d(stride=2) để downsample. EfficientNet thậm chí bỏ luôn MaxPool đầu.

Không có lựa chọn "đúng tuyệt đối" — pool tiết kiệm param, stride conv linh hoạt. Thiết kế mới có xu hướng dùng stride conv nhiều hơn, nhưng pool vẫn xuất hiện vì rẻ và đơn giản.

16

Pooling cho 1D và 3D

  • nn.MaxPool1d / nn.AvgPool1d: input \( (B, C, L) \) — time series, audio waveform, 1D signal. Ví dụ trong WaveNet-style hay TCN.
  • nn.MaxPool2d / nn.AvgPool2d: input \( (B, C, H, W) \) — ảnh. Đã thảo luận chi tiết.
  • nn.MaxPool3d / nn.AvgPool3d: input \( (B, C, D, H, W) \) — video (D là time-axis) hoặc medical volume (CT, MRI). Dùng cùng với Conv3d.

Các phiên bản này khác nhau ở số trục pool, công thức output size áp dụng độc lập theo từng trục. Có đầy đủ Adaptive variant: AdaptiveAvgPool1d, AdaptiveAvgPool3d, ...

17

"Pooling is a big mistake" — Hinton

Geoffrey Hinton đã phát biểu nhiều lần (Reddit AMA 2014, các talk sau đó) rằng "The pooling operation used in convolutional neural networks is a big mistake and the fact that it works so well is a disaster."

Lý do của Hinton:

  • Pooling vứt đi thông tin về vị trí chính xác của feature — chỉ giữ "có" hoặc "không có" ở vùng rộng.
  • Translation invariance là một kiểu equivariance "thô" — không phân biệt được object với background khi cả hai cùng có cùng feature pattern.
  • Mạng học pattern thay vì học cấu trúc phân cấp của object.

Hinton đề xuất Capsule Network (Sabour, Frosst & Hinton, 2017 — arXiv:1710.09829) thay pooling bằng dynamic routing between capsules: thay vì rút gọn về 1 scalar, mỗi capsule giữ một vector mã hóa pose / orientation / scale của feature.

Thực tế đến nay: Capsule Network chưa scale lên ImageNet, pooling vẫn dominant trong CNN. Critique của Hinton vẫn đáng đọc khi suy nghĩ về limitation của kiến trúc hiện hành, nhưng người dùng thực tế chưa có lý do bỏ pool.

18

Visualization 4x4 → 2x2

Input 4x4 chia thành 4 vùng 2x2 không overlap:

Input X (4x4):
   1   3 | 2   4
   5   6 | 7   8
   ---------+---------
   3   2 | 1   0
   1   2 | 3   4

Max Pool 2x2 stride 2 → Output Y (2x2):
   max(1,3,5,6)  max(2,4,7,8)        6   8
   max(3,2,1,2)  max(1,0,3,4)   =    3   4

Avg Pool 2x2 stride 2 → Output Y (2x2):
   mean(1,3,5,6)  mean(2,4,7,8)      3.75   5.25
   mean(3,2,1,2)  mean(1,0,3,4)  =   2.00   2.00

Quan sát: max pool giữ giá trị nổi bật trong từng cửa sổ; avg pool đưa ra số đại diện smooth hơn nhưng giảm tín hiệu peak.

19

Pattern pooling trong các kiến trúc kinh điển

Kiến trúcNămPooling
LeNet-51998Conv → AvgPool 2x2 → Conv → AvgPool 2x2 → FC
AlexNet20125 Conv + 3 MaxPool 3x3 stride 2 (overlapping)
VGG-16201413 Conv chia 5 block, mỗi block kết thúc 1 MaxPool 2x2
GoogLeNet / Inception2014MaxPool xen kẽ trong Inception block + GAP cuối
ResNet20151 MaxPool 3x3 stride 2 đầu, các block sau dùng Conv stride 2, GAP cuối
DenseNet2017AvgPool 2x2 trong transition layer + GAP cuối
EfficientNet2019Hầu hết stride conv, không MaxPool, GAP cuối
ConvNeXt2022Downsample bằng Conv 2x2 stride 2 (no pool), GAP cuối

Xu hướng rõ: pool giữa block ngày càng ít, GAP ở cuối ngày càng phổ biến.

20

Code Python — Max Pool tay và PyTorch

Max Pool 2x2 stride 2 bằng NumPy:

import numpy as np

def max_pool_2x2(x: np.ndarray) -> np.ndarray:
    """x shape (H, W) với H, W chẵn. Trả về (H/2, W/2)."""
    H, W = x.shape
    assert H % 2 == 0 and W % 2 == 0
    # Reshape thành khối 2x2 rồi lấy max
    blocks = x.reshape(H // 2, 2, W // 2, 2)
    return blocks.max(axis=(1, 3))

X = np.array([
    [1, 3, 2, 4],
    [5, 6, 7, 8],
    [3, 2, 1, 0],
    [1, 2, 3, 4],
])
print(max_pool_2x2(X))
# [[6 8]
#  [3 4]]

So sánh với PyTorch:

import torch
import torch.nn as nn

x = torch.tensor(X, dtype=torch.float32).unsqueeze(0).unsqueeze(0)  # (1, 1, 4, 4)
pool = nn.MaxPool2d(kernel_size=2, stride=2)
print(pool(x).squeeze().numpy())
# [[6. 8.]
#  [3. 4.]]

Một CNN nhỏ với Conv + Pool + GAP:

cnn = nn.Sequential(
    nn.Conv2d(3, 32, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),                       # 32x32 -> 16x16

    nn.Conv2d(32, 64, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),                       # 16x16 -> 8x8

    nn.Conv2d(64, 128, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d((1, 1)),             # GAP -> (B, 128, 1, 1)
    nn.Flatten(),
    nn.Linear(128, 10),
)

x = torch.randn(8, 3, 32, 32)
print(cnn(x).shape)   # torch.Size([8, 10])

Demo visualize before / after pool trên feature map thật:

import torch.nn.functional as F
from torchvision import datasets, transforms

ds = datasets.MNIST("./data", train=True, download=True,
                    transform=transforms.ToTensor())
img, _ = ds[0]                       # (1, 28, 28)
img = img.unsqueeze(0)               # (1, 1, 28, 28)

# Một conv đơn giản phát hiện edge
conv = nn.Conv2d(1, 1, kernel_size=3, padding=1, bias=False)
conv.weight.data = torch.tensor([[[[-1., -1., -1.],
                                    [ 0.,  0.,  0.],
                                    [ 1.,  1.,  1.]]]])

feat = F.relu(conv(img))             # (1, 1, 28, 28)
pooled = F.max_pool2d(feat, 2, 2)    # (1, 1, 14, 14)
print(feat.shape, "->", pooled.shape)
21

Bài tập

  1. Cho input shape (1, 32, 32) qua MaxPool2d(kernel_size=2, stride=2). Tính output shape. Tiếp tục qua hai pool 2x2 nữa, output thành bao nhiêu?
  2. Tính output shape khi input \( 224 \times 224 \) qua MaxPool2d(kernel_size=3, stride=2, padding=1) (pool đầu vào của ResNet).
  3. So sánh số param giữa Conv2d(64, 64, kernel_size=3, stride=2, padding=1)MaxPool2d(2, 2) — cả hai đều giảm spatial nửa. Câu trả lời bằng số.
  4. Implement avg_pool_2x2 bằng NumPy giống max_pool_2x2 trong bài. Verify khớp với nn.AvgPool2d(2, 2).
  5. Apply Global Average Pooling cho tensor x = torch.randn(4, 256, 7, 7) bằng cả nn.AdaptiveAvgPool2d((1, 1))x.mean(dim=(2, 3)). Verify hai cách cho kết quả giống nhau.
  6. Lấy 1 ảnh CIFAR-10 (kích thước 32x32), đẩy qua Conv2d(3 → 16, 3, padding=1) → ReLU. Visualize feature map của 4 channel đầu trước và sau MaxPool2d(2, 2) bằng matplotlib. Quan sát "feature mạnh ở vùng nào".
  7. Train 2 CNN trên CIFAR-10 (5 epoch, cùng hyperparameter): phiên bản A dùng MaxPool2d(2, 2) giữa block, phiên bản B dùng Conv2d(stride=2). So sánh test accuracy và số param.
  8. Demo gradient sparse của Max Pool: dùng torch.autograd.grad trên F.max_pool2d với input 4x4, kernel 2 stride 2. Verify chỉ 4 vị trí có gradient khác 0 (đúng vị trí max).
Đáp án ngắn
  1. 32 → 16 → 8 → 4. Sau 3 pool, output \( (1, 4, 4) \).
  2. \( \lfloor (224 + 2 \cdot 1 - 3) / 2 \rfloor + 1 = 112 \). Output 112x112.
  3. Conv: \( 64 \cdot 64 \cdot 3 \cdot 3 + 64 = 36{,}928 \) param. MaxPool: 0 param.
  4. Dùng blocks.mean(axis=(1, 3)) thay cho max. Kết quả khớp PyTorch đến lỗi float.
  5. Output cả hai cách: shape \( (4, 256) \) sau squeeze, giá trị bằng nhau (chênh do float precision < \( 10^{-6} \)).
  6. Sau MaxPool 2x2, feature map còn 16x16; các đốm sáng (activation cao) giữ lại, vùng yếu bị nén.
  7. Hai cách cho accuracy gần nhau (chênh 1–2%). Phiên bản B nhiều param hơn (Conv stride 2 có weight) nhưng học được downsample tốt hơn trong một số seed.
  8. Tensor gradient có shape 4x4 với đúng 4 phần tử khác 0 — chính là các vị trí được chọn ở mỗi cửa sổ 2x2 trong forward.

Bài kế tiếp: Bài 30 — Flatten và Fully Connected — cách nối feature map sau Conv / Pool về vector phẳng và đưa vào FC để cho ra logit phân loại.