İçeriğe atla
md ~/yazilar/capraz-dogrulama-tek-bolme-yaniltir
Atatürk'ü Saygı ve Özlemle Anıyoruz

10 Kasım · 88. yıl dönümü

Atatürk'ü Saygı ve Özlemle Anıyoruz

Cumhuriyetimizin kurucusu Mustafa Kemal Atatürk, 10 Kasım 1938'de saat 09.05'te İstanbul'da, Dolmabahçe Sarayı'nda hayatını kaybetti. Her yıl bu saatte bütün ülkede saygı duruşunda bulunuyoruz.

Bize emanet ettiği Cumhuriyeti yaşatmak hepimizin görevi.

Benim naçiz vücudum elbet bir gün toprak olacaktır, fakat Türkiye Cumhuriyeti ilelebet payidar kalacaktır.Mustafa Kemal Atatürk
Tüm yazılar

· 7 dk okuma

Tek Bölme Yanıltır: Çapraz Doğrulama ile Dürüst Model Değerlendirmesi

Tek Bölme Yanıltır: Çapraz Doğrulama ile Dürüst Model Değerlendirmesi

Bir makine öğrenmesi modelinin ne kadar iyi olduğunu ölçmenin en yaygın yolu veriyi bir kez eğitim ve test olarak bölmektir. Hızlıdır, anlaşılırdır ve tek bir sayı verir. Sorun şu ki o sayı, verinin hangi satırlarının teste düştüğüne göre çok değişebilir. Bu yazıda Beyin Ağırlığı Tahmini projemde tek bölmenin beni nasıl yanılttığını, çapraz doğrulamaya geçerken düştüğüm tuzağı ve modeli sonunda nasıl seçtiğimi, hepsini ölçtüğüm sayılarla anlatıyorum.

Proje ve veri

Proje basit bir soruya cevap veriyor: baş hacmi bilinen bir yetişkinin beyni yaklaşık kaç gramdır? Veri, 1905'te yayımlanmış bir çalışmadan 237 yetişkinin baş hacmi, beyin ağırlığı, cinsiyeti ve yaş grubu. Hedef değişken beyin ağırlığı; ortalaması 1.283 g, standart sapması 120 g.

Modeli değerlendirmek için iki metrik kullanıyorum:

  • Ortalama mutlak hata (MAE): Tahminin gerçekten ortalama kaç gram saptığı. Birimi gram, yorumu kolay.
  • R²: Beyin ağırlığındaki değişkenliğin ne kadarının açıklandığı. 1 mükemmel, 0 "hep ortalamayı söylemek" kadar iyi demek.

Tek bölmenin verdiği sayı

Projenin ilk sürümü klasik yolu izliyordu: verinin %70'iyle eğit, %30'uyla test et.

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=0)

Doğrusal regresyon bu bölmede R² 0,659 verdi ve README'ye "~0,658" olarak yazıldı. Kod tekrar çalıştırılabilirdi, random_state=0 her seferinde aynı bölmeyi veriyordu. Yani sonuç tekrarlanabilirdi. Ama tekrarlanabilir olmak, temsil edici olmak demek değil.

Bunu görmek için aynı modeli aynı veride 1.000 farklı rastgele bölmeyle denedim. Tek değişen random_state:

R²
En düşük0,352
%5'lik dilim0,517
Ortanca0,634
%95'lik dilim0,722
En yüksek0,784

Aynı model, aynı veri. Sadece hangi 71 kişinin teste düştüğü değişince R² 0,35 ile 0,78 arasında gidip geliyor. İlk sürümdeki 0,659, bölmelerin yaklaşık üçte birinin (%35) ulaştığı bir değer: ortancanın biraz üstünde, şanslı tarafta. 237 satırlık bir veride test kümesi 71 kişi; birkaç sıra dışı kişinin teste düşüp düşmemesi sonucu belirgin biçimde değiştiriyor.

Tek bir bölme bu dağılımdan rastgele çekilmiş tek bir sayı. Hangi uçtan geldiği bilinemez.

Çapraz doğrulama: her satır bir kez test olur

K katlı çapraz doğrulama (k-fold cross-validation) bu rastlantıyı azaltır. Veri k parçaya bölünür. Model k kez eğitilir: her seferinde bir parça test, kalanı eğitim olur. Sonunda her satır tam bir kez test verisinde yer almış olur ve k tane skorun ortalaması alınır.

