Tüm rehberler
Rehber Yapay Zeka 15 · Mimari

Multimodal Modeller
Görüntü ile Metni Birleştir.

Metin ile görüntüyü aynı model içinde birleştir. LLaVA mimarisi, image encoder ve projection layer. Visual instruction tuning, document understanding ve chart analizi.

00 Neden Multimodal?

Tek modalite modelleri gerçek dünya problemlerinin yalnızca bir boyutunu anlayabilir; multimodal modeller metin ve görüntüyü bir arada işleyerek bu sınırı aşar.

GPT-3'ün veya LLaMA'nın metin yetenekleri ne kadar etkileyici olursa olsun, bir fotoğrafa bakıp içeriği açıklama, bir grafiği analiz etme ya da bir belgeden tablo çıkarma gibi görevler için yetersiz kalırlar. Görüntü içermesi gereken bir konuşmada, saf dil modeli ya görseli hiç işleyemez ya da onu metin olarak tanımlayan bir insan aracısına ihtiyaç duyar. Vision-Language Model (VLM) — ya da daha genel adıyla multimodal model — bu boşluğu kapatır: hem görüntü hem de metin token'ları aynı model içinde işlenir.

GPT-4V (2023), Gemini Pro Vision ve LLaVA gibi modeller bu alanda dönüm noktaları oldu. GPT-4V, karmaşık grafik analizi, kod diyagramları ve görsel muhakeme gerektiren görevlerde insan düzeyine yaklaşan performans sergiledi. Açık kaynak tarafında ise LLaVA, araştırmacılara kendi VLM'lerini eğitip geliştirme imkânı sundu.

Kullanım Alanları

Görüntü AçıklamaBir fotoğrafın içeriğini doğal dilde tanımla — erişilebilirlik, içerik moderasyonu
Visual QAGörüntü hakkında sorulara yanıt ver — "Bu grafikte en yüksek değer nedir?"
Document QAFatura, sözleşme veya form görsellerinden bilgi çıkar
Chart AnaliziGrafik ve diyagramları yorumla, trendleri açıkla, veriyi tablo formatına dönüştür
Medical ImagingRöntgen veya MRI gibi tıbbi görüntüleri metin raporlarıyla ilişkilendir
Kod + DiyagramMimari diyagramlardan kod üret, UI mockup'larından HTML yaz

Modalite Birleştirme Stratejileri

VLM'ler modaliteleri birleştirmek için farklı yaklaşımlar kullanır. Early fusion: görüntü ve metin en baştan birleştirilir, model her şeyi birlikte öğrenir — çok veri gerektirir. Late fusion: her modalite bağımsız işlenir, sonuçlar birleştirilir — esneklik yüksektir ama modaliteler arası etkileşim zayıftır. Cross-attention fusion: LLaVA ve GPT-4V'nin kullandığı yaklaşım; görüntü encoder'ı bağımsız çalışır, ardından LLM katmanlarına cross-attention ile entegre edilir. En yaygın ve başarılı yaklaşım budur.

01 Görüntü → Visual Encoder → Feature map (196, 1024)
02 Feature map → Projection Layer → LLM token boyutu (196, 4096)
03 [System tokens] + [196 image tokens] + [Text tokens] → LLM
04 LLM → Autoregressif yanıt üretimi

01 Visual Encoder

Visual encoder, ham piksel değerlerini LLM'nin anlayabileceği yoğun özellik vektörlerine dönüştüren mimarinin ilk katmanıdır.

VLM'lerin büyük çoğunluğu visual encoder olarak Vision Transformer (ViT) tabanlı bir CLIP modeli kullanır. CLIP (Contrastive Language-Image Pre-Training), büyük ölçekte görüntü-metin çiftleri üzerinde contrastive öğrenme ile eğitilmiştir; bu sayede görsel özellikler zaten dil semantiğiyle hizalanmış olarak gelir — VLM için mükemmel bir başlangıç noktasıdır.

CLIP ViT-L/14

CLIP ViT-L/14, LLaVA-1.0 ve pek çok açık VLM'de kullanılan standart visual encoder'dır. Görüntüyü 224×224'e yeniden boyutlandırır ve 14×14 piksellik patch'lere böler: 224/14 = 16, yani 16×16 = 256 patch. Her patch bir token'a dönüştürülür; [CLS] token dahil 257 token üretilir. Her token 1024 boyutlu bir vektördür. Çıktı: (257, 1024) feature matrisi — ancak çoğu VLM [CLS] token'ı çıkarır ve (256, 1024) kullanır.

