Baholash — taxmin qilmang, o'lchang
Tizim ishlayapti. Lekin qanchalik yaxshi?
"Yaxshi ishlayapti shekilli" — bu javob emas. O'zgartirish kiritganingizda yaxshilandimi yoki yomonlashdimi, bilmaysiz.
Avval retrieval, keyin generatsiya
Javob yomon bo'lsa, ikki sabab bor:
- Kerakli chunk topilmadi — LLM qanchalik yaxshi bo'lsa ham yo'q ma'lumotdan javob yasay olmaydi
- Chunk topildi, lekin LLM yomon javob yozdi
Birinchisini birinchi tekshiring. Ko'pchilik xatosi: javob yomon bo'lganda darhol promptni o'zgartirishga kirishadi. Agar muammo qidiruvda bo'lsa — prompt hech narsani o'zgartirmaydi.
Gold set
Sizga savol -> to'g'ri chunk juftliklari kerak. Qo'lda tayyorlanadi.
gold = { "Nihol narxi qancha?": {"c_narx_1", "c_narx_2"}, "Yetkazib berish necha kun?": {"c_yetkazish_1"}, "Kafolat muddati?": {"c_kafolat_1"}, "Qanday to'lash mumkin?": {"c_tolov_1"}, "Ofis qayerda?": {"c_manzil_1"}, }
Nechta savol kerak:
- 20-30 ta — birinchi baholash uchun
- 50-100 ta — ishonchli taqqoslash uchun
- Har bir mavzudan kamida 3-5 ta
Bir marta tayyorlaysiz, keyin har bir o'zgarishda qayta ishlatasiz.
To'rtta metrika
import numpy as np def hit_at_k(natija, gold_ids, k): """Top-k da kamida BITTA to'g'ri chunk bormi (0 yoki 1).""" return 1.0 if set(natija[:k]) & gold_ids else 0.0 def recall_at_k(natija, gold_ids, k): """Top-k da to'g'ri chunklarning ULUSHI.""" return len(set(natija[:k]) & gold_ids) / len(gold_ids) def mrr(natija, gold_ids): """Birinchi to'g'ri chunk qaysi o'rinda.""" for i, chunk in enumerate(natija, 1): if chunk in gold_ids: return 1.0 / i return 0.0 def ndcg_at_k(natija, gold_ids, k): """Tartibni hisobga olgan sifat.""" dcg = sum((1.0 if c in gold_ids else 0.0) / np.log2(i + 1) for i, c in enumerate(natija[:k], 1)) ideal = sum(1.0 / np.log2(i + 1) for i in range(1, min(len(gold_ids), k) + 1)) return dcg / ideal if ideal else 0.0
Nega Hit@k yetarli emas
Ikkita qidiruvni solishtiramiz. Ikkalasi ham kerakli chunkni topadi — lekin biri uni 1-o'ringa, ikkinchisi 5-o'ringa qo'yadi.
Metrika zaif kuchli ----------------------------- Recall@5 1.000 1.000 Hit@5 1.000 1.000 <- BIR XIL ko'rinadi MRR 0.200 1.000 <- FARQ shu yerda NDCG@5 0.387 1.000
Bu raqamlar quyidagi holatdan: to'g'ri chunk zaif da 5-o'rinda,
kuchli da 1-o'rinda. MRR = 1/5 = 0.200.
Hit@5 va Recall@5 ikkalasida bir xil. Faqat Hit@k ni
o'lchasangiz — ikki tizim bir xil ko'rinadi.
Lekin ular bir xil emas. Tartib muhim, chunki:
- LLM ga top-3 yoki top-5 beriladi. Chunk 5-o'rinda bo'lsa, kontekst oxirida qoladi — LLM lar o'rtadagi ma'lumotni yomonroq ishlatadi ("lost in the middle")
top_kni 3 ga tushirsangiz — chunk butunlay yo'qoladi
Xulosa: MRR va NDCG ishlating. Ular tartibni ko'radi.
Baholash funksiyasi
def baholash(natijalar, gold, nom=""): hisobot = {} for k in (1, 3, 5): hisobot[f"Recall@{k}"] = np.mean( [recall_at_k(natijalar[q], g, k) for q, g in gold.items()]) hisobot[f"Hit@{k}"] = np.mean( [hit_at_k(natijalar[q], g, k) for q, g in gold.items()]) hisobot["MRR"] = np.mean( [mrr(natijalar[q], g) for q, g in gold.items()]) hisobot["NDCG@5"] = np.mean( [ndcg_at_k(natijalar[q], g, 5) for q, g in gold.items()]) if nom: print(f"\n{nom}") for k, v in hisobot.items(): print(f" {k:<10} {v:.3f}") return hisobot
Haqiqiy bazada ishlatish
from app.database import SessionLocal from app.services.search import search_chunks db = SessionLocal() natijalar = { savol: [c["id"] for c in search_chunks(db, savol, top_k=10)] for savol in gold } db.close() baholash(natijalar, gold, "Hozirgi tizim")
Nimani yaxshilash kerak
Recall past (< 0.7)?
-> chunking o'lchamini o'zgartiring
-> boshqa embedding modeli sinang
-> hybrid search qo'shing
Recall yaxshi, MRR past?
-> RERANKER qo'shing (12-bob)
-> aynan shu muammoni hal qiladi
Ikkalasi ham yaxshi, javob yomon?
-> endi PROMPT ustida ishlang (13-bob)
Tartib muhim. Prompt — oxirgi bosqich, birinchi emas.
Reranker foydasini o'lchash
12-bobda aytilgan edi: reranker foydasi Recall@5 da ko'rinmaydi.
oldin = baholash(vektor_natijalari, gold, "Reranker DAN OLDIN") keyin = baholash(rerank_natijalari, gold, "Reranker DAN KEYIN") print(f"\nRecall@5 : {keyin['Recall@5'] - oldin['Recall@5']:+.3f}") print(f"MRR : {keyin['MRR'] - oldin['MRR']:+.3f} <- ASOSIY")
Agar Recall@5 o'zgarmasa va MRR oshmasa — reranker sizga
kerak emas. WDE Nihol botida aynan shunday: reranker yo'q va
tizim yaxshi ishlaydi.
Qaror qabul qilishning to'g'ri yo'li shu: o'lchang, keyin qo'shing. Aksincha emas.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.