İçeriğe geç

Production Notları: Feature Drift, Multi-Modal Content, ve Türkçe NLP'nin Zorlukları

Modül 4'ün kapanışı: content-based recommender'ı 6 ay production'da tuttuğunda karşılaşacağın gerçek problemler. Feature distribution drift, multi-modal embedding (image+text+audio) ile cold-start gücü, CLIP/SBERT modern yaklaşımlar, Türkçe NLP özelinde stemming + BERTurk.

Şükrü Yusuf KAYA
24 dakikalık okuma
İleri
Production Notları: Feature Drift, Multi-Modal Content, ve Türkçe NLP'nin Zorlukları
🚨 Bu dersin amacı
Modül 4'te bir content-based recommender kurdun. Şimdi hayal et — bunu 6 ay production'da tutuyorsun. Hangi sorunlarla karşılaşırsın? Feature drift (kategori taksonomisi değişti), multi-modal ihtiyacı (kullanıcılar fotoğrafa da bakıyor), Türkçe NLP spesifik dertleri. Bu derste somut çözümler.

1) Feature Drift — Sessiz Performans Düşüşü#

Sorun#

Modelinin bugün ürettiği öneri kalitesi, 6 ay önceki eğitim sırasındaki dağılımdan farklı bir dünyaya çıkar.
Ocak 2026: Model eğitildi - "Smartphone" kategorisi: 5,000 item - Genre dağılımı: %40 Drama, %25 Action, ... - Ortalama review uzunluğu: 120 kelime Temmuz 2026: 6 ay sonra - "Smartphone" kategorisi: 8,500 item (yeni model dalgaları) - "Smartphone" alt-kategorileri eklendi: "Foldable", "Gaming Phone" - Reviews kısaldı (TikTok kuşağı influence): ortalama 45 kelime Model bu değişiklikleri bilmiyor — eski feature distribution'da düşünüyor.

Üç Tip Drift#

a) Concept Drift

Feature-label ilişkisi değişiyor. "Action film" tanımı 2010'da farklı, 2026'da farklı.

b) Data Drift (Covariate Shift)

Feature'ların dağılımı değişiyor. Yeni ürün gözükçe genre dağılımı kayıyor.

c) Schema Drift

Feature'ların kendisi değişiyor. Yeni kategori eklendi, eski silindi, JSON format'ı değişti.

Tespit Yöntemleri#

Statistical Tests

from scipy.stats import ks_2samp, chi2_contingency # Numerik feature için Kolmogorov-Smirnov test stat, p_value = ks_2samp(reference_dist, current_dist) if p_value < 0.01: alert("Feature drift detected") # Kategorik feature için chi-square test contingency = pd.crosstab(reference_categories, current_categories) chi2, p_value, _, _ = chi2_contingency(contingency)

Population Stability Index (PSI)

PSI = Σ (current_pct - reference_pct) * log(current_pct / reference_pct) PSI < 0.1: stable PSI 0.1-0.2: minor drift, watch PSI > 0.2: major drift, retrain

Model-Based Detection

İki dataset'i ayırt etmek için classifier eğit. Eğer accuracy ≈ 0.5 → stable. Eğer accuracy >> 0.5 → drift var.

Müdahale#

  • Automatic retraining cycle: Haftalık/aylık tam retrain.
  • Online learning: Stream-based feature güncelleme.
  • Schema versioning: Feature schema'sını versiyonla, transition planı kur.
python
# monitoring/drift_detector.py — Feature drift detection
import numpy as np
from scipy.stats import ks_2samp
 
def psi(
reference: np.ndarray,
current: np.ndarray,
n_bins: int = 10,
) -> float:
"""
Population Stability Index — numerik feature drift ölçümü.
 
Returns: PSI değeri
< 0.1: stable
0.1-0.2: minor drift
> 0.2: major drift
"""
# Reference distribution'ın bin'lerini kur
bin_edges = np.percentile(
reference, np.linspace(0, 100, n_bins + 1)
)
# 0 division önlemek için, ilk ve son bin'i biraz genişlet
bin_edges[0] = -np.inf
bin_edges[-1] = np.inf
 
ref_pct = np.histogram(reference, bin_edges)[0] / len(reference)
cur_pct = np.histogram(current, bin_edges)[0] / len(current)
 
# Sıfır division önle
eps = 1e-6
ref_pct = np.clip(ref_pct, eps, 1.0)
cur_pct = np.clip(cur_pct, eps, 1.0)
 
