Mục lục
- Mục tiêu bài học
- Kernel, convolution, feature map — ba khái niệm cốt lõi
- Phép convolution 2D — công thức và minh hoạ
- Cross-correlation và convolution trong DL
- Các tham số của một Conv layer
- Output size formula
- Padding — 'valid' và 'same'
- Stride lớn hơn 1 — downsampling
- Multi-channel input và shape của kernel
- Weight sharing và so sánh param Conv vs MLP
- PyTorch API — nn.Conv2d
- 1x1 Convolution — channel mixing
- Depthwise Separable Convolution — MobileNet
- Dilated (Atrous) Convolution
- Receptive field — vùng input ảnh hưởng output
- Kernel filter học gì — quan sát qua các tầng
- Conv1d và Conv3d
- Common kernel size — 3x3, 5x5, 7x7, 1x1
- Code Python — convolution bằng NumPy và Sobel filter
- Code Python — nn.Conv2d và verify output shape
- Bài tập
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Mô tả được phép convolution 2D bằng công thức dot product trượt qua input.
- Tính ra output size của một Conv layer theo input size,
kernel_size,stride,padding,dilation. - Tính được số parameter của một Conv layer multi-channel và so sánh với một Linear layer tương đương.
- Giải thích vì sao weight sharing làm CNN có ít parameter và translation-equivariant.
- Đặt đúng
in_channels,out_channels,kernel_size,stride,paddingtrongnn.Conv2d. - Phân biệt 1x1 convolution, depthwise separable, dilated convolution và biết task nào dùng cái nào.
- Tính receptive field của một stack Conv layer.
Bài này nối Bài 27 — Vì sao MLP không hiệu quả cho ảnh (động lực dẫn đến CNN) với Bài 29 — Pooling Layer (downsampling đi kèm Conv để tăng receptive field).
Kernel, convolution, feature map — ba khái niệm cốt lõi
Kernel (còn gọi là filter) là một ma trận trọng số nhỏ, thường \( 3 \times 3 \) hoặc \( 5 \times 5 \), trong đó mỗi ô là một số học được. Ví dụ một kernel \( 3 \times 3 \):
\[ K = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} \]
Đây là Sobel kernel theo trục \( x \) — detect cạnh dọc. Trong CNN, kernel khởi tạo ngẫu nhiên và học qua backpropagation; không tự đặt tay (trừ trường hợp xử lý ảnh cổ điển).
Convolution là phép trượt kernel qua từng vị trí của input và tính dot product giữa kernel với cửa sổ tương ứng. Output mỗi vị trí là một số thực — tổ hợp tuyến tính của những pixel local mà kernel nhìn thấy.
Feature map là tensor 2D (hoặc 3D với multi-channel) chứa toàn bộ output của một kernel sau khi trượt hết input. Một filter sinh ra một feature map; \( K \) filter sinh ra \( K \) feature map xếp chồng.
Trực giác: mỗi feature map đáp ứng cao tại những vùng input "trông giống" pattern mà kernel đó đại diện. Một filter có thể chuyên detect edge ngang, filter khác detect texture chấm, filter sâu hơn detect bộ phận khuôn mặt — mạng tự học cái gì hữu ích cho task.
Phép convolution 2D — công thức và minh hoạ
Cho input \( X \in \mathbb{R}^{H \times W} \) và kernel \( K \in \mathbb{R}^{k_h \times k_w} \). Output \( Y \) tại vị trí \( (i, j) \):
\[ Y_{i, j} = \sum_{u = 0}^{k_h - 1} \sum_{v = 0}^{k_w - 1} X_{i + u,\, j + v} \cdot K_{u, v} \]
Tức: đặt kernel chồng lên cửa sổ \( k_h \times k_w \) của input bắt đầu tại \( (i, j) \), nhân ô-với-ô, cộng tất cả lại thành một số. Trượt kernel sang vị trí kế tiếp, lặp lại.
Ví dụ cụ thể với \( X \in \mathbb{R}^{4 \times 4} \) và \( K \in \mathbb{R}^{3 \times 3} \):
Input X: Kernel K:
[1 2 3 0] [1 0 -1]
[0 1 2 3] [1 0 -1]
[3 0 1 2] [1 0 -1]
[2 3 0 1]
Tại vị trí \( (0, 0) \): kernel chồng lên cửa sổ \( X[0:3, 0:3] \). Tính:
\[ Y_{0, 0} = 1\cdot 1 + 2\cdot 0 + 3\cdot(-1) + 0\cdot 1 + 1\cdot 0 + 2\cdot(-1) + 3\cdot 1 + 0\cdot 0 + 1\cdot(-1) = -2 \]
Trượt kernel sang phải 1 ô (stride = 1), tính \( Y_{0, 1} \), v.v. Với input \( 4 \times 4 \) và kernel \( 3 \times 3 \), stride 1, không padding — output có shape \( 2 \times 2 \).
Mỗi giá trị trong output là kết hợp local: chỉ phụ thuộc \( k_h \cdot k_w \) pixel input gần kề. Đây là khác biệt cốt lõi so với Linear / fully-connected — nơi mỗi output phụ thuộc toàn bộ input.
Cross-correlation và convolution trong DL
Trong toán học và DSP, convolution chính tắc có bước "flip" kernel trước khi dot product:
\[ (X * K)_{i, j} = \sum_{u, v} X_{i - u,\, j - v} \cdot K_{u, v} \]
Còn phép không flip — chỉ dot product trượt — gọi là cross-correlation:
\[ (X \star K)_{i, j} = \sum_{u, v} X_{i + u,\, j + v} \cdot K_{u, v} \]
Trong deep learning, hàm "convolution" mà PyTorch / TensorFlow gọi thực ra là cross-correlation (không flip). Lý do đơn giản: kernel học được, nên flip hay không không thay đổi kết quả cuối — mạng tự học weight đã "pre-flipped" nếu cần. Việc bỏ flip rẻ hơn một chút về tính toán.
Hệ quả thực hành: khi đọc paper hoặc code, "convolution" trong context DL đồng nghĩa với cross-correlation. Không cần nhớ chi tiết flip trừ khi làm xử lý tín hiệu cổ điển.
Các tham số của một Conv layer
Khi định nghĩa một Conv layer, có sáu tham số quan trọng:
kernel_size: kích thước kernel, ví dụ \( 3 \times 3 \), \( 5 \times 5 \), \( 7 \times 7 \). Kernel vuông là phổ biến; có thể bất đối xứng (vd \( 1 \times 7 \) trong Inception).stride: bước trượt giữa các vị trí kế tiếp. Default 1 (trượt 1 ô). Stride 2 nghĩa là bỏ qua 1 ô — output bị downsample gần một nửa.padding: số ô 0 thêm vào viền input. Default 0 (không padding). Padding 1 với kernel \( 3 \times 3 \) giữ nguyên output size.dilation: khoảng cách giữa các phần tử trong kernel. Default 1 (kernel liền). Dilation 2 nghĩa là kernel "rỗng" — phần tử cách nhau 1 ô (xem mục 14).in_channels: số channel của input. Ảnh RGB có 3, ảnh xám có 1, feature map từ Conv trước có thể có 64, 128, ...out_channels: số filter trong layer này = số feature map output. Tự chọn — quyết định "độ rộng" của Conv layer.
Trong PyTorch:
nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
stride=1,
padding=1,
dilation=1,
bias=True,
)
Output size formula
Cho input chiều cao \( H_{\text{in}} \), kernel chiều cao \( k \), padding \( p \) (mỗi bên), stride \( s \), dilation \( d \). Output chiều cao:
\[ H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2p - d(k - 1) - 1}{s} \right\rfloor + 1 \]
Trường hợp \( d = 1 \) (default) gọn lại thành:
\[ H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor + 1 \]
Công thức tương tự cho chiều rộng \( W_{\text{out}} \). Vài ví dụ thường gặp với input \( 32 \times 32 \):
| Cấu hình | Tính | Output |
|---|---|---|
| \( k = 3, s = 1, p = 0 \) | \( \lfloor (32 - 3)/1 \rfloor + 1 \) | 30 |
| \( k = 3, s = 1, p = 1 \) | \( \lfloor (32 + 2 - 3)/1 \rfloor + 1 \) | 32 (same) |
| \( k = 3, s = 2, p = 1 \) | \( \lfloor (32 + 2 - 3)/2 \rfloor + 1 \) | 16 (downsample) |
| \( k = 5, s = 1, p = 2 \) | \( \lfloor (32 + 4 - 5)/1 \rfloor + 1 \) | 32 (same) |
| \( k = 7, s = 2, p = 3 \) | \( \lfloor (32 + 6 - 7)/2 \rfloor + 1 \) | 16 |
Quy tắc nhanh để giữ same size với kernel lẻ: padding = (kernel_size - 1) // 2.
Padding — 'valid' và 'same'
Có hai chế độ padding phổ biến (thừa hưởng từ TensorFlow):
- 'valid' — không padding (\( p = 0 \)). Output nhỏ hơn input. Mỗi vị trí output chỉ tính từ pixel input thật, không có 0 giả.
- 'same' — padding sao cho output size = input size khi stride = 1. Với kernel \( 3 \times 3 \): \( p = 1 \). Với \( 5 \times 5 \): \( p = 2 \).
Vì sao thường thích 'same'? Vài lý do:
- Không mất dữ liệu biên — pixel ở mép vẫn được kernel "nhìn" đủ số lần.
- Dễ stack nhiều Conv: shape không bị thu nhỏ dần qua mỗi layer, chỉ thu nhỏ khi cố ý dùng stride hoặc pooling.
- Tính output size dễ — chỉ cần để ý stride.
PyTorch hỗ trợ padding='same' từ 1.9+, nhưng chỉ với stride = 1. Cách thông dụng vẫn là truyền số int trực tiếp:
nn.Conv2d(64, 128, kernel_size=3, padding=1) # same
nn.Conv2d(64, 128, kernel_size=5, padding=2) # same
nn.Conv2d(64, 128, kernel_size=3, padding='same') # same (chỉ stride=1)
nn.Conv2d(64, 128, kernel_size=3, padding=0) # valid
Padding bằng 0 là default. PyTorch còn cho phép padding_mode khác ('reflect', 'replicate', 'circular') — dùng khi 0-padding gây artifact ở biên (vd super-resolution).
Stride lớn hơn 1 — downsampling
Mặc định stride=1: kernel trượt từng ô. Với stride=2, kernel nhảy 2 ô — output có chiều cao và rộng giảm ~một nửa.
Ý nghĩa: stride lớn hơn 1 là một dạng downsampling tích hợp trong chính Conv layer. Hai cách downsample phổ biến trong CNN:
- Conv với stride = 2: học cách downsample qua trọng số (learnable). Dùng trong ResNet, MobileNet, EfficientNet.
- Pooling layer riêng: max-pool hoặc average-pool, không học (xem Bài 29). Dùng trong VGG, LeNet.
Modern architecture sau 2015 nghiêng về strided convolution thay pooling: cùng tác dụng giảm size, nhưng learnable, đôi khi accuracy tốt hơn nhẹ. Pooling vẫn dùng khi muốn rẻ và đơn giản.
# Block downsample điển hình
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1)
# input (B, 64, 32, 32)
# output (B, 128, 16, 16)
Multi-channel input và shape của kernel
Input thực tế hiếm khi 1 channel. Ảnh RGB có 3, feature map từ Conv trước có thể có \( C_{\text{in}} = 64, 128, 256 \).
Với input \( X \in \mathbb{R}^{C_{\text{in}} \times H \times W} \), một filter có shape \( (C_{\text{in}}, k_h, k_w) \) — tức kernel mở rộng theo trục channel để khớp với input. Convolution tính qua cả ba trục:
\[ Y_{i, j} = \sum_{c = 0}^{C_{\text{in}} - 1} \sum_{u = 0}^{k_h - 1} \sum_{v = 0}^{k_w - 1} X_{c,\, i + u,\, j + v} \cdot K_{c, u, v} + b \]
Mỗi filter cho ra một feature map 2D (gộp thông tin từ mọi channel input). Conv layer thường có nhiều filter: với \( C_{\text{out}} \) filter, output có \( C_{\text{out}} \) feature map xếp chồng — shape \( (C_{\text{out}}, H_{\text{out}}, W_{\text{out}}) \).
Shape đầy đủ của weight tensor trong một Conv layer:
\[ W \in \mathbb{R}^{C_{\text{out}} \times C_{\text{in}} \times k_h \times k_w}, \quad b \in \mathbb{R}^{C_{\text{out}}} \]
Mỗi bias là một scalar, cộng vào toàn bộ feature map của filter tương ứng (broadcasting qua không gian).
Một cách hiểu khác: mỗi filter là một detector. \( C_{\text{out}} \) filter trong cùng layer = \( C_{\text{out}} \) detector học song song, mỗi cái nhìn vào cùng input nhưng học pattern khác nhau.
Weight sharing và so sánh param Conv vs MLP
Điểm cực kỳ quan trọng: cùng một kernel áp dụng mọi vị trí trong input. Đây là weight sharing. Hệ quả:
- Số parameter của Conv layer không phụ thuộc kích thước ảnh \( H, W \). Một Conv \( 3 \times 3 \) có cùng param dù input \( 32 \times 32 \) hay \( 1024 \times 1024 \).
- Conv là translation-equivariant: dịch chuyển input thì feature map cũng dịch tương ứng. Một mèo ở góc trái hay góc phải đều được cùng filter "nhận diện".
Công thức param của một Conv layer:
\[ \#\text{params} = C_{\text{out}} \cdot C_{\text{in}} \cdot k_h \cdot k_w + C_{\text{out}} \]
Số cuối là bias (\( C_{\text{out}} \)). Vài ví dụ:
- Conv 1 filter \( 3 \times 3 \) trên ảnh 1 channel: \( 1 \cdot 1 \cdot 3 \cdot 3 + 1 = 10 \) param.
- Conv 64 filter \( 3 \times 3 \) trên input 3 channel (RGB): \( 64 \cdot 3 \cdot 3 \cdot 3 + 64 = 1\,792 \) param.
- Conv 128 filter \( 3 \times 3 \) trên input 64 channel: \( 128 \cdot 64 \cdot 3 \cdot 3 + 128 = 73\,856 \) param.
So sánh với MLP (Linear) cho cùng "task" — biến ảnh \( 32 \times 32 \times 3 \) thành 64 feature ở mọi vị trí (giả sử output cũng giữ shape \( 32 \times 32 \)):
- Linear "flatten" input \( 3072 \) thành output \( 32 \cdot 32 \cdot 64 = 65\,536 \): cần \( 3072 \cdot 65536 + 65536 \approx 2.01 \times 10^8 \) param.
- Conv2d(3, 64, kernel=3, padding=1): chỉ \( 1\,792 \) param — kém Linear hơn 110\,000 lần.
Nói cách khác: Conv không "thiếu khả năng" so với MLP — nó chỉ áp dụng một prior rất mạnh (locality + translation invariance). Prior này đúng với ảnh tự nhiên, nên Conv vừa tiết kiệm vừa thường tổng quát hóa tốt hơn.
PyTorch API — nn.Conv2d
Signature đầy đủ (PyTorch 2.x):
nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
)
Một block điển hình:
import torch
import torch.nn as nn
conv = nn.Conv2d(in_channels=3, out_channels=64,
kernel_size=3, stride=1, padding=1)
x = torch.randn(8, 3, 32, 32) # (B, C, H, W)
y = conv(x)
print(y.shape) # torch.Size([8, 64, 32, 32])
print(conv.weight.shape) # torch.Size([64, 3, 3, 3])
print(conv.bias.shape) # torch.Size([64])
Một số quy ước PyTorch cần nhớ:
- Input shape luôn là \( (B, C, H, W) \) — channels-first. Khác với TensorFlow / Keras default (channels-last \( (B, H, W, C) \)).
kernel_size,stride,padding,dilationnhận int hoặc tuple.kernel_size=3tương đương(3, 3); nếu muốn kernel chữ nhật thì truyền(3, 5).groups: chia kernel thành \( g \) nhóm độc lập.groups=1là Conv thường.groups=in_channelslà depthwise Conv (xem mục 13).bias=Falsekhi sau Conv có BatchNorm (BN sẽ học shift thay) — xem Bài 23 — Batch Normalization.
1x1 Convolution — channel mixing
Kernel \( 1 \times 1 \) nhìn qua trục không gian chỉ thấy một pixel, nhưng vẫn nhìn đủ \( C_{\text{in}} \) channel. Tác dụng: nó hoạt động như một Linear layer áp dụng độc lập tại mỗi vị trí — kết hợp channel.
Đặc trưng:
- Dimension reduction: Conv2d(256, 64, kernel_size=1) giảm số channel từ 256 xuống 64 với rất ít param: \( 256 \cdot 64 + 64 = 16\,448 \).
- Network-in-Network (Lin et al., 2013): xen kẽ Conv \( 3 \times 3 \) và Conv \( 1 \times 1 \) để thêm phi tuyến mà không tăng receptive field.
- ResNet bottleneck: pattern \( 1 \times 1 \to 3 \times 3 \to 1 \times 1 \) giảm channel → conv \( 3 \times 3 \) trên channel ít → mở rộng lại. Tiết kiệm rất nhiều compute (xem He et al., 2016 — ResNet-50, 101, 152 dùng pattern này).
# Bottleneck block kiểu ResNet
nn.Sequential(
nn.Conv2d(256, 64, kernel_size=1, bias=False), # giảm channel
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 256, kernel_size=1, bias=False), # mở rộng lại
nn.BatchNorm2d(256),
)
Depthwise Separable Convolution — MobileNet
Conv thường trộn không gian (qua \( k \times k \)) và channel (qua \( C_{\text{in}} \)) cùng lúc. Depthwise separable (Howard et al., 2017 — MobileNet) tách thành hai bước rời:
- Depthwise Conv: kernel \( 3 \times 3 \) áp dụng độc lập từng channel. Tức \( C_{\text{in}} \) filter, mỗi filter chỉ \( 3 \times 3 \times 1 \), không trộn channel. PyTorch:
groups=in_channels. - Pointwise Conv (1x1): \( 1 \times 1 \) Conv trộn các channel từ bước trước thành \( C_{\text{out}} \) feature map.
def depthwise_separable(in_c, out_c):
return nn.Sequential(
nn.Conv2d(in_c, in_c, kernel_size=3, padding=1,
groups=in_c, bias=False), # depthwise
nn.BatchNorm2d(in_c), nn.ReLU(),
nn.Conv2d(in_c, out_c, kernel_size=1, bias=False), # pointwise
nn.BatchNorm2d(out_c), nn.ReLU(),
)
So sánh param với Conv thường (cùng \( C_{\text{in}} = 256, C_{\text{out}} = 256, k = 3 \)):
- Conv thường: \( 256 \cdot 256 \cdot 9 + 256 = 590\,080 \).
- Depthwise separable: depthwise \( 256 \cdot 9 + 256 = 2\,560 \) + pointwise \( 256 \cdot 256 + 256 = 65\,792 \) = 68\,352 — giảm gần 9 lần. Compute (FLOPs) giảm tỷ lệ tương tự.
Trade-off: chất lượng feature có thể giảm nhẹ so với Conv thường, nhưng bù lại đủ rẻ để chạy real-time trên mobile. Đây là nền tảng MobileNet v1/v2/v3, Xception, EfficientNet.
Dilated (Atrous) Convolution
Dilated convolution (còn gọi atrous — "có lỗ" trong tiếng Pháp) chèn khoảng cách giữa các phần tử kernel. Với dilation \( d = 2 \) trên kernel \( 3 \times 3 \), kernel "rỗng" trông như sau (X = phần tử có trọng số, . = bỏ qua):
d = 1 (thường) d = 2 d = 3
X X X X . X . X X . . X . . X
X X X . . . . . . . . . . . .
X X X X . X . X . . . . . . .
. . . . . X . . X . . X
X . X . X . . . . . . .
. . . . . . .
X . . X . . X
Effective kernel size với dilation \( d \) là \( d (k - 1) + 1 \). Số param không đổi (vẫn \( k \times k \)) nhưng vùng input bị "nhìn" lớn hơn rất nhiều.
Mục đích: tăng receptive field mà không tăng param và không cần downsample. Quan trọng cho:
- Segmentation (DeepLab — Chen et al., 2018): cần output cùng resolution input nhưng vẫn cần context lớn. Stack dilated Conv với rate tăng dần (1, 2, 4, 8) cho effective receptive field rộng.
- Audio: WaveNet (van den Oord et al., 2016) dùng dilated Conv 1D để nhìn xa hàng nghìn sample mà chỉ vài chục layer.
nn.Conv2d(64, 64, kernel_size=3, padding=2, dilation=2)
# effective kernel = 5x5
# nhưng chỉ 64*64*9 + 64 = 36,928 param (như kernel 3x3 thường)
Lưu ý: padding phải tính lại theo dilation để giữ output size. Formula đầy đủ ở mục 6.
Receptive field — vùng input ảnh hưởng output
Receptive field của một neuron output là vùng pixel input mà nó "nhìn thấy" — vùng có thể ảnh hưởng đến giá trị của neuron đó qua chuỗi Conv.
Một Conv \( 3 \times 3 \) đơn: receptive field = \( 3 \times 3 \). Stack 2 Conv \( 3 \times 3 \) (stride 1): mỗi neuron ở layer 2 nhìn vào \( 3 \times 3 \) neuron layer 1, mỗi neuron layer 1 lại nhìn \( 3 \times 3 \) input → receptive field = \( 5 \times 5 \). Stack 3 layer: \( 7 \times 7 \).
Công thức đệ quy với stride 1, dilation 1:
\[ r_l = r_{l - 1} + (k_l - 1) \]
Với stride > 1, mỗi layer nhân receptive field theo stride tích lũy:
\[ r_l = r_{l - 1} + (k_l - 1) \cdot \prod_{i < l} s_i \]
Ví dụ trong VGG-16 lúc trước MaxPool đầu: 2 Conv \( 3 \times 3 \) stride 1 → receptive field = 5. Sau MaxPool stride 2, 2 Conv \( 3 \times 3 \) tiếp: receptive field = \( 5 + 2 \cdot 2 + 2 \cdot 2 = 13 \). Càng sâu, receptive field càng phủ phần lớn ảnh.
Lý do quan trọng: muốn detect object lớn, cần receptive field đủ rộng để bao phủ nó. Đây là động lực cho mạng sâu, pooling, stride > 1, hoặc dilated convolution.
Một mẹo VGG (Simonyan & Zisserman, 2014): thay 1 Conv \( 7 \times 7 \) bằng 3 Conv \( 3 \times 3 \) stack. Cùng receptive field \( 7 \times 7 \), nhưng:
- Ít param hơn: \( 3 \cdot (C^2 \cdot 9) = 27 C^2 \) so với \( C^2 \cdot 49 = 49 C^2 \).
- Thêm 2 lớp phi tuyến (ReLU giữa các Conv) — tăng expressiveness.
Kernel filter học gì — quan sát qua các tầng
Zeiler & Fergus (2014) — "Visualizing and Understanding Convolutional Networks" — trực quan hóa filter của AlexNet và rút ra phân cấp tự nhiên:
- Layer đầu (1–2): detect low-level — edge ngang, edge dọc, gradient màu, blob nhỏ. Trông gần giống các filter cổ điển (Sobel, Gabor).
- Layer giữa (3–4): detect mid-level — texture (lông, vảy, vân gỗ), pattern đơn giản (góc, đường tròn), bộ phận nhỏ.
- Layer sâu (5+): detect high-level — mắt, mũi, miệng (mạng train trên mặt người), bánh xe, cánh chim, toàn bộ object.
Quan trọng: mạng không được lập trình để học như vậy. Phân cấp này nổi lên tự nhiên từ kiến trúc Conv stack + dataset đủ lớn. Đây là một trong những lý do CNN được tin là học representation có ý nghĩa (chứ không chỉ memorize).
Hệ quả thực hành: feature map của layer sâu trong mạng pretrained (ImageNet) đủ tổng quát để dùng cho task khác — đây là nền tảng của transfer learning (xem Bài 32).
Conv1d và Conv3d
Cùng ý tưởng "kernel trượt + dot product" mở rộng cho dữ liệu khác chiều:
- Conv1d: kernel trượt qua trục thời gian / sequence. Input shape \( (B, C, L) \). Dùng cho:
- Audio waveform thô (vd WaveNet — dilated Conv1d).
- Text — char-level CNN (Kim, 2014 — TextCNN).
- Time series ngắn — tín hiệu sensor, ECG.
- Conv3d: kernel \( k \times k \times k \) trượt qua không gian 3D. Input shape \( (B, C, D, H, W) \). Dùng cho:
- Volumetric medical imaging (MRI, CT) — 3D U-Net (Çiçek et al., 2016).
- Video — kernel mở rộng theo trục thời gian (vd C3D — Tran et al., 2015, I3D — Carreira & Zisserman, 2017).
nn.Conv1d(in_channels=64, out_channels=128, kernel_size=5, padding=2)
# input (B, 64, L) → output (B, 128, L)
nn.Conv3d(in_channels=3, out_channels=64, kernel_size=3, padding=1)
# input (B, 3, D, H, W) → output (B, 64, D, H, W)
Lưu ý: Conv3d rất tốn bộ nhớ — kernel có \( k^3 \) phần tử, FLOPs nhân thêm chiều \( D \). Thực tế nhiều mô hình video dùng tách thành Conv2d (không gian) + Conv1d (thời gian) để rẻ hơn (R(2+1)D, Slow-Fast).
Common kernel size — 3x3, 5x5, 7x7, 1x1
| Kernel size | Use case điển hình | Ghi chú |
|---|---|---|
| \( 3 \times 3 \) | VGG, ResNet, hầu hết CNN modern | Lựa chọn default. Tỉ lệ param / expressiveness tốt nhất. Stack nhiều layer \( 3 \times 3 \) thay cho kernel lớn. |
| \( 5 \times 5 \) | LeNet (1998), AlexNet (2012) một số layer đầu | Hiếm dùng trong mạng modern — gần như luôn thay bằng 2 layer \( 3 \times 3 \). |
| \( 7 \times 7 \) | ResNet conv đầu tiên, ViT patch embed (\( 16 \times 16 \) trên ảnh \( 224 \)) | Receptive field lớn ngay từ đầu, thường stride 2 để downsample. Sau lớp đầu, mạng chuyển sang \( 3 \times 3 \). |
| \( 1 \times 1 \) | Bottleneck (ResNet), pointwise (MobileNet), channel reduction | Không thay đổi không gian, chỉ trộn channel. Rất rẻ. |
| \( 11 \times 11 \), \( 9 \times 9 \) | AlexNet (2012) lớp đầu | Lịch sử — không dùng nữa. |
Quy tắc thực hành: nếu không có lý do đặc biệt, dùng \( 3 \times 3 \) ở mọi nơi (sau lớp đầu); xen \( 1 \times 1 \) khi cần đổi số channel hoặc làm bottleneck.
Code Python — convolution bằng NumPy và Sobel filter
Implement convolution 2D với 1 filter trên ảnh xám, không dùng framework:
import numpy as np
def conv2d_numpy(x: np.ndarray, k: np.ndarray) -> np.ndarray:
"""Cross-correlation 2D, stride=1, no padding."""
H, W = x.shape
kh, kw = k.shape
out = np.zeros((H - kh + 1, W - kw + 1))
for i in range(out.shape[0]):
for j in range(out.shape[1]):
out[i, j] = np.sum(x[i:i + kh, j:j + kw] * k)
return out
x = np.array([[1, 2, 3, 0],
[0, 1, 2, 3],
[3, 0, 1, 2],
[2, 3, 0, 1]], dtype=np.float32)
k = np.array([[1, 0, -1],
[1, 0, -1],
[1, 0, -1]], dtype=np.float32)
print(conv2d_numpy(x, k))
# [[-2. -2.]
# [ 3. -3.]]
Áp dụng Sobel filter (detect edge dọc) lên ảnh thật:
from PIL import Image
import numpy as np
img = Image.open("photo.jpg").convert("L") # ảnh xám
x = np.asarray(img, dtype=np.float32) / 255.0
sobel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype=np.float32)
edges = conv2d_numpy(x, sobel_x)
edges = (edges - edges.min()) / (edges.max() - edges.min())
Image.fromarray((edges * 255).astype(np.uint8)).save("edges.jpg")
Kết quả là ảnh edge map: vùng có chênh lệch sáng theo trục \( x \) sáng lên, vùng đồng màu tối. Đây là một kernel "thủ công"; CNN học các kernel tương tự (và nhiều cái phức tạp hơn) tự động qua backprop.
Code Python — nn.Conv2d và verify output shape
Chạy nn.Conv2d với một vài cấu hình và verify shape khớp công thức:
import torch
import torch.nn as nn
x = torch.randn(8, 3, 32, 32) # batch 8, 3 channel, 32x32
configs = [
dict(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1),
dict(in_channels=3, out_channels=16, kernel_size=3, stride=2, padding=1),
dict(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0),
dict(in_channels=3, out_channels=16, kernel_size=7, stride=2, padding=3),
dict(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=2, dilation=2),
]
for cfg in configs:
conv = nn.Conv2d(**cfg)
y = conv(x)
n_params = sum(p.numel() for p in conv.parameters())
print(f"{cfg} -> out {tuple(y.shape)}, params {n_params}")
Output (rút gọn):
k=3 s=1 p=1 -> out (8, 16, 32, 32), params 448
k=3 s=2 p=1 -> out (8, 16, 16, 16), params 448
k=5 s=1 p=0 -> out (8, 16, 28, 28), params 1216
k=7 s=2 p=3 -> out (8, 16, 16, 16), params 2368
k=3 s=1 p=2 d=2 -> out (8, 16, 32, 32), params 448
Quan sát:
- Param số chỉ phụ thuộc \( C_{\text{in}}, C_{\text{out}}, k \) — không phụ thuộc \( H, W \).
- Stride 2 chia đôi output size theo cả hai trục.
- Padding bù trừ kernel size để giữ same.
- Dilation 2 với padding 2 vẫn cho same — vì effective kernel size là \( 2 \cdot (3 - 1) + 1 = 5 \), nên padding 2 = \( (5 - 1)/2 \).
Verify công thức cho cấu hình cuối: \( H_{\text{out}} = \lfloor (32 + 4 - 2 \cdot 2 - 1)/1 \rfloor + 1 = 32 \). Khớp.
Bài tập
- Cho input shape \( (B, 3, 32, 32) \) và
nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1). Tính output shape và số parameter bằng tay, sau đó verify bằng PyTorch. - Tính số parameter của
nn.Conv2d(3, 64, kernel_size=3)vànn.Conv2d(64, 128, kernel_size=3). So sánh với một Linear layer biến input \( 3 \cdot 224 \cdot 224 \) thành 64 feature. - Implement
conv2d_numpy(x, k, stride, padding)bằng NumPy, hỗ trợ cả stride và padding. So sánh output vớinn.functional.conv2d(chú ý chuyển kernel sang shape \( (1, 1, k, k) \) và lưu ý PyTorch không flip kernel). - Apply ba kernel cụ thể lên một ảnh thật:
- Identity: ma trận 0 với 1 ở giữa.
- Box blur \( 3 \times 3 \): mọi phần tử = 1/9.
- Sharpen: \( [[0,-1,0],[-1,5,-1],[0,-1,0]] \).
- Build CNN có hai block
Conv2d → BatchNorm2d → ReLU → MaxPool(2)nhận input \( (B, 3, 32, 32) \) và output \( (B, 64, 8, 8) \). Tính ra số param của từng layer. - Cho receptive field công thức ở mục 15, tính receptive field của một mạng gồm: Conv \( 3 \times 3 \) stride 1, Conv \( 3 \times 3 \) stride 1, MaxPool stride 2, Conv \( 3 \times 3 \) stride 1, Conv \( 3 \times 3 \) stride 1, MaxPool stride 2.
- So sánh số param và FLOPs giữa Conv \( 3 \times 3 \) thường (in=128, out=128) và depthwise separable tương đương. Verify bằng
sum(p.numel() ...). - Implement dilated Conv \( 3 \times 3 \) với
dilation=2và effective kernel \( 5 \times 5 \). So sánh receptive field và số param với Conv \( 5 \times 5 \) thường — kết luận gì?
Đáp án ngắn
- Output \( (B, 16, 32, 32) \). Param: \( 16 \cdot 3 \cdot 3 \cdot 3 + 16 = 448 \).
- Conv2d(3, 64, 3): \( 64 \cdot 3 \cdot 9 + 64 = 1\,792 \). Conv2d(64, 128, 3): \( 128 \cdot 64 \cdot 9 + 128 = 73\,856 \). Linear \( 150\,528 \to 64 \): \( \approx 9.6 \times 10^6 \) param — nhiều hơn cả mạng CNN sâu.
- Kết quả phải khớp với
F.conv2dđến lỗi float (\( 10^{-6} \)). Nếu lệch, kiểm tra index padding. - Identity giữ ảnh nguyên. Blur làm mờ — tổng kernel = 1 nên giữ độ sáng. Sharpen tăng tương phản viền — tổng kernel = 1 nhưng có giá trị âm.
- Block 1: Conv2d(3, 32, 3, padding=1) → BN(32) → ReLU → MaxPool(2). Output \( (B, 32, 16, 16) \), param Conv = \( 32 \cdot 3 \cdot 9 + 32 = 896 \). Block 2: Conv2d(32, 64, 3, padding=1) → BN(64) → ReLU → MaxPool(2). Output \( (B, 64, 8, 8) \), param Conv = \( 64 \cdot 32 \cdot 9 + 64 = 18\,496 \).
- Conv1: r=3. Conv2: r=3+2=5. MaxPool2: r=5+1=6, stride tích lũy=2. Conv3: r=6+2·2=10. Conv4: r=10+2·2=14. MaxPool2: r=14+1·2=16, stride tích lũy=4. Receptive field cuối: \( 16 \times 16 \).
- Conv \( 3 \times 3 \) thường: \( 128 \cdot 128 \cdot 9 + 128 = 147\,584 \). Depthwise separable: \( (128 \cdot 9 + 128) + (128 \cdot 128 + 128) = 1\,280 + 16\,512 = 17\,792 \) — giảm ~8.3 lần.
- Dilated \( 3 \times 3 \) (d=2): effective \( 5 \times 5 \), nhưng chỉ 9 trọng số (sparse). Param = \( C^2 \cdot 9 + C \). Conv \( 5 \times 5 \) thường: \( C^2 \cdot 25 + C \). Cùng receptive field, dilated rẻ hơn ~2.8 lần param, nhưng "nhìn" thưa — nhiều task phù hợp, một số task cần kernel dày sẽ kém.
Bài kế tiếp: Bài 29 — Pooling Layer — Max Pooling và Average Pooling, downsampling không học, vai trò trong việc tăng receptive field và giảm compute.
- Krizhevsky, Sutskever & Hinton (2012) - ImageNet Classification with Deep Convolutional Neural Networks (AlexNet)
- Simonyan & Zisserman (2014) - Very Deep Convolutional Networks for Large-Scale Image Recognition (VGG)
- He et al. (2016) - Deep Residual Learning for Image Recognition (ResNet)
- Zeiler & Fergus (2014) - Visualizing and Understanding Convolutional Networks
- Lin, Chen & Yan (2013) - Network In Network
- Howard et al. (2017) - MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
- Chollet (2017) - Xception: Deep Learning with Depthwise Separable Convolutions
- Chen et al. (2018) - DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs
- van den Oord et al. (2016) - WaveNet: A Generative Model for Raw Audio
- Kim (2014) - Convolutional Neural Networks for Sentence Classification
- PyTorch Docs - nn.Conv2d
- PyTorch Docs - nn.Conv1d
- PyTorch Docs - nn.Conv3d
- CS231n - Convolutional Neural Networks Notes
- Distill - Computing Receptive Fields of Convolutional Neural Networks
