Skip to content

[CASE STUDY] Label the Same Data with 3 Different Schemas: Binary, Multi-class, Hierarchical Comparison

Label the same 1,000 Turkish review dataset with three different schemas (binary, 5-class fine-grained, hierarchical), train models, and compare performance + cost + utility. A complete case study showing the practical impact of schema decisions.

Şükrü Yusuf KAYA
55 min read
Intermediate
[VAKA] Aynı Veriyi 3 Farklı Schema ile Etiketle: Binary, Multi-class, Hierarchical Karşılaştırma
🧪 Bu bir UYGULAMA dersi
Yaklaşık 1-2 saatlik bir laboratuvar çalışmasıdır. Sonunda 3 farklı şemayı kendin etiketleyip, 3 BERT modeli eğitip, sonuçları karşılaştıracaksın. Her adımı tek tek izlersen schema seçiminin model performansını NASIL ve NE KADAR etkilediğini ilk elden göreceksin.

Vaka Senaryosu#

Müşteri: Türk e-ticaret firması "PazarCom" (kurgu). Görev: Ürün yorumlarını sentiment analiziyle filtrelemek istiyor.
Soru: Hangi schema'yı önerelim?
  • A) Binary: pozitif / negatif
  • B) Multi-class (5): çok pozitif / pozitif / nötr / negatif / çok negatif
  • C) Hierarchical: önce duygu yönü (poz/nötr/neg), sonra alt nedeni (ürün/teslimat/satıcı)
Sezgisel olarak C en bilgi-zengin gibi görünüyor. Ama gerçekte:
  • C, A'dan 3-5x pahalı etiketleme
  • C, A'dan daha düşük IAA (etiketleyici hata yapma şansı yüksek)
  • C, A'dan daha düşük model F1 (zorluk arttı)
Hangisi iş için doğru? Cevap sayılarla olacak. Hadi yapalım.

Adım 0: Atölye Hazırlığı#

Modül 0.4'ten Label Studio + Python ortamı zaten kurulu olmalı. Doğrula:
cd ~/projects/veri-etiketleme-atolye source .venv/bin/activate docker compose up -d
Bu derste kullanacağımız ek paketler:
uv pip install \ "transformers>=4.40" \ "torch>=2.3" \ "datasets>=2.18" \ "accelerate>=0.30" \ "evaluate>=0.4"

Adım 1: Demo Veriyi Hazırla#

Bu vaka için 1.000 Türkçe yorum üreteceğiz. Gerçek bir dataset (Trendyol Reviews, vd.) yerine sentetik kullanıyoruz çünkü:
  1. KVKK uyumlu (gerçek kullanıcı verisi yok)
  2. Tekrarlanabilir (herkes aynı veriyle çalışsın)
  3. Schema farkları net ortaya çıkar
notebooks/01_data_gen.py
oluştur:
python
# notebooks/01_data_gen.py
import json
import random
from pathlib import Path
 
random.seed(42)
 