psi_value = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
return float(psi_value)
 
 
def detect_categorical_drift(
reference_categories: list[str],
current_categories: list[str],
) -> dict:
"""
Kategorik feature drift — yeni kategoriler / kayıp kategoriler.
"""
ref_set = set(reference_categories)
cur_set = set(current_categories)
new_categories = cur_set - ref_set
lost_categories = ref_set - cur_set
 
# Frequency drift
from collections import Counter
ref_freq = Counter(reference_categories)
cur_freq = Counter(current_categories)
 
common = ref_set & cur_set
freq_changes = {}
for cat in common:
ref_pct = ref_freq[cat] / len(reference_categories)
cur_pct = cur_freq[cat] / len(current_categories)
change_pct = (cur_pct - ref_pct) / ref_pct if ref_pct > 0 else 0
if abs(change_pct) > 0.2: # %20+ change
freq_changes[cat] = {
"ref_pct": ref_pct,
"cur_pct": cur_pct,
"change_pct": change_pct,
}
 
return {
"new_categories": list(new_categories),
"lost_categories": list(lost_categories),
"significant_freq_changes": freq_changes,
}
 
 
# Test
np.random.seed(42)
ref = np.random.normal(100, 15, 10000)
cur = np.random.normal(105, 18, 5000) # mean ve std drift
print(f"PSI: {psi(ref, cur):.4f}")
# PSI: ~0.15 — minor-to-moderate drift
 
Feature drift detection — PSI ve kategorik drift.

2) Multi-Modal Content — Görsel + Metin + Ses#

Niçin Multi-Modal?#

Geleneksel CB sadece metin kullanır. Modern dünyada:
  • E-ticaret: ürün fotoğrafı + başlık + açıklama + kategori
  • Müzik: şarkı ses dosyası + metadata
  • Video: thumbnail + başlık + transcript + audio
  • Mod: kıyafet fotoğrafı + kategori + renk + dokunma
Tek modaliteden daha zengin feature → daha güçlü recommender.

Modern Multi-Modal Encoders#

CLIP (OpenAI 2021)

"Contrastive Language-Image Pre-training" — text ve image'ı aynı embedding uzayında map eder.
import clip import torch from PIL import Image device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load("ViT-B/32", device=device) # Image embedding image = preprocess(Image.open("product.jpg")).unsqueeze(0).to(device) with torch.no_grad(): image_embedding = model.encode_image(image) print(f"Image embedding: {image_embedding.shape}") # (1, 512) # Text embedding text = clip.tokenize(["a red dress with floral pattern"]).to(device) text_embedding = model.encode_text(text) # Similarity similarity = (image_embedding @ text_embedding.T).item()
Use case: Cold-start item için zero-shot embedding. Yeni ürünün sadece fotoğrafı var — CLIP ile anında embedding üret.

Sentence-BERT (SBERT)

Sentence-level metin embedding. Türkçe için:
paraphrase-multilingual-MiniLM-L12-v2
.
from sentence_transformers import SentenceTransformer model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") embeddings = model.encode([ "Christopher Nolan'ın yeni filmi", "Yapay zeka konulu bilim kurgu", ]) print(embeddings.shape) # (2, 384)

Audio Embedding (Spotify-tarzı)

Custom CNN ya da OpenAI Whisper-tarzı pretrained audio encoder.

Multi-Modal Fusion Stratejileri#

  1. Concatenation (basit):
    item_vec = [text_emb, image_emb, audio_emb]
  2. Weighted Average (kalibrasyon ile):
    item_vec = w_t·text + w_i·image
  3. Cross-Attention (modern): Transformer ile farklı modaliteler etkileşim
  4. Late Fusion (production): Her modalite ayrı skor, son aşamada birleştir
python
# multimodal/fusion.py — Multi-modal item embedding
import torch
import torch.nn as nn
 
class MultiModalFusion(nn.Module):
"""
Concatenation + projection ile multi-modal fusion.
"""
def __init__(
self,
text_dim: int = 384,
image_dim: int = 512,
category_dim: int = 64,
output_dim: int = 128,
):
super().__init__()
total_input = text_dim + image_dim + category_dim
self.fusion = nn.Sequential(
nn.Linear(total_input, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, output_dim),
)
 
