Bỏ qua để đến nội dung

Các kiến trúc CNN hiện đại: LeNet, AlexNet, VGG, GoogLeNet, ResNet, Xception, SENet, YOLO

Sau bài này, bạn sẽ:

  • Kể được dòng thời gian từ LeNet-5 (1998) tới SENet (2018), và nói được mỗi kiến trúc thêm một ý tưởng nào để giải quyết vấn đề gì.
  • Tự tính được vì sao hai lớp 3×33 \times 3 rẻ hơn một lớp 5×55 \times 5, vì sao tích chập 1×1 (1×1 convolution) và tích chập tách theo chiều sâu (depthwise separable convolution) cắt được nhiều tham số.
  • Giải thích được kết nối tắt (residual connection) của ResNet, và vì sao nó giúp huấn luyện mạng hàng trăm lớp.
  • Hiểu ý tưởng phát hiện vật thể một lượt của YOLO: lưới, hộp, độ tin cậy, IoU và NMS, và cài đặt được IoU, NMS bằng NumPy.

Từ năm 2010, cuộc thi ImageNet (ILSVRC) cho các đội khoảng 1.2 triệu ảnh, 1000 lớp, và chấm lỗi top-5: tỉ lệ ảnh mà nhãn đúng không nằm trong 5 dự đoán đầu của mô hình. Hãy nhìn lỗi của đội thắng (hoặc đứng đầu) qua vài năm:

NămMô hìnhLỗi top-5
2012AlexNet15.3% (đội thứ hai: 26.2%)
2014VGG (hạng hai)7.3%
2014GoogLeNet (hạng nhất)6.67%
2015ResNet3.57%
2017SENet2.251%

Lỗi giảm khoảng 7 lần trong 5 năm. Điều đáng chú ý: phép tích chập không đổi. Mọi mô hình trong bảng vẫn trượt bộ lọc trên ảnh như bài Mạng nơ-ron tích chập. Cái đổi là cách xếp các lớp tích chập. Mỗi kiến trúc gặp một bức tường, rồi tìm ra một ý tưởng để vượt qua:

  • Mạng lớn huấn luyện quá chậm, lại học thuộc lòng dữ liệu. (AlexNet)
  • Nên dùng bộ lọc cỡ nào: 3×33 \times 3, 5×55 \times 5, 11×1111 \times 11? (VGG)
  • Muốn dùng mọi cỡ cùng lúc mà không đắt. (GoogLeNet)
  • Thêm lớp thì lỗi huấn luyện lại tăng. (ResNet)
  • Một bộ lọc vừa nhìn không gian vừa trộn kênh, có lãng phí không? (Xception)
  • Kênh nào quan trọng với ảnh này? (SENet)

Từ “ảnh này là gì” tới “vật nào ở đâu”

Phần tiêu đề “Từ “ảnh này là gì” tới “vật nào ở đâu””

Phân loại trả lời một câu: ảnh có con mèo. Một chiếc xe tự lái cần nhiều hơn: có ba người đi bộ và một xe máy, ở những vị trí này, và phải trả lời trong vài chục mili giây. Đó là bài toán phát hiện vật thể (object detection). YOLO dùng chính một CNN để làm việc đó chỉ trong một lần chạy qua mạng.

Bài toán của bài này là: mỗi kiến trúc nổi tiếng đã thêm ý tưởng nào vào khung “tích chập, pooling, lớp cuối” của CNN, ý tưởng đó giải quyết vấn đề gì, và tốn (hoặc tiết kiệm) bao nhiêu tham số.

Mọi mạng trong bài (trừ YOLO ở phần đầu ra) có cùng một dáng:

  1. Thân (backbone): nhiều lớp tích chập. Càng vào sâu, ảnh càng nhỏ lại (nhờ pooling hoặc stride 2) còn số kênh càng nhiều. Lớp đầu bắt cạnh, lớp sau ghép cạnh thành hoa văn, rồi thành bộ phận của vật.
  2. Đầu (head): biến bản đồ đặc trưng cuối thành 1000 logit, rồi softmax.

Phần còn lại của mục này đi theo dòng thời gian. Mỗi mục nói vấn đề trước, ý tưởng sau.

LeCun và cộng sự (1998) dùng LeNet-5 để đọc chữ số viết tay, và hệ thống của họ đã được dùng để đọc séc ngân hàng. Ảnh vào 32×3232 \times 32, đi qua:

  • tích chập 5×55 \times 5 ra 6 kênh, rồi lấy mẫu xuống (subsampling, giống pooling trung bình);
  • tích chập 5×55 \times 5 ra 16 kênh, rồi lấy mẫu xuống;
  • một lớp tích chập ra 120 kênh (ảnh lúc này còn 1×11 \times 1), một lớp kết nối đầy đủ 84 nơ-ron, rồi 10 đầu ra.

Cộng số tham số từng lớp mà bài báo ghi, phần huấn luyện được có khoảng 60 nghìn. Ý tưởng của LeNet là chính cái khung tích chập → pooling, lặp lại, rồi lớp kết nối đầy đủ, huấn luyện đầu cuối bằng lan truyền ngược. Mọi mạng sau đều giữ khung này.

AlexNet (2012): lớn hơn, cùng dữ liệu lớn và GPU

Phần tiêu đề “AlexNet (2012): lớn hơn, cùng dữ liệu lớn và GPU”

