Doğruluk Metrikleri: RMSE, MAE, Precision@K, Recall@K, MAP, MRR, NDCG, HR@K — Tam Matematik + NumPy
8 ana doğruluk metriğinin tam matematiksel tanımı, sıfırdan NumPy implementasyonu, MovieLens üzerinde karşılaştırmalı çalıştırma, ve hangi durumda hangi metriği seçmen gerektiği — recommender mühendisinin metric cheat sheet'i.
Şükrü Yusuf KAYA
35 dakikalık okuma
İleri📏 Bu dersin amacı
Recommender'lar 8 farklı doğruluk metriği etrafında konuşur. Bu metriklerin bir kısmı birbirini ölçer, bir kısmı birbirine zıt sonuçlar verebilir. Aynı modelin RMSE'si düşük ama HR@10'u yüksek olabilir. Bu derste her metriği matematiksel olarak türetiyor, NumPy ile sıfırdan yazıyor ve MovieLens üzerinde karşılaştırmalı koşturuyoruz. Sonunda kendi cheat sheet'in olur.
8 Metrik — Hızlı Bakış#
| Metrik | Tip | Aralık | Yön (iyi) | Asıl Soru |
|---|---|---|---|---|
| RMSE | Rating prediction | [0, ∞) | ↓ düşük | "Tahminlerin gerçek puana ne kadar yakın?" |
| MAE | Rating prediction | [0, ∞) | ↓ düşük | "Ortalama tahmin hatası kaç?" |
| Precision@K | Top-N | [0, 1] | ↑ yüksek | "Önerdiğim K item'ın yüzde kaçı doğruydu?" |
| Recall@K | Top-N | [0, 1] | ↑ yüksek | "Doğruların yüzde kaçını yakaladım?" |
| HR@K (Hit Rate) | Top-N | [0, 1] | ↑ yüksek | "En az 1 doğru var mı?" |
| MAP@K | Ranking | [0, 1] | ↑ yüksek | "Sıra-hassas precision" |
| MRR | Sequential | [0, 1] | ↑ yüksek | "İlk doğru hangi sırada?" |
| NDCG@K | Ranking | [0, 1] | ↑ yüksek | "Sıralama ne kadar 'ideale' yakın?" |
Bu metriklerin her biri farklı amaç için tasarlanmış. Tek bir "doğru metrik" yok — soruya göre seçim.
1) RMSE ve MAE — Rating Prediction Klasikleri#
RMSE (Root Mean Squared Error)#
RMSE = √( (1/|D|) Σ (r_ui - r̂_ui)² )
Özellikleri:
- Büyük hataları karesi ile cezalandırır — 1 birim hata 1, 2 birim hata 4 ağırlık.
- Birimi rating ile aynı (1-5 ölçekli sistemde 0-4 arası).
- Netflix Prize bu metric üzerine kuruldu (0.9525 → 0.8567 hedefi).
MAE (Mean Absolute Error)#
MAE = (1/|D|) Σ |r_ui - r̂_ui|
Özellikleri:
- Lineer ceza — 1 birim hata 1, 2 birim hata 2.
- Outlier'lara karşı daha robust.
- Yorumlama daha kolay ("ortalama 0.7 puan sapıyoruz").
RMSE vs MAE — Hangisini Seçmeli?#
| Durum | Seç |
|---|---|
| Büyük hatalar çok ağır kayıp | RMSE |
| Outlier ratings (1★ inflation) | MAE |
| Production bias varsa | MAE (RMSE patlatır) |
| Geleneksel benchmark | RMSE (Netflix Prize) |
RMSE'nin Şişme Tuzağı#
Bir kullanıcı genelde 4★ verirse ve bir item'a 1★ verdiyse — RMSE bunu çok ağır cezalandırır. Halbuki bu kullanıcı için 1★ "outlier" olabilir, gerçekten anlamlı bir sapma değil. Pratik: ya MAE kullan, ya da rating'leri normalize et.
python
# metrics/rating_metrics.py — Rating prediction metrikleriimport numpy as np def rmse(predictions: np.ndarray, actuals: np.ndarray) -> float: """Root Mean Squared Error.""" return float(np.sqrt(np.mean((predictions - actuals) ** 2))) def mae(predictions: np.ndarray, actuals: np.ndarray) -> float: """Mean Absolute Error.""" return float(np.mean(np.abs(predictions - actuals))) def mse(predictions: np.ndarray, actuals: np.ndarray) -> float: """Mean Squared Error — RMSE'nin karekökü öncesi.""" return float(np.mean((predictions - actuals) ** 2)) # Smoke testnp.random.seed(42)n = 1000actuals = np.random.choice([1, 2, 3, 4, 5], size=n, p=[0.05, 0.10, 0.20, 0.35, 0.30]) # Model 1: Naive — herkese ortalama vermean_rating = actuals.mean()preds_naive = np.full(n, mean_rating) # Model 2: Düşük gürültülü modelpreds_good = actuals + np.random.normal(0, 0.5, n) # Model 3: Outlier yapan model — 5%'i çok kötü tahmin ederpreds_outlier = actuals + np.random.normal(0, 0.2, n)outlier_mask = np.random.random(n) < 0.05preds_outlier[outlier_mask] += np.random.choice([-4, 4], outlier_mask.sum()) print(f"Naive model | RMSE: {rmse(preds_naive, actuals):.4f} | MAE: {mae(preds_naive, actuals):.4f}")print(f"Good model | RMSE: {rmse(preds_good, actuals):.4f} | MAE: {mae(preds_good, actuals):.4f}")print(f"Outlier-prone model | RMSE: {rmse(preds_outlier, actuals):.4f} | MAE: {mae(preds_outlier, actuals):.4f}") # Çıktı:# Naive model | RMSE: 1.0996 | MAE: 0.9170# Good model | RMSE: 0.5031 | MAE: 0.4001# Outlier-prone model | RMSE: 0.9420 | MAE: 0.3950 ← outlier RMSE'yi şişiriyor# ama MAE iyi gösteriyor RMSE ve MAE — outlier davranışını gözle.
2) Precision@K ve Recall@K — Top-N'in Temelleri#
Tanımlar#
K = 10 önerdik. Test set'inde user'ın gerçek pozitif item set'i .
T_uTopK_u = modelin verdiği ilk K item HitSet = TopK_u ∩ T_u ← doğru olan kısım Precision@K = |HitSet| / K ← önerdiklerin yüzde kaçı doğru? Recall@K = |HitSet| / |T_u| ← doğruların yüzde kaçını yakaladım?
Tablolu Örnek#
User u'nun gerçek beğendiği item'lar: T_u = {A, B, C, D, E} Model top-10: [B, X, A, Y, Z, C, W, V, U, T] HitSet = {A, B, C} Precision@10 = 3 / 10 = 0.30 Recall@10 = 3 / 5 = 0.60
Trade-off#
Precision↑ Recall↓ — kısa liste ver, kalitesi yüksek olsun.
Precision↓ Recall↑ — uzun liste ver, daha çok yakala.
Recommender'da genelde Recall@K daha önemli (kapsama), ama NDCG (sırayı da hesaba katar) daha pratik bir bileşim.
F1@K#
İkisinin harmonik ortalaması:
F1@K = 2 · P · R / (P + R)
Pratikte F1 nadiren raporlanır (NDCG daha güçlüdür).
python
# metrics/topn_metrics.py — Top-N metrikleri (sıfırdan)import numpy as np def precision_at_k( recommended: list[int], # modelin önerileri, sıralı liste ground_truth: set[int], # gerçek pozitif item'lar k: int,) -> float: """Precision@K — önerdiğin ilk K'da doğru oranı.""" top_k = recommended[:k] hits = sum(1 for item in top_k if item in ground_truth) return hits / k def recall_at_k( recommended: list[int], ground_truth: set[int], k: int,) -> float: """Recall@K — doğruların yüzde kaçını ilk K'da yakaladın.""" if not ground_truth: return 0.0 top_k = recommended[:k] hits = sum(1 for item in top_k if item in ground_truth) return hits / len(ground_truth) def hit_rate_at_k( recommended: list[int], ground_truth: set[int], k: int,) -> float: """HR@K — en az 1 doğru var mı? 0 ya da 1.""" top_k = recommended[:k] return 1.0 if any(item in ground_truth for item in top_k) else 0.0 def f1_at_k(p: float, r: float) -> float: """F1@K — harmonik ortalama.""" if p + r == 0: return 0.0 return 2 * p * r / (p + r) # Testrecommended = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]ground_truth = {103, 105, 108, 999} # 4 gerçek pozitif print(f"Precision@10: {precision_at_k(recommended, ground_truth, 10):.4f}")print(f"Recall@10: {recall_at_k(recommended, ground_truth, 10):.4f}")print(f"HR@10: {hit_rate_at_k(recommended, ground_truth, 10):.4f}")print(f"F1@10: {f1_at_k(precision_at_k(recommended, ground_truth, 10), recall_at_k(recommended, ground_truth, 10)):.4f}") # Precision@10: 0.3000# Recall@10: 0.7500 (3/4 yakalandı, 999 dışarıda)# HR@10: 1.0000# F1@10: 0.4286 Top-N metrikleri — sıfırdan, kolay anlaşılır.
Yani: her doğru-hit pozisyonundaki Precision@k'yı ortala. Erken pozisyondaki hit'ler daha güçlü.
Tablolu Örnek#
Test set: T_u = {A, B, C} (3 gerçek pozitif) Top-10: [A, X, Y, B, Z, W, C, U, V, T] Pozisyonlar: A=1, B=4, C=7 AP = (1/3) * [P@1 + P@4 + P@7] = (1/3) * [1/1 + 2/4 + 3/7] = (1/3) * [1.0 + 0.5 + 0.4286] = 0.6429
MAP birden çok kullanıcı için: AP'lerin ortalaması.
MRR (Mean Reciprocal Rank)#
Sequential rec ve search'te yaygın. İlk doğru hit'in sırasının tersi:
Tablolu Örnek#
User A: ilk hit pozisyon 1 → RR = 1.0
User B: ilk hit pozisyon 5 → RR = 0.2
User C: hiç hit yok → RR = 0.0
MRR = (1.0 + 0.2 + 0.0) / 3 = 0.4
MAP vs MRR Farkı#
- MAP: Tüm doğru hit'leri sayar (multi-relevant).
- MRR: Sadece ilk hit'i önemser.
"Search engine"de MRR sık (kullanıcı genelde ilk doğru sonuca tıklar). Recommender'da MAP daha pratik.
python
# metrics/ranked_metrics.py — Sıralı metriklerimport numpy as np def average_precision_at_k( recommended: list[int], ground_truth: set[int], k: int,) -> float: """AP@K — sıralama-hassas precision.""" if not ground_truth: return 0.0 top_k = recommended[:k] hits = 0 sum_precisions = 0.0 for i, item in enumerate(top_k, start=1): if item in ground_truth: hits += 1 sum_precisions += hits / i return sum_precisions / min(len(ground_truth), k) def reciprocal_rank( recommended: list[int], ground_truth: set[int],) -> float: """RR — ilk doğru hit'in pozisyonunun tersi.""" for i, item in enumerate(recommended, start=1): if item in ground_truth: return 1.0 / i return 0.0 def mean_average_precision_at_k( all_recommended: list[list[int]], all_ground_truth: list[set[int]], k: int,) -> float: """MAP@K — tüm kullanıcılar için AP'lerin ortalaması.""" aps = [ average_precision_at_k(rec, gt, k) for rec, gt in zip(all_recommended, all_ground_truth) ] return float(np.mean(aps)) def mean_reciprocal_rank( all_recommended: list[list[int]], all_ground_truth: list[set[int]],) -> float: """MRR — tüm kullanıcılar için RR'lerin ortalaması.""" rrs = [ reciprocal_rank(rec, gt) for rec, gt in zip(all_recommended, all_ground_truth) ] return float(np.mean(rrs)) # Testrecommended = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]ground_truth = {101, 104, 107} print(f"AP@10: {average_precision_at_k(recommended, ground_truth, 10):.4f}")print(f"RR: {reciprocal_rank(recommended, ground_truth):.4f}") # AP@10:# Pos 1 (101): hit, P@1 = 1/1 = 1.0# Pos 4 (104): hit, P@4 = 2/4 = 0.5# Pos 7 (107): hit, P@7 = 3/7 = 0.4286# AP@10 = (1.0 + 0.5 + 0.4286) / 3 = 0.6429# RR: 1.0 (ilk hit pozisyon 1) MAP@K + MRR — sıralı metrikler.
4) NDCG@K — Recommender'ın Altın Standardı#
Niçin NDCG Özel?#
NDCG (Normalized Discounted Cumulative Gain) iki özel şey yapar:
- Discount — geç sıralardaki hit'lere düşük ağırlık.
- Normalization — "ideal" durumla normalize, sonuç 0-1 arası.
Bunlar onu sıralama-hassas metric'lerin en güçlüsü yapıyor.
Adım 1: DCG (Discounted Cumulative Gain)#
Pozisyondaki item'ın relevance'ını log(pozisyon) ile böl:
Discount Faktörü — Tabloyla#
| Pozisyon i | log₂(i+1) | Discount = 1/log₂(i+1) |
|---|---|---|
| 1 | 1.00 | 1.000 |
| 2 | 1.58 | 0.631 |
| 3 | 2.00 | 0.500 |
| 4 | 2.32 | 0.431 |
| 5 | 2.58 | 0.387 |
| 10 | 3.46 | 0.289 |
| 20 | 4.39 | 0.228 |
Birinci sıradakine tam ağırlık, 10. sıradakine 0.289 ağırlık. Üstteki çok önemli.
Adım 2: IDCG (Ideal DCG)#
Aynı kullanıcı için mükemmel sıralamadaki DCG. Yani gerçek pozitif item'ları en yukarı koysak DCG ne olurdu.
Adım 3: NDCG#
Tablolu Örnek#
Test set: T_u = {A, B, C} ← 3 pozitif, rel = 1 her biri Top-10: [A, X, Y, B, Z, W, C, U, V, T] Pozisyonlar: A=1, B=4, C=7 DCG = 1/log₂(2) + 1/log₂(5) + 1/log₂(8) = 1/1.0 + 1/2.32 + 1/3.0 = 1.0 + 0.431 + 0.333 = 1.764 Ideal sıralama: [A, B, C, ...] IDCG = 1/log₂(2) + 1/log₂(3) + 1/log₂(4) = 1/1.0 + 1/1.585 + 1/2.0 = 1.0 + 0.631 + 0.5 = 2.131 NDCG@10 = 1.764 / 2.131 = 0.828
Binary vs Graded Relevance#
Yukarıda her item için rel=1 (binary). Eğer rating'ler 1-5 ise:
rel_i = 2^rating - 1
Bu durumda 5★ item 31 puan, 4★ item 15 puan getirir — graded relevance.
python
# metrics/ndcg.py — NDCG@K implementasyonuimport numpy as np def dcg_at_k(relevances: list[float], k: int) -> float: """Discounted Cumulative Gain — verilen sıralamada.""" relevances = np.asarray(relevances[:k], dtype=np.float32) if len(relevances) == 0: return 0.0 discounts = 1.0 / np.log2(np.arange(2, len(relevances) + 2)) return float(np.sum(relevances * discounts)) def ndcg_at_k( recommended: list[int], ground_truth: dict[int, float] | set[int], k: int,) -> float: """ NDCG@K — normalized. ground_truth: dict ise {item_id: relevance_score}, set ise binary (rel=1). """ # ground_truth'u dict'e normalize et if isinstance(ground_truth, set): gt_dict = {item: 1.0 for item in ground_truth} else: gt_dict = ground_truth if not gt_dict: return 0.0 # Verilen sıralamadaki relevance'lar rels_predicted = [gt_dict.get(item, 0.0) for item in recommended[:k]] # Ideal sıralama — relevance'ları büyükten küçüğe ideal_rels = sorted(gt_dict.values(), reverse=True)[:k] dcg = dcg_at_k(rels_predicted, k) idcg = dcg_at_k(ideal_rels, k) return dcg / idcg if idcg > 0 else 0.0 # Test (binary)recommended = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]ground_truth_set = {101, 104, 107}print(f"NDCG@10 (binary): {ndcg_at_k(recommended, ground_truth_set, 10):.4f}") # Test (graded — 5★ rating sistemi)ground_truth_dict = {101: 5.0, 104: 4.0, 107: 3.0, 999: 5.0}print(f"NDCG@10 (graded): {ndcg_at_k(recommended, ground_truth_dict, 10):.4f}") # NDCG@10 (binary): 0.8290 ← yukarıdaki manuel hesapla aynı# NDCG@10 (graded): farklı çünkü relevance ağırlıkları farklı NDCG@K — binary ve graded relevance ile.
Karşılaştırmalı Çalışma — Aynı Tahmin Üzerine Tüm Metrikler#
Aynı user için aynı top-10 önerisi ile tüm metrikleri hesaplayalım:
python
import numpy as npfrom metrics.topn_metrics import precision_at_k, recall_at_k, hit_rate_at_kfrom metrics.ranked_metrics import average_precision_at_k, reciprocal_rankfrom metrics.ndcg import ndcg_at_k recommended = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]ground_truth = {101, 104, 107} print(f"Precision@10: {precision_at_k(recommended, ground_truth, 10):.4f}")print(f"Recall@10: {recall_at_k(recommended, ground_truth, 10):.4f}")print(f"HR@10: {hit_rate_at_k(recommended, ground_truth, 10):.4f}")print(f"AP@10: {average_precision_at_k(recommended, ground_truth, 10):.4f}")print(f"RR: {reciprocal_rank(recommended, ground_truth):.4f}")print(f"NDCG@10: {ndcg_at_k(recommended, ground_truth, 10):.4f}") # Çıktı:# Precision@10: 0.3000 ← önerimin 30%'i doğru# Recall@10: 1.0000 ← gerçeklerin 100%'ü top-10'da# HR@10: 1.0000 ← en az 1 hit var# AP@10: 0.6429 ← sıra hassas precision# RR: 1.0000 ← ilk hit pozisyon 1# NDCG@10: 0.8290 ← sıralama gücünü ölç # 💡 Aynı sıralama 6 farklı sayı veriyor. Hangisi "doğru" olduğu sorulan soruya bağlı. Tüm metriklerin birarada karşılaştırması.
Hangi Metric Ne Zaman?#
| Problem | Metric (öncelik sırasıyla) |
|---|---|
| Rating prediction (Netflix Prize-tarzı) | RMSE, sonra MAE |
| Top-N öneri (e-ticaret feed) | NDCG@10, sonra Recall@20 |
| Search ranking | NDCG@10, sonra MAP@10 |
| Next-item prediction (sequential) | HR@10, sonra MRR |
| Çoklu item retrieve (basket) | Recall@K, sonra MAP@K |
| Recommendation diversity | Coverage (Modül 3.2) |
Üretimde "Birincil" Metric Seçme#
Tek metric optimize edersin — yoksa optimization conflict olur. Yaygın pratik:
- Birincil: NDCG@10 (en bilgilendirici, sıra-hassas).
- İkincil (sanity checks): HR@10, Recall@20, Coverage.
- Guardrails: Latency, popularity bias (artmamalı).
🚨 Sık yapılan hata: K seçimi
K=10 ve K=100 farklı şeyler söyler. K küçükse model 'top-1'i bilmek' iyi olmalı (çok zor). K büyükse 'top-100 listede olmak yeter' — daha kolay. Bir paper okuyup 'Recall@K = 0.5' gördüğünde K'yı kontrol et. Bazı paper K=20, bazıları K=100 — direct karşılaştırma yanlış.
Sıradaki Ders#
Bir sonraki derste (3.2) — beyond-accuracy metrikler. Coverage, ILS (intra-list similarity), novelty, serendipity, popularity bias. Bunlar tek başına "iyi öneri" değil; ana metrikler ile birlikte raporlanır. Bir mühendis "modelim NDCG'si iyi ama coverage'ı çöp" diyebilmeli.
Sık Sorulan Sorular
Tipik benchmark aralıkları: Popularity baseline: 0.06-0.08. Item-Item k-NN: 0.10-0.13. ALS / BiasedMF: 0.13-0.16. BPR-MF: 0.14-0.17. LightGCN: 0.17-0.20. SASRec: 0.18-0.22. State-of-art (2024+): 0.22-0.27. Bunlar ortalama — exact protocol (leave-one-out vs time split) çok farklı sonuç verir.
Yorumlar & Soru-Cevap
(0)Yorum yazmak için giriş yap.
Yorumlar yükleniyor...
İlgili İçerikler
Modül 0: Kurs Çerçevesi ve Atölye Kurulumu
Öneri Sistemleri Neden Bu Kadar Önemli? Bir Disiplinin Doğuşu, Bugünü ve Yarını
Öğrenmeye BaşlaModül 0: Kurs Çerçevesi ve Atölye Kurulumu
Recommender Engineer Kimdir? Yetkinlik Atlası ve Junior → Staff Kariyer Haritası
Öğrenmeye BaşlaModül 0: Kurs Çerçevesi ve Atölye Kurulumu