فهرست مقاله
فرض کنید در یک جستوجوی مهندسی، چند نتیجه را مرتبط و چند نتیجه را نامرتبط علامت زدهایم. یک استفادهٔ ممکن از این قضاوتها، تغییر نمایش عددیِ خودِ پرسش است تا مرحلهٔ بازیابی بعدی ورودی متفاوتی بگیرد. این درس بهروزرسانی Rocchio را با چهار سند ساختگی، حساب کسری و یک مرجع مستقل اجرا میکند. متن واقعی، رفتار کاربر، مدل آموزشدیده و اندازهگیری بهبود موتور جستوجو در مثال نداریم.
۱. بازخورد ارتباط را از امتیاز کسینوسی جدا کنیم

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

بخش دقیق الگوریتم Rocchio در کتاب، پرسش اولیه، مرکز سندهای مرتبط و مرکز سندهای نامرتبط را با سه ضریب ترکیب میکند. فرمولِ مورد استفاده raw = alpha*q + beta*mean(relevant) - gamma*mean(nonrelevant) است. هر میانگین مؤلفهبهمؤلفه ساخته میشود؛ تعداد اعضای هر گروه، مخرجِ همان گروه است.
پرسشِ دستی (1,1,0) است. دو سند مرتبط R=(2,0,2) و S=(0,2,2) هستند؛ مرکز آنها (1,1,2) میشود. دو سند نامرتبط N=(0,4,0) و M=(0,0,2) مرکز (0,2,1) را میسازند. همهٔ وزنها و ضرایب بیواحدند و جمع آنها تبدیل واحد فیزیکی ندارد.
ضرایبِ انتخابی ما (alpha,beta,gamma)=(1,1/2,1) هستند؛ این انتخاب برای آشکارکردن اثر تفریق است، نه توصیهٔ تنظیم سامانه. حاصلِ خام (3/2,-1/2,0) است. اگر بهجای مرکز، جمع گروه را بگذاریم، تعداد سندهای قضاوتشده ناخواسته وزن گروه را تغییر میدهد. تکرار کاملِ گروه با شناسههای تازه، مرکز آن را ثابت نگه میدارد؛ تکرار فقط یک عضو معمولاً چنین خاصیتی ندارد.
اگر گروهی تهی باشد، برنامه سهم آن را بردار صفر تعریف میکند؛ تقسیم بر صفر نمیکند و سند فرضی نمیسازد. بنابراین نبودن بازخوردِ منفی با داشتن سندی که قضاوتش معلوم نیست یکسان تفسیر نمیشود. سندِ بدون برچسب در فهرست میماند، اما وارد هیچ مرکز نمیشود. این سیاستِ صریحِ نمونه است؛ صورتِ کسریِ فرمول کتاب بهتنهایی حالت گروه تهی را تعریف نمیکند.
۳. مختصهٔ منفی و بردار صفر را سیاست طراحی بدانیم

ما پروفایلِ وزنهای واژگانیِ نامنفی را انتخاب کردهایم و پس از جمعِ کامل، هر مختصهٔ منفی را صفر میکنیم؛ همین برخورد با وزن منفی در بخش Rocchio کتاب آمده است. پس پرسش تازه (3/2,0,0) خواهد بود. این سیاست را نباید به هر فضای نهفتهسازی تعمیم داد؛ در نمایشی که مؤلفهٔ علامتدار معنا دارد، حذف منفیها میتواند قرارداد دیگری بسازد.
جای این برش مهم است. اگر هر سهمِ منفیِ بازخورد را پیش از جمع صفر کنیم، عملاً جریمه را حذف کردهایم و در مثال (3/2,3/2,1) میگیریم. برنامه هم حاصل خام و هم حاصل بریده را برمیگرداند تا این تفاوت قابل بررسی بماند. این دو بردار، دو گزارش از دو مرحلهٔ محاسبهاند؛ عدد منفیِ خام به معنی احتمال منفی نیست.
ممکن است همهٔ مختصهها صفر شوند. خروجیِ صفر در این تابع معتبر است و پرچم zero_query دارد؛ برنامه برایش کسینوس یا رتبهٔ ساختگی محاسبه نمیکند. در مرحلهٔ بعد میتوان با قرارداد مشخص از کاربر توضیح بیشتری خواست یا مسیر دیگری انتخاب کرد. برگشت خودکار به پرسش قبلی در این کد وجود ندارد و از موفقیت حساب هم نتیجه گرفته نمیشود.
۴. مثال دستی را با مرجع مستقل و کد کامل بسنجیم

فایل rocchio_feedback_vector.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. مستندات Fraction مبنای حساب گویا هستند؛ کسرها را از عدد صحیح و مخرج میسازیم و مقدار شناور را نمیپذیریم. این سختگیری قرارداد مثال است؛ خودِ سازندهٔ Fraction ورودیهای بیشتری میپذیرد و تبدیل یک اعشار شناور الزاماً همان کسر دهدهیِ مورد انتظار نیست.
"""Original finite Rocchio example; stdlib, no corpus or ranking evaluation."""
from fractions import Fraction as F
from itertools import product
from math import lcm
import json
import platform
def rational(value, maximum):
if type(value) not in (int, F):
raise TypeError('actual int or Fraction required')
value = F(value)
if not 0 <= value <= maximum or value.denominator > 16:
raise ValueError('outside finite nonnegative rational profile')
return value
def vector(value, dimension):
if type(value) is not tuple:
raise TypeError('actual tuple required')
if len(value) != dimension:
raise ValueError('shared dimension required')
return tuple(rational(x, 8) for x in value)
def rocchio(query, documents, relevant, nonrelevant, weights=(1, F(1, 2), 1)):
"""Return signed raw and clipped query; absent classes contribute zero."""
if type(query) is not tuple:
raise TypeError('actual query tuple required')
if not 1 <= len(query) <= 4:
raise ValueError('dimension 1..4 required')
q = vector(query, len(query))
if type(documents) is not tuple:
raise TypeError('actual document tuple required')
if len(documents) > 8:
raise ValueError('at most eight documents')
docs = {}
for record in documents:
if type(record) is not tuple or len(record) != 2:
raise ValueError('document record must be pair')
name, values = record
if type(name) is not str:
raise TypeError('actual identifier string required')
if not 1 <= len(name) <= 8 or not all('A' <= c <= 'Z' for c in name):
raise ValueError('identifier is 1..8 uppercase ASCII letters')
if name in docs:
raise ValueError('duplicate document identifier')
docs[name] = vector(values, len(q))
for group in (relevant, nonrelevant):
if type(group) is not tuple:
raise TypeError('actual judgment tuple required')
if len(group) > 8:
raise ValueError('at most eight judgments per group')
if any(type(name) is not str for name in group):
raise TypeError('judgment identifiers must be strings')
if len(set(group)) != len(group) or any(name not in docs for name in group):
raise ValueError('judgments must name distinct known documents')
if set(relevant) & set(nonrelevant):
raise ValueError('judgment classes must be disjoint')
if type(weights) is not tuple:
raise TypeError('actual weight tuple required')
if len(weights) != 3:
raise ValueError('alpha beta gamma required')
alpha, beta, gamma = (rational(w, 2) for w in weights)
def centroid(group):
if not group:
return (F(0),) * len(q)
return tuple(sum((docs[name][j] for name in group), F(0)) / len(group)
for j in range(len(q)))
positive, negative = centroid(relevant), centroid(nonrelevant)
raw = tuple(alpha * x + beta * r - gamma * n
for x, r, n in zip(q, positive, negative, strict=True))
clipped = tuple(max(F(0), x) for x in raw)
return {'relevant_centroid': positive, 'nonrelevant_centroid': negative,
'raw': raw, 'query': clipped, 'zero_query': not any(clipped)}
def integer_ledger_reference(query, documents, relevant, nonrelevant, weights):
"""Valid fixtures only: direct per-document coefficients, integer sums.
No centroid or main update invocation. Each column is accumulated on an
integer common denominator; Fraction is formed once at the column end.
"""
docs = dict(documents)
alpha, beta, gamma = map(F, weights)
coefficients = [(tuple(map(F, query)), alpha)]
if relevant:
coefficients += [(tuple(map(F, docs[name])), beta / len(relevant))
for name in relevant]
if nonrelevant:
coefficients += [(tuple(map(F, docs[name])), -gamma / len(nonrelevant))
for name in nonrelevant]
raw = []
for j in range(len(query)):
terms = [(v[j].numerator * c.numerator,
v[j].denominator * c.denominator) for v, c in coefficients]
denominator = lcm(*(b for _, b in terms))
numerator = sum(a * (denominator // b) for a, b in terms)
raw.append(F(numerator, denominator))
raw = tuple(raw)
return raw, tuple(x if x >= 0 else F(0) for x in raw)
def rejects(call, error):
try:
call()
except error:
return True
return False
def main():
q = (1, 1, 0)
docs = (('R', (2, 0, 2)), ('S', (0, 2, 2)),
('N', (0, 4, 0)), ('M', (0, 0, 2)))
rel, non = ('R', 'S'), ('N', 'M')
weights = (1, F(1, 2), 1)
result = rocchio(q, docs, rel, non, weights)
checks = {}
checks['literal_centroids_raw_and_clipped_hand_witness'] = (
result['relevant_centroid'] == (F(1), F(1), F(2))
and result['nonrelevant_centroid'] == (F(0), F(2), F(1))
and result['raw'] == (F(3, 2), F(-1, 2), F(0))
and result['query'] == (F(3, 2), F(0), F(0))
and result['zero_query'] is False)
# Clipping each negative contribution before summing would erase feedback.
premature = tuple(F(x) + F(1, 2) * r
for x, r in zip(q, (1, 1, 2), strict=True))
checks['negative_contribution_not_clipped_before_complete_sum'] = (
premature == (F(3, 2), F(3, 2), F(1))
and premature != result['query'])
checks['absent_classes_are_zero_contributions_not_division_errors'] = (
rocchio(q, docs, (), (), weights)['query'] == tuple(map(F, q))
and rocchio(q, docs, ('R',), (), weights)['raw'] == (F(2), F(1), F(1))
and rocchio(q, docs, (), non, weights)['raw'] == (F(1), F(-1), F(-1)))
zero = rocchio((0, 0), (('N', (1, 2)),), (), ('N',))
checks['zero_output_is_flagged_and_signed_raw_is_preserved'] = (
zero['raw'] == (F(-1), F(-2)) and zero['query'] == (F(0), F(0))
and zero['zero_query'] is True
and rocchio(q, docs, rel, non, (0, 0, 0))['zero_query'] is True)
checks['document_and_judgment_order_do_not_change_centroids'] = (
rocchio(q, docs[::-1], rel[::-1], non[::-1], weights) == result)
doubled = docs + (('T', (2, 0, 2)), ('U', (0, 2, 2)))
checks['replicating_entire_relevant_class_preserves_its_mean'] = (
rocchio(q, doubled, ('R', 'S', 'T', 'U'), non, weights) == result)
swap = (2, 0, 1)
permuted = rocchio(tuple(q[i] for i in swap),
tuple((n, tuple(v[i] for i in swap)) for n, v in docs),
rel, non, weights)
checks['shared_feature_permutation_permutates_raw_and_query'] = all(
permuted[key] == tuple(result[key][i] for i in swap)
for key in ('raw', 'query', 'relevant_centroid', 'nonrelevant_centroid'))
grid = ((0, 0), (1, 0), (0, 1), (1, 1))
profiles = ((1, F(1, 2), 1), (1, 0, 0), (0, 1, 1),
(F(1, 2), 1, F(1, 2)))
cases, agree = 0, True
for query, first, second, assignments, w in product(
grid, grid, grid, tuple(product(range(3), repeat=2)), profiles):
entries = (('A', first), ('B', second))
positive = tuple(n for (n, _), a in zip(entries, assignments, strict=True) if a == 1)
negative = tuple(n for (n, _), a in zip(entries, assignments, strict=True) if a == 2)
actual = rocchio(query, entries, positive, negative, w)
expected_raw, expected_query = integer_ledger_reference(
query, entries, positive, negative, w)
agree &= actual['raw'] == expected_raw and actual['query'] == expected_query
agree &= actual['zero_query'] == (expected_query == (F(0), F(0)))
cases += 1
checks['independent_integer_ledger_2304_cases_one_group'] = bool(agree and cases == 2304)
checks['fractional_input_is_exact_without_float_coercion'] = (
rocchio((F(1, 3),), (('R', (F(2, 3),)),), ('R',), (),
(F(1, 2), F(1, 4), 0))['raw'] == (F(1, 3),))
checks['unknown_duplicate_and_contradictory_judgments_rejected'] = all((
rejects(lambda: rocchio(q, docs, ('X',), (), (0, 0, 0)), ValueError),
rejects(lambda: rocchio(q, docs, ('R', 'R'), ()), ValueError),
rejects(lambda: rocchio(q, docs, ('R',), ('R',)), ValueError),
rejects(lambda: rocchio(q, docs, (True,), ()), TypeError)))
class TupleSubclass(tuple):
pass
checks['vector_types_shapes_and_nonnegative_rational_domain'] = all((
rejects(lambda: rocchio([1], (), (), ()), TypeError),
rejects(lambda: rocchio(TupleSubclass((1,)), (), (), ()), TypeError),
rejects(lambda: rocchio((), (), (), ()), ValueError),
rejects(lambda: rocchio((0,) * 5, (), (), ()), ValueError),
rejects(lambda: rocchio((True,), (), (), ()), TypeError),
rejects(lambda: rocchio((0.5,), (), (), ()), TypeError),
rejects(lambda: rocchio((F(1, 17),), (), (), ()), ValueError),
rejects(lambda: rocchio((-1,), (), (), ()), ValueError),
rejects(lambda: rocchio((9,), (), (), ()), ValueError),
rejects(lambda: rocchio((1,), (('A', (1, 2)),), (), ()), ValueError)))
checks['document_identity_and_weight_profiles_checked'] = all((
rejects(lambda: rocchio(q, docs + (docs[0],), (), ()), ValueError),
rejects(lambda: rocchio(q, (('bad', (0, 0, 0)),), (), ()), ValueError),
rejects(lambda: rocchio(q, docs, (), (), (True, 0, 0)), TypeError),
rejects(lambda: rocchio(q, docs, (), (), (3, 0, 0)), ValueError),
rejects(lambda: rocchio(q, docs, (), (), (0, 0)), ValueError)))
extreme_docs = tuple((chr(65 + i), (8, F(1, 16), 0, 8)) for i in range(8))
extreme = rocchio((8, 8, 8, 8), extreme_docs,
tuple(n for n, _ in extreme_docs), (), (2, 2, 2))
checks['inclusive_bounds_and_empty_document_catalogue'] = (
extreme['query'] == (F(32), F(129, 8), F(16), F(32))
and rocchio((1,), (), (), ())['query'] == (F(1),)
and rejects(lambda: rocchio((1,), tuple((chr(65+i), (1,)) for i in range(9)), (), ()), ValueError))
derived = rocchio((0,), (('R', (0,)), ('S', (0,)), ('T', (F(1, 16),))),
('R', 'S', 'T'), (), (0, F(1, 3), 0))
checks['input_denominator_bound_does_not_truncate_derived_rationals'] = (
derived['relevant_centroid'] == (F(1, 48),)
and derived['raw'] == (F(1, 144),)
and derived['query'] == (F(1, 144),)
and rejects(lambda: rocchio((0,), (('R', (F(1, 17),)),),
('R',), (), (0, F(1, 3), 0)), ValueError))
assert all(type(value) is bool for value in checks.values())
assert all(checks.values()), checks
packed = {key: [str(x) for x in value] if type(value) is tuple else value
for key, value in result.items()}
output = {
'runtime': {'python': platform.python_version(), 'dependencies': 'stdlib only'},
'contract': {'coordinates': 'dimensionless nonnegative actual int/Fraction,0..8,denominator<=16',
'dimension': '1..4', 'documents': '0..8 unique IDs',
'weights': 'alpha,beta,gamma actual int/Fraction,0..2,denominator<=16',
'absent_class': 'zero vector contribution',
'clipping': 'after complete signed sum, negative coordinates to zero',
'zero_output': 'valid flagged result, no cosine call'},
'fixture': {'query': list(q), 'documents': docs, 'relevant': rel,
'nonrelevant': non, 'weights': [str(x) for x in weights]},
'result': packed,
'premature_clipping_wrong_result': [str(x) for x in premature],
'oracle_cases_inside_one_group': cases,
'checks': checks, 'check_count': len(checks), 'all_passed': all(checks.values()),
'limits': ['Synthetic supplied vectors and judgments; no logs, corpus or model',
'No normalization, ranking or quality-improvement measurement',
'Nonnegative lexical-profile clipping is not universal embedding policy',
'Exact finite rational checks are not a proof of all real inputs']}
print(json.dumps(output, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
بُعد از یک تا چهار و فهرست سندها حداکثر هشت عضو دارد. هر مختصه بین صفر و هشت و هر ضریب بین صفر و دو است؛ مخرج ورودی حداکثر شانزده است. این حد برای ورودی است، نه وعدهٔ باقیماندن مخرجِ حاصل در همان محدوده. شناسهها یکتا و گروههای قضاوت جدا هستند؛ برچسب ناشناخته، تکرار شناسه، ناسازگاری بُعد و bool رد میشوند. حتی ضریب صفر، قضاوت نامعتبر را مجاز نمیکند.
مرجع مستقل ابتدا به هر سند ضریبِ سهمِ خودش را میدهد، سپس برای هر ستون مخرج مشترک میسازد و صورتهای صحیح را جمع میکند؛ تابعِ مرکز یا بهروزرسانیِ اصلی را فراخوانی نمیکند. چهار پرسش، شانزده جفت سند، نه حالت برچسبگذاری و چهار تنظیمِ ضریب، ۲۳۰۴ مقایسه در یک گروه آزمون میسازند. پاسخ دستی، جابهجایی ترتیب ورودی، جایگشت مشترک ویژگیها و سیاست گروه تهی نیز جدا بررسی شدهاند.
{
"runtime": {
"python": "3.14.5",
"dependencies": "stdlib only"
},
"contract": {
"coordinates": "dimensionless nonnegative actual int/Fraction,0..8,denominator<=16",
"dimension": "1..4",
"documents": "0..8 unique IDs",
"weights": "alpha,beta,gamma actual int/Fraction,0..2,denominator<=16",
"absent_class": "zero vector contribution",
"clipping": "after complete signed sum, negative coordinates to zero",
"zero_output": "valid flagged result, no cosine call"
},
"fixture": {
"query": [
1,
1,
0
],
"documents": [
[
"R",
[
2,
0,
2
]
],
[
"S",
[
0,
2,
2
]
],
[
"N",
[
0,
4,
0
]
],
[
"M",
[
0,
0,
2
]
]
],
"relevant": [
"R",
"S"
],
"nonrelevant": [
"N",
"M"
],
"weights": [
"1",
"1/2",
"1"
]
},
"result": {
"relevant_centroid": [
"1",
"1",
"2"
],
"nonrelevant_centroid": [
"0",
"2",
"1"
],
"raw": [
"3/2",
"-1/2",
"0"
],
"query": [
"3/2",
"0",
"0"
],
"zero_query": false
},
"premature_clipping_wrong_result": [
"3/2",
"3/2",
"1"
],
"oracle_cases_inside_one_group": 2304,
"checks": {
"literal_centroids_raw_and_clipped_hand_witness": true,
"negative_contribution_not_clipped_before_complete_sum": true,
"absent_classes_are_zero_contributions_not_division_errors": true,
"zero_output_is_flagged_and_signed_raw_is_preserved": true,
"document_and_judgment_order_do_not_change_centroids": true,
"replicating_entire_relevant_class_preserves_its_mean": true,
"shared_feature_permutation_permutates_raw_and_query": true,
"independent_integer_ledger_2304_cases_one_group": true,
"fractional_input_is_exact_without_float_coercion": true,
"unknown_duplicate_and_contradictory_judgments_rejected": true,
"vector_types_shapes_and_nonnegative_rational_domain": true,
"document_identity_and_weight_profiles_checked": true,
"inclusive_bounds_and_empty_document_catalogue": true,
"input_denominator_bound_does_not_truncate_derived_rationals": true
},
"check_count": 14,
"all_passed": true,
"limits": [
"Synthetic supplied vectors and judgments; no logs, corpus or model",
"No normalization, ranking or quality-improvement measurement",
"Nonnegative lexical-profile clipping is not universal embedding policy",
"Exact finite rational checks are not a proof of all real inputs"
]
}
در مجموع چهارده گروهِ نامدار واقعاً اجرا شدهاند؛ تعداد حالتهای داخل یک گروه به تعداد گروهها اضافه نشده است. مقایسهها در این دامنه کسری و دقیقاند و تلورانس شناور ندارند. با این حال، موفقیت این حالتهای محدود اثبات همهٔ ورودیهای ممکن یا همهٔ مدلهای بازیابی نیست. هیچ کتابخانهٔ جستوجو، سرویس مدل یا آزمایش سرعت در اجرای ثبتشده حضور ندارد.
۵. تغییر پرسش را بهبود کیفیت تضمینشده ننامیم

این تابع فقط پرسش را تغییر میدهد. در درس بازرتبهبندی MMR، مجموعهٔ نامزدها و امتیازهایشان داده شده بود و انتخاب بعدی به انتخابهای قبلی وابسته میشد. آن مرحله جایگزین قضاوت ارتباط یا این بهروزرسانی نیست. برای سنجش مفیدبودن پرسش تازه باید بازیابی و ارزیابی با دادهٔ جدا انجام شود؛ چنین نتیجهای در مثال نداریم.
پرسش اول: چرا مختصهٔ نرمافزار در حاصل خام صفر شد؟ پاسخ: سهم پرسش صفر، سهم مثبت (1/2)*2=1 و سهم منفی 1*1=1 است؛ تفاضل صفر میشود. این صفر حاصل حساب است و نبود دانش نرمافزار در سندهای واقعی را ثابت نمیکند.
پرسش دوم: اگر هیچ قضاوتی نباشد چه میشود؟ پاسخ: دو سهم گروه صفرند و حاصل alpha*q است؛ با آلفای یک همین پرسش اولیه میماند. گروه تهی را با تقسیم بر صفر محاسبه نمیکنیم.
پرسش سوم: آیا دو برابر کردن اعضای هر دو گروه نتیجه را دو برابر میکند؟ پاسخ: اگر کل هر گروه با همان بردارها و شناسههای تازه تکرار شود، مرکزها ثابتاند و پرسش تازه تغییر نمیکند. دوبار فرستادن همان شناسه مجاز نیست؛ افزودن انتخابیِ یک سند هم لزوماً مرکز را حفظ نمیکند.
پرسش چهارم: آیا نتیجهٔ صفر یعنی هیچ سند مرتبطی وجود ندارد؟ پاسخ: خیر؛ فقط ترکیبِ همین بردارها، ضرایب و برش خروجی صفر داده است. برای مثال، پرسش صفر و بازخورد منفیِ (1,2) حاصل خام (-1,-2) و خروجی صفر میدهند؛ دربارهٔ کل مجموعه حکم نمیدهیم.
منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند. کتاب حقنشر ۲۰۰۸ و صفحههای HTML تاریخ تولید ۲۰۰۹ دارند؛ اینها خبر تازه نیستند. مستندات Python 3.14.8، تاریخ بهروزرسانی صفحه و اجرای واقعی 3.14.5 جدا ثبت شدهاند. بردارها و قضاوتها تألیفیاند؛ هیچ ادعایی دربارهٔ مشتری، محصول، محبوبیت یا کیفیت اندازهگیریشدهٔ شرکت از آنها ساخته نشده است.
منابع و مطالعهٔ بیشتر
- کتاب بازیابی اطلاعات؛ فرمول Rocchio و حذف وزن منفی در پروفایل واژگانی
- کتاب بازیابی اطلاعات؛ فضای مشترک بردارها و تمایز از امتیاز کسینوسی
- Python 3.14؛ حساب دقیق گویا با Fraction
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

