অধ্যায় 1 · অ্যাডভান্সড RAG: হাইব্রিড রিট্রিভাল ও রিরাংকিং
রিরাংকিং ও রিট্রিভাল মূল্যায়ন
- পৃষ্ঠা 2 / 22
- 26 মিনিট পড়া
আগের পাতা শেষ হয়েছিল হিট-রেট দিয়ে: ২১টা প্রশ্নের কয়টায় উত্তর শীর্ষ ১, ৩ বা ৫-এ ছিল। সংখ্যাটা ভোঁতা। উত্তর কোথায় বসেছে তা সে বলে না, আংশিক কাজের প্রমাণকে সে গুরুত্ব দেয় না, আর একটা প্রশ্নের ব্যবধানের কোনো মানে আছে কি না তাও সে বলতে পারে না। জাপানের ওসাকার আইকোকে জিজ্ঞেস করা হলো "নতুন রিরাংকার কি কাজে লাগছে?" সে দুভাবে ভুল করতে পারে: একটা সংখ্যা বেড়েছে বলে "হ্যাঁ", অথবা কমেছে বলে "না"। এই পাতা তাকে সেই সরঞ্জাম দেয়, যাতে সে বলতে পারে "এতটা, আর এই পরিমাণ অনিশ্চয়তা নিয়ে"।
পাতায় আসছে ক্রস-এনকোডার ধাঁচের একটা রিরাংকার, রিট্রিভালের চারটা প্রচলিত মেট্রিক আর সঙ্গের সততা-সরঞ্জাম; তারপর আকার ঠিক করা, ডুপ্লিকেট, ডেটার সতেজতা, টেন্যান্টের সীমানা, সাইটেশন যাচাই, অ্যাবলেশন আর চালানো যায় এমন একটা বেঞ্চমার্ক। সব নতুন কোড evidencedesk/retrieval/-এ, আর তা আগের পাতার ক্যাটালগ, লেবেল, সার্চার ও TwoStage আবার ব্যবহার করে। দুটো সতর্কতা। "ডেন্স" এখনো মানে এমবেডিং ও সাদৃশ্য পাতার খেলনা এমবেডার। আর এখানে কোনো আসল ক্রস-এনকোডার চলে না: রিরাংকারগুলো নির্ধারিত স্ট্যান্ড-ইন, যারা শব্দ পড়ে, অর্থ নয়। সংখ্যাগুলো ২৪টা প্রশ্নে পদ্ধতি দেখায়, প্রোডাকশনের কোনো মডেল কী করত তা নয়।
যা শিখবেন
- ক্রস-এনকোডার কীভাবে একটা (কোয়েরি, প্যাসেজ) জোড়াকে স্কোর দেয়, কেন সেই স্কোর সম্ভাবনা নয়, আর রিরাংকার অফলাইনে কীভাবে টেস্ট করবেন।
- Recall@k, precision@k, MRR ও nDCG: সঠিক সংজ্ঞা, এই কোর্স কোন ভ্যারিয়েন্ট ব্যবহার করে, আর প্রতিটা কী দেখতে পায় না।
- ২৪টা লেবেলে একটা পার্থক্য সততার সঙ্গে কীভাবে জানাবেন: ইন্টারভ্যাল, প্রশ্নভিত্তিক ফ্লিপ, টাই ও স্লাইস।
- ক্যান্ডিডেটের সংখ্যা, চূড়ান্ত কনটেক্সটের আকার, ডুপ্লিকেট, সতেজতা ও টেন্যান্ট-ফিল্টার কীভাবে মান ও খরচ বদলায়।
- অ্যাবলেশন কীভাবে চালাবেন, রিট্রিভালের ফল রিলিজ গেটে কীভাবে দেবেন, আর সুপারিশ কীভাবে লিখবেন।
ক্রস-এনকোডার রিরাংকার ব্যবহার করুন
একটা বাই-এনকোডার (এমবেডিং ও সাদৃশ্য-এর খেলনা) কোয়েরি ও প্যাসেজ আলাদাভাবে এমবেড করে ভেক্টর তুলনা করে। একটা ক্রস-এনকোডার কোয়েরি ও একটা প্যাসেজ একসঙ্গে পড়ে জোড়াটার জন্য একটা সংখ্যা দেয়। দাম: প্রতিটা (কোয়েরি, ক্যান্ডিডেট) জোড়ায় একবার মডেল চালানো, আগে থেকে কিছু হিসাব করে রাখা যায় না। তাই সে TwoStage-এর দ্বিতীয় ধাপে বসে: সস্তা প্রথম ধাপ কর্পাসকে কয়েক ডজন ক্যান্ডিডেটে নামায়, ব্যয়বহুল ধাপ শুধু সেগুলো সাজায়।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): sentence-transformers ডকুমেন্টেশন retrieve-and-rerank দেখায়: প্রথম ধাপ যেমন ১০০টা ক্যান্ডিডেট আনে, একটা
CrossEncoderপ্রতিটা জোড়া নতুন করে স্কোর করে, আপনি স্কোর ধরে সাজান। পুরো কর্পাসে খোঁজার জন্য ক্রস-এনকোডারকে সে ভুল সরঞ্জাম বলে (তার উদাহরণ: ১০,০০০ বাক্য, প্রায় ৫ কোটি জোড়া, প্রায় ৬৫ ঘণ্টা; বাই-এনকোডারে প্রায় ৫ সেকেন্ড; হার্ডওয়্যারভেদে বদলায়)। ডকুমেন্টেশন আউটপুটকে "০ থেকে ১-এর মধ্যে" স্কোর বলে, কিন্তুcross-encoder/ms-marco-MiniLM-L6-v2মডেল কার্ড8.607138ও-4.320078-এর মতো কাঁচা (raw) স্কোর ছাপে: এটা র্যাংকিং স্কোর, কখনো সম্ভাবনা নয়। মডেলটির লাইসেন্স Apache-2.0, প্যারামিটার ২২.৭M, ইংরেজি ডেটায় ট্রেন করা, আর প্রথম লোডে ওয়েট ডাউনলোড করে; বাংলায় এর মান দেখানো হয়নি (যাচাই হয়নি)। বর্তমান ডকুমেন্টেশন দেখে নিন।
কিট ইন্টারফেস ঠিক করে আর স্ট্যান্ড-ইন দেয়। PairScorer একটা প্রোটোকল, মেথড একটাই, predict(pairs): ভেতরে (কোয়েরি, প্যাসেজ) জোড়ার তালিকা, বাইরে প্রতি জোড়ায় একটা ফ্লোট; নাম লাইব্রেরির কলের মতো। PairReranker এমন যেকোনো স্কোরারকে Reranker প্রোটোকলে বসায়, যে জোড়া স্কোর করেছে তা গোনে (এটাই খরচ), আর প্যাসেজ max_words শব্দে ছাঁটতে পারে (আসল মডেল সীমিত সংখ্যক টোকেন পড়ে)।
LexicalPairScorer হলো স্ট্যান্ড-ইন: হাতে-ওজন-দেওয়া চারটা বৈশিষ্ট্য (কোয়েরির শব্দ কতটা আছে, পাশাপাশি শব্দ-জোড়া, RD2-24-এর মতো হুবহু আইডেন্টিফায়ার, শিরোনামের মিল), যা 10 * ওজনযুক্ত যোগফল - 4 সূত্রে স্কোর হয়, তাই আসল মডেলের মতোই কাঁচা স্কোর ০ থেকে ১-এর বাইরে যায়। সে প্যারাফ্রেজ পড়তে পারে না। রিকল-প্রশ্নের ছয়টা ক্যান্ডিডেট স্কোর করুন:
from evidencedesk.retrieval.catalog import Scope, load_catalog
from evidencedesk.retrieval.labels import load_labels
from evidencedesk.retrieval.lifecycle import build_searchers
from evidencedesk.retrieval.rerankers import LexicalPairScorer
cat, labels = load_catalog("paragraph"), load_labels()
parts, allowed = build_searchers(cat), cat.allowed(Scope())
q = "Is my bike affected by RD2-24?"
top = parts["hybrid"].search(q, 20, allowed)[:6] # stage one: the six best of twenty candidates
scorer = LexicalPairScorer() # stage two: one score per (query, passage) pair
raw = scorer.predict([(q, f"{c.chunk.title} || {c.chunk.text}") for c in top])
for c, s in sorted(zip(top, raw), key=lambda pair: -pair[1]):
print(f"first-stage rank {c.rank} raw score {s:>6} {c.chunk.id}")first-stage rank 1 raw score 9.333 kc-recalls-v1:c0
first-stage rank 4 raw score 0.167 kc-assembly-v1:c2
first-stage rank 2 raw score -0.667 kc-returns-v3:c0
first-stage rank 3 raw score -0.667 kc-service-v1:c1
first-stage rank 5 raw score -3.167 kc-sizing-v1:c2
first-stage rank 6 raw score -3.167 kc-assembly-v1:c1নেতিবাচক সংখ্যার পাশে ৯.৩ কোনো সম্ভাবনা নয়। ২ ও ৩ নম্বর -০.৬৬৭-এ টাই করেছে এবং প্রথম ধাপের ক্রম ধরে রেখেছে: এটা টাই-ব্রেক, বিচার নয় (ties_at_top এগুলো গোনে)। আসল স্কোরার একই ইন্টারফেসের পেছনে একই কয়েক লাইন; ডাউনলোড লাগে বলে এর কোনো আউটপুট নেই:
# not run here: the first load downloads model weights (needs network, disk and the sentence-transformers package)
from sentence_transformers import CrossEncoder
class HubPairScorer:
# same predict() shape as LexicalPairScorer: PairReranker(HubPairScorer()) is a one-line swap
def __init__(self, name: str = "cross-encoder/ms-marco-MiniLM-L6-v2") -> None:
self.model = CrossEncoder(name)
def predict(self, pairs):
return [float(s) for s in self.model.predict(list(pairs))] # raw scores such as 8.6 or -4.3recall@k, precision@k ও MRR মাপুন
তিনটাই আগের পাতার লেবেল থেকে শুরু: একটা চাঙ্ক প্রাসঙ্গিক যদি তার গ্রেড ২ হয় (সে প্রশ্নের উত্তর দেয়)। একটা প্রশ্নের জন্য, অনুমোদিত সেটে Rটা প্রাসঙ্গিক চাঙ্ক থাকলে:
| মেট্রিক | সংজ্ঞা | যা দেখতে পায় না |
|---|---|---|
| recall@k | শীর্ষ k-তে প্রাসঙ্গিক চাঙ্ক / R | শীর্ষ k-র ভেতরের ক্রম; কোন চাঙ্কগুলো আবর্জনা |
| precision@k | শীর্ষ k-তে প্রাসঙ্গিক চাঙ্ক / k | কয়টা প্রাসঙ্গিক চাঙ্ক আছে: R = 1 হলে এটা কখনো 1/k ছাড়ায় না |
| MRR | প্রশ্নগুলোর গড়: ১ / (প্রথম প্রাসঙ্গিক চাঙ্কের র্যাংক), না পেলে ০ | প্রথম হিটের পরের সবকিছু: "একাধিক সঠিক উত্তরের জন্য কোনো ক্রেডিট নেই" |
MRR-এর সংজ্ঞা NIST TREC প্রশ্ন-উত্তর পাতা অনুযায়ী। শীর্ষ k-তে precision ও recall হলো সেট-মাপ; IR বইয়ের মতে precision@k প্রচলিত মাপগুলোর মধ্যে সবচেয়ে অস্থির। ছোট তালিকাতেও k দিয়েই ভাগ হয়। যে প্রশ্নের কোনো প্রাসঙ্গিক চাঙ্ক নেই (তিনটা উত্তরহীন প্রশ্ন), তা অসংজ্ঞায়িত, শূন্য নয়: তাদের বাদ রাখা হয়। আগে হাতে-হিসাব-করা একটা তালিকায় কোড মিলিয়ে দেখুন:
from evidencedesk.retrieval.metrics import Judged, ndcg_at, precision_at, recall_at, reciprocal_rank
# One question. Two chunks answer it (grade 2, say the FAQ copy and the policy); one more is useful context (grade 1).
# Our list has five slots: [useless, answer, context, useless, answer]. `pool` = grades of ALL allowed chunks, best first.
j = Judged("demo", ("direct",), True, grades=(0, 2, 1, 0, 2), pool=(2, 2, 1))
for k in (1, 2, 5):
print(f"k={k} recall {recall_at(j, k):.3f} precision {precision_at(j, k):.3f}")
print(f"reciprocal rank {reciprocal_rank(j):.3f} (first answer at rank 2)")
print(f"ndcg@5 exponential gain {ndcg_at(j, 5):.3f} linear gain {ndcg_at(j, 5, 'linear'):.3f}")
print(f"the same list with the two answers first: {ndcg_at(Judged('d', (), True, (2, 2, 1, 0, 0), j.pool), 5):.3f}")k=1 recall 0.000 precision 0.000
k=2 recall 0.500 precision 0.500
k=5 recall 1.000 precision 0.400
reciprocal rank 0.500 (first answer at rank 2)
ndcg@5 exponential gain 0.659 linear gain 0.674
the same list with the two answers first: 1.000দ্বিতীয় কপি ঢুকলে recall ১ ছোঁয়; precision-এর সীমা R/k; reciprocal rank প্রথম উত্তরেই থেমে যায়। এবার আগের পাতার তিন কৌশল, প্রতি প্রশ্নে দশটা ফল:
from evidencedesk.retrieval.metrics import judge_run, summarize
names = ["recall@1", "recall@3", "recall@5", "precision@3", "mrr", "ndcg@5", "ndcg@5:linear"]
print(f"{'':<8}" + "".join(f"{n:>14}" for n in names))
runs = {}
for name in ("sparse", "dense", "hybrid"):
runs[name] = judge_run(parts[name], cat, labels, Scope(), 10)
print(f"{name:<8}" + "".join(f"{v:>14.3f}" for v in summarize(runs[name], names).values())) recall@1 recall@3 recall@5 precision@3 mrr ndcg@5 ndcg@5:linear
sparse 0.714 0.905 0.952 0.302 0.813 0.840 0.837
dense 0.667 0.857 0.905 0.286 0.756 0.779 0.774
hybrid 0.762 0.905 0.952 0.302 0.835 0.846 0.839ক্রম আগের পাতার সঙ্গে মেলে, আর নতুন কলামগুলো বাড়তি তথ্য দেয়। precision@3 ০.৩০২, সীমা ০.৩৩৩, কারণ প্রায় প্রতিটা প্রশ্নের উত্তরসহ চাঙ্ক একটাই: এটা লেবেলের বৈশিষ্ট্য, ত্রুটি নয়। MRR হাইব্রিডকে স্পার্সের ০.০২২ ওপরে রাখে; পরের অংশগুলো এটাকে পরিপ্রেক্ষিতে বসায়।
গ্রেড-করা প্রাসঙ্গিকতা লেবেলে nDCG মাপুন
কিছু প্রমাণ অন্য প্রমাণের চেয়ে ভালো। rc02 ("Is my bike affected by RD2-24?")-এর একটা গ্রেড-২ চাঙ্ক (রিকল নোটিশ) আর একটা গ্রেড-১ চাঙ্ক (সিরিয়াল নম্বর কোথায়) আছে। nDCG গ্রেড ২-কে ১-এর ওপরে, আর ১-কে কিছুই-না-এর ওপরে পুরস্কৃত করে আর সর্বোত্তম সম্ভাব্য স্কোর দিয়ে ভাগ করে, তাই নিখুঁত তালিকা পায় ১.০। পাঠ্যবইয়ের রূপ (IR বই, সমীকরণ ৪৪) গেইন 2^grade - 1 আর ডিসকাউন্ট log2(1 + rank) ব্যবহার করে; এই কোর্স ঠিক সেটাই ব্যবহার করে, আর variant="linear" (গেইন = গ্রেড) অন্য প্রচলিত রূপ। লাইব্রেরিভেদে ভিন্নতা আছে, আর একটা ইভ্যালুয়েটরের ডকুমেন্টেশন বলে না সে কোনটা ব্যবহার করে: এক টুলের nDCG অন্য টুলের সঙ্গে তুলনার আগে মিলিয়ে নিন।
# file: evidencedesk/retrieval/metrics.py (excerpt)
def gain(grade: int, variant: str = "exp") -> float:
if variant == "exp":
return 2.0**grade - 1 # the textbook form: grade 1 -> 1, grade 2 -> 3
if variant == "linear":
return float(grade) # the other common form: grade 1 -> 1, grade 2 -> 2
raise ValueError("variant must be 'exp' or 'linear'")
def dcg(grades: Iterable[int], variant: str = "exp") -> float:
"""sum of gain(grade) / log2(1 + rank), ranks counted from 1."""
return sum(gain(g, variant) / math.log2(1 + n) for n, g in enumerate(grades, 1))
def ndcg_at(j: Judged, k: int, variant: str = "exp") -> float | None:
"""DCG of the list / DCG of the best possible list. 1.0 = nothing better could be shown. Libraries differ in `variant`."""
ideal = dcg(j.pool[:k], variant)
return dcg(j.grades[:k], variant) / ideal if ideal else Noneআদর্শ তালিকা আসে অনুমোদিত প্রতিটা চাঙ্ক থেকে (pool), সার্চ যা ফেরত দিয়েছে তা থেকে নয়, তাই গ্রেড-২ চাঙ্ক বাদ পড়লে আর কোনো ভুল না থাকলেও দাম দিতে হয়; আর এটা স্কোপের ওপর নির্ভর করে। হাতে-হিসাব-করা উদাহরণে দুই ভ্যারিয়েন্ট ০.৬৫৯ ও ০.৬৭৪ দেয়, আর উত্তর শুরুতে এলে ১.০০০। কৌশলের টেবিলে তারা বড়জোর তৃতীয় দশমিকে আলাদা, যা স্বাভাবিক যখন বেশিরভাগ গ্রেড ০ বা ২।
একটা মেট্রিক কী সমর্থন করতে পারে আর কী পারে না
২১টা উত্তরযোগ্য প্রশ্নে একটা প্রশ্ন recall@1-এ ৪.৮ পয়েন্টের সমান। কিট তিনটা সরঞ্জাম দেয়: সিড-করা বুটস্ট্র্যাপ ইন্টারভ্যাল (প্রশ্নগুলো বারবার নমুনা নিয়ে, resample), ফ্লিপ গণনা (যে প্রশ্নে B, A-কে হারায় বা A-র কাছে হারে, কেবল সেগুলোই তথ্য বহন করে) এক্স্যাক্ট (exact) সাইন-টেস্টের p সহ, আর আকারসহ ট্যাগ অনুযায়ী স্লাইস।
from evidencedesk.retrieval.metrics import bootstrap_ci, by_tag, paired, per_question
mrr = {n: per_question(runs[n], "mrr") for n in runs}
for n, values in mrr.items():
lo, hi = bootstrap_ci(list(values.values()), seed=0)
print(f"{n:<7} MRR {sum(values.values()) / len(values):.3f} 95% interval [{lo}, {hi}] ({len(values)} questions)")
d = paired(mrr["sparse"], mrr["hybrid"])
print(f"hybrid vs sparse: better on {list(d.better)}, worse on {list(d.worse)}, same on {d.tied}; sign-test p = {d.p}")
print("hybrid MRR by tag (value/questions):", ", ".join(f"{t} {v:.2f}/{n}" for t, (v, n) in by_tag(runs["hybrid"], "mrr").items()))sparse MRR 0.813 95% interval [0.683, 0.94] (21 questions)
dense MRR 0.756 95% interval [0.59, 0.905] (21 questions)
hybrid MRR 0.835 95% interval [0.686, 0.952] (21 questions)
hybrid vs sparse: better on ['rc05', 'rc07', 'rc10'], worse on ['h01', 'h02', 'h03'], same on 15; sign-test p = 1.0
hybrid MRR by tag (value/questions): bangla 1.00/3, direct 1.00/2, exact_term 0.83/4, paraphrase 0.75/6, restricted 1.00/1, stale 1.00/1, structured 0.67/6ইন্টারভ্যালগুলো প্রায় পুরোপুরি মেলে। হাইব্রিড তিন প্রশ্নে স্পার্সকে হারায় আর অন্য তিন প্রশ্নে হারে: সমান-সমান, p = ১.০, যদিও গড় আলাদা। প্যাটার্নটাই কাজের: হাইব্রিড প্যারাফ্রেজ ও বাংলা প্রশ্নে জেতে (rc05, rc07, rc10) আর তিনটা কাঠামোবদ্ধ-ডকুমেন্টের প্রশ্নে হারে। স্লাইস পড়ুন আকারসহ: "structured 0.67" ছয়টা প্রশ্নের ওপর দাঁড়িয়ে, "restricted 1.00" একটার ওপর; দুই-ট্যাগের প্রশ্ন দুই স্লাইসে বসে। নিয়ম: এক বা দুই প্রশ্নের ব্যবধান গোলমাল; ইন্টারভ্যাল ও ফ্লিপ জানান; তৃতীয় দশমিকের বিজয়ী বাছবেন না।
ক্যান্ডিডেটের সংখ্যা ও চূড়ান্ত কনটেক্সটের আকার ঠিক করুন
দুটো সেটিং দুই-ধাপের পাইপলাইনের আকার ঠিক করে: ক্যান্ডিডেট (প্রথম ধাপ আনে, দ্বিতীয় ধাপ স্কোর করে) আর চূড়ান্ত আকার (final) (প্রম্পটে পৌঁছানো চাঙ্কের সংখ্যা)। বেশি ক্যান্ডিডেট উত্তর পুলে থাকার সম্ভাবনা বাড়ায় আর খরচ রৈখিকভাবে বাড়ে; বড় final আকার recall বাড়ায়, কিন্তু টোকেন ও বিভ্রান্তিও বাড়ায়। সুইপে আগের পাতার শব্দ-মিলের রিরাংকার চলে। লেটেন্সি একটা কাল্পনিক দামের তালিকা (প্রথম ধাপে ৪ ms আর প্রতি স্কোর-করা জোড়ায় ১.৫ ms); কিট জোড়া ও টোকেন গোনে, কখনো সময় মাপে না।
from evidencedesk.retrieval.evalreport import Config, Costs, run_config
print("candidates final recall@final mrr pairs/question context tokens est. ms (hypothetical)")
for depth, final in [(3, 3), (3, 5), (10, 1), (10, 3), (10, 5), (20, 3)]:
r = run_config(Config(reranker="overlap", depth=depth, final=final), parts, cat, labels)
print(f"{depth:>10}{final:>7}{r.summary['recall@' + str(final)]:>14.3f}{r.summary['mrr']:>7.3f}{r.scored_per_q:>16.1f}{r.tokens_per_q:>16.0f}{r.latency_ms(Costs(4.0, 1.5)):>12}")candidates final recall@final mrr pairs/question context tokens est. ms (hypothetical)
3 3 0.905 0.849 3.0 97 8.5
3 5 0.905 0.849 3.0 97 8.5
10 1 0.857 0.857 10.0 36 19.0
10 3 0.952 0.897 10.0 105 19.0
10 5 0.952 0.897 10.0 173 19.0
20 3 0.952 0.897 20.0 105 34.0৩ ক্যান্ডিডেটে পুলই সীমা, আর recall ০.৯০৫-এ থামে। দশ ক্যান্ডিডেট ৭টা বাড়তি জোড়ার বিনিময়ে recall@3-তে +০.০৪৭ আনে; ২০ কিছুই আনে না, কাল্পনিক লেটেন্সি ১৯ থেকে ৩৪ ms-এ তোলে। এখানে ১০ হলো বাঁক-বিন্দু (knee), যা ২৪টা লেবেল ও ৫২টা চাঙ্কের জন্য নির্দিষ্ট। final ১ হলে টোকেন বাঁচে (১০৫-এর বদলে ৩৬), কিন্তু recall ০.০৯৫ ছেড়ে দিতে হয়। যে সেটিংগুলো সেরাটার গোলমালের সীমার মধ্যে পড়ে, তাদের মধ্যে সবচেয়ে ছোটটা বাছুন, আর কর্পাস বাড়লে আবার মাপুন।
রিরাংকিং যখন ক্ষতি করে
রিরাংকার জোড়া ও সময় খরচ করে, প্রথম ধাপ যা এনেছে শুধু তা-ই নতুন করে সাজাতে পারে, আর প্রথম ধাপ যা জানত তা ফেলেও দিতে পারে। অ্যাবলেশনে স্ট্যান্ড-ইন পরীক্ষাধীন পাইপলাইন, আর প্রতি সারিতে একটা সেটিং বদলায় (অ্যাবলেশন অংশ দেখুন):
from evidencedesk.retrieval.evalreport import ablate
changes = [("reranker", "none"), ("reranker", "overlap"), ("reranker", "blend"), ("max_words", 8),
("depth", 3), ("prefilter", False)]
print(f"{'change (one at a time)':<22}{'mrr':>7}{'delta':>8} better/worse p pairs/q")
for row in ablate(Config(reranker="pair", final=5), changes, parts, cat, labels):
print(f"{row['change']:<22}{row['run'].summary['mrr']:>7.3f}{row['delta']:>+8.3f} {len(row['better'])}/{len(row['worse']):<11} {row['p']:<6} {row['run'].scored_per_q:>6.1f}")change (one at a time) mrr delta better/worse p pairs/q
(base) 0.817 +0.000 0/0 1.0 20.0
reranker=none 0.835 +0.018 4/2 0.688 0.0
reranker=overlap 0.897 +0.080 4/0 0.125 20.0
reranker=blend 0.841 +0.024 3/2 1.0 20.0
max_words=8 0.770 -0.047 1/2 1.0 20.0
depth=3 0.802 -0.015 2/1 1.0 3.0
prefilter=False 0.738 -0.079 0/3 0.25 20.0রিরাংকার সরালে MRR বাড়ে (+০.০১৮): স্ট্যান্ড-ইন শুধু শব্দ পড়ে, আর ডেন্স অংশ যে প্যারাফ্রেজ ও বাংলা প্রশ্নকে আগে বসিয়েছিল তাদের নামিয়ে দেয়। প্যাসেজ ৮ শব্দে ছাঁটলে MRR ০.০৪৭ কমে: RD2-24 রিকল নোটিশের এগারোতম শব্দ, তাই স্কোরিংয়ের আগেই কাটা পড়ে; আসল মডেলের একই সীমা আছে, টোকেনে। প্রথম ধাপের ক্রম ও রিরাংকারের ক্রম RRF দিয়ে মেশালে বেশিরভাগ ক্ষতি ফেরে, সস্তা একটা সুরক্ষা। আর p কলাম: সেরা পরিবর্তনেরও, ওভারল্যাপ রিরাংকারের (+০.০৮০, ৪টায় ভালো, ০টায় খারাপ), p = ০.১২৫। এখানে কিছুই গোলমাল থেকে আলাদা করা যায় না।
ডুপ্লিকেট ও প্রায়-ডুপ্লিকেট প্রমাণ ধরুন
ডকুমেন্ট কপি হয়: একটা পলিসি পাতা আর একটা FAQ একই বাক্য ভাগ করে। ডুপ্লিকেট প্রম্পটের জায়গা নষ্ট করে আর ভিন্ন প্রমাণকে শীর্ষ k থেকে ঠেলে বের করে। দুটো নতুন ডেটা ফাইল যোগ করে ৩০ দিনের নিয়মের একটা FAQ কপি আর একটা পার্টনার পাতা, যাতে শব্দ একই কিন্তু ১৪ দিন। dedupe বিগিনারের শব্দ-ওভারল্যাপ মাপ (কনটেক্সটের মান, বাজেট ও পুনরুৎপাদনযোগ্যতা) ব্যবহার করে, সঙ্গে একটা নিয়ম: যেসব চাঙ্কের সংখ্যা আলাদা, তারা ভিন্ন দাবি, দুটোই থাকে। ব্লকটা বাকি প্রমাণও সাজায়: টোকেন বাজেট (fit_tokens), সবচেয়ে জোরালো চাঙ্ককে প্রান্তে রেখে পড়ার ক্রম (edges_first) আর উদ্ধৃত স্প্যানসহ বাক্য-স্তরের কমপ্রেশন (pick_spans)।
from evidencedesk.retrieval.catalog import CORPUS, DOCS
from evidencedesk.retrieval.evidence import context_tokens, dedupe, edges_first, fit_tokens, keeps_evidence, pick_spans
from evidencedesk.retrieval.labels import DATA
from evidencedesk.retrieval.searchers import HybridSearcher
dup_cat = load_catalog("paragraph", folders=(CORPUS, DOCS, DATA / "dups")) # adds a FAQ copy and a 14-day partner page
hyb = HybridSearcher(*[build_searchers(dup_cat)[n] for n in ("sparse", "dense")])
q = "How long do I have to return a bike?"
found = hyb.search(q, 5, dup_cat.allowed(Scope()))
print("before:", [c.chunk.id for c in found])
d = dedupe(found)
print("after: ", [c.chunk.id for c in d.kept])
print("merged:", d.merged, "| same words, different number, kept apart:", d.held_back)
print("reading order (ranks):", [c.rank for c in edges_first(d.kept)])
print("tokens kept under a 100-token budget:", context_tokens(fit_tokens(d.kept, 100)[0]), "of", context_tokens(d.kept))
lost, before, after = [], 0, 0
for x in labels: # sentence-level compression on the normal catalog, three chunks per question
if x.answerable:
top = parts["hybrid"].search(x.question, 3, allowed)
spans = pick_spans(x.question, top)
before, after = before + context_tokens(top), after + context_tokens(spans)
if any(x.grade_of(c.chunk) == 2 for c in top) and not keeps_evidence(x, spans):
lost.append(x.id)
print(f"best sentence per chunk: {before} -> {after} tokens over 21 questions; answer cut off for {lost}")before: ['kc-returns-faq-v1:c0', 'kc-returns-partner-v1:c0', 'kc-returns-v3:c0', 'kc-warranty-v2:c0', 'kc-sizing-v1:c1']
after: ['kc-returns-faq-v1:c0', 'kc-returns-partner-v1:c0', 'kc-warranty-v2:c0', 'kc-sizing-v1:c1']
merged: {'kc-returns-faq-v1:c0': ('kc-returns-v3:c0',)} | same words, different number, kept apart: (('kc-returns-partner-v1:c0', 'kc-returns-faq-v1:c0'),)
reading order (ranks): [1, 3, 4, 2]
tokens kept under a 100-token budget: 99 of 186
best sentence per chunk: 2057 -> 1148 tokens over 21 questions; answer cut off for ['rc12', 'h06', 'h07']পলিসির কপি FAQ চাঙ্কে মিশে গেছে (merged এখনো তার নাম দেয়, তাই দুই সোর্সই তালিকায় রাখা যায়) আর ১৪ দিনের পাতা টিকে আছে; সেটা প্রম্পটে থাকবে কি না, তা মেটাতে হবে এমন একটা সংঘাত, যেমন কনটেক্সটের মান, বাজেট ও পুনরুৎপাদনযোগ্যতা-য়। লেবেল দুই কপিকেই উত্তর গোনে, তাই dedupe কাগজে recall@k কমায়, অথচ মডেলকে একই প্রমাণ দেয়: একে মাপুন হিট-রেট ও MRR দিয়ে। পড়ার ক্রম একটা অনুমান, যা আপনার মডেলে পরীক্ষা করতে হবে, ফল নয়। কমপ্রেশন সতর্কবার্তার উদাহরণ: প্রতি চাঙ্কের সেরা বাক্য প্রম্পটকে আনুমানিক ২০৫৭ থেকে ১১৪৮ টোকেনে নামায় আর তিনটা প্রশ্নে উত্তর কেটে ফেলে (rc12, h06, h07)। ব্যবহারের আগে মাপুন।
সতেজতা ও মুছে ফেলা সামলান
একটা ডকুমেন্ট বদলায় (৩০ দিন হয় ৪৫) বা সরাতে হয়। Replace নতুন সংস্করণ যোগ করে আর পুরনোগুলোকে superseded চিহ্নিত করে (বিগিনারের update_document): অডিটের জন্য রাখা, স্কোপ-ফিল্টারে লুকানো। Delete প্রতিটা চাঙ্ক সরায়, ডেটা-ধারণ বা আইনি অনুরোধের জন্য। ইনডেক্স সঙ্গে না বদলালে কোনোটাই কাজ করে না। তিনটা অবস্থা:
from dataclasses import replace
from evidencedesk.chunking import chunk_document, load_document
from evidencedesk.retrieval.catalog import CORPUS
from evidencedesk.retrieval.lifecycle import delete_document, replace_document, served
doc = load_document(CORPUS / "returns-policy.md") # the policy changes: 30 days becomes 45
v4 = [replace(c, id=c.id.replace("v3", "v4"), doc_id="kc-returns-v4", version=4, text=c.text.replace("30 days", "45 days")) for c in chunk_document(doc)]
cat2, change = replace_document(cat, v4)
print("superseded:", list(change.superseded))
top2 = lambda found: [c.chunk.id for c in found][:2]
print("old index, no filter: ", top2(parts["hybrid"].search(q, 3, None)))
print("old index, new filter: ", top2(parts["hybrid"].search(q, 3, cat2.allowed(Scope()))), "<- answer gone")
print("rebuilt index, new filter: ", top2(build_searchers(cat2)["hybrid"].search(q, 3, cat2.allowed(Scope()))))
_, gone = delete_document(cat2, "kc-returns-v2") # a retention rule: delete, do not just hide
print("deleted", list(gone.removed), "- old index still serves:", sorted(served(parts["sparse"], ["store credit"], None, 20) & set(gone.removed)))superseded: ['kc-returns-v3:c0', 'kc-returns-v3:c1', 'kc-returns-v3:c2']
old index, no filter: ['kc-returns-v2:c0', 'kc-returns-v3:c0']
old index, new filter: ['kc-warranty-v2:c0', 'kc-sizing-v1:c1'] <- answer gone
rebuilt index, new filter: ['kc-returns-v4:c0', 'kc-warranty-v2:c0']
deleted ['kc-returns-v2:c0', 'kc-returns-v2:c1'] - old index still serves: ['kc-returns-v2:c1']ফিল্টার ছাড়া পুরনো ইনডেক্স এখনো বাতিল পলিসি দেখায়। নতুন ফিল্টার দিলে সে পুরনো টেক্সট লুকায় কিন্তু উত্তরও হারায়, কারণ নতুন ডকুমেন্ট কখনো এমবেড হয়নি। শুধু রিবিল্ড (বা আপসার্ট) ঠিক। মুছে ফেলার পরও পুরনো ইনডেক্সে টেক্সট থাকে: ফিল্টার নিরাপত্তা-জাল, মুছে ফেলা নয়। লিখে রাখুন কী রি-ইনডেক্সিং ঘটায়, পুরনো চাঙ্ক কতক্ষণ দৃশ্যমান থাকতে পারে, আর প্রতিটা পরিবর্তনের পর পুরনো প্রশ্ন করা একটা টেস্ট। রিট্রিভাল অপারেশন ও ডেটার সতেজতা একে অপারেশনে রূপ দেয়।
মাল্টি-টেন্যান্ট রিট্রিভালের সীমানা নকশা করুন
দুই কোম্পানি একটা ইনডেক্স ভাগ করে, আর Harbor Bikes-এর প্রশ্ন কখনো Kestrel-এর চাঙ্ক ফেরত দেবে না, ইচ্ছা করে লেখা হুবহু আইডেন্টিফায়ারেও না। সীমানা হলো অনুমোদিত সেট: স্কোপের নিয়ম এবং টেন্যান্টের ডকুমেন্ট। নকশার মূল কথা: মালিকহীন ডকুমেন্ট কাউকে ফেরত দেওয়া হয় না (ফেইল-ক্লোজড, অর্থাৎ সন্দেহ হলে কিছুই দেওয়া হয় না); অজানা টেন্যান্ট পায় খালি সেট, যাকে প্রতিটা সার্চার "কিছু না" ধরে, কখনো "ফিল্টার নেই" (None) নয়; ফিল্টার চলে র্যাংকিংয়ের আগে। তারপর প্রতিটা প্রশ্ন প্রতিটা টেন্যান্ট হয়ে করুন:
from evidencedesk.retrieval.lifecycle import TENANT_DOCS, Tenancy, boundary_leaks
full = load_catalog("paragraph", folders=(CORPUS, DOCS, TENANT_DOCS)) # adds two documents of a second company
tenancy, searchers = Tenancy.load(), build_searchers(full)
asks = [x.question for x in labels] + ["HB-77", "Harbor Bikes returns 60 days", "RD2-24", "store credit"]
print("documents with no owner:", tenancy.unowned(full), "| leaks with the tenant filter:", len(boundary_leaks(searchers, full, tenancy, asks, Scope())))
for t in ("kestrel", "harbor", "nobody"):
top = searchers["hybrid"].search(q, 1, tenancy.allowed(full, t, Scope()))
print(f"{t:<8} -> {top[0].chunk.id if top else 'nothing'}")
unfiltered = {c.chunk.id for a in asks for s in searchers.values() for c in s.search(a, 5, full.allowed(Scope())) if c.chunk.doc_id.startswith("hb-")}
print("without the tenant filter the same questions return:", sorted(unfiltered))documents with no owner: [] | leaks with the tenant filter: 0
kestrel -> kc-returns-v3:c0
harbor -> hb-returns-v1:c0
nobody -> nothing
without the tenant filter the same questions return: ['hb-recall-v1:c0', 'hb-returns-v1:c0']২৮টা প্রশ্ন, তিন সার্চার ও দুই টেন্যান্টে কোনো ফাঁস নেই, আর প্রতিটা টেন্যান্ট নিজের উত্তর পায়। শেষ লাইনটা কন্ট্রোল: ফিল্টার ছাড়া একই প্রশ্ন অন্য কোম্পানির চাঙ্ক ফেরত দেয়। কন্ট্রোল ছাড়া ফাঁস-টেস্ট হয়তো কখনো বিপজ্জনক প্রশ্নই করেনি। এটা রিট্রিভাল স্তর পরীক্ষা করে; টেন্যান্ট কে তা ঠিক করা অ্যাপ্লিকেশনের কাজ (প্রম্পট ইনজেকশন ও অ্যাক্সেসের সীমানা), যা ইনজেশন ও অনুমতি-সচেতন রিট্রিভাল পাতায় আরও বিস্তারিত আসবে।
সাইটেশনের কভারেজ ও সমর্থন মূল্যায়ন করুন
রিট্রিভাল শেষ হয় এমন উত্তরে, যাতে সাইটেশন থাকে। প্রতি উত্তরে তিনটা হার, ইনলাইন [chunk id] চিহ্নসহ: coverage (যে বাক্যে কোনো সাইটেশন আছে), resolve (যে সাইটেশন মডেলকে দেখানো চাঙ্কের নাম বলে) আর support (সাইটেশনসহ বাক্য, যার শব্দ ও সংখ্যা উদ্ধৃত টেক্সটে আছে: বিগিনারের check_support)। হাতে-লেখা ছয়টা উত্তর, প্রতিটায় একটা ঘটনা:
import json
from evidencedesk.retrieval.evidence import check_sentences, citation_scores
text = {c.id: c.text for c in cat.chunks}
print(f"{'case':<6}{'coverage':>9}{'resolve':>9}{'support':>9} what the case tests")
for line in (DATA / "answers.jsonl").read_text(encoding="utf-8").splitlines():
a = json.loads(line)
s = citation_scores(check_sentences(a["answer"], {i: text[i] for i in a["shown"]}))
print(f"{a['id']:<6}{s['coverage']:>9}{s['resolve']:>9}{s['support']:>9} {a['note']}")case coverage resolve support what the case tests
ans1 1.0 1.0 1.0 Every sentence cites a shown chunk that holds its words and numbers.
ans2 0.5 1.0 1.0 The second sentence makes a claim and cites nothing.
ans3 1.0 0.0 0.0 A real chunk id, but the model was not shown that chunk.
ans4 1.0 1.0 0.0 The id resolves, the words match, the number is wrong.
ans5 1.0 1.0 1.0 The cited text says opened helmets CANNOT be returned. Words and numbers all match.
ans6 1.0 1.0 0.5 One sentence is supported; the other cites the wrong chunk and contradicts the right one.প্রতিটা ব্যর্থতা আলাদা কলামে ধরা পড়ে। ans5 সীমারেখা: সে নিখুঁত স্কোর পায় অথচ ভুল, কারণ উদ্ধৃত টেক্সট বলছে খোলা হেলমেট ফেরত দেওয়া যায় না, আর যাচাই শুধু শব্দ ও সংখ্যা মেলায়। রিজলভ-হওয়া, "সমর্থিত" সাইটেশন সমর্থনের প্রমাণ নয়। না-বোধক বাক্য (negation) ধরতে লাগে মানুষ বা মানুষের লেবেলে ক্যালিব্রেট-করা এনটেইলমেন্ট জাজ (মূল্যায়নের নকশা ও জাজ ক্যালিব্রেশন), আর সংখ্যা-যাচাই সেট-ভিত্তিক: চাঙ্কের যেকোনো জায়গায় "4" থাকলেই পাস।
অ্যাবলেশন পরীক্ষা চালান
একটা অ্যাবলেশন একবারে একটা উপাদান বদলায়, যাতে বোঝা যায় কোন অংশ নিজের খরচ তুলেছে। Config প্রতিটা বাছাই একটা ফ্রোজেন ডেটাক্লাসে রাখে; ablate বেস চালায় আর প্রতিটা এক-ফিল্ডের পরিবর্তনে একটা করে সারি, জানায় মেট্রিক, MRR-এর পরিবর্তন, ভালো/খারাপ গণনা, p ও খরচ ("রিরাংকিং যখন ক্ষতি করে" অংশের টেবিল ঠিক এটাই)। সারিকে রায় নয়, টাপল হিসেবে জানান: "pre-filter off: MRR -0.079, খারাপ ৩টায়, ভালো ০টায়, p = 0.25", টেবিলের সবচেয়ে স্পষ্ট ক্ষতি, যেমন আগের পাতা বলেছিল।
মিনি-ল্যাব: হাইব্রিড RAG বেঞ্চমার্ক
বেঞ্চমার্ক একই চাঙ্ক, ফিল্টার ও লেবেলে সাতটা ভ্যারিয়েন্ট চালিয়ে একটা টেবিল ছাপে। বুটস্ট্র্যাপ (--seed) ছাড়া সবকিছু নির্ধারিত। "vs first" গোনে কোন প্রশ্নে reciprocal rank প্রথম সারির চেয়ে বেশি (+) বা কম (-)। এটা চালান, তারপর এই পাতার পাঁচটা টেস্ট ফাইল:
python -m evidencedesk.retrieval.benchmark --gatevariant r@1 r@3 mrr 95% interval ndcg@5 vs first pairs/q
sparse 0.714 0.905 0.813 [0.68, 0.94] 0.840 - 0.0
dense 0.667 0.857 0.756 [0.59, 0.91] 0.779 +3/-6 0.0
hybrid 0.762 0.905 0.835 [0.69, 0.95] 0.846 +3/-3 0.0
hybrid+overlap 0.857 0.952 0.897 [0.76, 1.00] 0.893 +4/-1 20.0
hybrid+feature 0.714 0.952 0.817 [0.68, 0.94] 0.837 +2/-2 20.0
hybrid+blend 0.762 0.952 0.841 [0.70, 0.95] 0.849 +4/-2 20.0
hybrid+pair (8 words) 0.667 0.905 0.770 [0.62, 0.91] 0.805 +3/-4 20.0
GATE: GOpython -m pytest tests/test_retrieval_metrics.py tests/test_retrieval_rerankers.py tests/test_retrieval_evidence.py \
tests/test_retrieval_lifecycle.py tests/test_retrieval_evalreport.py 2>&1 | tail -1 | sed 's/ in [0-9.]*s//'58 passedশেষ লাইনটা রিট্রিভালের রিলিজ গেট। রুব্রিক, রিগ্রেশন ও রিলিজ গেট gate.py বানিয়েছিল: পলিসি ফাইল, সংরক্ষিত বেসলাইন, ক্রিটিক্যাল কেস, ব্লক ও রিভিউ রায়। gate_result রিট্রিভালের একটা রানকে সেই গেটের পড়া ফলের আকৃতি দেয় (recall_at_k, pass_rate, প্রতি উত্তরযোগ্য প্রশ্নে একটা কেস); gate_policy_retrieval.json চারটা ক্রিটিক্যাল প্রশ্নের নাম দেয় (rc02, rc03, rc09, h08: আইডেন্টিফায়ার, বাতিল পলিসি, সীমিত নোট)। সেই গেট র্যাংকিংয়ের মান দেখতে পায় না, তাই retrieval_gate একটা রিভিউ লাইন যোগ করে যখন ভালো হওয়ার চেয়ে বেশি প্রশ্নে MRR খারাপ হয়। সংরক্ষিত বেসলাইনের (হাইব্রিড, তিন চাঙ্ক, রিরাংকার নেই) বিপরীতে চারটা পরিবর্তন:
from evidencedesk.gate import GatePolicy
from evidencedesk.retrieval.evalreport import retrieval_gate
policy = GatePolicy.model_validate_json((DATA / "gate_policy_retrieval.json").read_text(encoding="utf-8"))
base = run_config(Config(), parts, cat, labels) # the saved baseline: hybrid, no reranker, three chunks
for name, cfg in [("rerank overlap", Config(reranker="overlap")), ("rerank feature", Config(reranker="feature")),
("rerank, 8-word cut", Config(reranker="pair", max_words=8)), ("no pre-filter", Config(prefilter=False))]:
print(f"== {name}\n" + retrieval_gate(policy, run_config(cfg, parts, cat, labels), base).text())== rerank overlap
GATE: GO
== rerank feature
GATE: REVIEW
review: mrr worse for 4 question(s) ['h06', 'rc05', 'rc07', 'rc10'], better for 2 (sign-test p=0.688)
== rerank, 8-word cut
GATE: BLOCK
block: critical case rc02 failed its retrieved check
review: mrr worse for 4 question(s) ['h06', 'rc02', 'rc05', 'rc07'], better for 1 (sign-test p=0.375)
== no pre-filter
GATE: REVIEW
review: mrr worse for 3 question(s) ['h08', 'rc01', 'rc09'], better for 0 (sign-test p=0.25)ওভারল্যাপ রিরাংকার পরিষ্কার GO। ফিচার রিরাংকার বিগিনারের থ্রেশহোল্ড পার হয় (recall বেশি) তবু একটা রিভিউ লাইন পায়: চারটা প্রশ্ন খারাপ হয়েছে (তিনটা প্যারাফ্রেজ বা বাংলা, একটা টেবিল), আর p = ০.৬৮৮ বলে এটা গোলমালও হতে পারে, তাই সে ব্লক না করে একজন মানুষকে জিজ্ঞেস করে। ৮-শব্দে ছাঁটা BLOCK, কারণ একটা ক্রিটিক্যাল আইডেন্টিফায়ার প্রশ্ন ব্যর্থ হয়, গড় যা-ই হোক। pre-filter বাদ দেওয়া এই লেবেলে কেবল রিভিউ, অথচ এটাই সেই পরিবর্তন যা মানুষের সবচেয়ে বেশি দেখা দরকার। গেটের ইনপুট একটা সংকেত, রায় নয়।
আমার সুপারিশ (আগে নিজেরটা লিখুন)। প্রথম ধাপ হিসেবে pre-filter-সহ হাইব্রিড রাখুন; ফিচার বা ছাঁটা রিরাংকার শিপ করবেন না। ওভারল্যাপ রিরাংকার সেরা প্রার্থী (MRR ০.৮৯৭, হাইব্রিডের ০.৮৩৫-এর বিপরীতে), কিন্তু ইন্টারভ্যাল মেলে আর ৪–০ মানে p = ০.১২৫: আশাব্যঞ্জক, প্রমাণিত নয়। সিদ্ধান্তের আগে লেবেল-করা প্রশ্ন বাড়ান (কঠিন কাজ গুনে দেখায় কতগুলো), একই ফাইলে একটা আসল ক্রস-এনকোডার পরীক্ষা করুন, আর যে গভীরতা পোষাবে তাতে তার লেটেন্সি গুনুন। কোনো র্যাংকিং পদ্ধতি উত্তরহীন প্রশ্নে সিস্টেমকে উত্তর দেওয়া থেকে বিরত রাখে না; সেই যাচাই রিট্রিভালের বাইরে।
কিটে নতুন: মডিউল metrics, rerankers, evidence, lifecycle, evalreport, benchmark; ডেটা answers.jsonl, tenants.json, gate_policy_retrieval.json, baseline_retrieval.json, ফোল্ডার dups/, tenants/; পাঁচটা tests/test_retrieval_*.py ফাইল।
কাজের জায়গায় এটা কোথায় দেখা যায়
একটা সার্চ টিম এমন ডেমোর পর রিরাংকার শিপ করে, যেখানে পাঁচটা কোয়েরি ভালো হয়েছিল; পরের সপ্তাহে সাপোর্ট জানায় আইডেন্টিফায়ার-প্রশ্ন মিস হচ্ছে। ফ্লিপ গণনাসহ একটা লেবেল-করা সেট রিভিউতেই এটা দেখাত: "৪টায় ভালো, ৩টায় খারাপ, p = ১.০" আর "MRR ০.০২ বাড়ল" দুটো আলাদা বাক্য। একই টেবিল ক্যাপাসিটি প্ল্যানিংয়ে (প্রতি প্রশ্নে জোড়ার সংখ্যা গুণ মাপা প্রতি-জোড়া খরচ), অন-কল প্রশ্নে "এই রিলিজ কি রিট্রিভাল বদলেছে?" (সংরক্ষিত বেসলাইন) আর ভাগ-করা ইনডেক্সের রিভিউয়ে (কন্ট্রোলসহ ফাঁস-টেস্ট) কাজে লাগে।
সাধারণ ভুল
- ফ্লিপ ছাড়া গড় তুলনা। "০.৮৩৫ বনাম ০.৮১৩" মানে দুটো প্রশ্ন। ইন্টারভ্যাল আর ভালো/খারাপ গণনা জানান।
- nDCG ভ্যারিয়েন্ট মেশানো। গেইন
2^grade - 1ও রৈখিক গেইন আলাদা, লাইব্রেরিও তাই। প্রতিটা সংখ্যার পাশে ভ্যারিয়েন্ট লিখুন। - রিরাংকার স্কোরকে আস্থা ধরে নেওয়া। কাঁচা ৮.৬ মানে ৮৬% নয়, আর ভিন্ন কোয়েরির স্কোর তুলনীয় নয়। শুধু ক্রমের জন্য ব্যবহার করুন।
- লেবেলে টিউন করে সেটাই উদ্ধৃত করা। ২১টা প্রশ্নে গভীরতা, ওজন বা রিরাংকার বেছে একই ২১টা জানানো আশাবাদী। কিছু আলাদা রাখুন, বা আরও লেবেল করুন।
- কন্ট্রোল ছাড়া ফাঁস-টেস্ট, বা সমাধানকে শাস্তি দেওয়া মেট্রিক। দেখান যে ফিল্টার-ছাড়া রান ফাঁস করে; dedupe মাপুন হিট-রেট দিয়ে, কারণ recall প্রতিটা কপি গোনে।
নিজে চেষ্টা করুন
- সহজ (লোকাল ইনডেক্স ও রিট্রিভালের মান আবার দেখে): একটা প্রশ্নের উত্তরসহ চাঙ্ক দুটো। তালিকার গ্রেড
(0, 0, 2)। recall@3, precision@3 ও reciprocal rank হাতে অনুমান করুন, তারপরJudged("x", (), True, (0, 0, 2), (2, 2))দিয়ে মেলান। - মাঝারি (RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা আবার দেখে):
answers.jsonl-এর ফরম্যাটে এমন একটা উত্তর লিখুন যা ডেলিভারির সময় নিয়ে ভুল, অথচ coverage, resolve ও support সবই ১.০ পায়। কোন যাচাই এটা ধরবে তা ব্যাখ্যা করুন। - কঠিন (রুব্রিক, রিগ্রেশন ও রিলিজ গেট আবার দেখে): একই দিকে কতগুলো প্রশ্ন ফ্লিপ করলে, একটাও উল্টো না গেলে, সাইন-টেস্টের p ০.০৫-এর নিচে নামে? এটা এই লেবেল সেটের কত অংশ, আর রিলিজ গেটের জন্য এর মানে কী?
উত্তর
সহজ। দুটো প্রাসঙ্গিক চাঙ্কের একটা পাওয়া গেছে: recall ১/২ = ০.৫। তিন স্লটের একটা প্রাসঙ্গিক: precision ১/৩ = ০.৩৩৩, আর তার সীমা R/k। প্রথম হিট ৩ নম্বরে: reciprocal rank ১/৩ = ০.৩৩৩। পাস চেকলিস্ট: আপনি recall ভাগ করেছেন R দিয়ে আর precision k দিয়ে।
মাঝারি। একটা উত্তর: Express delivery takes 4 business days [kc-shipping-v1:c0]., এবং মডেলকে দেখানো হয়েছে kc-shipping-v1:c0 চাঙ্কটা; সেই চাঙ্কের টেক্সট বলে এক্সপ্রেস লাগে ২ কার্যদিবস আর স্ট্যান্ডার্ড "4 to 7"। "4" চাঙ্কে আছে, তাই সেট-ভিত্তিক সংখ্যা-যাচাই পাস করে, শব্দও।
from evidencedesk.retrieval.evidence import check_sentences, citation_scores
text = {c.id: c.text for c in cat.chunks}
claim = "Express delivery takes 4 business days [kc-shipping-v1:c0]."
print(citation_scores(check_sentences(claim, {"kc-shipping-v1:c0": text["kc-shipping-v1:c0"]}))){'coverage': 1.0, 'resolve': 1.0, 'support': 1.0}যা ধরবে: বাক্যটা চাঙ্কের পাশে রেখে পড়া একজন মানুষ, বা মানুষের লেবেলে ক্যালিব্রেট-করা দাবি-স্তরের এনটেইলমেন্ট যাচাই। পাস চেকলিস্ট: তিনটা হারই ১.০; আপনি এমন একটা যাচাইয়ের নাম দিয়েছেন যা অর্থ পড়ে, আর বিগিনারের যাচাইকে ভুল বলেননি, শুধু সীমিত বলেছেন।
কঠিন। ছয়। ২১টা প্রশ্নে এক্স্যাক্ট টেস্ট চালান, যেখানে nটা প্রশ্ন ভালো হয় আর কোনোটা খারাপ হয় না:
from evidencedesk.retrieval.metrics import paired
ref = {f"q{i}": 0.0 for i in range(21)}
for n in range(4, 8):
print(n, "questions flip upward, none back: p =", paired(ref, {**ref, **{f"q{i}": 1.0 for i in range(n)}}).p)4 questions flip upward, none back: p = 0.125
5 questions flip upward, none back: p = 0.062
6 questions flip upward, none back: p = 0.031
7 questions flip upward, none back: p = 0.016চারটা একমুখী ফ্লিপে ০.১২৫, পাঁচটায় ০.০৬২, তাই ০.০৫-এর নিচে নামার সবচেয়ে ছোট সংখ্যা ছয়: উত্তরযোগ্য সেটের ২৯%, আর একটাও উল্টো দিকে যায় না। যে পরিবর্তন ৩টা প্রশ্নে সাহায্য করে আর ১টাকে আঘাত করে, এখানে তাকে কখনো তাৎপর্যপূর্ণ বলা যায় না। রিলিজ গেটের জন্য: যা কখনো ভাঙবে না তার জন্য কঠোর নিয়ম (ক্রিটিক্যাল কেস), গড়ের জন্য উদার মার্জিন, আর ছোট নড়াচড়া মানে রিভিউয়ের অনুরোধ, রায় নয় (মজবুত মেট্রিক ও রিলিজ গেট আরও এগোয়)। পাস চেকলিস্ট: আপনি এক্স্যাক্ট টেস্ট ব্যবহার করেছেন; দেখেছেন সিদ্ধান্ত নেয় ফ্লিপ, প্রতিটা পরিবর্তনের আকার নয়; আর গেটের সঙ্গে যুক্ত করেছেন।
সারসংক্ষেপ
- ক্রস-এনকোডার প্রতিটা (কোয়েরি, প্যাসেজ) জোড়া একসঙ্গে স্কোর করে; প্রতি জোড়ায় একবার চলে, তাই ছোট ক্যান্ডিডেট সেট সাজায়, আর তার কাঁচা স্কোর র্যাংকিং স্কোর, সম্ভাবনা নয়। কিট ইন্টারফেস পরীক্ষা করে নির্ধারিত স্ট্যান্ড-ইন দিয়ে; এখানে কোনো আসল মডেল চলেনি।
- Recall@k, precision@k, MRR ও nDCG আলাদা প্রশ্নের উত্তর দেয়। প্রাসঙ্গিকতার সীমা আর nDCG ভ্যারিয়েন্টের নাম বলুন (গেইন
2^grade - 1, ডিসকাউন্টlog2(1 + rank); লাইব্রেরিভেদে ভিন্নতা আছে), আর অসংজ্ঞায়িত প্রশ্ন শূন্য না ধরে বাদ দিন। - ২৪টা লেবেলে ইন্টারভ্যাল, ভালো/খারাপ গণনা আর স্লাইসের আকার জানান। এক বা দুই প্রশ্নের ব্যবধান গোলমাল, আর দেখা ৪–০-ও p = ০.১২৫।
- রিরাংকিং ক্ষতি করতে পারে: হারানো প্রথম-ধাপের জ্ঞান, ছাঁটা প্যাসেজ, বাড়তি খরচ। ক্যান্ডিডেট ও চূড়ান্ত কনটেক্সটের আকার গোলমালের বিপরীতে ঠিক করুন, আর একবারে একটা সেটিং অ্যাবলেট করুন।
- শব্দ ও সংখ্যা দিয়ে dedupe করুন, বদলালে replace ও রিবিল্ড করুন, র্যাংকিংয়ের আগে টেন্যান্ট দিয়ে ফিল্টার করুন (কন্ট্রোল টেস্টসহ), আর সাইটেশন coverage, resolve ও support দিয়ে যাচাই করুন, মনে রেখে যে শব্দ-যাচাই না-বোধকতা (negation) ধরে না।
- রিট্রিভালের রান বিগিনারের রিলিজ গেটে দিন, ক্রিটিক্যাল কেস আর গোলমাল-সচেতন রিভিউ লাইনসহ।
এরপর: MCP আর্কিটেকচার ও লোকাল সার্ভার। রিট্রিভাল নিয়ে অধ্যায় আপাতত সম্পূর্ণ: পরের পাতা Model Context Protocol-এ যায়, যেখানে হোস্ট অ্যাপ্লিকেশন, ক্লায়েন্ট ও সার্ভার মডেলের কাছে টুল উন্মুক্ত করে, আর সেখানে আপনি টেস্ট করার মতো একটা লোকাল সার্ভার চালু করেন।