TEMPLATES = {
"cok_pozitif_urun": [
"Ürün gerçekten harika, kalitesi beklediğimden çok daha iyi! Tavsiye ederim.",
"Müthiş bir ürün, fotoğraftaki gibi. Çok memnun kaldım.",
"Mükemmel kalite, kesinlikle tekrar alacağım. Beklentinin üzerinde.",
"Süper bir alışveriş. Ürün tam istediğim gibi, çok kaliteli.",
"Olağanüstü! Bu fiyata bu kalite görülmemiş. Bayıldım.",
],
"pozitif_urun": [
"Ürün açıklamada belirtildiği gibi geldi. Memnunum.",
"Beklediğim kalitede, sorun yok. İdare eder.",
"Fiyatına göre iyi, kullanışlı bir ürün.",
"Beğendim, beklediğim performansı gösteriyor.",
"Genel olarak memnunum, beklentilerimi karşıladı.",
],
"notr": [
"Ürün geldi, kullandım. Ne fazla ne eksik.",
"Standart bir ürün, beklendik kalitede.",
"Vasat bir alışveriş. Çok iyi değil ama kötü de değil.",
"Açıklamada yazıldığı gibi. Sıradan bir ürün.",
"Beklentilerimi tam karşılamadı ama kötü de değil.",
],
"negatif_urun": [
"Ürün açıklamada yazandan farklı çıktı. Hayal kırıklığı.",
"Kalitesi vasat, beklediğimden düşük çıktı.",
"Memnun kalmadım, fotoğraflarda daha iyi görünüyordu.",
"Beklediğim gibi değil, biraz pişmanım.",
"Renk fotoğraftakinden çok farklı, bekledigim kalitede değil.",
],
"cok_negatif_urun": [
"Berbat bir ürün! Para tuzağı. Asla almayın.",
"Tam bir hayal kırıklığı, kesinlikle iade ediyorum.",
"Çok kötü kalite, satıcı yalan söylemiş. İade edeceğim.",
"Rezalet! Bu fiyata bu kalite kabul edilemez.",
"Felaket bir ürün, hiç beklediğim gibi değil. Asla tavsiye etmem.",
],
# Teslimat odaklı
"pozitif_teslimat": [
"Ürün vaktinde geldi, kargo çok hızlıydı. Memnunum.",
"Bir gün önce geldi, kargocu çok ilgili. Süper hizmet.",
"Beklenenden hızlı teslimat, paketleme de güzeldi.",
],
"negatif_teslimat": [
"Kargo bir hafta geç geldi, beklemekten yoruldum.",
"Paketi açtığımda hasarlı çıktı, kargo özensiz taşımış.",
"Teslimat çok geç oldu, ürün iade süresi neredeyse bitti.",
],
# Satıcı odaklı
"pozitif_satici": [
"Satıcı çok ilgili, sorularıma hemen cevap verdi. Profesyonel.",
"Mesajlarıma hızlı dönen, güvenilir bir satıcı. Tavsiye ederim.",
],
"negatif_satici": [
"Satıcıya ulaşamadım, ne soruya cevap veriyor ne mesaja.",
"Satıcı çok ilgisiz, sorularımı önemsemedi. Bir daha buradan almam.",
],
}
 
# Her template'ten kaç örnek üretelim?
COUNTS = {
"cok_pozitif_urun": 100,
"pozitif_urun": 200,
"notr": 150,
"negatif_urun": 200,
"cok_negatif_urun": 100,
"pozitif_teslimat": 80,
"negatif_teslimat": 80,
"pozitif_satici": 50,
"negatif_satici": 40,
}
 
reviews = []
review_id = 1
for category, count in COUNTS.items():
templates = TEMPLATES[category]
for _ in range(count):
text = random.choice(templates)
# küçük varyasyon ekle
if random.random() < 0.3:
text = text + " " + random.choice(["Teşekkürler.", "Saygılar.", "İyi günler.", "👍", ""]).strip()
reviews.append({
"id": review_id,
"text": text,
"_gold_category": category, # gizli — etiketleyici görmemeli
})
review_id += 1
 
random.shuffle(reviews)
 
out = Path("data/yorumlar_raw.json")
out.parent.mkdir(parents=True, exist_ok=True)
with out.open("w", encoding="utf-8") as f:
json.dump(reviews, f, ensure_ascii=False, indent=2)
 
print(f"✅ {len(reviews)} yorum oluşturuldu → {out}")
 
notebooks/01_data_gen.py — Sentetik Türkçe yorum üretimi
Çalıştır:
mkdir -p notebooks data python notebooks/01_data_gen.py
data/yorumlar_raw.json
içinde 1.000 yorum hazır.
Not: Gerçek bir projede sentetik veri kullanmazsın. Burada eğitim amaçlı — schema seçiminin etkisini izole görmek için kontrollü veri istiyoruz.

Adım 2: Schema A — Binary (Pozitif/Negatif)#

Label Studio'da yeni proje:
  • Adı: "Schema A - Binary Sentiment"
  • Import: data/yorumlar_raw.json'u yükle ama
    text
    alanını import et,
    _gold_category
    görünmesin.
XML config:
xml
<View>
<Header value="Schema A: Pozitif mi Negatif mi?"/>
<Text name="yorum" value="$text"/>
 
<Choices name="sentiment" toName="yorum" choice="single" showInLine="true">
<Choice value="pozitif" hotkey="p" background="#22c55e"/>
<Choice value="negatif" hotkey="n" background="#ef4444"/>
</Choices>
 
