Kitaplar / Yöntem ile Makine Öğrenmesi / Bölüm 1 — Denetimli öğrenmenin temelleri
Bölüm 1 — Denetimli öğrenmenin temelleri
Denetimli öğrenmede elimizde doğru cevabı bilinen örnekler vardır; model bu örneklerden genel bir kural çıkarır. Cevap bir sayıysa (fiyat) regresyon, bir sınıfsa (geçti/kaldı) sınıflandırma yapıyoruz demektir.
1. Doğrusal regresyon
Fikir. Veriye en iyi uyan düz çizgiyi buluruz. “En iyi”, tahminlerle gerçek değerler arasındaki farkların karelerinin ortalamasının (OKH, ortalama kare hata) en küçük olduğu çizgidir. Eğimi ve kesişimi gradyan inişiyle adım adım düzeltiriz: hatayı artıran yönün tersine küçük bir adım.
program DoğrusalRegresyon
{ Ev büyüklüğünden (m²) fiyatı (bin TL) tahmin eden doğrusal model: ŷ = a·x + b
Parametreler gradyan inişiyle öğrenilir, sonra kapalı form çözümüyle karşılaştırılır. }
değişken
X, Y: ondalık dizisi
a, b, ga, gb, hata, okh, oran: ondalık
i, adım, n: tamsayı
ox, oy, pay, payda: ondalık
fonksiyon Ölçekle(m2: ondalık): ondalık
başla
sonuç := m2 / 100 { 100 m² = 1 birim: sayılar küçük kalınca iniş kararlı olur }
bitir
başla
X := [50, 65, 80, 95, 110, 130, 150]
Y := [1450, 1900, 2250, 2700, 3100, 3600, 4150]
n := Uzunluk(X)
a := 0; b := 0; oran := 0.1
1'den 3000'e kadar her adım için
başla
ga := 0; gb := 0; okh := 0
0'dan n - 1'e kadar her i için
başla
hata := a * Ölçekle(X[i]) + b - Y[i]
ga := ga + 2 * hata * Ölçekle(X[i]) / n
gb := gb + 2 * hata / n
okh := okh + hata * hata / n
bitir
a := a - oran * ga
b := b - oran * gb
eğer adım = 1 veya adım mod 1000 = 0 ise
yaz(Biçimle('Adım {:5} OKH = {:12.2}', adım, okh), 1)
bitir
yaz(Biçimle('Gradyan inişi: fiyat ≈ {:.2} × m² + {:.2}', a / 100, b), 1)
{ Kapalı form (en küçük kareler): a = Σ(x-x̄)(y-ȳ) / Σ(x-x̄)² }
ox := Toplam(X) / n; oy := Toplam(Y) / n
pay := 0; payda := 0
0'dan n - 1'e kadar her i için
başla
pay := pay + (X[i] - ox) * (Y[i] - oy)
payda := payda + (X[i] - ox) ** 2
bitir
yaz(Biçimle('Kapalı form: fiyat ≈ {:.2} × m² + {:.2}', pay / payda, oy - pay / payda * ox), 1)
yaz(Biçimle('120 m² için tahmin: {:.0} bin TL', a * Ölçekle(120) + b), 1)
bitir.Programın çıktısı şöyledir:
Adım 1 OKH = 8265357.14
Adım 1000 OKH = 424.67
Adım 2000 OKH = 424.67
Adım 3000 OKH = 424.67
Gradyan inişi: fiyat ≈ 26.83 × m² + 129.40
Kapalı form: fiyat ≈ 26.83 × m² + 129.40
120 m² için tahmin: 3349 bin TLNe öğrendik. Gradyan inişi, formülün doğrudan verdiği çözüme 1000 adımda ulaştı. OKH sıfıra inmez, çünkü gerçek fiyatlar tam bir doğru üzerinde durmaz. Sık yapılan hata şudur: girdileri ölçeklemeden (100’e bölmeden) aynı öğrenme oranını kullanmak; adımlar çok büyük olur ve hata patlar.
Alıştırma. Ölçekle fonksiyonunu sonuç := m2 yapıp programı çalıştırınız. OKH ne oluyor? Hangi oran değeriyle yeniden yakınsıyor?
2. Lojistik regresyon
Fikir. Doğrusal bir puanı sigmoid fonksiyonundan geçirip 0 ile 1 arasında bir olasılığa çeviririz. Olasılık 0,5’i geçerse sınıf 1 deriz. Kayıp olarak çapraz entropi kullanılır: emin olduğu bir tahminde yanılan modeli çok ağır cezalandırır.
program LojistikRegresyon
{ Çalışma saatinden sınavı geçme olasılığını tahmin eder: P(geçer) = σ(w·x + b)
σ(z) = 1 / (1 + e^(-z)) her sayıyı 0 ile 1 arasına sıkıştırır. Kayıp: çapraz entropi. }
kullan Matematik
değişken
saat: ondalık dizisi
geçti: tamsayı dizisi
w, b, p, gw, gb, kayıp: ondalık
i, adım, n, isabet: tamsayı
başla
saat := [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0]
geçti := [0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1]
n := Uzunluk(saat)
w := 0; b := 0
1'den 2000'e kadar her adım için
başla
gw := 0; gb := 0; kayıp := 0
0'dan n - 1'e kadar her i için
başla
p := Sigmoid(w * saat[i] + b)
kayıp := kayıp - (geçti[i] * Ln(p) + (1 - geçti[i]) * Ln(1 - p)) / n
gw := gw + (p - geçti[i]) * saat[i] / n { çapraz entropinin türevi şaşırtıcı biçimde basittir }
gb := gb + (p - geçti[i]) / n
bitir
w := w - 0.5 * gw
b := b - 0.5 * gb
eğer adım mod 500 = 0 ise yaz(Biçimle('Adım {:4} kayıp = {:.4}', adım, kayıp), 1)
bitir
yaz(Biçimle('Model: P(geçer) = σ({:.3} × saat {} {:.3})', w, eğer b < 0 ise '-' değilse '+', Mutlak(b)), 1)
yaz(Biçimle('Karar sınırı (P = 0,5): {:.2} saat', -b / w), 1)
isabet := 0
0'dan n - 1'e kadar her i için
başla
p := Sigmoid(w * saat[i] + b)
eğer (p >= 0.5) = (geçti[i] = 1) ise isabet := isabet + 1
bitir
yaz('Eğitim doğruluğu: ' + isabet + '/' + n, 1)
yaz(Biçimle('2 saat çalışan için olasılık: {:.2}', Sigmoid(w * 2 + b)), 1)
yaz(Biçimle('5 saat çalışan için olasılık: {:.2}', Sigmoid(w * 5 + b)), 1)
bitir.Programın çıktısı şöyledir:
Adım 500 kayıp = 0.3650
Adım 1000 kayıp = 0.3649
Adım 1500 kayıp = 0.3649
Adım 2000 kayıp = 0.3649
Model: P(geçer) = σ(1.424 × saat - 4.630)
Karar sınırı (P = 0,5): 3.25 saat
Eğitim doğruluğu: 10/12
2 saat çalışan için olasılık: 0.14
5 saat çalışan için olasılık: 0.92Ne öğrendik. Model 3,25 saati bir eşik olarak öğrendi. 12 öğrenciden 2’si (2,5 saatte geçen, 4 saatte kalan) bu eşiğe uymuyor; hiçbir düz sınır onları doğru ayıramaz. Adı “regresyon” olsa da bu bir sınıflandırma yöntemidir.
Alıştırma. Veriye ikinci bir öznitelik ekleyiniz (örneğin uyku saati) ve modeli w1·saat + w2·uyku + b biçimine genişletiniz.
3. K-en yakın komşu (k-NN)
Fikir. “Bana arkadaşını söyle, sana kim olduğunu söyleyeyim.” Yeni bir örneğin sınıfını, ona en yakın k eğitim örneğinin oy çokluğu belirler. Model eğitilmez; tüm iş tahmin anında yapılır.
program KEnYakınKomşu
{ Meyveyi ağırlığına (g) ve kabuk pürüzlülüğüne (0-10) bakarak sınıflandırır.
Yeni bir meyvenin sınıfı, ona en yakın k eğitim örneğinin oy çokluğuyla belirlenir. }
kullan Matematik
değişken
veri: ondalık dizisi dizisi
etiket: metin dizisi
yeniler: ondalık dizisi dizisi
i: tamsayı
s: ondalık dizisi
fonksiyon Uzaklık(p, q: ondalık dizisi): ondalık
başla
{ Öklid uzaklığı; ağırlık 100 g ölçeğinde, pürüzlülük 1 ölçeğinde olsun diye ağırlığı 20'ye bölüyoruz }
sonuç := Kök(((p[0] - q[0]) / 20) ** 2 + (p[1] - q[1]) ** 2)
bitir
fonksiyon Sınıflandır(x: ondalık dizisi; k: tamsayı): metin
değişken
sıra: tamsayı dizisi
oyElma, oyPortakal, j: tamsayı
başla
sıra := Sırala(Aralık(Uzunluk(veri)), fonksiyon(n: tamsayı) => Uzaklık(veri[n], x))
oyElma := 0; oyPortakal := 0
0'dan k - 1'e kadar her j için
eğer etiket[sıra[j]] = 'elma' ise oyElma := oyElma + 1 değilse oyPortakal := oyPortakal + 1
sonuç := eğer oyElma > oyPortakal ise 'elma' değilse 'portakal'
bitir
başla
{ [186, 7] alışılmadık, portakala benzeyen bir elma: k=1 bu tek örneğe kanar }
veri := [[150, 2], [170, 1], [140, 3], [130, 2], [180, 2], [186, 7],
[160, 7], [200, 8], [190, 6], [210, 7], [175, 9]]
etiket := ['elma', 'elma', 'elma', 'elma', 'elma', 'elma',
'portakal', 'portakal', 'portakal', 'portakal', 'portakal']
yeniler := [[155, 3], [185, 7], [165, 5], [200, 3]]
0'dan Uzunluk(yeniler) - 1'e kadar her i için
başla
s := yeniler[i]
yaz(Biçimle('{} g, pürüzlülük {} → k=1: {:9} k=3: {:9} k=5: {}',
s[0], s[1], Sınıflandır(s, 1), Sınıflandır(s, 3), Sınıflandır(s, 5)), 1)
bitir
bitir.Programın çıktısı şöyledir:
155 g, pürüzlülük 3 → k=1: elma k=3: elma k=5: elma
185 g, pürüzlülük 7 → k=1: elma k=3: portakal k=5: portakal
165 g, pürüzlülük 5 → k=1: portakal k=3: portakal k=5: portakal
200 g, pürüzlülük 3 → k=1: elma k=3: elma k=5: elmaNe öğrendik. k=1 iken model, 185 g’lık meyveyi tek bir tuhaf örnek yüzünden elma sandı; k=3’te çoğunluk bu gürültüyü bastırdı. Özniteliklerin ölçeği çok önemlidir: ağırlığı 20’ye bölmeseydik uzaklığı neredeyse yalnızca gramlar belirlerdi.
Alıştırma. Uzaklık içindeki / 20 bölümünü kaldırınız. Hangi tahminler değişiyor, neden?
4. Naif Bayes
Fikir. Her sınıfta hangi sözcüklerin ne sıklıkta geçtiğini sayarız. Yeni bir metin geldiğinde Bayes kuralıyla “bu sözcükleri hangi sınıf üretmiş olabilir?” diye sorarız. “Naif” denmesinin nedeni, sözcüklerin birbirinden bağımsız olduğunu varsaymasıdır; bu varsayım yanlış olsa da yöntem şaşırtıcı derecede iyi çalışır.
program NaifBayes
{ E-postaları 'istenmeyen' ya da 'normal' diye sınıflandırır.
Bayes kuralı: P(sınıf | sözcükler) ∝ P(sınıf) × Π P(sözcük | sınıf)
"Naif" varsayım: sözcükler, sınıf bilindiğinde birbirinden bağımsızdır. }
kullan Matematik
değişken
metinler, sınıflar, sınıfAdları, denemeler: metin dizisi
sayım: sözlük { sınıf → (sözcük → kaç kez) }
belgeSayısı, toplamSözcük: sözlük
sözlükçe: sözlük { görülen tüm farklı sözcükler }
i: tamsayı
s, c: metin
fonksiyon Sözcükler(m: metin): metin dizisi
başla
sonuç := KüçükHarf(m).Ayır
bitir
fonksiyon Puan(m: metin; sınıfAdı: metin): ondalık
değişken
w: metin
p: ondalık
sayaç: sözlük
başla
{ Olasılıkları çarpmak yerine logaritmalarını topluyoruz: küçük sayılar sıfıra yuvarlanmasın }
p := Ln(belgeSayısı[sınıfAdı] / Uzunluk(metinler))
sayaç := sayım[sınıfAdı]
Sözcükler(m) içindeki her w için
{ Laplace düzeltmesi (+1): hiç görülmemiş sözcük olasılığı sıfırlamasın }
p := p + Ln((sayaç.Al(w, 0) + 1) / (toplamSözcük[sınıfAdı] + Uzunluk(sözlükçe)))
sonuç := p
bitir
başla
metinler := ['bedava para kazan hemen tıkla', 'kazan büyük ödül bedava',
'hemen tıkla indirim kampanya', 'toplantı yarın saat onda',
'rapor ekte toplantı notları', 'yarın öğle yemeği var mı',
'proje raporu yarın teslim']
sınıflar := ['istenmeyen', 'istenmeyen', 'istenmeyen', 'normal', 'normal', 'normal', 'normal']
sınıfAdları := ['istenmeyen', 'normal']
sayım := [:]; belgeSayısı := [:]; toplamSözcük := [:]; sözlükçe := [:]
sınıfAdları içindeki her c için
başla
sayım[c] := [:]; belgeSayısı[c] := 0; toplamSözcük[c] := 0
bitir
{ Eğitim: her sınıfta her sözcüğün kaç kez geçtiğini say }
0'dan Uzunluk(metinler) - 1'e kadar her i için
başla
c := sınıflar[i]
belgeSayısı[c] := belgeSayısı[c] + 1
Sözcükler(metinler[i]) içindeki her s için
başla
sayım[c][s] := sayım[c].Al(s, 0) + 1
toplamSözcük[c] := toplamSözcük[c] + 1
sözlükçe[s] := 1
bitir
bitir
yaz('Sözlükçe: ' + Uzunluk(sözlükçe) + ' farklı sözcük', 1)
denemeler := ['bedava ödül kazan', 'yarın toplantı var', 'hemen rapor teslim', 'indirim kampanya yarın']
denemeler içindeki her s için
yaz(Biçimle('{:24} → {:10} (log puanlar: istenmeyen {:.2}, normal {:.2})', '"' + s + '"',
eğer Puan(s, 'istenmeyen') > Puan(s, 'normal') ise 'istenmeyen' değilse 'normal',
Puan(s, 'istenmeyen'), Puan(s, 'normal')), 1)
bitir.Programın çıktısı şöyledir:
Sözlükçe: 23 farklı sözcük
"bedava ödül kazan" → istenmeyen (log puanlar: istenmeyen -8.71, normal -11.63)
"yarın toplantı var" → normal (log puanlar: istenmeyen -11.60, normal -8.45)
"hemen rapor teslim" → normal (log puanlar: istenmeyen -10.50, normal -10.24)
"indirim kampanya yarın" → istenmeyen (log puanlar: istenmeyen -10.21, normal -10.24)Ne öğrendik. Son örnekte “indirim kampanya” istenmeyene, “yarın” normale çekti; puanlar çok yakın çıktı. Logaritma kullanmak şarttır: yüzlerce küçük olasılığı çarpmak bilgisayarda sıfıra yuvarlanır. +1 eklenmezse eğitimde görülmemiş tek bir sözcük tüm olasılığı sıfır yapardı.
Alıştırma. Eğitim verisine iki istenmeyen ileti daha ekleyiniz ve son örneğin kararının nasıl değiştiğine bakınız. Hazır sürüm için kullan Dil ile gelen NaifBayes birimine göz atınız.
5. Karar ağaçları
Fikir. Veriyi evet/hayır sorularıyla tekrar tekrar ikiye (ya da daha fazla parçaya) böleriz. Her adımda, oluşan grupları en “saf” yapan soru seçilir. Saflığı Gini ölçüsü verir: tek sınıflı bir grupta 0, yarı yarıya karışık bir grupta 0,5.
program KararAğacı
{ Hava durumuna bakarak "tenis oynanır mı?" sorusunu yanıtlayan bir karar ağacı kurar.
Her düğümde, veriyi en "saf" gruplara ayıran öznitelik seçilir (Gini safsızlığı en düşük olan).
Gini = 1 − Σ p², p: gruptaki her sınıfın oranı. Tek sınıflı grupta Gini = 0. }
değişken
satırlar: metin dizisi dizisi { son sütun: sınıf (evet / hayır) }
öznitelikler: metin dizisi
tümü: tamsayı dizisi
i: tamsayı
fonksiyon Gini(sıra: tamsayı dizisi): ondalık
değişken
evet, j: tamsayı
p: ondalık
başla
evet := 0
sıra içindeki her j için eğer satırlar[j][4] = 'evet' ise evet := evet + 1
p := evet / Uzunluk(sıra)
sonuç := 1 - p * p - (1 - p) * (1 - p)
bitir
fonksiyon Değerler(sıra: tamsayı dizisi; sütun: tamsayı): metin dizisi
değişken j: tamsayı
başla
sonuç := []
sıra içindeki her j için
eğer değil (satırlar[j][sütun] içinde sonuç) ise sonuç.Ekle(satırlar[j][sütun])
bitir
fonksiyon Süzgeç(sıra: tamsayı dizisi; sütun: tamsayı; d: metin): tamsayı dizisi
başla
sonuç := Süz(sıra, fonksiyon(j: tamsayı) => satırlar[j][sütun] = d)
bitir
{ Ağırlıklı Gini: bölünmeden sonra çocukların Gini değerlerinin, büyüklükleriyle ağırlıklı ortalaması }
fonksiyon BölünmeGinisi(sıra: tamsayı dizisi; sütun: tamsayı): ondalık
değişken d: metin
başla
sonuç := 0
Değerler(sıra, sütun) içindeki her d için
sonuç := sonuç + Uzunluk(Süzgeç(sıra, sütun, d)) / Uzunluk(sıra) * Gini(Süzgeç(sıra, sütun, d))
bitir
prosedür Kur(sıra: tamsayı dizisi; kullanılmış: tamsayı dizisi; girinti: metin)
değişken
en, s: tamsayı
enIyi: ondalık
d: metin
başla
eğer Gini(sıra) = 0 veya Uzunluk(kullanılmış) = 4 ise
başla
yaz(girinti + '→ ' + satırlar[sıra[0]][4] + ' (' + Uzunluk(sıra) + ' örnek)', 1)
dön
bitir
en := -1; enIyi := 2
0'dan 3'e kadar her s için
eğer değil (s içinde kullanılmış) ve BölünmeGinisi(sıra, s) < enIyi ise
başla
enIyi := BölünmeGinisi(sıra, s); en := s
bitir
Değerler(sıra, en) içindeki her d için
başla
yaz(Biçimle('{}{} = {}? (Gini {:.3} → {:.3})', girinti, öznitelikler[en], d, Gini(sıra), enIyi), 1)
Kur(Süzgeç(sıra, en, d), kullanılmış + [en], girinti + ' ')
bitir
bitir
başla
öznitelikler := ['Hava', 'Sıcaklık', 'Nem', 'Rüzgâr']
satırlar := [
['güneşli', 'sıcak', 'yüksek', 'yok', 'hayır'], ['güneşli', 'sıcak', 'yüksek', 'var', 'hayır'],
['bulutlu', 'sıcak', 'yüksek', 'yok', 'evet'], ['yağmurlu', 'ılık', 'yüksek', 'yok', 'evet'],
['yağmurlu', 'serin', 'normal', 'yok', 'evet'], ['yağmurlu', 'serin', 'normal', 'var', 'hayır'],
['bulutlu', 'serin', 'normal', 'var', 'evet'], ['güneşli', 'ılık', 'yüksek', 'yok', 'hayır'],
['güneşli', 'serin', 'normal', 'yok', 'evet'], ['yağmurlu', 'ılık', 'normal', 'yok', 'evet'],
['güneşli', 'ılık', 'normal', 'var', 'evet'], ['bulutlu', 'ılık', 'yüksek', 'var', 'evet'],
['bulutlu', 'sıcak', 'normal', 'yok', 'evet'], ['yağmurlu', 'ılık', 'yüksek', 'var', 'hayır']]
tümü := Aralık(Uzunluk(satırlar))
yaz(Biçimle('Kökün Gini değeri: {:.3}', Gini(tümü)), 2)
0'dan 3'e kadar her i için
yaz(Biçimle('{:9} ile bölünürse ağırlıklı Gini: {:.3}', öznitelikler[i], BölünmeGinisi(tümü, i)), 1)
yaz('', 1)
Kur(tümü, [], '')
bitir.Programın çıktısı şöyledir:
Kökün Gini değeri: 0.459
Hava ile bölünürse ağırlıklı Gini: 0.343
Sıcaklık ile bölünürse ağırlıklı Gini: 0.440
Nem ile bölünürse ağırlıklı Gini: 0.367
Rüzgâr ile bölünürse ağırlıklı Gini: 0.429
Hava = güneşli? (Gini 0.459 → 0.343)
Nem = yüksek? (Gini 0.480 → 0.000)
→ hayır (3 örnek)
Nem = normal? (Gini 0.480 → 0.000)
→ evet (2 örnek)
Hava = bulutlu? (Gini 0.459 → 0.343)
→ evet (4 örnek)
Hava = yağmurlu? (Gini 0.459 → 0.343)
Rüzgâr = yok? (Gini 0.480 → 0.000)
→ evet (3 örnek)
Rüzgâr = var? (Gini 0.480 → 0.000)
→ hayır (2 örnek)Ne öğrendik. Ağaç, 14 günü yalnızca üç öznitelikle hatasız ayırdı ve Sıcaklığı hiç kullanmadı. Sonuç bir insanın okuyabileceği kurallardır: “güneşliyse neme bak, yağmurluysa rüzgâra bak”. Tehlikesi şudur: derin ağaçlar eğitim verisini ezberler (aşırı uyum). Bir sonraki bölümdeki orman ve artırma yöntemleri bu sorunu çözer.
Alıştırma. Kur prosedürüne bir derinlik sınırı ekleyiniz: derinlik 1’de durunca yapraktaki çoğunluk sınıfını yazdırınız. Bu tek katlı ağaca “karar kütüğü” denir ve AdaBoost’ta tekrar karşımıza çıkacak.