Bài 1

CNN — Mạng tích chập

Convolutional Neural Network (CNN) là kiến trúc nền tảng của thị giác máy tính hiện đại. Thay vì nối mọi pixel với mọi nơ-ron như MLP, CNN khai thác cấu trúc không gian của ảnh bằng hai ý tưởng cốt lõi: kết nối cục bộchia sẻ trọng số. Bài này đi từ trực giác "vì sao cần CNN", qua phép tích chập, pooling, trường tiếp nhận, cho tới một kiến trúc hoàn chỉnh kèm code Keras.

Vì sao không dùng MLP cho ảnh
Bài toán bùng nổ tham số và mất cấu trúc không gian

MLP (mạng nơ-ron đầy đủ kết nối — fully-connected) yêu cầu trải phẳng (flatten) ảnh thành một vector rồi nối mọi pixel đầu vào với mọi nơ-ron của lớp ẩn. Với ảnh nhỏ điều này còn chấp nhận được, nhưng với ảnh thật thì số tham số bùng nổ rất nhanh.

Ví dụ ảnh màu kích thước 224 × 224 × 3 khi flatten cho ra:

$$224 \times 224 \times 3 = 150528 \text{ giá trị đầu vào}$$

Chỉ riêng một lớp ẩn 1000 nơ-ron đã cần \(150528 \times 1000 \approx 1.5 \times 10^8\) trọng số — hơn 150 triệu tham số cho một lớp. Mạng kiểu này gần như không thể huấn luyện ổn định và rất dễ overfit.

Ngoài chi phí tham số, MLP còn có hai nhược điểm nghiêm trọng với dữ liệu ảnh:

CNN giải quyết cả ba vấn đề trên bằng kết nối cục bộ (mỗi nơ-ron chỉ nhìn một vùng nhỏ) và chia sẻ trọng số (cùng một bộ lọc dùng khắp ảnh).

Phép tích chập (Convolution)
Kernel trượt trên ảnh tạo feature map

Phép tích chập dùng một kernel (còn gọi là filter) — một ma trận trọng số nhỏ, ví dụ \(3 \times 3\) — trượt từng bước trên ảnh. Tại mỗi vị trí, ta đặt kernel lên một vùng (patch) cùng kích thước của ảnh, nhân từng phần tử tương ứng rồi cộng tất cả lại thành một con số. Tập hợp các con số này theo không gian tạo thành feature map (bản đồ đặc trưng).

Cụ thể, với ảnh đầu vào \(I\) và kernel \(K\) kích thước \(k \times k\), giá trị tại vị trí \((i, j)\) của feature map là tổng nhân-cộng cục bộ:

$$S(i, j) = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} I(i+m,\; j+n)\, \cdot\, K(m, n)$$

Đây chính là phép phát hiện mẫu: nếu vùng ảnh "khớp" với mẫu mà kernel mã hóa (chẳng hạn một cạnh dọc), tổng tích chập sẽ lớn; nếu không khớp, tổng sẽ nhỏ. Mỗi kernel vì thế đóng vai trò một bộ dò đặc trưng.

Kích thước feature map đầu ra. Gọi \(W\) là kích thước cạnh đầu vào, \(K\) là kích thước kernel, \(P\) là padding (số pixel đệm thêm quanh ảnh), \(S\) là stride (bước trượt). Kích thước cạnh đầu ra là:

$$O = \left\lfloor \frac{W - K + 2P}{S} \right\rfloor + 1$$

Ví dụ số cụ thể: với \(W = 32\), \(K = 3\), \(P = 1\), \(S = 1\):

$$O = \left\lfloor \frac{32 - 3 + 2 \cdot 1}{1} \right\rfloor + 1 = \left\lfloor \frac{31}{1} \right\rfloor + 1 = 32$$

Đầu ra vẫn là \(32 \times 32\) — kích thước được giữ nguyên. Đây là cấu hình rất phổ biến: kernel \(3 \times 3\) kèm padding 1 và stride 1 giúp giữ nguyên kích thước không gian sau mỗi lớp conv.

Tham số chia sẻ & kết nối cục bộ
Vì sao CNN tiết kiệm tham số khủng khiếp