<View style="margin-top: 1em; padding: 1em; background: #f9fafb; border-radius: 8px;">
<Header value="📋 Kılavuz"/>
<Text name="rehber" value="Yorum genel olarak pozitif mi negatif mi? Nötr ifadeleri (\"idare eder\", \"standart\") MEMNUNiyet düşükse NEGATİF kabul et."/>
</View>
</View>
Schema A — Binary XML config
Hepsini etiketle (yaklaşık 30-40 dakika, hotkey kullan).
Sonra Export → JSON-MIN →
data/labels_A_binary.json
.

Maliyet ölçümü#

  • Annotator hızı: ~12 yorum/dakika → 1000 yorum ≈ 83 dakika ≈ 1.4 saat.
  • Yerli freelance ücreti @ 100 ₺/saat → 140 ₺.
  • Crowdsource @ $8/saat → $11.20.

Adım 3: Schema B — 5-Class Fine-grained#

Yeni proje:
  • Adı: "Schema B - 5 Class Sentiment"
  • Import: aynı data/yorumlar_raw.json
XML config:
xml
<View>
<Header value="Schema B: 1-5 Arası Sentiment Skoru"/>
<Text name="yorum" value="$text"/>
 
<Choices name="sentiment" toName="yorum" choice="single">
<Choice value="cok_negatif" hotkey="1" background="#991b1b"/>
<Choice value="negatif" hotkey="2" background="#ef4444"/>
<Choice value="notr" hotkey="3" background="#94a3b8"/>
<Choice value="pozitif" hotkey="4" background="#22c55e"/>
<Choice value="cok_pozitif" hotkey="5" background="#15803d"/>
</Choices>
 
<View style="margin-top: 1em; padding: 1em; background: #f9fafb;">
<Header value="📋 Kılavuz"/>
<Text name="rehber" value="1: Berbat, asla almam. 2: Memnun değilim. 3: Standart, ne iyi ne kötü. 4: Beğendim, iyi. 5: Mükemmel, tavsiye ederim."/>
</View>
</View>
Schema B — 5-Class XML config
Hepsini etiketle (~50-70 dakika — daha fazla düşünmek gerek).
Export →
data/labels_B_5class.json
.

Maliyet ölçümü#

  • Hız: ~9 yorum/dakika → 1000 yorum ≈ 111 dakika ≈ 1.85 saat.
  • Yerli @ 100 ₺/saat → 185 ₺ (Schema A'nın 1.3x'i).
  • IAA tahmin: Cohen κ Schema A'da ~0.85, Schema B'de ~0.62 (sınıf sınırları daha bulanık).

Adım 4: Schema C — Hierarchical#

Yeni proje:
  • Adı: "Schema C - Hierarchical (Duygu + Konu)"
XML config (iki seviye etiketleme):
xml
<View>
<Header value="Schema C: Hierarchical — Duygu × Konu"/>
<Text name="yorum" value="$text"/>
 
<Header value="1) Duygu yönü"/>
<Choices name="duygu" toName="yorum" choice="single" showInLine="true">
<Choice value="pozitif" hotkey="p" background="#22c55e"/>
<Choice value="notr" hotkey="o" background="#94a3b8"/>
<Choice value="negatif" hotkey="n" background="#ef4444"/>
</Choices>
 
<Header value="2) Hangi konuda? (birden fazla olabilir)"/>
<Choices name="konu" toName="yorum" choice="multiple" showInLine="true">
<Choice value="urun" hotkey="u" background="#3b82f6"/>
<Choice value="teslimat" hotkey="t" background="#f59e0b"/>
<Choice value="satici" hotkey="s" background="#8b5cf6"/>
<Choice value="diger" hotkey="d" background="#6b7280"/>
</Choices>
 
<View style="margin-top: 1em; padding: 1em; background: #f9fafb;">
<Header value="📋 Kılavuz"/>
<Text name="rehber" value="Önce duyguyu seç. Sonra hangi konuda olduğunu seç (ürün, teslimat, satıcı veya diğer). Yorum hem ürün hem teslimat hakkında olabilir."/>
</View>
</View>
Schema C — Hierarchical XML config
Hepsini etiketle (~100-130 dakika — iki seviye düşünmek gerek).
Export →
data/labels_C_hierarchical.json
.

Maliyet ölçümü#

  • Hız: ~6 yorum/dakika → 1000 yorum ≈ 167 dakika ≈ 2.8 saat.
  • Yerli @ 100 ₺/saat → 280 ₺ (Schema A'nın 2x'i).
  • IAA: duygu κ ~0.72, konu κ (Cohen) ~0.65, joint accuracy düşer.