Gần 15 năm sau, ý tưởng vẫn vậy, nhưng ba thứ đã khác: có ImageNet với 1.2 triệu ảnh gán nhãn, có GPU, và có vài mẹo huấn luyện. Krizhevsky, Sutskever và Hinton (2012) xây một mạng 5 lớp tích chập và 3 lớp kết nối đầy đủ, khoảng 60 triệu tham số, gấp khoảng 1000 lần LeNet. Bốn điểm làm nó chạy được:

  • ReLU thay cho tanh. ReLU không bão hòa ở phía dương, nên gradient không bị bóp nhỏ. Trong bài báo, một mạng 4 lớp dùng ReLU đạt lỗi huấn luyện 25% trên CIFAR-10 nhanh hơn khoảng 6 lần bản dùng tanh.
  • Dropout (tỉ lệ 0.5) ở hai lớp kết nối đầy đủ đầu, nơi tập trung phần lớn tham số và dễ overfitting nhất.
  • Tăng cường dữ liệu (data augmentation): cắt ngẫu nhiên các mảnh ảnh, lật ngang, đổi cường độ màu, để 1.2 triệu ảnh “nhiều” hơn.
  • Hai GPU GTX 580 (mỗi cái 3 GB) chạy song song, huấn luyện mất 5 tới 6 ngày.

Kết quả: lỗi top-5 15.3%, trong khi đội thứ hai (không dùng mạng nơ-ron sâu) là 26.2%. Đây là thời điểm học sâu chiếm lấy thị giác máy tính.

VGG (2014): chỉ dùng bộ lọc 3×3, xếp chồng

Phần tiêu đề “VGG (2014): chỉ dùng bộ lọc 3×3, xếp chồng”

AlexNet dùng bộ lọc 11×1111 \times 11 ở lớp đầu, 5×55 \times 5 ở lớp sau. Simonyan và Zisserman (2014) hỏi: nếu chỉ dùng 3×33 \times 3 và xếp nhiều lớp thì sao?

Bước 1: hai lớp 3×3 nhìn được bao xa. Một ô ở lớp thứ nhất nhìn một vùng 3×33 \times 3 của ảnh. Một ô ở lớp thứ hai nhìn 3×33 \times 3 ô của lớp thứ nhất. Hai ô lớp một nằm ở hai mép của cửa sổ đó cách nhau 2 bước, và mỗi ô lại nhìn thêm 1 điểm ảnh về mỗi phía, nên vùng ảnh tổng cộng rộng 2+3=52 + 3 = 5. Tổng quát, với stride 1, mỗi lớp k×kk \times k nới trường tiếp nhận thêm k−1k - 1:

rL=1+L(k−1)r_L = 1 + L(k - 1)

Với k=3k = 3: hai lớp cho r2=5r_2 = 5, ba lớp cho r3=7r_3 = 7. Vậy hai lớp 3×33 \times 3 “nhìn” được đúng vùng của một lớp 5×55 \times 5.

Bước 2: so số tham số. Giả sử số kênh vào và ra đều là CC, bỏ qua hệ số tự do. Một lớp k×kk \times k có k2C2k^2 C^2 trọng số. Do đó:

2⋅9C2⏟hai lớp 3×3=18C2  <  25C2⏟một lớp 5×5\underbrace{2 \cdot 9C^2}_{\text{hai lớp } 3 \times 3} = 18C^2 \;<\; \underbrace{25C^2}_{\text{một lớp } 5 \times 5}

Hai lớp nhỏ tốn 72% số tham số của một lớp lớn. Với ba lớp so với một lớp 7×77 \times 7, khoảng cách còn lớn hơn: 27C227C^2 so với 49C249C^2.

Bước 3: thêm phi tuyến. Hai lớp có hai ReLU, một lớp chỉ có một. Cùng vùng nhìn, mạng có thêm một chỗ “uốn”, nên biểu diễn được hàm phức tạp hơn.

VGG-16 và VGG-19 (16 và 19 lớp có trọng số) chỉ là các khối “vài lớp 3×33 \times 3 rồi max pooling” lặp lại, cuối cùng là ba lớp kết nối đầy đủ. Thiết kế đơn giản đến mức người ta vẫn dùng VGG làm ví dụ đầu tiên. Nhưng nó nặng: VGG-16 có khoảng 138 triệu tham số, và khoảng 124 triệu trong số đó nằm ở ba lớp kết nối đầy đủ cuối (đếm bằng torchvision).

GoogLeNet và khối Inception (2014): nhánh song song, tích chập 1×1

Phần tiêu đề “GoogLeNet và khối Inception (2014): nhánh song song, tích chập 1×1”

Cùng năm, nhóm của Szegedy (2014) đặt một câu hỏi khác: tại sao phải chọn cỡ bộ lọc? Một vật nhỏ cần 3×33 \times 3, một vật to cần 5×55 \times 5. Khối Inception chạy cùng lúc bốn nhánh trên cùng một đầu vào: tích chập 1×11 \times 1, 3×33 \times 3, 5×55 \times 5, và max pooling 3×33 \times 3. Đầu ra của bốn nhánh có cùng chiều cao và chiều rộng (nhờ padding), nên được ghép theo chiều kênh.

Cách làm ngây thơ này rất đắt. Nhánh 5×55 \times 5 trên đầu vào 192 kênh, ra 32 kênh, tốn (25⋅192+1)⋅32=153 632(25 \cdot 192 + 1) \cdot 32 = 153\,632 tham số, và phép nhân thì nhân thêm với số vị trí trên ảnh.

