DINOv2: Panduan Lengkap Vision Foundation Model dari Meta AI untuk Embedding Gambar Tanpa Label
Halo temen-temen, di tutorial kali ini aku mau ngajak kalian kenalan sama salah satu model yang menurutku paling penting di dunia computer vision beberapa tahun terakhir, yaitu DINOv2 dari Meta AI. Kalau kalian sering ngerjain project computer vision, entah itu klasifikasi gambar, image retrieval, segmentation, atau bahkan depth estimation, kalian pasti sering kesel karena harus punya banyak data berlabel dulu baru bisa training model yang bagus. Nah, di sinilah DINOv2 datang sebagai penyelamat. Model ini dilatih secara self-supervised, artinya dia belajar dari jutaan gambar tanpa perlu label sama sekali, tapi hasilnya feature atau embedding yang dia hasilkan itu kuat banget dan bisa dipakai untuk banyak downstream task cuma dengan nambahin head kecil di atasnya.
Aku sendiri udah pakai DINOv2 di beberapa project nyata, mulai dari bikin sistem pencarian gambar mirip sampai klasifikasi produk, dan jujur aja hasilnya sering ngalahin model yang aku latih dari nol dengan susah payah. Yang bikin aku suka, kita nggak perlu training backbone-nya lagi, cukup pakai embedding-nya sebagai fitur, terus latih classifier sederhana di atasnya. Di tutorial ini kita bakal bahas dari instalasi, cara load model lewat torch.hub dan HuggingFace transformers, cara ekstrak CLS token dan patch embedding, cara pakai fitur buat image similarity dan retrieval, sampai training linear classifier alias linear probing. Jadi santai aja, ikutin pelan-pelan, dan aku jamin di akhir kalian bakal paham banget cara kerjanya.
Introduction: Apa Itu DINOv2 dan Kenapa Penting
DINOv2 adalah lanjutan dari DINO (self-DIstillation with NO labels) yang dikembangkan Meta AI. Namanya self-supervised learning, yang artinya model belajar representasi visual tanpa butuh anotasi manusia sama sekali. Bayangin aja, temen-temen, model ini dilatih pakai dataset raksasa berisih ratusan juta gambar yang dikurasi otomatis, dan dia belajar sendiri gimana caranya "memahami" isi gambar. Hasil akhirnya adalah sebuah foundation model, mirip konsepnya kayak model bahasa besar tapi buat gambar.
Kenapa disebut foundation model? Karena satu model ini bisa jadi fondasi buat banyak tugas berbeda. Fitur yang dihasilkan DINOv2 itu general-purpose, artinya nggak spesifik ke satu tugas. Kalian bisa ambil embedding-nya terus pakai buat:
- Klasifikasi gambar (image classification) cuma dengan linear probing
- Image retrieval atau pencarian gambar mirip berdasarkan kemiripan embedding
- Semantic segmentation dengan nambahin decoder head ringan
- Depth estimation alias estimasi kedalaman
- Clustering dan analisis visual lainnya
Yang bikin DINOv2 spesial dibanding model lain seperti CLIP adalah dia murni belajar dari gambar aja, tanpa teks. CLIP butuh pasangan gambar dan teks, sementara DINOv2 cuma butuh gambar. Efeknya, fitur DINOv2 itu sangat kaya secara spasial. Dia paham banget struktur lokal di dalam gambar, makanya bagus banget buat dense prediction task kayak segmentation dan depth.
Arsitektur di balik DINOv2 adalah Vision Transformer (ViT). Gambar dipecah jadi patch-patch kecil, biasanya ukuran 14x14 piksel per patch, terus tiap patch diubah jadi token dan diproses lewat transformer. Ada satu token spesial namanya CLS token yang jadi ringkasan global dari seluruh gambar, dan ada patch token yang mewakili tiap bagian lokal gambar. Nanti kita bakal banyak main sama dua jenis token ini.
DINOv2 tersedia dalam beberapa ukuran, temen-temen, jadi kalian bisa pilih sesuai kebutuhan dan resource:
| Model | Nama | Parameter | Dimensi Embedding |
|-------|------|-----------|-------------------|
| ViT-S/14 | dinov2vits14 | 21 juta | 384 |
| ViT-B/14 | dinov2vitb14 | 86 juta | 768 |
| ViT-L/14 | dinov2vitl14 | 300 juta | 1024 |
| ViT-g/14 | dinov2vitg14 | 1.1 miliar | 1536 |
Buat belajar dan prototyping, aku saranin mulai dari ViT-S atau ViT-B dulu karena ringan dan cepet. Kalau butuh akurasi maksimal dan punya GPU gede, baru naik ke ViT-L atau ViT-g. Angka 14 di belakang nama itu artinya patch size 14 piksel.
Instalasi: Menyiapkan Environment
Sebelum mulai koding, kita siapin dulu environment-nya. Aku selalu saranin pakai virtual environment biar dependency-nya rapi dan nggak nabrak sama project lain. Berikut cara setup-nya.
Pertama, bikin virtual environment dan aktifin:
python -m venv dinov2-env
source dinov2-env/bin/activate # Linux/Mac
atau di Windows: dinov2-env\Scripts\activate
Selanjutnya install PyTorch. Ini komponen paling penting. Kalau kalian punya GPU NVIDIA, install versi CUDA biar kenceng:
# Versi CUDA (kalau punya GPU NVIDIA)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
Versi CPU aja (kalau nggak punya GPU)
pip install torch torchvision
Terus install library pendukung yang kita butuhin sepanjang tutorial ini:
pip install transformers pillow numpy scikit-learn matplotlib
Penjelasan singkat tiap library, temen-temen:
torchdantorchvisionadalah fondasi buat load dan jalanin model DINOv2transformersdari HuggingFace buat cara load model yang lebih gampangpillow(PIL) buat baca dan proses gambarnumpybuat manipulasi arrayscikit-learnbuat bikin linear classifier dan hitung cosine similaritymatplotlibbuat visualisasi hasil
Buat cek apakah PyTorch dan GPU kalian kebaca dengan benar, jalanin script ini:
import torch
print("PyTorch version:", torch.version)
print("CUDA tersedia:", torch.cuda.isavailable())
if torch.cuda.isavailable():
print("Nama GPU:", torch.cuda.getdevicename(0))
Kalau outputnya nunjukin CUDA tersedia True, berarti GPU kalian siap dipakai. Kalau False juga nggak masalah, DINOv2 tetep bisa jalan di CPU cuma agak lebih lambat aja buat model besar.
Basic Usage: Load Model dan Ekstrak Embedding
Oke temen-temen, sekarang kita masuk ke bagian paling seru, yaitu ekstrak embedding dari gambar. Ada dua cara utama buat load DINOv2, yaitu lewat torch.hub langsung dari repo resmi Meta, atau lewat HuggingFace transformers. Aku bakal tunjukin dua-duanya.
Cara 1: Load Lewat torch.hub
Ini cara paling langsung dari repo resmi Meta AI. Cukup satu baris, model bakal otomatis di-download:
import torch
Load model DINOv2 ukuran Small (paling ringan)
model = torch.hub.load('facebookresearch/dinov2', 'dinov2vits14')
model.eval() # set ke mode evaluasi
Pindahin ke GPU kalau ada
device = 'cuda' if torch.cuda.isavailable() else 'cpu'
model = model.to(device)
print("Model berhasil di-load ke", device)
Nah, sekarang kita coba ekstrak fitur dari sebuah gambar. Sebelum masuk ke model, gambar harus di-preprocess dulu, yaitu di-resize dan dinormalisasi pakai statistik ImageNet. Berikut kode lengkapnya:
import torch
import torchvision.transforms as T
from PIL import Image
Definisikan preprocessing sesuai standar DINOv2
transform = T.Compose([
T.Resize(256, interpolation=T.InterpolationMode.BICUBIC),
T.CenterCrop(224),
T.ToTensor(),
T.Normalize(
mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225),
),
])
Baca gambar dan preprocess
image = Image.open('gambarcontoh.jpg').convert('RGB')
imgtensor = transform(image).unsqueeze(0).to(device) # tambah dimensi batch
Ekstrak embedding (CLS token global)
with torch.nograd():
features = model(imgtensor)
print("Shape embedding:", features.shape) # [1, 384] untuk ViT-S
Output features di sini adalah CLS token, yaitu vektor tunggal yang meringkas seluruh gambar. Buat ViT-S dimensinya 384, buat ViT-B 768, dan seterusnya sesuai tabel di atas. Vektor inilah yang jadi "sidik jari" gambar yang bisa kita pakai buat berbagai tugas.
Mengambil CLS Token dan Patch Token Sekaligus
Kadang kita nggak cuma butuh ringkasan global, tapi juga butuh fitur per bagian gambar. Buat itu, DINOv2 punya method forwardfeatures yang ngasih kita akses ke CLS token dan patch token sekaligus:
with torch.nograd():
output = model.forwardfeatures(imgtensor)
output adalah dictionary
clstoken = output['xnormclstoken'] # [1, 384] ringkasan global
patchtokens = output['xnormpatchtokens'] # [1, 256, 384] fitur per patch
print("CLS token shape:", clstoken.shape)
print("Patch token shape:", patchtokens.shape)
Kenapa ada 256 patch? Karena gambar 224x224 dibagi jadi patch 14x14, hasilnya 16x16 = 256 patch. Tiap patch punya vektor 384 dimensi (buat ViT-S). Patch token ini penting banget kalau kalian mau ngerjain segmentation atau mau ngerti bagian mana dari gambar yang penting.
Cara 2: Load Lewat HuggingFace Transformers
Kalau kalian lebih familiar dengan ekosistem HuggingFace, ada cara alternatif yang menurutku lebih rapi buat production karena preprocessing-nya udah dibundel:
from transformers import AutoImageProcessor, AutoModel
from PIL import Image
import torch
Load processor dan model
processor = AutoImageProcessor.frompretrained('facebook/dinov2-base')
model = AutoModel.frompretrained('facebook/dinov2-base')
device = 'cuda' if torch.cuda.isavailable() else 'cpu'
model = model.to(device).eval()
Proses gambar
image = Image.open('gambarcontoh.jpg').convert('RGB')
inputs = processor(images=image, returntensors='pt').to(device)
with torch.nograd():
outputs = model(**inputs)
lasthiddenstate: [1, 257, 768]
index 0 adalah CLS token, sisanya 256 patch token
lasthidden = outputs.lasthiddenstate
clstoken = lasthidden[:, 0, :] # [1, 768]
patchtokens = lasthidden[:, 1:, :] # [1, 256, 768]
pooleroutput juga tersedia sebagai ringkasan
pooled = outputs.pooleroutput # [1, 768]
print("CLS token:", clstoken.shape)
print("Patch tokens:", patchtokens.shape)
Enaknya pakai HuggingFace, AutoImageProcessor udah ngurus semua preprocessing yang benar, jadi kalian nggak perlu pusing mikirin resize dan normalisasi manual. Nama model yang tersedia di HuggingFace antara lain facebook/dinov2-small, facebook/dinov2-base, facebook/dinov2-large, dan facebook/dinov2-giant.
Advanced Usage: Retrieval, Similarity, dan Linear Probing
Sekarang kita udah bisa ekstrak embedding, saatnya pakai embedding itu buat sesuatu yang berguna. Di bagian ini kita bahas tiga aplikasi praktis: image similarity, image retrieval, dan linear probing buat klasifikasi.
Image Similarity: Mengukur Kemiripan Dua Gambar
Konsepnya sederhana, temen-temen. Dua gambar yang mirip bakal punya embedding yang berdekatan di ruang vektor. Cara ngukurnya paling umum pakai cosine similarity. Nilai 1 berarti sangat mirip, nilai 0 berarti nggak ada hubungan, dan nilai -1 berarti berlawanan.
import torch
import torch.nn.functional as F
def ekstrakembedding(pathgambar, model, transform, device):
"""Ekstrak CLS embedding dari satu gambar."""
image = Image.open(pathgambar).convert('RGB')
imgtensor = transform(image).unsqueeze(0).to(device)
with torch.nograd():
emb = model(imgtensor)
return emb
Ambil embedding dua gambar
emb1 = ekstrakembedding('kucing1.jpg', model, transform, device)
emb2 = ekstrakembedding('kucing2.jpg', model, transform, device)
Hitung cosine similarity
similarity = F.cosinesimilarity(emb1, emb2).item()
print(f"Kemiripan: {similarity:.4f}")
if similarity > 0.6:
print("Dua gambar ini mirip!")
else:
print("Dua gambar ini beda.")
Aku sering pakai teknik ini buat deteksi duplikat gambar atau ngelompokin foto produk yang mirip. Threshold 0.6 itu cuma contoh ya, kalian mesti sesuaiin sendiri berdasarkan data kalian.
Image Retrieval: Membangun Sistem Pencarian Gambar
Nah ini yang lebih keren. Bayangin kalian punya database ribuan gambar, terus kalian kasih satu gambar query, dan sistem harus balikin gambar-gambar paling mirip. Ini namanya image retrieval, dan DINOv2 jago banget buat ini. Idenya, kita precompute embedding semua gambar di database, terus pas ada query, kita bandingin embedding query sama semua embedding di database.
import torch
import torch.nn.functional as F
import os
def bangunindex(foldergambar, model, transform, device):
"""Precompute embedding untuk semua gambar di folder."""
embeddings = []
paths = []
for fname in os.listdir(foldergambar):
if fname.lower().endswith(('.jpg', '.png', '.jpeg')):
path = os.path.join(foldergambar, fname)
emb = ekstrakembedding(path, model, transform, device)
embeddings.append(emb)
paths.append(path)
# Gabungkan jadi satu tensor dan normalisasi
embeddings = torch.cat(embeddings, dim=0)
embeddings = F.normalize(embeddings, dim=1)
return embeddings, paths
def carimirip(querypath, embeddings, paths, model, transform, device, topk=5):
"""Cari topk gambar paling mirip dengan query."""
qemb = ekstrakembedding(querypath, model, transform, device)
qemb = F.normalize(qemb, dim=1)
# Dot product antara embedding ternormalisasi = cosine similarity
scores = torch.mm(qemb, embeddings.t()).squeeze(0)
top = torch.topk(scores, k=min(topk, len(paths)))
hasil = []
for skor, idx in zip(top.values, top.indices):
hasil.append((paths[idx], skor.item()))
return hasil
Contoh pemakaian
indexemb, indexpaths = bangunindex('databasegambar/', model, transform, device)
hasil = carimirip('query.jpg', indexemb, indexpaths, model, transform, device, topk=5)
print("Gambar paling mirip:")
for path, skor in hasil:
print(f" {path} -> skor {skor:.4f}")
Trik penting di sini, temen-temen, adalah normalisasi embedding pakai F.normalize. Setelah dinormalisasi, dot product antara dua vektor otomatis sama dengan cosine similarity, jadi kita bisa hitung kemiripan semua gambar sekaligus pakai matrix multiplication yang super cepet. Buat database yang gede banget (jutaan gambar), kalian bisa upgrade pakai library vector search kayak FAISS biar makin ngebut.
Linear Probing: Training Classifier Sederhana di Atas DINOv2
Ini adalah kekuatan sejati foundation model. Alih-alih training seluruh model dari nol yang butuh data banyak dan waktu lama, kita cukup ekstrak embedding DINOv2 (yang di-freeze, nggak diubah), terus latih classifier linear sederhana di atasnya. Teknik ini namanya linear probing, dan seringkali hasilnya udah sangat bagus cuma dengan sedikit data berlabel.
Alurnya begini: ekstrak embedding semua gambar training, terus latih logistic regression atau linear layer buat mapping embedding ke label kelas. Mari kita lihat dua pendekatan.
Pendekatan pertama pakai scikit-learn, cocok buat dataset kecil sampai menengah:
import torch
import numpy as np
from sklearn.linearmodel import LogisticRegression
from sklearn.metrics import accuracyscore
def ekstrakfiturdataset(daftarpath, model, transform, device):
"""Ekstrak embedding untuk list gambar, kembalikan array numpy."""
fitur = []
model.eval()
for path in daftarpath:
image = Image.open(path).convert('RGB')
t = transform(image).unsqueeze(0).to(device)
with torch.nograd():
emb = model(t)
fitur.append(emb.cpu().numpy().squeeze(0))
return np.array(fitur)
Misalkan kita punya data training dan label
trainpaths = ['img1.jpg', 'img2.jpg', '...'] # daftar path
trainlabels = [0, 1, 0] # label tiap gambar
testpaths = ['test1.jpg', 'test2.jpg']
testlabels = [0, 1]
Ekstrak fitur
Xtrain = ekstrakfiturdataset(trainpaths, model, transform, device)
Xtest = ekstrakfiturdataset(testpaths, model, transform, device)
Latih classifier linear
clf = LogisticRegression(maxiter=1000, C=1.0)
clf.fit(Xtrain, trainlabels)
Evaluasi
pred = clf.predict(Xtest)
akurasi = accuracyscore(testlabels, pred)
print(f"Akurasi: {akurasi:.4f}")
Pendekatan kedua pakai PyTorch murni, cocok kalau dataset kalian gede dan mau training pakai GPU dengan mini-batch:
import torch
import torch.nn as nn
class LinearProbe(nn.Module):
"""Classifier linear sederhana di atas embedding DINOv2."""
def init(self, diminput, jumlahkelas):
super().init()
self.fc = nn.Linear(diminput, jumlahkelas)
def forward(self, x):
return self.fc(x)
Asumsikan Xtrain dan ytrain sudah dalam bentuk tensor
Xtrain: [N, 384], ytrain: [N]
Xtraint = torch.tensor(Xtrain, dtype=torch.float32).to(device)
ytraint = torch.tensor(trainlabels, dtype=torch.long).to(device)
probe = LinearProbe(diminput=384, jumlahkelas=2).to(device)
optimizer = torch.optim.AdamW(probe.parameters(), lr=1e-3, weightdecay=1e-4)
criterion = nn.CrossEntropyLoss()
Training loop
probe.train()
for epoch in range(50):
optimizer.zerograd()
logits = probe(Xtraint)
loss = criterion(logits, ytraint)
loss.backward()
optimizer.step()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
Inference
probe.eval()
with torch.nograd():
Xtestt = torch.tensor(Xtest, dtype=torch.float32).to(device)
pred = probe(Xtestt).argmax(dim=1)
print("Prediksi:", pred.cpu().numpy())
Poin penting yang mau aku tekankan, temen-temen: karena backbone DINOv2 di-freeze dan cuma classifier yang dilatih, training-nya cepet banget dan butuh data jauh lebih sedikit dibanding training model dari nol. Aku pernah dapet akurasi di atas 90% cuma dengan beberapa puluh gambar per kelas. Ini yang bikin foundation model tuh sepowerful itu.
Tips: Caching Embedding Biar Nggak Ngulang
Karena ekstraksi embedding itu bagian yang paling makan waktu, aku selalu saranin buat nge-cache hasilnya. Sekali ekstrak, simpen ke disk, terus lain kali tinggal load. Ini ngirit waktu banget kalau kalian bolak-balik eksperimen:
import numpy as np
Simpan embedding ke disk
np.save('trainfeatures.npy', Xtrain)
np.save('trainlabels.npy', np.array(trainlabels))
Load lagi kapan-kapan
Xtrain = np.load('trainfeatures.npy')
trainlabels = np.load('trainlabels.npy')
Best Practices: Tips Praktis dari Pengalaman
Setelah cukup lama pakai DINOv2 di berbagai project, ada beberapa hal yang mau aku bagi biar temen-temen nggak jatuh ke lubang yang sama kayak aku dulu.
Pertama, soal pemilihan ukuran model. Jangan langsung lompat ke ViT-g yang paling gede. Mulai dari ViT-S atau ViT-B dulu buat prototyping. Seringkali ViT-B udah cukup buat sebagian besar kasus, dan selisih akurasinya sama ViT-L nggak sebanding sama tambahan biaya komputasinya. Naik ke model gede cuma kalau kalian bener-bener butuh dan udah mentok.
Kedua, selalu pakai model.eval() dan torch.nograd() waktu inference. Ini bukan cuma soal kebiasaan, tapi bener-bener ngaruh. model.eval() matiin dropout dan bikin batch norm konsisten, sementara torch.nograd() mematikan perhitungan gradien yang bikin inference jauh lebih hemat memori dan lebih cepet. Aku pernah lupa pasang torch.nograd() terus GPU-ku out of memory padahal cuma inference.
Ketiga, soal preprocessing. Konsistensi itu kunci. Kalau kalian ekstrak embedding database pakai preprocessing tertentu, pastikan query juga pakai preprocessing yang sama persis. Beda sedikit aja di resize atau normalisasi bisa bikin embedding-nya jadi nggak nyambung dan hasil retrieval-nya kacau. Makanya aku suka pakai HuggingFace AutoImageProcessor biar konsisten.
Keempat, normalisasi embedding sebelum hitung similarity. Kayak yang udah aku tunjukin di bagian retrieval, normalisasi L2 pakai F.normalize bikin perhitungan cosine similarity jadi gampang dan cepet. Ini best practice yang wajib kalian terapin.
Kelima, buat dataset gede, pikirin soal batching. Jangan ekstrak embedding satu-satu kalau punya ribuan gambar, itu lambat. Proses per batch biar GPU-nya terpakai maksimal:
import torch
from torch.utils.data import DataLoader, Dataset
class GambarDataset(Dataset):
def init(self, paths, transform):
self.paths = paths
self.transform = transform
def len(self):
return len(self.paths)
def getitem(self, idx):
img = Image.open(self.paths[idx]).convert('RGB')
return self.transform(img)
def ekstrakbatch(paths, model, transform, device, batchsize=32):
dataset = GambarDataset(paths, transform)
loader = DataLoader(dataset, batchsize=batchsize, numworkers=4)
semuafitur = []
model.eval()
with torch.nograd():
for batch in loader:
batch = batch.to(device)
emb = model(batch)
semuafitur.append(emb.cpu())
return torch.cat(semuafitur, dim=0)
Keenam, kalau butuh performa production yang tinggi, pertimbangkan pakai mixed precision (FP16) atau export ke ONNX. Mixed precision bisa ngurangin pemakaian memori dan mempercepat inference tanpa ngorbanin akurasi secara signifikan:
# Inference dengan mixed precision
with torch.nograd():
with torch.autocast(devicetype='cuda', dtype=torch.float16):
emb = model(imgtensor)
Ketujuh, buat dense task kayak segmentation, jangan pakai CLS token, pakai patch token. CLS token itu ringkasan global, sementara patch token nyimpen informasi spasial per bagian gambar. Kalian bisa reshape patch token dari [1, 256, dim] jadi grid [16, 16, dim] buat visualisasi atau input ke decoder segmentation.
Kedelapan, hati-hati sama lisensi dan penggunaan. DINOv2 dirilis dengan lisensi yang cukup permisif, tapi tetep cek dulu ketentuan terbarunya dari Meta sebelum pakai buat komersial. Ini penting biar aman secara legal.
Conclusion
Oke temen-temen, kita udah sampai di ujung tutorial. Kita udah belajar banyak banget hari ini, mulai dari konsep dasar DINOv2 sebagai self-supervised vision foundation model, cara load model lewat torch.hub dan HuggingFace transformers, cara ekstrak CLS token dan patch embedding, sampai aplikasi nyata kayak image similarity, image retrieval, dan linear probing buat klasifikasi.
Yang paling aku pengen kalian bawa pulang dari tutorial ini adalah mindset-nya. Di era foundation model kayak sekarang, kita nggak selalu harus training model dari nol dengan susah payah dan butuh data segunung. Cukup ambil model pretrained yang kuat kayak DINOv2, ekstrak embedding berkualitas tinggi darinya, terus latih head kecil di atasnya buat tugas spesifik kalian. Pendekatan ini hemat waktu, hemat data, hemat resource, dan seringkali hasilnya malah lebih bagus.
DINOv2 ini bener-bener game changer buat computer vision, apalagi buat kalian yang kerja di kondisi data berlabel terbatas. Coba deh mulai eksperimen sendiri, pakai gambar-gambar dari project kalian, dan rasain sendiri gimana kuatnya embedding yang dihasilkan. Mulai dari yang sederhana kayak image retrieval, terus naik ke linear probing, dan kalau udah pede, coba deh ke dense task kayak segmentation.
Kalau kalian nemu kesulitan atau ada pertanyaan, jangan sungkan buat eksperimen dan baca dokumentasi resminya. Praktek langsung itu guru terbaik. Semoga tutorial ini bermanfaat buat temen-temen semua, dan sampai ketemu di tutorial berikutnya. Selamat ngoding dan tetap semangat belajar!