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

۱. فضای ویژگی مشترک و معنای بردار ساختگی

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

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

پرس‌وجو (1, 1, 0) است. سندهای فرضی doc-b و doc-e، به ترتیب با همین بردار و سه برابر آن نمایش داده می‌شوند. doc-a مؤلفهٔ نخست بزرگی دارد، doc-c فقط در مؤلفهٔ سوم قرار می‌گیرد و doc-d در جهت مخالف پرس‌وجو است. مؤلفهٔ منفی در این تمرین مجاز است؛ بنابراین تفسیر آن به عنوان شمارش واژه درست نیست. هیچ قضاوت ارتباط انسانی برای این پنج شناسه تعریف نکرده‌ایم.

بخش ضرب داخلی در کتاب بازیابی اطلاعات، نمایش سند در فضای مشترک و نرمال‌سازی طول بردار را توضیح می‌دهد. انتخاب ویژگی در مثال حاضر مستقل و ساده‌تر از نمایش‌های متنی آن کتاب است. در یک سامانهٔ واقعی، باید روش ساخت ویژگی، مقیاس ستون‌ها و محدودیت اطلاعاتِ حذف‌شده ثبت شود؛ امتیاز کسینوسی به تنهایی آن اطلاعات را ایجاد نمی‌کند.

۲. شباهت کسینوسی و نرمال‌سازی عددی

نمایش مفهومی جهت بردارها پس از نرمال‌سازی؛ این تصویر نمودار مقیاس‌دار نیست
تصویر مفهومی: نمایش مفهومی جهت بردارها پس از نرمال‌سازی؛ این تصویر نمودار مقیاس‌دار نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تعریف cosine_similarity در scikit-learn، ضرب داخلی تقسیم بر حاصل‌ضرب طول اقلیدسی بردارهاست. برای بردارهای ناصفر، می‌توان ابتدا هر بردار را به طول واحد رساند و سپس ضرب داخلی گرفت. در مثال هندسی ما، جهت یکسان امتیاز نزدیک یک، جهت عمود صفر و جهت مخالف نزدیک منفی یک می‌گیرد. این مقدار احتمال درست بودن پاسخ یا درصد ارتباط سند نیست.

بردار صفر جهت ندارد؛ قرارداد تابع تألیفی ما آن را با خطا رد می‌کند. این رفتار را نباید به همهٔ کتابخانه‌ها نسبت داد؛ نمونهٔ مستندات scikit-learn برای سطر صفر رفتار مشخص دیگری نشان می‌دهد. کد ما آن کتابخانه را اجرا یا بازتولید کامل API آن را ادعا نمی‌کند. نوع‌های مجاز نیز فقط int و float دقیقِ قابل نمایش به صورت float متناهی هستند؛ bool، رشته و مقدارهای نامتناهی پذیرفته نمی‌شوند.

برای جلوگیری از ساختن مربع عدد بسیار بزرگ، ابتدا مؤلفه‌ها را بر بزرگ‌ترین قدرمطلق تقسیم می‌کنیم و بعد طولِ بردار مقیاس‌شده را با math.hypot می‌گیریم. تقسیم دوباره، بردار واحد می‌دهد. ضرب داخلی با fsum جمع می‌شود و حدهای عددی خروجی فقط برای گردکردن مرزی محدود می‌شوند. این روش محاسبهٔ دقیق با دقت دلخواه نیست؛ خطای شناور و از دست رفتن مؤلفه‌های بسیار کوچک همچنان باید در مسئلهٔ واقعی بررسی شوند.

۳. کد کامل و هجده بررسی اجراشده

آزمایش آفلاین شباهت کسینوسی در پایتون
تصویر مفهومی: آزمایش آفلاین شباهت کسینوسی در پایتون؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل را با نام cosine_vectors.py ذخیره و با دستور python cosine_vectors.py اجرا کنید. کد با پایتون ۳٫۱۴٫۵ و کتابخانهٔ استاندارد اجرا شده است. scikit-learn فقط مرجع تعریف است و نصب آن لازم نیست. فرمول مستقل سه‌پنجم برای بردار سه‌وچهار، جهت‌های عمود و مخالف، مقیاس مثبت و منفی، تساوی امتیاز و ورودی‌های نامعتبر در بررسی‌ها حضور دارند.

"""Authored synthetic document-vector geometry, not semantic retrieval evaluation."""
import json
import math
import platform
import sys

