فاز 19 · درس 51

پیاده‌سازی بازیابی مقالات با BM25 و گراف استنادی

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 19، مسیر الف، درس‌های 20 تا 29
نمایی شماتیک از کارت‌های مقالات علمی متصل به یکدیگر با خطوط نوری شبکه‌ای و ذره‌بینی که روی گره‌های اصلی تمرکز کرده است
فهرست مطالب
  1. اهداف یادگیری
  2. چرا دو پاس بازیابی؟
  3. ساختار داده‌ی مقاله
  4. معماری
  5. پیاده‌سازی BM25 از صفر
  6. پیمایش گراف استنادی
  7. حذف موارد تکراری و رتبه‌بندی
  8. پیکره‌ی داده‌ی ساختگی
  9. درس‌های 52 و 53 چه داده‌ای می‌خوانند؟
  10. راهنمای مطالعه‌ی کد
  11. جایگاه این بخش در ساختار کلی

در فرآیند توسعه‌ی سیستم‌های خودکار پژوهشی یا ایجنت‌های کاوشگر متون علمی، طرح فرضیه بسیار سریع انجام می‌شود، اما اعتبارسنجی پیشینه‌ی آن پرهزینه‌ترین بخش است. اگر بدون بررسی کافی وارد فاز آزمایش یا اجرای کد شویم، منابع محاسباتی زیادی برای اثبات چیزی که پیش‌تر حل شده به هدر می‌رود. چالش اصلی این است که موتورهای جستجوی متنی ساده مبتنی بر کلیدواژه، مقالاتی را که از دایره‌ی لغات متفاوت استفاده کرده‌اند یا پایه‌های نظری مشترک دارند نادیده می‌گیرند.

در این درس یک خط لوله‌ی بازیابی دوگانه طراحی می‌کنیم. در پاس اول با الگوریتم Okapi BM25 جستجوی واژگانی روی چکیده‌ها انجام می‌شود و در پاس دوم، گراف استنادی (Citation Graph) مقالات به روش BFS پیمایش می‌شود تا مقالات بنیادین و کارهای پیرو مرتبط شکار شوند. تلفیق این دو خروجی و حذف موارد تکراری با شناسه‌ی یکتا، نتایجی جامع و دقیق تولید می‌کند.

تمام کدهای این سیستم فقط با ساختارهای داده‌ی کتابخانه‌ی استاندارد پایتون نوشته شده‌اند و به بسته‌های سنگین بیرونی وابسته نیستند؛ بنابراین بدون نیاز به بارگذاری مدل‌های حجیم یا استفاده از سرویس‌های آنلاین خارجی، کاملاً مستقل و قابل حمل روی سیستم محلی اجرا می‌شوند.

ساختن فرضیه (Hypothesis) هزینه‌ای ندارد؛ فهمیدن اینکه آیا قبلاً کسی آن را اثبات کرده یا نه، بخش پرهزینه‌ی کار است. لایه‌ی بازیابی (Retrieval Layer) را طوری بسازید که قبل از بالا آمدن ساندباکس (Sandbox) توسط رانر (Runner)، به این سوال پاسخ دهد.

اهداف یادگیری

  • مدل‌سازی رکورد یک مقاله‌ی جمع‌وجور با فیلدهایی که در ادامه‌ی حلقه خوانده می‌شوند.
  • ساخت یک نمایه (Index) برای BM25 روی چکیده‌ها، تنها با استفاده از ساختارهای داده‌ی کتابخانه‌ی استاندارد (stdlib).
  • پیمایش گراف استنادی (Citation Graph) برای پیدا کردن مقالاتی که جستجوی واژگانی (Lexical Search) آن‌ها را از قلم انداخته است.
  • حذف موارد تکراری بین نتایج پاس واژگانی و پاس گرافی بر اساس شناسه پایدار مقاله.
  • کپسوله‌سازی دو API ساختگی (Mock) در قالب یک کلاینت واحد، تا با اضافه شدن اندپوینت‌های واقعی، محل فراخوانی در لایه‌های بالاتر نیازی به تغییر نداشته باشد.

چرا دو پاس بازیابی؟

جستجوی کلیدواژه‌ای روی چکیده‌ها، مقالاتی را برمی‌گرداند که واژگان مشترکی با عبارت جستجو دارند. این کار بخش عمده‌ی نیاز را پوشش می‌دهد، اما در دو حالت به بن‌بست می‌خورد: اول، زمانی که مقاله‌ی بنیادین از واژگان متفاوتی استفاده می‌کند؛ مثلاً جستجوی «اتنشن تنک» (Sparse Attention) مقاله‌ای با عنوان «انتخاب بلوک در مسیریابی ترنسفورمر» را پیدا نمی‌کند. دوم، زمانی که مقاله‌ی مرتبط، یک کار ادامه‌دار است که به مقاله‌ی مرجع شناخته‌شده‌ای ارجاع می‌دهد؛ در این وضعیت پیدا کردن مقاله مرجع و پیگیری رو به جلوی ارجاعات آن، بسیار کارآمدتر از جستجوی کورکورانه در تمام مجموعه‌ی چکیده‌هاست.

در این درس هر دو پاس را پیاده‌سازی می‌کنیم: الگوریتم BM25 روی چکیده‌ها موارد تطابق واژگانی را شکار می‌کند؛ سپس پیمایش گراف استنادی، مجموعه‌ی اولیه‌ی به‌دست‌آمده را با یک یا دو گام لغزش (Hop) به جلو و عقب گسترش می‌دهد. در نهایت، اجتماع این نتایج بر اساس شناسه‌ی مقاله یکتاسازی شده (Deduplicated) و با امتیازی ترکیبی رتبه‌بندی می‌شود.

ساختار داده‌ی مقاله

Paper
  id          : str           (stable identifier, "p001" for the mock corpus)
  title       : str
  abstract    : str
  year        : int
  authors     : list[str]
  references  : list[str]     (paper ids this paper cites)
  citations   : list[str]     (paper ids that cite this paper)
  source      : str           (which mock api supplied it, "arxiv" or "s2")

فیلدهای مراجع (References) و استنادات (Citations) گراف استنادی جهت‌دار را شکل می‌دهند. از آنجا که دو API ساختگی فیلدهای هم‌پوشان ولی نه‌چندان یکسانی برمی‌گردانند، بارگذار پیکره (Corpus Loader) آن‌ها را بر اساس id ادغام می‌کند.

بازیابی پیشینه‌ی پژوهشBM25 + پیمایش ارجاعات
LITERATURE RETRIEVALBM25 over abstracts returns the papers that share vocabulary with the query, and misses the foundational work that named the idea differently. The second pass starts from a known anchor and walks the citation graph a hop or two in each direction, surfacing follow-ups no keyword would catch. The two hit sets union, deduplicate on stable paper id, and rank, so the anchor that appeared in both passes is counted once.گذر لغوی + گذر گراف · اجتماع و حذف تکرار با شناسه"sparse attention"BM25 روی چکیده‌هاp003 · 0.81p007 · 0.66p011 · 0.54بذر از مبنای مشخصp007p015p019p021گام 1گام 2رتبه‌بندی: p007 p003 p019 p015 p011 p021p007 یک‌بار حفظ شد
الگوریتم BM25 روی چکیده‌ها مقالاتی را برمی‌گرداند که دایره‌ی واژگان مشترکی با پرس‌وجو دارند و کارهای پایه‌ای را که از واژگان دیگری برای بیان ایده استفاده کرده‌اند نادیده می‌گیرد. گذر دوم از یک مقاله مبنای شناخته‌شده آغاز می‌شود و گراف ارجاعات را یک یا دو گام لغزش در هر جهت می‌پیماید تا پژوهش‌های ادامه‌ای را که با هیچ کلیدواژه‌ای پیدا نمی‌شوند آشکار کند. نتایج این دو گذر با هم اجتماع یافته، بر اساس شناسه پایدار مقاله حذف تکرار می‌شوند و رتبه‌بندی می‌گردند، تا مقاله‌ای که در هر دو گذر آمده فقط یک‌بار شمرده شود.

