İçeriğe geç
academia.sh

Ders 12 / 23

Puan Ayarlama

Aynı sorgunun sırasını yeniden yazan iki ayar: alan ağırlığının ilk on sonuçta kaç belgeyi değiştirdiği ve bir belgeyi kaç basamak oynattığı, alan seçiminin ağırlıktan farklı olarak eşleşen kümeyi daraltması, işlev tabanlı puanda yenilik ile popülerliğin ilgililiği hangi ağırlıkta bastırdığı ve iki birleştirme kipinin farkı.

İçindekiler

Bir önceki ders ilgililik puanını bileşenlerine ayırdı: bir terimin belgede kaç kez geçtiği, kaç belgede geçtiği ve belgenin ne kadar uzun olduğu. Ayrıştırma yapıldı, ama bileşenlerin birbirine göre ağırlığı verili kabul edildi; her alan aynı sayıldı ve bu bir karar olarak hiç sorgulanmadı. Katalogda soru somuttur: “deniz gemi” araması yapan okur için bir sözcüğün kitabın adında geçmesiyle özetinde geçmesi aynı şey midir? Yazar adında geçmesi ne anlama gelir?

Bu ders iki ayarı ölçer. Alan ağırlığı, aynı terimin hangi alanda bulunduğuna göre puana giren katsayıdır. İşlev tabanlı puan, metinle ilgisi olmayan bir niceliğin — yayın yılı, ödünç sayısı — puana katılmasıdır. İkisi de “daha alakalı sonuç” gerekçesiyle savunulur; burada gerekçe sayıya çevrilir.

Derlem ve Dört Alan

Ölçümün tamamı tek bir derlem üzerinde yapılır: kütüphane katalogundan üretilmiş 1.200 kitap kaydı, tohum 20260801. Her kayıt dört alanda dizinlenir.

Kod Varsayım Değer Gerekçe
SI10 çözümleyici zinciri önceki konuda kuruldu belirteçler normalleştirilmiş gelir
SI11 dizinlenen alan ad, özet, konu, yazar her biri ayrı gönderi listesiyle
SI12 konu etiketi dörtte birinde metnin temasından farklı etiket yayınevi kararıdır
SI13 ödünç sayısı ve yıl belge alanı olarak dizinde durur işlev tabanlı puanın girdisi

SI12 bu dersin ayırt edici varsayımıdır: alanlar birbirinin kopyası olsaydı ağırlık hiçbir şeyi oynatmazdı.

// derlem.mjs — kutuphane katalogu: tohumlu derlem, ters dizin ve alan basina puanlayici.
export const N = 1200, TOHUM = 20260801, ALANLAR = ["ad", "ozet", "konu", "yazar"];
let d = TOHUM;                                            // 32 bit uretec, tasma yok
export const rast = () => { d = (d + 0x6D2B79F5) | 0; let t = Math.imul(d ^ (d >>> 15), 1 | d);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32; };
const sec = (a) => a[Math.floor(rast() * a.length)];
const TEMA = [["öykü|hikâye", "edebiyat", "kısa seçki derleme anlatı kurgu taşra"],
  ["masal", "çocuk edebiyatı", "çocuk resimli okul genç orman uyku"],
  ["tarih", "tarih", "osmanlı cumhuriyet arşiv belge kronik vakıf"],
  ["deniz", "gezi", "kıyı balıkçı liman gemi ada fener"],
  ["matematik", "bilim", "geometri sayı kanıt kuram çözüm olasılık"],
  ["şiir", "şiir", "dize toplu divan çeviri seçme aruz"]]
  .map(([k, konu, a]) => ({ k: k.split("|"), konu, ana: a.split(" ") }));
const ORTAK = "kitap cilt baskı yayın inceleme notlar giriş sözlük".split(" ");
const AD = "Ahmet Elif Selim Nuray Kemal Deniz Ayşe Ozan Meral Barış".split(" ");
const SOY = "Yıldız Aksu Demir Karaca Toprak Şen".split(" ");