Tích chập 1×1 là gì. Kernel 1×11 \times 1 không nhìn hàng xóm nào. Tại mỗi vị trí (i,j)(i, j), nó lấy vectơ CinC_{\text{in}} giá trị kênh ở đúng vị trí đó và nhân với một ma trận Cout×CinC_{\text{out}} \times C_{\text{in}}. Nói cách khác, nó là một lớp kết nối đầy đủ áp lên từng điểm ảnh, dùng chung trọng số cho mọi điểm. Khi Cout<CinC_{\text{out}} < C_{\text{in}}, nó nén thông tin của nhiều kênh vào ít kênh hơn.

GoogLeNet đặt một lớp 1×11 \times 1 giảm kênh trước các lớp 3×33 \times 3 và 5×55 \times 5. Ở nhánh 5×55 \times 5 trên: giảm 192 kênh xuống 16 trước, rồi mới 5×55 \times 5 ra 32 kênh. Số tham số:

(192+1)⋅16⏟1×1: 3 088+(25⋅16+1)⋅32⏟5×5: 12 832=15 920\underbrace{(192 + 1) \cdot 16}_{1 \times 1:\ 3\,088} + \underbrace{(25 \cdot 16 + 1) \cdot 32}_{5 \times 5:\ 12\,832} = 15\,920

ít hơn khoảng 10 lần. Ô code thứ nhất tính lại các số này.

GoogLeNet có 22 lớp có tham số, và bỏ các lớp kết nối đầy đủ to ở cuối, thay bằng pooling trung bình toàn cục (global average pooling): lấy trung bình mỗi kênh trên cả ảnh, được một vectơ, rồi một lớp tuyến tính ra 1000 lớp. Kết quả: lỗi top-5 6.67%, với số tham số ít hơn AlexNet khoảng 12 lần (theo bài báo). Tên GoogLeNet là lời chào tới LeNet.

VGG cho thấy sâu hơn thì tốt hơn. Vậy cứ thêm lớp? He và cộng sự (2015) thử, và thấy một chuyện lạ.

Vấn đề: thêm lớp, lỗi huấn luyện lại tăng. Trên CIFAR-10, một mạng “thường” (plain, không có kết nối tắt) 56 lớp có lỗi trên tập huấn luyện cao hơn mạng 20 lớp. Đây không phải overfitting: overfitting làm lỗi huấn luyện thấp còn lỗi kiểm tra cao. Ở đây mạng sâu học tệ hơn ngay trên dữ liệu nó được thấy. Các tác giả gọi đó là hiện tượng thoái hóa (degradation).

Chuyện này lạ vì có một lời giải dễ: lấy mạng 20 lớp đã học, thêm 36 lớp chỉ làm phép đồng nhất (identity), tức trả lại đúng đầu vào. Mạng 56 lớp đó tốt bằng mạng 20 lớp. Vậy lời giải tồn tại, chỉ là gradient descent không tìm ra. Một chồng tích chập và ReLU khó học được phép “không làm gì”.

Ý tưởng: học phần dư. Gọi x\mathbf{x} là đầu vào của một khối vài lớp, và H(x)H(\mathbf{x}) là hàm ta muốn khối đó biểu diễn. Thay vì bắt các lớp học thẳng HH, ResNet cho chúng học phần dư (residual):

F(x)=H(x)−xF(\mathbf{x}) = H(\mathbf{x}) - \mathbf{x}

rồi cộng x\mathbf{x} trở lại qua một đường tắt (shortcut). Đầu ra của khối là:

y=g(F(x)+x)\mathbf{y} = g\big(F(\mathbf{x}) + \mathbf{x}\big)

ở đây FF là hai lớp tích chập 3×33 \times 3, mỗi lớp có BatchNorm, ReLU ở giữa, và gg là ReLU sau phép cộng. Hình ở mục Minh họa vẽ đúng khối này. Nếu phép đồng nhất là tốt nhất, các lớp chỉ cần đẩy FF về 0, tức đẩy trọng số về gần 0. Việc đó dễ hơn nhiều so với học ra phép đồng nhất từ đầu.

Vì sao gradient đi xa được. Xét trường hợp một chiều, bỏ ReLU cuối cho gọn: y=x+f(x)y = x + f(x). Đạo hàm là:

dydx=1+f′(x)\frac{dy}{dx} = 1 + f'(x)

Số hạng 1 đến từ đường tắt. Mạng thường có y=f(x)y = f(x) nên đạo hàm chỉ là f′(x)f'(x). Qua 50 khối, quy tắc dây chuyền nhân 50 đạo hàm với nhau. Nếu mỗi f′f' cỡ 0.5, mạng thường còn 0.550≈10−150.5^{50} \approx 10^{-15}: gradient tiêu biến. Với kết nối tắt, mỗi thừa số là 1+f′(x)1 + f'(x), quanh 1 khi f′f' nhỏ, nên tích không sụp về 0. Gradient có một “đường cao tốc” đi thẳng từ lớp cuối về lớp đầu. (Lập luận này đúng nhất khi đường tắt là phép đồng nhất thuần túy; He và cộng sự (2016) phân tích kỹ hơn trong bài về ánh xạ đồng nhất.)

Khi kích thước không khớp. Phép cộng F(x)+xF(\mathbf{x}) + \mathbf{x} cần hai tensor cùng kích thước. Khi một khối giảm ảnh đi một nửa (stride 2) và tăng gấp đôi số kênh, đường tắt dùng một tích chập 1×11 \times 1 stride 2 để đổi x\mathbf{x} cho khớp. Đường tắt đồng nhất thì không thêm tham số nào.

