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

چهار واژه و چهار سند داریم. هر سطر یک واژه و هر ستون یک سند است. سطرهای ماتریس به ترتیب [2,0,2,0]، [0,1,0,1]، [2,0,2,0] و [0,1,0,1] هستند. بنابراین سندهای اول و سوم تکرارِ یک گروهاند و سندهای دوم و چهارم گروهِ دیگری دارند. هیچ واژهٔ واقعی یا قضاوتِ انسانیِ ارتباط به این شمارهها نسبت داده نشده است.
مقادیر، شمارشهای بدونِ واحدِ فیزیکیاند؛ ولتاژ، زمان یا احتمال نیستند. واژگان و ترتیبِ سطرها برای پرسش نیز باید همان باشد. کد، وزنهای حقیقیِ نامنفی را هم میپذیرد، ولی تبدیلِ متن، توکنسازی و ساختِ وزنها را انجام نمیدهد. عوض کردنِ ترتیبِ سطرها فقط در سند و ثابت گذاشتنِ پرسش، معنای محاسبه را خراب میکند، حتی اگر ابعاد برابر بمانند.
فصلِ نمایهسازیِ معناییِ نهفته در کتابِ بازیابیِ اطلاعاتِ استنفورد، نمایشِ کمرتبهٔ سند و بردنِ پرسش به فضای حاصل را شرح میدهد. از آن چارچوب برای قراردادِ مختصات استفاده میکنیم. مثالِ عددیِ این مقاله مستقل است؛ نتیجههای تاریخیِ منابع دربارهٔ مجموعههای واقعی، شاهدِ بهبودِ ارتباط در این چهار ستون محسوب نمیشوند.
ورودی باید آرایهای دوبعدی، ناتهی، حقیقی، متناهی و نامنفی باشد. bool در فهرست یا تاپلِ پایتون، حتی کنار عدد، پیش از تبدیل رد میشود؛ آرایهٔ NumPy با نوعِ bool نیز رد است. منشأ مقدارهای آرایهٔ عددیِ ازپیشتبدیلشده قابلِ تشخیص نیست. سقفِ آموزشی ۴۰۹۶ درایه و مقدارِ یک میلیون، حدِ NumPy نیست. داده به float64 کپی میشود؛ تغییرِ ورودی مدل را عوض نمیکند، اما دستکاریِ آرایههای داخلی پشتیبانی نمیشود.
۲. رتبهٔ بریده را با بازسازیِ دستی بفهمیم

تجزیه را به صورت A=UΣVᵀ مینویسیم. در حالتِ کاهشیافته، ستونهای U جهتهای فضای واژه، مقدارهای منفرد مقیاسها و سطرهای Vᵀ مختصاتِ اسناد را نگه میدارند. مستنداتِ NumPy 2.4 برای svd، شکلِ خروجیها و ترتیبِ نزولیِ مقدارهای منفرد را مشخص میکند. در کد، نامِ vh برای Vᵀ به کار رفته، چون ورودیِ ما حقیقی است.
دو بلوکِ تکراریِ مثال، دو جهتِ مستقل میسازند. در گروهِ اول، دو سطر و دو ستونِ فعال با مقدارِ دو داریم؛ مقدارِ منفردِ آن چهار است. بلوکِ دوم با مقدارِ یک، مقدارِ منفردِ دو میدهد. دو مقدارِ دیگر صفرند. پس رتبهٔ دقیقِ ماتریس دو است و بازسازی با هر دو مؤلفه باید همان ماتریسِ اولیه را برگرداند، تا خطای گردکردنِ عددی.
اگر تنها مؤلفهٔ بزرگتر را نگه داریم، سطرهای دوم و چهارمِ بازسازی صفر میشوند. چهار درایهٔ یک حذف شدهاند؛ مجموعِ مربعِ خطا چهار و نرمِ فروبنیوسِ خطا دو است. انرژیِ مربعیِ کلِ ماتریس بیست بود و شانزده واحدش باقی میماند. حفظِ این سهمِ بزرگ از انرژی، مانعِ حذفِ کاملِ یک گروه از اسناد نشده است.
در محاسبهٔ شناور، تشخیصِ صفر به سیاست نیاز دارد. مقدارِ منفرد زمانی فعال است که از یکتریلیونیمِ بزرگترین مقدار بیشتر باشد؛ k باید عددِ صحیحِ واقعی و مثبت و حداکثر برابرِ همین رتبهٔ عددی باشد. ماتریسِ صفر یا درخواستِ رتبهٔ سوم رد میشود. این آستانه قانونِ عمومیِ همهٔ دادهها نیست و با واحد و وزندهیِ مسئله باید بازبینی شود.
۳. پرسش و سند را به یک فضا ببریم