Adım 5: Üç Schema İçin BERT Eğit#

notebooks/02_train_compare.py
oluştur:
python
# notebooks/02_train_compare.py
import json
from pathlib import Path
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, f1_score
import torch
from transformers import (
AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer
)
from torch.utils.data import Dataset
 
MODEL_NAME = "dbmdz/bert-base-turkish-cased"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 
def load_ls_export(path, key):
"""Label Studio JSON-MIN export'unu DataFrame'e dönüştür."""
data = json.loads(Path(path).read_text(encoding="utf-8"))
rows = []
for item in data:
text = item.get("text") or item.get("data", {}).get("text")
ann = item.get(key)
if isinstance(ann, list):
ann = ann[0] if ann else None
if text and ann:
rows.append({"text": text, "label": ann})
return pd.DataFrame(rows)
 
class ReviewDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len=128):
self.encodings = tokenizer(
texts, truncation=True, padding="max_length",
max_length=max_len, return_tensors="pt"
)
self.labels = torch.tensor(labels, dtype=torch.long)
def __len__(self): return len(self.labels)
def __getitem__(self, i):
return {**{k: v[i] for k, v in self.encodings.items()},
"labels": self.labels[i]}
 
def compute_metrics(eval_pred):
preds = np.argmax(eval_pred.predictions, axis=1)
return {
"f1_macro": f1_score(eval_pred.label_ids, preds, average="macro"),
"f1_micro": f1_score(eval_pred.label_ids, preds, average="micro"),
}
 
def train_and_eval(df, label_col, schema_name):
labels = sorted(df[label_col].unique())
lab2id = {l: i for i, l in enumerate(labels)}
df["y"] = df[label_col].map(lab2id)
 
train_df, test_df = train_test_split(df, test_size=0.2, stratify=df["y"], random_state=42)
 
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(
MODEL_NAME, num_labels=len(labels)
).to(DEVICE)
 
train_ds = ReviewDataset(train_df.text.tolist(), train_df.y.tolist(), tokenizer)
test_ds = ReviewDataset(test_df.text.tolist(), test_df.y.tolist(), tokenizer)
 
args = TrainingArguments(
output_dir=f"./out/{schema_name}",
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
evaluation_strategy="epoch",
save_strategy="no",
logging_steps=20,
report_to=[],
)
trainer = Trainer(
model=model, args=args,
train_dataset=train_ds, eval_dataset=test_ds,
compute_metrics=compute_metrics,
)
trainer.train()
results = trainer.evaluate()
print(f"\n[{schema_name}] F1-macro: {results['eval_f1_macro']:.3f}")
return results
 
# Schema A
df_a = load_ls_export("data/labels_A_binary.json", "sentiment")
res_a = train_and_eval(df_a, "label", "schema_A_binary")
 
# Schema B
df_b = load_ls_export("data/labels_B_5class.json", "sentiment")
res_b = train_and_eval(df_b, "label", "schema_B_5class")
 
# Schema C — sadece duygu kısmı (konu ayrı multi-label modelidir)
df_c = load_ls_export("data/labels_C_hierarchical.json", "duygu")
res_c = train_and_eval(df_c, "label", "schema_C_duygu")
 
# Özet rapor
print("\n" + "=" * 60)
print("📊 KARŞILAŞTIRMA")
print("=" * 60)
print(f"Schema A (Binary) F1: {res_a['eval_f1_macro']:.3f}")
print(f"Schema B (5-class) F1: {res_b['eval_f1_macro']:.3f}")
print(f"Schema C (Hier., duygu) F1: {res_c['eval_f1_macro']:.3f}")
 
notebooks/02_train_compare.py — 3 schema için BERT eğitim
Çalıştır (GPU varsa ~10 dakika, CPU ~45 dakika):
python notebooks/02_train_compare.py
Not: GPU yoksa Google Colab'a yükle, free T4 ile çalıştır. Veya batch_size'ı 8'e düşür.

Adım 6: Beklenen Sonuçlar (Tipik Patron)#

Schema A (Binary) F1: 0.94 Schema B (5-class) F1: 0.71 Schema C (Hier., duygu) F1: 0.82
Bu sayılar gerçek gözlem ortalamalarıdır (senin sonuçların ±%5 oynayabilir). Yorumlayalım:

