Convolution Operation
Phép tích chập
Bạn có ma trận 6×6 (ảnh) và một 'cửa sổ' 3×3 (bộ lọc). Tại mỗi vị trí, bạn nhân từng cặp số rồi cộng. Khi trượt cửa sổ qua toàn bộ ma trận (stride 1, không padding), output có kích thước bao nhiêu?
Hình minh họa
Sandbox — input 6×6, kernel 3×3, feature map 4×4
Chọn một preset kernel. Ô vàng bên trái là vùng kernel đang ôm; ô đang tính ở feature map bên phải được viền sáng. Nhấn Bước tiếp để trượt, hoặc bật Auto để xem kernel đi hết ảnh.
Sobel X: Phát hiện cạnh dọc — đạo hàm theo trục x
Hãy đặc biệt chú ý khi đổi qua Sobel X và Sobel Y: feature map sáng ở các vị trí khác nhau, cho thấy mỗi kernel thực sự phát hiện một loại cạnh riêng. Identity cho output gần bằng vùng trung tâm của input — đúng như tên gọi.
Gallery — cùng input, 6 kernel, 6 feature map khác nhau
Mỗi mini-card là feature map 4×4 của một kernel. Màu nóng = giá trị cao, lạnh = thấp. Dùng để so sánh nhanh "con mắt" của từng kernel.
Quan sát:Sobel X và Sobel Y cho feature map gần như đối xứng nhau — một bên nhạy với cạnh dọc, một bên với cạnh ngang. Gaussian cho ra toàn giá trị trung bình (feature map khá "phẳng"). Sharpen khuếch đại tương phản — giá trị min/max trải rất rộng. Laplacian thường có min âm mạnh ở các đỉnh. Identity gần như trùng với input gốc.
Phép toán nhân từng cặp rồi cộng mà bạn vừa thấy trên sandbox chính là phép tích chập. Mỗi kernel là một "câu hỏi" mà ảnh phải trả lời: "ở vị trí này có cạnh dọc không?", "có đỉnh sáng không?", "có biến thiên lớn không?". Output feature map là bản đồ câu trả lời ở mọi vị trí.
Điều kỳ diệu: CNN không cần được lập trình thủ công các kernel này. Nó tự học ra các kernel tối ưu cho nhiệm vụ — nhận diện mèo, đọc biển số, chẩn đoán X-quang — chỉ từ dữ liệu và backprop.
Thợ kiểm tra vải ở chợ Bến Thành
Hãy tưởng tượng bạn là thợ kiểm vải. Bạn có một khuôn mẫu nhựa 3×3 với các đánh dấu "cao", "trung bình", "thấp". Đặt khuôn lên một điểm trên tấm vải 6×6, so sánh từng ô: ô nào khớp đánh dấu "cao" thì cộng điểm dương, ô nào khớp "thấp" thì cộng điểm âm. Tổng điểm tại vị trí đó cho biếtmức độ giống khuôn.
Bạn trượt khuôn sang trái, sang phải, xuống dưới, được một bản đồ 4×4 "mức độ giống". Đó chính là feature map.
Sau đó bạn dùng khuôn khác — khuôn phát hiện vết ố, khuôn tìm chỉ thưa, khuôn dò đường sọc. Mỗi khuôn cho một feature map riêng. Xếp chồng 32 feature map lên nhau, bạn có "siêu-ảnh" mô tả vải ở 32 khía cạnh khác nhau. Đây chính là output của một lớp conv trong CNN.
Ảnh 224×224, kernel 5×5, stride=2, padding=2. Output feature map có kích thước bao nhiêu?
Một lớp conv có input 32 kênh (RGB qua vài layer), output 64 kênh, kernel 3×3. Tổng số tham số (không tính bias) là bao nhiêu?
Giải thích
Phép tích chập 2D là phép toán cốt lõi của CNN. Cho ảnh đầu vào kích thước và kernel kích thước , output tại vị trí là:
Kích thước output tính bằng công thức tổng quát (với padding và stride ):
Nếu bạn thấy phép chia không chia hết, cấu hình không hợp lệ — cần chỉnh padding hoặc stride. Các framework thường tự làm tròn xuống và chỉ cảnh báo.
Chia sẻ trọng số (weight sharing): kernel 3×3 có 9 tham số, dùng ở mọi vị trí trên ảnh 224×224 = 50.176 vị trí. Fully-connected giữa hai feature map 224×224 cần tỷ tham số. Conv chỉ cần 9 — giảm 277 triệu lần, và vẫn học được các đặc trưng cục bộ tốt hơn.
Ảnh RGB có 3 kênh. Mỗi kernel phải có cùng số kênh để ôm hết input: kernel 3×3×3 = 27 tham số. Mỗi kênh kernel nhân phần tử với kênh input tương ứng, rồi cộng tất cảlại thành 1 giá trị scalar. 64 kernel output → 64 feature map, mỗi cái là một "quan sát" riêng về ảnh đầu vào.
Với : — hợp lệ. Nhưng : — framework làm tròn xuống còn 3. Hệ quả: 1 hàng/cột ở rìa bị bỏ! Luôn kiểm tra hoặc đặt padding để chia hết.
Trong CNN hiện đại, kernel 3×3 gần như là mặc định (VGG, ResNet, EfficientNet). Lý do: hai lớp 3×3 chồng lên nhau cho receptive field tương đương 5×5 nhưng với 2·9 = 18 tham số thay vì 25 — rẻ hơn và còn có thêm 1 lớp phi tuyến ở giữa. Kernel 1×1 dùng để trộn kênh mà không trộn không gian; kernel 7×7 chỉ còn ở lớp đầu (stem) để nuốt nhanh ảnh thô.
"""Phép tích chập 2D viết tay — phiên bản dễ đọc nhất."""
import numpy as np
def conv2d(image: np.ndarray, kernel: np.ndarray,
stride: int = 1, padding: int = 0) -> np.ndarray:
"""
Giả thiết: image và kernel đều 2D (một kênh).
Trả về feature map 2D.
"""
if padding > 0:
image = np.pad(image, padding, mode="constant", constant_values=0)
H, W = image.shape
kH, kW = kernel.shape
oH = (H - kH) // stride + 1
oW = (W - kW) // stride + 1
output = np.zeros((oH, oW), dtype=np.float32)
for i in range(oH):
for j in range(oW):
region = image[i * stride : i * stride + kH,
j * stride : j * stride + kW]
output[i, j] = np.sum(region * kernel) # element-wise × rồi sum
return output
# Sobel X — phát hiện cạnh dọc
sobel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype=np.float32)
# Gaussian blur 3×3 — chia 16 để giữ cường độ
gaussian = np.array([[1, 2, 1],
[2, 4, 2],
[1, 2, 1]], dtype=np.float32) / 16.0
# Laplacian — phát hiện đỉnh và rìa, tổng trọng số = 0
laplacian = np.array([[0, 1, 0],
[1, -4, 1],
[0, 1, 0]], dtype=np.float32)Trong thực tế, không ai viết vòng lặp Python cho conv — quá chậm. Các framework dùng thủ thuật im2col để biến tích chập thành một phép nhân ma trận lớn, sau đó gọi BLAS / cuDNN. Một GPU hiện đại chạy phép nhân ma trận nhanh hơn vòng lặp Python hàng chục nghìn lần.
"""Conv2D trong PyTorch — cách dùng thực tế."""
import torch
import torch.nn as nn
# Khai báo một lớp conv: 3 kênh input (RGB), 64 kênh output,
# kernel 3×3, stride 1, padding 1 (giữ nguyên kích thước không gian).
conv = nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
stride=1,
padding=1,
)
# Input batch: 8 ảnh, 3 kênh, 224×224
x = torch.randn(8, 3, 224, 224)
y = conv(x)
print(y.shape) # torch.Size([8, 64, 224, 224])
# Đếm tham số: 3·3·3·64 + 64 (bias) = 1.792
n_params = sum(p.numel() for p in conv.parameters())
print(f"Tham số: {n_params}") # 1792
# Gradient flow tự động — backprop qua phép tích chập
loss = y.mean()
loss.backward()
print(conv.weight.grad.shape) # [64, 3, 3, 3]Ứng dụng ngoài thị giác:
- 1D convolutioncho chuỗi thời gian, âm thanh, văn bản — kernel trượt theo chiều thời gian, phát hiện pattern cục bộ ("ba âm tiết lặp", "tăng rồi giảm").
- 3D convolution cho video (không gian + thời gian) và MRI (ba chiều không gian).
- Graph convolution khái quát hoá lên đồ thị bất kỳ — tổng hợp thông tin từ lân cận trong mạng xã hội, phân tử hoá học, mạng giao thông.
- Dilated / atrous convolution mở rộng receptive field mà không tăng tham số — chủ lực trong segmentation (DeepLab) và audio generation (WaveNet).
"""So sánh các kernel preset trên cùng một ảnh xám 6×6."""
import numpy as np
def conv2d(img, k):
H, W = img.shape; kH, kW = k.shape
oH, oW = H - kH + 1, W - kW + 1
out = np.zeros((oH, oW))
for i in range(oH):
for j in range(oW):
out[i, j] = np.sum(img[i:i+kH, j:j+kW] * k)
return out
img = np.array([
[10, 10, 10, 90, 90, 90],
[10, 20, 30, 90, 80, 70],
[10, 30, 50, 80, 60, 40],
[20, 40, 60, 70, 50, 30],
[40, 50, 70, 60, 40, 20],
[60, 70, 80, 40, 30, 10],
], dtype=np.float32)
kernels = {
"Sobel X": np.array([[-1,0,1],[-2,0,2],[-1,0,1]], dtype=np.float32),
"Sobel Y": np.array([[-1,-2,-1],[0,0,0],[1,2,1]], dtype=np.float32),
"Gaussian": np.array([[1,2,1],[2,4,2],[1,2,1]], dtype=np.float32) / 16,
"Sharpen": np.array([[0,-1,0],[-1,5,-1],[0,-1,0]], dtype=np.float32),
"Laplacian": np.array([[0,1,0],[1,-4,1],[0,1,0]], dtype=np.float32),
"Identity": np.array([[0,0,0],[0,1,0],[0,0,0]], dtype=np.float32),
}
for name, k in kernels.items():
fm = conv2d(img, k)
print(f"{name:<10} min={fm.min():6.1f} max={fm.max():6.1f} mean={fm.mean():6.1f}")
# Quan sát: Sobel X có min âm mạnh ở cột trái, max dương mạnh ở cột phải
# — phản ánh gradient chuyển từ tối sang sáng theo chiều ngang.Pitfalls thường gặp khi bắt đầu với CNN:
- Quên padding: feature map co dần qua mỗi layer → mất 10–20% pixel ở rìa sau vài layer. Luôn đặt padding để giữ kích thước, hoặc tính toán trước output size.
- Nhầm thứ tự chiều: PyTorch dùng
[B, C, H, W], TensorFlow mặc định[B, H, W, C]. Chuyển đổi nhầm → lỗi shape khó debug. - Không chuẩn hoá input: pixel 0–255 qua conv sẽ cho activation khổng lồ, gradient nổ. Luôn normalize về mean=0, std=1 (hoặc chia 255).
- Dùng kernel quá lớn: kernel 11×11 ở lớp đầu (AlexNet, 2012) đã lỗi thời. Hiện đại dùng 3×3 stacked — rẻ hơn, sâu hơn, hiệu quả hơn.
- Quên bias tương tác với BatchNorm: nếu layer sau là BN, bias của conv vô nghĩa (BN tự tính). Đặt
bias=Falseđể tiết kiệm tham số. - Áp dụng cùng kernel cho ảnh màu:Sobel cho ảnh xám cần 1 kênh kernel. Với RGB phải chạy kernel 3D (3 kênh) hoặc chuyển ảnh sang grayscale trước. Sai số im lặng — model không báo lỗi mà kết quả chỉ "kém".
- Bỏ qua kiểm tra output shape sau mỗi layer: khi thiết kế mạng, in shape sau mỗi lớp để chắc chắn downsample đúng như kế hoạch. Bug shape ở conv-5 rất khó truy ngược nếu chỉ thấy loss kỳ lạ.
Một neuron ở lớp conv-1 chỉ thấy 3×3 pixel gốc. Neuron ở lớp conv-2 (chồng lên conv-1) thấy 5×5 pixel gốc. Qua càng nhiều layer, receptive field càng lớn. Công thức truy ngược:
Với ResNet-50, receptive field ở lớp cuối khoảng 483×483 — đủ để "nhìn" cả một ảnh 224×224 nhiều lần. Đây là lý do feature ở tầng cuối mô tả được context toàn cục (loài chó, khung cảnh) thay vì chỉ cạnh/kết cấu cục bộ.
Từ 2020, Vision Transformer (ViT) và các biến thể (Swin, DeiT) cho thấy self-attention có thể cạnh tranh với conv trên tác vụ thị giác — đặc biệt khi có nhiều dữ liệu. Bài học: inductive bias "cục bộ" của conv là lợi thế khi dữ liệu ít, nhưng giới hạn khi dữ liệu rất nhiều. Các kiến trúc hiện đại (ConvNeXt, CoAtNet) kết hợp cả hai — lấy mạnh điểm của từng bên.
Mở rộng đọc thêm: sau khi hiểu convolution, bước kế tiếp là học các khối xây dựng của CNN hiện đại — pooling để downsample, kiến trúc CNN (LeNet → VGG → ResNet → EfficientNet), và so sánh với self-attention của Transformer. Hiểu sâu một phép toán cơ bản luôn đáng giá hơn lướt qua mười kỹ thuật hào nhoáng.
- Tích chập = trượt kernel qua input, nhân từng cặp (element-wise) rồi cộng — phát hiện đặc trưng cục bộ.
- Mỗi kernel phát hiện một loại đặc trưng: Sobel → cạnh; Gaussian → làm mờ; Laplacian → đỉnh/rìa; Identity → giữ nguyên.
- Công thức output: O = ⌊(W − K + 2P) / S⌋ + 1. Stride lớn → output nhỏ; padding giữ kích thước.
- Chia sẻ trọng số: kernel 3×3 có 9 tham số nhưng áp dụng mọi vị trí — tiết kiệm hàng triệu lần so với fully-connected.
- Multi-channel: kernel phải cùng số kênh với input; ảnh RGB + kernel 3×3 → kernel thực sự là 3×3×3 = 27 tham số.
- Framework DL dùng cross-correlation (không lật kernel) nhưng vẫn gọi là convolution theo thói quen ngành.
Kiểm tra hiểu biết
Kernel 3×3 trượt qua ảnh 6×6 (stride 1, không padding). Output có kích thước gì?