قراردادِ این درس، ستونهای Vᵀ بریده را نمایشِ سند میگیرد. پرسش نیز با qₖ=Σₖ⁻¹Uₖᵀq نگاشت میشود؛ تقسیم فقط بر مقدارهای فعال انجام میشود. اگر پرسش دقیقاً یک ستونِ آموزشی باشد، نگاشتِ آن باید ستونِ متناظرِ Vᵀ را بدهد. این برابری، شاهدی روشن برای سازگاریِ جهتِ ماتریس و مقیاسِ پرسش است.
میتوان قراردادِ دیگری با سندهای ΣVᵀ و پرسشِ Uᵀq ساخت، اما کسینوسهای آن لزوماً با قراردادِ حاضر برابر نیستند. حذفِ تقسیمِ پرسش و ثابت گذاشتنِ نمایشِ سند، مخلوط کردنِ دو هندسه است. در درسِ شباهتِ کسینوسیِ سندها، انتخابِ خودِ بردارها پیش از محاسبه اهمیت داشت؛ کاهشِ رتبه این مسئولیت را از بین نمیبرد.
پرسشِ گروهِ اول [2,0,2,0] در رتبهٔ دو، کسینوسهای یک، صفر، یک و صفر دارد. پرسشِ گروهِ دوم، ترتیبِ صفر، یک، صفر و یک میدهد. برای پرسشِ ترکیبی [2,1,2,1]، هر چهار امتیاز برابر با معکوسِ ریشهٔ دو است. این اعداد از ساختارِ متقارنِ مثال میآیند و برچسبِ کیفیت یا احتمالِ مرتبطبودن نیستند.
در رتبهٔ یک، پرسشِ گروهِ دوم و دو سندِ آن تقریباً به صفر میافتند. کسینوس با بردارِ صفر تعریف نمیشود؛ برنامه برای نرمِ حداکثرِ یکتریلیونیم، None میدهد. این مقدار را صفرِ شباهت یا نتیجهٔ منفی معنا نمیکنیم. پرسشِ صفر نیز همین سیاست را دارد. مرحلهٔ رتبهبندیِ نتایجِ نامعین در این مثال پیاده نشده است.
ورودیِ متناهی الزاماً مختصاتِ متناهی نمیسازد: تقسیم بر مقدارِ منفردِ بسیار کوچک ممکن است از دامنهٔ float64 بیرون برود. در این حالت، نگاشت FloatingPointError میدهد و امتیازِ NaN تولید نمیکند. اگر مختصات هنوز متناهی ولی بسیار بزرگ باشند، ابتدا بر بزرگترین قدرمطلق مقیاس میشوند؛ سپس طول و بردارِ واحد ساخته میشود تا مربعکردنِ مستقیم سرریز نکند. آستانهٔ صفر نیز بدون ساختنِ طولِ سرریزپذیر مقایسه میشود.
۴. محاسبه را با مرجعِ تحلیلیِ مستقل بسنجیم