معماری

رشته پرس‌وجوکلاینت آزمایشی arxivکلاینت آزمایشی semantic scholarبارگذاری پیکره متنینمایه bm25گراف استنادینتایج متنیگسترش گام لغزش 1 تا 2نتایج گرافmerge و حذف تکراری‌هافهرست رتبه‌بندی‌شده مقاله‌ها

کلاینت بازیابی، مدیریت هر دو پاس و مرحله‌ی merge را بر عهده دارد. فراخواننده عبارت جستجو را به آن تحویل می‌دهد و لیستی رتبه‌بندی‌شده دریافت می‌کند که هر رکورد آن شامل فیلدهای امتیاز به ازای هر مقاله است (bm25_score، graph_distance، recency_score، final_score) تا دلیل رتبه‌بندی را روشن کند.

پیاده‌سازی BM25 از صفر

این پیاده‌سازی از فرمول استاندارد Okapi BM25 با پارامترهای پیش‌فرض k1=1.5 و b=0.75 استفاده می‌کند. نمایه از دو دیکشنری تشکیل شده است: term -> doc_frequency و term -> list of (doc_id, term_count). طول سند برابر با تعداد توکن‌های چکیده است و میانگین طول اسناد نیز یک بار در زمان ساخت نمایه محاسبه می‌شود. امتیازدهی به عبارت جستجو، مجموع idf * tf_norm روی تک‌تک کلمات کوئری است که در آن tf_norm همان فراوانی وزنی نرمال‌شده بر اساس طول سند در الگوریتم استاندارد BM25 است.

توکنایزر متن را ابتدا با lower به حروف کوچک تبدیل می‌کند و سپس آن را بر اساس کاراکترهای غیرالفبایی-عددی برش می‌زند. در این بخش از ریشه‌یابی کلمات (Stemming) استفاده نشده است. در یک سیستم محیط عملیاتی (پروداکشن) می‌توان ریشه‌یاب (Stemmer) سبکی را جایگزین کرد، بدون آنکه نیازی به تغییر رابط کاربری باشد.

idf(t)=log⁡ ⁣(N−df+0.5df+0.5+1.0)tf_norm(t)=f⋅(k1+1)f+k1⋅(1−b+b⋅dl/avgdl)score(d,q)=∑t∈qidf(t)⋅tf_norm(t)\begin{aligned} \text{idf}(t) &= \log\!\left(\frac{N - \text{df} + 0.5}{\text{df} + 0.5} + 1.0\right) \\ \text{tf\_norm}(t) &= \frac{f \cdot (k_1 + 1)}{f + k_1 \cdot \left(1 - b + b \cdot \text{dl} / \text{avgdl}\right)} \\ \text{score}(d, q) &= \sum_{t \in q} \text{idf}(t) \cdot \text{tf\_norm}(t) \end{aligned}

پیمایش گراف استنادی

گراف یک بار بر اساس پیکره‌ی داده ساخته می‌شود. یال‌های رو به جلو از یک مقاله به مراجع آن می‌روند و یال‌های رو به عقب از یک مقاله به استنادکنندگان آن متصل می‌شوند. پیمایش به روش جستجوی اول سطح (BFS) انجام می‌شود که دانه‌های اولیه‌ی آن برترین نتایج BM25 هستند و حداکثر تا دو گام لغزش پیش می‌رود.