SigLIP

Google'ın SigLIP (Sigmoid Loss for Language-Image Pre-training) modeli, softmax yerine sigmoid contrastive loss kullanır. Bu önemli bir farktır çünkü softmax tüm batch'deki negatif örnekleri normalizer olarak kullanırken, sigmoid her çifti bağımsız değerlendirir. Sonuç: daha küçük batch size'da daha iyi eğitim, özellikle düşük çözünürlüklü görüntülerde CLIP'e üstünlük. LLaVA-1.6 ve PaliGemma SigLIP kullanır.

EVA-CLIP

EVA-CLIP, masked image modeling (EVA-02) ile CLIP pretraining'i birleştiren daha güçlü bir encoder'dır. LLaVA-1.5 ve InternVL bu encoder'ı tercih eder. 336×336 görüntüyü 14×14 patch'lerle işleyerek (336/14)² = 576 token üretir — daha yüksek çözünürlük daha ince detayları yakalar.

clip_visual_encoder.py
import torch
from transformers import CLIPVisionModel, CLIPImageProcessor
from PIL import Image

# ── CLIP ViT-L/14 visual encoder yükle ──────────────────────
model_id = "openai/clip-vit-large-patch14"
processor = CLIPImageProcessor.from_pretrained(model_id)
vision_model = CLIPVisionModel.from_pretrained(
    model_id,
    torch_dtype=torch.float16
).to("cuda")

# ── Görüntüyü işle ───────────────────────────────────────────
img = Image.open("sample.jpg").convert("RGB")
inputs = processor(images=img, return_tensors="pt")
pixel_values = inputs["pixel_values"].half().to("cuda")
print(f"Pixel values shape: {pixel_values.shape}")  # (1, 3, 224, 224)

# ── Feature extraction ───────────────────────────────────────
with torch.no_grad():
    outputs = vision_model(pixel_values=pixel_values, output_hidden_states=True)

# last_hidden_state: tüm token'ların çıktısı
features = outputs.last_hidden_state
print(f"Feature map shape: {features.shape}")        # (1, 257, 1024)
# 257 = 256 patch token + 1 [CLS] token

# LLaVA gibi modeller genellikle [CLS] token'ı atar
patch_features = features[:, 1:, :]                 # (1, 256, 1024)
print(f"Patch features: {patch_features.shape}")     # (1, 256, 1024)

# İkinci-sondan önceki katmanı kullanmak bazen daha iyi sonuç verir
penultimate = outputs.hidden_states[-2][:, 1:, :]  # (1, 256, 1024)
print(f"Penultimate layer: {penultimate.shape}")
NOT

LLaVA-1.5 sondan ikinci katmanı (hidden_states[-2]) kullanır, son katmanı değil. Araştırmalar, CLIP'in son katmanının dil semantiğine çok yakın, görsel detaylardan uzak olduğunu göstermiştir. Penultimate katman daha zengin görsel özellikler taşır.

02 Projection Layer

Visual encoder çıktısı ile LLM giriş boyutu farklıdır; projection layer bu boyut uyumsuzluğunu çözer ve iki modaliteyi ortak bir uzayda hizalar.

CLIP ViT-L/14'ün çıkardığı feature'lar 1024 boyutludur. Ancak tipik bir LLM (örn. Vicuna-13B) 5120 boyutlu token embedding bekler. Bu uyumsuzluğu gidermek için projection layer kullanılır: (N, 1024) → (N, 5120) dönüşümü, burada N görüntü token sayısıdır.

LLaVA'nın evriminde projection layer tasarımı önemli bir rol oynadı. LLaVA-1.0 tek bir linear layer kullandı — basit ama etkili bir başlangıç. LLaVA-1.5 ise iki katmanlı MLP (multi-layer perceptron) ile bunu iyileştirdi: Linear → GELU → Linear. Bu küçük değişiklik pek çok VQA benchmark'ında belirgin iyileşme sağladı. Daha karmaşık projectionlar (Q-Former, Perceiver Resampler) da mevcuttur ancak bu ek karmaşıklık her zaman karşılığını vermez.

Görsel Token Sayısı