def forward(
self,
text_emb: torch.Tensor,
image_emb: torch.Tensor,
category_emb: torch.Tensor,
) -> torch.Tensor:
x = torch.cat([text_emb, image_emb, category_emb], dim=-1)
return self.fusion(x)
 
 
# Eğitim — pozitif/negatif pair'larla contrastive loss
def contrastive_loss(
anchor: torch.Tensor,
positive: torch.Tensor,
negative: torch.Tensor,
margin: float = 0.2,
) -> torch.Tensor:
"""Triplet loss — positive yakın, negative uzak."""
pos_dist = (anchor - positive).pow(2).sum(dim=1)
neg_dist = (anchor - negative).pow(2).sum(dim=1)
loss = torch.relu(pos_dist - neg_dist + margin)
return loss.mean()
 
Multi-modal fusion — text + image + category birlikte projection.

3) Türkçe NLP — Agglutinative Dilin Zorlukları#

Türkçe agglutinative (eklemeli) bir dildir. Bir tek kökten muazzam sayıda kelime üretilebilir:
Kök: git- gitmek (to go) gittim (I went) gidiyorum (I am going) gideceğim (I will go) gidemedim (I couldn't go) gidemeyebileceğim (I might not be able to go)
Bu kelimelerin hepsi TF-IDF için farklı kelime. Yani vocabulary 5-10x şişer, ve aynı semantic kelime farklı bucket'larda kalır.

Çözüm Stratejileri#

a) Stemming (Kök Bulma)

Zemberek-NLP veya Snowball Turkish stemmer.
# pip install zemberek-python from zemberek import TurkishMorphology morphology = TurkishMorphology.create_with_defaults() word = "gidemeyebileceğim" analysis = morphology.analyze(word) for parse in analysis: print(parse.get_stem()) # "git"

b) Lemmatization

Daha akıllı — kelimeyi sözlük formuna indir.
# spaCy Turkish model import spacy nlp = spacy.load("tr_core_news_md") # 50MB model doc = nlp("Gideceğim yere ulaşamadım.") lemmas = [token.lemma_ for token in doc] # ["git", "yer", "ulaş", "..."]

c) Subword Tokenization

BPE (Byte-Pair Encoding) veya SentencePiece — kelimeleri sub-unit'lere böler.
"gidemeyebileceğim" → ["gid", "##e", "##me", "##ye", "##bil", "##e", "##ceğ", "##im"]
Avantaj: morfolojik bilgi korunur, vocabulary küçük, OOV yok.

d) Modern Yaklaşım: BERTurk

Türkçe için pretrained BERT modeli (Schweter 2020).
from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("dbmdz/bert-base-turkish-cased") model = AutoModel.from_pretrained("dbmdz/bert-base-turkish-cased") text = "Yapay zeka mühendisliği zorludur." tokens = tokenizer(text, return_tensors="pt") embeddings = model(**tokens).last_hidden_state # [CLS] token embedding → cümle representation sentence_emb = embeddings[0, 0] # 768-dim

Türkçe NLP Pratik Tavsiyesi#

Use CaseÖnerilen Yaklaşım
Hızlı baselineZemberek stem + TF-IDF
Orta kalitespaCy lemma + TF-IDF
Production semanticBERTurk (yavaş ama güçlü)
Search engineBERTurk + BM25 hybrid
Real-timeSubword (BPE) + small embeddings

🎉 Modül 4 Tamamlandı#

4 ders bitti:
  • 4.1: CB vs CF felsefesi + hybrid pattern'ler
  • 4.2: TF-IDF + BM25 + n-gram + kategorik encoding (NumPy)
  • 4.3: MovieLens-100K üzerinde sıfırdan content-based recommender
  • 4.4: Production gotcha'lar — drift, multi-modal, Türkçe NLP
İlk benchmark tablomuza ilk satırı ekledik: NDCG@10 = 0.089 (CB), %50 better than popularity baseline.
Bir sonraki modülde Modül 5 — Memory-Based Collaborative Filtering'e geçiyoruz. k-NN ile user-user ve item-item CF. Aynı dataset, NDCG@10'umuz 0.12-0.13'e çıkacak. ☕

Sık Sorulan Sorular

Endüstri konvansiyonu. Credit risk modeling'den geliyor (1990'lar). Recommender'da kullanırken biraz daha gevşek olabilirsin (örn. 0.15-0.25). Sınır otomatik retrain tetikleyecek seviye — false-positive tetikleme maliyetli.

Yorumlar & Soru-Cevap

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

İlgili İçerikler