10 katlı çapraz doğrulamayı ilk denediğimde sonuç şaşırtıcıydı: R² 0,459. Tek bölmenin 0,659'undan çok daha düşük. Model bir anda kötüleşmiş gibiydi.

Tuzak: sıralı veriyi karıştırmadan bölmek

Sorun modelde değil, bölmedeydi. scikit-learn'deki KFold varsayılan olarak veriyi karıştırmaz; satırları sırasıyla parçalara ayırır. Benim CSV dosyam ise sıralıydı: önce 20-46 yaş erkekler, sonra 46 yaş üstü erkekler, sonra kadınlar.

Karıştırmadan yapılan bölmede her test parçasının içine baktım:

ParçaİçindekilerR²
124 erkek, 20-460,034
224 erkek, 20-460,443
315 erkek 46+, 9 erkek 20-460,499
424 erkek, 46+0,432
524 erkek, 46+0,548
614 erkek 46+, 10 kadın 20-460,573
724 kadın, 20-460,385
819 kadın 20-46, 4 kadın 46+0,364
923 kadın, 46+0,784
1023 kadın, 46+0,533

Test parçalarının çoğu tek bir gruptan oluşuyor. Bir parçada R² 0,03'e kadar düşüyor.

Burada ilginç bir ayrıntı var. Aynı iki bölmede hatayı gram cinsinden de ölçtüm:

MAETest parçasındaki beyin ağırlığının standart sapması
Karıştırmadan57,3 g101 g
Karıştırarak56,5 g117 g

Tahminler neredeyse aynı derecede iyi: aradaki fark 1 gramın altında. R²'yi düşüren asıl şey test parçasının kendisi. Tek bir cinsiyet ve yaş grubundan oluşan bir parçada beyin ağırlıkları birbirine daha yakın. R² hatayı bu değişkenliğe oranladığı için, değişkenlik küçülünce aynı hata daha kötü görünüyor.

Bu yüzden ders şu: karıştırmadan yapılan çapraz doğrulama, modelin gerçek başarısını değil, verinin dosyadaki sırasını ölçer. Bölmeden önce karıştırmak isteğe bağlı bir ayar değil. Karıştırarak yapılan 10 katlı çapraz doğrulama R² 0,626 verdi.

Bir de şunu öğrendim: tek bir metriğe bakmak yetmiyor. R² tek başına "model çöktü" diyordu; MAE ise "tahminler aynı, ölçtüğün şey değişti" diyordu.

Tekrarlı çapraz doğrulama ve dürüst karşılaştırma

Karıştırmak da bir rastlantı ekler: bir karıştırma şanslı, bir başkası şanssız olabilir. Bunu da ortadan kaldırmak için 10 katlı çapraz doğrulamayı 5 kez, her seferinde farklı karıştırmayla tekrarladım. Toplam 50 bölme.

def compare_models(
    df: pd.DataFrame, n_splits: int = 10, n_repeats: int = 5, seed: int = 0
) -> list[Score]:
    """Bütün adayları (ve referans modeli) aynı bölmelerle değerlendirir."""
    # Aynı random_state = her model birebir aynı eğitim/test bölmeleriyle sınanır
    cv = RepeatedKFold(n_splits=n_splits, n_repeats=n_repeats, random_state=seed)
    return [evaluate(c, df, cv) for c in [*CANDIDATES, BASELINE]]

Bu kodda üç karar var:

  1. Bütün modeller aynı 50 bölmede sınanıyor. random_state sabit olduğu için her aday birebir aynı eğitim ve test satırlarını görüyor. Bir model daha kolay bölmelere denk geldiği için öne geçemiyor.
  2. Ön işleme modelin içinde. Ölçekleme ve kategorileri 0/1 sütunlarına çevirme scikit-learn pipeline'larının içinde yapılıyor. Böylece her bölmede bu adımlar sadece eğitim parçasından öğreniliyor; test parçasının ortalaması ya da sapması modele sızmıyor.
  3. Bir referans model var. Her zaman ortalamayı söyleyen bir "model" de aynı bölmelerde sınanıyor. Diğer modellerin ne kadar iyi olduğu ancak buna göre anlaşılır.

