U-Net adalah arsitektur encoder–decoder dengan skip connections yang sangat efektif untuk segmentasi piksel-tingkat. Di domain medis, U-Net unggul pada data terbatas dan ketidakseimbangan kelas. Di domain visual umum (jalan, bangunan, tanaman, bagian produk), U-Net memberikan baseline yang kuat dan mudah diproduksikan. Artikel ini memandu Anda dari konsep, pilihan loss, metrik, augmentasi, hingga contoh kode PyTorch yang siap dimodifikasi.
Bayangkan Anda sedang melihat sebuah foto hitam-putih organ tubuh dari hasil CT scan. Bagi mata manusia, gambar itu terlihat rumit: ada jaringan, pembuluh darah, dan mungkin ada lesi kecil yang hampir tak terlihat. Segmentasi citra membantu “mewarnai” bagian-bagian penting itu secara otomatis—menandai mana yang organ sehat, mana yang perlu diawasi, hingga batas lesi yang presisi. Di ruang klinik, ini sangat krusial: dokter bisa mendiagnosis lebih cepat, merencanakan tindakan dengan lebih akurat, dan memantau perkembangan pasien dari waktu ke waktu, tanpa harus menelusuri tiap piksel secara manual.
Di luar dunia medis, manfaatnya sama nyata. Dari langit, citra satelit menangkap pola jalan, atap bangunan, dan hamparan lahan. Segmentasi citra memetakan semuanya secara piksel-per-piksel: jalan dipisahkan dari trotoar, sawah dari permukiman, tanaman sehat dari yang terserang hama. Di konstruksi, retakan mikro pada beton bisa disorot jelas meski tersembunyi dalam tekstur permukaan. Di pabrik, sistem inspeksi kualitas dapat “mengerti” mana bagian produk yang cacat hanya dari bentuk dan tepinya.
Intinya, segmentasi bukan sekadar mengenali objek “apa”, tetapi juga menunjukkan “di mana” tepatnya objek itu berada dan seberapa jauh batasnya. Presisi di level piksel inilah yang tidak diberikan oleh klasifikasi biasa (yang hanya mengatakan “ada kucing di gambar”) atau deteksi objek (yang memberi kotak kasar). Dengan segmentasi, kita mendapatkan peta detail yang siap dipakai untuk keputusan penting—dari meja operasi hingga dashboard kota pintar.
Gambar 1. Segmentasi semantik citra satelit (jalan, bangunan, vegetasi, air).
Inti Arsitektur U-Net
Encoder itu seperti saat kita melihat sebuah kota dari peta skala besar. Kita bisa mengenali pola besar: garis jalan utama, area permukiman, kawasan industri, sungai yang membelah kota. Detail kecil—seperti lebar gang, posisi pagar, atau nomor rumah—belum terlihat. Di U-Net, tahap ini dilakukan lewat rangkaian konvolusi + aktivasi + normalisasi lalu pengecilan ukuran (downsampling). Tujuannya adalah merangkum informasi penting menjadi ringkas agar model “mengerti tema besarnya”: ini area organ hati, itu jaringan paru, yang ini latar belakang. Semakin ke bawah (semakin dalam lapisan), gambaran makin abstrak, tapi konteks makin kuat.
Setelah paham konteks, Decoder bekerja kebalikannya: kembali ke skala yang makin detail, mirip membuka denah. Kini kita bicara lokasi presisi: di mana pintu berada, bagaimana lebar koridor, di mana letak jendela. Secara teknis, ini dilakukan dengan upsampling (misalnya transposed convolution atau interpolasi) lalu konvolusi agar fitur yang tadinya “dipadatkan” bisa dikembalikan ke resolusi tinggi. Hasilnya, model bukan cuma tahu “ada organ A di gambar,” tetapi juga “tepatnya organ A berada di sini, garis tepinya di sini.”
Agar detail halus tidak hilang selama proses “mengecilkan lalu membesarkan” tadi, U-Net punya jembatan cepat bernama skip connections. Anggap ini sebagai catatan detail yang diambil dari peta skala menengah (encoder bagian awal) lalu diserahkan langsung ke tahap denah (decoder) yang selevel. Dengan begitu, ketika decoder menata ulang piksel beresolusi tinggi, ia punya “contekan” tepi, tekstur, dan pola halus yang mungkin sempat memudar. Inilah alasan U-Net terkenal tajam dalam menjaga batas objek—misalnya kontur tumor, tepi pembuluh darah, retakan tipis pada beton, atau garis jalan kecil di citra satelit.
Di ujung proses, Output U-Net bertugas menerjemahkan fitur menjadi label per piksel. Secara praktis, ada konvolusi 1×1 yang memetakan setiap piksel ke jumlah kelas yang diinginkan. Jika kasusnya biner (objek vs latar), aktivasi yang dipakai biasanya sigmoid; jika multi-kelas (organ berbeda, jenis infrastruktur berbeda), dipakai softmax agar tiap piksel memilih kelas yang paling cocok. Hasil akhirnya adalah peta segmentasi: peta yang menandai piksel mana termasuk organ/lesi/jalan/bangunan dan mana yang bukan.
Diringkas: encoder menguasai “apa” (konteks global), decoder menentukan “di mana” (lokasi presisi), skip connections memastikan detail halus tetap terbawa, dan output mengubah semua pemahaman itu menjadi label piksel yang siap dipakai—untuk diagnosis di klinik, pemetaan kota, inspeksi manufaktur, hingga pertanian presisi.
Gambar 2 (Header/Hero): Diagram arsitektur U-Net klasik
Variasi Populer U-Net — Kapan Dipakai & Tips Praktis
A. U-Net++
Kapan dipakai:
- Target berukuran kecil/halus (pembuluh retina, bronkus halus, retakan sempit).
- Perlu batas objek lebih rapih dan stabil terhadap noise.
Kelebihan: Nested skip connections memperkaya
konteks lintas resolusi → kontur lebih halus.
Kekurangan: Lebih berat (memori & waktu).
Tips:
- Aktifkan deep supervision untuk konvergensi lebih cepat.
- Gunakan Dice+CE atau Focal Tversky saat kelas minoritas.
B. Attention U-Net
Kapan dipakai:
- Objek kecil di area besar (polip kecil, nodul paru, jalan tipis pada citra satelit).
- Banyak latar belakang yang “mengganggu”.
Kelebihan: Attention gates menekan latar,
memperkuat sinyal objek target.
Kekurangan: Overhead komputasi ringan–sedang.
Tips:
- Tambahkan weight decay kecil (AdamW) untuk stabilitas.
- Coba threshold tuning saat inferensi (0.3–0.7) untuk F1/Dice optimal.
C. Residual / Dense U-Net
Kapan dipakai:
- Jaringan ingin dibuat lebih dalam tanpa kehilangan gradien.
- Data beragam/kompleks (multi-pusat, variasi alat pemindai, pencahayaan berbeda).
Kelebihan: Gradien stabil, fitur lebih kaya (variasi
tekstur/struktur).
Kekurangan: Parameter bertambah, perlu regularisasi baik.
Tips:
- Residual untuk kedalaman moderat–tinggi; Dense jika ingin reuse fitur kuat.
- Gunakan GroupNorm untuk mini-batch kecil (medis sering batch kecil).
D. nnU-Net (framework)
Kapan dipakai:
- Starting point terkuat di medis tanpa banyak trial-and-error.
- Ingin auto-config (patch size, spacing, augment, loss) berdasar data.
Kelebihan: Baseline sangat kuat; praktik
terbaik sudah dibundel.
Kekurangan: Opsi kustom kadang lebih terbatas, pipeline cukup
opiniated.
Tips:
- Mulai dengan nnU-Net untuk baseline, lalu fine-tune loss/augment spesifik masalah.
- Jaga konsistensi voxel spacing untuk kasus 3D.
E. UNet-Lite / Mobile U-Net
Kapan dipakai:
- Perangkat terbatas (edge device, bedside ultrasound, drone).
- Kebutuhan latensi rendah dengan RAM/GPU kecil.
Kelebihan: Jejak memori kecil, cepat.
Kekurangan: Akurasi bisa turun pada kontur rumit.
Tips:
- Kombinasikan dengan tiling + overlap untuk citra besar.
- Pertimbangkan post-process morfologi (open/close) untuk perbaiki tepi.
Contoh Studi Kasus
Studi kasus segmentasi citra medis dengan U-Net++ di mana kita mensimulasikan segmentasi struktur kecil/halus, seperti pembuluh darah retina atau retakan pada beton.
Rincian Studi Kasus:
- Masalah
yang diselesaikan:
Segmentasi objek kecil dan halus, seperti garis tipis (misalnya pembuluh darah atau retakan mikro), yang memerlukan model untuk menangkap detail halus dengan presisi tinggi. - Dataset
yang digunakan:
Dataset sintetis, yang terdiri dari gambar hitam-putih dengan garis melengkung acak (disimulasikan untuk objek kecil seperti pembuluh darah atau retakan). - Arsitektur
yang digunakan:
U-Net++, yang merupakan pengembangan dari U-Net klasik dengan nested skip connections untuk memperbaiki detail objek kecil dan tepi. - Loss
Function:
Kombinasi Binary Cross-Entropy (BCE) dan Dice Loss untuk meningkatkan ketepatan segmen di area objek yang lebih kecil.
Dataset Sintetis
Untuk membuat data sintetis, kita akan membuat gambar 256x256 dengan beberapa garis acak (misalnya, untuk mensimulasikan pembuluh darah atau retakan) dan memberi label biner (0 untuk latar belakang, 1 untuk objek).
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import random
from torch.utils.data import Dataset, DataLoader
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw
# Dataset Sintetis
class ThinVesselDataset(Dataset):
def __init__(self, num_samples=100, img_size=256):
self.num_samples = num_samples
self.img_size = img_size
def __len__(self):
return self.num_samples
def _random_curve(self, draw):
x = random.randint(20, self.img_size - 20)
y = random.randint(20, self.img_size - 20)
dx = random.randint(10, 30)
for i in range(random.randint(3, 7)):
x = min(self.img_size-1, x + dx + random.randint(-5, 5))
y = min(self.img_size-1, max(0, y + random.randint(-10, 10)))
draw.line([x-5, y, x+5, y], fill=255, width=2)
def __getitem__(self, idx):
img = np.zeros((self.img_size, self.img_size), dtype=np.float32) # background
mask = np.zeros((self.img_size, self.img_size), dtype=np.uint8) # mask kosong
# Create random curves (simulating thin vessels or cracks)
img_pil = Image.fromarray(img)
mask_pil = Image.fromarray(mask)
draw_img = ImageDraw.Draw(img_pil)
draw_mask = ImageDraw.Draw(mask_pil)
# Drawing 3-7 random curves (vessels)
for _ in range(random.randint(3, 7)):
self._random_curve(draw_img)
self._random_curve(draw_mask)
img = np.array(img_pil)
mask = np.array(mask_pil)
img = torch.tensor(img).unsqueeze(0).float() / 255.0 # Normalize to [0, 1]
mask = torch.tensor(mask).unsqueeze(0).float()
return img, mask
# Loader dataset
train_dataset = ThinVesselDataset(num_samples=100)
train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
# Visualisasi sampel data
img, msk = train_dataset[0]
plt.subplot(1, 2, 1); plt.imshow(img[0], cmap='gray'); plt.title("Image")
plt.subplot(1, 2, 2); plt.imshow(msk[0], cmap='gray'); plt.title("Mask")
plt.show()
Arsitektur U-Net++ yang Sederhana
Berikut adalah implementasi U-Net++ yang lebih sederhana. Kita akan menggunakan 2 level saja untuk memudahkan pemahaman, dengan skip connections dan up-sampling.
class SimpleUNetPlusPlus(nn.Module):
def __init__(self, in_channels=1, out_channels=1, base_filters=16):
super(SimpleUNetPlusPlus, self).__init__()
# Encoder (down-sampling)
self.enc1 = self.conv_block(in_channels, base_filters)
self.enc2 = self.conv_block(base_filters, base_filters * 2)
self.enc3 = self.conv_block(base_filters * 2, base_filters * 4)
# Decoder (up-sampling)
self.up2 = nn.ConvTranspose2d(base_filters * 2, base_filters, kernel_size=2, stride=2)
self.up3 = nn.ConvTranspose2d(base_filters * 4, base_filters * 2, kernel_size=2, stride=2)
# Skip Connections
self.skip2 = self.conv_block(base_filters + base_filters * 2, base_filters)
self.skip3 = self.conv_block(base_filters * 2 + base_filters * 4, base_filters * 2)
# Output layer
self.output = nn.Conv2d(base_filters, out_channels, kernel_size=1)
def conv_block(self, in_channels, out_channels):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(inplace=True)
)
def forward(self, x):
# Encoder
enc1 = self.enc1(x)
enc2 = self.enc2(enc1)
enc3 = self.enc3(enc2)
# Decoder + skip connections
up2 = self.up2(enc2)
skip2 = self.skip2(torch.cat([enc1, up2], dim=1)) # Concatenate skip2
up3 = self.up3(enc3)
skip3 = self.skip3(torch.cat([enc2, up3], dim=1)) # Concatenate skip3
# Final output
out = self.output(skip3)
return out
# Model instance
model = SimpleUNetPlusPlus(in_channels=1, out_channels=1, base_filters=16).to(device)
Loss Function dan Optimizer
Untuk training, kita akan menggunakan Binary Cross-Entropy Loss yang digabung dengan Dice Loss untuk lebih memperhatikan ketidakseimbangan kelas.
def dice_loss(pred, target, eps=1e-6):
intersection = (pred * target).sum()
return 1 - (2. * intersection + eps) / (pred.sum() + target.sum() + eps)
def combo_loss(pred, target):
bce_loss = nn.BCEWithLogitsLoss()(pred, target)
dce_loss = dice_loss(torch.sigmoid(pred), target)
return bce_loss + dce_loss
# Optimizer
optimizer = optim.Adam(model.parameters(), lr=1e-3)
Training dan Evaluasi
# Training loop
def train_one_epoch(model, dataloader, optimizer):
model.train()
running_loss = 0.0
for img, msk in dataloader:
img, msk = img.to(device), msk.to(device)
optimizer.zero_grad()
# Forward pass
output = model(img)
# Loss calculation
loss = combo_loss(output, msk)
# Backward pass
loss.backward()
optimizer.step()
running_loss += loss.item()
return running_loss / len(dataloader)
# Training loop example
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
for epoch in range(10):
train_loss = train_one_epoch(model, train_loader, optimizer)
print(f'Epoch [{epoch+1}/10], Loss: {train_loss:.4f}')
Visualisasi Hasil Prediksi
Setelah training, kita dapat melakukan inferensi pada data dan menampilkan hasilnya.
# Inferensi dan visualisasi
def infer_and_plot(model, dataloader):
model.eval()
with torch.no_grad():
img, msk = next(iter(dataloader))
img = img.to(device)
# Prediksi
output = model(img)
output = torch.sigmoid(output)
# Visualisasi hasil
plt.figure(figsize=(12, 6))
plt.subplot(1, 3, 1); plt.imshow(img[0].cpu().numpy()[0], cmap='gray'); plt.title("Input Image")
plt.subplot(1, 3, 2); plt.imshow(msk[0].cpu().numpy()[0], cmap='gray'); plt.title("Ground Truth")
plt.subplot(1, 3, 3); plt.imshow(output[0].cpu().numpy()[0], cmap='gray'); plt.title("Predicted Output")
plt.show()
# Menampilkan hasil inferensi
infer_and_plot(model, train_loader)
Hasilnya
Penjelasan
Dataset Sintetis: Dataset dibuat menggunakan gambar sederhana dengan garis melengkung untuk mensimulasikan pembuluh darah atau retakan.
U-Net++ (sederhana): Hanya menggunakan dua level dan
Loss Function: Menggunakan kombinasi antara
Training & Evaluasi: Proses pelatihan dilakukan dengan optimasi menggunakan Adam dan menggabungkan kedua loss function di atas.
Komentar
Posting Komentar