İçeriğe geç

Kitaplar / Yöntem ile Sıfırdan Dil Modeli / Bölüm 3 — Sayarak dil modeli

Bölüm 3 — Sayarak dil modeli

İlk dil modelimiz hiç eğitilmeyecek; yalnızca sayacak. Fikir şudur: bir sonraki karakter yalnızca bir önceki karaktere bağlıymış gibi davranalım. “ş” harfinden sonra neler geldiğini veri kümesinde sayarsak, “ş”den sonra ne geleceğinin olasılıklarını da bulmuş oluruz. İki karakterlik bu pencereye ikili (bigram) model denir.

P(sonraki=b∣şimdiki=a)=say(a,b)∑xsay(a,x)P(\text{sonraki} = b \mid \text{şimdiki} = a) = \frac{\text{say}(a, b)}{\sum_x \text{say}(a, x)}

Her atasözünü noktayla başlatıp noktayla bitiriyoruz. Böylece “nokta → d” çifti “atasözleri d harfiyle ne sıklıkla başlar?” sorusunu, “r → nokta” çifti de “r ile biten bir sözcükten sonra atasözü ne sıklıkla biter?” sorusunu yanıtlar.

Yöntem
program SayarakDilModeli
{ İkili (bigram) dil modeli: bir sonraki karakterin olasılığı yalnızca bir önceki
  karaktere bağlıdır. Model eğitilmez, sayılır:
     P(sonraki = b | şimdiki = a) = say(a, b) / say(a, herhangi)
  Her atasözünün başına ve sonuna '.' koyarız; böylece model nerede başlayıp
  nerede bitireceğini de öğrenir. Sonra modelden yeni "atasözleri" örnekleriz
  ve modelin veriye ne kadar şaşırdığını ölçeriz. }
kullan Atasozleri, Matematik

değişken
  veri, harfler: metin dizisi
  numara: sözlük
  say: tamsayı dizisi dizisi
  P: ondalık dizisi dizisi
  a: metin
  i, k, n, öncekiNo, sonrakiNo: tamsayı
  satırToplamı: tamsayı
  logToplam: ondalık
  sözcük, kar: metin
  sıra: tamsayı dizisi

fonksiyon Örnekle(olasılıklar: ondalık dizisi): tamsayı
{ Olasılık dağılımından bir numara çeker: [0, 1) arasında bir sayı tutup
  olasılıkları sırayla toplar, toplam o sayıyı geçtiği yerde durur. }
değişken r, birikim: ondalık; i: tamsayı
başla
  r := Rastgele(1000000) / 1000000
  birikim := 0
  0'dan Uzunluk(olasılıklar) - 1'e kadar her i için
  başla
    birikim := birikim + olasılıklar[i]
    eğer r < birikim ise dön i
  bitir
  dön Uzunluk(olasılıklar) - 1
bitir

başla
  Tohum(42)
  veri := Atasozleri.Hepsi
  harfler := ['.'] + Sırala(Benzersiz(Birleştir(veri, '').Karakterler))
  n := Uzunluk(harfler)
  numara := [:]
  0'dan n - 1'e kadar her i için numara[harfler[i]] := i

  { 1) Say: say[i][j] = i. karakterden sonra j. karakterin kaç kez geldiği }
  say := Eşle(Aralık(n), fonksiyon(x: tamsayı) => Eşle(Aralık(n), fonksiyon(z: tamsayı) => 0))
  veri içindeki her a için
  başla
    öncekiNo := 0
    (a + '.').Karakterler içindeki her kar için
    başla
      sonrakiNo := numara[kar]
      say[öncekiNo][sonrakiNo] := say[öncekiNo][sonrakiNo] + 1
      öncekiNo := sonrakiNo
    bitir
  bitir

  { 2) Olasılığa çevir. Her sayıma 1 eklemek (düzleştirme) hiç görülmemiş
       bir çiftin olasılığının sıfır olmasını engeller. }
  P := []
  0'dan n - 1'e kadar her i için
  başla
    satırToplamı := Toplam(say[i]) + n
    P.Ekle(Eşle(say[i], fonksiyon(c: tamsayı) => (c + 1) / satırToplamı))
  bitir

  yaz('Atasözleri en sık şu harflerle başlar:', 1)
  sıra := Sırala(Aralık(n), fonksiyon(x: tamsayı) => -P[0][x])
  0'dan 4'e kadar her k için
    yaz(Biçimle('  "{}"  olasılık {:.3}', harfler[sıra[k]], P[0][sıra[k]]), 1)
  yaz('"ş" harfinden sonra en sık gelenler:', 1)
  i := numara['ş']
  sıra := Sırala(Aralık(n), fonksiyon(x: tamsayı) => -P[i][x])
  0'dan 4'e kadar her k için
    yaz(Biçimle('  "{}"  olasılık {:.3}', harfler[sıra[k]], P[i][sıra[k]]), 1)

  { 3) Örnekle: '.' ile başla, bir sonraki karakteri çek, '.' gelene kadar sürdür }
  yaz('', 1); yaz('Modelin ürettiği "atasözleri":', 1)
  1'den 8'e kadar her k için
  başla
    sözcük := ''; i := 0
    tekrarla
      i := Örnekle(P[i])
      eğer i <> 0 ise sözcük := sözcük + harfler[i]
    i = 0 olana dek
    yaz('  ' + sözcük, 1)
  bitir

  { 4) Değerlendirme: ortalama negatif log-olabilirlik (kayıp) ve şaşkınlık }
  logToplam := 0; k := 0
  veri içindeki her a için
  başla
    öncekiNo := 0
    (a + '.').Karakterler içindeki her kar için
    başla
      sonrakiNo := numara[kar]
      logToplam := logToplam - Ln(P[öncekiNo][sonrakiNo])
      k := k + 1
      öncekiNo := sonrakiNo
    bitir
  bitir
  yaz('', 1)
  yaz(Biçimle('Ortalama kayıp (karakter başına): {:.4}', logToplam / k), 1)
  yaz(Biçimle('Şaşkınlık (perplexity): {:.2}   (rastgele tahmin: {})', Üstel(logToplam / k), n), 1)