Projection sonrasında görüntü, LLM tarafından sıralı bir token dizisi olarak işlenir. 256 görüntü token, LLM'nin context window'unun önemli bir bölümünü kaplar. Yüksek çözünürlüklü modeller (LLaVA-1.6) görüntüyü birden fazla parçaya (tile) böler ve her tile için ayrı encoder çalıştırır — bu 1000+ görüntü token anlamına gelir. Daha uzun context, daha ince detay farkındalığı sağlar ama işlem maliyeti artar.

projection_layer.py
import torch
import torch.nn as nn

# ── LLaVA-1.0: Tek linear projeksiyon ───────────────────────
class LinearProjection(nn.Module):
    """LLaVA-1.0 tarzı: tek linear katman."""
    def __init__(self, visual_dim: int = 1024, llm_dim: int = 4096):
        super().__init__()
        self.proj = nn.Linear(visual_dim, llm_dim)

    def forward(self, x):
        return self.proj(x)                       # (B, N, 1024) → (B, N, 4096)


# ── LLaVA-1.5: 2-layer MLP projeksiyon ──────────────────────
class MLPProjection(nn.Module):
    """LLaVA-1.5 tarzı: Linear → GELU → Linear."""
    def __init__(self, visual_dim: int = 1024, llm_dim: int = 4096):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Linear(visual_dim, llm_dim),
            nn.GELU(),
            nn.Linear(llm_dim, llm_dim),
        )

    def forward(self, x):
        return self.mlp(x)                        # (B, N, 1024) → (B, N, 4096)


# ── Test: görsel token'ların LLM boyutuna dönüşümü ──────────
visual_features = torch.randn(1, 256, 1024)         # CLIP çıktısı

proj_v1 = LinearProjection(visual_dim=1024, llm_dim=4096)
proj_v15 = MLPProjection(visual_dim=1024, llm_dim=4096)

out_v1 = proj_v1(visual_features)
out_v15 = proj_v15(visual_features)

print(f"Giriş:       {visual_features.shape}")    # (1, 256, 1024)
print(f"LLaVA-1.0:  {out_v1.shape}")             # (1, 256, 4096)
print(f"LLaVA-1.5:  {out_v15.shape}")            # (1, 256, 4096)

# Parametre karşılaştırması
p_v1 = sum(p.numel() for p in proj_v1.parameters())
p_v15 = sum(p.numel() for p in proj_v15.parameters())
print(f"Params v1.0:  {p_v1/1e6:.1f}M")          # ~4.2M
print(f"Params v1.5:  {p_v15/1e6:.1f}M")         # ~33.6M

LLM girişine eklenen 256 görüntü token'ı, metin token'larından önce yerleştirilir. LLM bu token'ların "görüntü mi metin mi" olduğunu ayırt etmez — hepsini aynı şekilde işler. Bu, görüntünün LLM tarafından metin gibi "okunduğu" anlamına gelir ve cross-attention gibi özel bir mekanizmaya gerek kalmaz.

03 LLaVA Mimarisi

LLaVA, mevcut güçlü bileşenleri minimal bir köprüyle birleştirerek son derece etkin bir VLM ortaya koydu.

LLaVA (Large Language and Vision Assistant), Liu ve arkadaşları tarafından 2023'te yayımlandı. Tasarım felsefesi son derece pragmatiktir: mevcut en güçlü görüntü encoder'ını (CLIP ViT-L/14) ve mevcut en güçlü açık kaynak dil modelini (Vicuna) bir projeksiyon katmanıyla bağla ve uçtan uca eğit. Bu minimalist yaklaşım hem rekabetçi performans hem de tekrarlanabilirlik açısından büyük bir başarı oldu.

Model Visual Encoder Görüntü Çözünürlüğü LLM Projeksiyon Token Sayısı
LLaVA-1.0 CLIP ViT-L/14 224×224 Vicuna-7B/13B Linear 256
LLaVA-1.5 CLIP ViT-L/14-336 336×336 Vicuna-7B/13B MLP (2-layer) 576
LLaVA-1.6 / NeXT CLIP ViT-L/14-336 336×336 × 4 tiles Mistral/Vicuna/LLaMA3 MLP (2-layer) 2304+
llava_inference.py
import torch
from transformers import LlavaForConditionalGeneration, AutoProcessor
from PIL import Image
import requests

