Semantic Segmentation: FCN, UNet, FPN, DeepLab, HRNet
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Phát biểu được bài toán semantic segmentation (phân vùng ngữ nghĩa): gán một lớp cho từng pixel, đầu ra , học bằng cross-entropy theo pixel.
- Tính được IoU (intersection over union) từng lớp, mIoU, pixel accuracy, và giải thích vì sao pixel accuracy đánh lừa khi các lớp mất cân bằng.
- Giải thích được năm cách giải cùng một mâu thuẫn “biết cái gì” và “biết ở đâu”: FCN, UNet, FPN, DeepLab, HRNet. Tính được trường tiếp nhận của tích chập giãn (dilated convolution).
- Phân biệt được nội suy lên song tuyến tính với tích chập chuyển vị (transposed convolution), và nhận ra hiện tượng ô bàn cờ.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Image Classification with CNNs: AlexNet → VGG → ResNet → MobileNet → EfficientNet, in Computer Vision(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Từ “trong ảnh có gì” tới “pixel này là gì”
Phần tiêu đề “Từ “trong ảnh có gì” tới “pixel này là gì””Một mạng phân loại nhìn ảnh chụp đường phố và trả lời: “có ô tô”. Với xe tự lái, câu trả lời đó vô dụng. Xe cần biết ô tô nằm ở đâu, đường kéo dài tới đâu, mép vỉa hè ở pixel nào. Bác sĩ đọc ảnh chụp cắt lớp cũng vậy: “có khối u” chưa đủ, cần đường viền của khối u để đo thể tích.
Semantic segmentation trả lời câu hỏi đó cho từng pixel: pixel này là đường, pixel kia là người, pixel nọ là trời. Kết quả là một tấm “bản đồ tô màu” cùng cỡ với ảnh. Hai người đứng cạnh nhau được tô cùng một màu “người”: bài toán không đếm từng vật. Tách từng vật riêng là việc của bài Instance và Universal Segmentation.
Mâu thuẫn: “cái gì” và “ở đâu”
Phần tiêu đề “Mâu thuẫn: “cái gì” và “ở đâu””Thử cách ngây thơ: lấy CNN phân loại, bỏ lớp cuối. Có hai vấn đề.
- Tầng sâu thì thô. Sau 5 lần giảm nửa, lưới chỉ còn cạnh ảnh. Mỗi ô của lưới ứng với một vùng pixel: biết vùng đó “có mèo”, nhưng không biết đường viền con mèo đi qua pixel nào.
- Tầng nông thì mù nghĩa. Bản đồ đặc trưng ở tầng đầu còn đủ chi tiết, nhưng chỉ “thấy” vài cạnh và đốm màu, chưa biết đó là mèo hay chó.
Giống như nhìn bản đồ: bản đồ thế giới cho bạn biết đây là Việt Nam, nhưng không chỉ được con hẻm. Bản đồ phường chỉ được con hẻm, nhưng nếu chỉ có nó, bạn không biết mình ở nước nào. Người tìm đường giỏi dùng cả hai.
Cả năm họ mô hình trong bài là năm cách kết hợp hai loại thông tin này. Bài toán của bài là: làm sao dựng một mạng nhận ảnh và trả ra một nhãn cho từng pixel, vừa đúng nghĩa vừa đúng đường viền, và đo chất lượng của nó cho công bằng với cả những lớp nhỏ?
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 vào như bài trước. Có lớp (tính cả nền). Nhãn đúng là một mask cỡ , mỗi ô là lớp của pixel ở hàng , cột .
Mạng trả ra một khối điểm : mỗi pixel có logit, một cho mỗi lớp. Softmax áp riêng cho từng pixel biến logit đó thành xác suất:
Nhãn đoán của pixel là lớp có xác suất lớn nhất, .
So với phân loại ảnh, chỉ có một thay đổi: thay vì một vectơ số cho cả ảnh, ta có vectơ như thế. Mỗi pixel là một bài phân loại nhỏ.
Hàm mất mát: cross-entropy theo pixel
Phần tiêu đề “Hàm mất mát: cross-entropy theo pixel”Vì mỗi pixel là một bài phân loại, ta dùng cross-entropy cho từng pixel rồi lấy trung bình trên cả ảnh:
Đọc từ trong ra ngoài: là xác suất mạng gán cho lớp đúng của pixel . Lấy thì xác suất gần 1 cho mất mát gần 0, xác suất gần 0 cho mất mát rất lớn. Chia cho để mất mát không phụ thuộc cỡ ảnh. Với một lô nhiều ảnh, F.cross_entropy của PyTorch lấy trung bình trên mọi pixel của mọi ảnh. FCN viết tổng thay vì trung bình; hai cách chỉ khác một hằng số nhân.
Công thức này có một hệ quả: lớp nhiều pixel chiếm phần lớn tổng. Nếu 80% pixel là nền, mạng giảm được phần lớn mất mát chỉ bằng cách đoán “nền” giỏi. UNet xử lý bằng cách nhân mất mát mỗi pixel với một trọng số : lớn hơn ở lớp hiếm và ở đường biên mỏng giữa hai tế bào chạm nhau.
Đo chất lượng: pixel accuracy, IoU, mIoU
Phần tiêu đề “Đo chất lượng: pixel accuracy, IoU, mIoU”Cách đo đơn giản nhất là pixel accuracy: tỉ lệ pixel đoán đúng.
Nó có cùng nhược điểm với hàm mất mát ở trên. Ô NumPy ở mục Ví dụ code dùng ảnh có 3 323 pixel nền, 709 pixel hình tròn và chỉ 64 pixel hình vuông nhỏ. Một mô hình bỏ sót hoàn toàn hình vuông vẫn có pixel accuracy .
IoU sửa điều đó bằng cách đo từng lớp riêng. Gọi là tập pixel đoán là lớp , là tập pixel thật sự thuộc lớp :
Tử số là phần giao: pixel đoán đúng là (true positive). Mẫu số là phần hợp, tách được thành ba phần không chồng nhau: đúng là và đoán là (TP), đoán là mà sai (false positive), là mà bị bỏ sót (false negative). IoU bằng 1 khi hai vùng trùng khít, bằng 0 khi không chạm nhau. Nó phạt cả đoán thừa lẫn đoán thiếu.
Với mô hình bỏ sót hình vuông: lớp vuông có nên . Lớp nền có , và 64 pixel vuông bị đoán thành nền là FP của lớp nền, nên .
mIoU (mean IoU) là trung bình cộng IoU của lớp, mỗi lớp cùng trọng số dù to hay nhỏ:
Mô hình trên có , thấp hơn hẳn 0.984. Đây là độ đo chính của các bài báo segmentation; FCN gọi nó là “mean IU”. Trên một tập kiểm tra, cách chuẩn là cộng dồn TP, FP, FN của mọi ảnh rồi mới chia, như ô PyTorch làm.
FCN: biến mạng phân loại thành mạng “toàn tích chập”
Phần tiêu đề “FCN: biến mạng phân loại thành mạng “toàn tích chập””FCN (Long, Shelhamer, Darrell, 2015) là bài báo mở đầu cho cách làm hiện đại. Ba ý:
Ý 1: thay lớp kết nối đầy đủ bằng tích chập. Một lớp kết nối đầy đủ nhận bản đồ chính là một tích chập có kernel phủ trọn bản đồ đó. Viết lại như vậy, mạng không còn đòi ảnh cỡ cố định. Đưa ảnh to hơn vào, thay vì một vectơ điểm, bạn nhận một lưới điểm: mỗi ô là dự đoán cho một vùng ảnh. Mạng chỉ gồm tích chập nên được gọi là mạng toàn tích chập (fully convolutional network). Cuối cùng, một tích chập đổi số kênh thành (21 với bộ PASCAL VOC: 20 lớp và nền).
Ý 2: nội suy lên. Lưới điểm của VGG16 nhỏ hơn ảnh 32 lần mỗi chiều. Tỉ lệ này gọi là output stride. FCN phóng lưới lên đúng cỡ ảnh bằng tích chập chuyển vị, khởi tạo đúng bằng nội suy song tuyến tính (mục sau). Bản này gọi là FCN-32s, và đường viền của nó rất “nhòe”.
Ý 3: nối tắt từ tầng nông. FCN-16s lấy bản đồ ở pool4 (output stride 16), cho qua tích chập thành kênh điểm, rồi cộng với lưới điểm của tầng cuối đã phóng lên 2 lần. FCN-8s làm thêm một lần với pool3 (output stride 8). Tầng sâu nói “cái gì”, tầng nông chỉnh lại “ở đâu”.
Nội suy lên: song tuyến tính và tích chập chuyển vị
Phần tiêu đề “Nội suy lên: song tuyến tính và tích chập chuyển vị”Có ba cách phóng một lưới lên :
- Láng giềng gần nhất (nearest neighbor): mỗi ô thô chép ra thành khối . Nhanh, nhưng ra hình bậc thang.
- Nội suy song tuyến tính (bilinear interpolation): mỗi pixel mới là trung bình có trọng số của 4 ô thô gần nhất, trọng số tỉ lệ với độ gần. Nội suy theo hàng trước, theo cột sau, nên gọi là “song” tuyến tính. Đường viền mượt hơn. Không có tham số học.
- Tích chập chuyển vị (transposed convolution, có bài gọi là “deconvolution”): mỗi ô vào nhân với một kernel học được rồi “đóng dấu” lên lưới ra, cách nhau ô; chỗ các dấu chồng nhau thì cộng lại. Kích thước ra (PyTorch, không có
output_padding) là . Với , : , gấp đôi, như UNet và ô PyTorch dùng.
Tích chập chuyển vị có một bẫy, Odena, Dumoulin và Olah (2016) gọi là hiện tượng ô bàn cờ (checkerboard artifacts). Khi không chia hết cho , ví dụ , , các dấu chồng nhau không đều: theo mỗi chiều, cứ một pixel ra nhận đóng góp từ hai ô vào thì pixel kế bên chỉ nhận từ một. Trên ảnh hai chiều, hoa văn này thành ô bàn cờ, và xếp chồng nhiều lớp làm nó nặng thêm. Cách sửa của họ: nội suy trước (gần nhất hoặc song tuyến tính), rồi tích chập thường. Chọn chia hết cho giảm vấn đề, nhưng tác giả lưu ý không hết hẳn.
UNet: hình chữ U đối xứng
Phần tiêu đề “UNet: hình chữ U đối xứng”UNet (Ronneberger, Fischer, Brox, 2015) ra đời cho ảnh kính hiển vi, nơi chỉ có vài chục ảnh có nhãn và đường biên giữa hai tế bào chỉ dày vài pixel. Thiết kế gồm hai nhánh:
- Nhánh co (contracting path, hay encoder): lặp lại “hai tích chập + ReLU, rồi max pooling ”. Mỗi lần giảm nửa độ phân giải thì số kênh gấp đôi: 64, 128, 256, 512, 1024.
- Nhánh giãn (expansive path, hay decoder): lặp lại “phóng lên 2 lần bằng tích chập chuyển vị (giảm nửa số kênh), nối kênh với bản đồ cùng tầng của nhánh co, rồi hai tích chập ”. Cuối cùng một tích chập ra kênh.
Điểm khác FCN nằm ở hai chữ: đối xứng và nối kênh. FCN có một bộ giải mã rất mỏng (cộng điểm kênh). UNet có nhánh giãn dày gần bằng nhánh co, và ở mỗi tầng nó nối (concatenate) toàn bộ đặc trưng của nhánh co vào, không chỉ kênh điểm. Đó là nối tắt (skip connection): chi tiết vị trí đi thẳng từ trái sang phải chữ U, không phải đi qua đáy chữ U nơi độ phân giải thấp nhất.
Bài báo gốc dùng tích chập không đệm, nên mỗi lớp mất một viền pixel: ô vào cho mask , và bản đồ của nhánh co phải cắt mép trước khi nối. Đa số cài đặt sau này dùng padding 1 để ra đúng cỡ ảnh, như ô PyTorch của bài.
FPN: kim tự tháp đặc trưng
Phần tiêu đề “FPN: kim tự tháp đặc trưng”FPN (Lin và cộng sự, 2017) vốn sinh ra cho bài toán phát hiện đối tượng, nơi vật có thể to hay nhỏ. Nó có ba đường:
- Từ dưới lên (bottom-up): xương sống CNN bình thường, cho các bản đồ ở output stride 4, 8, 16, 32.
- Từ trên xuống (top-down): bắt đầu từ bản đồ sâu nhất, phóng lên 2 lần bằng láng giềng gần nhất.
- Nối ngang (lateral connection): bản đồ cùng tầng của đường đi lên qua tích chập để có kênh, rồi cộng với bản đồ vừa phóng lên. Sau đó một tích chập làm mịn, theo bài báo là để giảm răng cưa do phóng lên.
Kết quả là một kim tự tháp (pyramid): mỗi tầng có độ phân giải riêng nhưng đều mang nghĩa của tầng sâu nhất. Panoptic FPN (Kirillov và cộng sự, 2019) gắn thêm một nhánh semantic segmentation nhẹ lên kim tự tháp này. So với UNet: FPN dùng phép cộng với số kênh cố định, nên rẻ hơn nối kênh; và nó giữ mọi tầng làm đầu ra, không chỉ tầng cuối.
DeepLab: đừng thu nhỏ quá, hãy giãn kernel
Phần tiêu đề “DeepLab: đừng thu nhỏ quá, hãy giãn kernel”Họ DeepLab của Chen và cộng sự đi hướng khác: thay vì thu nhỏ 32 lần rồi cố gắng phóng lại, bớt thu nhỏ ngay từ đầu. Bỏ stride ở vài tầng cuối thì output stride còn 16 hoặc 8. Nhưng làm vậy, mỗi kernel ở tầng sâu chỉ còn “thấy” một vùng ảnh nhỏ hơn trước. Cần một cách cho kernel nhìn xa hơn mà không thêm tham số.
Tích chập giãn (dilated hay atrous convolution, “à trous” là “có lỗ” trong tiếng Pháp) làm đúng điều đó. Viết một chiều cho gọn, theo DeepLabv3 (2017):
Ở đây là trọng số thứ của kernel, và dilation rate là khoảng cách giữa hai vị trí lấy mẫu. là tích chập thường. nghĩa là lấy mẫu cách một ô.
Kích thước hiệu dụng. Kernel có trọng số, giữa hai trọng số liền nhau có ô bị bỏ qua. Có khoảng như vậy, nên tổng chiều dài phủ là:
Với : cho , cho 13, cho 37. Số trọng số vẫn là .
Trường tiếp nhận cộng dồn. Trường tiếp nhận (receptive field) là vùng đầu vào ảnh hưởng tới một ô ra. Với các lớp stride 1 xếp chồng, mỗi lớp nới trường tiếp nhận thêm ô, vì nó gom ô kề nhau của lớp trước, mà mỗi ô đó đã thấy một vùng, hai vùng kề nhau lệch nhau một ô. Bắt đầu từ 1 ô:
Ba lớp thường cho . Ba lớp với cho (tự tính), cùng số tham số. Trường tiếp nhận lớn lên theo cấp số nhân thay vì cộng, mà độ phân giải không giảm.
ASPP. Vật có nhiều cỡ, nên một rate không đủ. ASPP (Atrous Spatial Pyramid Pooling) chạy song song nhiều nhánh trên cùng bản đồ rồi nối kênh kết quả. Ở DeepLabv3 với output stride 16: một tích chập , ba tích chập với rate 6, 12, 18, và một nhánh đặc trưng mức ảnh (lấy trung bình toàn bản đồ, tích chập , phóng lại). Nhánh cuối có lý do riêng: khi rate gần bằng cỡ bản đồ, phần lớn 9 vị trí lấy mẫu rơi ra ngoài vùng có dữ liệu, kernel thoái hóa thành ; nhánh trung bình toàn ảnh bù lại ngữ cảnh toàn cục.
DeepLabv3+ (Chen và cộng sự, 2018) thêm một bộ giải mã nhỏ kiểu UNet để sửa đường biên, tức là ghép hai ý tưởng lại.
HRNet: không bao giờ bỏ độ phân giải cao
Phần tiêu đề “HRNet: không bao giờ bỏ độ phân giải cao”Các họ trên đều theo một mẫu: thu nhỏ rồi phục hồi. HRNet (Wang và cộng sự, 2020) hỏi: sao phải đánh mất độ phân giải cao để rồi lấy lại?
HRNet giữ một nhánh độ phân giải cao (1/4 cạnh ảnh) suốt chiều dài mạng. Mỗi giai đoạn thêm một nhánh mới thấp hơn, tới bốn nhánh song song ở 1/4, 1/8, 1/16, 1/32, số kênh , , , . Định kỳ, các nhánh trao đổi thông tin: nhánh cao gửi xuống qua tích chập stride 2, nhánh thấp gửi lên qua nội suy song tuyến tính và tích chập , mỗi nhánh cộng mọi thứ nhận được. Với segmentation, đầu HRNetV2 phóng mọi nhánh lên 1/4 rồi nối kênh.
Hình dung lại hai tấm bản đồ: UNet đọc bản đồ thế giới rồi mới lần xuống bản đồ phường; HRNet trải cả bốn tấm trên bàn và liên tục đối chiếu.
Giới hạn
Phần tiêu đề “Giới hạn”- Không tách từng vật. Hai người đứng cạnh nhau chung một vùng “người”. Đếm và tách từng vật thuộc bài Instance và Universal Segmentation.
- Bài chỉ nói các họ dựa trên CNN. Các mô hình dựa trên Transformer (như Mask2Former) ở bài trên; cách Transformer xử lý ảnh ở bài Phân loại ảnh với Transformer.
- Nhãn pixel rất đắt. Vẽ mask cho một ảnh tốn nhiều công hơn gán một nhãn cho cả ảnh. Bài Data augmentation trong CV và bài Self-supervised learning với ảnh bàn cách tận dụng ít nhãn.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới vẽ UNet nhỏ đúng như ô PyTorch của bài. Hãy nhìn các mũi tên ngang: chúng mang bản đồ và của nhánh co sang nhánh giãn, bỏ qua đáy chữ U chỉ còn .
Bảng dưới đặt năm họ cạnh nhau theo một câu hỏi chung: mỗi họ lấy lại độ phân giải và ghép “cái gì” với “ở đâu” bằng cách nào.
| Tiêu chí | FCN (2015) | UNet (2015) | FPN (2017) | DeepLab v3/v3+ (2017–2018) | HRNet (2020) |
|---|---|---|---|---|---|
| Đường đi của độ phân giải | Thu nhỏ 32 lần, phóng lên một lần | Thu nhỏ rồi phóng lên từng tầng, đối xứng | Thu nhỏ, rồi đi ngược từ trên xuống từng tầng | Chỉ thu nhỏ 8 hoặc 16 lần | Giữ nhánh 1/4 suốt mạng, chạy song song nhánh thấp |
| Ghép chi tiết với ngữ cảnh | Cộng điểm kênh từ pool3, pool4 | Nối kênh toàn bộ đặc trưng cùng tầng | Tích chập rồi cộng, kênh | Atrous và ASPP nhiều rate; v3+ thêm bộ giải mã | Trao đổi lặp lại giữa các nhánh |
| Phóng lên bằng | Tích chập chuyển vị, khởi tạo song tuyến tính | Tích chập chuyển vị | Láng giềng gần nhất | Song tuyến tính | Song tuyến tính |
| Điểm mạnh | Đơn giản, dùng lại mạng phân loại | Đường biên sắc, học được từ ít ảnh | Đặc trưng nhiều tỉ lệ, rẻ, dùng chung cho phát hiện | Trường tiếp nhận lớn, ngữ cảnh nhiều cỡ | Bản đồ cao vừa sắc vừa giàu nghĩa |
| Cái giá | Đường viền nhòe | Tốn bộ nhớ ở các tầng độ phân giải cao | Cần thêm đầu ra cho segmentation | Tính toán nặng ở độ phân giải 1/8 | Nhiều nhánh, tốn bộ nhớ và tính toán |
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: ô NumPy với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô PyTorch với Python 3.14 và PyTorch 2.15 trên CPU. Ô NumPy chạy ngay trên trang, ô PyTorch chạy trên máy bạn. Hai ô độc lập, không dùng biến của nhau.
IoU, mIoU và nội suy lên bằng NumPy
Phần tiêu đề “IoU, mIoU và nội suy lên bằng NumPy”Ô dưới đây vẽ một ảnh có ba lớp: nền, một hình tròn bán kính 15 và một hình vuông . Hàm report dựng ma trận nhầm lẫn (confusion matrix) bằng np.bincount: ô đếm số pixel lớp đúng bị đoán là . Đường chéo là TP; tổng cột trừ TP là FP; tổng hàng trừ TP là FN. Ô thử bốn “mô hình”:
all bg: đoán nền ở mọi pixel.no square: đúng hết, trừ bỏ sót cả hình vuông.- Mô phỏng một mạng có output stride và : lấy trung bình mask one-hot trên từng ô làm lưới điểm thô, rồi phóng lên bằng láng giềng gần nhất (
nearest) hoặc song tuyến tính (bilin), và lấy argmax.
IoU, mIoU, pixel accuracy và nội suy lên
import numpy as np
# Ảnh 64x64 có ba lớp: 0 nền, 1 hình tròn, 2 hình vuông nhỏH = W = 64K = 3yy, xx = np.mgrid[0:H, 0:W]gt = np.zeros((H, W), dtype=int) # mask đúnggt[(yy - 24) ** 2 + (xx - 24) ** 2 <= 15 ** 2] = 1gt[44:52, 46:54] = 2print("pixels per class:", np.bincount(gt.ravel()))
def report(name, pred): # C[i, j]: số pixel lớp đúng i bị đoán là lớp j (ma trận nhầm lẫn) C = np.bincount(K * gt.ravel() + pred.ravel(), minlength=K * K) C = C.reshape(K, K) tp = np.diag(C) # phần giao của mask đoán và mask đúng, từng lớp iou = tp / (C.sum(0) + C.sum(1) - tp) # giao / hợp acc = tp.sum() / C.sum() # pixel accuracy print(f"{name:9s} acc = {acc:.3f} IoU = {np.round(iou, 3)} " f"mIoU = {iou.mean():.3f}")
report("all bg", np.zeros_like(gt))report("no square", np.where(gt == 2, 0, gt)) # bỏ sót cả hình vuông
for s in [4, 8]: # output stride: bản đồ điểm nhỏ hơn ảnh s lần mỗi chiều onehot = np.eye(K)[gt] # (64, 64, 3) # Điểm thô: trung bình one-hot trên từng ô s x s coarse = onehot.reshape(H // s, s, W // s, s, K).mean(axis=(1, 3)) near = coarse.repeat(s, axis=0).repeat(s, axis=1) # gần nhất # Song tuyến tính: vị trí tâm pixel trên lưới thô, nội suy từng trục c = (np.arange(H) + 0.5) / s - 0.5 f = np.floor(c).astype(int) i0, i1 = np.clip(f, 0, H // s - 1), np.clip(f + 1, 0, H // s - 1) t = (c - f)[:, None, None] rows = coarse[i0] * (1 - t) + coarse[i1] * t # nội suy theo hàng bil = rows[:, i0] * (1 - t[:, 0]) + rows[:, i1] * t[:, 0] # theo cột report(f"nearest/{s}", near.argmax(-1)) report(f"bilin/{s}", bil.argmax(-1))The first run downloads Python, about 10 MB.
Đọc kết quả:
all bg: pixel accuracy 0.811 chỉ nhờ nền chiếm 81% ảnh, nhưng mIoU 0.270.no square: pixel accuracy 0.984, IoU hình vuông 0, mIoU 0.660. Đúng các số đã tính tay ở mục Lý thuyết. Lỗi nghiêm trọng nhất (mất hẳn một vật) gần như không thấy trên pixel accuracy.- Output stride 4: song tuyến tính cho mIoU 0.925, láng giềng gần nhất 0.771. Phóng bằng láng giềng gần nhất làm đường tròn thành bậc thang (IoU 0.854 so với 0.972).
- Output stride 8: hình vuông lệch lưới nên bị chia ra nhiều ô thô, ô nào cũng nhiều nền hơn vuông. Cả hai cách phóng đều cho IoU hình vuông bằng 0, trong khi pixel accuracy vẫn 0.942 và 0.974. Thông tin đã mất ở lưới thô thì không phép nội suy nào lấy lại được. Đó là lý do FCN, UNet, FPN đưa chi tiết từ tầng nông sang, còn DeepLab và HRNet giữ độ phân giải cao hơn.
Ô này nội suy điểm của 3 lớp rồi mới argmax, như FCN làm. Đừng nội suy chính mask nhãn (xem Lỗi hay gặp).
UNet tí hon bằng PyTorch
Phần tiêu đề “UNet tí hon bằng PyTorch”File dưới đây tự sinh 512 ảnh huấn luyện và 128 ảnh kiểm tra (hạt giống 0). Mỗi ảnh xám có nhiễu, chứa một hình tròn và một hình vuông cùng độ sáng, nên mạng không thể chỉ nhìn độ sáng: phải nhận ra hình dạng. Mạng là UNet hai tầng như hình ở mục Minh họa, phóng lên bằng ConvTranspose2d(…, 2, 2) (kernel bằng stride, không chồng lấn). File huấn luyện hai lần với cùng hạt giống: một lần đủ nối tắt, một lần thay nối tắt bằng số 0, rồi in pixel accuracy, IoU từng lớp và mIoU trên tập kiểm tra. Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.
Run on your machine
UNet hai tầng học tách hình tròn và hình vuông
- Install
pip install torch numpy- Hardware
- CPU là đủ. Một đến hai phút cho cả hai lần huấn luyện trên máy phát triển
import numpy as npimport torchimport torch.nn.functional as Ffrom torch import nn
torch.manual_seed(0)rng = np.random.default_rng(0)S, K = 32, 3 # cạnh ảnh; số lớp: 0 nền, 1 hình tròn, 2 hình vuông
def make(n): # n ảnh xám có nhiễu, mỗi ảnh một hình tròn, một hình vuông yy, xx = np.mgrid[0:S, 0:S] X = rng.normal(0.2, 0.1, (n, 1, S, S)) M = np.zeros((n, S, S), dtype=np.int64) for i in range(n): cy, cx, r = *rng.integers(8, 24, 2), rng.integers(4, 9) M[i][(yy - cy) ** 2 + (xx - cx) ** 2 <= r * r] = 1 y, x, a = *rng.integers(0, 22, 2), rng.integers(6, 11) M[i, y:y + a, x:x + a] = 2 # hình vuông đè lên nếu chồng nhau X[i, 0][M[i] > 0] += rng.uniform(0.4, 0.8) # hai hình cùng độ sáng return torch.tensor(X, dtype=torch.float32), torch.tensor(M)
def block(i, o): # hai lớp tích chập 3x3, padding 1 giữ nguyên kích thước return nn.Sequential(nn.Conv2d(i, o, 3, padding=1), nn.ReLU(), nn.Conv2d(o, o, 3, padding=1), nn.ReLU())
class TinyUNet(nn.Module): # 2 tầng: 32x32 -> 16x16 -> 8x8 rồi đi lên def __init__(self, skip): super().__init__() self.skip = float(skip) # 0.0: thay nhánh nối tắt bằng số 0 self.e1, self.e2, self.mid = block(1, 16), block(16, 32), block(32, 64) self.u2, self.d2 = nn.ConvTranspose2d(64, 32, 2, 2), block(64, 32) self.u1, self.d1 = nn.ConvTranspose2d(32, 16, 2, 2), block(32, 16) self.head = nn.Conv2d(16, K, 1) # K điểm cho từng pixel def forward(self, x): e1 = self.e1(x) # (B, 16, 32, 32) e2 = self.e2(F.max_pool2d(e1, 2)) # (B, 32, 16, 16) m = self.mid(F.max_pool2d(e2, 2)) # (B, 64, 8, 8) d2 = self.d2(torch.cat([self.u2(m), e2 * self.skip], 1)) d1 = self.d1(torch.cat([self.u1(d2), e1 * self.skip], 1)) return self.head(d1) # (B, K, 32, 32)
(Xtr, Mtr), (Xte, Mte) = make(512), make(128) # tập huấn luyện, kiểm trafor skip in [True, False]: torch.manual_seed(0) net = TinyUNet(skip) opt = torch.optim.Adam(net.parameters(), lr=3e-3) for epoch in range(20): for b in torch.randperm(512).split(32): loss = F.cross_entropy(net(Xtr[b]), Mtr[b]) # TB mọi pixel opt.zero_grad() loss.backward() opt.step() with torch.no_grad(): P = net(Xte).argmax(1) # lớp đoán cho từng pixel iou = [((P == k) & (Mte == k)).sum().item() / ((P == k) | (Mte == k)).sum().item() for k in range(K)] print(f"skip={skip}: acc {(P == Mte).float().mean():.3f}, IoU " f"{np.round(iou, 3)}, mIoU {np.mean(iou):.3f}")Sample output
From one real run. A model gives a different answer each time.
skip=True: acc 0.987, IoU [0.996 0.879 0.83 ], mIoU 0.902skip=False: acc 0.953, IoU [0.983 0.676 0.46 ], mIoU 0.706Đọc kết quả:
- Có nối tắt: mIoU 0.902 trên 128 ảnh chưa thấy khi huấn luyện; hình tròn 0.879, hình vuông 0.83. Sau 20 lượt qua dữ liệu, UNet nhỏ đã phân biệt được hai hình cùng độ sáng chỉ nhờ hình dạng.
- Bỏ nối tắt: cùng kiến trúc, cùng hạt giống, chỉ khác là nhánh giãn không nhận bản đồ của nhánh co. mIoU tụt còn 0.706, IoU hình vuông còn 0.46. Mọi thông tin vị trí phải đi qua đáy , nên các góc vuông và đường biên bị nhòe, đúng điều mục Lý thuyết dự đoán.
- Pixel accuracy chỉ giảm từ 0.987 xuống 0.953: lại thêm một ví dụ cho thấy nó che mất khác biệt mà mIoU thấy rõ.
Dù đã đặt hạt giống, số có thể lệch ở chữ số thứ hai, thứ ba trên máy khác: số luồng CPU đổi thứ tự cộng số thực, và sai khác nhỏ lớn dần qua hàng trăm bước huấn luyện. Trên máy phát triển, chạy với 4 luồng cho mIoU 0.908 và 0.704. Khoảng cách giữa có và không có nối tắt thì giữ nguyên.
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:
- Semantic segmentation là phân loại từng pixel: mạng ra logit, softmax riêng từng pixel, học bằng cross-entropy lấy trung bình trên mọi pixel.
- Đo bằng và mIoU (trung bình đều các lớp). Pixel accuracy bị lớp lớn chi phối và che mất việc bỏ sót lớp nhỏ.
- Mâu thuẫn trung tâm: tầng sâu biết “cái gì” nhưng thô, tầng nông biết “ở đâu” nhưng ít nghĩa. FCN cộng điểm từ tầng nông; UNet dựng nhánh giãn đối xứng và nối kênh; FPN dựng kim tự tháp từ trên xuống bằng phép cộng; DeepLab bớt thu nhỏ và bù trường tiếp nhận bằng tích chập giãn () và ASPP; HRNet giữ nhánh độ phân giải cao suốt mạng.
- Phóng lên bằng song tuyến tính không có tham số; tích chập chuyển vị học được, nhưng dễ tạo ô bàn cờ khi kernel không chia hết cho stride.
Đọc thêm:
- Long, J., Shelhamer, E., Darrell, T. (2015). Fully Convolutional Networks for Semantic Segmentation, arXiv:1411.4038, mục 3 và 4. Cách biến mạng phân loại thành mạng toàn tích chập, và định nghĩa các độ đo ở mục 5.
- Ronneberger, O., Fischer, P., Brox, T. (2015). U-Net: Convolutional Networks for Biomedical Image Segmentation, arXiv:1505.04597, mục 2 và hình 1. Ngắn, hình 1 đủ để vẽ lại cả mạng; mục 3 có bản đồ trọng số cho đường biên.
- Lin, T.-Y. và cộng sự (2017). Feature Pyramid Networks for Object Detection, arXiv:1612.03144, mục 3. Nhánh từ trên xuống và nối ngang trong một trang.
- Chen, L.-C., Papandreou, G., Schroff, F., Adam, H. (2017). Rethinking Atrous Convolution for Semantic Image Segmentation, arXiv:1706.05587, mục 3; và Chen, L.-C. và cộng sự (2018). Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation, arXiv:1802.02611. Tích chập giãn, ASPP, và bộ giải mã của DeepLabv3+.
- Wang, J. và cộng sự (2020). Deep High-Resolution Representation Learning for Visual Recognition, arXiv:1908.07919, mục 3 và 4. Kiến trúc HRNet và ba kiểu đầu ra V1, V2, V2p.
- Odena, A., Dumoulin, V., Olah, C. (2016). Deconvolution and Checkerboard Artifacts, Distill. Hình tương tác cho thấy vì sao chồng lấn không đều sinh ô bàn cờ.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About