export const belgeler = Array.from({ length: N }, (_, id) => {
  const t = sec(TEMA), cek = t.k[t.k.length > 1 && rast() < 0.5 ? 1 : 0], oz = [cek, cek];
  const ad = [...new Set([cek, sec(t.ana), sec(t.ana)])];
  t.ana.forEach((s, j) => { if (rast() < 1 / (1 + j * 0.42)) oz.push(s); });
  ORTAK.forEach((s, j) => { if (rast() < 0.55 / (1 + j * 0.28)) oz.push(s); });
  if (rast() < 0.5) oz.push(sec(sec(TEMA).ana));          // baska temadan sizan sozcuk
  for (let k = oz.length - 1; k > 0; k -= 1) { const j = Math.floor(rast() * (k + 1));
    [oz[k], oz[j]] = [oz[j], oz[k]]; }
  // konu etiketi yayinevi kararidir: belgelerin dortte birinde metinle ayni temada degil
  return { id, ad: ad.join(" "), ozet: oz.join(" "), konu: (rast() < 0.25 ? sec(TEMA) : t).konu,
    yazar: `${sec(AD)} ${sec(SOY)}`, yil: 1975 + Math.floor(rast() ** 0.6 * 50),
    odunc: Math.floor(rast() ** 3 * 400) };
});

export const belirtec = (s) => s.toLocaleLowerCase("tr").match(/[\p{L}\p{N}]+/gu) ?? [];
export function dizinKur(bs) {                            // alan basina ters dizin
  const dz = {};
  for (const a of ALANLAR) {
    const gonderi = new Map(), boy = new Float64Array(bs.length);
    for (const b of bs) {
      const ts = belirtec(b[a]), say = new Map();
      boy[b.id] = ts.length;
      for (const t of ts) say.set(t, (say.get(t) ?? 0) + 1);
      for (const [t, tf] of say) {
        if (!gonderi.has(t)) gonderi.set(t, []);
        gonderi.get(t).push({ id: b.id, tf });
      }
    }
    dz[a] = { gonderi, boy, ort: boy.reduce((x, y) => x + y, 0) / bs.length };
  }
  return dz;
}
export function alanPuan(dz, a, terimler) {               // tf, ters belge sikligi, uzunluk
  const k1 = 1.2, b = 0.75, p = new Map();
  for (const t of terimler) {
    const g = dz[a].gonderi.get(t);
    if (!g) continue;
    const idf = Math.log(1 + (N - g.length + 0.5) / (g.length + 0.5));
    for (const e of g) {
      const nrm = 1 - b + (b * dz[a].boy[e.id]) / dz[a].ort;
      p.set(e.id, (p.get(e.id) ?? 0) + (idf * e.tf * (k1 + 1)) / (e.tf + k1 * nrm));
    }
  }
  return p;
}
export function ara(dz, sorgu, agirlik) {                 // alan agirliklariyla toplam puan
  const terimler = belirtec(sorgu), toplam = new Map();
  for (const a of ALANLAR) {
    if (!agirlik[a]) continue;
    for (const [id, v] of alanPuan(dz, a, terimler))
      toplam.set(id, (toplam.get(id) ?? 0) + agirlik[a] * v);
  }
  return [...toplam].sort((x, y) => y[1] - x[1] || x[0] - y[0]);
}

Puanlayıcı alan başına ayrı çalışır: her alanın kendi gönderi listesi, kendi ortalama uzunluğu ve kendi ters belge sıklığı vardır; toplam puan alan puanlarının ağırlıklı toplamıdır. Ağırlık sıfırsa o alan hiç taranmaz.

Ağırlık Sırayı Değiştirir, Alan Seçimi Kümeyi

Altı yapılandırma aynı sorguyu koşar. Dördü yalnız ağırlığı değiştirir, ikisi taranan alan kümesini daraltır.

// puan.mjs — ayni sorgu, degisen alan agirliklari: kume, ilk on, basamak ve bedel.
import { belgeler, dizinKur, ara, alanPuan, belirtec, ALANLAR, N, TOHUM } from "./derlem.mjs";
const dz = dizinKur(belgeler), SORGU = "deniz gemi", terim = belirtec(SORGU);
const giris = (a) => terim.reduce((s, t) => s + (dz[a].gonderi.get(t)?.length ?? 0), 0);
const KUR = { "eşit": { ad: 1, ozet: 1, konu: 1, yazar: 1 },
  "ad 5 kat": { ad: 5, ozet: 1, konu: 1, yazar: 1 },
  "ad 12 kat": { ad: 12, ozet: 1, konu: 1, yazar: 1 },
  "yazar 5 kat": { ad: 1, ozet: 1, konu: 1, yazar: 5 },
  "yalnız ad": { ad: 1 }, "yalnız özet": { ozet: 1 } };

const temel = ara(dz, SORGU, KUR["eşit"]);
const sira = (r) => new Map(r.map(([id], i) => [id, i + 1])), tSira = sira(temel);
const tOn = new Set(temel.slice(0, 10).map(([id]) => id));
console.log(`derlem ${N} belge, tohum ${TOHUM}; sorgu "${SORGU}"`);
console.log("alan terim sayısı / ortalama uzunluk: " +
  ALANLAR.map((a) => `${a} ${dz[a].gonderi.size}/${dz[a].ort.toFixed(1)}`).join(", "));