سقف دو گام لغزش کاملاً عامدانه تعیین شده است: یک گام لغزش بیش از حد سطحی است، چون ایجنت معمولاً نیازمند والد یا فرزند مستقیم (Immediate) در گراف است؛ از طرفی سه گام لغزش در یک گراف همبند، حجم نتایج را بیش از حد بالا می‌برد و معمولاً از موضوع اصلی منحرف می‌شود. در این درس محدودیت گام لغزش را به شکل پارامتری در تنظیمات قرار داده‌ایم تا حلقه‌های بعدی بتوانند آن را محدودتر کنند.

حذف موارد تکراری و رتبه‌بندی

دو پاس بازیابی، مجموعه‌هایی هم‌پوشان برمی‌گردانند. عملیات merge بر اساس شناسه‌ی مقاله انجام می‌شود. امتیاز نهایی هر مقاله از ترکیب وزن‌دار چند فاکتور به دست می‌آید:

final_score = w_bm25 * bm25_score_norm
            + w_graph * graph_score
            + w_recency * recency_score

در اینجا bm25_score_norm برابر با امتیاز BM25 تقسیم بر حداکثر امتیاز BM25 در مجموعه‌ی ادغام‌شده است (تا مقدار آن در بازه‌ی بین 0 و 1 قرار گیرد). فاکتور graph_score برای نتایج مستقیم واژگانی برابر با 1، برای یک گام لغزش برابر با 0.6، برای دو گام لغزش برابر با 0.3 و در غیر این صورت 0 است. فاکتور recency_score یک تابع خطی صعودی از 0 در حداقل سال انتشار موجود در پیکره تا 1 در حداکثر سال انتشار است.

وزن‌های پیش‌فرض 0.5، 0.3 و 0.2 هستند. این وزن‌ها در تنظیمات قابل تغییرند؛ مثلاً برای موضوعی قدیمی می‌توان وزن تازگی را کاهش داد، در حالی که در حوزه‌های پرتحول باید آن را بالاتر برد.

پیکره‌ی داده‌ی ساختگی

پیکره شامل 100 مقاله است که با تابع build_corpus() تولید می‌شوند. هر مقاله دارای عنوان و چکیده‌ی دست‌نویس در یکی از این پنج موضوع است: اتنشن تنک، افزایش داده با بازیابی، تطبیق‌دهنده‌های کم‌رتبه، تقطیر دانش از مجموعه داده و بسترهای ارزیابی. مراجع و استنادات طوری به هم وصل شده‌اند که هر موضوع یک زیرگراف همبند بسازد و چند یال هم میان موضوعات مختلف وجود داشته باشد.

دو کلاینت API ساختگی (ArxivMockClient و SemanticScholarMockClient) داده‌ها را از همین پیکره می‌خوانند اما فیلدهای متفاوتی را برمی‌گردانند: Arxiv عنوان، چکیده، سال انتشار و نویسندگان را می‌دهد؛ Semantic Scholar مراجع و استنادات را اضافه می‌کند. کلاینت بازیابی داده‌ها را بر اساس id اجتماع می‌گیرد؛ رسیدگی به مغایرت فیلدها بین کلاینت‌ها را به درس‌های بعدی موکول می‌کنیم.

درس‌های 52 و 53 چه داده‌ای می‌خوانند؟

رانر در درس 52 فیلدهای paper.id، paper.title و سه جمله‌ی اول چکیده را به عنوان کانتکست آزمایش مطالعه می‌کند. ارزیاب در درس 53 فیلدهای paper.year و paper.references را می‌خواند تا یک خط مبنا (Baseline) را به مقاله‌ای مشخص نسبت دهد.

کلاینت بازیابی خروجی را در قالب شیء RetrievalResult برمی‌گرداند که شامل لیست رتبه‌بندی‌شده و معیارهای ارزیابی به ازای هر کوئری است: تعداد نتایج، میانگین امتیاز، بالاترین امتیاز و کل زمان اجرای واقعی. رانر این موارد را ثبت می‌کند تا در مراحل بعدی بتوان کیفیت بازیابی را روی نمودار مشاهده‌پذیری پایش کرد.

