Kiến trúc mạng học sâu: từ nền tảng đến LLM·Chương 2 — CNN: học từ cấu trúc không gian

Chế độ xem

CNN: học đặc trưng cục bộ từ ảnh

Ở bài trước, ảnh được nhìn như một tensor có các trục chiều cao, chiều rộng và kênh màu. Cách biểu diễn này giữ lại một thông tin rất quan trọng: pixel nào nằm gần pixel nào.

CNN — mạng neural tích chập (Convolutional Neural Network) — được thiết kế để tận dụng cấu trúc đó. Thay vì mỗi neuron nhìn toàn bộ ảnh ngay từ đầu, CNN dùng các bộ lọc nhỏ quan sát từng vùng cục bộ rồi dùng lại cùng bộ lọc đó ở nhiều vị trí.

Nếu chỉ giữ lại một câu:

CNN học các bộ lọc nhỏ để phát hiện đặc trưng cục bộ, dùng lại chúng trên toàn ảnh, rồi ghép các đặc trưng đơn giản thành biểu diễn ngày càng phức tạp hơn.

Bài này dừng ở phép toán và shape. Cách xếp convolution thành backbone ConvNet (ResNet, ConvNeXt, …) là bài sau.

Sau bài này, bạn cần làm được gì?

Ở mức cốt lõi, bạn cần:

  • giải thích được vì sao ảnh không nên được xử lý giống một vector thông thường ngay từ đầu;
  • mô tả được kernel, feature map, channel, stride, padding và pooling;
  • đọc được shape qua một vài tầng convolution;
  • giải thích được vì sao CNN có ít tham số hơn một mạng fully-connected tương đương.

Ở mức hiểu sâu hơn, bạn nên:

  • tính được kích thước đầu ra và số tham số của một convolution layer;
  • phân biệt tính cục bộ, chia sẻ trọng số, receptive field và translation equivariance;
  • hiểu convolution nhiều kênh thực sự được tính như thế nào.

Nhìn toàn bộ CNN trước khi đi vào chi tiết

Một CNN phân loại ảnh cổ điển thường có mạch:

Sơ đồ tổng quát của một CNN: ảnh đầu vào, các tầng convolution/pooling, vector hóa và phân loại

Nguồn ảnh: Irisbox, Wikimedia Commons, giấy phép CC BY 4.0. Trang nguồn: https://commons.wikimedia.org/wiki/File:Convolutional_Neural_Network.png

Không cần học thuộc sơ đồ. Ta mở từng khối: convolution làm gì, shape đổi ra sao, rồi mới ghép thành một model nhỏ.

Vì sao không nên làm phẳng ảnh rồi đưa thẳng vào MLP?

Giả sử có ảnh xám 28×28 pixel. Khi làm phẳng (flatten) ảnh, ta nhận một vector dài:

28×28=78428\times28=784

Về mặt số học, MLP vẫn có thể học từ vector này. Nhưng kiến trúc không còn nói rõ rằng:

  • pixel (10, 10) nằm cạnh pixel (10, 11);
  • một đường biên thường được tạo từ nhiều pixel gần nhau;
  • cùng một cạnh có thể xuất hiện ở góc trái, giữa hoặc góc phải ảnh.

Nếu hidden layer đầu có 512 neuron thì riêng số weight đã là:

784×512=401,408784\times512=401{,}408

Với ảnh RGB 224×224, số giá trị đầu vào là:

224×224×3=150,528224\times224\times3=150{,}528

Nối trực tiếp với 1024 neuron sẽ cần khoảng 150,528×1024≈154150{,}528\times1024\approx154 triệu weight.

CNN giải quyết đồng thời hai vấn đề:

  1. Kết nối cục bộ: mỗi phép tính ban đầu chỉ nhìn một vùng nhỏ của ảnh, không nhìn toàn bộ.
  2. Chia sẻ trọng số: cùng một bộ lọc được dùng lại ở nhiều vị trí, nên model không cần học riêng một bộ weight cho mỗi góc ảnh.

Hai đặc tính này phù hợp tự nhiên với cách ảnh thực tế hoạt động: cạnh và texture xuất hiện cục bộ, và cùng một pattern có thể nằm ở nhiều vị trí khác nhau.

