Metin de veridir
Anadolu Market'e her gün yüzlerce ürün yorumu, Ilgaz Bank'ın çağrı merkezine yüzlerce şikayet kaydı geliyor. Bunlar sayı değil metin; ama "hangi konuda en çok şikayet var", "bu yorum olumlu mu", "bu şikayete benzeyen eski kayıtlar hangileri" soruları tamamen sayısal işlemlerle cevaplanır. Doğal dil işleme (Natural Language Processing, NLP) metni modelin anlayacağı sayılara çevirme ve sonra o sayılarla çalışma sanatıdır.
Bu dersin akışı tipik bir NLP projesinin akışıdır: temizle → parçala → say → vektöre çevir → benzerlik/sınıflandırma. Sonunda modern yaklaşıma (embeddings ve transformer'lar) kavramsal bir bakış atacağız.
Türkçe metni temizlemek: küçük harf tuzağı
Karakterler, Unicode ve Türkçe dersinde gördüğün tuzak burada can yakar. Python'un lower() metodu dilden bağımsızdır: "I" harfini "i" yapar (Türkçede "ı" olmalı), "İ" harfini ise "i" + birleşen nokta (U+0307) olarak iki karaktere çevirir. Sonuç: "İADE" küçültüldüğünde "iade" ile eşleşmez ve sayımların sessizce yanlış çıkar.
metin = "İADE talebim var, IŞIK modeli ARIZALI"
print(metin.lower())
print("İADE".lower() == "iade", len("İADE".lower()))
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
print(tr_kucuk(metin))
print(tr_kucuk("İADE") == "iade")
casefold() büyük-küçük harf duyarsız karşılaştırma için daha agresiftir ama yine Türkçe kurallarını bilmez. Türkçe metinde kendi tr_kucuk fonksiyonunu ilk adım yap ve tüm hattında (eğitimde ve tahminde) aynı fonksiyonu kullan.Sonraki adım noktalama ve rakamlar. string.punctuation yalnızca ASCII işaretleri içerir; Türkçe metinlerdeki tırnaklar (“ ” ‘ ’) ve üç nokta (…) kaçar. Regex ile "harf olmayan her şeyi boşluğa çevir" demek daha güvenlidir. Python 3'te \w Unicode harflerini (ç, ğ, ı, ö, ş, ü) zaten tanır.
Tokenizasyon, durak kelimeler ve n-gram
Tokenizasyon (tokenization) metni anlamlı parçalara (token) bölmektir. Basit ama sağlam yol re.findall ile harf dizilerini yakalamaktır. Durak kelimeler (stop words) "ve, bir, bu, da, için" gibi her metinde geçen ve tek başına bilgi taşımayan kelimelerdir; sayımlarda listenin tepesini kaplamasınlar diye çıkarılır. Türkçe için hazır listeler vardır ama küçük ve elle yazılmış bir liste neyi attığını bilmeni sağlar.
import re
from collections import Counter
DURAKLAR = {"ve", "bir", "bu", "da", "de", "çok", "ile", "için", "ama", "mi", "gibi", "daha", "en"}
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
def tokenle(metin, duraklar=DURAKLAR):
kelimeler = re.findall(r"[a-zçğıöşü]+", tr_kucuk(metin))
return [k for k in kelimeler if k not in duraklar and len(k) > 1]
yorumlar = [
"Kargo ÇOK geç geldi, ürün de kırık!",
"Ürün güzel ama kargo geç geldi.",
"İade süreci çok hızlıydı, teşekkürler.",
"Kargo hızlı, ürün güzel.",
]
tum = [t for y in yorumlar for t in tokenle(y)]
print(tokenle(yorumlar[0]))
print(Counter(tum).most_common(4))
# n-gram: ardışık n kelime
t = tokenle(yorumlar[1])
ikili = [" ".join(p) for p in zip(t, t[1:])]
print(ikili)
n-gram, ardışık n token'dır: "kargo geç" bir ikili (bigram). Tek kelimeler "geç" ile "geldi"yi ayrı sayar; ikili ise "kargo geç" ifadesini bir kavram olarak yakalar. zip(t, t[1:]) ikilileri, zip(t, t[1:], t[2:]) üçlüleri üretir.
Metni vektöre çevirmek: CountVectorizer ve TF-IDF
Modeller sayı ister. Kelime çantası (bag of words) modelinde her belge, sözlükteki her kelimenin kaç kez geçtiğini tutan bir satırdır. scikit-learn'de bunu CountVectorizer yapar. TF-IDF (term frequency - inverse document frequency) ise sayımı, kelimenin kaç belgede geçtiğine göre ağırlıklandırır: her yorumda geçen "ürün" düşük, yalnızca birkaç yorumda geçen "kırık" yüksek ağırlık alır.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
belgeler = ["kargo geç geldi", "ürün kırık geldi", "kargo hızlı ürün güzel"]
cv = CountVectorizer(preprocessor=tr_kucuk)
X = cv.fit_transform(belgeler)
print(cv.get_feature_names_out().tolist())
print(X.toarray())
tf = TfidfVectorizer(preprocessor=tr_kucuk)
T = tf.fit_transform(belgeler)
agirlik = dict(zip(tf.get_feature_names_out(), T.toarray()[1].round(2).tolist()))
print({k: v for k, v in agirlik.items() if v > 0})
İkinci belgede "kırık" (yalnız bu belgede var) "geldi"den (iki belgede var) daha yüksek ağırlık aldı. Sonuç seyrek matristir (sparse matrix): çoğu hücre 0 olduğu için yalnızca dolu hücreler saklanır; toarray() yalnızca küçük örneklerde göstermek içindir.
| Parametre | Ne işe yarar | Örnek |
|---|---|---|
preprocessor | Tokenizasyondan önce metne uygulanır | preprocessor=tr_kucuk |
stop_words | Atılacak kelimeler (Türkçe hazır yok, liste ver) | stop_words=list(DURAKLAR) |
ngram_range | Tekli, ikili... birlikte | (1, 2) |
min_df / max_df | Çok nadir / çok yaygın kelimeleri at | min_df=2, max_df=0.9 |
analyzer="char_wb" | Kelime yerine karakter n-gram | Türkçe ekler için güçlü |
token_pattern | Token regex'i (varsayılan 2+ harf) | r"(?u)\b\w+\b" |
Kosinüs benzerliği: benzer şikayeti bulmak
Ilgaz Bank'ta yeni bir şikayet geldiğinde temsilci "bunun benzeri daha önce nasıl çözülmüştü" diye bakmak ister. İki TF-IDF vektörü arasındaki kosinüs benzerliği (cosine similarity) aralarındaki açının kosinüsüdür: 1 aynı yön (çok benzer), 0 hiç ortak kelime yok.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
sikayetler = [
"Kartım ATM'de yutuldu, yeni kart istiyorum",
"Mobil uygulamaya giriş yapamıyorum, şifre hatası veriyor",
"Hesabımdan bilgim dışında para çekilmiş",
"Kredi kartı ekstremde tanımadığım bir harcama var",
"Uygulama açılmıyor, sürekli şifre hatası alıyorum",
]
vec = TfidfVectorizer(preprocessor=tr_kucuk)
X = vec.fit_transform(sikayetler)
yeni = "Uygulamaya giremiyorum, şifre hatası veriyor"
skor = cosine_similarity(vec.transform([yeni]), X).ravel()
for i in skor.argsort()[::-1][:2]:
print(round(skor[i], 2), sikayetler[i])
Dikkat: yeni metin transform ile vektöre çevrilir, fit_transform ile değil. Sözlük eğitim verisinden öğrenilir ve sabit kalır; yeni metindeki bilinmeyen kelimeler ("giremiyorum") yok sayılır. Bu da kelime temelli yöntemlerin zayıflığını gösterir: "giremiyorum" ile "giriş" aynı kökten ama farklı token.
Metin sınıflandırma: yorum duygu analizi
Ön işleme, pipeline ve model değerlendirme dersinde kurduğun Pipeline mantığı metinde de aynıdır: vektörleştirici + sınıflandırıcı. Metin için iki klasik model: MultinomialNB (Naive Bayes; kelime sayımlarıyla çok hızlı, küçük veride şaşırtıcı iyi) ve LogisticRegression (genelde biraz daha isabetli, katsayıları yorumlanabilir).
yorumlar = [
("Kargo çok hızlı geldi, ürün harika", "olumlu"),
("Ürün tam anlatıldığı gibi, çok memnunum", "olumlu"),
("Paketleme özenliydi, teşekkürler", "olumlu"),
("Fiyatına göre kalitesi süper", "olumlu"),
("Hızlı teslimat, güzel ürün, tavsiye ederim", "olumlu"),
("Müşteri hizmetleri çok ilgiliydi, memnunum", "olumlu"),
("Ürün kırık geldi, çok kötü", "olumsuz"),
("Kargo geç geldi, bir daha almam", "olumsuz"),
("Anlatıldığı gibi değil, iade etmek istiyorum", "olumsuz"),
("Kalitesi berbat, param boşa gitti", "olumsuz"),
("Müşteri hizmetlerine ulaşamadım, kötü deneyim", "olumsuz"),
("Paket ezik ve ürün eksik geldi", "olumsuz"),
]
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
def tr_kucuk(s):
return s.replace("I", "ı").replace("İ", "i").lower()
metinler = [m for m, _ in yorumlar]
etiketler = [e for _, e in yorumlar]
nb = make_pipeline(TfidfVectorizer(preprocessor=tr_kucuk), MultinomialNB()).fit(metinler, etiketler)
lr = make_pipeline(TfidfVectorizer(preprocessor=tr_kucuk), LogisticRegression(C=10)).fit(metinler, etiketler)
yeni = ["Ürün harika, kargo hızlı", "Ürün kırık geldi, iade istiyorum", "Kargo geç geldi ama ürün güzel"]
for m, a, b in zip(yeni, nb.predict(yeni), lr.predict(yeni)):
print(f"{a:8} {b:8} {m}")
print(lr.classes_.tolist(), lr.predict_proba(yeni[2:]).round(2).tolist())
Üçüncü yorum karışık: hem olumsuz ("geç") hem olumlu ("güzel") sinyal var. 12 örnekle eğitilmiş model "olumsuz" dese de olasılıklar 0.44 / 0.56 ile neredeyse yazı turadır; gerçek projelerde binlerce etiketli yorum, "nötr" sınıfı ve cross_val_score ile değerlendirme gerekir. predict_proba olasılıkları, düşük güvenli tahminleri insana yönlendirmek için kullanılır.
cross_val_score her katmanda vektörleştiriciyi yalnızca eğitim kısmına fit eder.Embeddings ve transformer'lar: kavramsal bakış
TF-IDF kelimeleri bağımsız sayar: "kredi kartı" ile "kart borcu" ortak token taşımıyorsa benzerlikleri 0'dır. Gömme (embedding) yaklaşımında her metin, anlamını yakalayan yoğun bir vektöre (ör. 384 ya da 768 sayı) çevrilir; anlamca yakın cümleler, farklı kelimeler kullansa bile yakın vektörler alır. Bu vektörleri transformer mimarisindeki dil modelleri üretir. Hugging Face, bu modellerin ve hazır araçların paylaşıldığı merkezdir.
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util
# Çok dilli bir model seç (Hugging Face'te "multilingual" modelleri ara)
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
cumleler = ["Kartım ATM'de kaldı", "Banka makinesi kartımı yuttu", "Kredi faizi ne kadar?"]
emb = model.encode(cumleler)
print(util.cos_sim(emb[0], emb[1:])) # ilk ikisi yüksek, üçüncü düşük çıkar
# pip install transformers torch
from transformers import pipeline
# Türkçe duygu analizi için Hugging Face'te "turkish sentiment" araması yap,
# model kartında lisansı ve eğitim verisini oku.
siniflandirici = pipeline("sentiment-analysis", model="<seçtiğin-türkçe-model>")
print(siniflandirici("Kargo çok geç geldi, hiç memnun değilim"))
pip install sentence-transformers ile deneyebilirsin. İlk çalıştırma modeli indirir; sonraki çalıştırmalar önbellekten (cache) gelir.Türkçe için araçlar: Türkçe eklemeli (agglutinative) bir dildir; "giremiyorum", "girişimizde", "girdi" aynı kökten çıkar. Zemberek Türkçe için kök bulma (stemming), biçimbirim çözümleme ve yazım denetimi yapan köklü bir açık kaynak kütüphanedir (Java tabanlı; Python'dan sarmalayıcılarla kullanılır). spaCy'nin Türkçe için temel tokenizasyon desteği ve topluluk tarafından eğitilmiş modelleri vardır. Hugging Face'te ise BERTurk gibi Türkçe ön-eğitimli transformer modelleri bulunur.
| Yaklaşım | Artı | Eksi | Ne zaman? |
|---|---|---|---|
| TF-IDF + LogisticRegression | Hızlı, ucuz, açıklanabilir, CPU yeter | Eş anlam ve ekleri yakalamaz | İlk model, temel çizgi (baseline) |
| Karakter n-gram TF-IDF | Ek ve yazım hatasına dayanıklı | Sözlük büyür | Türkçe, kısa ve hatalı metin |
| Cümle embedding + basit sınıflandırıcı | Anlam benzerliği, az etiketle iyi | Model indirme, GPU iyi olur | Semantik arama, kümeleme |
| Transformer ince ayar (fine-tuning) | En yüksek isabet | Veri, GPU, MLOps maliyeti | Kritik ve yüksek hacimli görev |
Derinleş: gerçek müşteri yorumlarıyla metin analizi
Dersteki metin teknikleri birkaç örnek cümleyle çalışıyordu. Şimdi bir yemek teslimat platformunun gerçek değerlendirme tablosuna geçelim: lezzet_kurye setindeki lk_ratings. Her satır bir siparişin değerlendirmesidir: food_score ve courier_score (1–5) ile isteğe bağlı comment. 3.979 değerlendirmenin 2.366'sında yorum var. Bu tablo, metin analizinde sık karşılaşılan iki gerçeği öğretir: metin alanlarının önemli bir kısmı boştur ve kısa yorumlar sık tekrar eder ("Güzeldi", "Süper" gibi).
import veri
r = veri.oku("lezzet_kurye", "lk_ratings")
print(len(r), "değerlendirme,", int(r["comment"].notna().sum()), "yorum")
print("farklı yorum metni:", r["comment"].nunique())
print(r["comment"].value_counts().head(5))
2.366 yorumda 275 farklı metin var: bir metin ortalama 8-9 kez geçiyor. (Bu veri hayali: yorumlar yaklaşık 300 metinlik bir havuzdan seçildi, gerçek platformda çeşitlilik daha fazladır.) Böyle bir veride önce metin başına özet çıkarmak en verimli adımdır: her kalıp kaç kez geçiyor, ortalama yemek ve kurye puanı ne?
import veri
r = veri.oku("lezzet_kurye", "lk_ratings").dropna(subset=["comment"])
ozet = r.groupby("comment").agg(adet=("order_id", "size"), yemek=("food_score", "mean"),
kurye=("courier_score", "mean"))
print(ozet.round(2).sort_values("yemek").head(6))
Kural tabanlı konu etiketleme
Model eğitmeden önce, iş birimiyle birlikte bir anahtar kelime sözlüğü kurmak çoğu zaman en hızlı ve açıklanabilir başlangıçtır. Türkçede kelimeler ek aldığı için tam kelime yerine kök parçası ara: "lezzet" hem "lezzetli" hem "lezzetliydi" ile eşleşir, "dökül" ise "dökülmüştü" ile. Küçük harfe çevirirken Türkçe I/İ kuralını unutma. Bir yorum birden çok konuya girebilir: "Biraz geç geldi ama lezzetli" hem gecikme hem lezzet konusudur.
def tr_kucuk(metin):
return metin.replace("I", "ı").replace("İ", "i").lower()
KONULAR = {
"gecikme": ["geç"],
"sıcaklık": ["soğuk"],
"hatalı sipariş": ["eksik", "yanlış", "dökül"],
"porsiyon": ["porsiyon"],
"lezzet": ["lezzet"],
}
import veri
from collections import Counter
def konular(metin):
k = tr_kucuk(metin)
return [ad for ad, kokler in KONULAR.items() if any(x in k for x in kokler)]
yorumlar = veri.oku("lezzet_kurye", "lk_ratings")["comment"].dropna()
sayim = Counter(k for y in yorumlar for k in konular(y))
print(dict(sayim.most_common()))
print(konular("Biraz geç geldi ama lezzetli"))
r["comment"].str.contains("geç") boş yorumlarda NaN döndürür ve bu maskeyle satır seçmek hata verir. na=False ver ya da önce dropna uygula. Boş yorumu "olumsuz" ya da "olumlu" saymak da hatadır: o müşteri yalnızca yazmamıştır.Puanı etiket yapmak ve sızıntı tuzağı
Elinde elle etiketlenmiş duygu verisi yoksa, yıldız puanı zayıf etiket (weak label) olarak kullanılabilir: food_score >= 4 olumlu, <= 2 olumsuz, 3 belirsiz (dışarıda bırakılır). Ama burada büyük bir tuzak var: aynı metin tekrar ettiği için rastgele eğitim/test bölmesinde test cümlelerinin çoğu eğitimde de bulunur. Model ezberinden yararlanır, başarıyı olduğundan yüksek raporlarsın. Gerçek başarıyı görmek için bölmeyi metne göre gruplu yap (GroupShuffleSplit): böylece test setindeki kalıpları model hiç görmemiş olur.
def tr_kucuk(metin):
return metin.replace("I", "ı").replace("İ", "i").lower()
import veri
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split, GroupShuffleSplit
from sklearn.metrics import balanced_accuracy_score
r = veri.oku("lezzet_kurye", "lk_ratings").dropna(subset=["comment"])
r = r[r["food_score"] != 3]
X = r["comment"]
y = np.where(r["food_score"] >= 4, "olumlu", "olumsuz")
model = make_pipeline(TfidfVectorizer(preprocessor=tr_kucuk),
LogisticRegression(max_iter=1000, class_weight="balanced"))
a, b, ya, yb = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
print("rastgele bölme:", round(balanced_accuracy_score(yb, model.fit(a, ya).predict(b)), 3))
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=0).split(X, y, groups=X))
model.fit(X.iloc[tr], y[tr])
print("gruplu bölme:", round(balanced_accuracy_score(y[te], model.predict(X.iloc[te])), 3))
Dengeli doğruluk rastgele bölmede 0,693 iken gruplu bölmede 0,588'e düşüyor: görülmemiş metinlerde model yazı turanın az üstünde. Puan gürültülü bir etiket, metinler de kısa. Ders: bu veriyle güçlü bir duygu modeli bekleme; konu sayımı ve metin başına puan özeti çok daha dürüst bir analizdir. Gerçek serbest metin biriktiğinde model sorusu yeniden açılır.
Türkçe ve sözlük patlaması
Eklemeli dillerde tek bir kökten onlarca biçim türer; kelime temelli sözlük hızla büyür ve her biçim az görülür. Üç çare: kök bulma (Zemberek), karakter n-gram (TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 4))) ve alt-kelime (subword) tokenizasyonu kullanan transformer modelleri. Karakter n-gram'ı denemek tek parametre değiştirmektir ve Türkçede çoğu zaman belirgin kazanç getirir.
Değerlendirme ve dengesizlik
Şikayetlerin %80'i tek kategorideyse doğruluk (accuracy) yanıltır; sınıf bazında precision/recall ve classification_report kullan, gerekirse class_weight="balanced". Etiketleme tutarlılığı da önemlidir: iki kişi aynı yorumu farklı etiketliyorsa modelin tavanı o anlaşma oranıdır.
Model ne öğrendi?
LogisticRegression'da coef_ ile get_feature_names_out() eşleştirilerek her sınıf için en etkili kelimeler listelenir. Bu liste hem hata ayıklamaya (model "kargo" kelimesini olumsuzlukla mı eşleştirmiş?) hem iş ekibine güven vermeye yarar.