00 Görüntüden Anlam — CNN'den ViT'e
Konvolüsyon ile öğrenilen yerel özellikler yarım asırlık bir gelenektir; Vision Transformer bu geleneği kırdı ve global bağımlılıkları tek adımda yakaladı.
Bilgisayarla görme (computer vision) tarihinin büyük bölümünde hâkim paradigma Convolutional Neural Network (CNN) mimarisiydi. Konvolüsyon katmanları, küçük filtreler kaydırarak local receptive field prensibiyle çalışır: her nöron yalnızca girişin küçük bir bölgesini görür. Bu yerel işlem özellik hiyerarşisi oluşturur — ilk katmanlar kenarları, sonrakiler dokuları, derin katmanlar nesneleri öğrenir. Pooling katmanları uzamsal çözünürlüğü azaltırken translasyon değişmezliğini artırır.
CNN'lerin doğasında iki temel sınırlılık vardır. Birincisi, global bağımlılık yakalamak için derin bir ağ gerekir; görüntünün sol köşesindeki bir nesnenin sağ köşesindekiyle ilişkisini anlamak için bilginin çok sayıda konvolüsyon katmanından geçmesi gerekir. İkincisi, receptive field büyütmek için ya ağı derinleştirmek ya da dilated convolution gibi kesmeler kullanmak gerekir — her ikisi de hesaplama maliyeti ve eğitim zorluğu getirir.
2020 yılında Google Brain'den Dosovitskiy ve arkadaşlarının yayımladığı "An Image is Worth 16x16 Words" makalesi radikal bir fikri ortaya attı: görüntüyü küçük patch'lere böl ve NLP'deki transformer ile tam olarak aynı şekilde işle. Bu yaklaşım Vision Transformer (ViT)'in doğuşunu işaret etti. Yeterli veriyle eğitildiğinde ViT, CNN'leri ImageNet gibi standart benchmark'larda geçti ve daha önce NLP'ye özgü olan global dikkat mekanizmasını görüntü dünyasına taşıdı.
"An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale" — Dosovitskiy et al., 2020. ViT ailesinin kurucu makalesi. JFT-300M gibi büyük veri kümelerinde ön-eğitim, ViT'in CNN'lere karşı üstünlüğünü ortaya koymuştur.
ImageNet benchmark'ında yıllar içindeki gelişim, vision mimarilerinin evrimini özetler. Aşağıdaki tablo, öne çıkan mimarilerin parametre sayısını, ImageNet top-1 doğruluğunu ve yayımlanma yılını karşılaştırmaktadır.
| Mimari | Parametre | Top-1 Acc. | Yıl | Temel Yenilik |
|---|---|---|---|---|
| AlexNet | 61M | 63.3% | 2012 | Derin CNN, ReLU, Dropout |
| VGG-16 | 138M | 73.0% | 2014 | 3×3 konvolüsyon yığınları |
| ResNet-50 | 25M | 79.0% | 2015 | Residual connections |
| EfficientNet-B7 | 66M | 84.4% | 2019 | Compound scaling |
| ViT-B/16 | 86M | 81.8% | 2020 | Pure transformer, patches |
| ViT-L/16 | 307M | 85.2% | 2020 | Büyük ViT, JFT pretrain |
| Swin-L | 197M | 87.3% | 2021 | Shifted window attention |
| ViT-G/14 (EVA) | 1.8B | 90.0% | 2023 | Masked image modeling |
CNN ile ViT arasındaki kavramsal farkı somutlaştırmak için basit bir karşılaştırma yapalım. CNN, komşu pikseller arasındaki ilişkiyi konvolüsyon filtresiyle öğrenirken ViT, tüm patch'lerin birbirleriyle olan ilişkisini attention matrisiyle hesaplar.
import torch
import torch.nn as nn
# ── CNN yaklaşımı: yerel, kaydırmalı filtre ──────────────────
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 3×3 konvolüsyon — yalnızca komşu pikselleri görür
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(128, 1000)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv2(
nn.functional.relu(self.conv1(x)))))
return self.fc(x.flatten(1))
# ── ViT yaklaşımı: global, tüm patch'ler aynı anda ──────────
# 224×224 görüntü, 16×16 patch → 196 token
# Her token diğer 195 token'la attention hesaplar
from transformers import ViTModel
import torch
model = ViTModel.from_pretrained("google/vit-base-patch16-224")
x = torch.randn(1, 3, 224, 224) # batch=1, C=3, H=224, W=224
out = model(pixel_values=x)
# last_hidden_state: (1, 197, 768)
# 197 = 196 patch token + 1 CLS token
print(f"ViT çıktısı: {out.last_hidden_state.shape}")
# → torch.Size([1, 197, 768])
01 Patch Embedding
Görüntüyü küçük patch'lere bölerek her birini sabit boyutlu bir embedding vektörüne dönüştürmek, transformer'ın görüntüyü "metin gibi" işlemesine olanak tanır.
Patch embedding, ViT mimarisinin ilk ve kritik adımıdır. Süreç şöyle işler: H×W×C boyutundaki bir girdi görüntüsü, P×P boyutunda N adet örtüşmeyen patch'e bölünür. Burada N = (H/P) × (W/P) ve her patch P²·C boyutunda düzleştirilmiş bir vektöre dönüşür. Standart ViT-Base konfigürasyonu için: 224×224 görüntü, 16×16 patch → N = 14×14 = 196 token.
Bu düzleştirilmiş patch vektörleri (her biri 16²×3 = 768 boyutunda) bir linear projeksiyon ile D-boyutlu embedding uzayına gönderilir. Bu projeksiyon, NLP'deki token embedding matrisinin tam görsel analogudur — öğrenilen bir ağırlık matrisi her patch'i anlamlı bir temsile dönüştürür.
Diziye iki ek öğe eklenir. Birincisi, [CLS] token: BERT'ten ödünç alınan ve tüm dizi bilgisini özetlemek amacıyla öğrenilen bir vektör. Klasifikasyon görevi için bu token'ın çıktısı kullanılır. İkincisi, pozisyonel embedding: transformer inherent olarak sıra bilgisi taşımaz; patch'lerin hangi konumda olduğunu belirtmek için öğrenilen ya da sabit pozisyonel vektörler eklenir. ViT-Base 1D öğrenilmiş pozisyonel embedding kullanır.
import torch
import torch.nn as nn
class PatchEmbedding(nn.Module):
"""
Görüntüyü patch token dizisine dönüştür.
2D konvolüsyon ile lineer projeksiyonu verimli uygula.
Args:
img_size : Girdi görüntüsü boyutu (kare varsayılır)
patch_size : Her patch'in kenar uzunluğu
in_chans : Kanal sayısı (RGB=3)
embed_dim : Çıktı embedding boyutu
"""
def __init__(self,
img_size: int = 224,
patch_size: int = 16,
in_chans: int = 3,
embed_dim: int = 768):
super().__init__()
self.img_size = img_size
self.patch_size = patch_size
self.n_patches = (img_size // patch_size) ** 2
# kernel_size=patch_size, stride=patch_size → örtüşmeyen patch'ler
# Her patch: P²×C → embed_dim (lineer projeksiyon = Conv2d)
self.proj = nn.Conv2d(
in_chans, embed_dim,
kernel_size=patch_size,
stride=patch_size
)
# [CLS] token: tüm dizinin özetini taşır
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
# Pozisyonel embedding: n_patches + 1 (CLS için)
self.pos_embed = nn.Parameter(
torch.zeros(1, self.n_patches + 1, embed_dim)
)
nn.init.trunc_normal_(self.cls_token, std=.02)
nn.init.trunc_normal_(self.pos_embed, std=.02)
def forward(self, x):
# x: (B, C, H, W)
B = x.size(0)
# Konvolüsyon → (B, embed_dim, H/P, W/P)
x = self.proj(x)
# Flatten spatial dims → (B, n_patches, embed_dim)
x = x.flatten(2).transpose(1, 2)
# CLS token'ı batch boyunca genişlet ve başa ekle
cls = self.cls_token.expand(B, -1, -1) # (B, 1, embed_dim)
x = torch.cat([cls, x], dim=1) # (B, 197, embed_dim)
# Pozisyonel embedding ekle
x = x + self.pos_embed
return x
# ─── Test ────────────────────────────────────────────────────
embed = PatchEmbedding(img_size=224, patch_size=16, embed_dim=768)
img = torch.randn(2, 3, 224, 224) # batch=2
tokens = embed(img)
print(f"Patch token sayısı : {embed.n_patches}") # 196
print(f"Toplam dizi uzunluğu: {tokens.shape[1]}") # 197 (196 + CLS)
print(f"Token boyutu : {tokens.shape[2]}") # 768
# → (2, 197, 768)
Patch boyutu, hesaplama maliyetini doğrusal değil karesel etkiler: 16×16 patch → 196 token → 196² attention operasyonu. 8×8 patch seçmek token sayısını 784'e çıkarır ve attention maliyeti dört katına fırlar. Yüksek çözünürlüklü görüntüler için Swin Transformer veya efficient attention varyantlarını tercih edin.
02 Vision Transformer (ViT) Mimarisi
Patch embedding'in ardından standart transformer encoder blokları devreye girer; [CLS] token'ının son durumu sınıflandırma için kullanılır.
ViT mimarisi temelde üç bölümden oluşur: patch embedding katmanı, ardışık transformer encoder blokları ve sınıflandırma head'i. Transformer encoder bloğu NLP transformer'ından birebir alınmıştır: Layer Normalization → Multi-Head Self-Attention → Residual → Layer Normalization → MLP → Residual. Pre-normalization (Post-LN yerine Pre-LN) eğitimi stabilize eder ve büyük ViT modellerinde tercih edilir.
ViT üç ana konfigürasyonda gelir. ViT-Base (ViT-B): 12 katman, 12 kafa, d_model=768, toplam 86M parametre. ViT-Large (ViT-L): 24 katman, 16 kafa, d_model=1024, 307M parametre. ViT-Huge (ViT-H): 32 katman, 16 kafa, d_model=1280, 632M parametre. Patch size iki seçenekle sunulur: 16 (daha fazla token, daha iyi doğruluk) ve 32 (daha az hesaplama).
Sınıflandırma için dizinin sonundaki [CLS] token'ının gizli durumu linear bir başa bağlanır. Pre-training aşamasında JFT-300M (300 milyon görüntü, 30.000 sınıf) veya ImageNet-21k (14M görüntü, 21.841 sınıf) kullanılır. Fine-tuning için bu head değiştirilir.
| Model | Katman | Kafalar | d_model | MLP dim | Parametre |
|---|---|---|---|---|---|
| ViT-Ti/16 | 12 | 3 | 192 | 768 | 5.7M |
| ViT-S/16 | 12 | 6 | 384 | 1536 | 22M |
| ViT-B/16 | 12 | 12 | 768 | 3072 | 86M |
| ViT-L/16 | 24 | 16 | 1024 | 4096 | 307M |
| ViT-H/14 | 32 | 16 | 1280 | 5120 | 632M |
import torch
from transformers import ViTForImageClassification, ViTFeatureExtractor
from PIL import Image
import requests
# ── Model ve processor yükle ──────────────────────────────────
model_name = "google/vit-base-patch16-224"
processor = ViTFeatureExtractor.from_pretrained(model_name)
model = ViTForImageClassification.from_pretrained(model_name)
model.eval()
# ── Görüntü yükle ────────────────────────────────────────────
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
# ── Ön işleme: resize, normalize, tensor ─────────────────────
inputs = processor(images=image, return_tensors="pt")
# inputs["pixel_values"]: (1, 3, 224, 224)
# ── Inference ────────────────────────────────────────────────
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits # (1, 1000)
pred_class = logits.argmax(dim=-1).item()
label = model.config.id2label[pred_class]
print(f"Tahmin: {label}") # → "Egyptian cat" vb.
# ── Feature extraction: [CLS] token embedding ────────────────
from transformers import ViTModel
vit_enc = ViTModel.from_pretrained(model_name)
vit_enc.eval()
with torch.no_grad():
enc_out = vit_enc(**inputs)
# last_hidden_state: (1, 197, 768)
cls_embedding = enc_out.last_hidden_state[:, 0, :] # [CLS] token
print(f"CLS embedding boyutu: {cls_embedding.shape}") # (1, 768)
# Tüm patch embedding'leri al (spatial features)
patch_embeddings = enc_out.last_hidden_state[:, 1:, :] # (1, 196, 768)
print(f"Patch embeddings: {patch_embeddings.shape}")
HuggingFace ViTFeatureExtractor sınıfı deprecate edilmiş; yerine AutoImageProcessor kullanın. İkisi aynı işlemi yapar: görüntüyü 224×224'e resize, mean/std ile normalize ve tensor'a çevirir.
03 CLIP Mimarisi
CLIP, görüntü ve metni aynı embedding uzayına eşleştirerek iki modalite arasında köprü kurar ve sıfır örnekle sınıflandırmayı mümkün kılar.
Contrastive Language-Image Pre-training (CLIP), 2021 yılında OpenAI tarafından tanıtıldı. Temel fikir şudur: İnternetten derlenen 400 milyon görüntü-metin çifti üzerinde, görüntü ile ona karşılık gelen metni aynı noktaya, alakasız görüntü-metin çiftlerini ise birbirinden uzağa iter. Bu işlem için contrastive loss kullanılır.
Mimari iki koldan oluşur. Image encoder: bir Vision Transformer (ViT-B/32 veya ViT-L/14) ya da ResNet. Text encoder: GPT-2 benzeri bir transformer. Her iki encoder çıktısı ortak bir embedding boyutuna projeksiyon yapılır (örn. 512). Eğitim sırasında, aynı batch'teki N görüntü-metin çifti için N×N benzerlik matrisi hesaplanır ve köşegen elemanlar (gerçek çiftler) maksimize edilir.
Sıcaklık parametresi (τ — temperature) contrastive loss'taki keskinliği kontrol eder. Küçük τ, model eşleşen çiftlere güvenle yakınlaştığında sert kararlar verir; büyük τ daha yumuşak bir dağılım üretir. CLIP bu parametreyi eğitim sırasında öğrenir ve yaklaşık 0.07 değerine ulaşır.
import torch
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import requests
# ── CLIP yükle (ViT-B/32 tabanlı) ───────────────────────────
model_name = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(model_name)
model = CLIPModel.from_pretrained(model_name)
model.eval()
# ── Görüntü ve metin girdileri ───────────────────────────────
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
texts = ["a photo of a cat", "a photo of a dog", "a photo of a car"]
inputs = processor(
text=texts,
images=image,
return_tensors="pt",
padding=True
)
# ── Embedding'leri ayrı ayrı al ──────────────────────────────
with torch.no_grad():
img_emb = model.get_image_features(inputs["pixel_values"])
txt_emb = model.get_text_features(
inputs["input_ids"],
inputs["attention_mask"]
)
# L2 normalize et
img_emb = img_emb / img_emb.norm(dim=-1, keepdim=True)
txt_emb = txt_emb / txt_emb.norm(dim=-1, keepdim=True)
# Kosinüs benzerliği: (1, 512) · (3, 512).T → (1, 3)
similarity = (img_emb @ txt_emb.T) * 100
print("Benzerlik skorları:", similarity.softmax(dim=-1))
# → en yüksek skor "a photo of a cat" için beklenir
# Image embedding boyutu: 512
print(f"Görüntü embedding: {img_emb.shape}") # (1, 512)
print(f"Metin embedding : {txt_emb.shape}") # (3, 512)
CLIP'in daha büyük varyantları mevcuttur: openai/clip-vit-large-patch14 (ViT-L/14, 768 boyutlu embedding) ve openai/clip-vit-large-patch14-336 (336×336 girdi). LLaVA ve GPT-4V gibi vision-language modeller genellikle CLIP image encoder'ı backbone olarak kullanır.
04 Zero-Shot Sınıflandırma
CLIP'in öğrendiği ortak embedding uzayı sayesinde hiç görmediği sınıfları, yalnızca metin tanımlarıyla sınıflandırmak mümkündür.
Zero-shot classification CLIP'in en etkileyici özelliğidir. Geleneksel sınıflandırıcılar eğitim setindeki sınıfları tanır ve yeni bir sınıf için yeniden eğitim gerektirir. CLIP'te ise sınıf adları doğal dil cümleleri olarak text encoder'a verilir, görüntü de image encoder'a verilir ve iki embedding kosinüs benzerliğiyle karşılaştırılır. En yüksek skor alan sınıf tahmin olarak döner — hiç eğitim gerekmez.
Sınıf tanımının kalitesi doğruluğu doğrudan etkiler. Kısa etiket adı ("cat") yerine bağlam cümlesi ("a photo of a cat") kullanmak tipik olarak %1-3 doğruluk artışı sağlar. Bu, OpenAI'ın prompt engineering bulguları arasındadır. ImageNet sınıfları için "a photo of a {}, a type of animal" gibi şablonlar kullanılabilir.
Top-k prediction için logit'leri softmax'ten geçirip artan sırayla sıralamak yeterlidir. CLIP ile ImageNet üzerinde zero-shot olarak %76+ top-1 doğruluk elde edilebilir — bu, tam olarak eğitilmiş erken dönem CNN'lerle rekabet eder.
import torch
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import requests
model_name = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(model_name)
model = CLIPModel.from_pretrained(model_name)
model.eval()
# ── ImageNet benzeri sınıf listesi ───────────────────────────
imagenet_classes = [
"tench", "goldfish", "great white shark", "tiger shark",
"hammerhead shark", "electric ray", "stingray", "cock",
"hen", "ostrich", "tabby cat", "tiger cat",
"Persian cat", "Siamese cat", "Egyptian cat",
]
# ── Prompt şablonu: basit etiket yerine cümle ────────────────
def make_prompts(classes):
return [f"a photo of a {c}" for c in classes]
prompts = make_prompts(imagenet_classes)
# ── Test görüntüsü ────────────────────────────────────────────
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
# ── Toplu metin embedding ─────────────────────────────────────
txt_inputs = processor(text=prompts, return_tensors="pt", padding=True)
img_inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
txt_emb = model.get_text_features(**txt_inputs)
img_emb = model.get_image_features(**img_inputs)
txt_emb = txt_emb / txt_emb.norm(dim=-1, keepdim=True)
img_emb = img_emb / img_emb.norm(dim=-1, keepdim=True)
# ── Skor hesapla ve Top-5 göster ─────────────────────────────
logits = (100.0 * img_emb @ txt_emb.T).squeeze() # (15,)
probs = logits.softmax(dim=-1)
top5 = probs.topk(5)
print("── Top-5 Zero-Shot Tahminler ──")
for prob, idx in zip(top5.values, top5.indices):
print(f" {imagenet_classes[idx]:20s} {prob.item()*100:.1f}%")
# ── Kendi görüntünüzle deneyin ────────────────────────────────
def zero_shot_classify(image_path: str, class_names: list[str], top_k: int = 3):
"""Yerel görüntü için zero-shot sınıflandırma."""
image = Image.open(image_path).convert("RGB")
prompts = [f"a photo of a {c}" for c in class_names]
inputs = processor(text=prompts, images=image,
return_tensors="pt", padding=True)
with torch.no_grad():
out = model(**inputs)
logits = out.logits_per_image.squeeze()
probs = logits.softmax(dim=-1)
top = probs.topk(top_k)
return [(class_names[i], p.item()) for p, i in zip(top.values, top.indices)]
CLIP openai/clip-vit-large-patch14 modeli, prompt kalitesiyle birleşince ImageNet'te zero-shot %76.2 top-1 doğruluk sağlar. Ensemble prompt stratejisi (birden fazla şablonun ortalaması) bu değeri %77.0'a çıkarır. open_clip kütüphanesi daha güçlü açık kaynak CLIP varyantlarına erişim sağlar.
05 SAM — Segment Anything Model
Meta'nın SAM modeli, görüntü segmentasyonunu bir foundation model sorununa dönüştürdü: tek bir model her nesneyi, her görüntüde segmentleyebilir.
Segment Anything Model (SAM), Meta AI Research tarafından 2023 yılında tanıtıldı. Temel iddiası şudur: 1 milyar+ maske ve 11 milyon görüntü üzerinde eğitilen tek bir model, herhangi bir prompt ile herhangi bir nesneyi segmentleyebilir. Bu, segmentasyon görevini prompt engineering sorununa indirgedi ve zero-shot segmentasyonu mümkün kıldı.
SAM mimarisi üç bileşenden oluşur. Image encoder: ViT-H (632M parametre) tabanlı, görüntüyü bir dizi embedding'e dönüştürür. Bu hesaplama görüntü başına bir kez yapılır ve önbelleğe alınabilir. Prompt encoder: nokta, bounding box veya maske promptlarını embedding'e çevirir. Mask decoder: transformer tabanlı, image embedding ve prompt embedding'i birleştirerek maske tahmin eder. Maskenin kesin belirsizliğine (hangi nesneyi kastettiniz?) karşı multimask output ile birden fazla alternatif maske üretir.
SAM1 ile SAM2 arasındaki temel fark: SAM2 (2024) video segmentasyonunu destekler, daha hızlı çalışır ve daha küçük model varyantları sunar. SAM2, video üzerinde nesneleri zamansal tutarlılıkla takip edebilir.
| Model | Image Enc. | Parametre | Video | Hız |
|---|---|---|---|---|
| SAM (ViT-B) | ViT-B | 91M | Hayır | Hızlı |
| SAM (ViT-L) | ViT-L | 308M | Hayır | Orta |
| SAM (ViT-H) | ViT-H | 636M | Hayır | Yavaş |
| SAM2 (Hiera-T) | Hiera-T | 38.9M | Evet | Çok hızlı |
| SAM2 (Hiera-L) | Hiera-L | 224M | Evet | Orta |
# ── Kurulum ───────────────────────────────────────────────────
# pip install segment-anything
# Checkpoint indir: SAM ViT-H
# wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth
import torch
import numpy as np
from segment_anything import sam_model_registry, SamPredictor
from PIL import Image
# ── Model yükleme ─────────────────────────────────────────────
sam_checkpoint = "sam_vit_h_4b8939.pth"
model_type = "vit_h" # "vit_b" | "vit_l" | "vit_h"
device = "cuda" if torch.cuda.is_available() else "cpu"
sam = sam_model_registry[model_type](checkpoint=sam_checkpoint)
sam.to(device=device)
print(f"SAM yüklendi — cihaz: {device}")
print(f"Parametre sayısı: {sum(p.numel() for p in sam.parameters()):,}")
# ── SAM2 için alternatif (daha yeni) ─────────────────────────
# pip install sam2
# from sam2.build_sam import build_sam2
# from sam2.sam2_image_predictor import SAM2ImagePredictor
#
# sam2 = build_sam2("sam2_hiera_large.yaml", "sam2_hiera_large.pt")
# predictor = SAM2ImagePredictor(sam2)
ViT-H tabanlı SAM modeli 2.4 GB disk alanı kaplar ve inference için en az 8 GB GPU belleği gerektirir. Geliştirme ortamında ViT-B versiyonunu (375 MB) kullanmak daha pratiktir. Üretimde GPU olmadan CPU inference mümkündür ancak görüntü başına 30-60 saniye sürebilir.
06 SAM Prompting
SAM'a üç farklı prompt türü verilebilir: nokta koordinatı, bounding box veya maske — her biri farklı kullanım senaryolarına uygundur.
SAM'ın güçlü tarafı, çok çeşitli promptları anlayabilmesidir. Nokta prompt: görüntüde bir (x, y) koordinatı belirt; foreground (1) veya background (0) etiketiyle birlikte gönderilir. SAM bu noktayı içeren en olası nesneyi segmentler. Birden fazla foreground/background noktası verilerek segmentasyonu rafine edebilirsiniz. Bounding box prompt: (x1, y1, x2, y2) formatında dikdörtgen verin; SAM bu kutu içindeki ana nesneyi segmentler. Nesnenin tam sınırını bilmiyorsanız gevşek bir box bile iyi sonuç verir. Maske prompt: önceki bir segmentasyonu giriş olarak vererek yinelemeli iyileştirme yapabilirsiniz.
Multimask output, SAM'ın belirsizliği çözmek için üç alternatif maske üretmesidir: bütün nesne, parça ve küçük bölge. Hangi maskenin doğru olduğunu belirtmek zorunda değilsiniz; model her maskeye bir güven skoru atar.
import numpy as np
import cv2
from segment_anything import sam_model_registry, SamPredictor
# SAM predictoru başlat
sam = sam_model_registry["vit_b"](checkpoint="sam_vit_b_01ec64.pth")
predictor = SamPredictor(sam)
# Görüntüyü yükle ve ayarla (image encoder bir kez çalışır)
image = cv2.imread("photo.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
predictor.set_image(image) # ViT-H image embedding hesapla
# ── Nokta prompt: kedi üzerinde bir nokta ────────────────────
input_point = np.array([[500, 375]]) # x, y koordinatı
input_label = np.array([1]) # 1=foreground, 0=background
masks, scores, logits = predictor.predict(
point_coords = input_point,
point_labels = input_label,
multimask_output = True, # 3 alternatif maske
)
# masks : (3, H, W) bool array
# scores: (3,) — her maskenin güven skoru
best_mask = masks[np.argmax(scores)] # en yüksek skorlu maske
print(f"En iyi maske skoru: {scores.max():.3f}")
print(f"Maske boyutu : {best_mask.shape}") # (H, W)
# ── Bounding box prompt ───────────────────────────────────────
input_box = np.array([425, 600, 700, 875]) # x1, y1, x2, y2
masks_box, scores_box, _ = predictor.predict(
point_coords = None,
point_labels = None,
box = input_box[None, :], # (1, 4)
multimask_output = False,
)
print(f"Box prompt maske skoru: {scores_box[0]:.3f}")
# ── Maske görselleştir (opencv) ───────────────────────────────
def overlay_mask(img, mask, alpha=0.5, color=(0, 255, 0)):
overlay = img.copy()
overlay[mask] = color
return cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0)
result = overlay_mask(image, best_mask)
cv2.imwrite("segmented.jpg", cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
from segment_anything import SamAutomaticMaskGenerator
import numpy as np
import cv2
# ── Otomatik maske üretici: tüm nesneleri bul ────────────────
mask_generator = SamAutomaticMaskGenerator(
model = sam,
points_per_side = 32, # grid yoğunluğu
pred_iou_thresh = 0.86, # IoU eşiği
stability_score_thresh = 0.92, # kararlılık eşiği
crop_n_layers = 1,
crop_n_points_downscale_factor = 2,
min_mask_region_area = 100, # px² — küçük maskeleri filtrele
)
image = cv2.imread("photo.jpg")[..., ::-1] # BGR → RGB
masks = mask_generator.generate(image)
print(f"Tespit edilen nesne sayısı: {len(masks)}")
# Her maske bir sözlük: segmentation, area, bbox, predicted_iou...
for i, m in enumerate(masks[:3]):
print(f" Maske {i}: alan={m['area']}, IoU={m['predicted_iou']:.3f}, "
f"bbox={m['bbox']}")
07 timm Kütüphanesi
PyTorch Image Models (timm), 1000'den fazla pretrained vision modelini tek API ile sunar ve vision araştırmalarının de facto standart kütüphanesidir.
timm (PyTorch Image Models), Ross Wightman tarafından geliştirilen ve şu an HuggingFace bünyesindeki açık kaynak bir vision model kütüphanesidir. ResNet, EfficientNet, ConvNeXt, Swin Transformer, DeiT, ViT ve daha pek çok mimariyi tek satır kodla yükleyip fine-tune etmenizi sağlar. Tüm modeller ImageNet üzerinde ön eğitimlere sahiptir ve tutarlı bir API sunar.
timm.create_model() fonksiyonu, model adı ve birkaç parametre ile tam bir model oluşturur. pretrained=True ile ImageNet ağırlıkları otomatik indirilir. num_classes=0 ile classification head kaldırılır ve model saf bir feature extractor'a dönüşür. features_only=True ise ara katman çıktılarını (feature map) döndürür; bu dense prediction (segmentasyon, detection) görevleri için kullanışlıdır.
# pip install timm
import timm
import torch
# ── Mevcut modelleri listele ──────────────────────────────────
all_models = timm.list_models()
print(f"Toplam model sayısı: {len(all_models)}") # 1000+
# Belirli mimariyi filtrele
vit_models = timm.list_models("vit_*", pretrained=True)
print(f"Pretrained ViT modelleri: {len(vit_models)}")
# ── Model oluşturma ───────────────────────────────────────────
# Standart sınıflandırıcı (ImageNet 1000 sınıf)
model = timm.create_model("resnet50", pretrained=True)
model.eval()
# Swin Transformer
swin = timm.create_model("swin_base_patch4_window7_224", pretrained=True)
# EfficientNet-B4
eff = timm.create_model("efficientnet_b4", pretrained=True)
# ── Feature extractor modu (head kaldır) ──────────────────────
feat_model = timm.create_model(
"resnet50",
pretrained = True,
num_classes = 0, # classification head kaldırıldı
global_pool = "" # global pooling kaldırıldı → spatial features
)
x = torch.randn(2, 3, 224, 224)
feat = feat_model(x)
print(f"ResNet50 feature map: {feat.shape}") # (2, 2048, 7, 7)
# ── features_only modu: ara katman çıktıları ─────────────────
multi_scale = timm.create_model(
"resnet50",
pretrained = True,
features_only = True,
out_indices = (1, 2, 3, 4) # stage 2-5 çıktıları
)
feats = multi_scale(x)
for i, f in enumerate(feats):
print(f"Stage {i+2}: {f.shape}")
# Stage 2: (2, 256, 56, 56)
# Stage 3: (2, 512, 28, 28)
# Stage 4: (2, 1024, 14, 14)
# Stage 5: (2, 2048, 7, 7)
# ── Model konfigürasyon bilgisi ───────────────────────────────
cfg = timm.get_pretrained_cfg("resnet50")
print(f"Giriş boyutu : {cfg.input_size}") # (3, 224, 224)
print(f"Mean / Std : {cfg.mean} / {cfg.std}")
print(f"Interpolasyon : {cfg.interpolation}")
08 Feature Extraction ile Transfer Learning
Büyük veri kümesinde öğrenilen ağırlıkları dondurup yalnızca küçük bir classification head eğitmek, sınırlı veriyle bile güçlü sonuçlar üretir.
Transfer learning'in iki ana stratejisi vardır. Feature extraction: pretrained modelin tüm ağırlıkları dondurulur (requires_grad=False); yalnızca yeni eklenen classification head eğitilir. Bu yaklaşım az veriyle bile iyi çalışır, eğitim çok hızlıdır ancak domain farklıysa alt katman özellikleri yetersiz kalabilir. Full fine-tuning: tüm ağırlıklar güncellenir; daha yüksek doğruluk sağlar ancak daha fazla veri ve hesaplama gerektirir. Kural olarak: hedef domain kaynak domain'e (ImageNet) yakınsa feature extraction tercih edilir; uzaksa (uydu görüntüsü, tıbbi görüntü) fine-tuning zorunlu hale gelir.
Pratikte hibrit bir yaklaşım da yaygındır: başta feature extraction yapın (head eğitin), ardından tüm modeli çok düşük learning rate ile fine-tune edin. Bu, modelin ilk durumundan çok sapmadan yavaş bir uyum sağlamasına izin verir.
import torch
import torch.nn as nn
import timm
from torch.utils.data import DataLoader, TensorDataset
# ── Frozen ViT + custom head ──────────────────────────────────
class FrozenViTClassifier(nn.Module):
def __init__(self, num_classes: int, model_name: str = "vit_base_patch16_224"):
super().__init__()
# Pretrained backbone: head kaldır
self.backbone = timm.create_model(model_name, pretrained=True, num_classes=0)
# Tüm backbone ağırlıklarını dondur
for param in self.backbone.parameters():
param.requires_grad = False
# Sadece bu head eğitilecek
embed_dim = self.backbone.num_features # 768 ViT-B için
self.head = nn.Sequential(
nn.LayerNorm(embed_dim),
nn.Linear(embed_dim, 256),
nn.GELU(),
nn.Dropout(0.3),
nn.Linear(256, num_classes)
)
def forward(self, x):
with torch.no_grad():
features = self.backbone(x) # (B, 768) — CLS token
return self.head(features)
# ── Eğitim döngüsü ────────────────────────────────────────────
def train_head(model, train_loader, val_loader, epochs=10, lr=1e-3):
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
# Yalnızca head parametrelerini optimize et
optimizer = torch.optim.AdamW(model.head.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
for epoch in range(epochs):
model.train()
total_loss = 0.0
for imgs, labels in train_loader:
imgs, labels = imgs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(imgs), labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
# Validasyon
model.eval()
correct = total = 0
with torch.no_grad():
for imgs, labels in val_loader:
imgs, labels = imgs.to(device), labels.to(device)
preds = model(imgs).argmax(dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
print(f"Epoch {epoch+1:2d} | Loss: {total_loss/len(train_loader):.4f} "
f"| Val Acc: {correct/total*100:.1f}%")
# ── Örnek kullanım ────────────────────────────────────────────
NUM_CLASSES = 10
model = FrozenViTClassifier(num_classes=NUM_CLASSES)
# Kaç parametre eğitiliyor?
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Eğitilen parametre : {trainable:,}") # ~200K
print(f"Toplam parametre : {total:,}") # ~86M
print(f"Eğitim oranı : {trainable/total*100:.2f}%") # ~0.23%
Backbone ağırlıklarını dondurup tüm görüntüleri önceden işleyerek embedding'leri diske kaydetmek (pre-computing features), eğitim sürecini dramatik biçimde hızlandırır. Backbone forward pass her epoch'ta tekrar çalışmaz; yalnızca baş eğitilir.
09 Fine-tuning Vision Models
Tüm model ağırlıklarını küçük bir learning rate ile güncellemek, domain'e özgü özelliklerin yeniden uyarlanmasını sağlar ve en yüksek doğruluk potansiyelini sunar.
Full fine-tuning kararını etkileyen faktörler: elinizde ne kadar veri var? Hedef domain, ImageNet'e ne kadar yakın? Hesaplama bütçeniz nedir? Genel kural olarak, <1000 örnek için feature extraction, 1000-10000 için kısmi fine-tuning (son birkaç katman), >10000 için full fine-tuning tercih edilir.
Data augmentation, küçük veri setlerinde overfitting'i önlemek için kritiktir. torchvision.transforms ile standart augmentation: RandomHorizontalFlip (yatay yansıma), ColorJitter (parlaklık, kontrast, doygunluk), RandomCrop (rastgele kırpma), RandomRotation. Daha güçlü augmentation için RandAugment veya AugMix kullanılabilir.
Learning rate seçimi kritiktir. Pretrained ağırlıkları bozmamak için çok düşük LR (1e-5 ile 1e-4 arası) kullanılır. Differential learning rate: alt katmanlar için daha düşük LR, üst katmanlar için daha yüksek LR. Warmup + cosine decay kombinasyonu stabil eğitim sağlar.
import torch
import torch.nn as nn
import timm
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from torch.optim.lr_scheduler import CosineAnnealingLR
# ── Augmentation ──────────────────────────────────────────────
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.4, contrast=0.4,
saturation=0.4, hue=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std =[0.229, 0.224, 0.225]),
])
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std =[0.229, 0.224, 0.225]),
])
# ── Dataset (ImageFolder yapısı) ─────────────────────────────
# data/
# train/ class_a/ *.jpg
# class_b/ *.jpg
# val/ class_a/ *.jpg
# class_b/ *.jpg
train_ds = datasets.ImageFolder("data/train", transform=train_transform)
val_ds = datasets.ImageFolder("data/val", transform=val_transform)
train_dl = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4)
val_dl = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4)
# ── Model: ViT + custom head ──────────────────────────────────
num_classes = len(train_ds.classes)
model = timm.create_model(
"vit_base_patch16_224",
pretrained = True,
num_classes = num_classes
)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
# ── Differential learning rate ────────────────────────────────
# Backbone için düşük LR, head için yüksek LR
param_groups = [
{"params": [p for n, p in model.named_parameters()
if "head" not in n],
"lr": 1e-5}, # backbone
{"params": model.head.parameters(),
"lr": 1e-4}, # classification head
]
optimizer = torch.optim.AdamW(param_groups, weight_decay=0.01)
scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-7)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# ── Eğitim döngüsü ────────────────────────────────────────────
def run_epoch(model, loader, optimizer=None, train=True):
model.train() if train else model.eval()
total_loss = correct = total = 0
ctx = torch.enable_grad() if train else torch.no_grad()
with ctx:
for imgs, labels in loader:
imgs, labels = imgs.to(device), labels.to(device)
if train: optimizer.zero_grad()
logits = model(imgs)
loss = criterion(logits, labels)
if train:
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item()
correct += (logits.argmax(1) == labels).sum().item()
total += labels.size(0)
return total_loss / len(loader), correct / total
EPOCHS = 20
best_val_acc = 0.0
for ep in range(EPOCHS):
tr_loss, tr_acc = run_epoch(model, train_dl, optimizer, train=True)
vl_loss, vl_acc = run_epoch(model, val_dl, train=False)
scheduler.step()
print(f"Ep {ep+1:2d} | TrLoss {tr_loss:.3f} TrAcc {tr_acc*100:.1f}% "
f"| VlLoss {vl_loss:.3f} VlAcc {vl_acc*100:.1f}%")
if vl_acc > best_val_acc:
best_val_acc = vl_acc
torch.save(model.state_dict(), "best_vit.pt")
print(f" ✓ En iyi model kaydedildi (acc={vl_acc*100:.2f}%)")
01 Görüntü → RandomResizedCrop + Augmentation → Normalize 02 PatchEmbedding → [CLS] + pos_embed → Transformer Encoder × 12 03 [CLS] token → LayerNorm → Linear head → logits 04 CrossEntropyLoss (label_smoothing=0.1) → AdamW + CosineAnnealing 05 Gradient Clip (norm=1.0) → Backward → Step → Checkpoint
label_smoothing=0.1 ile CrossEntropyLoss, modelin tahminlerine aşırı güvenmesini engeller. Sıfır yerine hedef dağılımın 0.1 payını eşit şekilde tüm sınıflara dağıtır. Bu basit teknik genellikle 0.5-1.0% doğruluk artışı sağlar ve model kalibrasyonunu iyileştirir.