· 7 dk okuma
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üşük | 0,352 |
| %5'lik dilim | 0,517 |
| Ortanca | 0,634 |
| %95'lik dilim | 0,722 |
| En yüksek | 0,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 | İçindekiler | R² |
|---|---|---|
| 1 | 24 erkek, 20-46 | 0,034 |
| 2 | 24 erkek, 20-46 | 0,443 |
| 3 | 15 erkek 46+, 9 erkek 20-46 | 0,499 |
| 4 | 24 erkek, 46+ | 0,432 |
| 5 | 24 erkek, 46+ | 0,548 |
| 6 | 14 erkek 46+, 10 kadın 20-46 | 0,573 |
| 7 | 24 kadın, 20-46 | 0,385 |
| 8 | 19 kadın 20-46, 4 kadın 46+ | 0,364 |
| 9 | 23 kadın, 46+ | 0,784 |
| 10 | 23 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:
| MAE | Test parçasındaki beyin ağırlığının standart sapması | |
|---|---|---|
| Karıştırmadan | 57,3 g | 101 g |
| Karıştırarak | 56,5 g | 117 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:
- Bütün modeller aynı 50 bölmede sınanıyor.
random_statesabit 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. - Ö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.
- 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ı:
| Model | MAE (g) | R² |
|---|---|---|
| Doğrusal (baş hacmi) | 56,7 ± 8,7 | 0,602 ± 0,128 |
| Doğrusal (baş hacmi, cinsiyet, yaş grubu) | 56,2 ± 8,3 | 0,611 ± 0,122 |
| Polinom, 2. derece (baş hacmi) | 57,0 ± 9,0 | 0,603 ± 0,127 |
| Ridge (baş hacmi, cinsiyet, yaş grubu) | 56,2 ± 8,3 | 0,612 ± 0,120 |
| Random Forest (baş hacmi, cinsiyet, yaş grubu) | 59,5 ± 10,1 | 0,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.