Khối nút cổ chai. ResNet-50, 101, 152 dùng khối 1×1→3×3→1×11 \times 1 \to 3 \times 3 \to 1 \times 1: lớp 1×11 \times 1 đầu giảm kênh, lớp 3×33 \times 3 làm việc trên ít kênh, lớp 1×11 \times 1 sau tăng kênh lại. Đây là đúng ý tưởng giảm kênh của Inception.

Kết quả trên ImageNet: mạng thường 34 lớp có lỗi top-1 28.54%, tệ hơn mạng thường 18 lớp (27.94%). Thêm kết nối tắt, ResNet-34 còn 25.03%, tốt hơn ResNet-18. ResNet-152 có 152 lớp mà vẫn tốn ít phép tính hơn VGG-16 (11.3 so với 15.3 tỉ FLOPs), và tổ hợp các ResNet đạt lỗi top-5 3.57%. Kết nối tắt từ đó có mặt khắp nơi, kể cả trong mọi khối Transformer (bài Transformer).

Xception (2017): tích chập tách theo chiều sâu

Phần tiêu đề “Xception (2017): tích chập tách theo chiều sâu”

Một lớp tích chập 3×33 \times 3 thường làm hai việc cùng lúc: nhìn hàng xóm trong không gian (vùng 3×33 \times 3), và trộn thông tin giữa các kênh (mỗi bộ lọc nhìn mọi kênh vào). Chollet (2017) đặt giả thuyết rằng hai việc này tách riêng được, và tách thì hiệu quả hơn.

Tích chập tách theo chiều sâu làm hai bước:

  1. Depthwise: mỗi kênh vào có một bộ lọc k×kk \times k riêng, chỉ nhìn kênh đó. Không trộn kênh.
  2. Pointwise: một tích chập 1×11 \times 1 trộn các kênh, như ở Inception.

Đếm tham số (bỏ qua hệ số tự do). Tích chập thường có k2CinCoutk^2 C_{\text{in}} C_{\text{out}}. Bản tách có k2Cink^2 C_{\text{in}} (depthwise) cộng CinCoutC_{\text{in}} C_{\text{out}} (pointwise). Chia hai số cho nhau:

k2Cin+CinCoutk2CinCout=1Cout+1k2\frac{k^2 C_{\text{in}} + C_{\text{in}} C_{\text{out}}}{k^2 C_{\text{in}} C_{\text{out}}} = \frac{1}{C_{\text{out}}} + \frac{1}{k^2}

Dòng này có được bằng cách chia từng số hạng ở tử cho mẫu: số hạng đầu còn 1/Cout1/C_{\text{out}}, số hạng sau còn 1/k21/k^2. Với k=3k = 3 và Cout=256C_{\text{out}} = 256, tỉ số là 1/256+1/9≈0.1151/256 + 1/9 \approx 0.115: bản tách tốn khoảng 8.7 lần ít hơn. Khi số kênh lớn, 1/Cout1/C_{\text{out}} gần 0, nên mức tiết kiệm tiến gần k2=9k^2 = 9 lần.

Chollet nhìn Inception theo cách này: một khối Inception chia kênh thành vài nhóm, mỗi nhóm có tích chập không gian riêng. Đẩy tới cực điểm, mỗi kênh một nhóm, ta được tích chập tách theo chiều sâu. Tên Xception nghĩa là “Extreme Inception”. Mạng có 36 lớp tích chập, chia thành 14 module, có kết nối tắt kiểu ResNet. Với khoảng 22.9 triệu tham số (Inception V3 có 23.6 triệu), Xception hơn Inception V3 một chút trên ImageNet (top-1 79.0% so với 78.2%) và hơn rõ trên một bộ dữ liệu nội bộ 350 triệu ảnh. Vì số tham số gần bằng nhau, cái lợi đến từ cách dùng tham số, không phải từ việc có nhiều hơn. Cùng ý tưởng này là nền của MobileNet, mạng cho điện thoại (bài Phân loại ảnh với CNN).

Đến đây, mọi kênh ra của một lớp được đối xử như nhau. Nhưng với ảnh một con mèo, kênh bắt “lông” quan trọng hơn kênh bắt “bánh xe”. Hu và cộng sự (2018) thêm một khối nhỏ học cách tăng hoặc giảm từng kênh tùy theo ảnh: khối squeeze-and-excitation (SE). Gọi U\mathbf{U} là đầu ra của một lớp, có CC kênh, mỗi kênh Uc\mathbf{U}_c là một ma trận H×WH \times W.

Bước 1, squeeze (ép): tóm mỗi kênh thành một số bằng trung bình toàn cục:

zc=1HW∑i=1H∑j=1WUc(i,j)z_c = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W} U_c(i, j)

Ta được vectơ z\mathbf{z} có CC thành phần, một “bản tóm tắt” ảnh này theo từng kênh.

Bước 2, excitation (kích): cho z\mathbf{z} qua một MLP nhỏ có nút cổ chai, rồi sigmoid:

s=σ(W2 ReLU(W1z))\mathbf{s} = \sigma\big(\mathbf{W}_2\, \mathrm{ReLU}(\mathbf{W}_1 \mathbf{z})\big)