console.log("dokunulan gönderi girişi: " + ALANLAR.map((a) => `${a} ${giris(a)}`).join(", ") + "\n");
console.log("yapılandırma".padEnd(14) + "eşleşen".padStart(9) + "giriş".padStart(7) +
  "ilk10 değişen".padStart(15) + "en büyük sıçrama".padStart(18) + "  ilk sıradaki");
for (const [ad, w] of Object.entries(KUR)) {
  const r = ara(dz, SORGU, w), y = sira(r);
  const degisen = r.slice(0, 10).filter(([id]) => !tOn.has(id)).length;
  let en = 0, kim = -1;
  for (const [id] of temel.slice(0, 50))
    if (y.has(id) && Math.abs(tSira.get(id) - y.get(id)) > en) {
      en = Math.abs(tSira.get(id) - y.get(id)); kim = id;
    }
  const b = belgeler[r[0][0]];
  console.log(ad.padEnd(14) + `${r.length}`.padStart(9) +
    `${Object.keys(w).reduce((s, a) => s + giris(a), 0)}`.padStart(7) +
    `${degisen}`.padStart(15) + (kim < 0 ? "temel" : `${en} (belge ${kim})`).padStart(18) +
    `  ${b.ad} / ${b.yazar}`);
}

const bilesen = (id) => ALANLAR
  .map((a) => `${a} ${(alanPuan(dz, a, terim).get(id) ?? 0).toFixed(2)}`).join(", ");
const yz = ara(dz, SORGU, KUR["yazar 5 kat"])[0][0];
console.log(`\nyazar 5 kat ile ilk sıraya gelen belge ${yz}: ${belgeler[yz].ad} / ` +
  `${belgeler[yz].yazar}\n  eşit ağırlıkta ${tSira.get(yz)}. sırada; bileşen: ${bilesen(yz)}`);
console.log(`eşit ağırlıkta ilk sıradaki belge ${temel[0][0]}: ${belgeler[temel[0][0]].ad} / ` +
  `${belgeler[temel[0][0]].yazar}\n  bileşen: ${bilesen(temel[0][0])}`);
derlem 1200 belge, tohum 20260801; sorgu "deniz gemi"
alan terim sayısı / ortalama uzunluk: ad 43/2.9, ozet 51/8.4, konu 7/1.1, yazar 16/2.0
dokunulan gönderi girişi: ad 265, ozet 302, konu 0, yazar 131

yapılandırma    eşleşen  giriş  ilk10 değişen  en büyük sıçrama  ilk sıradaki
eşit                325    698              0             temel  deniz kıyı gemi / Ozan Demir
ad 5 kat            325    698              4    44 (belge 365)  deniz gemi / Barış Toprak
ad 12 kat           325    698              4    44 (belge 365)  deniz gemi / Barış Toprak
yazar 5 kat         325    698             10   131 (belge 143)  deniz gemi kıyı / Deniz Aksu
yalnız ad           199    265              9  144 (belge 1122)  deniz gemi / Selim Demir
yalnız özet         211    302              8   159 (belge 852)  deniz fener balıkçı / Ozan Toprak

yazar 5 kat ile ilk sıraya gelen belge 622: deniz gemi kıyı / Deniz Aksu
  eşit ağırlıkta 17. sırada; bileşen: ad 4.59, ozet 2.69, konu 0.00, yazar 2.21
eşit ağırlıkta ilk sıradaki belge 143: deniz kıyı gemi / Ozan Demir
  bileşen: ad 4.59, ozet 5.46, konu 0.00, yazar 0.00

Belge içerikleri tohuma bağlıdır; dokunulan gönderi girişi sayısı değildir, o sayı doğrudan gönderi listelerinin uzunluğudur.

İlk sütun dört satırda aynı: 325. Ağırlık değiştirmek eşleşen kümeye dokunmaz, çünkü ağırlık puana giren bir çarpandır, eşleşme koşulu değildir. Bir belge sorgu terimlerinden en az birini taşıyorsa kümededir; ağırlık yalnız nerede duracağını belirler. Son iki satırda küme 199 ve 211’e iniyor, çünkü orada değişen şey ağırlık değil hangi alanların tarandığıdır. Yalnız ad alanında arandığında adında hiçbir sorgu terimi geçmeyen 126 belge kümeden çıkar. Ağırlık ile alan seçimi aynı ayar gibi konuşulur, oysa biri sırayı, öteki kümeyi değiştirir.