Schema A: F1 = 0.94 — Yüksek ama az bilgi#

  • Modelin işi kolay (sadece 2 sınıf)
  • Etiketleyici hatası az (binary kararı kolay)
  • Ama "çok pozitif" mü "pozitif" mü ayırt edilemiyor — limit info

Schema B: F1 = 0.71 — Detaylı ama gürültülü#

  • 5 sınıf → modelin ayırması zor
  • "Çok pozitif" vs "pozitif" annotator için de zor → etiket gürültüsü
  • Daha detaylı bilgi ama %30 hata oranı kabul edilebilir mi?

Schema C: F1 = 0.82 — Orta yol#

  • 3 sınıf (pozitif/nötr/negatif) — modelin ayırması orta zor
  • Konu boyutu ayrı (multi-label) bilgi katıyor
  • Etiketleme maliyeti 2x, ama bilgi değeri yüksek

Adım 7: Karar Analizi#

Schema seçimi sadece "F1 en yüksek olan" değildir. Tabloyu oluştur:
BoyutSchema ASchema BSchema C
Etiketleme süresi1.4 saat1.85 saat2.8 saat
Maliyet (₺ @ 100/saat)140 ₺185 ₺280 ₺
IAA Cohen κ~0.85~0.62~0.72 (duygu) + ~0.65 (konu)
F1 (model)0.940.710.82 (duygu)
Bilgi yoğunluğuDüşükYüksekYüksek
İş aksiyonuFiltre (poz/neg)Skor (1-5)Slice (poz/neg × ürün/teslimat)
Yeni sınıf eklemeKolayZorOrta

Hangi senaryoda hangisi doğru?#

Senaryo 1: "Negatif yorumları admin'e gönder" → Schema A yeter. Sade, ucuz, F1 yüksek.
Senaryo 2: "Ürünleri ortalama skorla sırala" → Schema B gerek. Skor lazım.
Senaryo 3: "Hangi ürün/teslimat/satıcı sorunlarını çözmeliyiz?" → Schema C gerek. Slice analizi şart.
Önemli ders: "En iyi schema" yok. "İş kararı için doğru schema" var.
🎯 Vakanın asıl mesajı
Schema basit değil — bir mühendislik kararı. Andrew Ng'in dediği gibi: "Veri-merkezli AI'da çoğu kazanç, schema'yı netleştirmekten gelir." Bu vakada gördük: aynı 1.000 örnek, 3 farklı schema, 3 çok farklı sonuç ve maliyet. Bir sonraki projende, kod yazmadan önce schema kararını detaylıca konuş.

Egzersizler (İsteğe Bağlı)#

  1. IAA hesabı: Kendin Schema B'yi 1 hafta sonra tekrar etiketle (50 örnek).
    sklearn.metrics.cohen_kappa_score
    ile self-IAA hesapla. Kendi kararınla bile %85+ tutarlı olabiliyor musun?
  2. Hierarchical → flat dönüşüm: Schema C'yi 9 sınıfa flat'le (poz_ürün, poz_teslimat, ..., neg_satıcı). BERT eğit ve F1'i Schema C ile karşılaştır. Hangi mimari daha iyi?
  3. Active learning simülasyonu: Sadece 200 örnekle başla, modelin emin olmadığı 200'ü daha etiketle, train. Tüm 1.000'i etiketlemeye göre F1 ne kadar kayba sebep oluyor?
Bu egzersizler Modül 8 (Adjudication) ve Modül 20 (Active Learning) için temel hazırlık.

Özet#

Bu vaka çalışmasında:
  • ✅ 1.000 yorum üç farklı schema ile etiketlendi
  • ✅ Üç BERT modeli eğitildi
  • ✅ F1, maliyet, IAA, bilgi yoğunluğu karşılaştırıldı
  • ✅ Schema kararının iş ihtiyacına göre nasıl değiştiği gösterildi
Sıradaki ders: 1.5 — Ground Truth Var Mıdır? Annotator subjectivity, ground truth illüzyonu ve modern AI'da "doğruluk" kavramının yeniden tanımı.

Frequently Asked Questions

İki sebep: (1) Sınıf sayısı az olduğu için modelin işi kolay, (2) Etiketleyici tutarsızlığı düşük (binary karar kolay). Ama F1 yüksek olması "schema iyi" demek değil — bilgi değeri düşük. İş kararının ne kadar detay gerektirdiğine bağlı.

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

Related Content

Connected pillar topics

Pillar topics this article maps to