# ── LLaVA-1.5 yükle ─────────────────────────────────────────
model_id = "llava-hf/llava-1.5-7b-hf"
processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    low_cpu_mem_usage=True,
).to("cuda")

# ── Görüntü yükle ────────────────────────────────────────────
url = "https://llava-vl.github.io/static/images/view.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# ── Prompt hazırla (LLaVA-1.5 formatı) ──────────────────────
# <image> token görüntünün yerleşeceği konumu belirtir
prompt = "USER: <image>\nBu fotoğrafı Türkçe olarak detaylıca açıkla.\nASSISTANT:"

# ── Input'ları hazırla ───────────────────────────────────────
inputs = processor(
    text=prompt,
    images=image,
    return_tensors="pt"
).to("cuda", dtype=torch.float16)

print(f"Input IDs: {inputs['input_ids'].shape}")
print(f"Pixel values: {inputs['pixel_values'].shape}")
# Input IDs: (1, tekst_uzunluğu)
# Pixel values: (1, 3, 336, 336)

# ── Yanıt üret ───────────────────────────────────────────────
with torch.no_grad():
    output = model.generate(
        **inputs,
        max_new_tokens=200,
        do_sample=False,
        temperature=1.0,
    )

# Sadece yeni üretilen token'ları decode et
generated = output[0][inputs["input_ids"].shape[1]:]
response = processor.decode(generated, skip_special_tokens=True)
print(f"Model yanıtı:\n{response}")
NOT

LLaVA-1.5-7B için gereken minimum VRAM: fp16'da yaklaşık 14GB. Daha düşük VRAM için load_in_4bit=True (bitsandbytes ile) kullanılabilir; bu ~5GB'a düşürür ancak yanıt kalitesi biraz azalır. llava-1.5-7b en yaygın kullanılan boyuttur; 13B versiyonu belirgin biçimde daha iyi sonuçlar verir.

04 Visual Instruction Tuning

LLaVA'nın başarısının anahtarı, GPT-4 tarafından üretilen yüksek kaliteli görsel talimat verisiyle uçtan uca fine-tuning yapmaktır.

Visual instruction tuning, bir VLM'i gerçek kullanıcı talimatlarına (soru-cevap, analiz, konuşma) yanıt vermek üzere ince ayar yapma sürecidir. LLaVA'nın yenilikçi katkısı, görsel talimat verisi üretmek için GPT-4'ü kullanmasıydı: önce COCO görüntülerinin metin açıklamaları GPT-4'e verildi, ardından GPT-4 bu açıklamalara dayanarak soru-cevap çiftleri, detaylı açıklamalar ve çok turlu konuşmalar üretti. Sonuç: 150K yüksek kaliteli görsel talimat örneği.

Eğitim Aşamaları

Stage 1 — Feature Alignment: Sadece projeksiyon katmanı eğitilir, visual encoder ve LLM dondurulur. Amaç: görsel feature'ları LLM'nin token uzayıyla hizalamak. ~600K görüntü-metin çifti kullanılır, 1 epoch. Stage 2 — Visual Instruction Tuning: Hem projeksiyon hem de LLM fine-tune edilir; visual encoder dondurulur. LLaVA-Instruct-150K verisi kullanılır. Tam konuşma formatında, çok turlu sorular.

llava_chat_format.py
import base64
import io
from PIL import Image

# ── Görüntüyü base64'e çevir ─────────────────────────────────
def image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as f:
        img_bytes = f.read()
    return base64.b64encode(img_bytes).decode("utf-8")

# ── LLaVA-1.5 konuşma formatı ───────────────────────────────
# Sistem: model rolünü tanımla
# User: <image> token + soru (görüntü ilk turda bir kez verilir)
# Assistant: yanıt
# Çok turlu konuşmada görüntü sadece ilk turda verilir

single_turn = """USER: <image>
Bu grafikte hangi ürün en yüksek satış rakamına ulaşmıştır?
ASSISTANT:"""

multi_turn = """USER: <image>
Bu faturanın toplam tutarı nedir?
ASSISTANT: Faturanın toplam tutarı 2.450,00 TL'dir.
USER: Vergi oranı nedir ve vergi tutarı kaçtır?
ASSISTANT:"""

# ── HuggingFace chat template ile otomatik formatlama ────────
from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("llava-hf/llava-1.5-7b-hf")