Kernel: cửa sổ nhỏ chứa các trọng số có thể học

Ta bắt đầu với một ảnh đơn kênh và một kernel 3×3:

K=[k11k12k13k21k22k23k31k32k33]K= \begin{bmatrix} k_{11} & k_{12} & k_{13}\\ k_{21} & k_{22} & k_{23}\\ k_{31} & k_{32} & k_{33} \end{bmatrix}

Kernel này chỉ có 9 trọng số. Thao tác cơ bản: đặt kernel lên một vùng 3×3 của ảnh, nhân từng phần tử tương ứng rồi cộng tất cả lại.

Giả sử patch ảnh là:

P=[228229128]P= \begin{bmatrix} 2&2&8\\ 2&2&9\\ 1&2&8 \end{bmatrix}

và dùng một kernel minh họa phát hiện thay đổi theo chiều ngang:

K=[−101−101−101]K= \begin{bmatrix} -1&0&1\\ -1&0&1\\ -1&0&1 \end{bmatrix}

Một giá trị đầu ra là:

z=(−1⋅2)+(0⋅2)+(1⋅8)+(−1⋅2)+(0⋅2)+(1⋅9)+(−1⋅1)+(0⋅2)+(1⋅8)=20\begin{aligned} z={}&(-1\cdot2)+(0\cdot2)+(1\cdot8)\\ &+(-1\cdot2)+(0\cdot2)+(1\cdot9)\\ &+(-1\cdot1)+(0\cdot2)+(1\cdot8)\\ ={}&20 \end{aligned}

Giá trị lớn cho thấy vùng ảnh này phản ứng mạnh với mẫu mà kernel đang tìm — ở đây là thay đổi sáng/tối theo chiều ngang.

Kernel trên chỉ dùng để minh họa phép tính. Trong CNN thật, bạn không cần tự thiết kế kernel: các phần tử của kernel là tham số (parameter) và được học tự động bằng backpropagation, giống hệt weight trong MLP. Ban đầu kernel gần như ngẫu nhiên; sau nhiều lần cập nhật gradient, nó dần trở thành phép biến đổi hữu ích cho nhiệm vụ cụ thể.

Kernel quét qua ảnh: feature map, chia sẻ trọng số, kết nối cục bộ

Ta dùng cùng một kernel tại nhiều vị trí khác nhau.

Mỗi vị trí tạo ra một số. Các số đó xếp thành ma trận đầu ra gọi là bản đồ đặc trưng (feature map).

ảnh đầu vào
   ↓
kernel hỏi: "mẫu tôi đang tìm có xuất hiện ở đây không?"
   ↓
feature map cho biết mức phản ứng tại từng vị trí

Nếu dùng nhiều kernel khác nhau, ta có nhiều feature map khác nhau.

Chia sẻ trọng số

Giả sử ảnh có một kênh và ta dùng một kernel 3×3. Kernel có 3×3=93\times3=9 weight; thêm một bias thì 10 tham số. Dù kernel được áp dụng ở 100 hay 10.000 vị trí, ta vẫn dùng chính 9 weight đó. Đây gọi là chia sẻ trọng số (weight sharing).

Ý nghĩa thực tế: nếu một kiểu cạnh hữu ích ở góc trái ảnh, thì nó thường cũng hữu ích ở góc phải — ta không cần học lại một bộ weight hoàn toàn mới cho từng vị trí.

Kết nối cục bộ và vùng tiếp nhận

Trong dense layer, một neuron nhìn toàn bộ vector đầu vào. Trong convolution 3×3, một vị trí đầu ra chỉ nhìn một vùng 3×3 cục bộ của ảnh.

Vùng đầu vào ảnh hưởng trực tiếp đến một đơn vị đầu ra gọi là vùng tiếp nhận (receptive field). Ở layer đầu, vùng tiếp nhận nhỏ (ví dụ 3×3 pixel gốc). Khi xếp nhiều layer chồng lên nhau, mỗi neuron ở layer sau gián tiếp “thấy” vùng ảnh gốc ngày càng rộng — vùng tiếp nhận hiệu dụng tăng dần theo độ sâu.

Ảnh màu: kernel phải đi qua tất cả kênh đầu vào

