Phân loại ảnh với CNN: AlexNet → VGG → ResNet → MobileNet → EfficientNet
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu vì sao tích chập (convolution) hợp với ảnh: nhìn cục bộ, chia sẻ trọng số, nên ít tham số hơn lớp kết nối đầy đủ (fully connected layer) hàng trăm nghìn lần.
- Tự tính được kích thước đầu ra , số tham số của một lớp, và trường tiếp nhận (receptive field) của một chồng lớp.
- Kể được ý chính của từng mạng trong dòng phân loại ảnh (image classification): AlexNet, VGG, ResNet, MobileNet, EfficientNet, và bài toán mỗi mạng giải.
- Cài được tích chập 2D bằng NumPy, huấn luyện một CNN nhỏ bằng PyTorch, đếm tham số các mạng có sẵn trong torchvision.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Các kiến trúc CNN hiện đại: LeNet, AlexNet, VGG, GoogLeNet, ResNet, Xception, SENet, YOLO, thuộc phần Học sâu (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Bài toán: một ảnh vào, một nhãn ra
Phần tiêu đề “Bài toán: một ảnh vào, một nhãn ra”Bạn có một ảnh chữ số viết tay điểm ảnh và muốn máy nói đó là số mấy. Hay một ảnh chụp và muốn máy nói đó là mèo, chó hay ấm trà. Đó là phân loại ảnh: đầu vào là ảnh, đầu ra là một nhãn trong một danh sách lớp cố định.
Bài toán này có một sân thi đấu nổi tiếng: cuộc thi ImageNet Large Scale Visual Recognition Challenge (ILSVRC, Russakovsky và cộng sự, 2015), tổ chức hằng năm từ 2010. Phần phân loại có 1 000 lớp và khoảng 1.2 triệu ảnh huấn luyện. Năm mạng trong tên bài đều là mốc trên con đường này.
Thử cách đơn giản: trải phẳng ảnh
Phần tiêu đề “Thử cách đơn giản: trải phẳng ảnh”Cách đầu tiên ai cũng nghĩ tới: trải ảnh thành một vectơ dài rồi cho vào mạng nơ-ron thường. Ảnh có 150 528 số. Một lớp kết nối đầy đủ ra chỉ 1 000 nơ-ron đã cần khoảng 150 triệu trọng số.
Tệ hơn nữa là chuyện vị trí. Mỗi trọng số gắn với một điểm ảnh cố định. Con mèo ở góc trái và con mèo ở góc phải đi qua hai bộ trọng số khác hẳn nhau. Mạng phải học lại “tai mèo trông thế nào” ở từng chỗ trong ảnh.
Hai quan sát về ảnh
Phần tiêu đề “Hai quan sát về ảnh”- Cục bộ. Một cạnh, một nét, một góc chỉ cần nhìn vài điểm ảnh sát nhau. Không cần nối một điểm ở góc trên với một điểm ở góc dưới ngay từ lớp đầu.
- Lặp lại ở mọi nơi. Một cạnh dọc ở bên trái hay bên phải trông như nhau. Vậy dùng cùng một bộ lọc cho mọi vị trí.
Hãy hình dung một chiếc kính lúp nhỏ chỉ biết tìm một kiểu nét. Bạn trượt nó khắp trang giấy và ghi lại chỗ nào nó “sáng lên”. Đó là một kernel. Nhiều kính lúp, mỗi chiếc tìm một kiểu nét, là một lớp tích chập. Xếp chồng nhiều lớp: lớp đầu tìm cạnh, lớp sau ghép cạnh thành nét cong, góc, rồi thành mắt, tai, rồi thành mèo.
Bài toán của bài này là: làm sao dựng một mô hình nhận ảnh làm đầu vào, ít tham số, nhận ra cùng một mẫu ở mọi vị trí, và đủ sâu để ghép mẫu nhỏ thành vật lớn; rồi làm sao cho nó sâu hơn, chính xác hơn mà vẫn rẻ?
Lý thuyết
Phần tiêu đề “Lý thuyết”Phát biểu bài toán
Phần tiêu đề “Phát biểu bài toán”Ảnh là : cao , rộng điểm ảnh, kênh ( với ảnh xám, với ảnh màu). Một CNN là hàm có tham số , biến ảnh thành một vectơ logit, mỗi lớp một số. Softmax đổi logit thành xác suất, lớp có xác suất cao nhất là dự đoán . Huấn luyện là chọn để hàm mất mát cross-entropy trên tập huấn luyện nhỏ nhất.
ILSVRC chấm bằng lỗi top-5 (top-5 error): dự đoán tính là đúng nếu nhãn thật nằm trong 5 lớp có xác suất cao nhất. Lý do: nhiều ảnh có hơn một vật, và 1 000 lớp có những lớp rất gần nhau (nhiều giống chó).
Tích chập trên một kênh
Phần tiêu đề “Tích chập trên một kênh”Lấy ảnh một kênh và kernel cỡ . Giá trị ở ô của bản đồ đặc trưng là:
Đọc bằng lời: đặt góc trên trái của kernel lên ô của ảnh, nhân từng cặp ô chồng lên nhau, cộng cả tích, cộng thêm hệ số tự do . Chỉ số đếm từ 0, giống NumPy, để khớp với ô code bên dưới.
Lấy kernel cạnh dọc
Nó lấy tổng cột trái trừ tổng cột phải của cửa sổ. Vùng phẳng (hai cột bằng nhau) cho 0. Chỗ tối bên trái, sáng bên phải cho số âm; chỗ sáng bên trái, tối bên phải cho số dương. Vậy bản đồ đặc trưng “sáng lên” ở các cạnh dọc, và dấu cho biết chiều của cạnh.
Trong toán, phép trên là tương quan chéo (cross-correlation): tích chập “đúng nghĩa” lật kernel trước khi trượt. Thư viện học sâu không lật nhưng vẫn gọi là tích chập; vì kernel được học, điều này không ảnh hưởng gì.
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”Ảnh màu có kênh. Khi đó mỗi kernel có cỡ : nó phủ cùng lúc cả ba kênh, và tổng ở công thức trên chạy thêm qua các kênh. Mỗi kernel vẫn cho một bản đồ đặc trưng. Một lớp có kernel cho bản đồ, tức đầu ra có kênh.
Đếm tham số: mỗi kernel có trọng số và một hệ số tự do, có kernel, nên
Con số này không phụ thuộc cỡ ảnh : cùng các kernel đó được trượt trên ảnh to hay nhỏ.
So với lớp kết nối đầy đủ: biến ảnh thành 16 bản đồ cùng cỡ . Kết nối đầy đủ nối mỗi số ra với mọi số vào, cần trọng số. Tích chập chỉ cần trọng số (ô code thứ hai in hai số này), ít hơn khoảng 116 nghìn lần. Hai quan sát ở phần Trực giác chính là hai nguồn tiết kiệm: cục bộ (mỗi số ra chỉ nhìn 27 số vào thay vì 3 072) và chia sẻ trọng số (weight sharing: mọi vị trí dùng chung một kernel).
Chia sẻ trọng số còn cho một tính chất nữa: dịch ảnh đi bao nhiêu thì bản đồ đặc trưng dịch đi bấy nhiêu. Mẫu nào cũng được nhận ra dù nằm ở đâu.
Kích thước đầu ra
Phần tiêu đề “Kích thước đầu ra”Hai tham số nữa điều khiển việc trượt. Padding (đệm) : thêm hàng và cột số 0 quanh ảnh. Stride (bước nhảy) : mỗi lần kernel dời đi ô thay vì 1.
Theo một chiều, ảnh sau khi đệm dài . Mép trái của cửa sổ đứng được ở các vị trí , miễn là cửa sổ còn nằm trọn trong ảnh, tức mép trái không vượt quá . Số vị trí như vậy là số bước nằm vừa trong đoạn đó, cộng thêm vị trí 0:
Dấu (làm tròn xuống) có vì bước cuối có thể bị hụt: phần thừa ở mép bị bỏ qua. Chiều rộng tính y như vậy. Với ảnh và :
| Padding | Stride | Cỡ đầu ra |
|---|---|---|
| 0 | 1 | |
| 1 | 1 | (giữ nguyên cỡ) |
| 0 | 2 | |
| 1 | 2 |
Pooling dùng đúng công thức này: max pooling với , cho khi chẵn. VGG dùng , , cho mọi lớp tích chập để giữ nguyên cỡ, rồi để pooling lo việc thu nhỏ: .
Trường tiếp nhận
Phần tiêu đề “Trường tiếp nhận”Một ô của bản đồ đặc trưng “thấy” bao nhiêu điểm ảnh gốc? Vùng đó gọi là trường tiếp nhận. Một lớp : mỗi ô thấy điểm ảnh. Xếp thêm một lớp nữa: mỗi ô của lớp thứ hai nhìn ô của lớp thứ nhất, các ô này chồng lên nhau và mỗi ô nhìn điểm ảnh, nên tổng cộng thấy . Mỗi lớp , stride 1, nới trường tiếp nhận thêm :
Có stride thì mỗi bước sau nới rộng hơn, vì một ô ở lớp sau ứng với nhiều điểm ảnh hơn. Với lớp thứ có kernel và stride :
Tích các stride đứng trước cho biết một bước ở lớp dài bao nhiêu điểm ảnh gốc. Ví dụ CNN nhỏ ở ô PyTorch: tích chập (trường tiếp nhận 3), max pooling stride 2 (thêm , thành 4), tích chập sau một stride 2 (thêm , thành 8), pooling nữa (thêm , thành 10). Mỗi ô của bản đồ cuối thấy vùng , rộng hơn cả ảnh (phần thừa rơi vào vùng đệm). Đó là cách mạng sâu “nhìn” được cả vật lớn dù mỗi kernel chỉ nhỏ .
AlexNet (2012): mạng sâu trên GPU
Phần tiêu đề “AlexNet (2012): mạng sâu trên GPU”AlexNet (Krizhevsky, Sutskever, Hinton, 2012) có 5 lớp tích chập và 3 lớp kết nối đầy đủ, khoảng 60 triệu tham số. Lớp đầu dùng 96 kernel với stride 4 trên ảnh . Ở ILSVRC-2012, một biến thể của nó đạt lỗi top-5 15.3%, trong khi đội về nhì đạt 26.2%.
Theo bài báo, khác biệt đến từ ReLU thay cho tanh (học nhanh hơn nhiều lần), dropout và tăng cường dữ liệu (data augmentation: cắt ngẫu nhiên, lật ngang) để bớt overfit, và hai GPU GTX 580 chạy 5 tới 6 ngày. Phần lớn tham số nằm ở các lớp kết nối đầy đủ cuối, đúng như phép đếm ở trên dự đoán: bản torchvision có khoảng 2.5 triệu tham số ở phần tích chập và 58.6 triệu ở phần kết nối đầy đủ.
VGG (2014): chỉ dùng kernel
Phần tiêu đề “VGG (2014): chỉ dùng kernel 3×33 \times 33×3”VGG (Simonyan và Zisserman, 2014, đăng ở ICLR 2015) hỏi: nếu chỉ dùng kernel nhỏ nhất có trái, phải, trên, dưới, tức , rồi xếp thật nhiều lớp thì sao? Mạng có 16 tới 19 lớp có trọng số, mọi tích chập , stride 1, padding 1, thêm 5 lớp max pooling. VGG về nhất phần định vị và nhì phần phân loại ở ILSVRC 2014.
Vì sao ba lớp tốt hơn một lớp ? Theo công thức trường tiếp nhận, ba lớp cũng thấy điểm mỗi chiều. Nhưng với kênh vào và ra:
Lớp tốn hơn 81%. Thêm nữa, ba lớp có ba ReLU xen giữa, nên hàm mà chồng lớp biểu diễn được “uốn” nhiều hơn một lớp. Cái giá: VGG-16 có khoảng 138 triệu tham số (phần lớn, lại, ở các lớp kết nối đầy đủ) và tốn khoảng 15.3 tỉ phép nhân-cộng mỗi ảnh.
ResNet (2015–2016): khối dư
Phần tiêu đề “ResNet (2015–2016): khối dư”Nếu sâu hơn là tốt hơn, cứ xếp thêm lớp? He và cộng sự (2016) thấy điều ngược lại. Trên CIFAR-10, mạng thường 56 lớp có lỗi trên chính tập huấn luyện cao hơn mạng 20 lớp. Đây không phải overfitting (overfitting thì lỗi huấn luyện thấp, lỗi kiểm tra cao). Tác giả gọi đó là vấn đề suy giảm (degradation).
Lập luận của họ: lấy mạng nông đã học xong, thêm các lớp làm ánh xạ đồng nhất (identity mapping, ra đúng bằng vào). Mạng sâu này cho đúng kết quả của mạng nông. Vậy mạng sâu hơn có ít nhất một nghiệm không tệ hơn. Thế mà gradient descent không tìm ra: học đúng ánh xạ đồng nhất qua vài lớp phi tuyến hóa ra khó.
Cách sửa: gọi là hàm mà vài lớp liên tiếp cần học. Thay vì học thẳng , cho các lớp học phần dư , rồi cộng lại đầu vào qua một kết nối tắt (residual connection, shortcut):
Nếu ánh xạ đồng nhất là tốt nhất, các lớp chỉ cần đẩy trọng số về gần 0 để , việc dễ hơn nhiều. Thêm khối không còn làm mạng tệ đi. Đường tắt đồng nhất không thêm tham số nào và gần như không tốn phép tính (chỉ một phép cộng). Khi số kênh hay cỡ bản đồ đổi, đường tắt dùng một tích chập để khớp chiều.
Hình dưới vẽ một khối dư. Hãy nhìn đường tắt vòng qua hai lớp tích chập và gặp lại nhánh chính ở phép cộng.
Một cách nhìn bổ sung, từ bài báo tiếp theo của cùng nhóm (He và cộng sự, 2016, “Identity Mappings”): đạo hàm của theo chính nó là ma trận đơn vị, nên gradient có một đường đi thẳng qua các phép cộng từ lớp cuối về lớp đầu.
Mạng 50 lớp trở lên dùng khối “nút cổ chai” (bottleneck) : hạ số kênh, tích chập, rồi nâng lại, cho rẻ. ResNet-152 có 152 lớp, sâu gấp 8 lần VGG nhưng chỉ tốn 11.3 tỉ phép nhân-cộng, ít hơn VGG-16. Một tổ hợp (ensemble) các ResNet đạt lỗi top-5 3.57% và đứng nhất phần phân loại ILSVRC 2015.
MobileNet (2017): tách tích chập làm hai
Phần tiêu đề “MobileNet (2017): tách tích chập làm hai”Đến đây mạng đã chính xác, nhưng quá nặng cho điện thoại. MobileNet (Howard và cộng sự, 2017) thay mỗi lớp tích chập thường bằng tích chập tách theo chiều sâu (depthwise separable convolution), gồm hai bước:
- Depthwise: mỗi kênh vào có một kernel riêng, chỉ lọc trong kênh đó. Lọc không gian, không trộn kênh.
- Pointwise: một tích chập trộn kênh thành kênh. Trộn kênh, không nhìn hàng xóm.
Tích chập thường làm cả hai việc trong một bước. Đếm phép nhân-cộng trên bản đồ đầu ra cỡ : mỗi số ra của tích chập thường cần phép, có số ra; còn hai bước tách thì lần lượt cần phép cho mỗi số trong số, và phép cho mỗi số trong số:
Chia dòng dưới cho dòng trên, triệt tiêu, còn lại:
Với và cỡ hàng trăm, số hạng rất nhỏ, tỉ lệ gần : ít hơn 8 tới 9 lần. Ví dụ trong bài báo, cũng là ô code thứ hai: bản đồ , 512 kênh vào và ra, tích chập thường cần 462.4 triệu phép nhân-cộng, bản tách chỉ 52.3 triệu. Số tham số giảm cùng tỉ lệ (bỏ ở cả hai dòng). Bản MobileNet đầy đủ có 4.2 triệu tham số, 569 triệu phép nhân-cộng, độ chính xác top-1 70.6% trên ImageNet. Hai siêu tham số, hệ số độ rộng (nhân số kênh) và hệ số độ phân giải (nhân cỡ ảnh), cho bạn chọn mạng nhỏ hơn nữa.
Các bản sau, MobileNetV2 (Sandler và cộng sự, 2018) và MobileNetV3 (Howard và cộng sự, 2019), thêm kết nối tắt và tìm kiến trúc tự động.
EfficientNet (2019): co giãn cả ba chiều cùng lúc
Phần tiêu đề “EfficientNet (2019): co giãn cả ba chiều cùng lúc”Có thêm tài nguyên tính toán thì làm mạng to hơn theo chiều nào? Sâu hơn (thêm lớp, độ sâu ), rộng hơn (thêm kênh, độ rộng ), hay cho ảnh to hơn (độ phân giải )? Tan và Le (2019) nhận xét: số phép tính tỉ lệ với , nhưng với và . Gấp đôi số kênh thì cả lẫn gấp đôi; gấp đôi cạnh ảnh thì gấp 4.
Họ thấy tăng riêng một chiều thì lợi ích chóng bão hòa, và đề xuất co giãn kết hợp (compound scaling) bằng một hệ số duy nhất:
Ràng buộc làm số phép tính, tỉ lệ với , tăng khoảng : mỗi bậc gấp đôi chi phí. Các hằng số tìm bằng dò lưới nhỏ trên mạng gốc: , , (tích ).
Mạng gốc EfficientNet-B0 được tìm bằng tìm kiếm kiến trúc tự động, dùng khối của MobileNetV2 (MBConv) thêm squeeze-and-excitation. B0 có 5.3 triệu tham số, 0.39 tỉ phép tính, top-1 77.1%. Tăng cho B1 tới B7; theo bài báo, B7 đạt top-1 84.3%, nhỏ hơn 8.4 lần và nhanh hơn 6.1 lần so với mạng tích chập tốt nhất trước đó.
Dùng lại mạng đã huấn luyện
Phần tiêu đề “Dùng lại mạng đã huấn luyện”Bạn hiếm khi huấn luyện các mạng này từ đầu. Học chuyển giao (transfer learning) lấy một mạng đã học trên ImageNet, thay lớp phân loại cuối bằng lớp mới có số lớp của bài toán của bạn. Có hai cách hay dùng (theo hướng dẫn transfer learning của PyTorch): đóng băng (frozen) mọi lớp tích chập, chỉ học lớp cuối; hoặc fine-tune cả mạng với tốc độ học nhỏ. Lý do nó hiệu quả: các lớp đầu học cạnh, màu, kết cấu, những thứ chung cho hầu hết ảnh.
Giới hạn
Phần tiêu đề “Giới hạn”- Tích chập nhìn cục bộ. Quan hệ giữa hai vùng xa nhau chỉ có ở lớp sâu. Bài Phân loại ảnh với Transformer cho mọi patch nhìn nhau ngay từ lớp đầu.
- Đếm tham số không phải đo tốc độ. MobileNet ít phép tính, nhưng tốc độ thật còn tùy phần cứng và thư viện.
- Bài chỉ phân loại cả ảnh. Gán nhãn cho từng điểm ảnh thuộc bài Semantic Segmentation; làm giàu dữ liệu ảnh thuộc bài Image Augmentation.
Minh họa
Phần tiêu đề “Minh họa”Minh họa dưới đây cho bạn tự trượt một kernel trên một chữ số . Bên trái là ảnh với khung cửa sổ, bên phải là bản đồ đặc trưng (, ). Bên dưới là dòng phép tính: chín tích cộng lại thành đúng số ở ô đang sáng. Hãy thử:
- Chọn kernel đồng nhất (chỉ ô giữa bằng 1): mỗi ô của bản đồ đặc trưng bằng đúng điểm ảnh ở giữa cửa sổ, nên bản đồ là ảnh gốc bị cắt mất viền.
- Chọn kernel cạnh dọc, đặt cửa sổ ở vùng nền trống: chín tích đều bằng 0, ô ra bằng 0.
- Dời cửa sổ sang trái và phải một nét dọc của chữ số: bên này nét ô ra âm, bên kia dương, giữa nét gần 0.
- Đổi sang kernel cạnh ngang ở cùng vị trí: nét dọc gần như biến mất, các nét ngang hiện lên.
Bảng dưới đặt năm mạng cạnh nhau. Cột “Tham số” là số đếm thật bằng torchvision ở ô PyTorch thứ hai (bản ImageNet, 1 000 lớp). AlexNet trong torchvision theo bản “One weird trick” (Krizhevsky, 2014), hơi khác bản 2012. MobileNet bản đầu không có trong torchvision nên bảng ghi số của bài báo.
| Mạng | Năm | Ý chính | Tham số |
|---|---|---|---|
| AlexNet | 2012 | Mạng sâu trên GPU, ReLU, dropout, tăng cường dữ liệu | 61.1M |
| VGG-16 | 2014 | Chỉ kernel xếp chồng, 16 lớp có trọng số | 138.4M |
| ResNet-18 / ResNet-50 | 2015 | Khối dư , huấn luyện được mạng hàng trăm lớp | 11.7M / 25.6M |
| MobileNet | 2017 | Tích chập tách theo chiều sâu, ít hơn 8 tới 9 lần phép tính | 4.2M (bài báo) |
| MobileNetV2 / V3-Small | 2018 / 2019 | Thêm kết nối tắt, tìm kiến trúc tự động | 3.5M / 2.5M |
| EfficientNet-B0 | 2019 | Co giãn kết hợp độ sâu, độ rộng, độ phân giải | 5.3M |
Cột năm là năm bài báo đầu tiên lên arXiv (ResNet: tháng 12/2015, đăng ở CVPR 2016). Hãy nhìn cột cuối: sau VGG, mạng chính xác hơn nhưng nhỏ đi, vì các ý tưởng về sau chủ yếu tiết kiệm tham số và phép tính.
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 11/2026: hai ô NumPy với Python 3.14, NumPy 2.4, SciPy 1.18 và scikit-learn 1.8 (bản có trong Pyodide); hai ô PyTorch với Python 3.14, PyTorch 2.15 và torchvision 0.29 trên CPU. Hai ô NumPy chạy ngay trên trang, hai ô PyTorch chạy trên máy bạn. Các ô độc lập, không dùng biến của nhau. Ô đầu lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm SciPy và scikit-learn.
Tích chập viết tay bằng NumPy
Phần tiêu đề “Tích chập viết tay bằng NumPy”Ô dưới lấy ảnh chữ số 1 đầu tiên trong load_digits của scikit-learn (ảnh , giá trị 0 tới 16, chia 16 để về đoạn ), trượt kernel cạnh dọc bằng hai vòng lặp đúng như công thức, rồi so với scipy.signal.correlate2d. Cuối cùng in cỡ đầu ra theo công thức cho vài cặp .
Tích chập 2D trên một chữ số 8×8
import numpy as npfrom scipy.signal import correlate2d, convolve2dfrom sklearn.datasets import load_digits
# Ảnh chữ số 8x8 đầu tiên có nhãn 1, đưa giá trị về đoạn [0, 1]digits = load_digits()idx = int(np.flatnonzero(digits.target == 1)[0])X = digits.images[idx] / 16.0
# Kernel cạnh dọc: cột trái dương, cột phải âmK = np.array([[1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, -1.0]])
def out_size(n, k, p, s): # Công thức kích thước đầu ra theo một chiều return (n + 2 * p - k) // s + 1
def conv2d(X, K): # Tích chập viết tay: padding 0, stride 1 k = K.shape[0] H = out_size(X.shape[0], k, 0, 1) W = out_size(X.shape[1], k, 0, 1) out = np.zeros((H, W)) for i in range(H): for j in range(W): # Tổng tích từng ô của cửa sổ k x k với kernel out[i, j] = np.sum(X[i:i + k, j:j + k] * K) return out
F = conv2d(X, K)print("label:", digits.target[idx], "image:", X.shape)print(np.round(X, 1))print("feature map:", F.shape)print(np.round(F, 2))print("same as scipy correlate2d:", np.allclose(F, correlate2d(X, K, mode="valid")))print("same as convolve2d (flipped):", np.allclose(F, convolve2d(X, K, mode="valid")))for p, s in [(0, 1), (1, 1), (0, 2), (1, 2)]: print(f"k=3 p={p} s={s}: {out_size(8, 3, p, s)}x{out_size(8, 3, p, s)}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Ảnh và
feature map: (6, 6): nét dọc của chữ số 1 nằm ở cột 3 và 4 (đếm từ 0); bản đồ có cỡ . - Các cột của bản đồ: cột 1 và 2 âm (khoảng tới ): cửa sổ có nền tối bên trái, nét sáng bên phải. Cột 3 và 4 dương (tới ): mép phải của nét. Kernel tìm ra hai cạnh của nét dọc, và dấu cho biết cạnh nào.
same as scipy correlate2d: True: vòng lặp tay trùng với tương quan chéo của SciPy.same as convolve2d (flipped): False:convolve2dlật kernel trước khi trượt (tích chập đúng nghĩa toán), nên ra kết quả khác. Với kernel này, lật trái phải chỉ đổi dấu, nên bản đồ ra đổi dấu toàn bộ.- Bốn dòng cuối: khớp bảng kích thước đầu ra ở mục Lý thuyết.
Đếm tham số và phép tính
Phần tiêu đề “Đếm tham số và phép tính”Ô thứ hai chỉ dùng Python thuần. Nó làm lại ba phép đếm của mục Lý thuyết: lớp kết nối đầy đủ so với tích chập, tích chập thường so với tách theo chiều sâu (lớp của MobileNet), và ba lớp so với một lớp kèm trường tiếp nhận. Các phép đếm bỏ qua hệ số tự do cho gọn.
Đếm tham số: kết nối đầy đủ, tích chập, depthwise separable
# Đếm tham số và phép nhân-cộng (bỏ qua bias cho gọn)
def dense(h, w, c_in, c_out): # Lớp kết nối đầy đủ: mỗi điểm ra nối với mọi điểm vào return (h * w * c_in) * (h * w * c_out)
def conv(k, c_in, c_out): # Tích chập thường: c_out kernel, mỗi kernel k x k x c_in return k * k * c_in * c_out
def separable(k, c_in, c_out): # Depthwise (một kernel k x k mỗi kênh) + pointwise 1x1 return k * k * c_in + c_in * c_out
# 1) Ảnh 32x32x3 ra 16 bản đồ đặc trưng cùng cỡprint(f"dense 32x32x3 -> 32x32x16: {dense(32, 32, 3, 16):,}")print(f"conv 3x3, 3 -> 16: {conv(3, 3, 16):,}")
# 2) Một lớp của MobileNet: bản đồ 14x14, 512 kênh vào và rahw = 14 * 14std, sep = conv(3, 512, 512), separable(3, 512, 512)print(f"standard: {std:,} params, {std * hw / 1e6:.1f}M mult-adds")print(f"separable: {sep:,} params, {sep * hw / 1e6:.1f}M mult-adds")print(f"ratio: {sep / std:.4f} vs 1/N + 1/k^2 = {1 / 512 + 1 / 9:.4f}")
# 3) Ba lớp 3x3 xếp chồng so với một lớp 7x7, C kênh vào và raC = 64print(f"three 3x3: {3 * conv(3, C, C):,} one 7x7: {conv(7, C, C):,}")rf = 1for layer in range(1, 4): rf += 3 - 1 # mỗi lớp 3x3, stride 1, nới trường nhìn thêm 2 print(f"after {layer} conv 3x3: receptive field {rf}x{rf}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- 50 331 648 so với 432: cùng biến ảnh thành 16 kênh , tích chập ít hơn khoảng 116 nghìn lần.
- 462.4M và 52.3M phép nhân-cộng: trùng với bảng 3 trong bài báo MobileNet. Tỉ lệ 0.1131 khớp đúng , tức ít hơn khoảng 8.8 lần.
- 110 592 so với 200 704: với , đúng và của VGG. Ba dòng cuối cho trường tiếp nhận : ba lớp thấy bằng một lớp .
CNN nhỏ học trên ảnh chữ số
Phần tiêu đề “CNN nhỏ học trên ảnh chữ số”File dưới huấn luyện một CNN hai khối tích chập trên 1 797 ảnh chữ số của load_digits, chia 80% để học và 20% để kiểm tra. Mỗi khối là tích chập (padding 1, giữ cỡ), ReLU và max pooling : . Cuối cùng là một lớp kết nối đầy đủ ra 10 logit. Không cần tải dữ liệu: bộ ảnh đi kèm scikit-learn.
Chạy trên máy bạn
CNN hai lớp tích chập trên load_digits
- Cần cài
pip install torch scikit-learn- Phần cứng
- CPU là đủ, không cần GPU. Dưới một phút trên máy phát triển; nạp PyTorch có thể mất thêm vài chục giây
import torchfrom torch import nnfrom sklearn.datasets import load_digitsfrom sklearn.model_selection import train_test_split
torch.manual_seed(0)
# 1 797 ảnh chữ số 8x8, giá trị đưa về [0, 1], thêm trục kênhdigits = load_digits()X = torch.tensor(digits.images / 16.0, dtype=torch.float32).unsqueeze(1)y = torch.tensor(digits.target)X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, random_state=0, stratify=y)
# Hai khối tích chập: 8x8 -> 4x4 -> 2x2, rồi một lớp phân loạimodel = nn.Sequential( nn.Conv2d(1, 8, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(8, 16, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(16 * 2 * 2, 10),)n_params = sum(p.numel() for p in model.parameters())print("input:", tuple(X_tr.shape), "params:", n_params)print("feature maps:", tuple(model[:6](X_tr[:1]).shape))
opt = torch.optim.Adam(model.parameters(), lr=0.005)loss_fn = nn.CrossEntropyLoss()for epoch in range(1, 31): model.train() perm = torch.randperm(len(X_tr)) total = 0.0 for start in range(0, len(X_tr), 64): batch = perm[start:start + 64] loss = loss_fn(model(X_tr[batch]), y_tr[batch]) opt.zero_grad() loss.backward() # lan truyền ngược tới từng ô của kernel opt.step() total += loss.item() * len(batch) if epoch % 10 == 0: model.eval() with torch.no_grad(): acc = (model(X_te).argmax(1) == y_te).float().mean() print(f"epoch {epoch} train loss {total / len(X_tr):.3f} " f"test acc {acc.item():.3f}")Kết quả mẫu
Lấy từ một lần chạy thật. Câu trả lời của mô hình mỗi lần một khác.
input: (1437, 1, 8, 8) params: 1898feature maps: (1, 16, 2, 2)epoch 10 train loss 0.104 test acc 0.961epoch 20 train loss 0.037 test acc 0.961epoch 30 train loss 0.014 test acc 0.975Đọc kết quả:
params: 1898: lớp tích chập đầu , lớp thứ hai , lớp cuối . Tổng 1 898, đúng công thức đếm.feature maps: (1, 16, 2, 2): sau hai lần pooling, mỗi ảnh còn 16 bản đồ , tức 64 số vào lớp cuối. Mỗi ô của bản đồ này có trường tiếp nhận , bao trọn ảnh.test acc 0.975: sau 30 epoch, mạng chưa tới 2 000 tham số đoán đúng 97.5% ảnh chưa thấy. Số này có thể lệch chút ít trên máy hoặc bản PyTorch khác.
Ở ô NumPy bạn tự đặt kernel cạnh dọc; ở đây loss.backward() tính gradient của theo từng ô của mọi kernel, và mạng tự học ra kernel của mình.
Đếm tham số các mạng trong torchvision
Phần tiêu đề “Đếm tham số các mạng trong torchvision”File cuối dựng bảy mạng của bảng ở mục Minh họa với weights=None: chỉ dựng kiến trúc với trọng số ngẫu nhiên, không tải gì từ mạng Internet. Nếu muốn trọng số đã học trên ImageNet để làm transfer learning, bạn đổi weights=None thành weights="DEFAULT", và torchvision sẽ tải tệp trọng số về.
Chạy trên máy bạn
Số tham số của bảy CNN trong torchvision
- Cần cài
pip install torch torchvision- Phần cứng
- CPU là đủ, cần khoảng 1 GB RAM trống cho VGG-16. Vài giây tới vài phút, phần lớn là dựng VGG-16
from torchvision import models
# weights=None: chỉ dựng kiến trúc, trọng số ngẫu nhiên, không tải gìnames = ["alexnet", "vgg16", "resnet18", "resnet50", "mobilenet_v2", "mobilenet_v3_small", "efficientnet_b0"]for name in names: model = models.get_model(name, weights=None) n = sum(p.numel() for p in model.parameters()) print(f"{name:20s} {n / 1e6:6.1f}M params")Kết quả mẫu
Lấy từ một lần chạy thật.
alexnet 61.1M paramsvgg16 138.4M paramsresnet18 11.7M paramsresnet50 25.6M paramsmobilenet_v2 3.5M paramsmobilenet_v3_small 2.5M paramsefficientnet_b0 5.3M paramsVGG-16 nặng gấp hơn 10 lần ResNet-18 dù nông hơn, vì ba lớp kết nối đầy đủ cuối của nó chiếm khoảng 124 triệu tham số. ResNet bỏ chúng, thay bằng phép lấy trung bình toàn cục (global average pooling) trên mỗi bản đồ đặc trưng rồi một lớp kết nối đầy đủ duy nhất. EfficientNet-B0 ra 5.3 triệu, khớp số trong bài báo.
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:
- Tích chập trượt một kernel nhỏ trên ảnh: nhìn cục bộ và dùng chung trọng số, nên một lớp chỉ có tham số, không phụ thuộc cỡ ảnh, và mẫu được nhận ra ở mọi vị trí.
- Cỡ đầu ra mỗi chiều là . Xếp chồng lớp nhỏ thì trường tiếp nhận lớn dần, khi stride bằng 1.
- AlexNet (2012) đưa CNN sâu lên GPU; VGG (2014) xếp chồng kernel ; ResNet (2015–2016) cho khối học phần dư nên huấn luyện được mạng rất sâu.
- MobileNet (2017) tách tích chập thành depthwise và pointwise, chi phí còn khoảng ; EfficientNet (2019) co giãn độ sâu, độ rộng, độ phân giải cùng lúc theo một hệ số .
Đọc thêm:
- Zhang, A., Lipton, Z. C., Li, M., Smola, A. J. Dive into Deep Learning, chương 8 “Modern Convolutional Neural Networks”. Sách mở, có code cho AlexNet, VGG, ResNet và nhiều mạng khác theo đúng thứ tự lịch sử.
- Stanford CS231n. Convolutional Neural Networks for Visual Recognition: Convolutional Networks. Ghi chú bài giảng về công thức kích thước, chia sẻ trọng số và phân tích từng lớp của VGG. Ghi chú dùng cho cỡ kernel và , cho stride, padding.
- Krizhevsky, A., Sutskever, I., Hinton, G. E. (2012). ImageNet Classification with Deep Convolutional Neural Networks, NeurIPS 2012; và Simonyan, K., Zisserman, A. (2014). Very Deep Convolutional Networks for Large-Scale Image Recognition, arXiv:1409.1556, mục 2.3. Hai bài báo mở đầu; mục 2.3 của VGG có lập luận ba lớp .
- He, K., Zhang, X., Ren, S., Sun, J. (2016). Deep Residual Learning for Image Recognition, arXiv:1512.03385, mục 1 và 3; và cùng nhóm tác giả (2016). Identity Mappings in Deep Residual Networks, arXiv:1603.05027. Vấn đề suy giảm, khối dư, và vì sao đường tắt đồng nhất cho tín hiệu đi thẳng.
- Howard, A. G. và cộng sự (2017). MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications, arXiv:1704.04861, mục 3.1; và Tan, M., Le, Q. V. (2019). EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks, arXiv:1905.11946, mục 3. Phép đếm của depthwise separable và công thức co giãn kết hợp. Danh sách mạng có sẵn: tài liệu torchvision 0.29.
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