# Chat template formatı (daha modern yaklaşım)
conversation = [
    {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "text", "text": "Bu görüntüdeki metni Türkçeye çevir."}
        ]
    }
]

prompt = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True
)
print(prompt)
# USER: <image>\nBu görüntüdeki metni Türkçeye çevir. ASSISTANT:
NOT

LLaVA-Instruct-150K veri seti üç bileşenden oluşur: (1) Conversation: 58K örnek, gerçekçi soru-cevap diyalogları. (2) Detailed Description: 23K örnek, kapsamlı görüntü açıklamaları. (3) Complex Reasoning: 77K örnek, görüntü üzerinde çok adımlı muhakeme. Bu dağılım, modelin farklı görev türlerinde dengeli performans sergilemesini sağlar.

05 Document Understanding

Belgelerden bilgi çıkarma, VLM'lerin en pratik uygulama alanlarından biridir; OCR tabanlı yaklaşımlardan end-to-end modellere geçiş hız kazanmaktadır.

Geleneksel belge anlama pipeline'ı iki aşamalıdır: önce bir OCR (Optical Character Recognition) motoru (Tesseract, Google Cloud Vision, Azure OCR) metni görüntüden çıkarır, ardından bir NLP modeli bu metni işler. Bu yaklaşım hem birden fazla model yönetimi gerektiren karmaşık bir sistem oluşturur hem de OCR hatalarını sonraki aşamaya taşır. Tablolar, formlar ve karışık düzenler için özellikle sorunlu olabilir.

End-to-end VLM yaklaşımı bu sorunları bertaraf eder: model belge görselini doğrudan işler ve hem görsel düzen bilgisini hem de metin içeriğini aynı anda kullanır. LLaVA-1.5 gibi modeller, yeterli çözünürlük verildiğinde makul düzeyde OCR yapabilir ve bağlamsal anlama yetenekleri sayesinde tablo yapılarını da tanıyabilir.

DocVQA

DocVQA (Document Visual Question Answering) benchmark'ı, belge görüntüleri üzerinde soru-cevap görevini standardize eden önemli bir değerlendirme setidir. Fatura, form, bilimsel makale, gazete gibi çeşitli belge türlerini kapsar. LLaVA-1.5 bu benchmark'ta ~67%, GPT-4V ~88%, özelleştirilmiş modeller (Donut, LayoutLMv3) ise ~90%+ ANLS skoru elde eder.

document_qa_pipeline.py
import torch
import fitz                                         # PyMuPDF
from PIL import Image
from transformers import LlavaForConditionalGeneration, AutoProcessor

# ── PDF sayfasını görüntüye dönüştür ─────────────────────────
def pdf_page_to_image(pdf_path: str, page_num: int = 0, dpi: int = 200) -> Image:
    """PyMuPDF ile PDF sayfasını PIL Image olarak döndür."""
    doc = fitz.open(pdf_path)
    page = doc[page_num]
    # DPI'yı artır: daha iyi metin okunabilirliği
    mat = fitz.Matrix(dpi / 72.0, dpi / 72.0)
    pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
    doc.close()
    return img

# ── Model ve processor yükle ─────────────────────────────────
model_id = "llava-hf/llava-1.5-7b-hf"
processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.float16
).to("cuda")

# ── Belge üzerinde soru-cevap ────────────────────────────────
def document_qa(image: Image, question: str) -> str:
    prompt = f"USER: <image>\n{question}\nASSISTANT:"
    inputs = processor(text=prompt, images=image, return_tensors="pt")
    inputs = {k: v.to("cuda") for k, v in inputs.items()}
    if "pixel_values" in inputs:
        inputs["pixel_values"] = inputs["pixel_values"].half()
    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=300, do_sample=False)
    generated = out[0][inputs["input_ids"].shape[1]:]
    return processor.decode(generated, skip_special_tokens=True)

# ── Kullanım örneği ──────────────────────────────────────────
page_img = pdf_page_to_image("invoice.pdf", page_num=0, dpi=200)

sorular = [
    "Bu faturanın tarihi nedir?",
    "Toplam ödenecek tutar nedir?",
    "Faturayı düzenleyen şirketin adı nedir?",
    "Bu belgede kaç satır ürün/hizmet var?",
]

for soru in sorular:
    yanit = document_qa(page_img, soru)
    print(f"S: {soru}\nC: {yanit}\n")
DİKKAT

LLaVA-1.5 görüntüyü 336×336'ya yeniden boyutlandırır. Bu, yoğun metin içeren belgeler için yetersiz olabilir. Yüksek çözünürlüklü belgeler için LLaVA-1.6 (tiling ile 672×672 veya daha yüksek) veya özelleştirilmiş belge modelleri (Donut, LLaMA-OCR) tercih edilmelidir. DPI=200 ile render etmek metin okunabilirliğini artırır ancak her zaman yeterli olmaz.

06 Chart ve Diagram Analizi

VLM'ler grafiklerden veri okumayı, trendleri tespit etmeyi ve yapısal çıkarım yapmayı öğrenebilir — ancak kesin sayısal değerler için sınırlılıklar mevcuttur.

Grafik ve diyagram analizi, VLM'lerin hem güçlü hem de sınırlı yönlerini en net biçimde ortaya koyan görev türlerinden biridir. Bar chart'ta en uzun çubuğu bulmak, zaman serisi grafiğinde trendi tanımlamak, pasta dilim oranlarını kıyaslamak gibi görevlerde modern VLM'ler oldukça başarılıdır. Ancak kesin sayısal değerleri okumak (örn. "bu çubuk tam olarak 47.3 mi 48.1 mi?"), özellikle küçük fontlarla yazılmış eksen etiketlerini yorumlamak ve birden fazla çakışan serisi ayırt etmek hâlâ zorlu görevlerdir.

ChartQA Benchmark

ChartQA, çeşitli grafik türleri üzerinde soru-cevap görevini değerlendiren standart bir benchmark'tır. GPT-4V bu benchmark'ta ~78%, LLaVA-1.5-13B ~18%, InternVL2 ~83% başarı oranına ulaşır. Fark büyüktür; genel amaçlı VLM'ler grafik analizi için özelleştirilmiş modellerle rekabet etmekte güçlük çeker.

Prompt Engineering for Charts

Grafik analizinde prompt tasarımı kritik önem taşır. Modelden doğrudan yanıt istemek yerine, adım adım düşünmesini istemek ve çıktıyı yapılandırılmış bir formata (JSON, tablo) yönlendirmek çok daha güvenilir sonuçlar verir.

chart_analysis.py
import torch
import json
from PIL import Image
from transformers import LlavaForConditionalGeneration, AutoProcessor

model_id = "llava-hf/llava-1.5-13b-hf"               # 13B daha iyi sayısal anlama
processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.float16
).to("cuda")

# ── Yapılandırılmış çıkarım prompt'u ─────────────────────────
CHART_PROMPT = """USER: <image>
Bu grafiği analiz et ve aşağıdaki bilgileri çıkar:
1. Grafik türü (bar, line, pie, scatter, vb.)
2. X ekseni: ne temsil ediyor ve birimler neler?
3. Y ekseni: ne temsil ediyor ve birimler neler?
4. En yüksek değer: hangi kategori/zaman, değer nedir?
5. En düşük değer: hangi kategori/zaman, değer nedir?
6. Genel trend ya da öne çıkan gözlem (1-2 cümle)

Yanıtını şu JSON formatında ver:
{
  "grafik_turu": "...",
  "x_ekseni": "...",
  "y_ekseni": "...",
  "en_yuksek": {"kategori": "...", "deger": "..."},
  "en_dusuk": {"kategori": "...", "deger": "..."},
  "gozlem": "..."
}
ASSISTANT:"""

def analyze_chart(image_path: str) -> dict:
    image = Image.open(image_path).convert("RGB")
    inputs = processor(
        text=CHART_PROMPT, images=image, return_tensors="pt"
    ).to("cuda", dtype=torch.float16)

    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=400, do_sample=False)
    generated = out[0][inputs["input_ids"].shape[1]:]
    text = processor.decode(generated, skip_special_tokens=True)

    # JSON bloğu çıkarmayı dene
    try:
        start = text.find("{")
        end = text.rfind("}") + 1
        if start != -1 and end > start:
            return json.loads(text[start:end])
    except json.JSONDecodeError:
        pass
    return {"ham_yanit": text}

# ── Kullanım ─────────────────────────────────────────────────
result = analyze_chart("sales_chart.png")
print(json.dumps(result, ensure_ascii=False, indent=2))
# Örnek çıktı:
# {
#   "grafik_turu": "bar chart",
#   "x_ekseni": "Çeyrekler (Q1-Q4 2023)",
#   "y_ekseni": "Satış (milyon TL)",
#   "en_yuksek": {"kategori": "Q4", "deger": "~48M TL"},
#   "en_dusuk": {"kategori": "Q1", "deger": "~31M TL"},
#   "gozlem": "Satışlar yıl boyunca tutarlı artış göstermiş..."
# }
NOT

Grafik analizinde model sınırlılıkları: (1) Kesin sayısal değerler %10-20 hata payıyla okunabilir. (2) Çakışan seriler ve karmaşık efsaneler (legend) yorumlanmakta güçlük çekilir. (3) Log scale grafikler genellikle yanlış yorumlanır. Yüksek hassasiyet gereken uygulamalar için matplotlib/plotly ile veri kaynaklarına doğrudan erişim tercih edin; VLM'yi yalnızca "yaklaşık anlama" için kullanın.

07 InternVL ve PaliGemma

LLaVA ekosistemi dışında gelişen güçlü açık kaynak VLM'ler, farklı tasarım tercihleri ve güçlü benchmark performanslarıyla dikkat çekmektedir.

2024 itibarıyla açık kaynak VLM ekosistemi LLaVA'nın çok ötesine geçmiştir. InternVL2, PaliGemma, Qwen-VL ve Idefics3 gibi modeller farklı güç/verimlilik dengelerinde ilgi çekici seçenekler sunmaktadır.

Model Geliştirici Parametre Visual Encoder LLM Tabanı ChartQA DocVQA
LLaVA-1.5-13B Microsoft / Haotian Liu 13B CLIP ViT-L Vicuna-13B 18.2% 67.1%
InternVL2-8B Shanghai AI Lab 8B InternViT-300M InternLM2.5 83.3% 91.6%
PaliGemma-3B Google DeepMind 3B SigLIP-400M Gemma-2B ~68% ~84%
Qwen-VL-7B Alibaba DAMO 7B CLIP ViT-L Qwen-7B 65.7% 65.1%
GPT-4V OpenAI ? ? GPT-4 78.5% 87.2%

InternVL2

InternVL2, tüm boyut kategorilerinde SOTA performans sergileyen bir model ailesidir (1B'dan 108B'a kadar). Kendi geliştirdiği InternViT visual encoder'ını ve InternLM dil modelini kullanır. Yüksek çözünürlük için "dynamic tiling" uygular: görüntü içeriğine göre adaptif sayıda tile (1-12 arası) kullanılır. Belge anlama ve grafik analizinde LLaVA'yı büyük farkla geçmektedir.

PaliGemma

Google DeepMind'ın PaliGemma modeli, küçük boyutlarda (3B) etkileyici çok görevli performans sergiler. SigLIP-So400M visual encoder + Gemma-2B LLM. Hem genel VQA hem de özelleştirilmiş fine-tuning senaryoları için tasarlanmıştır. HuggingFace üzerinden erişim için lisans onayı gereklidir.

internvl_inference.py
import torch
import numpy as np
from PIL import Image
from transformers import AutoTokenizer, AutoModel
import torchvision.transforms as T
from torchvision.transforms.functional import InterpolationMode

# ── InternVL2-8B yükle ───────────────────────────────────────
model_id = "OpenGVLab/InternVL2-8B"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModel.from_pretrained(
    model_id,
    torch_dtype=torch.bfloat16,
    trust_remote_code=True,
    low_cpu_mem_usage=True,
).eval().cuda()

# ── InternVL görüntü ön işleme ───────────────────────────────
IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD  = (0.229, 0.224, 0.225)

transform = T.Compose([
    T.Lambda(lambda img: img.convert("RGB")),
    T.Resize((448, 448), interpolation=InterpolationMode.BICUBIC),
    T.ToTensor(),
    T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])

def load_image(path: str) -> torch.Tensor:
    img = Image.open(path).convert("RGB")
    pixel_values = transform(img).unsqueeze(0).to(torch.bfloat16).cuda()
    return pixel_values

# ── Inference ────────────────────────────────────────────────
pixel_values = load_image("chart.png")
generation_config = {
    "max_new_tokens": 512,
    "do_sample": False,
}

response = model.chat(
    tokenizer,
    pixel_values,
    "Bu grafiği analiz ederek ana trendleri ve önemli veri noktalarını Türkçe açıkla.",
    generation_config,
)
print(response)

08 Multi-Image ve Video

Tek bir görüntünün ötesine geçmek — birden fazla görüntüyü karşılaştırmak veya video framelerini işlemek — VLM'lerin temporal ve karşılaştırmalı akıl yürütme kapasitesini test eder.

Gerçek dünya uygulamalarının büyük bölümü tek bir görüntüyle sınırlı değildir. Bir ürünün "önce-sonra" karşılaştırması, bir konuşma boyunca paylaşılan birden fazla grafik veya bir video dizisindeki değişimler — bunların hepsi çok görüntülü veya video anlama gerektirir.

Çok Görüntülü Konuşma

LLaVA ve türevleri, birden fazla görüntüyü destekler: her görüntü için bir <image> token yerleştirilerek input dizisi oluşturulur ve karşılık gelen pixel_values tensörleri sırayla verilir. Görüntüler otomatik olarak sırayla işlenir. Context window sınırı kritik kısıtlayıcıdır; her görüntü yüzlerce token tüketir.

Video İşleme

Video, pratikte frame sampling ile ele alınır: videodan her N karede bir frame çekilir (örn. 1fps veya sabit sayıda frame). Bu frameler çok görüntülü giriş olarak modele verilir. Video-LLaVA bu yaklaşımı resmileştirmiş bir modeldir. Temporal understanding — yani olayların zamansal sırasını anlama — henüz mükemmelleşmemiş bir alandır. Uzun videolar için frame sayısı context kısıtıyla çatışır; 8-16 frame çoğu kullanım için makul bir denge sunar.

multi_image_conversation.py
import torch
from PIL import Image
from transformers import LlavaForConditionalGeneration, AutoProcessor

model_id = "llava-hf/llava-1.5-7b-hf"
processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.float16
).to("cuda")

# ── Önce-Sonra karşılaştırma ─────────────────────────────────
img_before = Image.open("before.jpg").convert("RGB")
img_after  = Image.open("after.jpg").convert("RGB")

# Her görüntü için ayrı <image> token
prompt = ("USER: <image><image>\n"
          "İlk görüntü 'önce', ikinci görüntü 'sonra' durumunu gösteriyor. "
          "İkisi arasındaki temel farkları listele.\nASSISTANT:")

inputs = processor(
    text=prompt,
    images=[img_before, img_after],            # görüntü listesi
    return_tensors="pt"
).to("cuda", dtype=torch.float16)

with torch.no_grad():
    out = model.generate(**inputs, max_new_tokens=300, do_sample=False)
generated = out[0][inputs["input_ids"].shape[1]:]
print(processor.decode(generated, skip_special_tokens=True))

# ── Video frame sampling ─────────────────────────────────────
import cv2

def sample_video_frames(video_path: str, n_frames: int = 8) -> list:
    """Videodan eşit aralıklı n_frames kare çıkar."""
    cap = cv2.VideoCapture(video_path)
    total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    indices = [int(i * total / n_frames) for i in range(n_frames)]
    frames = []
    for idx in indices:
        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
        ret, frame = cap.read()
        if ret:
            rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frames.append(Image.fromarray(rgb))
    cap.release()
    return frames

def video_qa(video_path: str, question: str, n_frames: int = 8) -> str:
    frames = sample_video_frames(video_path, n_frames)
    # Her kare için <image> token
    image_tokens = "<image>" * len(frames)
    prompt = f"USER: {image_tokens}\n{question}\nASSISTANT:"

    inputs = processor(
        text=prompt, images=frames, return_tensors="pt"
    ).to("cuda", dtype=torch.float16)

    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=200, do_sample=False)
    generated = out[0][inputs["input_ids"].shape[1]:]
    return processor.decode(generated, skip_special_tokens=True)

# Kullanım
# answer = video_qa("tutorial.mp4", "Bu videoda hangi adımlar gösteriliyor?", n_frames=8)
# print(answer)
DİKKAT

8 frame × ~576 token/frame = ~4600 görüntü token. LLaVA-1.5'in context window'u genellikle 2048 token olarak ayarlıdır. Çok sayıda frame için context window'u büyük LLM'lere (LLaVA-1.6 + Mistral 32K context) veya özelleştirilmiş video modellere (Video-LLaVA, InternVideo2) yönelmek gerekir. Uzun videolar için özetleme + seçici frame sampling stratejisi önerilir.