Ảnh RGB thường có ba kênh R, G, B. Nếu input có shape theo quy ước PyTorch:

(C_in, H, W) = (3, 224, 224)

thì một kernel 3×3 phải có chiều sâu bằng số kênh đầu vào: 3 × 3 × 3. Nó nhìn ba ma trận 3×3 tương ứng R, G và B rồi cộng kết quả lại.

Nếu layer có 64 kernel, layer tạo ra 64 kênh đầu ra.

Input : C_in × H × W
Weight: C_out × C_in × K_h × K_w
Output: C_out × H_out × W_out

Số channel đầu ra bằng số filter mà layer học — bạn chọn con số này khi định nghĩa layer, không phụ thuộc vào số channel ảnh gốc.

Số tham số của convolution layer

Với kernel Kh×KwK_h\times K_w, CinC_{in} kênh đầu vào và CoutC_{out} kênh đầu ra, số weight là KhKwCinCoutK_h K_w C_{in} C_{out}. Nếu mỗi output channel có một bias:

parameters=KhKwCinCout+Cout\text{parameters}=K_hK_wC_{in}C_{out}+C_{out}

Ví dụ convolution 3×3, input 3 channel, output 64 channel:

3×3×3×64+64=1,7923\times3\times3\times64+64=1{,}792

Số tham số không phụ thuộc vào chiều cao và chiều rộng ảnh. Cùng 1.792 tham số đó có thể quét trên ảnh nhỏ hay lớn — chỉ thay đổi lượng phép tính và bộ nhớ activation, không thay đổi số weight.

So sánh để thấy mức chênh lệch: kết nối dense từ RGB 224×224 tới 64 neuron cần 224×224×3×64=9,633,792224\times224\times3\times64=9{,}633{,}792 weight; convolution 3×3 tạo ra cùng 64 channel đầu ra chỉ cần 3×3×3×64=1,7283\times3\times3\times64=1{,}728 weight (chưa kể bias). Hai layer này không tương đương về chức năng, nhưng con số cho thấy kết nối cục bộ + chia sẻ trọng số tiết kiệm tham số đến mức nào.

Stride và padding: bước nhảy và biên ảnh

Stride là bước nhảy của kernel sau mỗi lần tính. stride = 1 dịch một ô mỗi lần — kernel đi từng bước nhỏ; stride = 2 nhảy hai ô mỗi lần — output nhỏ nhanh hơn nhưng bỏ qua một số vị trí.

Ví dụ một chiều, kernel size 3:

Input:  1 2 3 4 5 6 7

stride = 1
[1 2 3]
  [2 3 4]
    [3 4 5]
      [4 5 6]
        [5 6 7]

stride = 2
[1 2 3]
    [3 4 5]
        [5 6 7]

Padding thêm viền quanh ảnh — phổ biến nhất là thêm toàn số 0, gọi là zero padding. Nếu ảnh 5×5, kernel 3×3, stride 1 và không padding, kernel không đặt được ở sát rìa nên output chỉ còn 3×3. Thêm padding 1 mỗi phía thì vùng tính trở thành 7×7, output giữ nguyên 5×5.

Padding không chỉ là mẹo giữ shape: cách xử lý biên ảnh hưởng trực tiếp đến thông tin ở rìa ảnh — pixel gần rìa sẽ ít được “nhìn” hơn nếu không có padding.

Công thức kích thước đầu ra

Xét một chiều với kích thước input NN, kernel KK, padding PP, stride SS:

Nout=⌊N+2P−KS⌋+1N_{out}=\left\lfloor\frac{N+2P-K}{S}\right\rfloor+1

Giữ nguyên kích thước: N=32N=32, K=3K=3, P=1P=1, S=1S=1 cho Nout=32N_{out}=32.

Giảm khoảng một nửa: cùng N,K,PN,K,P nhưng S=2S=2 cho Nout=16N_{out}=16.

Kỹ năng quan trọng khi đọc CNN là theo dõi shape như:

3×224×224
→ 64×112×112
→ 128×56×56
→ 256×28×28

Dilation: mở vùng nhìn mà không tăng số weight