FEATURES = ('circuit_feature', 'signal_feature', 'software_feature')
QUERY = (1, 1, 0)
DOCUMENTS = (('doc-a', (1000, 0, 0)), ('doc-b', (1, 1, 0)),
             ('doc-c', (0, 0, 3)), ('doc-d', (-1, -1, 0)), ('doc-e', (3, 3, 0)))


def finite_vector(values):
    values = tuple(values)
    if not values or any(type(x) not in (int, float) for x in values):
        raise ValueError('A nonempty vector of exact int or float coordinates is required')
    try:
        coordinates = tuple(float(x) for x in values)
    except OverflowError as exc:
        raise ValueError('Coordinates must be representable as finite floats') from exc
    if not all(math.isfinite(x) for x in coordinates):
        raise ValueError('Every coordinate must be finite')
    if not any(coordinates):
        raise ValueError('The zero vector has no direction in this contract')
    return coordinates


def unit_vector(values):
    values = finite_vector(values)
    scale = max(abs(x) for x in values)
    scaled = tuple(x / scale for x in values)
    length = math.hypot(*scaled)
    return tuple(x / length for x in scaled)


def cosine(left, right):
    left, right = finite_vector(left), finite_vector(right)
    if len(left) != len(right):
        raise ValueError('Coordinate dimensions must agree')
    score = math.fsum(x * y for x, y in zip(unit_vector(left), unit_vector(right)))
    # Bound only floating-point endpoint roundoff, not a relevance threshold.
    return max(-1.0, min(1.0, score))


def rank_documents(query, documents):
    rows = [{'document_id': identifier, 'vector': vector, 'cosine': cosine(query, vector)}
            for identifier, vector in documents]
    return sorted(rows, key=lambda row: (-row['cosine'], row['document_id']))


def rejects(call):
    try:
        call()
    except ValueError:
        return True
    return False


def run_example():
    ranking = rank_documents(QUERY, DOCUMENTS)
    scores = {row['document_id']: row['cosine'] for row in ranking}
    expected = {'doc-a': 1 / math.sqrt(2), 'doc-b': 1.0,
                'doc-c': 0.0, 'doc-d': -1.0, 'doc-e': 1.0}
    close = lambda a, b: math.isclose(a, b, rel_tol=1e-12, abs_tol=1e-12)
    raw_dot_ranking = sorted(DOCUMENTS, key=lambda item: (
        -math.fsum(a * b for a, b in zip(QUERY, item[1])), item[0]))
    extreme = sys.float_info.max
    checks = {
        'five_scores_match_independent_geometric_reference': all(close(scores[key], value) for key, value in expected.items()),
        'known_three_four_reference_is_three_fifths': close(cosine((3, 4, 0), (1, 0, 0)), 0.6),
        'same_direction_is_one': close(cosine((3, 4, 0), (6, 8, 0)), 1),
        'orthogonal_axes_score_zero': cosine((1, 0, 0), (0, 0, 3)) == 0,
        'opposite_direction_is_negative_one': close(cosine((1, 1, 0), (-1, -1, 0)), -1),
        'positive_rescaling_preserves_fixture_score': close(cosine((3, 4, 0), QUERY), cosine((30, 40, 0), QUERY)),
        'negative_rescaling_flips_fixture_score': close(cosine((-3, -4, 0), QUERY), -cosine((3, 4, 0), QUERY)),
        'cosine_is_symmetric_for_fixture_pairs': all(close(cosine(QUERY, v), cosine(v, QUERY)) for _, v in DOCUMENTS),
        'unit_vector_has_unit_length': close(math.hypot(*unit_vector((3, 4, 0))), 1),
        'all_fixture_scores_within_signed_unit_interval': all(-1 <= value <= 1 for value in scores.values()),
        'ranking_matches_independent_id_order': [row['document_id'] for row in ranking] == ['doc-b', 'doc-e', 'doc-a', 'doc-c', 'doc-d'],
        'exact_score_ties_use_ascending_identifier': scores['doc-b'] == scores['doc-e'] and ranking[0]['document_id'] == 'doc-b',
        'listing_order_does_not_break_tie_policy': rank_documents(QUERY, tuple(reversed(DOCUMENTS))) == ranking,
        'raw_dot_and_cosine_rank_differ_in_fixture': raw_dot_ranking[0][0] == 'doc-a' and ranking[0]['document_id'] == 'doc-b',
        'extreme_finite_magnitudes_remain_finite': all(close(cosine(v, QUERY), 1) for v in (
            (extreme, extreme, 0), (1e-300, 1e-300, 0))),
        'zero_empty_and_mismatched_vectors_rejected': all(rejects(call) for call in (
            lambda: cosine((0, 0, 0), QUERY), lambda: unit_vector(()), lambda: cosine((1, 0), QUERY))),
        'all_nonfinite_coordinates_rejected': all(rejects(lambda value=value: cosine((value, 1, 0), QUERY))
                                                  for value in (float('nan'), float('inf'), -float('inf'))),
        'boolean_string_and_unrepresentable_integer_rejected': all(rejects(lambda value=value: cosine((value, 1, 0), QUERY))
                                                                  for value in (True, '1', 10**1000)),
    }
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'features': FEATURES, 'query': QUERY,
            'coordinate_semantics': 'authored signed dimensionless coefficients, not learned embeddings or term counts',
            'ranking': ranking, 'raw_dot_first_id': raw_dot_ranking[0][0],
            'known_reference_scores': expected, 'trained_model_or_relevance_evaluation': False,
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), indent=2))