Sıra tarafında ilk sürpriz ad 5 kat ile ad 12 kat satırlarının aynı olmasıdır: ilk onda dört belge değişiyor, en büyük sıçrama kırk dört basamak, katsayıyı iki buçuk kat daha artırmak hiçbir şey eklemiyor. Ağırlık büyüdükçe sıralama ad alanının kendi iç sırasına yakınsar ve orada durur; “biraz daha yukarı çekelim” cümlesinin karşılığı bir noktadan sonra boştur.

İkinci sürpriz yazar 5 kat satırındadır: ilk onun onu birden değişiyor. Nedeni çıktının alt bölümünde görünüyor. Yazar alanında “Deniz” bir addır; 622 numaralı belge eşit ağırlıkta on yedinci sıradayken yazar bileşeninin beşle çarpılması onu ilk sıraya taşıyor, eşit ağırlıkta birinci olan 143 numaralı belgenin yazar bileşeni sıfır olduğu için o belge 132. sıraya düşüyor. Tek bir katsayı, yüz otuz bir basamak. Alan ağırlığı burada ilgililiği artırmıyor, alan karışmasını büyütüyor.

Konu alanı satır boyunca sessiz: dokunulan gönderi girişi sıfır, çünkü “deniz” ve “gemi” hiçbir konu etiketinde geçmiyor. Konu ağırlığını yüze çıkarmak bu sorguda hiçbir şeyi değiştirmez; bir ağırlığın etkisi o alanda sorgu teriminin bulunmasına bağlıdır.

İşlev Tabanlı Puan ve Bastırma Noktası

İkinci ayar metnin dışından gelir. Kütüphane “yeni ve çok ödünç alınan kitaplar yukarıda çıksın” ister. Bu istek puana iki biçimde katılabilir: metin puanıyla çarpılarak ya da metin puanına eklenerek. İkisi aynı isteğin gerçekleştirimidir ve sonuçları aynı değildir.

// islev.mjs — islev tabanli puan: iki birlestirme kipi ve ilgililigin bastirilma noktasi.
import { belgeler, dizinKur, ara } from "./derlem.mjs";
const dz = dizinKur(belgeler), SORGU = "deniz gemi";
const metin = ara(dz, SORGU, { ad: 1, ozet: 1, konu: 1, yazar: 1 }), PM = metin[0][1];
const mSira = new Map(metin.map(([id], i) => [id, i + 1]));
const mOn = new Set(metin.slice(0, 10).map(([id]) => id));
const kat = (b) => 0.6 * (Math.log1p(b.odunc) / Math.log1p(400)) + 0.4 * ((b.yil - 1975) / 49);
const on = (f) => metin.map(([id, p]) => [id, f(p, belgeler[id])])
  .sort((x, y) => y[1] - x[1] || x[0] - y[0]).slice(0, 10);
const say = (r) => `${r.filter(([id]) => !mOn.has(id)).length}/` +
  `${r.filter(([id]) => mSira.get(id) > 50).length}`;
const ort = (r, f) => (r.reduce((s, [id]) => s + f(belgeler[id]), 0) / r.length).toFixed(0);

console.log(`sorgu "${SORGU}", ${metin.length} eşleşen belge; küme her ağırlıkta aynı`);
console.log(`en yüksek metin puanı ${PM.toFixed(2)}; ilk on ödünç ortalaması ` +
  `${ort(metin.slice(0, 10), (b) => b.odunc)}, yıl ortalaması ` +
  `${ort(metin.slice(0, 10), (b) => b.yil)}\n`);
console.log("w".padEnd(6) + "çarpımsal".padStart(11) + "toplamsal".padStart(11) +
  "ort ödünç".padStart(11) + "ort yıl".padStart(9) + "  toplamsal kipte ilk sıradaki");
for (const w of [0.25, 0.5, 1, 2, 4, 8]) {
  const c = on((p, b) => p * (1 + w * kat(b))), t = on((p, b) => p + w * PM * kat(b));
  const b = belgeler[t[0][0]];
  console.log(`${w}`.padEnd(6) + say(c).padStart(11) + say(t).padStart(11) +
    ort(t, (x) => x.odunc).padStart(11) + ort(t, (x) => x.yil).padStart(9) +
    `  ${b.ad} (${b.yil}, ${b.odunc} ödünç, metinde ${mSira.get(b.id)}.)`);
}
sorgu "deniz gemi", 325 eşleşen belge; küme her ağırlıkta aynı
en yüksek metin puanı 10.05; ilk on ödünç ortalaması 100, yıl ortalaması 2006