W1\mathbf{W}_1 có kích thước Cr×C\tfrac{C}{r} \times C (nén CC số xuống C/rC/r), W2\mathbf{W}_2 có kích thước C×CrC \times \tfrac{C}{r} (mở lại CC số). Bài báo chọn r=16r = 16. Sigmoid đưa mỗi scs_c về khoảng (0,1)(0, 1): đó là “trọng số” của kênh cc.

Bước 3, scale (co giãn): nhân cả kênh với trọng số của nó, U~c=scUc\tilde{\mathbf{U}}_c = s_c \mathbf{U}_c.

Trọng số s\mathbf{s} thay đổi theo từng ảnh, vì z\mathbf{z} tính từ chính ảnh đó. Đây là một dạng chú ý theo kênh (channel attention), họ hàng xa của attention trong Transformer. Khối SE rất rẻ: chỉ có W1\mathbf{W}_1 và W2\mathbf{W}_2, khoảng 2C2/r2C^2/r tham số. Gắn vào mỗi khối của ResNet-50, số tham số tăng từ 25.6 lên 28.1 triệu (khoảng 10%), số phép tính gần như không đổi (3.87 so với 3.86 tỉ FLOPs), còn lỗi top-1 giảm từ 24.80% xuống 23.29%. Đội dùng SENet thắng ILSVRC 2017 với lỗi top-5 2.251%.

YOLO (2016): phát hiện vật thể trong một lượt

Phần tiêu đề “YOLO (2016): phát hiện vật thể trong một lượt”

Các mạng trên trả lời “ảnh này là gì”. Phát hiện vật thể cần thêm “ở đâu”: đầu ra là một danh sách hộp bao (bounding box), mỗi hộp có vị trí, kích thước, và nhãn lớp. Cách làm phổ biến trước đó (họ R-CNN) đề xuất khoảng hai nghìn vùng có thể chứa vật, rồi chạy bộ phân loại trên từng vùng. Chính xác, nhưng chậm.

Redmon và cộng sự (2016) đặt câu hỏi ngược lại: một CNN có thể nhìn ảnh một lần (You Only Look Once) rồi trả ra mọi hộp cùng lúc không? Ý tưởng:

Lưới. Chia ảnh thành lưới S×SS \times S ô (bài báo dùng S=7S = 7). Ô nào chứa tâm của một vật thì ô đó chịu trách nhiệm phát hiện vật ấy.

Mỗi ô dự đoán gì. Mỗi ô dự đoán BB hộp (B=2B = 2). Mỗi hộp gồm 5 số: tâm (x,y)(x, y) tính theo ô, chiều rộng và chiều cao (w,h)(w, h) tính theo cả ảnh, và một độ tin cậy (confidence). Ngoài ra mỗi ô dự đoán CC xác suất lớp (với bộ dữ liệu PASCAL VOC, C=20C = 20), dùng chung cho các hộp của ô. Vậy đầu ra của mạng là một tensor:

S×S×(5B+C)=7×7×(5⋅2+20)=7×7×30S \times S \times (5B + C) = 7 \times 7 \times (5 \cdot 2 + 20) = 7 \times 7 \times 30

Mạng có 24 lớp tích chập (nhiều lớp 1×11 \times 1 giảm kênh, học từ GoogLeNet) rồi 2 lớp kết nối đầy đủ, và lớp cuối ra thẳng tensor này. Phát hiện vật thể thành một bài hồi quy: một lần lan truyền xuôi là xong.

IoU: hai hộp trùng nhau bao nhiêu. Để biết một hộp dự đoán tốt cỡ nào, ta so nó với hộp thật bằng IoU (Intersection over Union, tỉ lệ giao trên hợp):

IoU(A,B)=diện tıˊch(A∩B)diện tıˊch(A∪B)\mathrm{IoU}(A, B) = \frac{\text{diện tích}(A \cap B)}{\text{diện tích}(A \cup B)}

IoU bằng 1 khi hai hộp trùng khít, bằng 0 khi không chạm nhau. Ví dụ: hộp AA từ (10,10)(10, 10) tới (50,50)(50, 50) và hộp BB từ (12,14)(12, 14) tới (52,54)(52, 54), mỗi hộp 40×40=160040 \times 40 = 1600. Phần giao chạy từ x=12x = 12 tới 5050 (rộng 38) và y=14y = 14 tới 5050 (cao 36), diện tích 13681368. Phần hợp là tổng hai diện tích trừ phần giao (để không đếm phần giao hai lần): 1600+1600−1368=18321600 + 1600 - 1368 = 1832. Vậy IoU=1368/1832≈0.75\mathrm{IoU} = 1368 / 1832 \approx 0.75.

Độ tin cậy của một hộp được huấn luyện để bằng Pr⁡(coˊ vật)×IoU\Pr(\text{có vật}) \times \mathrm{IoU} giữa hộp dự đoán và hộp thật: gần 0 nếu ô không chứa vật, gần IoU nếu có. Lúc dùng, điểm của hộp cho một lớp là xác suất lớp đó nhân với độ tin cậy.

NMS: bỏ hộp trùng. Một con mèo to có thể được vài ô và vài hộp cùng “nhận”. Ta được nhiều hộp chồng lên nhau quanh cùng một vật. NMS (Non-Maximum Suppression, khử phi cực đại) dọn chúng, làm riêng cho từng lớp:

  1. Sắp các hộp theo điểm giảm dần.
  2. Giữ hộp điểm cao nhất.
  3. Bỏ mọi hộp còn lại có IoU với hộp vừa giữ từ một ngưỡng trở lên (thường 0.5): chúng nhiều khả năng là cùng một vật.
  4. Lặp lại với các hộp còn sót, tới khi hết.