ورودی‌ها پیش از محاسبه به دنبالهٔ عددی متناهی تبدیل می‌شوند. برای ضرب داخلی، بُعد دو بردار باید برابر باشد؛ از zip برای پنهان کردن اختلاف طول استفاده نمی‌کنیم. تبدیل عدد صحیح به float نیز می‌تواند دقتش را تغییر دهد؛ عدد صحیحی که اصلاً در دامنهٔ float جا نگیرد رد می‌شود. نمونه‌های بسیار بزرگ و بسیار کوچک فقط نشان می‌دهند مسیر مقیاس‌بندی روی همین موارد اجرا شده، نه اینکه هر ترکیب عددی ممکن بی‌خطاست.

برای پاسخ‌های مرجع از تلورانس عددی مشخص استفاده کرده‌ایم. این تلورانس، آستانهٔ پذیرش نتیجهٔ جست‌وجو نیست. همچنین تبدیل امتیاز نزدیک یک به عبارت «سند کاملاً درست» در کد وجود ندارد. اگر ویژگی‌ها از ابتدا انتخاب نامناسبی باشند، محاسبهٔ درست همان ویژگی‌ها مسئلهٔ ارتباط را حل نمی‌کند. موفقیت هجده بررسی، قرارداد محدود ریاضی و ترتیب‌دهی همین داده را تأیید می‌کند.

۴. رتبه‌بندی، اثر مقیاس و تساوی امتیاز

مرتب‌سازی با امتیاز برابر و قاعدهٔ شناسه
تصویر مفهومی: مرتب‌سازی با امتیاز برابر و قاعدهٔ شناسه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ترتیب ثبت‌شده doc-b، سپس doc-e، doc-a، doc-c و doc-d است. دو سند نخست امتیاز برابر و نزدیک یک دارند، doc-a حدود ۰٫۷۰۷۱، doc-c صفر و doc-d نزدیک منفی یک می‌گیرد. تفاوت بسیار کوچک با یک دقیق، نتیجهٔ محاسبهٔ شناور است. امتیاز منفی در این فضای دست‌نویس فقط جهت مخالف را نشان می‌دهد؛ از آن نمی‌توان غلط یا خطرناک بودن محتوای یک سند واقعی را نتیجه گرفت.

{
  "python": "3.14.5",
  "features": [
    "circuit_feature",
    "signal_feature",
    "software_feature"
  ],
  "query": [
    1,
    1,
    0
  ],
  "coordinate_semantics": "authored signed dimensionless coefficients, not learned embeddings or term counts",
  "ranking": [
    {
      "document_id": "doc-b",
      "vector": [
        1,
        1,
        0
      ],
      "cosine": 0.9999999999999998
    },
    {
      "document_id": "doc-e",
      "vector": [
        3,
        3,
        0
      ],
      "cosine": 0.9999999999999998
    },
    {
      "document_id": "doc-a",
      "vector": [
        1000,
        0,
        0
      ],
      "cosine": 0.7071067811865475
    },
    {
      "document_id": "doc-c",
      "vector": [
        0,
        0,
        3
      ],
      "cosine": 0.0
    },
    {
      "document_id": "doc-d",
      "vector": [
        -1,
        -1,
        0
      ],
      "cosine": -0.9999999999999998
    }
  ],
  "raw_dot_first_id": "doc-a",
  "known_reference_scores": {
    "doc-a": 0.7071067811865475,
    "doc-b": 1.0,
    "doc-c": 0.0,
    "doc-d": -1.0,
    "doc-e": 1.0
  },
  "trained_model_or_relevance_evaluation": false,
  "checks": {
    "five_scores_match_independent_geometric_reference": true,
    "known_three_four_reference_is_three_fifths": true,
    "same_direction_is_one": true,
    "orthogonal_axes_score_zero": true,
    "opposite_direction_is_negative_one": true,
    "positive_rescaling_preserves_fixture_score": true,
    "negative_rescaling_flips_fixture_score": true,
    "cosine_is_symmetric_for_fixture_pairs": true,
    "unit_vector_has_unit_length": true,
    "all_fixture_scores_within_signed_unit_interval": true,
    "ranking_matches_independent_id_order": true,
    "exact_score_ties_use_ascending_identifier": true,
    "listing_order_does_not_break_tie_policy": true,
    "raw_dot_and_cosine_rank_differ_in_fixture": true,
    "extreme_finite_magnitudes_remain_finite": true,
    "zero_empty_and_mismatched_vectors_rejected": true,
    "all_nonfinite_coordinates_rejected": true,
    "boolean_string_and_unrepresentable_integer_rejected": true
  }
}