Beş aday ve referans modelin sonuçları, 50 bölmenin ortalaması ve standart sapması:

ModelMAE (g)R²
Doğrusal (baş hacmi)56,7 ± 8,70,602 ± 0,128
Doğrusal (baş hacmi, cinsiyet, yaş grubu)56,2 ± 8,30,611 ± 0,122
Polinom, 2. derece (baş hacmi)57,0 ± 9,00,603 ± 0,127
Ridge (baş hacmi, cinsiyet, yaş grubu)56,2 ± 8,30,612 ± 0,120
Random Forest (baş hacmi, cinsiyet, yaş grubu)59,5 ± 10,10,551 ± 0,149
Referans (hep ortalama)94,5 ± 14,8−0,048 ± 0,055

Tek değişkenli doğrusal modelin dürüst R²'si 0,602. İlk sürümün bildirdiği 0,659'dan düşük, ama bu sefer sayının arkasında 50 bölme var ve ± 0,128'lik sapma, tek bölmenin neden bu kadar oynak olduğunu da açıklıyor.

Hangi model? Bir standart hata kuralı

Tabloda en düşük MAE Ridge'in: 56,19 g. Tek değişkenli doğrusal model 56,74 g ile yarım gram geride. Peki Ridge'i mi seçmeliyim?

Bu yarım gram, ölçümün kendi belirsizliği yanında çok küçük. Bunu ölçmek için bir standart hata kuralını kullandım: en iyi modelin ortalama hatasına, o ortalamanın standart hatasını ekleyip bir eşik buluyorum. Eşiğin altında kalan modeller arasından en basitini seçiyorum.

def select_model(scores: list[Score]) -> tuple[Score, str]:
    """Bir standart hata kuralı: en iyiye yeterince yakın olan en basit modeli seçer."""
    ranked = [s for s in scores if s.name != BASELINE.name]
    best = min(ranked, key=lambda s: s.mae)
    threshold = best.mae + best.mae_std / math.sqrt(best.folds)
    # ranked basitlik sırasında olduğu için eşiği geçen ilk model en basitidir
    chosen = next(s for s in ranked if s.mae <= threshold)
    # ...

Ridge'in standart sapması 8,3 g ve 50 bölme var; standart hata 8,3 / √50 ≈ 1,17 g. Eşik 56,19 + 1,17 = 57,36 g. Aday listesi basitlik sırasında: ilk aday tek değişkenli doğrusal model ve 56,74 g ile eşiğin altında. Seçilen model bu oldu:

beyin ağırlığı ≈ 325,6 + 0,2634 × baş hacmi

Cinsiyet ve yaş grubunu eklemek yarım gram kazandırıyor. Bu kazanç ölçüm belirsizliğinin içinde ve kullanıcıdan iki bilgi daha istemeye değmiyor. Random Forest ise 237 satırda en kötü sonucu verdi: veri bu kadar küçükken ezberlemeye daha yatkın.

Özet

  • Tek bir eğitim/test bölmesi, rastgele çekilmiş tek bir sayıdır. Bu veride aynı model 1.000 bölmede R² 0,35 ile 0,78 arasında sonuç verdi. İlk sürümün 0,659'u şanslı taraftaydı.
  • K katlı çapraz doğrulama her satırı bir kez teste koyar, ama scikit-learn'ün KFold'u varsayılan olarak karıştırmaz. Sıralı bir dosyada karıştırmadan bölmek R²'yi 0,459'a düşürdü.
  • O düşüş tahminlerin kötüleşmesinden değil, tek gruplu test parçalarından geliyordu: MAE neredeyse aynıydı. Bir metriğe değil, en az ikisine bakmak gerekiyor.
  • Tekrarlı çapraz doğrulama, bütün modeller için aynı bölmeler, pipeline içinde ön işleme ve bir referans model, karşılaştırmayı dürüst yaptı.
  • Bir standart hata kuralı, en iyiye yeterince yakın olan en basit modeli seçmek için ilkeli bir gerekçe verdi.

Modeli tarayıcıda çalıştıran hesaplayıcı brain.miracdeprem.com adresinde; model sayfasında karşılaştırma tablosu ve seçim gerekçesi de var. Kaynak kodu GitHub'da: github.com/MrcDprm/brain-weight-prediction.