Kết nối cục bộ (local connectivity): mỗi giá trị trong feature map chỉ phụ thuộc vào một vùng nhỏ \(k \times k\) của đầu vào, không phải toàn bộ ảnh. Điều này hợp lý vì các đặc trưng thị giác cơ bản (cạnh, góc, đốm màu) đều mang tính cục bộ.

Chia sẻ trọng số (weight sharing): cùng một kernel được dùng để quét khắp ảnh. Một kernel \(3 \times 3\) chỉ có 9 trọng số (cộng 1 bias), và 9 trọng số đó tạo ra toàn bộ feature map dù ảnh lớn cỡ nào. So với MLP cần hàng trăm triệu trọng số ở ví dụ trước, đây là mức tiết kiệm hàng vạn lần.

Hệ quả quan trọng là bất biến tịnh tiến (translation invariance / equivariance): vì cùng kernel quét mọi nơi, một đặc trưng (ví dụ cạnh ngang) được phát hiện bất kể nó nằm ở góc trái trên hay giữa ảnh. CNN không phải học lại cùng một mẫu cho từng vị trí.

Trực giác: kernel giống một "kính lúp dò mẫu". Bạn chỉ cần học một bộ kính lúp tốt, rồi rê nó khắp ảnh — thay vì sắm một bộ kính riêng cho mỗi điểm ảnh như MLP.
Kênh & nhiều filter
Từ RGB tới hàng chục feature map

Ảnh thật có nhiều kênh (channel) — ảnh màu RGB có 3 kênh. Khi đó mỗi kernel cũng có chiều sâu bằng số kênh đầu vào: một kernel \(3 \times 3\) trên ảnh RGB thực ra là khối \(3 \times 3 \times 3\). Kernel nhân-cộng trên cả 3 kênh rồi cộng dồn lại thành một feature map duy nhất.

Một lớp conv thường có nhiều filter, mỗi filter học một loại đặc trưng khác nhau và sinh ra một feature map riêng. Nếu lớp có \(C_{out}\) filter, đầu ra sẽ có \(C_{out}\) feature map xếp chồng — trở thành đầu vào nhiều kênh cho lớp tiếp theo.

Số tham số của một lớp conv với kernel \(K \times K\), \(C_{in}\) kênh vào, \(C_{out}\) filter:

$$\text{params} = (K \times K \times C_{in} \times C_{out}) + C_{out}$$

trong đó số hạng \(+\, C_{out}\) là các bias (mỗi filter một bias). Ví dụ lớp conv \(3 \times 3\), từ 3 kênh vào sang 64 filter:

$$(3 \times 3 \times 3 \times 64) + 64 = 1728 + 64 = 1792 \text{ tham số}$$

Chỉ 1.792 tham số cho một lớp xử lý ảnh — nhỏ hơn rất nhiều so với hàng trăm triệu của MLP, mà lại áp dụng được cho ảnh ở mọi kích thước.

Hàm kích hoạt & Pooling
Phi tuyến và giảm kích thước không gian

Bản thân tích chập là phép tuyến tính. Để mạng học được quan hệ phi tuyến phức tạp, sau mỗi lớp conv ta áp một hàm kích hoạt, phổ biến nhất là ReLU:

$$\text{ReLU}(x) = \max(0,\; x)$$

ReLU đơn giản, tính nhanh, giảm hiện tượng vanishing gradient so với sigmoid/tanh, và tạo độ thưa (nhiều giá trị 0) giúp mạng dễ tối ưu.

Pooling dùng để giảm kích thước không gian (downsampling) của feature map: trượt một cửa sổ (thường \(2 \times 2\), stride 2) và tổng hợp mỗi vùng thành một giá trị.

Pooling mang lại ba lợi ích: (1) giảm số chiều nên giảm tính toán và bộ nhớ; (2) tăng bất biến với dịch chuyển nhỏ (vật thể nhích vài pixel vẫn cho cùng kết quả pool); (3) mở rộng gián tiếp trường tiếp nhận. Kích thước đầu ra của pooling tính theo công thức tương tự conv, với \(K\) là kích thước cửa sổ pool:

$$O = \left\lfloor \frac{W - K}{S} \right\rfloor + 1$$

Ví dụ feature map \(32 \times 32\) qua max pooling \(2 \times 2\) stride 2: \(O = \lfloor (32 - 2)/2 \rfloor + 1 = 16\), tức còn \(16 \times 16\).