اگر به جای کسینوس فقط ضرب داخلی خام را مرتب کنیم، doc-a با مؤلفهٔ هزار در جایگاه نخست می‌آید. پس بزرگی بردار می‌تواند نتیجهٔ آن روش را تغییر دهد. نرمال‌سازی، اثر مقیاس مثبتِ کل بردار را در هندسه حذف می‌کند؛ اما تغییر مستقل یک ستون، عوض کردن ویژگی‌ها یا گردشدن شدید عددها همان عمل نیست. دو سند هم‌جهتِ مثال، پس از مقیاس‌بندی همان نمایش واحد را دارند.

برای تساوی دقیق امتیاز، شناسهٔ سند به ترتیب صعودی قاعدهٔ دوم است؛ به همین علت doc-b پیش از doc-e می‌آید و وارونه کردن فهرست ورودی نتیجه را عوض نمی‌کند. امتیازهای فقط نزدیک، در این سیاست هم‌رتبه محسوب نمی‌شوند. برای ترکیب چند فهرست رتبه، آموزش RRF و پنجرهٔ نامزدهای جست‌وجو مرحله‌ای متفاوت را بررسی می‌کند؛ ترکیب رتبه‌ها نیز قضاوت ارتباط ایجاد نمی‌کند.

۵. تمرین‌های پاسخ‌دار و ارزیابی ارتباط واقعی

تفکیک امتیاز ریاضی از قضاوت ارتباط سند
تصویر مفهومی: تفکیک امتیاز ریاضی از قضاوت ارتباط سند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: چرا سه برابر کردن بردار doc-b جهت را عوض نمی‌کند؟ پاسخ: ضریب مثبت به همهٔ مؤلفه‌ها یکسان اعمال شده و پس از نرمال‌سازی همان بردار واحد حاصل می‌شود. تمرین دوم: آیا امتیاز صفر یعنی سند هیچ اطلاعاتی ندارد؟ پاسخ: خیر؛ در مثال، جهت doc-c بر پرس‌وجو عمود است، اما این مشاهده به نمایش انتخاب‌شده مربوط می‌شود و دربارهٔ محتوای واقعی حکم نمی‌دهد.

تمرین سوم: آیا باید بردار صفر را همیشه امتیاز صفر داد؟ پاسخ: خیر؛ سیاست این برنامه رد کردن آن است و قرارداد کتابخانه‌های دیگر ممکن است متفاوت باشد. تمرین چهارم: چرا جایگاه نخستِ درست در آزمون، کیفیت موتور جست‌وجو را ثابت نمی‌کند؟ پاسخ: پاسخ مرجع ما هندسی است؛ مجموعهٔ پرس‌وجوی واقعی و قضاوت ارتباط انسانی وجود ندارد. درس ارزیابی رتبه‌بندی جست‌وجو به آن مسئولیت جدا می‌پردازد.

برای استفادهٔ مهندسی، ابتدا معنای ویژگی‌ها و روش تولیدشان را ثبت کنید؛ سپس محاسبهٔ امتیاز، ترتیب‌دهی و ارزیابی ارتباط را هرکدام با دادهٔ مناسب بسنجید. منابع این درس در ۲۴ مهر ۱۴۰۵ خوانده شده‌اند. نسخهٔ مرجع scikit-learn برابر ۱٫۹٫۱ و مستندات پایتون ۳٫۱۴٫۸ بوده‌اند؛ اجرای واقعی فقط با پایتون ۳٫۱۴٫۵ انجام شده است. متن آموزش پایهٔ تألیفی است و ادعای مدل معنایی آموزش‌دیده یا خبر پژوهشی تازه ندارد.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.