İçeriğe geç

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.

ŷ=ax+b,OKH=1n∑i(ŷi−yi)2\hat{y} = a x + b, \qquad \text{OKH} = \frac{1}{n}\sum_i (\hat{y}_i - y_i)^2

Yöntem
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:

Çıktı
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 TL

Ne öğ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.

P(y=1∣x)=σ(wx+b)=11+e−(wx+b)P(y=1 \mid x) = \sigma(w x + b) = \frac{1}{1 + e^{-(w x + b)}}

Yöntem
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:

Çıktı
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.92

Ne öğ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.

d(p,q)=∑j(pj−qj)2d(p, q) = \sqrt{\sum_j (p_j - q_j)^2}

Yöntem
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:

Çıktı
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: elma

Ne öğ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.

P(c∣w1…wn)∝P(c)∏iP(wi∣c)P(c \mid w_1 \ldots w_n) \propto P(c) \prod_i P(w_i \mid c)

Yöntem
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:

Çıktı
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.

Gini=1−∑kpk2\text{Gini} = 1 - \sum_k p_k^2

Yöntem
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:

Çıktı
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.