راهنمای مطالعه‌ی کد

فایل code/main.py کلاس‌های Paper، ArxivMockClient، SemanticScholarMockClient، BM25Index، CitationGraph، RetrievalClient و یک دموی قطعی (Deterministic) را تعریف می‌کند. کلاینت‌های ساختگی و پیکره‌ی داده درون همان فایل قرار گرفته‌اند تا این درس کاملاً مستقل و قابل حمل بماند. پیاده‌سازی BM25 در قالب یک کلاسِ 60 خطی انجام شده و پیمایش گراف نیز یک متد تک‌مرحله‌ای است.

فایل code/tests/test_retrieval.py مسیر جستجوی واژگانی، مسیر گرافی، مرحله‌ی merge، حذف موارد تکراری و همچنین حالت کوئری خالی را پوشش می‌دهد.

جایگاه این بخش در ساختار کلی

درس 50 یک فرضیه تولید می‌کند. درس 51 به جستجوی مقالات می‌پردازد تا مشخص شود آیا تکلیف این فرضیه قبلاً مشخص شده یا نه. اگر اثبات نشده باشد، درس 52 آزمایش را اجرا می‌کند. در نهایت درس 53 نتایج بازیابی و معیارهای آزمایش را می‌خواند تا نظر نهایی را صادر کند. کلاینت بازیابی کم‌هزینه‌ترین بخش در میان این چهار مرحله است و پیش از همه در هماهنگ‌کننده (Orchestrator) به اجرا درمی‌آید.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا کلاینت بازیابی یک گذر BM25 و یک گذر گراف ارجاعات اجرا می‌کند؟
  2. پیش از درسچرا پیمایش گراف ارجاعات به دو گام لغزش محدود شده است؟
  3. حین درسمقدار idf برای عبارتی که در همه‌ی اسناد تکرار شده باشد چقدر است؟
  4. حین درسمرحله‌ی merge از کدام کلید برای حذف موارد تکراریِ دو گذر استفاده می‌کند؟
  5. حین درستابع graph_score برای مقاله‌ای که دو گام لغزش با مجموعه بذر فاصله دارد چه مقداری برمی‌گرداند؟
  6. حین درسچرا پیکره متنی فقط یک بار درون یک RetrievalClient بارگذاری می‌شود و به ازای هر پرس‌وجو واکشی مجدد نمی‌گردد؟

سؤالات متداول

چرا جستجوی مبتنی بر BM25 به تنهایی برای بازیابی مقالات کافی نیست؟

جستجوی واژگانی تنها مقالاتی را می‌یابد که واژگان مشترکی با عبارت جستجو دارند و کارهایی را که از اصطلاحات مترادف یا ادبیات متفاوت استفاده کرده‌اند از قلم می‌اندازد. پیمایش گراف استنادی این نقص را با پیگیری ارجاعات مقالات کلیدی پوشش می‌دهد.

چرا عمق پیمایش در گراف استنادات به دو گام محدود شده است؟

یک گام لغزش (Hop) برای رسیدن به پیشینه‌ها و کارهای مشتق‌شده کافی نیست، در حالی که رفتن به عمق سه گام و بیشتر به دلیل اتصالات انبوه گراف، باعث انفجار تعداد نتایج و انحراف شدید از موضوع جستجو می‌شود.

امتیاز نهایی رتبه‌بندی مقالات چگونه محاسبه می‌شود؟

امتیاز نهایی از ترکیب وزن‌دار سه فاکتور به دست می‌آید: امتیاز نرمال‌شده‌ی BM25، امتیاز گراف (بر اساس میزان فاصله از نتایج مستقیم)، و فاکتور تازگی انتشار مقاله در بازه زمانی کل پیکره.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.