Dilation tạo khoảng cách giữa các phần tử kernel — như thể “dãn” kernel ra mà không thêm weight. Kernel 3×3 bình thường nhìn vùng 3×3. Với dilation 2, ba vị trí trên một hàng cách nhau một pixel:

Dilation 1:  x x x          (nhìn vùng 3×3)
Dilation 2:  x . x . x      (nhìn vùng 5×5 nhưng chỉ dùng 3 weight)

Số weight vẫn là 3×3, nhưng vùng tiếp nhận rộng hơn. Dilation thường hữu ích khi cần giữ nguyên độ phân giải không gian trong khi vẫn muốn nhìn vùng rộng — ví dụ trong bài toán segmentation.

Nếu dilation là DD, công thức đầy đủ:

Nout=⌊N+2P−D(K−1)−1S⌋+1N_{out}=\left\lfloor\frac{N+2P-D(K-1)-1}{S}\right\rfloor+1

Khi D=1D=1, công thức này trùng với công thức rút gọn ở trên. Ở mức nhập môn, điều cần nhớ: dilation là cách mở rộng vùng quan sát mà không thêm weight.

Sau convolution thường có activation

Trước activation, convolution chỉ là phép biến đổi affine (tuyến tính nếu không có bias) — nhiều layer convolution liên tiếp không có activation vẫn có thể gộp thành một layer tuyến tính duy nhất. CNN đặt một hàm kích hoạt — thường là ReLU — ngay sau convolution để đưa tính phi tuyến vào mạng.

Logic giống MLP: z=Wx+bz=Wx+b rồi a=ϕ(z)a=\phi(z). Khác biệt chính là cấu trúc của WW: convolution dùng kết nối cục bộ và chia sẻ trọng số.

Pooling và global average pooling: giảm không gian, rồi nối vào classifier

Pooling gộp một vùng nhỏ thành một giá trị duy nhất — mục đích là giảm kích thước không gian. Max pooling 2×2 lấy giá trị lớn nhất trong mỗi ô 2×2:

[1734]→7\begin{bmatrix} 1&7\\ 3&4 \end{bmatrix} \rightarrow 7

Nếu stride là 2, chiều cao và chiều rộng thường giảm khoảng một nửa. Average pooling lấy trung bình thay vì max.

CNN hiện đại không bắt buộc pooling sau mọi convolution. Nhiều kiến trúc dùng convolution stride 2 để giảm kích thước luôn. Pooling chỉ là một trong các cách giảm độ phân giải, không phải thành phần bắt buộc.

Global average pooling (GAP) lấy trung bình của toàn bộ không gian trong từng channel:

64 × 16 × 16  →  64 giá trị   (mỗi channel nén thành 1 số)

Kết quả là một vector 64 chiều, rồi linear layer phân loại trực tiếp. Cách này gọn hơn nhiều so với flatten toàn bộ feature map (64 × 16 × 16 = 16.384 chiều) rồi kết nối fully-connected — flatten dễ tạo số lượng parameter rất lớn.

Một CNN phân loại ảnh đơn giản

Ghép các khối vừa học:

Giả sử padding giữ nguyên shape ở convolution:

Input                3 × 64 × 64
Conv, 32 filters    32 × 64 × 64
Pool                32 × 32 × 32
Conv, 64 filters    64 × 32 × 32
Pool                64 × 16 × 16
Global avg pool     64
Linear              số lớp

Theo dõi được mạch shape như thế này — resolution thu nhỏ dần, channel tăng dần — quan trọng hơn học thuộc tên từng layer.

Vùng tiếp nhận mở rộng khi xếp nhiều layer

Ba convolution 3×3, stride 1: neuron ở layer 1 nhìn vùng 3×3 ảnh gốc; neuron ở layer 2 nhìn nhiều neuron layer 1 nên gián tiếp phụ thuộc vùng 5×5 ảnh gốc; layer sâu hơn tiếp tục gom vùng rộng hơn nữa.

Nói đơn giản: layer đầu phát hiện pattern nhỏ (cạnh, texture cục bộ), layer sau gom nhiều pattern nhỏ thành thứ phức tạp hơn — mà không cần dùng kernel khổng lồ.

Cách mô tả “cạnh → texture → bộ phận → vật thể” hữu ích để hình dung, nhưng không nên hiểu cứng nhắc. Trên thực tế, một feature map không nhất thiết tương ứng với đúng một khái niệm mà con người đặt tên — model học những gì hữu ích cho loss, không phải những gì con người quen diễn đạt.

Tính đồng biến với phép tịnh tiến

Do cùng kernel được dùng ở mọi vị trí, convolution stride 1 có tính chất: khi một pattern dịch sang phải trên ảnh, phản ứng trên feature map cũng dịch sang phải theo. Đây gọi là tính đồng biến với phép tịnh tiến (translation equivariance).

Cần phân biệt với khái niệm hay bị nhầm:

Khái niệmNghĩaVí dụ
Đồng biến (equivariance)Input dịch → output cũng dịch theo tương ứngCạnh dịch sang phải → feature map dịch theo
Bất biến (invariance)Input dịch nhưng output cuối gần như không đổiNhãn phân loại “mèo” không đổi dù mèo ở đâu trong ảnh

Convolution có equivariance, không phải invariance. Để đạt được invariance ở tầng cuối (nhãn không đổi dù object ở vị trí nào), ta thường dùng global pooling hoặc augmentation — không phải convolution tự làm điều đó.

Padding, stride và pooling cũng có thể làm tính đồng biến không còn chính xác tuyệt đối ở biên ảnh. Không nên khẳng định “CNN hoàn toàn bất biến với vị trí”.

CNN học gì, và không chỉ để phân loại?

Một cách nhìn thực dụng:

Input pixels
   ↓
phản ứng với pattern cục bộ
   ↓
kết hợp nhiều pattern
   ↓
representation phục vụ task
   ↓
classification / detection / segmentation / ...

Điều model học phụ thuộc dữ liệu, nhãn, loss, architecture và quá trình huấn luyện. CNN không có sẵn “bộ phát hiện mắt” hay “bộ phát hiện lá bệnh” trước khi train, trừ khi dùng weight đã pretrain.

Cùng phép convolution phục vụ nhiều đầu ra:

TaskOutput điển hình
Phân loại ảnhmột ảnh → một nhãn (healthy / class_a / class_b)
Phát hiện vật thểnhiều object và box
Phân đoạnnhãn theo pixel hoặc vùng
Dữ liệu lưới khácspectrogram, ảnh y tế 2D/3D, 1D conv cho chuỗi, 3D conv cho video

Ý tưởng chính vẫn là khai thác cấu trúc cục bộ và chia sẻ trọng số.

CNN thường là baseline hợp lý khi dữ liệu có cấu trúc lưới, tính cục bộ có ý nghĩa, cùng pattern xuất hiện ở nhiều vị trí, và có backbone pretrained phù hợp. Không có quy tắc “ảnh thì luôn phải CNN”. Vision Transformer và kiến trúc lai cũng xử lý ảnh tốt. CNN vẫn là cách quan trọng để hiểu architecture đưa giả định về dữ liệu vào model.

Convolution trong thư viện thường là cross-correlation

Trong toán học, convolution đúng nghĩa thường lật kernel (flip) trước khi trượt. Nhiều thư viện Deep Learning thực ra thực hiện cross-correlation — không lật — nhưng vẫn đặt tên API là Conv2d. Khác biệt này không quan trọng trong thực hành vì kernel được học: nếu cần lật thì model tự học ra kernel đã lật sẵn. Tài liệu của lĩnh vực thường gọi chung là “convolution” theo quy ước, không phân biệt chặt.

Các lỗi hiểu phổ biến

“CNN tự phát hiện cạnh bằng các kernel viết sẵn”

Không đúng. Trong CNN học sâu thông thường, kernel được học từ dữ liệu — không do người lập trình thiết kế.

“Một kernel chỉ nhìn một channel”

Không đúng. Với convolution chuẩn, một filter đầu ra nhìn tất cả input channel cùng lúc.

“Có 64 channel nghĩa là ảnh có 64 màu”

Không. Channel ở đây là 64 feature map mà model học được — không liên quan đến màu sắc. Chỉ ở layer đầu vào, channel mới tương ứng R/G/B.

“Pooling bắt buộc phải có sau mỗi convolution”

Không. Nhiều kiến trúc hiện đại dùng convolution stride 2 để giảm resolution, bỏ qua pooling hoàn toàn.

“CNN hoàn toàn không quan tâm vị trí”

Không chính xác. Convolution có translation equivariance (phản ứng dịch theo input), nhưng model vẫn có thể nhạy với vị trí, padding, cách crop và cấu trúc không gian của dữ liệu.

“Ảnh càng lớn thì convolution càng nhiều parameter”

Sai. Số tham số của một convolution layer chỉ phụ thuộc vào kernel size và số channel — không phụ thuộc H×WH\times W. Ảnh lớn hơn tăng lượng phép tính và bộ nhớ activation, không tăng số weight.

Một bài tập đọc architecture

Cho input:

batch × channel × height × width
32 × 3 × 128 × 128

Model:

Conv 3×3, out=32, padding=1, stride=1
MaxPool 2×2, stride=2
Conv 3×3, out=64, padding=1, stride=1
MaxPool 2×2, stride=2

Hãy tự tính shape sau từng bước.

Đáp án:

Input     : 32 × 3  × 128 × 128
Conv 1    : 32 × 32 × 128 × 128
Pool 1    : 32 × 32 × 64  × 64
Conv 2    : 32 × 64 × 64  × 64
Pool 2    : 32 × 64 × 32  × 32

Lưu ý: số 32 đầu tiên trong mỗi dòng là batch size (32 ảnh mỗi batch), số 32 ở Conv 1 là số channel đầu ra. Hai con số trùng nhau nhưng ý nghĩa hoàn toàn khác.

Nếu chỉ nhớ 10 ý

  • Ảnh có cấu trúc không gian; pixel gần nhau thường tạo pattern cục bộ.
  • CNN dùng kernel nhỏ thay vì kết nối mọi đầu vào với mọi neuron.
  • Kernel quét trên nhiều vị trí và tạo feature map.
  • Cùng kernel được dùng lại: chia sẻ trọng số.
  • Một convolution layer thường có nhiều kernel nên tạo nhiều output channel.
  • Với convolution chuẩn, mỗi filter nhìn tất cả input channel.
  • Stride điều khiển bước dịch; padding điều khiển biên và kích thước đầu ra.
  • Pooling hoặc stride có thể giảm độ phân giải; GAP nén mỗi channel thành một số trước classifier.
  • Xếp nhiều layer làm receptive field hiệu dụng lớn dần.
  • CNN khai thác cấu trúc lưới; bài sau xếp các phép này thành ConvNet.

Câu hỏi tự kiểm tra

  1. Vì sao việc làm phẳng ảnh vẫn giữ các giá trị pixel nhưng làm mất một phần cấu trúc hữu ích đối với kiến trúc?
  2. Một kernel 3×3 trên ảnh RGB có bao nhiêu weight nếu chỉ tạo một output channel và không tính bias?
  3. Nếu layer có 64 filter thì output có bao nhiêu channel?
  4. Vì sao weight sharing giúp giảm số parameter?
  5. Stride 2 thường ảnh hưởng kích thước feature map thế nào?
  6. Padding 1 với kernel 3×3 và stride 1 thường có tác dụng gì với kích thước không gian?
  7. Receptive field của layer sâu khác layer đầu như thế nào?
  8. Translation equivariance khác translation invariance ở đâu?
  9. Pooling có bắt buộc trong mọi CNN không?
  10. Ảnh lớn hơn có làm số parameter của cùng một Conv2d(3, 64, 3) tăng không? Điều gì vẫn tăng?

Mạch nối sang bài tiếp theo

Đến đây ta có các khối cơ bản: convolution, activation, giảm resolution, channel và vùng tiếp nhận.

Câu hỏi tiếp theo không phải “còn những tên model nào trong lịch sử?”, mà là:

Khi xếp các khối này thành backbone sâu, ta ổn định huấn luyện, trộn thông tin không gian/kênh và kiểm soát chi phí bằng cách nào? ConvNet hiện đại khác một stack convolution thuần ở điểm nào?

Bài tiếp theo lấy residual ConvNet làm khung, rồi đến ConvNeXt và một vài hướng đang được dùng trong thực tế.

Tài liệu tham khảo và đọc thêm