کدِ کامل با Python 3.14.5 و NumPy 2.4.6 اجرا شده است. نسخهٔ مستندِ خواندهشده، شاخهٔ 2.4 است؛ شمارهٔ آن تاریخِ انتشارِ مقاله نیست. محاسبه از SVD کاهشیافته استفاده میکند. برای بررسی، به جای اجرایِ دوبارهٔ همان تجزیه به عنوانِ مرجع، پاسخِ تحلیلیِ دو بلوکِ جدا را به کار میبریم.
"""Small explicit LSI convention: rows terms, columns documents, docs Vh."""
from dataclasses import dataclass
import json
import platform
import numpy as np
RANK_RTOL = 1e-12
ZERO_NORM = 1e-12
ATOL = 1e-12
def counts(value, ndim):
# Reject booleans before NumPy can coerce a mixed Python sequence to ints.
def has_boolean(item):
if isinstance(item, (bool, np.bool_)):
return True
if isinstance(item, (list, tuple)):
return any(has_boolean(child) for child in item)
return isinstance(item, np.ndarray) and item.dtype.kind == 'b'
if has_boolean(value):
raise ValueError('boolean counts are not accepted')
try:
raw = np.asarray(value)
except (TypeError, ValueError) as exc:
raise ValueError('rectangular real numeric input required') from exc
if raw.dtype.kind not in 'iuf' or raw.ndim != ndim or any(n == 0 for n in raw.shape):
raise ValueError('nonempty real numeric shape required')
data = np.array(raw, dtype=np.float64, copy=True)
if not np.isfinite(data).all() or (data < 0).any():
raise ValueError('finite nonnegative counts required')
if (data > 1e6).any() or data.size > 4096:
raise ValueError('outside this small teaching resource profile')
return data
@dataclass(frozen=True)
class LSI:
u: np.ndarray
s: np.ndarray
documents: np.ndarray
singular_values: np.ndarray
numerical_rank: int
@classmethod
def fit(cls, matrix, k):
a = counts(matrix, 2)
if type(k) is not int or k < 1:
raise ValueError('k must be an actual positive int')
u, s, vh = np.linalg.svd(a, full_matrices=False)
rank = int(np.count_nonzero(s > RANK_RTOL * s[0])) if s[0] else 0
if k > rank:
raise ValueError('k exceeds numerical rank')
return cls(u[:, :k].copy(), s[:k].copy(), vh[:k].copy(), s.copy(), rank)
def reconstruct(self):
return (self.u * self.s) @ self.documents
def project(self, query):
q = counts(query, 1)
if q.shape != (self.u.shape[0],):
raise ValueError('query vocabulary size mismatch')
try:
with np.errstate(over='raise', invalid='raise', divide='raise'):
projected = (self.u.T @ q) / self.s
except FloatingPointError as exc:
raise FloatingPointError('query projection exceeds finite float64 range') from exc
if not np.isfinite(projected).all():
raise FloatingPointError('query projection is not finite')
return projected
def similarities(self, query):
qk = self.project(query)
uq = stable_unit_or_none(qk)
result = []
for column in self.documents.T:
ud = stable_unit_or_none(column)
result.append(None if uq is None or ud is None
else float(np.clip(np.dot(uq, ud), -1, 1)))
return tuple(result)
def stable_unit_or_none(vector):
# Scaling first prevents squared-norm overflow for finite huge coordinates.
scale = float(np.max(np.abs(vector)))
if scale == 0:
return None
scaled = vector / scale
length = float(np.linalg.norm(scaled))
# Compare norm to threshold without forming scale*length (may overflow).
return None if scale <= ZERO_NORM / length else scaled / length
def rejects(function, *args):
try:
function(*args)
except ValueError:
return True
return False
def close(a, b):
return bool(np.allclose(a, b, rtol=1e-12, atol=ATOL))
def readable(values):
return [None if x is None else round(float(x), 12) for x in values]
def run():
a = np.array([[2, 0, 2, 0], [0, 1, 0, 1],
[2, 0, 2, 0], [0, 1, 0, 1]], dtype=float)
one, two = LSI.fit(a, 1), LSI.fit(a, 2)
q_first, q_second, q_mixed = [2, 0, 2, 0], [0, 1, 0, 1], [2, 1, 2, 1]
# Independent analytic oracle: two disjoint repeated blocks.
# In Vh geometry the coordinates of a query (a,b,c,d) are
# ((a+c)/(4sqrt2), (b+d)/(2sqrt2)), up to joint basis signs.
# Compare sign-invariant cosines, never the LAPACK singular vectors.
from math import hypot
def analytic_scores(q):
x, y = (q[0]+q[2])/4, (q[1]+q[3])/2
length = hypot(x, y)
return (x/length, y/length, x/length, y/length) if length else (None,)*4
checks = {}
checks['analytic_singular_values_rank'] = close(two.singular_values, [4, 2, 0, 0]) and two.numerical_rank == 2
expected_one = np.array([[2,0,2,0],[0,0,0,0],[2,0,2,0],[0,0,0,0]], dtype=float)
checks['analytic_rank_one_reconstruction'] = close(one.reconstruct(), expected_one)
checks['rank_one_residual_two_and_energy'] = close(np.linalg.norm(a-one.reconstruct(), 'fro'), 2) and close(np.sum(a*a), 20)
checks['rank_two_reconstructs_original'] = close(two.reconstruct(), a)
checks['basis_orthonormality'] = close(two.u.T @ two.u, np.eye(2)) and close(two.documents @ two.documents.T, np.eye(2))
queries = [[i,j,k,l] for i in range(3) for j in range(3) for k in range(3) for l in range(3)]
checks['analytic_query_cosines_81_inside_ONE_group'] = all(
all(x is None and y is None or x is not None and y is not None and abs(x-y) <= 1e-12
for x,y in zip(two.similarities(q), analytic_scores(q))) for q in queries)
checks['training_document_folds_to_own_column'] = all(close(two.project(a[:, j]), two.documents[:, j]) for j in range(4))
checks['rank_one_loses_second_query_and_documents'] = one.similarities(q_second) == (None,)*4 and one.similarities(q_first)[1::2] == (None,None)
checks['zero_query_undefined_policy'] = two.similarities([0,0,0,0]) == (None,)*4
with_zero = LSI.fit(np.column_stack((a, np.zeros(4))), 2)
checks['zero_document_undefined_policy'] = with_zero.similarities(q_mixed)[-1] is None
swapped = LSI.fit(a[:, [3,2,1,0]], 2)
checks['document_permutation_covariance'] = close(swapped.similarities(q_mixed), tuple(reversed(two.similarities(q_mixed))))
flip = np.array([-1, 1])
signed = LSI(two.u*flip, two.s, flip[:, None]*two.documents, two.singular_values, 2)
checks['joint_basis_sign_invariance'] = close(signed.reconstruct(), a) and close(signed.similarities(q_mixed), two.similarities(q_mixed))
near = np.diag([1.0, 1e-14])
checks['rank_threshold_and_invalid_k'] = (LSI.fit(near,1).numerical_rank == 1 and rejects(LSI.fit,near,2)
and all(rejects(LSI.fit,a,k) for k in (0,-1,True,1.0,3)) and rejects(LSI.fit,np.zeros((2,2)),1))
checks['invalid_data_and_query_shape'] = (all(rejects(LSI.fit,x,1) for x in
([], [[1],[2,3]], [[-1]], [[float('nan')]], [[float('inf')]], [[1+2j]], [[True]], [['1']], [[1e7]]))
and all(rejects(two.project,q) for q in ([1,2], [[1,2,3,4]], [0,-1,0,0], [False]*4)))
snapshot = a.copy()
fitted = LSI.fit(a, 2)
a[0,0] = 9
checks['input_snapshot_no_alias'] = close(fitted.reconstruct(), snapshot)
a[0,0] = 2
from math import sqrt
tiny = LSI.fit([[1e-200]], 1)
tiny_two = LSI.fit(np.diag([1e-200, 2e-200]), 2)
checks['finite_huge_projection_stable_cosine'] = (
close(tiny.similarities([1.0]), [1.0])
and close(LSI.fit([[1e-100]], 1).similarities([1.0]), [1.0])
and close(tiny_two.similarities([1.0, 1.0]), [2/sqrt(5), 1/sqrt(5)]))
import warnings
overflow_rejected = False
with warnings.catch_warnings(record=True) as recorded:
warnings.simplefilter('always')
try:
LSI.fit([[1e-320]], 1).similarities([1.0])
except FloatingPointError:
overflow_rejected = True
checks['nonfinite_projection_explicit_error_no_warning'] = overflow_rejected and not recorded
checks['mixed_booleans_rejected_before_coercion'] = (
rejects(LSI.fit, [[True, 1]], 1)
and rejects(LSI.fit, ((np.bool_(False), 1),), 1)
and rejects(two.project, [True, 0, 1, 0])
and rejects(two.project, (0, np.bool_(True), 0, 1)))
assert all(type(v) is bool and v for v in checks.values()), checks
return {'python': platform.python_version(), 'numpy': np.__version__,
'orientation': 'rows=terms, columns=documents', 'matrix': a.tolist(),
'singular_values_rounded': readable(two.singular_values),
'numerical_rank': two.numerical_rank, 'rank_rtol': RANK_RTOL, 'zero_norm_policy_threshold': ZERO_NORM,
'rank_one_reconstruction_rounded': np.round(one.reconstruct(),12).tolist(),
'rank_one_frobenius_residual': round(float(np.linalg.norm(a-one.reconstruct(),'fro')),12),
'rank_two_frobenius_residual': round(float(np.linalg.norm(a-two.reconstruct(),'fro')),12),
'rank_two_first_query_cosines': readable(two.similarities(q_first)),
'rank_two_second_query_cosines': readable(two.similarities(q_second)),
'rank_two_mixed_query_cosines': readable(two.similarities(q_mixed)),
'rank_one_second_query_cosines': readable(one.similarities(q_second)),
'tiny_matrix_finite_large_projection_cosine': readable(tiny.similarities([1.0])),
'subnormal_matrix_projection_overflow_policy': 'FloatingPointError, no warning or NaN score',
'checks': checks}
if __name__ == '__main__':
print(json.dumps(run(), ensure_ascii=False, indent=2))
برای پرسشِ [a,b,c,d]، مختصاتِ تحلیلی در قراردادِ حاضر، تا علامتِ مشترکِ محورها، برابرِ (a+c)/(4√2) و (b+d)/(2√2) است. در کسینوس میتوان عاملِ مشترکِ ریشهٔ دو را حذف کرد. مرجع با همین دو جمع و دو مقیاس ساخته میشود؛ U و Vᵀ محاسبهشده را برای تولیدِ جواب نمیخواند.
تمامِ ۸۱ پرسش با مؤلفههای صفر، یک و دو در یک گروه با این مرجع سنجیده شدهاند. علامتِ ستونهای U ممکن است همراه با سطرهای Vᵀ برعکس شود؛ بازسازی و کسینوس تغییر نمیکنند. آزمونِ مستقلِ تغییرِ علامتِ مشترک نیز همین ناوردایی را بررسی میکند؛ هیچ شکلِ خاصی از علامتِ خروجیِ LAPACK معیارِ پذیرش نیست.
{
"python": "3.14.5",
"numpy": "2.4.6",
"orientation": "rows=terms, columns=documents",
"matrix": [
[
2.0,
0.0,
2.0,
0.0
],
[
0.0,
1.0,
0.0,
1.0
],
[
2.0,
0.0,
2.0,
0.0
],
[
0.0,
1.0,
0.0,
1.0
]
],
"singular_values_rounded": [
4.0,
2.0,
0.0,
-0.0
],
"numerical_rank": 2,
"rank_rtol": 1e-12,
"zero_norm_policy_threshold": 1e-12,
"rank_one_reconstruction_rounded": [
[
2.0,
0.0,
2.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0
],
[
2.0,
0.0,
2.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0
]
],
"rank_one_frobenius_residual": 2.0,
"rank_two_frobenius_residual": 0.0,
"rank_two_first_query_cosines": [
1.0,
0.0,
1.0,
0.0
],
"rank_two_second_query_cosines": [
0.0,
1.0,
0.0,
1.0
],
"rank_two_mixed_query_cosines": [
0.707106781187,
0.707106781187,
0.707106781187,
0.707106781187
],
"rank_one_second_query_cosines": [
null,
null,
null,
null
],
"tiny_matrix_finite_large_projection_cosine": [
1.0
],
"subnormal_matrix_projection_overflow_policy": "FloatingPointError, no warning or NaN score",
"checks": {
"analytic_singular_values_rank": true,
"analytic_rank_one_reconstruction": true,
"rank_one_residual_two_and_energy": true,
"rank_two_reconstructs_original": true,
"basis_orthonormality": true,
"analytic_query_cosines_81_inside_ONE_group": true,
"training_document_folds_to_own_column": true,
"rank_one_loses_second_query_and_documents": true,
"zero_query_undefined_policy": true,
"zero_document_undefined_policy": true,
"document_permutation_covariance": true,
"joint_basis_sign_invariance": true,
"rank_threshold_and_invalid_k": true,
"invalid_data_and_query_shape": true,
"input_snapshot_no_alias": true,
"finite_huge_projection_stable_cosine": true,
"nonfinite_projection_explicit_error_no_warning": true,
"mixed_booleans_rejected_before_coercion": true
}
}
هجده گروهِ نامدار موفق شدند؛ سه گروهِ دامنهٔ عددی و bool به پانزده گروهِ ساختاری افزوده شدهاند. برای ماتریسِ تکدرایهایِ یک ضربدر ده به توانِ منفی دویست و پرسشِ یک، مختصات بسیار بزرگ است ولی کسینوسِ درستِ یک باقی میماند. در توانِ منفی سیصدوبیست، نگاشت با خطای صریح و بدون هشدار رد میشود. تلورانسِ عددی در کد مشخص است؛ عدمِ همگراییِ کتابخانه نیز ممکن است بالا بیاید. سرعت و بازیابیِ واقعی سنجیده نشدهاند.
۵. چهار پرسش دربارهٔ اطلاعاتِ حذفشده و شباهت