Trường tiếp nhận (Receptive Field)
Một nơ-ron sâu "nhìn thấy" bao nhiêu ảnh gốc

Trường tiếp nhận của một nơ-ron là vùng ảnh đầu vào có ảnh hưởng tới giá trị của nơ-ron đó. Ở lớp conv đầu tiên với kernel \(3 \times 3\), mỗi nơ-ron chỉ "thấy" một vùng \(3 \times 3\) của ảnh gốc — rất hẹp, chỉ đủ bắt cạnh và góc.

Càng xuống sâu, trường tiếp nhận càng lớn vì mỗi nơ-ron tổng hợp nhiều nơ-ron của lớp trước, mà mỗi nơ-ron đó lại tổng hợp một vùng riêng. Chồng hai lớp conv \(3 \times 3\) cho trường tiếp nhận hiệu dụng \(5 \times 5\); ba lớp cho \(7 \times 7\); và pooling/stride còn nhân nhanh con số này lên.

$$\text{RF}_{l} = \text{RF}_{l-1} + (K_l - 1)\prod_{i=1}^{l-1} S_i$$

Ý nghĩa thực tế: nơ-ron ở các lớp nông chỉ nắm chi tiết cục bộ, còn nơ-ron ở lớp sâu có trường tiếp nhận đủ rộng để "nhìn" gần như toàn ảnh, nhờ đó nắm được ngữ cảnh và hình dạng toàn cục — điều kiện cần để nhận diện đối tượng hoàn chỉnh.

Kiến trúc CNN điển hình
Trích đặc trưng rồi phân loại

Một CNN cổ điển ghép các khối lặp lại rồi nối phần phân loại ở cuối:

Kiến trúc CNN điển hình Scroll / zoom · Mở draw.io ↗

Cách chia vai trò rất rõ ràng:

Hai kỹ thuật gần như luôn xuất hiện trong CNN hiện đại:

Code minh hoạ
Một CNN nhỏ với Keras

Dưới đây là một CNN gọn cho phân loại ảnh \(32 \times 32 \times 3\) (kiểu CIFAR-10, 10 lớp). Để ý cấu trúc Conv2D → MaxPooling2D lặp lại, rồi Flatten → Dense ở cuối:

from tensorflow.keras import models, layers

model = models.Sequential([
    # Khối 1: 32 filter 3x3, giữ nguyên kích thước (padding same)
    layers.Conv2D(32, (3, 3), padding="same",
                  activation="relu", input_shape=(32, 32, 3)),
    layers.BatchNormalization(),
    layers.MaxPooling2D((2, 2)),          # 32x32 -> 16x16

    # Khối 2: 64 filter
    layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
    layers.BatchNormalization(),
    layers.MaxPooling2D((2, 2)),          # 16x16 -> 8x8

    # Phần phân loại
    layers.Flatten(),
    layers.Dense(128, activation="relu"),
    layers.Dropout(0.5),                       # chống overfit
    layers.Dense(10, activation="softmax"),    # 10 lớp
])

model.compile(optimizer="adam",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

model.summary()

Khi gọi model.summary() bạn sẽ thấy độ phân giải không gian giảm dần (32 → 16 → 8) trong khi số kênh tăng (3 → 32 → 64) — đúng tinh thần "nén không gian, mở rộng ngữ nghĩa" đã nói ở trên.

Tiền xử lý: luôn chuẩn hóa ảnh trước khi đưa vào mạng — thường chia giá trị pixel cho 255 để đưa về khoảng \([0, 1]\). Kèm theo đó, dùng data augmentation (lật ngang, xoay nhẹ, dịch, đổi độ sáng) để nhân tạo thêm dữ liệu, giúp mạng tổng quát hóa tốt hơn và giảm overfit.
Điều cốt lõi cần nhớ: CNN học đặc trưng theo kiểu phân cấp — các tầng nông học những thứ đơn giản (cạnh, màu, kết cấu), các tầng sâu kết hợp chúng thành đặc trưng phức tạp hơn (hình dạng, bộ phận, rồi cả đối tượng). Chính cấu trúc phân cấp này là điều làm CNN mạnh mẽ với ảnh.
← Quay lại
Tổng quan Computer Vision