bitir.

Programın çıktısı şöyledir:

Çıktı
Atasözleri en sık şu harflerle başlar:
  "a"  olasılık 0.106
  "b"  olasılık 0.091
  "d"  olasılık 0.087
  "k"  olasılık 0.077
  "s"  olasılık 0.062
"ş" harfinden sonra en sık gelenler:
  " "  olasılık 0.164
  "e"  olasılık 0.119
  "ı"  olasılık 0.119
  "a"  olasılık 0.090
  "i"  olasılık 0.067

Modelin ürettiği "atasözleri":
  p ayekacu suşl ol a dene mavminaker yı
  uz
  siladesi bi hz olz
  tin çi mazhoğıllat asese ydückerun hectatıri or
  ktademaşmin shzuz
  dü yema dışlır domağıraccaktlkayolır hacin vağadokuter
  giraparyı
  suboğrmayolale k

Ortalama kayıp (karakter başına): 2.4175
Şaşkınlık (perplexity): 11.22   (rastgele tahmin: 30)

Çıktıyı okumak

Sayılar veri hakkında gerçek şeyler söylüyor. Atasözlerinin %10’dan fazlası “a” ile başlıyor (“acele”, “ağaç”, “akıl”, “armut”). “ş” harfinden sonra en sık boşluk geliyor, çünkü “taş”, “iş”, “ateş” gibi sözcükler ş ile biter.

Üretilen “atasözleri” ise neredeyse anlamsız. Yine de bir şeyler öğrenilmiş: sözcük uzunlukları makul, ünlüler ile ünsüzler sırayla geliyor, “ol”, “bi”, “ya” gibi Türkçe heceler görünüyor. Model yalnızca bir önceki harfi bildiği için bir sözcüğün ortasında olduğunu, bir cümlenin neresinde bulunduğunu hatırlayamıyor.

Kayıp ve şaşkınlık

Bir dil modelinin ne kadar iyi olduğunu tek sayıyla ölçebiliriz. Verideki her karakter için modelin o karaktere verdiği olasılığın eksi logaritmasını alıp ortalarız. Bu, 1. bölümde tanıdığımız kayıptır.

kayıp=−1N∑i=1NlnP(xi∣xi−1),şaşkınlık=ekayıp\text{kayıp} = -\frac{1}{N}\sum_{i=1}^{N} \ln P(x_i \mid x_{i-1}), \qquad \text{şaşkınlık} = e^{\text{kayıp}}

Kayıp, sezgisel bir sözcükle de anlatılabilir: şaşkınlık (perplexity). Şaşkınlığı 11,2 olan bir model, her adımda 11 eşit olasılıklı seçenek arasından seçim yapıyormuş gibi kararsızdır. Hiçbir şey bilmeyen bir model 30 karakterin hepsine eşit olasılık verirdi; şaşkınlığı 30, kaybı ln 30 ≈ 3,40 olurdu. Bu sayıyı akılda tutmakta yarar vardır: eğitilmemiş her modelimiz bu civarda başlayacaktır.

Program, sayımlara 1 ekleyerek düzleştirme yapıyor. Bu olmasaydı veride hiç görülmemiş bir çiftin olasılığı 0 olurdu ve log 0 tanımsız olduğu için kayıp sonsuza giderdi. Düzleştirmenin küçük bir bedeli vardır: düzleştirmesiz sayımın kaybı 2,341 iken düzleştirilmiş modelinki 2,418’dir.

Alıştırma. Programı üçlü (trigram) modele çeviriniz: olasılığı son iki karaktere göre hesaplayınız. Tablonuz kaç satırlık olur? Kayıp nasıl değişiyor ve üretilen metin daha Türkçe görünüyor mu?