پرسش اول: چرا با نگهداشتنِ بیشترِ انرژی، یک گروه ناپدید شد؟ پاسخ: هدفِ برش، کوچک کردنِ خطای ماتریسی در رتبهٔ محدود است؛ گروهِ دوم سهمِ کوچکتری از انرژی دارد. چهار شمارشِ یک حذف میشوند و باقیماندهٔ فروبنیوس دو میشود. این معیار هیچ الزامِ مستقیمی برای حفظِ همهٔ موضوعها یا پاسخگویی به همهٔ پرسشها ندارد.
پرسش دوم: آیا میتوان بر مقدارِ منفردِ صفر تقسیم کرد و رتبهٔ چهار ساخت؟ پاسخ: خیر؛ نگاشتِ معکوس در آن جهت تعریفِ پایدار ندارد. برنامه k بزرگتر از رتبهٔ عددی را رد میکند. شاهدِ قطری با مقدارهای یک و یکصدتریلیونیم نیز نشان میدهد مؤلفهٔ دوم با آستانهٔ اعلامشده حذف میشود؛ این سیاست باید همراهِ مدل ثبت شود.
پرسش سوم: برعکس شدنِ علامتِ مختصات یعنی مدل عوض شده است؟ پاسخ: اگر علامتِ محور در U و Vᵀ با هم برگردد، بازسازی، نگاشتِ سازگار و کسینوسها ثابت میمانند. مقایسهٔ خامِ تکتکِ بردارهای منفرد، معیارِ مناسبی برای تغییرِ رفتار نیست. برای مقدارهای منفردِ تکراری حتی جهتِ پایه میتواند بچرخد؛ آزمون باید کمیتِ موردِ استفاده را بررسی کند.
پرسش چهارم: افزودنِ سند با نگاشتِ ثابت، مدل را دوباره آموزش میدهد؟ پاسخ: خیر؛ فقط مختصاتِ آن سند در پایهٔ قبلی ساخته میشود. هموقوعیِ تازه و واژههای تازه خودکار واردِ پایه نمیشوند. بازخوردِ Rocchio نیز مسئلهٔ جداگانهٔ تغییرِ بردارِ پرسش است. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ انتخابِ رتبه و ادعای بهبودِ بازیابی به ارزیابیِ مستقلِ مجموعهٔ واقعی نیاز دارند.
منابع و مطالعهٔ بیشتر
- کتابِ بازیابیِ اطلاعاتِ استنفورد؛ نمایهسازیِ معناییِ نهفته
- NumPy2.4؛ تجزیهٔ مقدارِ منفرد با linalg.svd
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