Với YOLO, NMS thêm 2 tới 3 điểm mAP (mean average precision, thước đo chuẩn của phát hiện vật thể, cao hơn là tốt hơn). Kết quả: YOLO chạy 45 khung hình mỗi giây với 63.4 mAP trên PASCAL VOC 2007, đủ nhanh cho video thời gian thực. Điểm yếu, theo chính bài báo: mỗi ô chỉ có 2 hộp và một lớp, nên YOLO khó với nhiều vật nhỏ đứng sát nhau, như một đàn chim, và định vị kém chính xác hơn các phương pháp hai bước. Các phiên bản sau (YOLOv2, YOLOv3 của chính Redmon, rồi nhiều bản của các nhóm khác) thêm hộp neo (anchor box) và dự đoán ở nhiều tỉ lệ ảnh để bù những điểm yếu này.

NămKiến trúcSố lớpSố tham số (xấp xỉ)Ý tưởng chính
1998LeNet-57 (không tính lớp vào)60 nghìn ¹Khung tích chập → pooling → kết nối đầy đủ
2012AlexNet8 (5 tích chập + 3 kết nối đầy đủ)61 triệu ²ReLU, dropout, tăng cường dữ liệu, GPU
2014VGG-16 / VGG-1916 / 19138 / 144 triệu ²Chỉ dùng 3×33 \times 3, xếp chồng
2014GoogLeNet226.6 triệu ²Nhánh song song, 1×11 \times 1 giảm kênh, pooling trung bình toàn cục
2015ResNet-18 / 50 / 15218 / 50 / 15211.7 / 25.6 / 60.2 triệu ²Kết nối tắt, học phần dư
2017Xception36 lớp tích chập22.9 triệu ³Tích chập tách theo chiều sâu
2018SE-ResNet-505028.1 triệu ³Khối SE: trọng số cho từng kênh
2016YOLO24 tích chập + 2 kết nối đầy đủkhông nêu ⁴Phát hiện vật thể một lượt: lưới, hộp, độ tin cậy

Nguồn của cột số tham số: ¹ cộng số tham số huấn luyện được của từng lớp trong bài báo LeNet-5; ² tự đếm bằng torchvision 0.29 (weights=None, GoogLeNet không tính hai bộ phân loại phụ dùng khi huấn luyện), khớp con số trong bài báo AlexNet (60 triệu) và VGG (138, 144 triệu); ³ bảng trong bài báo Xception và SENet; ⁴ bài báo YOLO không ghi tổng số tham số.

Hai điều nhìn ra từ bảng: số tham số không tăng theo độ sâu (ResNet-152 sâu gấp gần 10 lần VGG-16 mà ít tham số hơn), và hầu hết các ý tưởng sau VGG là để tiêu tham số khôn hơn, không phải để có nhiều hơn.

  • Bài chỉ kể ý tưởng. Huấn luyện một mạng phân loại ảnh thật, dùng lại trọng số đã học sẵn (transfer learning), và các mạng nhẹ như MobileNet, EfficientNet nằm ở bài Phân loại ảnh với CNN.
  • Số tham số chưa phải tất cả. Tốc độ thật còn phụ thuộc số phép tính (FLOPs), bộ nhớ cho kích hoạt và phần cứng. Tích chập tách theo chiều sâu ít phép nhân hơn nhiều, nhưng trên GPU không phải lúc nào cũng nhanh hơn tương ứng.
  • YOLO ở đây chỉ ở mức ý tưởng. Hàm mất mát nhiều thành phần, hộp neo và cách đo mAP chi tiết không có trong bài này.

Hình dưới vẽ một khối của ResNet-18. Hãy nhìn hai đường đi từ x\mathbf{x}: đường chính đi qua hai lớp tích chập, đường tắt đi vòng qua và gặp lại đường chính ở dấu cộng, trước ReLU cuối. Đường tắt không có lớp nào, nên không có tham số nào.

Bảng dưới là khối Inception đầu tiên của GoogLeNet (khối “3a” trong bài báo), đặt trên đầu vào 28×28×19228 \times 28 \times 192. Hãy nhìn cột thứ hai: mọi nhánh lớn đều có một lớp 1×11 \times 1 giảm kênh đứng trước.

NhánhLớp giảm kênh 1×11 \times 1Lớp chínhKênh ra
1×11 \times 1không có1×11 \times 164
3×33 \times 3192 → 963×33 \times 3128
5×55 \times 5192 → 165×55 \times 532
Poolingkhông cómax pooling 3×33 \times 3, rồi 1×11 \times 132
Ghép theo kênh256

Đầu ra là 28×28×25628 \times 28 \times 256: cùng chiều cao, chiều rộng với đầu vào, chỉ khác số kênh. Cả khối có khoảng 159 nghìn tham số (theo bảng 1 của bài báo).

Ba ô dưới đây chạy độc lập. Hai ô đầu chỉ dùng NumPy và SciPy, chạy ngay trên trang; ô thứ nhất lần đầu chạy lâu hơn một chút vì trình duyệt phải tải SciPy. Ô thứ ba cần PyTorch nên chạy trên máy bạn. Code chạy thử lần cuối tháng 11/2026, với Python 3.14, NumPy 2.4 (bản có trong Pyodide), PyTorch 2.15 và torchvision 0.29 trên CPU.

