Mạng tích chập (CNN)
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu ảnh được lưu thành ma trận hay tensor nhiều kênh, và một bộ lọc (filter) 3×3 trượt trên ảnh làm mờ, làm nét hay tìm cạnh thế nào.
- Tính được phép tích chập (convolution) 2D bằng tay và bằng NumPy, biết vì sao trong học sâu nó thực ra là cross-correlation, và tính được kích thước đầu ra theo padding, stride.
- Đếm được số tham số của một lớp tích chập nhiều kênh, và giải thích vì sao mạng tích chập (Convolutional Neural Network, CNN) cần ít tham số hơn MLP nhờ kết nối cục bộ và chia sẻ trọng số.
- Nhận ra cấu trúc tích chập → kích hoạt → pooling → … → lớp kết nối đầy đủ (fully connected layer), tính được trường tiếp nhận (receptive field), và đọc được một CNN nhỏ viết bằng PyTorch.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Huấn luyện mạng nơ-ron: vanishing/exploding gradient, BatchNorm, Dropout, thuộc phần Học sâu (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Ảnh là một bảng số
Phần tiêu đề “Ảnh là một bảng số”Với máy tính, một ảnh xám là một bảng số. Mỗi ô là một điểm ảnh (pixel), số càng lớn thì càng sáng. Đây là một ảnh xám 8×8, nền tối (0) và một hình vuông sáng (9) ở giữa. Bài này dùng đúng ảnh này từ đầu tới cuối:
Ảnh màu thì có ba bảng chồng lên nhau, mỗi bảng một kênh (channel): đỏ, lục, lam (RGB). Một ảnh cao , rộng , có kênh là một tensor cỡ , tức một mảng ba chiều. Ảnh chụp 224×224 có số.
Thử cách đã biết: trải phẳng rồi đưa vào MLP
Phần tiêu đề “Thử cách đã biết: trải phẳng rồi đưa vào MLP”Bạn đã biết MLP. Cách thẳng nhất là trải phẳng ảnh thành một vectơ dài, rồi đưa vào lớp kết nối đầy đủ. Có ba chỗ không ổn:
- Quá nhiều tham số. Ảnh 224×224×3 nối tới chỉ 1000 nơ-ron ẩn đã cần triệu trọng số, cho riêng lớp đầu.
- Vứt bỏ hàng xóm. Sau khi trải phẳng, hai điểm ảnh cạnh nhau theo chiều dọc nằm cách nhau cả một hàng trong vectơ. MLP không biết điểm nào gần điểm nào; nó phải tự học lại điều đó từ dữ liệu.
- Học lại cùng một thứ ở mọi chỗ. Một cái cạnh ở góc trái và cùng cái cạnh đó ở góc phải đi qua hai nhóm trọng số khác nhau. Mạng phải học cách nhận ra cạnh riêng cho từng vị trí.
Ý tưởng: một cửa sổ nhỏ trượt khắp ảnh
Phần tiêu đề “Ý tưởng: một cửa sổ nhỏ trượt khắp ảnh”Người làm xử lý ảnh đã có cách khác từ lâu. Lấy một bảng nhỏ 3×3, gọi là bộ lọc (filter) hay kernel. Đặt nó lên một góc ảnh, nhân từng ô của bảng với điểm ảnh nằm dưới nó, cộng hết lại thành một số. Trượt sang phải một ô, làm lại. Hết hàng thì xuống một ô. Các số thu được xếp thành một ảnh mới.
Các con số trong kernel quyết định ảnh mới trông thế nào:
| Kernel | Giá trị | Tác dụng |
|---|---|---|
| Làm mờ trung bình | chín ô đều bằng | Mỗi điểm thành trung bình của 9 điểm quanh nó, nên chi tiết nhòe đi |
| Làm nét | tâm , bốn ô trên dưới trái phải , góc | Điểm khác hàng xóm thì càng bị đẩy khác hơn, nên đường viền rõ hơn |
| Sobel dọc | cột trái ; cột giữa ; cột phải | Đo “bên phải sáng hơn bên trái bao nhiêu”, nên chỉ sáng lên ở cạnh dọc |
Ý hay nằm ở hai chỗ. Kernel chỉ nhìn một vùng nhỏ, nên nó tôn trọng hàng xóm. Và cùng một kernel dùng cho mọi vị trí, nên 9 số đủ để tìm cạnh dọc ở bất cứ đâu trong ảnh.
Bài toán của bài này là: thay vì tự tay thiết kế kernel như Sobel, để mạng nơ-ron tự học các kernel từ dữ liệu, xếp nhiều lớp như vậy chồng lên nhau, và hiểu mỗi lớp nhận gì, trả gì, tốn bao nhiêu tham số.
Lý thuyết
Phần tiêu đề “Lý thuyết”Phép tích chập 2D
Phần tiêu đề “Phép tích chập 2D”Gọi là ảnh xám và là kernel cỡ . Ký hiệu là điểm ảnh ở hàng , cột , và là ô ở hàng , cột của kernel, đều đếm từ 0 như trong NumPy. Đặt góc trên trái của kernel tại vị trí , nhân từng cặp ô rồi cộng:
Ở đây là hệ số tự do (bias), một số chung cho cả kernel; các bộ lọc xử lý ảnh ở trên có . Bảng kết quả gọi là bản đồ đặc trưng (feature map): nó cho biết ở mỗi vị trí, vùng ảnh giống mẫu hình mà kernel tìm tới đâu.
Tính tay một ô. Áp Sobel dọc lên ảnh ở trên, tại . Cửa sổ là hàng 2 tới 4, cột 0 tới 2 của :
Phép nhân từng ô với ô cùng chỗ. Cộng chín số lại được . Cột phải sáng, cột trái tối: đây là cạnh trái của hình vuông. Dời cửa sổ tới , cả cửa sổ toàn số 9, cột trái và cột phải triệt tiêu nhau, nên : giữa hình vuông không có cạnh. Ở cạnh phải, tối nằm bên phải, nên giá trị âm, .
Ảnh 8×8, kernel 3×3: góc trên trái của kernel chỉ đặt được ở 6 vị trí mỗi chiều để kernel không lòi ra ngoài, nên cỡ 6×6.
Padding và stride
Phần tiêu đề “Padding và stride”Padding (đệm) : thêm vòng số 0 quanh ảnh trước khi trượt. Không có padding thì mỗi lớp làm ảnh co lại điểm mỗi chiều, và các điểm ảnh ở mép được kernel ghé qua ít lần hơn điểm ở giữa. Với , giữ nguyên kích thước.
Stride (bước trượt) : mỗi lần kernel nhảy ô thay vì 1. Stride 2 làm đầu ra nhỏ đi khoảng một nửa mỗi chiều.
Với ảnh vuông cạnh (ở đây là số điểm ảnh mỗi cạnh, không phải số đặc trưng), ảnh sau khi đệm có cạnh . Kernel cạnh đặt ở vị trí đầu tiên chiếm từ ô 0 tới ô , nên phần còn lại để trượt dài ô. Mỗi bước nhảy ô, nên có bước trọn vẹn, cộng thêm vị trí xuất phát:
Dấu là làm tròn xuống: bước cuối mà kernel lòi ra ngoài thì bỏ. Vài ví dụ với ảnh 8×8 của bài:
| Kernel, padding, stride | Phép tính | Cạnh đầu ra |
|---|---|---|
| , , (“valid”) | 6 | |
| , , (“same”) | 8 | |
| , , | 3 |
Nhiều kênh vào, nhiều kênh ra
Phần tiêu đề “Nhiều kênh vào, nhiều kênh ra”Gọi số kênh vào là và số kênh ra là , đúng như hai tham số in_channels, out_channels của nn.Conv2d trong PyTorch. Ảnh màu có . Khi đó kernel không phải 3×3 mà là : nó có một lát 3×3 riêng cho mỗi kênh. Ở mỗi vị trí, nhân từng ô trên cả ba kênh rồi cộng tất cả thành một số, cộng thêm . Vậy một kernel, dù ảnh vào có bao nhiêu kênh, cho ra một bản đồ đặc trưng.
Một lớp tích chập thường có nhiều kernel, mỗi kernel tìm một mẫu hình riêng (cạnh dọc, cạnh ngang, một vệt màu…). kernel cho ra bản đồ, xếp chồng thành một tensor . Tensor này lại là đầu vào có kênh cho lớp tích chập kế tiếp.
Đếm tham số. Mỗi kernel có trọng số và 1 hệ số tự do. Cả lớp:
Không có trong công thức: số tham số không phụ thuộc kích thước ảnh. Ví dụ, ảnh xám (), 8 kernel 3×3: tham số, dù ảnh là 28×28 hay 1000×1000.
Pooling
Phần tiêu đề “Pooling”Lớp pooling (gộp) làm bản đồ đặc trưng nhỏ lại. Phổ biến nhất là max-pooling 2×2, stride 2: chia bản đồ thành các ô 2×2 không chồng nhau, mỗi ô giữ lại số lớn nhất. Còn có average pooling, giữ trung bình. Pooling không có tham số và làm riêng trên từng kênh.
Lấy bản đồ Sobel dọc 6×6 của ảnh, qua ReLU (số âm thành 0), rồi max-pooling 2×2. Kết quả 3×3 (ô code đầu tiên tính lại):
Bản đồ còn một phần tư số ô, nhưng thông tin chính vẫn còn: “có cạnh dọc kiểu tối sang sáng ở cột bên trái”. Pooling có ba tác dụng: giảm số phép tính ở các lớp sau, mở rộng vùng ảnh mà mỗi giá trị phía sau “thấy”, và làm kết quả ít nhạy với xê dịch nhỏ, như mục sau.
Cấu trúc một CNN
Phần tiêu đề “Cấu trúc một CNN”Một CNN điển hình xếp các khối như sau, đọc từ ảnh tới kết quả:
- Tích chập → kích hoạt (thường là ReLU) → pooling, lặp lại vài lần. Mỗi lần, ảnh nhỏ đi về chiều cao và chiều rộng, còn số kênh thường tăng lên (ví dụ 8, rồi 16, rồi 32).
- Trải phẳng (flatten) tensor cuối thành vectơ.
- Một hay vài lớp kết nối đầy đủ, lớp cuối có số đầu ra bằng số lớp, rồi softmax và cross-entropy như MLP.
Các lớp đầu học các mẫu hình đơn giản như cạnh và vệt màu. Các lớp sau ghép chúng thành mẫu hình phức tạp hơn: góc, nét cong, rồi các bộ phận. Cả mạng học bằng lan truyền ngược như MLP; trọng số của các kernel chỉ là thêm tham số để cập nhật.
Vì sao CNN tốt hơn MLP với ảnh
Phần tiêu đề “Vì sao CNN tốt hơn MLP với ảnh”So một lớp tích chập với một lớp kết nối đầy đủ có cùng đầu vào và đầu ra:
- Kết nối cục bộ (local connectivity). Mỗi giá trị đầu ra chỉ nối với điểm ảnh xung quanh, không phải cả ảnh. Điều này mã hóa sẵn một giả định đúng với ảnh: điểm gần nhau liên quan với nhau nhiều hơn.
- Chia sẻ trọng số (weight sharing). Cùng một kernel dùng ở mọi vị trí. Mẫu hình hữu ích ở góc này cũng hữu ích ở góc kia, nên học một lần là đủ.
- Ít tham số hơn hẳn. Hai điều trên cộng lại: lớp tích chập 1 → 8 kênh, kernel 3×3, trên ảnh 28×28 cho 26×26×8 giá trị ra với 80 tham số. Một lớp kết nối đầy đủ cho cùng số giá trị ra cần 4 245 280 tham số (ô code thứ hai).
- Đẳng biến với phép dịch (translation equivariance). Dịch vật trong ảnh sang phải 1 điểm ảnh thì bản đồ đặc trưng cũng dịch sang phải đúng 1 ô, các giá trị giữ nguyên, vì cùng một kernel quét mọi chỗ. Pooling biến điều này thành bất biến gần đúng (approximate invariance): kết quả ít đổi khi vật xê dịch nhỏ. Chỉ là gần đúng: với ảnh của bài, dịch hình vuông sang phải 1 điểm ảnh làm bản đồ 3×3 sau pooling ở trên đổi từ một cột khác 0 thành hai cột. Còn nếu lấy số lớn nhất của cả bản đồ (pooling toàn cục), kết quả là 36 ở cả hai trường hợp.
Đổi lại, CNN giả định ảnh có cấu trúc cục bộ và lặp lại. Với dữ liệu dạng bảng, nơi thứ tự các cột không có nghĩa, giả định đó sai và CNN không có lợi thế.
Trường tiếp nhận
Phần tiêu đề “Trường tiếp nhận”Trường tiếp nhận (receptive field) của một giá trị trong bản đồ đặc trưng là vùng ảnh vào có ảnh hưởng tới nó. Sau một lớp 3×3, mỗi giá trị nhìn 3×3 điểm ảnh. Thêm một lớp 3×3 nữa: mỗi giá trị nhìn 3×3 giá trị của lớp trước, mỗi giá trị đó lại nhìn 3×3 điểm ảnh, chồng lên nhau thành 5×5.
Tổng quát, đi từ ảnh vào, gọi là cạnh trường tiếp nhận và là khoảng cách (tính bằng điểm ảnh của ảnh vào) giữa hai giá trị kề nhau ở lớp hiện tại. Lúc đầu , . Mỗi lớp có kernel và stride cập nhật:
Cập nhật đầu đúng vì kernel thêm giá trị mỗi chiều, mỗi giá trị cách nhau điểm ảnh. Cập nhật thứ hai đúng vì stride bỏ qua giá trị trong mỗi giá trị.
- Ba lớp 3×3 stride 1: . Trường tiếp nhận tăng đều, mỗi lớp thêm 2.
- CNN của ô code thứ hai (tích chập 3×3, pooling 2×2 stride 2, tích chập 3×3, pooling 2×2 stride 2): , rồi ( thành 2), rồi , rồi . Mỗi giá trị cuối cùng tóm tắt một vùng 10×10 của ảnh 28×28.
Pooling và stride làm tăng, nên các lớp sau mở rộng trường tiếp nhận nhanh hơn. Nhờ vậy mạng đủ sâu có thể nhìn cả vật, dù mỗi kernel chỉ 3×3.
Giới hạn
Phần tiêu đề “Giới hạn”- Bất biến chỉ gần đúng. Đẳng biến với phép dịch đúng cho tích chập stride 1, nhưng stride, pooling và mép ảnh làm nó lệch, như ví dụ dịch 1 điểm ảnh ở trên. CNN cũng không tự bất biến với phép xoay hay đổi tỉ lệ; người ta bù bằng tăng cường dữ liệu (xoay, lật, cắt ảnh khi huấn luyện), một chủ đề của phần Thị giác máy tính.
- Chỉ nhìn cục bộ ở mỗi lớp. Muốn liên hệ hai vùng xa nhau, mạng cần đủ nhiều lớp. Kiến trúc nào xếp lớp thế nào cho hiệu quả là chủ đề của bài Các kiến trúc CNN hiện đại.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới có ảnh xám 8×8 của bài, kernel 3×3 và bản đồ đặc trưng 6×6. Năm nút chọn kernel: Đồng nhất, Làm mờ, Làm nét, Sobel dọc, Sobel ngang. Bấm vào một ô của bản đồ đặc trưng (hoặc dùng phím mũi tên, hay hai nút lùi, tới) để dời cửa sổ. Khung 3×3 trên ảnh cho thấy cửa sổ đang nằm đâu, bảng bên cạnh kernel ghi chín phép nhân từng ô và tổng của chúng. Hàng và cột đếm từ 0. Hãy thử:
- Lúc mở, hình chọn Sobel dọc và cửa sổ ở hàng 1, cột 1. Cột phải của kernel gặp hai số 9, nên tổng là . Bấm ô hàng 2, cột 0: cột phải của cửa sổ là ba số 9, phép tính ra , giá trị lớn nhất của bản đồ. Bấm ô hàng 2, cột 2: cửa sổ toàn số 9, tổng là 0. Bấm ô hàng 2, cột 5: , cạnh phải có dấu ngược lại.
- Sobel ngang: hai hàng trên của bản đồ dương (lớn nhất 36), hai hàng dưới âm, hai hàng giữa bằng 0. Kernel này tìm cạnh ngang.
- Đồng nhất: kernel chỉ có số 1 ở tâm, nên mỗi ô ra đúng bằng điểm ảnh ở tâm cửa sổ. Bản đồ là phần 6×6 ở giữa ảnh: hình vuông 9 trên nền 0, mất một vòng mép.
- Làm mờ rồi Làm nét: làm mờ cho 9.00 ở bốn ô giữa và nhỏ dần ra mép, tới 1.00 ở bốn góc. Làm nét đẩy góc hình vuông lên 27, giữa hình vuông giữ 9, và tạo viền ngay bên ngoài.
Ví dụ code
Phần tiêu đề “Ví dụ code”Hai ô đầu chạy trên trang, chỉ cần NumPy hoặc Python thuần, và chạy độc lập với nhau. Ô thứ ba huấn luyện CNN bằng PyTorch nên chạy trên máy bạn.
Ô thứ nhất viết hàm conv2d bằng hai vòng lặp, đúng như công thức ở mục Lý thuyết, áp Sobel dọc lên ảnh 8×8, rồi qua ReLU và max-pooling 2×2.
Tích chập 2D, Sobel và max-pooling bằng NumPy
import numpy as np
# Ảnh xám 8x8: một hình vuông sáng (9) trên nền tối (0)IMAGE = np.array([ [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 9, 9, 9, 0, 0], [0, 0, 9, 9, 9, 9, 0, 0], [0, 0, 9, 9, 9, 9, 0, 0], [0, 0, 9, 9, 9, 9, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],])# Sobel dọc: phát hiện cạnh dọc (sáng dần từ trái sang phải)SOBEL_V = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
def conv2d(x, kernel, stride=1): """Tích chập 'valid', không lật kernel (cross-correlation).""" k = kernel.shape[0] n_out = (x.shape[0] - k) // stride + 1 out = np.zeros((n_out, n_out)) for i in range(n_out): for j in range(n_out): r, c = i * stride, j * stride window = x[r:r + k, c:c + k] # cửa sổ k x k out[i, j] = np.sum(window * kernel) # nhân từng ô rồi cộng return out
fmap = conv2d(IMAGE, SOBEL_V)print("feature map", fmap.shape)print(fmap.astype(int))# Stride 2: cửa sổ nhảy 2 ô, đầu ra chỉ còn (8 - 3) // 2 + 1 = 3print("stride 2:", conv2d(IMAGE, SOBEL_V, stride=2).shape)
# Kích hoạt ReLU rồi max-pooling 2x2, bước 2relu = np.maximum(fmap, 0)pooled = relu.reshape(3, 2, 3, 2).max(axis=(1, 3))print("after ReLU + 2x2 max-pool", pooled.shape)print(pooled.astype(int))Lần chạy đầu cần tải Python, khoảng 10 MB.
Bản đồ đặc trưng 6×6 có hai cột bên trái dương, đọc từ trên xuống là 9, 27, 36, 36, 27, 9 (cạnh trái, tối sang sáng), và hai cột bên phải là các số đối (cạnh phải, sáng sang tối). Hai cột giữa bằng 0, vì cửa sổ nằm trọn trong vùng đồng màu nên hai cột của kernel triệt tiêu nhau. Số giảm dần ở hàng trên và hàng dưới vì cửa sổ chỉ phủ một phần hình vuông: ở hàng 2 và 3 của bản đồ, cả ba hàng của cửa sổ chạm hình vuông, nên tổng là ; ở hàng 1, chỉ hai hàng dưới của cửa sổ chạm, nên còn ; ở hàng 0, chỉ hàng dưới cùng chạm, còn . Hàng 4 và 5 đối xứng như vậy. Với stride 2, cạnh đầu ra là , khớp công thức. ReLU bỏ hết số âm, max-pooling 2×2 giữ lại 27, 36, 27 ở cột trái, đúng ma trận 3×3 ở mục Pooling.
Dòng pooled = relu.reshape(3, 2, 3, 2).max(axis=(1, 3)) là cách NumPy chia bản đồ 6×6 thành 3×3 khối 2×2 rồi lấy số lớn nhất của từng khối, không cần vòng lặp. Thử đổi dòng fmap = conv2d(IMAGE, SOBEL_V) thành conv2d(IMAGE, SOBEL_V.T) (Sobel ngang): bản đồ dương ở hai hàng trên, âm ở hai hàng dưới, và sau pooling các số 27, 36, 27 nằm ở hàng trên thay vì cột trái.
Ô thứ hai đếm tham số cho ảnh xám 28×28 cỡ chữ số viết tay, so một MLP với một CNN nhỏ. Nó dùng đúng hai công thức của bài: kích thước đầu ra và số tham số.
Đếm tham số: MLP và CNN
# Đếm tham số: MLP và CNN cho cùng một ảnh xám 28x28, 10 lớp
def out_size(n, k, p=0, s=1): """Kích thước đầu ra: floor((n + 2p - k) / s) + 1.""" return (n + 2 * p - k) // s + 1
def dense(n_in, n_out): return n_in * n_out + n_out # trọng số + hệ số tự do
def conv(c_in, c_out, k): return k * k * c_in * c_out + c_out # mỗi kernel k x k x c_in, 1 bias
# MLP: trải phẳng 784 điểm ảnh -> 128 -> 10mlp = dense(28 * 28, 128) + dense(128, 10)print(f"MLP params: {mlp:,}")
# CNN: conv 3x3 (1->8) -> pool 2 -> conv 3x3 (8->16) -> pool 2 -> densen = 28total = 0for c_in, c_out in [(1, 8), (8, 16)]: total += conv(c_in, c_out, 3) n = out_size(n, k=3) # tích chập "valid" print(f"conv {c_in}->{c_out}: {n}x{n}x{c_out}", end="") n = out_size(n, k=2, s=2) # max-pooling 2x2, bước 2 print(f" -> pool: {n}x{n}x{c_out}")total += dense(n * n * 16, 10)print(f"CNN params: {total:,}")
# Chỉ riêng lớp đầu: kết nối đầy đủ cần bao nhiêu tham số# để ra cùng số giá trị như conv 1->8 (26x26x8)?print(f"dense 784 -> 26*26*8: {dense(784, 26 * 26 * 8):,}")print(f"conv 1->8, 3x3: {conv(1, 8, 3):,}")Lần chạy đầu cần tải Python, khoảng 10 MB.
MLP 784 → 128 → 10 có 101 770 tham số, gần hết nằm ở lớp đầu (). CNN có 5 258 tham số, ít hơn khoảng 19 lần. Đường đi của kích thước cho thấy làm tròn xuống: 28 → 26 → 13 → 11 → 5, vì pooling 2×2 trên cạnh 11 chỉ có vị trí, cột cuối bị bỏ. Phần lớn tham số của CNN (4 010) nằm ở lớp kết nối đầy đủ cuối, lớp tích chập thứ hai chỉ có .
Hai dòng cuối là so sánh rõ nhất: lớp tích chập 1 → 8 kênh cho ra 26×26×8 = 5 408 giá trị với 80 tham số; một lớp kết nối đầy đủ ra cùng số giá trị cần 4 245 280 tham số, gấp hơn 50 nghìn lần.
Ô thứ ba huấn luyện một CNN nhỏ trên bộ chữ số viết tay load_digits của scikit-learn (1 797 ảnh 8×8, một kênh, có sẵn khi cài, không cần tải). Nó so với một MLP cùng số epoch, rồi thử cả hai trên ảnh kiểm tra bị dịch sang phải 1 điểm ảnh.
Chạy trên máy bạn
CNN và MLP trên chữ số 8×8
- Cần cài
pip install torch scikit-learn- Phần cứng
- CPU là đủ. Khoảng 40 giây tới 2 phút tùy máy
import torchfrom sklearn.datasets import load_digitsfrom sklearn.model_selection import train_test_splitfrom torch import nn
torch.manual_seed(0)# 1797 ảnh chữ số 8x8, giá trị 0..16; chia về 0..1X, y = load_digits(return_X_y=True)X = torch.tensor(X / 16.0, dtype=torch.float32).reshape(-1, 1, 8, 8)y = torch.tensor(y)X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.3, random_state=0, stratify=y)
cnn = nn.Sequential( # ảnh vào: 1 x 8 x 8 nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 16 x 4 x 4 nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 32 x 2 x 2 nn.Flatten(), nn.Linear(32 * 2 * 2, 10))mlp = nn.Sequential( # trải phẳng 64 điểm ảnh nn.Flatten(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 10))
def accuracy(model, X, y): with torch.no_grad(): return (model(X).argmax(1) == y).float().mean().item()
def train(model, epochs=15): opt = torch.optim.Adam(model.parameters(), lr=0.01) loss_fn = nn.CrossEntropyLoss() for _ in range(epochs): perm = torch.randperm(len(X_tr)) for i in range(0, len(X_tr), 64): # mini-batch 64 ảnh idx = perm[i:i + 64] opt.zero_grad() loss_fn(model(X_tr[idx]), y_tr[idx]).backward() opt.step()
# Ảnh kiểm tra dịch sang phải 1 điểm ảnh (cột trái thành 0)X_shift = torch.roll(X_te, shifts=1, dims=3)X_shift[..., 0] = 0for name, model in [("CNN", cnn), ("MLP", mlp)]: train(model) n_par = sum(p.numel() for p in model.parameters()) print(f"{name}: {n_par:5d} params " f"test acc {accuracy(model, X_te, y_te):.3f} " f"shifted {accuracy(model, X_shift, y_te):.3f}")Kết quả mẫu
Lấy từ một lần chạy thật.
CNN: 6090 params test acc 0.976 shifted 0.650MLP: 4810 params test acc 0.957 shifted 0.461Đọc kết quả:
- Số tham số gần bằng nhau. CNN có 6 090 tham số: 160 ở lớp tích chập đầu (), 4 640 ở lớp thứ hai (), 1 290 ở lớp kết nối đầy đủ cuối. MLP có 4 810. Hai lớp tích chập dùng
padding=1nên giữ nguyên cạnh, chỉ pooling làm ảnh nhỏ đi: 8 → 4 → 2. - CNN đúng hơn trên ảnh kiểm tra: 0.976 so với 0.957, sau cùng 15 epoch với cùng bộ tối ưu Adam. Ảnh 8×8 rất nhỏ nên MLP vẫn làm khá tốt; khoảng cách thường rộng ra khi ảnh lớn hơn.
- Dịch ảnh 1 điểm ảnh, cả hai đều tụt mạnh, nhưng CNN ít hơn: 0.650 so với 0.461. Các lớp tích chập đẳng biến với phép dịch, nhưng lớp kết nối đầy đủ ở cuối vẫn gắn mỗi trọng số với một vị trí, và trên ảnh 8×8, 1 điểm ảnh là một phần tám bề ngang. Đây là con số cụ thể cho chữ “gần đúng” ở mục Lý thuyết. Cách bù thường dùng là huấn luyện với ảnh đã dịch, xoay ngẫu nhiên (bài Data augmentation trong CV).
Seed cố định, nên ba lần chạy trên máy phát triển cho cùng kết quả; trên máy khác hay bản PyTorch khác, số có thể lệch đôi chút.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Ảnh là tensor . Một lớp tích chập trượt các kernel nhỏ trên ảnh, nhân từng ô rồi cộng; trong học sâu phép này không lật kernel, nên thực chất là cross-correlation.
- Cạnh đầu ra là . Số tham số là , không phụ thuộc kích thước ảnh. Pooling thu nhỏ bản đồ đặc trưng, không có tham số.
- CNN xếp các khối tích chập → ReLU → pooling, rồi trải phẳng và đưa vào lớp kết nối đầy đủ. Kết nối cục bộ và chia sẻ trọng số làm nó ít tham số hơn MLP rất nhiều, và đẳng biến với phép dịch; pooling thêm bất biến gần đúng.
- Trường tiếp nhận lớn dần theo độ sâu: mỗi lớp thêm , và pooling, stride làm nó tăng nhanh hơn.
Đọc thêm:
- Zhang, A., Lipton, Z. C., Li, M., Smola, A. J. (2020), bản dịch của nhóm Dịch thuật Việt Nam. Đắm mình vào Học Sâu, chương 6: Mạng Nơ-ron Tích chập. Tiếng Việt, đi từ lớp kết nối đầy đủ tới tích chập, đệm và sải bước, đa kênh, gộp, có code chạy được.
- Goodfellow, I., Bengio, Y., Courville, A. (2016). Deep Learning, chương 9: Convolutional Networks. Mục 9.1 nói về tích chập và cross-correlation, mục 9.2 về tương tác thưa, chia sẻ tham số và đẳng biến, mục 9.3 về pooling.
- Dumoulin, V., Visin, F. (2016). A guide to convolution arithmetic for deep learning. Hình vẽ từng trường hợp padding và stride, kèm công thức kích thước đầu ra. Ký hiệu khác: là cạnh đầu vào, là cạnh đầu ra.
- Stanford CS231n. Convolutional Neural Networks for Visual Recognition: Convolutional Networks. Ghi chú bài giảng về các lớp của CNN, ví dụ đếm tham số của lớp đầu AlexNet có và không có chia sẻ trọng số. Ký hiệu khác: là cạnh ảnh, là cạnh kernel, là padding, là stride.
- Azulay, A., Weiss, Y. (2019). Why do deep convolutional networks generalize so poorly to small image transformations?. JMLR 20(184). Cho thấy CNN hiện đại có thể đổi dự đoán khi ảnh chỉ dịch vài điểm ảnh, vì stride và pooling bỏ qua định lý lấy mẫu.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu