Mundarija (24)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Nega retrievalni alohida baholash kerak
- 2.2. Metrikalar: recall@k, precision@k, MRR, nDCG
- 2.3. Baholash to'plami: savol → to'g'ri chunk
- 2.4. BM25 noldan
- 2.5. Dense qidiruv: bilim qayerdan keladi
- 2.6. Gibrid qidiruv: RRF va vaznli birlashtirish
- 2.7. Reranking: ikki bosqichli qidiruv
- 2.8. k ni tanlash: kontekst, shovqin va xarajat
- 2.9. Answer-in-context va xatolar tahlili
- 2.10. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Metrikalar noldan va baholash to'plami
- Misol 2 — BM25, dense va gibrid: savol turlari bo'yicha
- Misol 3 — Reranking: ikki bosqichli qidiruv
- Misol 4 — k ni tanlash, answer-in-context va xatolar tahlili
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
25.9-dars: RAG — retrieval sifati va baholash
25-QISM — KATTA TIL MODELLARI · 9-dars
1. Kirish va motivatsiya
25.8-darsda RAG ning birinchi yarmini qurdik: hujjatlarni chunklarga bo'ldik, indeks yaratdik va savolga eng yaqin bo'laklarni qaytardik. Endi eng muhim savol: bu qidiruv qanchalik yaxshi ishlaydi? RAG tizimidagi javob sifati ikki qismga bo'linadi — to'g'ri chunk topildimi (retrieval) va model uni to'g'ri o'qidimi (generatsiya). Agar kerakli chunk kontekstga tushmagan bo'lsa, eng kuchli LLM ham to'g'ri javob bera olmaydi: u yo "bilmayman" deydi, yo — yomonrog'i — ishonch bilan to'qib chiqaradi.
Muammo shundaki, RAG ni ko'pincha "ko'z bilan" baholashadi: o'nta savol berib, javoblar "yaxshiga o'xshaydi" deyishadi. Bu yondashuv ikki narsani yashiradi. Birinchisi — savol turlari: foydalanuvchilar hujjatdagi rasmiy atamalar bilan emas, o'z so'zlari bilan so'raydi ("ta'til" o'rniga "dam olish", "xizmat safari" o'rniga "komandirovka"), ba'zan esa aksincha — aniq kod yoki raqam bilan ("AK-7564 bo'yicha qoida nima?"). Bir usul bir turda a'lo, boshqasida ojiz bo'lishi mumkin, umumiy o'rtacha esa buni ko'rsatmaydi. Ikkinchisi — xatoning manbai: javob noto'g'ri bo'lsa, aybdor qidiruvmi yoki o'quvchi model?
Real vaziyat. Kompaniya ichki qoidalar bo'yicha RAG yordamchisini ishga tushirdi. Ishlab chiquvchilar 20 ta savolda sinab ko'rishdi — hammasi joyida edi, chunki savollarni hujjatlarni o'qib turib, o'sha so'zlar bilan yozishgan. Bir haftadan keyin xodimlar shikoyat qila boshladi: "Moliya bo'limida ishchi dam olishga necha kunga chiqadi?" degan savolga yordamchi masofaviy ish haqidagi qoidani keltirdi. Keyinroq embedding asosidagi qidiruvga o'tishdi — endi so'zlashuv tilidagi savollar yaxshilandi, lekin hujjat kodlari bilan qidirish buzildi. Hech kim qidiruvni savol turlari bo'yicha, bir xil baholash to'plamida o'lchamagan edi.
Bu darsda retrieval metrikalarini noldan yozamiz, savol turlari belgilangan baholash to'plamini yaratamiz, BM25, dense (LSA) va gibrid qidiruvni juftlashgan tarzda taqqoslaymiz, ikki bosqichli reranking quramiz va kontekstdagi chunklar soni k ni sifat, shovqin va xarajat nuqtai nazaridan tanlaymiz.
Bu darsda:
- Nega retrievalni alohida baholash kerak
- Metrikalar: recall@k, precision@k, MRR, nDCG
- Baholash to'plami: savol → to'g'ri chunk
- BM25 noldan va dense qidiruv (LSA)
- Gibrid qidiruv: RRF va vaznli birlashtirish
- Reranking: ikki bosqichli qidiruv
- k ni tanlash: kontekst, shovqin va xarajat
- Answer-in-context va xatolar tahlili
- Tuzoqlar
ℹ Misollar real numpy/sklearn bilan (Python 3.14). Korpus — kod ichida generatsiya qilingan kompaniya ichki qoidalari (12 mavzu x 8 bo'lim = 96 chunk). LLM chaqirilmaydi: retrieval metrikalari LLM siz hisoblanadi, "o'quvchi" o'rnida esa oddiy ekstraktiv qoida ishlatiladi.
2. Nazariya — chuqur tushuntirish
2.1. Nega retrievalni alohida baholash kerak
RAG ZANJIRI:
savol -> [RETRIEVAL: top-k chunk] -> [LLM: kontekst + savol -> javob]
JAVOB NOTO'G'RI BO'LSA, IKKI XIL SABAB:
1. kerakli chunk top-k ga tushmagan - retrieval xatosi
2. chunk bor, lekin model uni ishlatmagan - generatsiya xatosi
birinchisini LLM tuzata olmaydi: kontekstda yo'q narsani faqat to'qiydi
NEGA ALOHIDA:
retrieval metrikalari arzon va deterministik - LLM chaqiruvi kerak emas
har o'zgarishni (chunk hajmi, embedding, k, reranker) sekundlarda o'lchash mumkin
yakuniy javob sifati (LLM ni baholash darsi) - qimmatroq va shovqinliroq
TARTIB:
avval retrieval: "to'g'ri chunk top-k da bormi?" (recall@k)
keyin generatsiya: "chunk bo'lsa, javob to'g'rimi?"RAG ni ikki qavatda baholang: retrieval (kerakli chunk topildimi) va generatsiya (topilgan chunkdan to'g'ri javob chiqdimi) — ular turli usullar bilan tuzatiladi.
2.2. Metrikalar: recall@k, precision@k, MRR, nDCG
BELGILAR: tartib - tizim qaytargan chunklar ro'yxati; rel - relevant chunklar to'plami
RECALL@k = |top-k ∩ rel| / |rel| - relevantlarning qanchasi top-k da
PRECISION@k = |top-k ∩ rel| / k - top-k ning qancha qismi relevant
RR = 1 / (birinchi relevant o'rni) ; MRR - savollar bo'yicha o'rtacha
DCG@k = sum (2^rel_i - 1) / log2(i + 1), i = 1..k
nDCG@k = DCG@k / IDCG@k - IDCG: ideal tartibdagi DCG
1-MISOL, QO'LDA (tartib d3 d7 d1 d9 d4; relevant d1 (daraja 2), d4 (daraja 1)):
recall@3 = 1/2 = 0.50, precision@3 = 1/3 = 0.33, RR = 1/3
DCG@5 = 3/log2(4) + 1/log2(6) = 1.887
IDCG@5 = 3/log2(2) + 1/log2(3) = 3.631 -> nDCG@5 = 0.520
d1 1-o'ringa chiqsa: RR 1.000, nDCG 0.933, recall@5 o'zgarmaydi 1.00-bob
QAYSI BIRI QACHON:
recall@k - RAG uchun asosiy: LLM ga k ta chunk beriladi, tartib ichida kamroq muhim
MRR - bitta to'g'ri javob bo'lsa, "qanchalik tepada" ni o'lchaydi
nDCG - bir necha darajali relevantlik (aynan shu / qisman foydali)
precision@k - bitta relevant bo'lsa recall@k / k ga teng - kam ma'lumot beradiRAG da asosiy metrika — recall@k (kerakli chunk kontekstga tushdimi); MRR va nDCG tartib sifatini qo'shimcha ko'rsatadi.
2.3. Baholash to'plami: savol → to'g'ri chunk
TARKIB: (savol, oltin chunk id(lar)i, savol turi, [javob satri])
QAYERDAN OLINADI:
foydalanuvchi loglari - eng haqiqiy; belgilash qo'lda
mutaxassis yozgan savollar - sifatli, lekin qimmat
sintetik: chunkdan savol - LLM "shu chunkka savol yoz" (arzon, lekin
hujjat so'zlarini takrorlashga moyil -> juda oson)
ATAYLAB: parafraz, kod/raqam, bir necha chunk talab qiladigan savollar
TURLAR BO'YICHA BELGILASH (bizning to'plam, 288 savol):
parafraz - so'zlashuv atamalari: "Moliya bo'limida ishchi dam olishga necha kunga chiqadi?"
kod - aniq identifikator: "AK-7564 bo'yicha qoida nima?"
raqam - aniq qiymat bilan: "Qaysi bo'limda yiliga 19 kunlik ta'til beriladi?"
TEKSHIRUVLAR (1-misol):
javob satri oltin chunkda bormi (100%), takror savollar (0), turlar muvozanati
QOIDALAR:
dev (sozlash) va test (yakuniy) ajratilsin
to'plam versiyalansin: o'zgarganda eski natijalar bilan solishtirib bo'lmaydi
(LLM ni baholash darsida batafsil) Baholash to'plami — savol, oltin chunk va savol turi; turlarsiz o'rtacha metrika zaif joyni yashiradi (1-misolda BM25: umumiy MRR 0.736, parafrazda — 0.209).
2.4. BM25 noldan
BM25(q, d) = sum_{w in q} IDF(w) * tf(w, d) * (k1 + 1) / (tf(w, d) + k1 * (1 - b + b * |d| / avgdl))
IDF(w) = log(1 + (N - df(w) + 0.5) / (df(w) + 0.5))
tf - so'z hujjatda necha marta; to'yinadi (k1 = 1.5): 10 marta != 10 barobar
|d|/avgdl - uzun hujjat jazolanadi (b = 0.75)
IDF - kam uchraydigan so'z (kod, raqam) katta vazn oladi
KUCHLI TOMONLARI:
aniq atama, kod, raqam, nom - "AK-7564" bitta hujjatda -> IDF 4.2 atrofida
o'rgatish kerak emas, tushuntirish oson, tez
ZAIF TOMONI:
sinonim va parafrazni ko'rmaydi: "dam olish" != "ta'til"
1-misol: parafrazda recall@1 0.083, kod va raqamda 1.000
O'ZBEK TILI:
qo'shimchalar: "bo'limi", "bo'limida", "bo'limda" - turli token
eng oddiy yechim - o'zak kesish (bizda: so'zning birinchi 6 harfi)
yaxshiroq - morfologik tahlil yoki subword (23.3-dars)BM25 — leksik qidiruv: aniq so'z, kod va raqamda juda kuchli, sinonim va parafrazda ojiz.
2.5. Dense qidiruv: bilim qayerdan keladi
DENSE (25.7-dars): savol va chunk -> vektor; o'xshashlik = kosinus
bizda embedding o'rnida LSA: TF-IDF + TruncatedSVD(12)
SINONIMNI QAYERDAN BILADI:
faqat o'zi ko'rgan matnlardan - "ta'til" va "dam olish" bir xil kontekstda uchrasa
2-misol, cos("dam olish", "ta'til"):
faqat 96 chunkda o'rgatilgan LSA 0.00 - so'z lug'atda ham yo'q
+ 360 belgisiz fon matn bilan 1.00
fon = haqiqiy embedding modelning pretraining korpusi o'rnida
ZAIF TOMONI:
kod va raqam: "AK-7564" bitta hujjatda - 12 o'lchovga siqilganda yo'qoladi
2-misol, LSA (fon bilan): kodda recall@1 0.28, parafrazda 0.22, MRR 0.404
HAQIQIY TIZIMDA:
tayyor embedding modeli 25.7-bob - katta korpusda o'rgatilgan, sinonimni biladi
lekin domen atamalari, ichki kodlar, yangi mahsulot nomlari - baribir zaifDense qidiruv semantik o'xshashlikni ko'radi, lekin bu bilim faqat o'rgatish korpusidan keladi; aniq kod va raqamlarni u yomon ajratadi.
2.6. Gibrid qidiruv: RRF va vaznli birlashtirish
RRF (Reciprocal Rank Fusion):
ball(d) = sum_{retriever r} 1 / (60 + o'rin_r(d))
faqat O'RINLAR ishlatiladi - ballarning shkalasi muhim emas (BM25 0..20, kosinus -1..1)
har retrieverdan top-n nomzod (bizda 30); ball = 0 bo'lgan hujjat hissa qo'shmaydi
VAZNLI BIRLASHTIRISH:
ball(d) = alfa * minmax(BM25) + (1 - alfa) * minmax(kosinus)
ballar kattaligini saqlaydi: BM25 "aniq moslik" da qanchalik ishonchli ekanini
2-MISOL NATIJASI (MRR):
usul parafraz kod raqam
BM25 0.209 1.000 1.000
LSA (fon) 0.404 0.478 0.491
RRF 0.771 1.000 0.741
vaznli 0.5 0.910 1.000 1.000
RRF - BM25: parafraz +0.562 (SE 0.037), raqam -0.259 (SE 0.031) - ikkalasi sezilarli
nega RRF raqamda yutqazdi: BM25 oltin chunkni 1-o'ringa qo'yadi, lekin boshqa 7 bo'lim
hujjati ham yaqin o'rinlarda; LSA raqamni ko'rmaydi va ulardan birini 1-o'ringa
qo'yadi -> o'rinlar yig'indisida oltin chunk yutqazadi. Vaznli usulda BM25 ning
katta ball farqi saqlanadi
TANLASH:
alfa va RRF doimiysi - dev savollarda (test da emas!)
savol turi bo'yicha tekshirish shart: umumiy MRR RRF 0.838, vaznli 0.970Gibrid qidiruv leksik va semantik signalni birlashtiradi; RRF sodda va shkalaga bog'liq emas, lekin o'rinlarga o'tishda ball farqini yo'qotadi — buni o'lchab tekshiring.
2.7. Reranking: ikki bosqichli qidiruv
1-BOSQICH (tez, keng): BM25 / dense / gibrid -> top-N nomzod (N = 20..100)
2-BOSQICH (sekin, aniq): har (savol, nomzod) juftligini alohida baholash -> qayta tartib
NEGA IKKI BOSQICH:
aniq model (cross-encoder, LLM) qimmat - butun korpusga qo'llab bo'lmaydi
1-bosqich faqat "to'g'ri chunkni N ichiga tushirishi" kerak (recall@N)
2-bosqich 1-o'rinni tuzatadi (recall@1, MRR)
HAQIQIY TIZIMLARDA 2-BOSQICH:
cross-encoder - savol va chunkni BIRGA o'qiydigan transformer (24.8, BERT)
LLM-reranker - "qaysi chunk savolga javob beradi" deb so'rash
bizda: juftlik xususiyatlari + LogisticRegression (3-misol)
3-MISOL XUSUSIYATLARI:
bm25, lsa (kosinus), rrf_orin, bolim_mos, raqam_mos, kod_mos, soz_ulushi
o'rgatish: GroupKFold - oltin chunk bo'yicha (bir chunkning savollari bir foldda)
3-MISOL NATIJASI:
recall@1: RRF 0.747 -> +reranker 0.948; MRR 0.838 -> 0.974 (+0.136, SE 0.015)
1-o'rin: 58 savol tuzaldi, 0 ta buzildi
CHEGARA:
1-bosqich yo'qotgan chunkni reranker qaytara olmaydi: shift = recall@N
RRF: N=1 0.747, N=3 0.906, N=5 0.948, N=10 1.000Reranking: tez retrieval keng to'r tashlaydi, aniqroq model faqat nomzodlarni qayta tartiblaydi; yakuniy sifat chegarasi — 1-bosqichning recall@N.
2.8. k ni tanlash: kontekst, shovqin va xarajat
k OSHSA:
+ recall@k o'sadi (kerakli chunk kontekstga tushish ehtimoli)
- shovqin: keraksiz chunklar ulushi 1 - recall@k / k
- xarajat: kirish tokenlari ~ k ga proporsional (25.2-dars)
- chalg'itish: o'xshash, lekin noto'g'ri chunk (boshqa bo'limning xuddi shunday
qoidasi) modelni adashtirishi mumkin; uzun kontekst o'rtasidagi ma'lumot
yomonroq ishlatilishi mumkin ("lost in the middle")
4-MISOL (RRF, FARAZIY narx):
k oltin kontekstda shovqin token/so'rov
1 0.750 0.25 28
3 0.906 0.70 84
5 0.948 0.81 140
10 1.000 0.90 280
TANLASH QOIDASI:
maqsad metrika (yakuniy javob aniqligi) bo'yicha eng yaxshisidan sezilarli
yomon bo'lmagan ENG KICHIK k - arzonroq va shovqinsizroq
4-misol, oddiy o'quvchi: eng yaxshi k = 8 0.885-bob, tanlov k = 5 (0.882)k — recall, shovqin va narx orasidagi murosa: yakuniy aniqlik bo'yicha "eng yaxshisidan sezilarli yomon bo'lmagan eng kichik k" ni tanlang.
2.9. Answer-in-context va xatolar tahlili
ANSWER-IN-CONTEXT: kontekstda javob bormi?
qat'iy: oltin chunk top-k da (belgilangan to'plam kerak)
satr bo'yicha: javob satri ("28 kunlik") kontekstdagi biror chunkda
bo'sh tekshiruv ("28" raqami bormi) - soxta "topildi" beradi:
4-misol: hatto bu toza korpusda ham k=1 va k=8 da 2 tadan soxta holat
KONTEKSTDA BOR != JAVOB TO'G'RI:
4-misol, k = 10: oltin chunk hamma savolda kontekstda 1.000-bob,
lekin oddiy o'quvchi 0.885 aniqlik berdi; parafrazda 33 savolda chalg'idi
XATOLAR TAHLILI (4-misol, oltin top-3 da yo'q, 27 / 288):
12 parafraz: 1-o'rinda boshqa bo'lim, boshqa mavzu - "ish haqi", "ish kiyimi" dagi
umumiy "ish" so'zi va "uydan" noto'g'ri mavzuga tortdi; 1-o'rinda deyarli
doim Marketing bo'limining ikki chunki ("markaz" chunklar - hubness)
8 raqam: mavzu to'g'ri, bo'lim xato (LSA raqamni ko'rmaydi, RRF o'rinlarni aralashtiradi)
7 parafraz: bo'lim to'g'ri, mavzu xato (namuna: "ish haqi" -> o'sha bo'limning
masofaviy ish qoidasi)
har toifa o'z yechimiga ega: vaznli birlashtirish, reranker, fon korpus, o'zak kesishXatolarni toifalarga ajrating: "qaysi tur, 1-o'rinda nima turibdi, oltin chunk qayerda" — bu keyingi o'zgarishni tanlashga eng tez yo'l.
2.10. Tuzoqlar
Asosiy tuzoqlar: savollarni hujjatni o'qib turib, uning so'zlari bilan yozish (baholash juda oson bo'lib qoladi); savol turlarisiz faqat umumiy o'rtachani ko'rsatish; tenglikda oltin chunkni optimistik o'ringa qo'yish (ball 0 bo'lgan barcha hujjatlar teng bo'lsa, "1-o'rin" tasodifan chiqishi mumkin); alfa, RRF doimiysi va k ni test to'plamida tanlash; reranker ni xuddi o'sha chunklarning savollarida o'rgatib baholash (GroupKFold kerak); 1-bosqich recall@N ni tekshirmasdan reranker ga umid bog'lash; "kontekstda raqam bor" degan bo'sh tekshiruvni "javob topildi" deb hisoblash; k ni "ko'proq — yaxshiroq" deb oshirish va shovqin hamda narxni o'lchamaslik; embedding modelini "hamma narsani tushunadi" deb, kod va raqamli savollarni tekshirmaslik.
3. Tez ma'lumotnoma
import math
import numpy as np
# metrikalar
recall_k = len(set(tartib[:k]) & rel) / len(rel)
precision_k = len(set(tartib[:k]) & rel) / k
rr = next((1 / (i + 1) for i, d in enumerate(tartib) if d in rel), 0.0)
dcg = sum((2 ** daraja.get(d, 0) - 1) / math.log2(i + 2) for i, d in enumerate(tartib[:k]))
# tenglikda optimizmsiz o'rin
orin = int((ball >= ball[oltin]).sum())
# BM25 (bitta so'z hissasi)
idf = math.log(1 + (N - df + 0.5) / (df + 0.5))
hissa = idf * tf * (k1 + 1) / (tf + k1 * (1 - b + b * dl / avgdl))
# RRF va vaznli birlashtirish
rrf_ball[tartib[:n]] += 1 / (60 + np.arange(1, n + 1))
vaznli = alfa * minmax(bm25_ball) + (1 - alfa) * minmax(kosinus)
# juftlashgan taqqoslash (har savol bo'yicha RR farqi)
d = 1 / orin_a - 1 / orin_b
se = d.std(ddof=1) / math.sqrt(len(d))
sezilarli = abs(d.mean()) > 2 * seRetrieval baholash xulosasi
RAG = retrieval + generatsiya; avval retrievalni alohida o'lchang
recall@k - asosiy; MRR, nDCG - tartib sifati
baholash to'plami: savol, oltin chunk, savol TURI; dev/test
BM25 - kod/raqam; dense - parafraz; gibrid - ikkalasi (turi bo'yicha tekshiring)
reranker - 2-bosqich; shift = 1-bosqich recall@N
k - eng yaxshisidan sezilarli yomon bo'lmagan eng kichik qiymat4. Batafsil misollar
Misollar real numpy/sklearn bilan (Python 3.14). Har misolda korpus bir xil kod bilan qayta generatsiya qilinadi (urug' 0).
Misol 1 — Metrikalar noldan va baholash to'plami
"""Retrieval metrikalari noldan: recall@k, precision@k, MRR, nDCG va baholash to'plami."""
import math
import re
import numpy as np
BOLIMLAR = ["moliya", "marketing", "kadrlar", "axborot", "logistika", "savdo",
"yuridik", "omborxona"]
# (rasmiy atama - hujjatlarda, so'zlashuv atamasi - foydalanuvchi savollarida)
SINONIM = {
"tatil": ("ta'til", "dam olish"), "xodim": ("xodim", "ishchi"),
"ariza": ("ariza", "so'rovnoma"), "rahbar": ("rahbar", "boshliq"),
"maosh": ("maosh", "ish haqi"), "masofa": ("masofaviy", "uydan"),
"safar": ("xizmat safari", "komandirovka"),
"kasal": ("kasallik varaqasi", "bemorlik ma'lumotnomasi"),
"parol": ("parol", "maxfiy kod"), "noutbuk": ("noutbuk", "kompyuter"),
"malaka": ("malaka oshirish", "o'qish kursi"), "tushlik": ("tushlik", "ovqat"),
"avtobus": ("xizmat avtobusi", "transport"), "sugurta": ("sug'urta", "polis"),
"forma": ("forma", "ish kiyimi"), "ruxsat": ("ruxsat", "izn"),
}
# (hujjat shabloni, javob, raqamli savol, parafraz savol)
MAVZULAR = [
("{b} bo'limi {xodim}lari uchun yillik {tatil} tartibi. Har bir {xodim}ga yiliga {n} kunlik {tatil} beriladi. {ariza} {rahbar}ga ikki hafta oldin topshiriladi.",
"{n} kunlik", "Qaysi bo'limda yiliga {n} kunlik {tatil} beriladi?",
"{b} bo'limida {xodim} {tatil}ga necha kunga chiqadi?"),
("{b} bo'limida {maosh} har oyning {n}-kunida kartaga o'tkaziladi. {maosh} kechiksa {xodim} {rahbar}ga yozma murojaat qiladi.",
"{n}-kunida", "Qaysi bo'limda {maosh} oyning {n}-kunida o'tkaziladi?",
"{b} bo'limida {maosh} qachon tushadi?"),
("{b} bo'limi {xodim}lari haftada {n} soat {masofa} ishlashi mumkin. {masofa} ish uchun {rahbar} {ruxsat}i talab qilinadi.",
"{n} soat", "Qaysi bo'limda haftada {n} soat {masofa} ishlash mumkin?",
"{b} bo'limida qancha vaqt {masofa} faoliyat yuritsa bo'ladi?"),
("{b} bo'limi {xodim}lari {safar}ga chiqqanda kunlik xarajat {n} ming so'm qilib belgilangan. {safar} hisoboti uch kunda topshiriladi.",
"{n} ming so'm", "Qaysi bo'limda {safar}da kunlik xarajat {n} ming so'm?",
"{b} bo'limida {safar} paytida sutkalik pul qancha?"),
("{b} bo'limida {kasal} {n} kun ichida kadrlar xizmatiga topshirilishi shart. {kasal} bo'lmasa kun ishsiz hisoblanadi.",
"{n} kun ichida", "Qaysi bo'limda {kasal} {n} kun ichida topshiriladi?",
"{b} bo'limida {kasal}ni qachongacha olib kelish kerak?"),
("{b} bo'limi tizimlarida {parol} kamida {n} belgidan iborat bo'ladi. {parol} har uch oyda yangilanadi.",
"{n} belgidan", "Qaysi bo'limda {parol} kamida {n} belgidan iborat?",
"{b} bo'limida {parol} uzunligi qanday bo'lishi lozim?"),
("{b} bo'limi {xodim}lariga berilgan {noutbuk} har {n} yilda almashtiriladi. Buzilgan {noutbuk} haqida {rahbar}ga xabar beriladi.",
"{n} yilda", "Qaysi bo'limda {noutbuk} har {n} yilda almashtiriladi?",
"{b} bo'limida {noutbuk} qachon yangisiga o'zgartiriladi?"),
("{b} bo'limi {xodim}lari yiliga kamida {n} soat {malaka}dan o'tadi. {malaka} xarajatini kompaniya to'laydi.",
"{n} soat", "Qaysi bo'limda yiliga {n} soat {malaka} talab qilinadi?",
"{b} bo'limida {malaka} davomiyligi qancha?"),
("{b} bo'limida {tushlik} uchun har kuni {n} ming so'm kompensatsiya beriladi. {tushlik} tanaffusi bir soat davom etadi.",
"{n} ming so'm", "Qaysi bo'limda {tushlik} uchun {n} ming so'm beriladi?",
"{b} bo'limida {tushlik} pulining qancha qismi qoplanadi?"),
("{b} bo'limi {xodim}lari uchun {avtobus} ertalab {n} daqiqa oralig'ida qatnaydi. {avtobus}dan foydalanish bepul.",
"{n} daqiqa", "Qaysi bo'limda {avtobus} {n} daqiqa oralig'ida qatnaydi?",
"{b} bo'limida {avtobus} qanchalik tez-tez keladi?"),
("{b} bo'limi {xodim}lari tibbiy {sugurta} bilan ta'minlanadi. {sugurta} qoplamasi {n} million so'mgacha.",
"{n} million", "Qaysi bo'limda {sugurta} qoplamasi {n} million so'mgacha?",
"{b} bo'limida {sugurta} davolanishni qancha summagacha yopadi?"),
("{b} bo'limida {forma} har {n} oyda bir marta beriladi. {forma}ni yo'qotgan {xodim} uni o'zi qoplaydi.",
"{n} oyda", "Qaysi bo'limda {forma} har {n} oyda beriladi?",
"{b} bo'limida yangi {forma} qanchalik tez-tez tarqatiladi?"),
]
def bosh_harf(matn):
return re.sub(r"(^|\. )([a-z])", lambda m: m.group(1) + m.group(2).upper(), matn)
def korpus(seed=0, n_fon=30):
"""96 chunk (12 mavzu x 8 bo'lim), 288 savol (3 tur) va belgisiz fon matnlar."""
rng = np.random.default_rng(seed)
rasmiy = {k: v[0] for k, v in SINONIM.items()}
ogzaki = {k: v[1] for k, v in SINONIM.items()}
chunklar, savollar, fon = [], [], []
for m, (shablon, javob, raqam, parafraz) in enumerate(MAVZULAR):
sonlar = rng.choice(np.arange(3, 29), len(BOLIMLAR), replace=False)
for j, b in enumerate(BOLIMLAR):
n = int(sonlar[j])
kod = f"{'ABCDEFGHKLMN'[m]}{'KLMNPRST'[j]}-{int(rng.integers(1000, 9999))}"
i = len(chunklar)
chunklar.append({"id": i, "mavzu": m, "bolim": b, "javob": javob.format(n=n),
"matn": bosh_harf(f"Hujjat {kod}. "
+ shablon.format(b=b.capitalize(), n=n, **rasmiy))})
savollar.append({"q": bosh_harf(parafraz.format(b=b.capitalize(), **ogzaki)),
"tur": "parafraz", "gold": i})
savollar.append({"q": f"{kod} bo'yicha qoida nima?", "tur": "kod", "gold": i})
savollar.append({"q": bosh_harf(raqam.format(n=n, **rasmiy)), "tur": "raqam", "gold": i})
for _ in range(n_fon): # fon: har eslatmada atama varianti tasodifiy
f = re.sub(r"\{(\w+)\}", lambda mm: SINONIM[mm.group(1)][int(rng.integers(0, 2))]
if mm.group(1) in SINONIM else "{" + mm.group(1) + "}", shablon)
fon.append(f.format(b="kompaniya", n=int(rng.integers(3, 29))))
return chunklar, savollar, fon
def tokenlar(matn):
"""Kichik harf, so'z/kod tokenlari; so'zlar 6 belgigacha kesiladi (oddiy o'zak)."""
t = re.findall(r"[a-z0-9'\-]+", matn.lower())
return [w if re.search(r"\d", w) else w[:6] for w in t if w.strip("-'")]
class BM25:
def __init__(self, hujjatlar, k1=1.5, b=0.75):
self.docs = [tokenlar(h) for h in hujjatlar]
self.N, self.k1, self.b = len(self.docs), k1, b
self.avgdl = np.mean([len(d) for d in self.docs])
df = {}
for d in self.docs:
for w in set(d):
df[w] = df.get(w, 0) + 1
self.idf = {w: math.log(1 + (self.N - c + 0.5) / (c + 0.5)) for w, c in df.items()}
self.tf = [{w: d.count(w) for w in set(d)} for d in self.docs]
def ball(self, savol):
s = np.zeros(self.N)
for w in tokenlar(savol):
for i, (tf, d) in enumerate(zip(self.tf, self.docs)):
if w in tf:
norm = self.k1 * (1 - self.b + self.b * len(d) / self.avgdl)
s[i] += self.idf[w] * tf[w] * (self.k1 + 1) / (tf[w] + norm)
return s
def recall_k(tartib, rel, k):
return len(set(tartib[:k]) & set(rel)) / len(rel)
def precision_k(tartib, rel, k):
return len(set(tartib[:k]) & set(rel)) / k
def rr(tartib, rel):
for i, d in enumerate(tartib):
if d in rel:
return 1.0 / (i + 1)
return 0.0
def ndcg_k(tartib, daraja, k):
"""daraja: {hujjat: relevantlik darajasi}; DCG = sum (2^rel - 1) / log2(o'rin + 1)."""
dcg = sum((2 ** daraja.get(d, 0) - 1) / math.log2(i + 2) for i, d in enumerate(tartib[:k]))
ideal = sorted(daraja.values(), reverse=True)[:k]
idcg = sum((2 ** r - 1) / math.log2(i + 2) for i, r in enumerate(ideal))
return dcg / idcg if idcg > 0 else 0.0
def main() -> None:
print("=== 1. Metrikalar qo'lda: bitta savol ===")
tartib = ["d3", "d7", "d1", "d9", "d4"]
rel = {"d1", "d4"}
daraja = {"d1": 2, "d4": 1}
print(f" tizim tartibi {tartib}, relevant {sorted(rel)}")
for k in (1, 3, 5):
print(f" k={k}: recall@k {recall_k(tartib, rel, k):.2f}, precision@k "
f"{precision_k(tartib, rel, k):.2f}")
print(f" RR = 1/3 = {rr(tartib, rel):.3f} (birinchi relevant 3-o'rinda)")
dcg = 3 / math.log2(4) + 1 / math.log2(6)
idcg = 3 / math.log2(2) + 1 / math.log2(3)
print(f" nDCG@5 (d1 = 2, d4 = 1): DCG {dcg:.3f} / IDCG {idcg:.3f} = "
f"{ndcg_k(tartib, daraja, 5):.3f}")
tartib2 = ["d1", "d3", "d7", "d9", "d4"]
print(f" d1 birinchi o'ringa chiqsa: RR {rr(tartib2, rel):.3f}, "
f"nDCG@5 {ndcg_k(tartib2, daraja, 5):.3f}, recall@5 o'zgarmaydi "
f"{recall_k(tartib2, rel, 5):.2f}")
print("\n=== 2. Baholash to'plami: savol -> to'g'ri chunk ===")
chunklar, savollar, _ = korpus()
print(f" chunklar {len(chunklar)}, savollar {len(savollar)}")
for tur in ("parafraz", "kod", "raqam"):
s = next(x for x in savollar if x["tur"] == tur)
print(f" [{tur}] {s['q']}")
print(f" oltin chunk: {chunklar[0]['matn']}")
javob_bor = np.mean([chunklar[s["gold"]]["javob"] in chunklar[s["gold"]]["matn"]
for s in savollar])
takror = len(savollar) - len({s["q"] for s in savollar})
print(f" tekshiruv: javob oltin chunkda {javob_bor:.0%}, takror savollar {takror}")
turlar = {t: sum(s["tur"] == t for s in savollar) for t in ("parafraz", "kod", "raqam")}
print(f" turlar: {turlar}")
print("\n=== 3. BM25 ni baholash (288 savol) ===")
bm = BM25([c["matn"] for c in chunklar])
natija = {k: [] for k in ("r1", "r3", "r10", "p3", "rr", "nd", "ndg")}
for s in savollar:
b = bm.ball(s["q"]) # tenglikda oltin chunk oxiriga (optimizmsiz baho)
tartib = sorted(range(len(b)), key=lambda i: (-b[i], i == s["gold"]))
g = [s["gold"]]
m = chunklar[s["gold"]]["mavzu"]
daraja = {c["id"]: 1 for c in chunklar if c["mavzu"] == m} # shu mavzu - qisman
daraja[s["gold"]] = 2 # aynan shu chunk
natija["r1"].append(recall_k(tartib, g, 1))
natija["r3"].append(recall_k(tartib, g, 3))
natija["r10"].append(recall_k(tartib, g, 10))
natija["p3"].append(precision_k(tartib, g, 3))
natija["rr"].append(rr(tartib, g))
natija["nd"].append(ndcg_k(tartib, {s["gold"]: 1}, 10))
natija["ndg"].append(ndcg_k(tartib, daraja, 10))
for nom, kalit in [("recall@1", "r1"), ("recall@3", "r3"), ("recall@10", "r10"),
("precision@3", "p3"), ("MRR", "rr"), ("nDCG@10 (ikkilik)", "nd"),
("nDCG@10 (darajali)", "ndg")]:
v = np.array(natija[kalit])
print(f" {nom:<19} {v.mean():.3f} (SE {v.std(ddof=1) / math.sqrt(len(v)):.3f})")
print(f" bitta relevant bo'lsa precision@3 = recall@3 / 3: "
f"{np.mean(natija['r3']) / 3:.3f}")
print("\n=== 4. O'rtacha nimani yashiradi: savol turi bo'yicha ===")
print(" tur recall@1 recall@10 MRR")
turlar_arr = np.array([s["tur"] for s in savollar])
mrr_tur = {}
for tur in ("parafraz", "kod", "raqam"):
m = turlar_arr == tur
mrr_tur[tur] = np.array(natija["rr"])[m].mean()
print(f" {tur:<9} {np.array(natija['r1'])[m].mean():>9.3f} "
f"{np.array(natija['r10'])[m].mean():>10.3f} {mrr_tur[tur]:>6.3f}")
eng_yomon = min(mrr_tur, key=mrr_tur.get)
print(f" eng zaif tur: {eng_yomon} (MRR {mrr_tur[eng_yomon]:.3f}) - umumiy MRR "
f"{np.mean(natija['rr']):.3f} buni yashiradi")
print(" ⭐ Baholash to'plamini savol turlari bilan belgilang va har turni alohida o'lchang")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Metrikalar qo'lda: bitta savol ===
tizim tartibi ['d3', 'd7', 'd1', 'd9', 'd4'], relevant ['d1', 'd4']
k=1: recall@k 0.00, precision@k 0.00
k=3: recall@k 0.50, precision@k 0.33
k=5: recall@k 1.00, precision@k 0.40
RR = 1/3 = 0.333 (birinchi relevant 3-o'rinda)
nDCG@5 (d1 = 2, d4 = 1): DCG 1.887 / IDCG 3.631 = 0.520
d1 birinchi o'ringa chiqsa: RR 1.000, nDCG@5 0.933, recall@5 o'zgarmaydi 1.00
=== 2. Baholash to'plami: savol -> to'g'ri chunk ===
chunklar 96, savollar 288
[parafraz] Moliya bo'limida ishchi dam olishga necha kunga chiqadi?
[kod] AK-7564 bo'yicha qoida nima?
[raqam] Qaysi bo'limda yiliga 19 kunlik ta'til beriladi?
oltin chunk: Hujjat AK-7564. Moliya bo'limi xodimlari uchun yillik ta'til tartibi. Har bir xodimga yiliga 19 kunlik ta'til beriladi. Ariza rahbarga ikki hafta oldin topshiriladi.
tekshiruv: javob oltin chunkda 100%, takror savollar 0
turlar: {'parafraz': 96, 'kod': 96, 'raqam': 96}
=== 3. BM25 ni baholash (288 savol) ===
recall@1 0.694 (SE 0.027)
recall@3 0.726 (SE 0.026)
recall@10 0.858 (SE 0.021)
precision@3 0.242 (SE 0.009)
MRR 0.736 (SE 0.024)
nDCG@10 (ikkilik) 0.755 (SE 0.023)
nDCG@10 (darajali) 0.561 (SE 0.022)
bitta relevant bo'lsa precision@3 = recall@3 / 3: 0.242
=== 4. O'rtacha nimani yashiradi: savol turi bo'yicha ===
tur recall@1 recall@10 MRR
parafraz 0.083 0.573 0.209
kod 1.000 1.000 1.000
raqam 1.000 1.000 1.000
eng zaif tur: parafraz (MRR 0.209) - umumiy MRR 0.736 buni yashiradi
⭐ Baholash to'plamini savol turlari bilan belgilang va har turni alohida o'lchangNima ko'rsatdi: 1-bo'limda metrikalar qo'lda tekshirildi: relevant d1 va d4 bo'lgan tartibda recall@3 0.50, precision@3 0.33, RR 0.333, darajali nDCG@5 0.520 (DCG 1.887 / IDCG 3.631). d1 ni birinchi o'ringa chiqarsak recall@5 o'zgarmaydi (1.00), lekin RR 1.000 ga, nDCG 0.933 ga ko'tariladi — recall "bor-yo'q"ni, MRR va nDCG esa "qanchalik tepada"ni o'lchaydi. 2-bo'limda baholash to'plami: 96 chunk, 288 savol, har turdan 96 tadan; har savolning javob satri oltin chunkda bor va takror savollar yo'q. 3-bo'limda BM25: recall@1 0.694, recall@10 0.858, MRR 0.736. Bitta relevant bo'lgani uchun precision@3 aynan recall@3 / 3 (0.242) — u yangi ma'lumot bermaydi. Darajali nDCG (shu mavzudagi boshqa bo'limlar chunki — qisman relevant) ikkilik nDCG dan past (0.561 va 0.755): mavzu jihatidan yaqin chunklar top-10 ning barcha ideal o'rinlarini egallamaydi. Eng muhim natija 4-bo'limda: kod va raqamli savollarda BM25 mukammal (1.000), parafrazda esa recall@1 atigi 0.083, MRR 0.209. Umumiy MRR 0.736 bu zaif joyni butunlay yashiradi. Bog'liq bo'limlar: 2.2, 2.3, 2.4.
Misol 2 — BM25, dense va gibrid: savol turlari bo'yicha
"""BM25, dense (LSA) va gibrid (RRF): savol turlari bo'yicha juftlashgan taqqoslash."""
import math
import re
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
BOLIMLAR = ["moliya", "marketing", "kadrlar", "axborot", "logistika", "savdo",
"yuridik", "omborxona"]
# (rasmiy atama - hujjatlarda, so'zlashuv atamasi - foydalanuvchi savollarida)
SINONIM = {
"tatil": ("ta'til", "dam olish"), "xodim": ("xodim", "ishchi"),
"ariza": ("ariza", "so'rovnoma"), "rahbar": ("rahbar", "boshliq"),
"maosh": ("maosh", "ish haqi"), "masofa": ("masofaviy", "uydan"),
"safar": ("xizmat safari", "komandirovka"),
"kasal": ("kasallik varaqasi", "bemorlik ma'lumotnomasi"),
"parol": ("parol", "maxfiy kod"), "noutbuk": ("noutbuk", "kompyuter"),
"malaka": ("malaka oshirish", "o'qish kursi"), "tushlik": ("tushlik", "ovqat"),
"avtobus": ("xizmat avtobusi", "transport"), "sugurta": ("sug'urta", "polis"),
"forma": ("forma", "ish kiyimi"), "ruxsat": ("ruxsat", "izn"),
}
# (hujjat shabloni, javob, raqamli savol, parafraz savol)
MAVZULAR = [
("{b} bo'limi {xodim}lari uchun yillik {tatil} tartibi. Har bir {xodim}ga yiliga {n} kunlik {tatil} beriladi. {ariza} {rahbar}ga ikki hafta oldin topshiriladi.",
"{n} kunlik", "Qaysi bo'limda yiliga {n} kunlik {tatil} beriladi?",
"{b} bo'limida {xodim} {tatil}ga necha kunga chiqadi?"),
("{b} bo'limida {maosh} har oyning {n}-kunida kartaga o'tkaziladi. {maosh} kechiksa {xodim} {rahbar}ga yozma murojaat qiladi.",
"{n}-kunida", "Qaysi bo'limda {maosh} oyning {n}-kunida o'tkaziladi?",
"{b} bo'limida {maosh} qachon tushadi?"),
("{b} bo'limi {xodim}lari haftada {n} soat {masofa} ishlashi mumkin. {masofa} ish uchun {rahbar} {ruxsat}i talab qilinadi.",
"{n} soat", "Qaysi bo'limda haftada {n} soat {masofa} ishlash mumkin?",
"{b} bo'limida qancha vaqt {masofa} faoliyat yuritsa bo'ladi?"),
("{b} bo'limi {xodim}lari {safar}ga chiqqanda kunlik xarajat {n} ming so'm qilib belgilangan. {safar} hisoboti uch kunda topshiriladi.",
"{n} ming so'm", "Qaysi bo'limda {safar}da kunlik xarajat {n} ming so'm?",
"{b} bo'limida {safar} paytida sutkalik pul qancha?"),
("{b} bo'limida {kasal} {n} kun ichida kadrlar xizmatiga topshirilishi shart. {kasal} bo'lmasa kun ishsiz hisoblanadi.",
"{n} kun ichida", "Qaysi bo'limda {kasal} {n} kun ichida topshiriladi?",
"{b} bo'limida {kasal}ni qachongacha olib kelish kerak?"),
("{b} bo'limi tizimlarida {parol} kamida {n} belgidan iborat bo'ladi. {parol} har uch oyda yangilanadi.",
"{n} belgidan", "Qaysi bo'limda {parol} kamida {n} belgidan iborat?",
"{b} bo'limida {parol} uzunligi qanday bo'lishi lozim?"),
("{b} bo'limi {xodim}lariga berilgan {noutbuk} har {n} yilda almashtiriladi. Buzilgan {noutbuk} haqida {rahbar}ga xabar beriladi.",
"{n} yilda", "Qaysi bo'limda {noutbuk} har {n} yilda almashtiriladi?",
"{b} bo'limida {noutbuk} qachon yangisiga o'zgartiriladi?"),
("{b} bo'limi {xodim}lari yiliga kamida {n} soat {malaka}dan o'tadi. {malaka} xarajatini kompaniya to'laydi.",
"{n} soat", "Qaysi bo'limda yiliga {n} soat {malaka} talab qilinadi?",
"{b} bo'limida {malaka} davomiyligi qancha?"),
("{b} bo'limida {tushlik} uchun har kuni {n} ming so'm kompensatsiya beriladi. {tushlik} tanaffusi bir soat davom etadi.",
"{n} ming so'm", "Qaysi bo'limda {tushlik} uchun {n} ming so'm beriladi?",
"{b} bo'limida {tushlik} pulining qancha qismi qoplanadi?"),
("{b} bo'limi {xodim}lari uchun {avtobus} ertalab {n} daqiqa oralig'ida qatnaydi. {avtobus}dan foydalanish bepul.",
"{n} daqiqa", "Qaysi bo'limda {avtobus} {n} daqiqa oralig'ida qatnaydi?",
"{b} bo'limida {avtobus} qanchalik tez-tez keladi?"),
("{b} bo'limi {xodim}lari tibbiy {sugurta} bilan ta'minlanadi. {sugurta} qoplamasi {n} million so'mgacha.",
"{n} million", "Qaysi bo'limda {sugurta} qoplamasi {n} million so'mgacha?",
"{b} bo'limida {sugurta} davolanishni qancha summagacha yopadi?"),
("{b} bo'limida {forma} har {n} oyda bir marta beriladi. {forma}ni yo'qotgan {xodim} uni o'zi qoplaydi.",
"{n} oyda", "Qaysi bo'limda {forma} har {n} oyda beriladi?",
"{b} bo'limida yangi {forma} qanchalik tez-tez tarqatiladi?"),
]
def bosh_harf(matn):
return re.sub(r"(^|\. )([a-z])", lambda m: m.group(1) + m.group(2).upper(), matn)
def korpus(seed=0, n_fon=30):
"""96 chunk (12 mavzu x 8 bo'lim), 288 savol (3 tur) va belgisiz fon matnlar."""
rng = np.random.default_rng(seed)
rasmiy = {k: v[0] for k, v in SINONIM.items()}
ogzaki = {k: v[1] for k, v in SINONIM.items()}
chunklar, savollar, fon = [], [], []
for m, (shablon, javob, raqam, parafraz) in enumerate(MAVZULAR):
sonlar = rng.choice(np.arange(3, 29), len(BOLIMLAR), replace=False)
for j, b in enumerate(BOLIMLAR):
n = int(sonlar[j])
kod = f"{'ABCDEFGHKLMN'[m]}{'KLMNPRST'[j]}-{int(rng.integers(1000, 9999))}"
i = len(chunklar)
chunklar.append({"id": i, "mavzu": m, "bolim": b, "javob": javob.format(n=n),
"matn": bosh_harf(f"Hujjat {kod}. "
+ shablon.format(b=b.capitalize(), n=n, **rasmiy))})
savollar.append({"q": bosh_harf(parafraz.format(b=b.capitalize(), **ogzaki)),
"tur": "parafraz", "gold": i})
savollar.append({"q": f"{kod} bo'yicha qoida nima?", "tur": "kod", "gold": i})
savollar.append({"q": bosh_harf(raqam.format(n=n, **rasmiy)), "tur": "raqam", "gold": i})
for _ in range(n_fon): # fon: har eslatmada atama varianti tasodifiy
f = re.sub(r"\{(\w+)\}", lambda mm: SINONIM[mm.group(1)][int(rng.integers(0, 2))]
if mm.group(1) in SINONIM else "{" + mm.group(1) + "}", shablon)
fon.append(f.format(b="kompaniya", n=int(rng.integers(3, 29))))
return chunklar, savollar, fon
def tokenlar(matn):
"""Kichik harf, so'z/kod tokenlari; so'zlar 6 belgigacha kesiladi (oddiy o'zak)."""
t = re.findall(r"[a-z0-9'\-]+", matn.lower())
return [w if re.search(r"\d", w) else w[:6] for w in t if w.strip("-'")]
class BM25:
def __init__(self, hujjatlar, k1=1.5, b=0.75):
self.docs = [tokenlar(h) for h in hujjatlar]
self.N, self.k1, self.b = len(self.docs), k1, b
self.avgdl = np.mean([len(d) for d in self.docs])
df = {}
for d in self.docs:
for w in set(d):
df[w] = df.get(w, 0) + 1
self.idf = {w: math.log(1 + (self.N - c + 0.5) / (c + 0.5)) for w, c in df.items()}
self.tf = [{w: d.count(w) for w in set(d)} for d in self.docs]
def ball(self, savol):
s = np.zeros(self.N)
for w in tokenlar(savol):
for i, (tf, d) in enumerate(zip(self.tf, self.docs)):
if w in tf:
norm = self.k1 * (1 - self.b + self.b * len(d) / self.avgdl)
s[i] += self.idf[w] * tf[w] * (self.k1 + 1) / (tf[w] + norm)
return s
class LSA:
"""TF-IDF + TruncatedSVD; fon matnlar faqat 'embedding' ni o'rgatishga ketadi."""
def __init__(self, hujjatlar, fon=(), k=12, seed=0):
self.vek = TfidfVectorizer(analyzer=tokenlar, sublinear_tf=True)
X = self.vek.fit_transform(list(fon) + list(hujjatlar))
self.svd = TruncatedSVD(k, random_state=seed).fit(X)
Z = self.svd.transform(self.vek.transform(hujjatlar))
self.Z = Z / np.linalg.norm(Z, axis=1, keepdims=True)
def ball(self, savol):
z = self.svd.transform(self.vek.transform([savol]))[0]
return self.Z @ (z / (np.linalg.norm(z) + 1e-12))
def rrf(*balllar, k=60, n=30):
"""Reciprocal Rank Fusion: har ro'yxatdan top-n nomzod (ball > 0)."""
s = np.zeros(len(balllar[0]))
for r in balllar:
tartib = np.argsort(-r, kind="stable")[:n]
tartib = tartib[r[tartib] > 0]
s[tartib] += 1.0 / (k + np.arange(1, len(tartib) + 1))
return s
def minmax(x):
return (x - x.min()) / (x.max() - x.min() + 1e-12)
def orin(ball, gold):
"""Oltin chunkning o'rni (1 dan); tenglikda - eng yomon o'rin (optimizmsiz)."""
return int((ball >= ball[gold]).sum())
def main() -> None:
chunklar, savollar, fon = korpus()
matnlar = [c["matn"] for c in chunklar]
bm = BM25(matnlar)
lsa0 = LSA(matnlar, fon=(), k=12)
lsa = LSA(matnlar, fon=fon, k=12)
print("=== 1. Retrieverlar ===")
print(f" BM25: noldan, k1 = 1.5, b = 0.75; lug'at {len(bm.idf)} token")
print(f" LSA: TF-IDF + SVD(12); fonsiz - faqat {len(matnlar)} chunk, "
f"fon bilan - yana {len(fon)} belgisiz matn")
print(" RRF: 1 / (60 + o'rin), har ro'yxatdan top-30; vaznli: 0.5 minmax(BM25) + 0.5 minmax(LSA)")
for nom, model in (("fonsiz", lsa0), ("fon bilan", lsa)):
qator = []
for a, b in (("dam olish", "ta'til"), ("komandirovka", "xizmat safari"),
("dam olish", "xizmat safari")):
za, zb = (model.svd.transform(model.vek.transform([x]))[0] for x in (a, b))
cos = za @ zb / (np.linalg.norm(za) * np.linalg.norm(zb) + 1e-12)
qator.append(f"{a} ~ {b}: {cos:.2f}")
print(f" LSA {nom:<9} cos: " + "; ".join(qator))
usullar = {
"BM25": bm.ball,
"LSA fonsiz": lsa0.ball,
"LSA fon": lsa.ball,
"RRF": lambda q: rrf(bm.ball(q), lsa.ball(q)),
"vaznli": lambda q: 0.5 * minmax(bm.ball(q)) + 0.5 * minmax(lsa.ball(q)),
}
orinlar = {n: np.array([orin(f(s["q"]), s["gold"]) for s in savollar])
for n, f in usullar.items()}
tur = np.array([s["tur"] for s in savollar])
print("\n=== 2. Savol turi bo'yicha: recall@1 / recall@5 / MRR ===")
print(" usul parafraz kod raqam")
for n, o in orinlar.items():
qator = []
for t in ("parafraz", "kod", "raqam"):
m = tur == t
qator.append(f"{(o[m] <= 1).mean():.2f}/{(o[m] <= 5).mean():.2f}/{(1 / o[m]).mean():.3f}")
print(f" {n:<12} " + " ".join(f"{q:<18}" for q in qator))
print("\n=== 3. Juftlashgan farq (RR, har savol), SE va xulosa ===")
for a, b in (("RRF", "BM25"), ("RRF", "LSA fon"), ("vaznli", "RRF")):
for t in ("parafraz", "kod", "raqam"):
m = tur == t
d = 1 / orinlar[a][m] - 1 / orinlar[b][m]
se = d.std(ddof=1) / math.sqrt(m.sum())
x = ("sezilarli yaxshi" if d.mean() > 2 * se else
"sezilarli yomon" if d.mean() < -2 * se else "sezilarli farq yo'q")
print(f" {a:<6} - {b:<7} [{t:<8}] {d.mean():+.3f} (SE {se:.3f}) -> {x}")
print("\n=== 4. Qaror: har turda eng yaxshisidan sezilarli yomon bo'lmaganlar ===")
for t in ("parafraz", "kod", "raqam"):
m = tur == t
rrm = {n: 1 / o[m] for n, o in orinlar.items()}
eng = max(rrm, key=lambda n: rrm[n].mean())
yaxshi = []
for n in usullar:
d = rrm[eng] - rrm[n]
se = d.std(ddof=1) / math.sqrt(m.sum())
if n == eng or d.mean() <= 2 * se:
yaxshi.append(n)
print(f" {t:<9} eng yaxshi {eng} (MRR {rrm[eng].mean():.3f}); teng kuchlilar: {yaxshi}")
umumiy = {n: (1 / o).mean() for n, o in orinlar.items()}
print(" umumiy MRR: " + ", ".join(f"{n} {v:.3f}" for n, v in umumiy.items()))
print(" ⭐ Gibrid - leksik va semantik signalni birlashtiradi; turi bo'yicha tekshiring")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Retrieverlar ===
BM25: noldan, k1 = 1.5, b = 0.75; lug'at 241 token
LSA: TF-IDF + SVD(12); fonsiz - faqat 96 chunk, fon bilan - yana 360 belgisiz matn
RRF: 1 / (60 + o'rin), har ro'yxatdan top-30; vaznli: 0.5 minmax(BM25) + 0.5 minmax(LSA)
LSA fonsiz cos: dam olish ~ ta'til: 0.00; komandirovka ~ xizmat safari: 0.00; dam olish ~ xizmat safari: 0.00
LSA fon bilan cos: dam olish ~ ta'til: 1.00; komandirovka ~ xizmat safari: 0.90; dam olish ~ xizmat safari: -0.11
=== 2. Savol turi bo'yicha: recall@1 / recall@5 / MRR ===
usul parafraz kod raqam
BM25 0.08/0.18/0.209 1.00/1.00/1.000 1.00/1.00/1.000
LSA fonsiz 0.01/0.03/0.053 0.29/0.82/0.507 0.48/0.94/0.642
LSA fon 0.22/0.66/0.404 0.28/0.81/0.478 0.28/0.80/0.491
RRF 0.69/0.85/0.771 1.00/1.00/1.000 0.55/0.99/0.741
vaznli 0.89/0.92/0.910 1.00/1.00/1.000 1.00/1.00/1.000
=== 3. Juftlashgan farq (RR, har savol), SE va xulosa ===
RRF - BM25 [parafraz] +0.562 (SE 0.037) -> sezilarli yaxshi
RRF - BM25 [kod ] +0.000 (SE 0.000) -> sezilarli farq yo'q
RRF - BM25 [raqam ] -0.259 (SE 0.031) -> sezilarli yomon
RRF - LSA fon [parafraz] +0.367 (SE 0.042) -> sezilarli yaxshi
RRF - LSA fon [kod ] +0.522 (SE 0.035) -> sezilarli yaxshi
RRF - LSA fon [raqam ] +0.250 (SE 0.026) -> sezilarli yaxshi
vaznli - RRF [parafraz] +0.139 (SE 0.030) -> sezilarli yaxshi
vaznli - RRF [kod ] +0.000 (SE 0.000) -> sezilarli farq yo'q
vaznli - RRF [raqam ] +0.259 (SE 0.031) -> sezilarli yaxshi
=== 4. Qaror: har turda eng yaxshisidan sezilarli yomon bo'lmaganlar ===
parafraz eng yaxshi vaznli (MRR 0.910); teng kuchlilar: ['vaznli']
kod eng yaxshi BM25 (MRR 1.000); teng kuchlilar: ['BM25', 'RRF', 'vaznli']
raqam eng yaxshi BM25 (MRR 1.000); teng kuchlilar: ['BM25', 'vaznli']
umumiy MRR: BM25 0.736, LSA fonsiz 0.401, LSA fon 0.458, RRF 0.838, vaznli 0.970
⭐ Gibrid - leksik va semantik signalni birlashtiradi; turi bo'yicha tekshiringNima ko'rsatdi: 1-bo'lim dense qidiruvning bilimi qayerdan kelishini ko'rsatdi: faqat 96 chunkda o'rgatilgan LSA uchun dam olish va ta'til orasidagi kosinus 0.00 — bu so'zlar chunklarda umuman uchramaydi. 360 ta belgisiz fon matn qo'shilganda (ularda ikkala atama aralash ishlatiladi) kosinus 1.00, komandirovka va xizmat safari — 0.90, aloqasiz juftlik esa -0.11. 2-bo'lim — savol turlari bo'yicha jadval: BM25 kod va raqamda 1.00, parafrazda MRR 0.209; LSA (fon bilan) parafrazda 0.404 ga ko'tariladi, lekin kod (0.478) va raqamda (0.491) yiqiladi — 12 o'lchovli vektorda noyob kod va raqam deyarli iz qoldirmaydi. RRF parafrazda ikkala retrieverdan ham ancha yaxshi (0.771): BM25 bo'limni, LSA mavzuni topadi, ularning kesishmasi oltin chunk. Lekin raqamli savollarda RRF BM25 dan sezilarli yomon: -0.259, SE 0.031. Sababi — RRF faqat o'rinlarni ko'radi: BM25 oltin chunkni 1-o'ringa, qolgan bo'limlarning xuddi shunday qoidalarini keyingi o'rinlarga qo'yadi, raqamni ko'rmaydigan LSA esa ulardan birini tepaga chiqaradi va yig'indida oltin chunk yutqazadi. Vaznli birlashtirish (0.5 minmax) ball farqini saqlagani uchun uchala turda ham eng yaxshi yoki teng: parafraz 0.910, kod va raqam 1.000, umumiy MRR 0.970 (RRF — 0.838). 4-bo'limdagi qaror: parafrazda faqat vaznli usul eng yaxshisiga teng, kod va raqamda BM25 va vaznli. Bu natija kutilganidek "gibrid har doim yaxshi" emas: gibridning aniq qanday birlashtirilgani muhim. alfa = 0.5 oldindan belgilangan; amalda u dev savollarda tanlanadi. Bog'liq bo'limlar: 2.4, 2.5, 2.6.
Misol 3 — Reranking: ikki bosqichli qidiruv
"""Ikki bosqichli qidiruv: RRF top-20 -> LogReg reranker (savol-chunk xususiyatlari)."""
import math
import re
import warnings
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GroupKFold
BOLIMLAR = ["moliya", "marketing", "kadrlar", "axborot", "logistika", "savdo",
"yuridik", "omborxona"]
# (rasmiy atama - hujjatlarda, so'zlashuv atamasi - foydalanuvchi savollarida)
SINONIM = {
"tatil": ("ta'til", "dam olish"), "xodim": ("xodim", "ishchi"),
"ariza": ("ariza", "so'rovnoma"), "rahbar": ("rahbar", "boshliq"),
"maosh": ("maosh", "ish haqi"), "masofa": ("masofaviy", "uydan"),
"safar": ("xizmat safari", "komandirovka"),
"kasal": ("kasallik varaqasi", "bemorlik ma'lumotnomasi"),
"parol": ("parol", "maxfiy kod"), "noutbuk": ("noutbuk", "kompyuter"),
"malaka": ("malaka oshirish", "o'qish kursi"), "tushlik": ("tushlik", "ovqat"),
"avtobus": ("xizmat avtobusi", "transport"), "sugurta": ("sug'urta", "polis"),
"forma": ("forma", "ish kiyimi"), "ruxsat": ("ruxsat", "izn"),
}
# (hujjat shabloni, javob, raqamli savol, parafraz savol)
MAVZULAR = [
("{b} bo'limi {xodim}lari uchun yillik {tatil} tartibi. Har bir {xodim}ga yiliga {n} kunlik {tatil} beriladi. {ariza} {rahbar}ga ikki hafta oldin topshiriladi.",
"{n} kunlik", "Qaysi bo'limda yiliga {n} kunlik {tatil} beriladi?",
"{b} bo'limida {xodim} {tatil}ga necha kunga chiqadi?"),
("{b} bo'limida {maosh} har oyning {n}-kunida kartaga o'tkaziladi. {maosh} kechiksa {xodim} {rahbar}ga yozma murojaat qiladi.",
"{n}-kunida", "Qaysi bo'limda {maosh} oyning {n}-kunida o'tkaziladi?",
"{b} bo'limida {maosh} qachon tushadi?"),
("{b} bo'limi {xodim}lari haftada {n} soat {masofa} ishlashi mumkin. {masofa} ish uchun {rahbar} {ruxsat}i talab qilinadi.",
"{n} soat", "Qaysi bo'limda haftada {n} soat {masofa} ishlash mumkin?",
"{b} bo'limida qancha vaqt {masofa} faoliyat yuritsa bo'ladi?"),
("{b} bo'limi {xodim}lari {safar}ga chiqqanda kunlik xarajat {n} ming so'm qilib belgilangan. {safar} hisoboti uch kunda topshiriladi.",
"{n} ming so'm", "Qaysi bo'limda {safar}da kunlik xarajat {n} ming so'm?",
"{b} bo'limida {safar} paytida sutkalik pul qancha?"),
("{b} bo'limida {kasal} {n} kun ichida kadrlar xizmatiga topshirilishi shart. {kasal} bo'lmasa kun ishsiz hisoblanadi.",
"{n} kun ichida", "Qaysi bo'limda {kasal} {n} kun ichida topshiriladi?",
"{b} bo'limida {kasal}ni qachongacha olib kelish kerak?"),
("{b} bo'limi tizimlarida {parol} kamida {n} belgidan iborat bo'ladi. {parol} har uch oyda yangilanadi.",
"{n} belgidan", "Qaysi bo'limda {parol} kamida {n} belgidan iborat?",
"{b} bo'limida {parol} uzunligi qanday bo'lishi lozim?"),
("{b} bo'limi {xodim}lariga berilgan {noutbuk} har {n} yilda almashtiriladi. Buzilgan {noutbuk} haqida {rahbar}ga xabar beriladi.",
"{n} yilda", "Qaysi bo'limda {noutbuk} har {n} yilda almashtiriladi?",
"{b} bo'limida {noutbuk} qachon yangisiga o'zgartiriladi?"),
("{b} bo'limi {xodim}lari yiliga kamida {n} soat {malaka}dan o'tadi. {malaka} xarajatini kompaniya to'laydi.",
"{n} soat", "Qaysi bo'limda yiliga {n} soat {malaka} talab qilinadi?",
"{b} bo'limida {malaka} davomiyligi qancha?"),
("{b} bo'limida {tushlik} uchun har kuni {n} ming so'm kompensatsiya beriladi. {tushlik} tanaffusi bir soat davom etadi.",
"{n} ming so'm", "Qaysi bo'limda {tushlik} uchun {n} ming so'm beriladi?",
"{b} bo'limida {tushlik} pulining qancha qismi qoplanadi?"),
("{b} bo'limi {xodim}lari uchun {avtobus} ertalab {n} daqiqa oralig'ida qatnaydi. {avtobus}dan foydalanish bepul.",
"{n} daqiqa", "Qaysi bo'limda {avtobus} {n} daqiqa oralig'ida qatnaydi?",
"{b} bo'limida {avtobus} qanchalik tez-tez keladi?"),
("{b} bo'limi {xodim}lari tibbiy {sugurta} bilan ta'minlanadi. {sugurta} qoplamasi {n} million so'mgacha.",
"{n} million", "Qaysi bo'limda {sugurta} qoplamasi {n} million so'mgacha?",
"{b} bo'limida {sugurta} davolanishni qancha summagacha yopadi?"),
("{b} bo'limida {forma} har {n} oyda bir marta beriladi. {forma}ni yo'qotgan {xodim} uni o'zi qoplaydi.",
"{n} oyda", "Qaysi bo'limda {forma} har {n} oyda beriladi?",
"{b} bo'limida yangi {forma} qanchalik tez-tez tarqatiladi?"),
]
def bosh_harf(matn):
return re.sub(r"(^|\. )([a-z])", lambda m: m.group(1) + m.group(2).upper(), matn)
def korpus(seed=0, n_fon=30):
"""96 chunk (12 mavzu x 8 bo'lim), 288 savol (3 tur) va belgisiz fon matnlar."""
rng = np.random.default_rng(seed)
rasmiy = {k: v[0] for k, v in SINONIM.items()}
ogzaki = {k: v[1] for k, v in SINONIM.items()}
chunklar, savollar, fon = [], [], []
for m, (shablon, javob, raqam, parafraz) in enumerate(MAVZULAR):
sonlar = rng.choice(np.arange(3, 29), len(BOLIMLAR), replace=False)
for j, b in enumerate(BOLIMLAR):
n = int(sonlar[j])
kod = f"{'ABCDEFGHKLMN'[m]}{'KLMNPRST'[j]}-{int(rng.integers(1000, 9999))}"
i = len(chunklar)
chunklar.append({"id": i, "mavzu": m, "bolim": b, "javob": javob.format(n=n),
"matn": bosh_harf(f"Hujjat {kod}. "
+ shablon.format(b=b.capitalize(), n=n, **rasmiy))})
savollar.append({"q": bosh_harf(parafraz.format(b=b.capitalize(), **ogzaki)),
"tur": "parafraz", "gold": i})
savollar.append({"q": f"{kod} bo'yicha qoida nima?", "tur": "kod", "gold": i})
savollar.append({"q": bosh_harf(raqam.format(n=n, **rasmiy)), "tur": "raqam", "gold": i})
for _ in range(n_fon): # fon: har eslatmada atama varianti tasodifiy
f = re.sub(r"\{(\w+)\}", lambda mm: SINONIM[mm.group(1)][int(rng.integers(0, 2))]
if mm.group(1) in SINONIM else "{" + mm.group(1) + "}", shablon)
fon.append(f.format(b="kompaniya", n=int(rng.integers(3, 29))))
return chunklar, savollar, fon
def tokenlar(matn):
"""Kichik harf, so'z/kod tokenlari; so'zlar 6 belgigacha kesiladi (oddiy o'zak)."""
t = re.findall(r"[a-z0-9'\-]+", matn.lower())
return [w if re.search(r"\d", w) else w[:6] for w in t if w.strip("-'")]
class BM25:
def __init__(self, hujjatlar, k1=1.5, b=0.75):
self.docs = [tokenlar(h) for h in hujjatlar]
self.N, self.k1, self.b = len(self.docs), k1, b
self.avgdl = np.mean([len(d) for d in self.docs])
df = {}
for d in self.docs:
for w in set(d):
df[w] = df.get(w, 0) + 1
self.idf = {w: math.log(1 + (self.N - c + 0.5) / (c + 0.5)) for w, c in df.items()}
self.tf = [{w: d.count(w) for w in set(d)} for d in self.docs]
def ball(self, savol):
s = np.zeros(self.N)
for w in tokenlar(savol):
for i, (tf, d) in enumerate(zip(self.tf, self.docs)):
if w in tf:
norm = self.k1 * (1 - self.b + self.b * len(d) / self.avgdl)
s[i] += self.idf[w] * tf[w] * (self.k1 + 1) / (tf[w] + norm)
return s
class LSA:
"""TF-IDF + TruncatedSVD; fon matnlar faqat 'embedding' ni o'rgatishga ketadi."""
def __init__(self, hujjatlar, fon=(), k=12, seed=0):
self.vek = TfidfVectorizer(analyzer=tokenlar, sublinear_tf=True)
X = self.vek.fit_transform(list(fon) + list(hujjatlar))
self.svd = TruncatedSVD(k, random_state=seed).fit(X)
Z = self.svd.transform(self.vek.transform(hujjatlar))
self.Z = Z / np.linalg.norm(Z, axis=1, keepdims=True)
def ball(self, savol):
z = self.svd.transform(self.vek.transform([savol]))[0]
return self.Z @ (z / (np.linalg.norm(z) + 1e-12))
def rrf(*balllar, k=60, n=30):
"""Reciprocal Rank Fusion: har ro'yxatdan top-n nomzod (ball > 0)."""
s = np.zeros(len(balllar[0]))
for r in balllar:
tartib = np.argsort(-r, kind="stable")[:n]
tartib = tartib[r[tartib] > 0]
s[tartib] += 1.0 / (k + np.arange(1, len(tartib) + 1))
return s
NOMZOD = 20
XUSUSIYATLAR = ["bm25", "lsa", "rrf_orin", "bolim_mos", "raqam_mos", "kod_mos", "soz_ulushi"]
def xususiyatlar(savol, cid, bm_b, lsa_b, orin, chunk_tok):
q = tokenlar(savol)
c = chunk_tok[cid]
raqamlar = [w for w in q if re.fullmatch(r"\d+(-\w+)?", w)]
kodlar = [w for w in q if re.fullmatch(r"[a-z]{2}-\d{4}", w)]
bolim = [w for w in q if w in {b[:6] for b in BOLIMLAR}]
return [bm_b, lsa_b, 1.0 / orin,
float(any(w in c for w in bolim)),
float(any(w in c for w in raqamlar)),
float(any(w in c for w in kodlar)),
np.mean([w in c for w in q])]
def main() -> None:
chunklar, savollar, fon = korpus()
matnlar = [c["matn"] for c in chunklar]
chunk_tok = [set(tokenlar(m)) for m in matnlar]
bm, lsa = BM25(matnlar), LSA(matnlar, fon=fon, k=12)
print(f"=== 1. 1-bosqich: RRF, har savolga top-{NOMZOD} nomzod ===")
X, y, guruh, sav_id, nomzodlar, rrf_orin = [], [], [], [], [], []
for si, s in enumerate(savollar):
b1, b2 = bm.ball(s["q"]), lsa.ball(s["q"])
f = rrf(b1, b2)
tartib = np.argsort(-f, kind="stable")
rrf_orin.append(int((f >= f[s["gold"]]).sum())) # tenglikda - eng yomon o'rin
top = tartib[:NOMZOD]
nomzodlar.append(top)
b1n = b1[top] / (b1[top].max() + 1e-12)
for o, cid in enumerate(top, 1):
X.append(xususiyatlar(s["q"], cid, b1n[o - 1], b2[cid], o, chunk_tok))
y.append(int(cid == s["gold"]))
guruh.append(s["gold"])
sav_id.append(si)
X, y, guruh, sav_id = map(np.array, (X, y, guruh, sav_id))
rrf_orin = np.array(rrf_orin)
print(f" juftliklar {len(y)} (musbat {y.sum()}), oltin top-{NOMZOD} ichida: "
f"{(rrf_orin <= NOMZOD).mean():.3f} - bu reranker uchun shift")
print("\n=== 2. 2-bosqich: LogReg, GroupKFold (oltin chunk bo'yicha, 4 fold) ===")
ehtimol = np.zeros(len(y))
koef = []
for oq, ts in GroupKFold(4).split(X, y, guruh):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
m = LogisticRegression(C=1.0, max_iter=2000).fit(X[oq], y[oq])
ehtimol[ts] = m.predict_proba(X[ts])[:, 1]
koef.append(m.coef_[0])
koef = np.mean(koef, axis=0)
for n, k in sorted(zip(XUSUSIYATLAR, koef), key=lambda t: -abs(t[1])):
print(f" {n:<11} {k:+.2f}")
print(" har savol faqat o'z chunki ko'rilmagan foldning modeli bilan baholandi")
rerank_orin = []
for si, s in enumerate(savollar):
m = sav_id == si
oltin = nomzodlar[si] == s["gold"]
rerank_orin.append(int((ehtimol[m] >= ehtimol[m][oltin][0]).sum()) if oltin.any() else 10 ** 6)
rerank_orin = np.array(rerank_orin)
print("\n=== 3. Natija: RRF va RRF + reranker ===")
tur = np.array([s["tur"] for s in savollar])
print(" tur RRF r@1 +rerank r@1 RRF MRR +rerank MRR farq MRR (SE)")
for t in ("parafraz", "kod", "raqam", "hammasi"):
m = tur == t if t != "hammasi" else np.ones(len(tur), bool)
a, b = 1 / rrf_orin[m], 1 / rerank_orin[m]
d = b - a
se = d.std(ddof=1) / math.sqrt(m.sum())
belgi = "*" if abs(d.mean()) > 2 * se else " "
print(f" {t:<9} {(rrf_orin[m] == 1).mean():>8.3f} {(rerank_orin[m] == 1).mean():>12.3f} "
f"{a.mean():>9.3f} {b.mean():>12.3f} {d.mean():+.3f} ({se:.3f}){belgi}")
print(" * - farq 2*SE dan katta")
print("\n=== 4. Narx va chegaralar ===")
print(f" reranker har savolga {NOMZOD} ta juftlikni ko'radi (butun korpus - {len(chunklar)})")
print(" 1-bosqich shifti (oltin top-N ichida): " + ", ".join(
f"N={n}: {(rrf_orin <= n).mean():.3f}" for n in (1, 3, 5, 10, 20)))
print(" N kichik - arzon, lekin 1-bosqich yo'qotgan chunkni reranker qaytara olmaydi")
yomonlashgan = int(((rerank_orin > 1) & (rrf_orin == 1)).sum())
tuzalgan = int(((rerank_orin == 1) & (rrf_orin > 1)).sum())
print(f" 1-o'rin: tuzalgan {tuzalgan}, buzilgan {yomonlashgan}")
print(" ⭐ Tez retrieval keng to'r tashlaydi, aniqroq model faqat nomzodlarni tartiblaydi")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. 1-bosqich: RRF, har savolga top-20 nomzod ===
juftliklar 5760 (musbat 288), oltin top-20 ichida: 1.000 - bu reranker uchun shift
=== 2. 2-bosqich: LogReg, GroupKFold (oltin chunk bo'yicha, 4 fold) ===
lsa +5.55
raqam_mos +5.29
kod_mos +4.38
bolim_mos +4.20
rrf_orin +2.82
bm25 +1.40
soz_ulushi -0.32
har savol faqat o'z chunki ko'rilmagan foldning modeli bilan baholandi
=== 3. Natija: RRF va RRF + reranker ===
tur RRF r@1 +rerank r@1 RRF MRR +rerank MRR farq MRR (SE)
parafraz 0.688 0.844 0.771 0.922 +0.151 0.027-bob*
kod 1.000 1.000 1.000 1.000 +0.000 0.000-bob
raqam 0.552 1.000 0.741 1.000 +0.259 0.031-bob*
hammasi 0.747 0.948 0.838 0.974 +0.136 0.015-bob*
* - farq 2*SE dan katta
=== 4. Narx va chegaralar ===
reranker har savolga 20 ta juftlikni ko'radi (butun korpus - 96)
1-bosqich shifti (oltin top-N ichida): N=1: 0.747, N=3: 0.906, N=5: 0.948, N=10: 1.000, N=20: 1.000
N kichik - arzon, lekin 1-bosqich yo'qotgan chunkni reranker qaytara olmaydi
1-o'rin: tuzalgan 58, buzilgan 0
⭐ Tez retrieval keng to'r tashlaydi, aniqroq model faqat nomzodlarni tartiblaydiNima ko'rsatdi: 1-bosqichda RRF har savolga 20 nomzod qaytardi — 288 savolning barchasida oltin chunk shu 20 talik ichida (shift 1.000). 2-bosqichdagi LogReg har (savol, nomzod) juftligini 7 ta xususiyat bilan baholadi va GroupKFold bilan o'rgatildi: har savol o'z oltin chunki ko'rilmagan foldning modeli bilan baholangan. Koeffitsientlar mantiqiy: eng katta vaznlar lsa (+5.55), raqam_mos (+5.29), kod_mos (+4.38) va bolim_mos (+4.20) da — reranker aynan RRF yo'qotgan aniq moslik signallarini qaytardi. Natija: recall@1 0.747 dan 0.948 ga, MRR 0.838 dan 0.974 ga (+0.136, SE 0.015 — sezilarli). Raqamli savollarda MRR 0.741 dan 1.000 ga, parafrazda 0.771 dan 0.922 ga ko'tarildi; 58 savolda 1-o'rin tuzaldi, birortasida ham buzilmadi. 4-bo'lim chegarani ko'rsatadi: agar 1-bosqichdan faqat 3 nomzod olsak, shift 0.906 — qolgan savollarni hech qanday reranker tuzata olmaydi; 10 nomzod bu korpusda yetarli. Diqqat: bolim_mos kabi xususiyatlar domen bilimiga (bo'limlar ro'yxati) tayanadi — haqiqiy tizimda bu vazifani cross-encoder yoki LLM-reranker bajaradi, lekin baholash sxemasi (nomzodlar, GroupKFold, juftlashgan farq) aynan shunday qoladi. Bog'liq bo'lim: 2.7.
Misol 4 — k ni tanlash, answer-in-context va xatolar tahlili
"""k ni tanlash: answer-in-context, shovqin, xarajat, oddiy o'quvchi va xatolar tahlili."""
import math
import re
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
BOLIMLAR = ["moliya", "marketing", "kadrlar", "axborot", "logistika", "savdo",
"yuridik", "omborxona"]
# (rasmiy atama - hujjatlarda, so'zlashuv atamasi - foydalanuvchi savollarida)
SINONIM = {
"tatil": ("ta'til", "dam olish"), "xodim": ("xodim", "ishchi"),
"ariza": ("ariza", "so'rovnoma"), "rahbar": ("rahbar", "boshliq"),
"maosh": ("maosh", "ish haqi"), "masofa": ("masofaviy", "uydan"),
"safar": ("xizmat safari", "komandirovka"),
"kasal": ("kasallik varaqasi", "bemorlik ma'lumotnomasi"),
"parol": ("parol", "maxfiy kod"), "noutbuk": ("noutbuk", "kompyuter"),
"malaka": ("malaka oshirish", "o'qish kursi"), "tushlik": ("tushlik", "ovqat"),
"avtobus": ("xizmat avtobusi", "transport"), "sugurta": ("sug'urta", "polis"),
"forma": ("forma", "ish kiyimi"), "ruxsat": ("ruxsat", "izn"),
}
# (hujjat shabloni, javob, raqamli savol, parafraz savol)
MAVZULAR = [
("{b} bo'limi {xodim}lari uchun yillik {tatil} tartibi. Har bir {xodim}ga yiliga {n} kunlik {tatil} beriladi. {ariza} {rahbar}ga ikki hafta oldin topshiriladi.",
"{n} kunlik", "Qaysi bo'limda yiliga {n} kunlik {tatil} beriladi?",
"{b} bo'limida {xodim} {tatil}ga necha kunga chiqadi?"),
("{b} bo'limida {maosh} har oyning {n}-kunida kartaga o'tkaziladi. {maosh} kechiksa {xodim} {rahbar}ga yozma murojaat qiladi.",
"{n}-kunida", "Qaysi bo'limda {maosh} oyning {n}-kunida o'tkaziladi?",
"{b} bo'limida {maosh} qachon tushadi?"),
("{b} bo'limi {xodim}lari haftada {n} soat {masofa} ishlashi mumkin. {masofa} ish uchun {rahbar} {ruxsat}i talab qilinadi.",
"{n} soat", "Qaysi bo'limda haftada {n} soat {masofa} ishlash mumkin?",
"{b} bo'limida qancha vaqt {masofa} faoliyat yuritsa bo'ladi?"),
("{b} bo'limi {xodim}lari {safar}ga chiqqanda kunlik xarajat {n} ming so'm qilib belgilangan. {safar} hisoboti uch kunda topshiriladi.",
"{n} ming so'm", "Qaysi bo'limda {safar}da kunlik xarajat {n} ming so'm?",
"{b} bo'limida {safar} paytida sutkalik pul qancha?"),
("{b} bo'limida {kasal} {n} kun ichida kadrlar xizmatiga topshirilishi shart. {kasal} bo'lmasa kun ishsiz hisoblanadi.",
"{n} kun ichida", "Qaysi bo'limda {kasal} {n} kun ichida topshiriladi?",
"{b} bo'limida {kasal}ni qachongacha olib kelish kerak?"),
("{b} bo'limi tizimlarida {parol} kamida {n} belgidan iborat bo'ladi. {parol} har uch oyda yangilanadi.",
"{n} belgidan", "Qaysi bo'limda {parol} kamida {n} belgidan iborat?",
"{b} bo'limida {parol} uzunligi qanday bo'lishi lozim?"),
("{b} bo'limi {xodim}lariga berilgan {noutbuk} har {n} yilda almashtiriladi. Buzilgan {noutbuk} haqida {rahbar}ga xabar beriladi.",
"{n} yilda", "Qaysi bo'limda {noutbuk} har {n} yilda almashtiriladi?",
"{b} bo'limida {noutbuk} qachon yangisiga o'zgartiriladi?"),
("{b} bo'limi {xodim}lari yiliga kamida {n} soat {malaka}dan o'tadi. {malaka} xarajatini kompaniya to'laydi.",
"{n} soat", "Qaysi bo'limda yiliga {n} soat {malaka} talab qilinadi?",
"{b} bo'limida {malaka} davomiyligi qancha?"),
("{b} bo'limida {tushlik} uchun har kuni {n} ming so'm kompensatsiya beriladi. {tushlik} tanaffusi bir soat davom etadi.",
"{n} ming so'm", "Qaysi bo'limda {tushlik} uchun {n} ming so'm beriladi?",
"{b} bo'limida {tushlik} pulining qancha qismi qoplanadi?"),
("{b} bo'limi {xodim}lari uchun {avtobus} ertalab {n} daqiqa oralig'ida qatnaydi. {avtobus}dan foydalanish bepul.",
"{n} daqiqa", "Qaysi bo'limda {avtobus} {n} daqiqa oralig'ida qatnaydi?",
"{b} bo'limida {avtobus} qanchalik tez-tez keladi?"),
("{b} bo'limi {xodim}lari tibbiy {sugurta} bilan ta'minlanadi. {sugurta} qoplamasi {n} million so'mgacha.",
"{n} million", "Qaysi bo'limda {sugurta} qoplamasi {n} million so'mgacha?",
"{b} bo'limida {sugurta} davolanishni qancha summagacha yopadi?"),
("{b} bo'limida {forma} har {n} oyda bir marta beriladi. {forma}ni yo'qotgan {xodim} uni o'zi qoplaydi.",
"{n} oyda", "Qaysi bo'limda {forma} har {n} oyda beriladi?",
"{b} bo'limida yangi {forma} qanchalik tez-tez tarqatiladi?"),
]
def bosh_harf(matn):
return re.sub(r"(^|\. )([a-z])", lambda m: m.group(1) + m.group(2).upper(), matn)
def korpus(seed=0, n_fon=30):
"""96 chunk (12 mavzu x 8 bo'lim), 288 savol (3 tur) va belgisiz fon matnlar."""
rng = np.random.default_rng(seed)
rasmiy = {k: v[0] for k, v in SINONIM.items()}
ogzaki = {k: v[1] for k, v in SINONIM.items()}
chunklar, savollar, fon = [], [], []
for m, (shablon, javob, raqam, parafraz) in enumerate(MAVZULAR):
sonlar = rng.choice(np.arange(3, 29), len(BOLIMLAR), replace=False)
for j, b in enumerate(BOLIMLAR):
n = int(sonlar[j])
kod = f"{'ABCDEFGHKLMN'[m]}{'KLMNPRST'[j]}-{int(rng.integers(1000, 9999))}"
i = len(chunklar)
chunklar.append({"id": i, "mavzu": m, "bolim": b, "javob": javob.format(n=n),
"matn": bosh_harf(f"Hujjat {kod}. "
+ shablon.format(b=b.capitalize(), n=n, **rasmiy))})
savollar.append({"q": bosh_harf(parafraz.format(b=b.capitalize(), **ogzaki)),
"tur": "parafraz", "gold": i})
savollar.append({"q": f"{kod} bo'yicha qoida nima?", "tur": "kod", "gold": i})
savollar.append({"q": bosh_harf(raqam.format(n=n, **rasmiy)), "tur": "raqam", "gold": i})
for _ in range(n_fon): # fon: har eslatmada atama varianti tasodifiy
f = re.sub(r"\{(\w+)\}", lambda mm: SINONIM[mm.group(1)][int(rng.integers(0, 2))]
if mm.group(1) in SINONIM else "{" + mm.group(1) + "}", shablon)
fon.append(f.format(b="kompaniya", n=int(rng.integers(3, 29))))
return chunklar, savollar, fon
def tokenlar(matn):
"""Kichik harf, so'z/kod tokenlari; so'zlar 6 belgigacha kesiladi (oddiy o'zak)."""
t = re.findall(r"[a-z0-9'\-]+", matn.lower())
return [w if re.search(r"\d", w) else w[:6] for w in t if w.strip("-'")]
class BM25:
def __init__(self, hujjatlar, k1=1.5, b=0.75):
self.docs = [tokenlar(h) for h in hujjatlar]
self.N, self.k1, self.b = len(self.docs), k1, b
self.avgdl = np.mean([len(d) for d in self.docs])
df = {}
for d in self.docs:
for w in set(d):
df[w] = df.get(w, 0) + 1
self.idf = {w: math.log(1 + (self.N - c + 0.5) / (c + 0.5)) for w, c in df.items()}
self.tf = [{w: d.count(w) for w in set(d)} for d in self.docs]
def ball(self, savol):
s = np.zeros(self.N)
for w in tokenlar(savol):
for i, (tf, d) in enumerate(zip(self.tf, self.docs)):
if w in tf:
norm = self.k1 * (1 - self.b + self.b * len(d) / self.avgdl)
s[i] += self.idf[w] * tf[w] * (self.k1 + 1) / (tf[w] + norm)
return s
class LSA:
"""TF-IDF + TruncatedSVD; fon matnlar faqat 'embedding' ni o'rgatishga ketadi."""
def __init__(self, hujjatlar, fon=(), k=12, seed=0):
self.vek = TfidfVectorizer(analyzer=tokenlar, sublinear_tf=True)
X = self.vek.fit_transform(list(fon) + list(hujjatlar))
self.svd = TruncatedSVD(k, random_state=seed).fit(X)
Z = self.svd.transform(self.vek.transform(hujjatlar))
self.Z = Z / np.linalg.norm(Z, axis=1, keepdims=True)
def ball(self, savol):
z = self.svd.transform(self.vek.transform([savol]))[0]
return self.Z @ (z / (np.linalg.norm(z) + 1e-12))
def rrf(*balllar, k=60, n=30):
"""Reciprocal Rank Fusion: har ro'yxatdan top-n nomzod (ball > 0)."""
s = np.zeros(len(balllar[0]))
for r in balllar:
tartib = np.argsort(-r, kind="stable")[:n]
tartib = tartib[r[tartib] > 0]
s[tartib] += 1.0 / (k + np.arange(1, len(tartib) + 1))
return s
NARX_1K_TOKEN = 0.003 # FARAZIY narx (1000 kirish tokeni uchun) - haqiqiy narx emas
TOKEN_SOZ = 1.6 # FARAZIY: o'zbekcha so'zga o'rtacha token (25.2-darsdagidek o'lchang)
def oquvchi(savol, kontekst, matnlar):
"""Oddiy ekstraktiv o'quvchi (LLM EMAS): so'z mosligi eng ko'p chunkdan javob oladi."""
q = set(tokenlar(savol))
ball = [len(q & set(tokenlar(matnlar[c]))) for c in kontekst]
return kontekst[int(np.argmax(ball))] # tenglikda - retrieval tartibi
def main() -> None:
chunklar, savollar, fon = korpus()
matnlar = [c["matn"] for c in chunklar]
bm, lsa = BM25(matnlar), LSA(matnlar, fon=fon, k=12)
tartiblar = [np.argsort(-rrf(bm.ball(s["q"]), lsa.ball(s["q"])), kind="stable")
for s in savollar]
tur = np.array([s["tur"] for s in savollar])
uzunlik = np.array([len(m.split()) for m in matnlar])
K = (1, 2, 3, 5, 8, 10)
print("=== 1. k bo'yicha: kontekstda javob bormi va u qanchaga tushadi (RRF) ===")
print(" k oltin_kontekstda javob_satri faqat_raqam shovqin token/so'rov narx/1000")
oltin, raqam = {}, {}
for k in K:
o, satr, r, tok = [], [], [], []
for s, t in zip(savollar, tartiblar):
top = t[:k]
o.append(s["gold"] in top)
satr.append(any(chunklar[s["gold"]]["javob"] in matnlar[c] for c in top))
son = re.match(r"\d+", chunklar[s["gold"]]["javob"]).group()
r.append(any(son in re.findall(r"\d+", matnlar[c]) for c in top))
tok.append(uzunlik[top].sum() * TOKEN_SOZ)
oltin[k], raqam[k] = np.array(o), np.array(r)
shovqin = 1 - oltin[k].mean() / k
narx = np.mean(tok) * NARX_1K_TOKEN # 1000 so'rov: tok * 1000 / 1000 * narx
print(f" {k:>2} {oltin[k].mean():>17.3f} {np.mean(satr):>12.3f} {raqam[k].mean():>12.3f} "
f"{shovqin:>8.2f} {np.mean(tok):>13.0f} {narx:>10.2f}")
for k in (1, 8):
print(f" k={k:<2}: faqat raqamni qidirish {int((raqam[k] & ~oltin[k]).sum())} ta soxta "
f"'topildi' beradi (oltin chunk kontekstda yo'q)")
print(" shovqin = kontekstdagi keraksiz chunklar ulushi; narx - FARAZIY birlikda")
print("\n=== 2. Ko'proq kontekst - ko'proq chalg'ituvchi: oddiy o'quvchi ===")
print(" k o'quvchi aniqligi parafraz kod raqam")
aniq = {}
for k in K:
aniq[k] = np.array([chunklar[oquvchi(s["q"], list(t[:k]), matnlar)]["javob"]
== chunklar[s["gold"]]["javob"] for s, t in zip(savollar, tartiblar)])
print(f" {k:>2} {aniq[k].mean():>19.3f} " + " ".join(
f"{aniq[k][tur == x].mean():>8.3f}" for x in ("parafraz", "kod", "raqam")))
eng_k = max(K, key=lambda k: aniq[k].mean())
for k in K:
d = aniq[eng_k].astype(float) - aniq[k].astype(float)
se = d.std(ddof=1) / math.sqrt(len(d))
if d.mean() <= 2 * se:
print(f" eng yaxshi k = {eng_k} ({aniq[eng_k].mean():.3f}); undan sezilarli "
f"yomon bo'lmagan eng kichik k = {k} ({aniq[k].mean():.3f})")
break
m = tur == "parafraz"
d = aniq[10][m].astype(float) - aniq[1][m].astype(float)
se = d.std(ddof=1) / math.sqrt(m.sum())
chalg = int((oltin[10][m] & ~aniq[10][m]).sum())
print(f" parafraz: k=10 - k=1 = {d.mean():+.3f} (SE {se:.3f}); k=10 da oltin kontekstda, "
f"lekin o'quvchi chalg'idi: {chalg} savol")
print("\n=== 3. Xatolar tahlili: oltin chunk top-3 da yo'q ===")
xato = [(s, t) for s, t in zip(savollar, tartiblar) if s["gold"] not in t[:3]]
print(f" xatolar: {len(xato)} / {len(savollar)}")
toifa = {}
for s, t in xato:
g, b = chunklar[s["gold"]], chunklar[int(t[0])]
if g["mavzu"] == b["mavzu"]:
nom = "mavzu to'g'ri, bo'lim xato"
elif g["bolim"] == b["bolim"]:
nom = "bo'lim to'g'ri, mavzu xato"
else:
nom = "ikkalasi xato"
joy = "4-10" if int(np.where(t == s["gold"])[0][0]) < 10 else ">10"
toifa[(s["tur"], nom, joy)] = toifa.get((s["tur"], nom, joy), 0) + 1
for (t_, nom, joy), n in sorted(toifa.items(), key=lambda x: (-x[1], x[0])):
print(f" {n:>3} [{t_}] 1-o'rinda: {nom}; oltin o'rni {joy}")
for tanlov in ("parafraz", "raqam"):
s, t = next((s, t) for s, t in xato if s["tur"] == tanlov)
print(f" namuna [{tanlov}]: {s['q']}")
print(f" 1-o'rin: {matnlar[int(t[0])][16:95]}...")
print(f" oltin : {matnlar[s['gold']][16:95]}...")
print(" ⭐ k - sifat, shovqin va narx orasidagi murosa; uni o'lchab tanlang")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. k bo'yicha: kontekstda javob bormi va u qanchaga tushadi (RRF) ===
k oltin_kontekstda javob_satri faqat_raqam shovqin token/so'rov narx/1000
1 0.750 0.750 0.757 0.25 28 0.09
2 0.882 0.882 0.889 0.56 56 0.17
3 0.906 0.906 0.906 0.70 84 0.25
5 0.948 0.948 0.951 0.81 140 0.42
8 0.972 0.972 0.979 0.88 223 0.67
10 1.000 1.000 1.000 0.90 280 0.84
k=1 : faqat raqamni qidirish 2 ta soxta 'topildi' beradi (oltin chunk kontekstda yo'q)
k=8 : faqat raqamni qidirish 2 ta soxta 'topildi' beradi (oltin chunk kontekstda yo'q)
shovqin = kontekstdagi keraksiz chunklar ulushi; narx - FARAZIY birlikda
=== 2. Ko'proq kontekst - ko'proq chalg'ituvchi: oddiy o'quvchi ===
k o'quvchi aniqligi parafraz kod raqam
1 0.750 0.688 1.000 0.562
2 0.844 0.667 1.000 0.865
3 0.858 0.656 1.000 0.917
5 0.882 0.656 1.000 0.990
8 0.885 0.656 1.000 1.000
10 0.885 0.656 1.000 1.000
eng yaxshi k = 8 0.885-bob; undan sezilarli yomon bo'lmagan eng kichik k = 5 0.882-bob
parafraz: k=10 - k=1 = -0.031 (SE 0.018); k=10 da oltin kontekstda, lekin o'quvchi chalg'idi: 33 savol
=== 3. Xatolar tahlili: oltin chunk top-3 da yo'q ===
xatolar: 27 / 288
12 [parafraz] 1-o'rinda: ikkalasi xato; oltin o'rni 4-10
8 [raqam] 1-o'rinda: mavzu to'g'ri, bo'lim xato; oltin o'rni 4-10
7 [parafraz] 1-o'rinda: bo'lim to'g'ri, mavzu xato; oltin o'rni 4-10
namuna [parafraz]: Marketing bo'limida ish haqi qachon tushadi?
1-o'rin: Marketing bo'limi xodimlari haftada 25 soat masofaviy ishlashi mumkin. Masofavi...
oltin : Marketing bo'limida maosh har oyning 27-kunida kartaga o'tkaziladi. Maosh kechi...
namuna [raqam]: Qaysi bo'limda yiliga 28 kunlik ta'til beriladi?
1-o'rin: Axborot bo'limi xodimlari uchun yillik ta'til tartibi. Har bir xodimga yiliga 1...
oltin : Kadrlar bo'limi xodimlari uchun yillik ta'til tartibi. Har bir xodimga yiliga 2...
⭐ k - sifat, shovqin va narx orasidagi murosa; uni o'lchab tanlangNima ko'rsatdi: 1-bo'limda k oshgan sari oltin chunk kontekstga tushish ulushi 0.750 (k = 1) dan 0.906 (k = 3), 0.948 (k = 5) va 1.000 (k = 10) ga o'sdi, lekin shovqin ham 0.25 dan 0.90 ga, kirish tokenlari esa so'rovga 28 dan 280 ga ko'paydi (narx — faraziy birlikda, lekin k ga proporsional ekani haqiqiy). Bu korpusda javob satrlari noyob, shuning uchun "javob satri kontekstda" ustuni oltin ustun bilan bir xil; faqat raqamni qidiradigan bo'sh tekshiruv esa k = 1 va k = 8 da 2 tadan soxta "topildi" berdi. 2-bo'lim — kontekstdan javob oluvchi oddiy o'quvchi (LLM emas, so'z mosligi qoidasi): aniqlik k = 1 da 0.750, k = 5 da 0.882, k = 8 va 10 da 0.885 — ya'ni k = 5 dan keyin faqat narx o'sadi. Qaror qoidasi bo'yicha eng kichik yetarli k = 5. Parafrazli savollarda esa aksincha: kontekst kengaygani sari aniqlik 0.688 dan 0.656 ga tushdi — k = 10 da oltin chunk kontekstda bo'lsa ham o'quvchi 33 savolda boshqa bo'limning o'xshash qoidasini tanladi. Farq (-0.031, SE 0.018) 2·SE chegarasidan biroz kichik — bu to'plamda sezilarli emas, lekin yo'nalishi "ko'proq kontekst — ko'proq chalg'ituvchi" g'oyasiga mos; haqiqiy LLM da bu ta'sirning kattaligini o'z tizimingizda o'lchash kerak. 3-bo'lim — 27 ta xato toifalandi: 12 ta parafrazli savolda 1-o'rinda boshqa bo'lim va boshqa mavzu turibdi (tekshirilganda — ish haqi va ish kiyimi dagi umumiy ish so'zi va uydan savolni noto'g'ri mavzuga tortgan, 1-o'rinda esa deyarli doim Marketing bo'limining ikki chunki: ko'p savolga "yaqin" chiqadigan markaz chunklar), 8 ta raqamli savolda mavzu to'g'ri, bo'lim xato (raqam 28 ni LSA ko'rmaydi), 7 ta parafrazda bo'lim to'g'ri, mavzu xato (namuna: Marketing bo'limida ish haqi qachon tushadi? — o'sha bo'limning masofaviy ish qoidasi 1-o'rinda). Barcha xatolarda oltin chunk 4-10 o'rinlarda — ya'ni ularni reranker (3-misol) tuzata oladi. Bog'liq bo'limlar: 2.8, 2.9.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "RAG ni yakuniy javoblar bo'yicha baholash yetarli" | Retrievalni alohida o'lchang: kontekstda yo'q javobni LLM tuzata olmaydi |
| "Umumiy MRR yuqori — qidiruv yaxshi" | 1-misolda umumiy MRR 0.736, parafrazda 0.209 |
| "Embedding qidiruvi BM25 dan har doim yaxshi" | 2-misolda LSA kod va raqamda BM25 dan ancha yomon (0.478 va 1.000) |
| "Embedding sinonimni o'zi biladi" | Faqat o'rgatish korpusida ko'rgan bo'lsa: fonsiz cos 0.00, fon bilan 1.00 |
| "Gibrid har doim ikkalasidan yaxshi" | RRF raqamli savollarda BM25 dan sezilarli yomon (-0.259, SE 0.031) |
| "Reranker har qanday xatoni tuzatadi" | Faqat 1-bosqich nomzodlari ichida: shift = recall@N |
| "k qancha katta bo'lsa, shuncha yaxshi" | 4-misolda k = 5 dan keyin aniqlik o'smadi, token 2 barobar oshdi |
| "Kontekstda javob bor — demak javob to'g'ri" | k = 10 da recall 1.000, o'quvchi aniqligi 0.885 |
| "Precision@k — asosiy RAG metrikasi" | Bitta relevant chunkda u shunchaki recall@k / k |
6. Keng tarqalgan xatolar va yechimlari
1. Tenglikda optimistik o'rin
orin = int((ball > ball[oltin]).sum()) + 1 # ⚠️ tenglar oldinda
orin = int((ball >= ball[oltin]).sum()) # ✅ eng yomon holat2. Savol turlarisiz o'rtacha
print("MRR", np.mean(rr)) # ⚠️
for t in turlar: print(t, np.mean(rr[tur == t])) # ✅3. RRF da ball 0 bo'lgan hujjatlar ham o'rin oladi
tartib = np.argsort(-ball) # ⚠️ 0 lar ham "reyting"da
tartib = np.argsort(-ball)[:n]; tartib = tartib[ball[tartib] > 0] # ✅4. Reranker ni o'sha chunklarning savollarida baholash
LogisticRegression().fit(X, y); ehtimol = model.predict_proba(X) # ⚠️ oqib ketish
for oq, ts in GroupKFold(4).split(X, y, oltin_chunk): ... # ✅5. alfa va k ni test to'plamida tanlash
alfa = max(alfalar, key=lambda a: mrr(test, a)) # ⚠️
alfa = max(alfalar, key=lambda a: mrr(dev, a)); mrr(test, alfa) # ✅6. Bo'sh answer-in-context tekshiruvi
topildi = "28" in kontekst # ⚠️ soxta moslik
topildi = oltin_id in top_k or javob_satri in kontekst # ✅7. Mustaqil (juftlashmagan) taqqoslash
se = math.sqrt(a.var() / n + b.var() / n) # ⚠️ savollar bir xil
d = a - b; se = d.std(ddof=1) / math.sqrt(n) # ✅ juftlashgan7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 18-qism (o'tilgan): juftlashgan taqqoslash, SE, GroupKFold, oqib ketish
- 23.4-dars (o'tilgan): TF-IDF — BM25 ning yaqin qarindoshi; 23.3 — subword tokenizatsiya (o'zak kesish o'rniga)
- 25.7-dars (o'tilgan): embeddinglar va kosinus o'xshashlik; 25.8 — chunking va indeks (chunk hajmi ham recall@k ga ta'sir qiladi)
- Keyingi darslar: LLM ni baholash — yakuniy javob sifati, LLM-as-judge, bootstrap va regressiya testlari; Agentlar va tool calling — qidiruv agentning bitta "asbobi" sifatida; Amaliyot — to'liq RAG tizimi
8. Eng yaxshi amaliyotlar
RAG ni ikki qavatda baholang: avval retrieval (recall@k), keyin javob sifati.
Baholash to'plamida savol turlarini belgilang va har turni alohida hisobot qiling.
Parafrazli, kodli va raqamli savollarni ataylab qo'shing — hujjat so'zlari bilan yozilgan savollar juda oson.
Tenglikda optimizmsiz o'rin hisoblang.
Gibridni birlashtirish usuli bilan birga tekshiring (RRF, vaznli); parametrlarni dev da tanlang.
Reranker dan oldin 1-bosqich recall@N ni o'lchang.
k ni aniqlik, shovqin va narx bo'yicha tanlang: eng yaxshisidan sezilarli yomon bo'lmagan eng kichik k.
Xatolarni toifalang: savol turi, 1-o'rindagi chunk, oltin chunkning o'rni.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
1. # tartib [d5, d2, d8], relevant {d2}: recall@1, recall@3, RR?
2. # bitta relevant chunkda precision@5 va recall@5 qanday bog'langan?
3. # nDCG qachon MRR dan ko'proq ma'lumot beradi?
4. # nega BM25 "AK-7564" ni ajoyib topadi, LSA(12) esa yo'q?
5. # LSA "dam olish" ni "ta'til" ga yaqin deb bilishi uchun nima kerak?
6. # RRF formulasi?
7. # nega RRF raqamli savollarda BM25 dan yomon bo'lishi mumkin?
8. # reranker ning sifat chegarasi nima?
9. # k 3 dan 10 ga oshsa, qaysi uch narsa o'zgaradi?
10. # "kontekstda '28' raqami bor" tekshiruvining xavfi?
11. # reranker ni baholashda nega GroupKFold?
12. # tenglikda oltin chunkning o'rni qanday hisoblanadi?Javoblar
- recall@1 = 0, recall@3 = 1, RR = 1/2
- precision@5 = recall@5 / 5
- Relevantlik darajali bo'lsa (aynan shu / qisman foydali) yoki bir nechta relevant chunk bo'lsa
- Kod bitta hujjatda — IDF katta; 12 o'lchovli SVD noyob tokenni deyarli yo'qotadi
- Ikkala atama o'xshash kontekstda uchraydigan o'rgatish (fon) korpusi
ball(d) = sum 1 / (60 + o'rin_r(d))barcha retrieverlar bo'yicha- U faqat o'rinlarni ko'radi: BM25 ning katta ball farqi yo'qoladi, raqamni ko'rmaydigan retriever boshqa hujjatni tepaga chiqaradi
- 1-bosqichning recall@N (nomzodlar ichida oltin chunk bormi)
- recall@k (o'sadi), shovqin (o'sadi), token/narx (o'sadi)
- Raqam boshqa chunkda tasodifan bo'lishi mumkin — soxta "topildi"
- Bir chunkning savollari o'rgatishda ham, baholashda ham bo'lmasligi uchun (oqib ketish)
(ball >= ball[oltin]).sum()— tenglar oltin chunkdan oldinda deb
Vazifa 2: Xatolarni tuzating
1. orin = list(np.argsort(-ball)).index(oltin) + 1 # ball ko'p 0 ga teng
2. mrr = np.mean([1 / orin(bm25, s) for s in savollar])
print("BM25 yaxshi" if mrr > 0.7 else "yomon")
3. model = LogisticRegression().fit(X, y)
print(recall_at_1(model.predict_proba(X)))
4. for k in range(1, 11):
natija[k] = recall_k(test, k)
k_tanlov = max(natija, key=natija.get)
5. topildi = any(str(son) in kontekst for son in savol_raqamlari)Javoblar
1. orin = int((ball >= ball[oltin]).sum()) # optimizmsiz
2. for t in ("parafraz", "kod", "raqam"): # turi bo'yicha
m = tur == t
print(t, np.mean(rr[m]))
3. for oq, ts in GroupKFold(4).split(X, y, oltin_chunk): # chunk bo'yicha ajratish
model = LogisticRegression().fit(X[oq], y[oq])
ehtimol[ts] = model.predict_proba(X[ts])[:, 1]
4. # recall@k k bilan doim o'sadi - maqsad metrika yakuniy aniqlik + narx,
# tanlov dev da: eng yaxshisidan sezilarli yomon bo'lmagan ENG KICHIK k
for k in K:
d = aniq[eng_k] - aniq[k]
if d.mean() <= 2 * d.std(ddof=1) / math.sqrt(len(d)):
k_tanlov = k
break
5. topildi = oltin_id in top_k or javob_satri in kontekst # qat'iy tekshiruvVazifa 3: Metrikalar va to'plam
Modellang:
- recall@k, precision@k, RR, nDCG — noldan, qo'lda tekshiring
- Uch turdagi savollar bilan baholash to'plami
- Javob satri oltin chunkda ekanini tekshirish
- Tur bo'yicha hisobot
Vazifa 4: Retrieverlar
Modellang:
- BM25 noldan
- LSA fonsiz va fon bilan; sinonim kosinusi
- RRF va vaznli birlashtirish
- Turlar bo'yicha juftlashgan farq va qaror
Vazifa 5: Reranker
Modellang:
- 1-bosqich top-N va shift
- Juftlik xususiyatlari
- GroupKFold bilan LogReg
- Tuzalgan va buzilgan 1-o'rinlar
Vazifa 6: k va xatolar
Modellang:
- k bo'yicha recall, shovqin, token, faraziy narx
- Oddiy o'quvchi aniqligi
- Eng kichik yetarli k
- Xatolarni toifalash
Vazifa 7: O'ylash
Jamoa rahbari aytdi: "Biz BM25 dan zamonaviy embedding qidiruviga o'tdik. Sinov savollarimizning 20 tasidan 19 tasida to'g'ri hujjat birinchi o'rinda — demak yangi qidiruv yaxshi, eski kodni o'chiramiz." Siz nima deysiz?
Javob
Qisqa javob: 20 ta savol va bitta umumiy son — qaror uchun yetarli emas. Avval savollar qayerdan olinganini, turlar bo'yicha natijani va BM25 bilan juftlashgan farqni ko'rish kerak.
1. Savollar qanday yozilgan. Agar ular hujjatni o'qib, uning so'zlari bilan yozilgan bo'lsa, har qanday qidiruv yaxshi ko'rinadi. Foydalanuvchilar so'zlashuv tilida so'raydi — parafrazli savollar kerak.
2. Turlar bo'yicha. 2-misolda dense (LSA) parafrazda BM25 dan yaxshi, lekin kod (0.478 va 1.000) va raqamli savollarda ancha yomon. Agar sinov to'plamida kodli savollar bo'lmasa, bu yiqilish ko'rinmaydi — ichki tizimlarda esa "AK-7564 buyrug'i" kabi so'rovlar ko'p.
3. Statistika. 20 savolda 19 to'g'ri — ishonch oralig'i keng (taxminan 0.75–0.99). Eski va yangi usulni bir xil savollarda juftlashgan farq va SE bilan solishtirish kerak.
4. O'chirish o'rniga birlashtirish. 2-misolda eng yaxshi natija gibridda (vaznli: umumiy MRR 0.970), 3-misolda esa reranker bilan (0.974). BM25 ni o'chirish — kod va raqamli savollarni yo'qotish demak.
Tavsiya:
# 1. 200+ savol: parafraz, kod/raqam, bir necha chunkli - turlari belgilangan
# 2. BM25, embedding, gibrid - bir xil savollarda, turi bo'yicha recall@k va MRR
# 3. juftlashgan farq + SE; dev da parametr, test da yakuniy baho
# 4. xatolar toifalari - keyingi qadamni tanlash uchunRahbarga javob: "Yangi qidiruv so'zlashuv savollarida yaxshiroq bo'lishi mumkin, lekin kod va raqam bilan so'rovlarda odatda yomonroq. Keling, turlari belgilangan kattaroq to'plamda ikkalasini va ularning gibridini solishtiraylik — ehtimol eng yaxshisi BM25 ni o'chirish emas, birlashtirish."
Nimani mustahkamlaydi: 2.2, 2.3, 2.6, 2.7, 2.8-bo'limlar.
Xulosa
Bu darsda retrieval metrikalarini noldan yozdik, savol turlari belgilangan baholash to'plamini yaratdik, BM25, dense va gibrid qidiruvni turlar bo'yicha taqqosladik, ikki bosqichli reranking qurdik va k ni sifat, shovqin va narx bo'yicha tanladik.
Eng muhim uch fikr:
Retrievalni alohida va savol turlari bo'yicha baholang. 1-misolda BM25 ning umumiy MRR i
0.736, lekin parafrazli savollarda —0.209(recall@10.083), kod va raqamlida esa1.000. RAG da asosiy metrika — recall@k: kerakli chunk kontekstga tushmasa, LLM uni tuzata olmaydi. Tenglikda oltin chunkni optimizmsiz o'ringa qo'ying.Leksik va semantik qidiruv bir-birini to'ldiradi, lekin birlashtirish usuli muhim. 2-misolda LSA sinonimni faqat fon korpus bilan o'rgandi (
cos 0.00→1.00) va kod/raqamda yiqildi. RRF parafrazda BM25 dan+0.562yaxshi, lekin raqamli savollarda-0.259yomon (SE 0.031) — o'rinlarga o'tishda BM25 ning ball farqi yo'qoladi. Vaznli birlashtirish uchala turda eng yaxshi yoki teng (umumiy MRR0.970). 3-misolda LogReg reranker RRF ustidan MRR ni0.838dan0.974ga ko'tardi (SE 0.015), 58 savolni tuzatib, birortasini buzmadi; uning chegarasi — 1-bosqich recall@N.k — murosa, uni o'lchab tanlang. 4-misolda k = 1 dan 10 gacha recall
0.750dan1.000ga, shovqin0.25dan0.90ga, tokenlar 10 barobar o'sdi; oddiy o'quvchining aniqligi k = 5 dan keyin o'smadi (0.882va0.885). Kontekstda javob borligi to'g'ri javobni kafolatlamaydi, "raqam bor" kabi bo'sh tekshiruv esa soxta moslik beradi. Xatolarni toifalash (tur, 1-o'rin, oltin o'rni) keyingi o'zgarishni tanlashga eng tez yo'l.
Keyingi darsda LLM ni baholash: ochiq javobli vazifalarda eval to'plami, avtomatik metrikalar va ularning cheklovlari, LLM-as-judge va uning tarafkashliklari, bootstrap ishonch intervali va regressiya testlari.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!