Ô dưới đếm tham số cho bốn ý tưởng của mục Lý thuyết, có tính cả hệ số tự do: hai lớp 3×33 \times 3 so với một lớp 5×55 \times 5 (256 kênh), nhánh 5×55 \times 5 của Inception có và không có 1×11 \times 1, tích chập tách theo chiều sâu so với tích chập thường, và chi phí của một khối SE. Ô cũng kiểm tra trường tiếp nhận bằng cách đặt một điểm sáng giữa ảnh, chập hai lần với kernel 3×33 \times 3, rồi đo vùng khác 0.

Đếm tham số: VGG, Inception, Xception, SENet

import numpy as np
from scipy.signal import convolve2d
# 1. Hai lớp 3x3 so với một lớp 5x5 (256 kênh)
c = 256
params_2x_3x3 = 2 * (3 * 3 * c * c + c)
params_1x_5x5 = 5 * 5 * c * c + c
print(f"2x 3x3: {params_2x_3x3:,} params | 1x 5x5: {params_1x_5x5:,} params")
# Kiểm tra trường tiếp nhận: 1 điểm sáng giữa ảnh 9x9
img = np.zeros((9, 9))
img[4, 4] = 1.0
k = np.ones((3, 3))
conv1 = convolve2d(img, k, mode="same")
conv2 = convolve2d(conv1, k, mode="same")
rf_h = np.count_nonzero(conv2.any(axis=1))
rf_w = np.count_nonzero(conv2.any(axis=0))
print(f"Receptive field (2x 3x3): {rf_h}x{rf_w}")
# 2. Nhánh 5x5 của Inception: 192 vào, 32 ra (có và không có 1x1 16 kênh)
cin, cout, cred = 192, 32, 16
direct_5x5 = 5 * 5 * cin * cout + cout
reduced_5x5 = (1 * 1 * cin * cred + cred) + (5 * 5 * cred * cout + cout)
print(f"Inception 5x5 direct: {direct_5x5:,} | reduced: {reduced_5x5:,}")
# 3. Tích chập thường so với tách theo chiều sâu (256 kênh vào/ra)
std_conv = 3 * 3 * c * c + c
depthwise = (3 * 3 * c + c) + (1 * 1 * c * c + c)
print(f"Standard 3x3: {std_conv:,} | Depthwise separable: {depthwise:,}")
# 4. Khối SE: r = 16, C = 256
r = 16
se_params = (c * (c // r) + (c // r)) + ((c // r) * c + c)
print(f"SE block (r={r}): {se_params:,} params")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Hai lớp 3×3: 1,180,160 tham số, một lớp 5×5: 1,638,656. Tỉ số khoảng 0.72, đúng 18/2518/25 (hệ số tự do gần như không đáng kể). Vùng khác 0 sau hai lần chập là 5×5: một điểm sáng ảnh hưởng tới vùng 5×55 \times 5 của đầu ra, tức mỗi ô đầu ra nhìn vùng 5×55 \times 5 của ảnh vào.
  • Nhánh 5×5 của Inception: 153,632 tham số khi làm thẳng, 15,920 khi có 1×1 giảm xuống 16 kênh, ít hơn khoảng 10 lần, khớp phép tính ở mục Lý thuyết.
  • Tích chập 3×3 thường: 590,080; bản tách theo chiều sâu: 68,352, ít hơn 8.6 lần. Công thức không tính hệ số tự do cho 1/(1/256+1/9)≈8.71/(1/256 + 1/9) \approx 8.7; hệ số tự do làm tỉ số lệch đi một chút.
  • Khối SE với r=16r = 16 chỉ tốn 8,464 tham số, bằng 1.4% của một lớp 3×33 \times 3 cùng số kênh. Đó là lý do SENet thêm được khối này vào mọi khối của ResNet mà mạng chỉ nặng thêm khoảng 10%.

Ô dưới cài đặt IoU và NMS đúng như các bước ở mục YOLO, trên 5 hộp tự đặt cho một lớp: ba hộp quanh một con mèo, hai hộp quanh một con chó. Hộp có dạng (x1,y1,x2,y2)(x_1, y_1, x_2, y_2): góc trên trái và góc dưới phải. Hàm iou tính IoU của một hộp với nhiều hộp cùng lúc; hàm nms trả về chỉ số các hộp được giữ, theo thứ tự giữ.

IoU và NMS trên 5 hộp

import numpy as np
def iou(box, boxes):
# box: (4,), boxes: (N, 4) với dạng (x1, y1, x2, y2)
x1 = np.maximum(box[0], boxes[:, 0])
y1 = np.maximum(box[1], boxes[:, 1])
x2 = np.minimum(box[2], boxes[:, 2])
y2 = np.minimum(box[3], boxes[:, 3])
inter = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
area1 = (box[2] - box[0]) * (box[3] - box[1])
area2 = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
union = area1 + area2 - inter
return inter / union
def nms(boxes, scores, threshold):
# Sắp xếp theo điểm giảm dần
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(int(i))
if order.size == 1:
break
# Tính IoU của hộp điểm cao nhất với các hộp còn lại
ious = iou(boxes[i], boxes[order[1:]])
# Giữ lại các hộp có IoU nhỏ hơn ngưỡng
order = order[1:][ious < threshold]
return keep
# 5 hộp: 3 hộp mèo (0..2), 2 hộp chó (3..4)
boxes = np.array([
[10, 10, 50, 50], # hộp 0 (mèo)
[12, 14, 52, 54], # hộp 1 (mèo)
[14, 17, 54, 57], # hộp 2 (mèo)
[100, 100, 140, 140], # hộp 3 (chó)
[104, 105, 144, 145], # hộp 4 (chó)
])
scores = np.array([0.90, 0.80, 0.60, 0.85, 0.70])
print("IoU hop 0 voi cac hop 1..4:", np.round(iou(boxes[0], boxes[1:]), 2))
print("IoU hop 3 voi hop 4: ", np.round(iou(boxes[3], boxes[4:])[0], 2))
print("NMS voi nguong 0.5: ", nms(boxes, scores, 0.5))
print("NMS voi nguong 0.7: ", nms(boxes, scores, 0.7))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • IoU của hộp 0 với các hộp khác là 0.75, 0.59, 0, 0. Số 0.75 khớp phép tính tay ở mục Lý thuyết. Hộp 2 lệch nhiều hơn nên IoU thấp hơn. Hai hộp của con chó không chạm hộp 0, nên IoU bằng 0. IoU của hai hộp chó với nhau là 0.65.
  • Với ngưỡng 0.5, NMS giữ [0, 3]: hộp mèo điểm cao nhất (0.90), rồi hộp chó điểm cao nhất (0.85). Ba hộp còn lại đều trùng từ 0.5 trở lên với một hộp đã giữ. Mỗi vật đúng một hộp.
  • Với ngưỡng 0.7, NMS giữ [0, 3, 4, 2]: chỉ hộp 1 (IoU 0.75) bị bỏ. Hộp 4 (IoU 0.65 với hộp 3) và hộp 2 (IoU 0.59 với hộp 0) sống sót, nên mỗi con vật bị đếm hai lần. Ngưỡng là một siêu tham số, và quá cao thì không dọn hết hộp trùng.

File dưới định nghĩa khối BasicBlock của ResNet (hai lớp 3×33 \times 3 có BatchNorm, đường tắt đồng nhất, hoặc 1×11 \times 1 stride 2 khi đổi kích thước), so số tham số của nó với khối tương ứng trong resnet18 của torchvision, rồi làm một thí nghiệm nhỏ về hiện tượng thoái hóa: huấn luyện một mạng sâu có và không có đường tắt trên bộ ảnh chữ số 8×88 \times 8 load_digits của scikit-learn (có sẵn khi cài, không tải mạng).

Câu 1/5 Thay một lớp tích chập 7×77 \times 7 bằng ba lớp 3×33 \times 3 nối tiếp (stride 1, cùng CC kênh vào và ra, bỏ qua hệ số tự do). Trường tiếp nhận và số tham số thay đổi thế nào?
Câu 2/5 Đầu vào có 256 kênh. Bạn muốn một lớp 5×55 \times 5 ra 64 kênh, và chèn trước nó một lớp 1×11 \times 1 giảm xuống 32 kênh. Bỏ qua hệ số tự do, số tham số khoảng bao nhiêu, so với khi không chèn?
Câu 3/5 Một mạng thường (không có kết nối tắt) 56 lớp có lỗi trên tập huấn luyện cao hơn một mạng 20 lớp cùng kiểu. Giải thích nào khớp với hiện tượng này?
Câu 4/5 Một lớp 3×33 \times 3 có 128 kênh vào và 128 kênh ra (bỏ qua hệ số tự do). Thay nó bằng tích chập tách theo chiều sâu (depthwise 3×33 \times 3 rồi pointwise 1×11 \times 1). Số tham số giảm khoảng bao nhiêu lần?
Câu 5/5 Hai người đứng sát nhau trong ảnh. Mô hình cho hai hộp đúng, mỗi hộp một người, IoU giữa hai hộp là 0.4. Bạn chạy NMS với ngưỡng 0.3. Điều gì xảy ra?

Ý chính:

  • Mọi kiến trúc trong bài giữ khung của LeNet-5 (tích chập, pooling, rồi lớp phân loại) và thêm một ý tưởng. AlexNet (2012): lớn hơn, với ReLU, dropout, GPU và ImageNet. VGG (2014): chỉ 3×33 \times 3; LL lớp nhìn vùng 1+2L1 + 2L với ít tham số hơn một lớp lớn (18C218C^2 so với 25C225C^2).
  • Tích chập 1×11 \times 1 là lớp kết nối đầy đủ trên từng điểm ảnh, dùng để giảm kênh trước lớp đắt (GoogLeNet, nút cổ chai của ResNet). Tích chập tách theo chiều sâu (Xception) tách việc nhìn không gian khỏi việc trộn kênh, tiết kiệm gần k2k^2 lần.
  • ResNet học phần dư: y=g(F(x)+x)\mathbf{y} = g(F(\mathbf{x}) + \mathbf{x}). Phép đồng nhất trở nên dễ học, gradient có đường đi thẳng qua đường tắt, nên mạng 152 lớp vẫn huấn luyện được. SENet cho mỗi kênh một trọng số sc∈(0,1)s_c \in (0, 1) tính từ chính ảnh, với rất ít tham số.
  • YOLO biến phát hiện vật thể thành một lần hồi quy: lưới S×SS \times S, mỗi ô BB hộp có độ tin cậy và CC xác suất lớp. IoU đo hai hộp trùng nhau bao nhiêu; NMS giữ hộp điểm cao nhất và bỏ các hộp trùng với nó quá ngưỡng.

Đọc thêm:

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