Veri Mühendisliği
RehberVeri Mühendisliği03 · Pipeline

Veri Etiketleme
Label Studio & Weak Supervision.

Etiketleme maliyetini düşür, kaliteyi ölç, Snorkel ile programatik etiket üret ve active learning döngüsüyle minimum insan emeğiyle maksimum dataset oluştur.

00 Etiketleme Maliyeti Problemi

Supervised learning'in gizli maliyeti: kaliteli etiketli veri üretmek, genellikle model geliştirme maliyetinin %80'ini oluşturabilir.

1 milyon metin için sentiment analizi etiketi: profesyonel annotator başına saatte 200 örnek, saat başı 20$ maliyet varsayımıyla yaklaşık 100.000$ ve 3 ay. Bu maliyet, ML projelerinin büyük çoğunluğunun mümkün olamamasına neden olur.

YaklaşımMaliyetKaliteHız
İnsan AnnotasyonYüksekEn yüksekYavaş
Crowdsourcing (MTurk)OrtaDeğişkenHızlı
Weak SupervisionDüşükOrta-İyiÇok hızlı
Active LearningOrtaYüksekİteratif
Semi-SupervisedDüşükİyiHızlı
Data AugmentationÇok düşükDeğişkenÇok hızlı
Hibrit Strateji: En iyi sonuç genellikle kombinasyonla gelir: 1) Weak supervision ile geniş etiketli dataset, 2) active learning ile belirsiz örnekleri öncelikle insana gönder, 3) semi-supervised learning ile etiketlenmemiş veriyi model güdümüyle kullan.

01 Label Studio Kurulumu ve Proje

Label Studio, metin, görüntü, ses ve video için açık kaynak annotation aracıdır. Docker ile dakikalar içinde çalıştırılabilir.

terminal — kurulum
# Docker ile kurulum (önerilen)
docker run -it -p 8080:8080 \
  -v $(pwd)/label-studio-data:/label-studio/data \
  heartexlabs/label-studio:latest

# Veya pip ile
pip install label-studio
label-studio start --port 8080

# Python SDK kurulumu
pip install label-studio-sdk
ls_project_setup.py — Label Studio SDK
from label_studio_sdk import Client
import json

# ── Bağlantı ────────────────────────────────────────────────────────
ls = Client(url="http://localhost:8080", api_key="YOUR_API_KEY")
ls.check_connection()

# ── NER Projesi Oluştur ──────────────────────────────────────────────
NER_CONFIG = """
<View>
  <Labels name="label" toName="text">
    <Label value="PER" background="#F44336"/>
    <Label value="ORG" background="#2196F3"/>
    <Label value="LOC" background="#4CAF50"/>
    <Label value="MISC" background="#FF9800"/>
  </Labels>
  <Text name="text" value="$text"/>
</View>
"""

project = ls.start_project(
    title="Türkçe NER Etiketleme",
    label_config=NER_CONFIG,
    description="Haber metinlerinde kişi, kurum ve lokasyon tespiti",
)

# ── Veri yükle ───────────────────────────────────────────────────────
tasks = [
    {"text": "Türkiye Cumhurbaşkanı Ankara'da açıklama yaptı."},
    {"text": "Apple CEO'su Tim Cook İstanbul'u ziyaret etti."},
    {"text": "Borsa İstanbul bugün rekor kırdı."},
]
project.import_tasks(tasks)

# ── Tamamlanan annotation'ları dışa aktar ────────────────────────────
annotations = project.export_tasks(export_type="JSON")
with open("annotations.json", "w") as f:
    json.dump(annotations, f, ensure_ascii=False, indent=2)

02 Annotasyon Türleri

Label Studio farklı görev türleri için farklı label config şablonları sunar.

Görev TürüLabel Config ElementiÇıktı Formatı
Text Classification<Choices>Kategori listesi
NER (Named Entity)<Labels> + <Text>start/end/label dizileri
Image Classification<Choices> + <Image>Kategori
Bounding Box<RectangleLabels> + <Image>x, y, w, h, label
Segmentation<PolygonLabels> + <Image>Polygon noktaları
Relation<Relations>Span çiftleri arası ilişki
bbox_config.xml — Nesne Tespiti Annotation
<View>
  <Image name="image" value="$image" zoom="true"/>
  <RectangleLabels name="label" toName="image">
    <Label value="kişi"    background="#FF6B6B"/>
    <Label value="araç"   background="#4ECDC4"/>
    <Label value="bina"   background="#45B7D1"/>
    <Label value="ağaç"   background="#96CEB4"/>
  </RectangleLabels>
</View>
parse_annotations.py — NER çıktısını spaCy formatına dönüştür
import json
from typing import List, Tuple

def ls_to_spacy(annotations_file: str) -> List[Tuple]:
    """Label Studio NER çıktısını spaCy training formatına çevir."""
    with open(annotations_file) as f:
        data = json.load(f)

    training_data = []
    for task in data:
        text = task["data"]["text"]
        entities = []
        for ann in task.get("annotations", []):
            for result in ann.get("result", []):
                if result["type"] == "labels":
                    start = result["value"]["start"]
                    end   = result["value"]["end"]
                    label = result["value"]["labels"][0]
                    entities.append((start, end, label))
        if entities:
            training_data.append((text, {"entities": entities}))
    return training_data

spacy_data = ls_to_spacy("annotations.json")
print(f"{len(spacy_data)} etiketli örnek hazır.")

03 Annotation Guideline Yazma

İyi annotation guideline yüksek inter-annotator agreement'ın temelidir. Belirsiz örnekler için karar ağacı zorunludur.

01 Tanım           → Her kategori için net, tek cümlelik tanım yaz
02 Örnekler        → Her kategori için 5+ pozitif ve 3+ negatif örnek ekle
03 Sınır Durumlar  → Belirsiz örnekler için karar ağacı yaz
04 Pilot Round     → 50 örnek üzerinde annotator'larla pilot; soruları topla
05 Güncelle        → Pilot sonuçlarına göre guideline'ı revize et
06 Kalibrasyon     → Tüm annotator'lar aynı 30 örnek üzerinde kalibre edilir

NER Örnek Kural: Şirket adı (ORG) ile ürün adı (MISC) arasındaki karar: "Apple" bağımsız geçiyorsa ORG; "Apple iPhone" gibi ürün bağlamında sadece "iPhone" MISC, "Apple" ORG. Tam unvan belirtiliyorsa tüm unvan tek span olarak işaretlenir: "Türkiye Büyük Millet Meclisi" → tek LOC değil, ORG.

04 Inter-Annotator Agreement

Annotator'lar arasındaki tutarlılığı ölçmek annotation kalitesinin nesnel göstergesidir.

Cohen's Kappa (κ)İki annotator arasında anlaşmayı şans anlaşmasından arındırılmış ölçer. >0.8: mükemmel, 0.6–0.8: iyi, <0.4: zayıf.
Fleiss' KappaCohen's Kappa'nın ikiden fazla annotator için genellemesi. Crowdsourcing değerlendirmesinde kullanılır.
Krippendorff's αEksik veri ve ordinal/interval skala için daha sağlam bir ölçüt.
F1 (NER için)NER görevinde iki annotator arasındaki span F1 skoru tutarlılık ölçütü olarak kullanılır.
iaa_metrics.py
from sklearn.metrics import cohen_kappa_score
import numpy as np

# ── Cohen's Kappa (text classification) ────────────────────────────
ann1 = ["POS", "NEG", "NEU", "POS", "NEG", "POS", "NEU"]
ann2 = ["POS", "NEG", "POS", "POS", "NEU", "POS", "NEU"]
kappa = cohen_kappa_score(ann1, ann2)
print(f"Cohen's Kappa: {kappa:.4f}")

# ── Fleiss' Kappa (birden fazla annotator) ──────────────────────────
def fleiss_kappa(ratings: np.ndarray) -> float:
    """
    ratings: (n_subjects, n_categories) — her kategori için oy sayısı
    """
    n, k = ratings.shape
    N = ratings[0].sum()
    p_j = ratings.sum(axis=0) / (n * N)
    P_i = ((np.sum(ratings ** 2, axis=1) - N) / (N * (N - 1)))
    P_bar = P_i.mean()
    P_e   = (p_j ** 2).sum()
    return (P_bar - P_e) / (1 - P_e)

# 7 metin, 3 kategoriye (POS/NEG/NEU) 3 annotator'ın oyları
ratings = np.array([
    [2, 1, 0], [3, 0, 0], [0, 0, 3],
    [2, 1, 0], [0, 2, 1], [3, 0, 0], [1, 1, 1],
])
print(f"Fleiss' Kappa: {fleiss_kappa(ratings):.4f}")

# ── Yorumlama ───────────────────────────────────────────────────────
def interpret_kappa(k: float) -> str:
    if k > 0.8:   return "Mükemmel anlaşma"
    if k > 0.6:   return "İyi anlaşma"
    if k > 0.4:   return "Orta anlaşma"
    if k > 0.2:   return "Zayıf anlaşma"
    return "Anlaşma yok — guideline gözden geçirin"

05 Weak Supervision — Snorkel

Programatik etiketleme: kural tabanlı, uzak denetim ve diğer heuristikleri birleştirerek gürültülü ama bol etiket üret.

Snorkel, birden fazla zayıf denetim kaynağını (Labeling Function) birleştirerek her örnek için olasılıksal etiket üretir. Her LF bazı örnekleri etiketler, bazılarını atlar (abstain). Çakışma ve anlaşmazlıklar Label Model ile çözülür.

labeling_functions.py — Snorkel LF'ler
import re
from snorkel.labeling import labeling_function, LabelingFunction, PandasLFApplier
from snorkel.labeling.model import LabelModel
import pandas as pd

# Etiket sabitleri
ABSTAIN  = -1
NEGATIF  =  0
POZITIF  =  1

# ── Kural tabanlı LF'ler ─────────────────────────────────────────────
@labeling_function()
def lf_positive_keywords(x):
    pos_words = ["harika", "mükemmel", "süper", "çok iyi", "beğendim", "tavsiye"]
    text = x.text.lower()
    return POZITIF if any(w in text for w in pos_words) else ABSTAIN

@labeling_function()
def lf_negative_keywords(x):
    neg_words = ["berbat", "rezalet", "kötü", "çöp", "hayal kırıklığı", "tavsiye etmem"]
    text = x.text.lower()
    return NEGATIF if any(w in text for w in neg_words) else ABSTAIN

@labeling_function()
def lf_rating_high(x):
    # "5/5", "5 yıldız", "5 puan" gibi ifadeler
    if re.search(r'[45]\s*[/⭐★]\s*5', x.text):
        return POZITIF
    return ABSTAIN

@labeling_function()
def lf_rating_low(x):
    if re.search(r'[12]\s*[/⭐★]\s*5', x.text):
        return NEGATIF
    return ABSTAIN

@labeling_function()
def lf_negation(x):
    # "beğenmedim", "iyi değil" gibi olumsuzlama
    if re.search(r'(beğenmedim|iyi değil|çalışmıyor|gelmedi)', x.text.lower()):
        return NEGATIF
    return ABSTAIN

@labeling_function()
def lf_emoji_positive(x):
    pos_emojis = ["😍", "🥰", "👍", "❤️", "✨", "🔥"]
    return POZITIF if any(e in x.text for e in pos_emojis) else ABSTAIN

@labeling_function()
def lf_emoji_negative(x):
    neg_emojis = ["😡", "👎", "💩", "😤", "🤮"]
    return NEGATIF if any(e in x.text for e in neg_emojis) else ABSTAIN

06 Majority Vote & Label Model

LF çıktılarını birleştirmenin iki yolu: basit çoğunluk oyu ve Snorkel'in olasılıksal Label Model'i.

label_model.py
from snorkel.labeling import PandasLFApplier, LFAnalysis
from snorkel.labeling.model import LabelModel, MajorityLabelVoter
import pandas as pd

# ── Dataset ──────────────────────────────────────────────────────────
df = pd.read_csv("reviews.csv")   # 'text' kolonu
df_train = df[df["split"] == "train"]
df_valid = df[df["split"] == "valid"]  # 200 insan etiketli örnek

# ── LF'leri uygula ───────────────────────────────────────────────────
lfs = [
    lf_positive_keywords, lf_negative_keywords,
    lf_rating_high, lf_rating_low,
    lf_negation, lf_emoji_positive, lf_emoji_negative,
]
applier = PandasLFApplier(lfs=lfs)
L_train = applier.apply(df_train)
L_valid = applier.apply(df_valid)

# ── LF analizi ───────────────────────────────────────────────────────
print(LFAnalysis(L=L_train, lfs=lfs).lf_summary())
# Coverage, Conflicts, Overlaps, Accuracy gösterir

# ── Majority Vote (baseline) ─────────────────────────────────────────
mv = MajorityLabelVoter(cardinality=2)
mv_labels = mv.predict(L_valid)
print(f"Majority Vote doğruluk: {(mv_labels == df_valid['label'].values).mean():.3f}")

# ── Label Model (daha iyi) ───────────────────────────────────────────
label_model = LabelModel(cardinality=2, verbose=True)
label_model.fit(L_train=L_train, n_epochs=500, lr=0.01, seed=42)

# Validation accuracy
lm_acc = label_model.score(L=L_valid, Y=df_valid["label"].values, metric="accuracy")
print(f"Label Model doğruluk: {lm_acc:.3f}")

# ── Olasılıksal etiket üret ──────────────────────────────────────────
probs = label_model.predict_proba(L=L_train)   # (n_samples, 2)
labels = label_model.predict(L=L_train)
df_train = df_train.copy()
df_train["weak_label"]    = labels
df_train["label_prob_pos"] = probs[:, 1]
# Düşük güven örnekleri filtrele (active learning için kuyruğa al)
uncertain = df_train[(df_train["label_prob_pos"] > 0.3) & (df_train["label_prob_pos"] < 0.7)]
print(f"Belirsiz {len(uncertain)} örnek insan incelemesine gönderilecek.")

07 Active Learning Döngüsü

Model belirsizliğini rehber olarak kullanarak en değerli örnekleri insan annotasyonuna gönder — minimum maliyet, maksimum kazanç.

01 Seed Set        → 100–500 insan etiketli örnekle modeli başlat
02 Tahmin          → Etiketlenmemiş veri üzerinde model olasılıkları hesapla
03 Seçim Stratejisi → En belirsiz örnekleri seç (uncertainty sampling)
04 Annotasyon      → Label Studio'ya gönder, insan etiketlesin
05 Yeniden Eğit    → Yeni etiketlerle modeli güncelle
06 Değerlendir     → Validasyon setinde performansı ölç
07 Tekrar          → Yeterli performansa veya bütçe bitene dek tekrarla
active_learning.py
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer

def uncertainty_sampling(model, X_pool: np.ndarray, n: int = 50) -> np.ndarray:
    """En belirsiz n örneğin indeksini döndür (least confidence)."""
    probs = model.predict_proba(X_pool)
    # En yüksek sınıf olasılığını al — düşük = belirsiz
    confidence = probs.max(axis=1)
    return np.argsort(confidence)[:n]   # en düşük confidence'lar

def entropy_sampling(model, X_pool: np.ndarray, n: int = 50) -> np.ndarray:
    """Entropi bazlı seçim — çok sınıflı için daha iyi."""
    probs = model.predict_proba(X_pool)
    probs = np.clip(probs, 1e-10, 1)
    entropy = -(probs * np.log(probs)).sum(axis=1)
    return np.argsort(entropy)[-n:]    # en yüksek entropi

# ── Döngü ────────────────────────────────────────────────────────────
vectorizer = TfidfVectorizer(max_features=5000)
model = LogisticRegression(max_iter=1000)

labeled_indices = list(range(200))   # başlangıç seed set
unlabeled_indices = list(range(200, 10000))

for iteration in range(10):
    X_lab = vectorizer.fit_transform([texts[i] for i in labeled_indices])
    y_lab = [labels[i] for i in labeled_indices]
    model.fit(X_lab, y_lab)

    X_pool = vectorizer.transform([texts[i] for i in unlabeled_indices])
    query_indices = uncertainty_sampling(model, X_pool, n=50)
    real_indices  = [unlabeled_indices[i] for i in query_indices]

    # Label Studio'ya gönder (otomatik)
    send_to_label_studio(real_indices)   # API çağrısı
    new_labels = wait_for_annotations(real_indices)  # webhook bekle

    labeled_indices.extend(real_indices)
    unlabeled_indices = [i for i in unlabeled_indices if i not in real_indices]
    print(f"İterasyon {iteration+1}: {len(labeled_indices)} etiketli örnek")

08 Semi-Supervised Learning ve Practical

Az etiketli veriyle eğitilen modeli etiketlenmemiş veriye uygulayarak pseudo-label üret; bu pseudo-label'ları yeniden eğitimde kullan.

semi_supervised.py — pseudo labeling
import numpy as np
from sklearn.semi_supervised import LabelPropagation, LabelSpreading
from transformers import pipeline

# ── Pseudo labeling — confidence threshold ──────────────────────────
def pseudo_label(model, X_unlabeled, threshold=0.9):
    probs = model.predict_proba(X_unlabeled)
    max_probs = probs.max(axis=1)
    high_conf = max_probs >= threshold
    pseudo_labels = model.predict(X_unlabeled)
    # Yalnızca yüksek güvenli pseudo etiketleri kabul et
    return X_unlabeled[high_conf], pseudo_labels[high_conf]

# ── Label Propagation (grafik tabanlı) ──────────────────────────────
# -1 = etiketlenmemiş
y_mixed = np.concatenate([y_labeled, -1 * np.ones(n_unlabeled, dtype=int)])
X_mixed = np.concatenate([X_labeled, X_unlabeled])
lp = LabelSpreading(kernel="knn", n_neighbors=7, alpha=0.2, max_iter=30)
lp.fit(X_mixed, y_mixed)
all_labels = lp.transduction_

# ── Transformers ile zero-shot pseudo labeling ──────────────────────
classifier = pipeline(
    "zero-shot-classification",
    model="facebook/bart-large-mnli",
)
candidate_labels = ["pozitif", "negatif", "nötr"]
text = "Bu ürün beklentilerimi tamamen karşıladı."
result = classifier(text, candidate_labels)
print(result["labels"][0])  # En yüksek skorlu etiket
Practical Özet — NER + Snorkel: 1) 500 örnek Label Studio'da insan etiketleme → 2) 8 LF yaz (kural + regex + sözlük) → 3) Snorkel Label Model ile 10k örnek etiketle → 4) Active learning ile belirsiz 200 örneği insana gönder → 5) Nihai model 10.700 etiketli örnekle eğitilir. Başlangıca göre maliyet %95 azalır.