w       çarpımsal  toplamsal  ort ödünç  ort yıl  toplamsal kipte ilk sıradaki
0.25          4/0        5/0        185     2013  deniz kıyı gemi (2008, 235 ödünç, metinde 1.)
0.5           5/0        6/0        191     2013  deniz gemi balıkçı (2013, 398 ödünç, metinde 14.)
1             6/0        6/0        191     2013  deniz gemi balıkçı (2013, 398 ödünç, metinde 14.)
2             6/0        7/2        198     2017  deniz gemi balıkçı (2013, 398 ödünç, metinde 14.)
4             6/0        8/5        257     2018  deniz gemi balıkçı (2013, 398 ödünç, metinde 14.)
8             6/0       10/8        324     2021  deniz kıyı balıkçı (2024, 301 ödünç, metinde 123.)

Sütunlardaki iki sayı şöyle okunur: ilk on sonuçtan kaçı metin sıralamasının ilk onunda değildi, bunların kaçı metin sıralamasında ellinci sıradan sonra geliyordu.

Çarpımsal kip w bir olduktan sonra donuyor: dört, beş, altı ve orada kalıyor; ikinci sayı hiçbir ağırlıkta sıfırdan çıkmıyor. Nedeni çarpanın yapısıdır: metin puanı çarpan olarak kaldığı için sıralama, metin puanı ile katsayının çarpımına yakınsar ve metinde çok zayıf bir belge bu çarpımı kazanamaz. Çarpımsal kip ilk onu yeniden düzenler, doldurmaz.

Toplamsal kipte katkı, metin puanından bağımsız bir büyüklük olarak eklenir ve w büyüdükçe metin puanı toplamın içinde erir. w dörtte ilk onun beşi metin sıralamasının elli sonrasından geliyor; sayılabilir bir ölçüt gerekirse bastırma noktası buradadır: ilk onun yarısı artık sorguyla değil, ödünç sayısıyla seçilmiştir. w sekizde ilk on tamamen değişiyor, ilk sıradaki belge metin sıralamasında yüz yirmi üçüncü, ilk onun ödünç ortalaması 100’den 324’e ve yıl ortalaması 2006’dan 2021’e çıkıyor. Liste hâlâ “deniz gemi” sorgusunun yanıtıdır ve her belgesi sorgu terimlerinden birini taşır; ama sıra artık kütüphanenin en çok ödünç verilen yeni kitaplarının sırasıdır.

Küme bütün bu ağırlıklarda 325’te sabit: işlev tabanlı puan hiçbir belgeyi eklemez ya da çıkarmaz. Kullanıcının gördüğü şeyin ilk on sonuç olduğu düşünülürse ayrım incelir — küme değişmese de görülen küme tamamen değişmiştir.

Özet

  • Alan ağırlığı eşleşen kümeye dokunmaz: dört ağırlık yapılandırmasında da 325 belge döndü. Kümeyi değiştiren şey ağırlık değil alan seçimidir; yalnız ad alanında arandığında küme 199’a indi.
  • Ağırlığın etkisi doyar: ad ağırlığını 5 kattan 12 kata çıkarmak ilk ondaki değişimi dört belgede ve en büyük sıçramayı kırk dört basamakta bıraktı. Ağırlık tarama bedelini de değiştirmez; 698 gönderi girişini 265’e indiren tek karar bir alanı kapatmaktır.
  • Yanlış alana verilen ağırlık ilgililiği değil alan karışmasını büyütür: yazar ağırlığı beşe çıkınca ilk onun onu birden değişti, eşit ağırlıkta birinci olan belge 132. sıraya düştü. Konu alanı ise bu sorguda hiç iş görmez, çünkü dokunulan gönderi girişi sıfırdır.
  • İşlev tabanlı puanda çarpımsal kip ilk onu yeniden düzenlemekle kalır, toplamsal kip w dörtte ilk onun yarısını metin sıralamasının elli sonrasından doldurur; w sekizde ilk sıradaki belge metinde yüz yirmi üçüncüdür.

Sonraki Adım

Buraya kadar sorulan her soru bir belge listesi istedi: hangi kitaplar, hangi sırayla. Ödünç masasının sorduğu şey çoğu zaman belge değil sayıdır: bu aramanın sonuçlarından kaçı 2015 sonrası basılmış, hangi konu başlığında kaç kitap düşüyor, eşleşen kümenin ortalama yayın yılı nedir. Bu sorular aynı ters dizin üzerinde çalışır ama çıktıları sıralanmış bir liste değildir, üstelik yanıtın eşleşen küme üzerinde mi bütün derlem üzerinde mi hesaplandığı sonucu tümüyle değiştirir. Sonraki ders belge yerine sayı üreten sorguları ve bunların bellek maliyetini ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat