فهرست مقاله
قرار گرفتنِ یک مرحلهٔ نرمالسازی میانِ تولیدِ ویژگی و محاسبهٔ شباهت، قراردادِ نمایش را تغییر میدهد. برابر بودنِ شکلِ آرایه کافی نیست؛ باید بدانیم کدام مؤلفهها با هم مقایسه میشوند و چه اطلاعاتی کنار میرود. در این درس، بردارهای کوچکِ تألیفی را با هستهٔ LayerNorm تبدیل میکنیم و تفاوتِ آن را با نرمالسازیِ طول نشان میدهیم. هیچ متنِ واقعی، مدلِ آموزشدیده یا سامانهٔ بازیابی ارزیابی نشده است.
۱. هر سطر، یک بردارِ ویژگی با قراردادِ محدود

هر سطرِ ورودی، ویژگیهای یک نمونه است و ستونها جایگاههای ثابتِ همان ویژگیها هستند. بردارهای [1,3] و [2,2] را در نظر بگیرید. اعداد بدونِ واحدِ فیزیکیاند؛ ترکیبِ مستقیمِ ولتاژ و زمان در یک بردار، صرفاً با اجرای این تابع توجیه نمیشود. ترتیب، مقیاس و معنای ویژگیها باید در ماژولِ تولیدکننده مشخص باشند؛ نرمالسازی این مسئولیت را به عهده نمیگیرد.
دامنهٔ برنامه عمداً کوچک است: یک تا هشت سطر، دو تا هشت ویژگی در هر سطر و طولِ یکسانِ سطرها. ظرفها دقیقاً tuple و مؤلفهها دقیقاً int در بازهٔ منفیِ ۳۲ تا مثبتِ ۳۲ هستند. bool، زیرکلاسِ عدد، float، NaN و رشته پذیرفته نمیشوند. این محدودیت، سیاستِ مثال برای بازبینیِ روشن است؛ محدودیتِ عمومیِ LayerNorm یا چارچوبهای یادگیری نیست.
مستندِ LayerNorm در PyTorch 2.14، محاسبه روی آخرین ابعادِ تعیینشده و استفاده از آمارِ خودِ ورودی در آموزش و ارزیابی را شرح میدهد. اینجا فقط یک محورِ ویژگی داریم و مرحلهٔ آفین را حذف کردهایم؛ gamma و beta یادگرفتنی، گرادیان و اجرای PyTorch در برنامه وجود ندارند. بنابراین خروجی، آزمونِ برابریِ کاملِ یک لایهٔ چارچوب نیست.
۲. میانگین و واریانس روی کدام محورند؟

برای سطرِ x با N ویژگی، میانگین μ برابرِ مجموعِ مؤلفهها تقسیم بر N است. سپس v را از میانگینِ مربعِ فاصلهها تا μ میسازیم و هر خروجی را yᵢ=(xᵢ−μ)/√(v+ε) میگیریم. مخرجِ واریانس N است، نه N−1. واژهٔ biased در مستندِ PyTorch به همین قرارداد اشاره دارد؛ هدف، برآوردِ بیاریبِ واریانسِ یک جامعهٔ آماری نیست.
رابطِ LayerNormalization در Keras 3 نیز میانگین و واریانسِ هر نمونه را با تقسیم بر تعدادِ ویژگیها مینویسد و محورهای باقیمانده، از جمله محورِ دسته، را جدا میکند. برای [1,3]، میانگین دو و واریانس یک است. با ε=1/4، خروجی دقیقاً [−2/√5,2/√5] و تقریباً [−0.894427191,0.894427191] میشود؛ سطرِ [2,2] به صفر میرسد.
اگر همین ماتریس را اشتباهاً ستونبهستون نرمال کنیم، خروجیِ سطرِ نخست حدودِ [−0.707107,0.707107] و سطرِ دوم برعکسِ آن میشود. صفر بودنِ سطرِ ثابت از دست رفته است. در قراردادِ درست، افزودن یا جابهجاییِ نمونههای دیگر نتیجهٔ یک سطر را عوض نمیکند؛ جابهجاییِ ویژگیها نیز فقط جایِ مؤلفههای خروجی را همراهِ همان ترتیب تغییر میدهد.
اپسیلونِ نمونه بزرگ و آموزشی انتخاب شده تا اثرش دیده شود. برنامه فقط Fraction مثبت در بازهٔ 1/1024 تا 16 با مخرجِ حداکثرِ ۱۰۲۴ میپذیرد. مقدارِ پیشفرضِ دو رابطِ خواندهشده یکسان نیست؛ پس این انتخاب را پیشفرضِ مشترکِ کتابخانهها معرفی نمیکنیم. گذاشتنِ ε بیرونِ ریشه، یا جایگزینیِ N−1، دو محاسبهٔ متفاوت میسازد: در مثالِ نخست، اندازهٔ مؤلفه بهترتیب 0.8 یا 2/3 خواهد شد.
۳. اطلاعاتِ حذفشده و شرطِ مقیاس

بردارِ [8,10] نیز دقیقاً همان نمایشِ [1,3] را میدهد؛ میانگینِ نُه به جایِ دو حذف میشود. پس از روی خروجی نمیتوان جابهجاییِ مشترکِ اولیه را تعیین کرد. این شاهدِ محدودیتِ نمایش است، نه اثباتِ بد بودنِ آن برای هر کاربرد. اگر سطحِ مطلقِ ویژگی معنا دارد، حذفِ آن باید تصمیمی آگاهانه و قابلآزمون در قراردادِ ماژولها باشد.
در درسِ بردارِ سند و شباهتِ کسینوسی، تقسیم بر طولِ بردار مطرح بود؛ آن کار میانگین را کم نمیکند. بردارِ واحدِ [1,3] مؤلفههای مثبت و مجموعِ مربعِ یک دارد، اما خروجیِ حاضر مؤلفهٔ منفی نیز دارد و مجموعِ مربعش 8/5 است. واریانسِ خروجی v/(v+ε)، یعنی 4/5 است؛ مثبت بودنِ ε اجازه نمیدهد بیقید بگوییم واریانس دقیقاً یک شده است.
دو برابر کردنِ ورودی با ε ثابت، خروجی را به حدودِ [−0.970143,0.970143] میرساند. برای بردارِ غیرثابت و ضریبِ مثبتِ a، حفظِ خروجی مستلزمِ مقیاسکردنِ همزمانِ ε با a² است؛ این رابطه در حسابِ حقیقیِ فرمول برقرار است و کد در دامنهٔ محدود با تلورانس آن را میسنجد. در ضریبِ منفی، علامتِ خروجی نیز برمیگردد. این نتیجه را به دقتِ نامحدودِ محاسباتِ شناور تعمیم نمیدهیم.
ادغامِ آفینِ BatchNorm در استنتاج به آمارِ ثابت تکیه داشت. اینجا میانگین و واریانس به سطرِ جاری وابستهاند؛ آزمونِ نقطهٔ میانی نشان میدهد کلِ نگاشت، یک تبدیلِ آفینِ ثابت روی همهٔ ورودیها نیست. حذفِ مرحله با همان استدلالِ آمارِ ثابت درست نخواهد بود، حتی اگر شکلِ ورودی و خروجی برابر بماند.
۴. کدِ کامل و مرجعِ مستقلِ اختلافهای جفتی

کد با CPython 3.14.5 و فقط کتابخانهٔ استاندارد اجرا شده است. مستنداتِ خواندهشده PyTorch 2.14 و Keras 3 هستند؛ نسخهٔ دقیقِ فرعیِ Keras در صفحه نمایش داده نشده و هیچکدام اجرا نشدهاند. تابعِ اصلی از fsum و ریشهٔ شناور استفاده میکند؛ دامنهٔ کوچک، ادعای پایداری برای اعدادِ بسیار بزرگ یا ورودیِ دلخواه ایجاد نمیکند.
"""Bounded per-row LayerNorm core; no learned affine stage or framework call."""
from decimal import Decimal, localcontext
from fractions import Fraction
from itertools import permutations, product
from math import fsum, isclose, isfinite, sqrt
import json
import platform
ABS_TOL = 2e-12
REL_TOL = 2e-12
DECIMAL_PRECISION = 70
MAX_ROWS = 8
MAX_FEATURES = 8
MAX_ABS_FEATURE = 32
def validate(rows, epsilon):
if type(rows) is not tuple or not 1 <= len(rows) <= MAX_ROWS:
raise ValueError('rows must be an exact tuple of one to eight rows')
width = None
for row in rows:
if type(row) is not tuple or not 2 <= len(row) <= MAX_FEATURES:
raise ValueError('each row must be an exact tuple of two to eight features')
if width is None:
width = len(row)
if len(row) != width:
raise ValueError('all rows must have the same feature count')
if any(type(x) is not int or abs(x) > MAX_ABS_FEATURE for x in row):
raise ValueError('features must be exact ints in [-32,32]')
if (type(epsilon) is not Fraction or not Fraction(1, 1024) <= epsilon <= 16
or epsilon.denominator > 1024):
raise ValueError('epsilon must be a Fraction in [1/1024,16], denominator <=1024')
def normalize_features(rows, epsilon):
"""Each row is independent; variance denominator is its feature count."""
validate(rows, epsilon)
result = []
for row in rows:
mean = fsum(row) / len(row)
centered = tuple(x - mean for x in row)
variance = fsum(x * x for x in centered) / len(row)
denominator = sqrt(variance + float(epsilon))
result.append(tuple(x / denominator for x in centered))
return tuple(result)
def pairwise_fraction_oracle(row, epsilon):
"""Independent bounded-fixture arithmetic, not a second centered-square loop."""
n = len(row)
mean = Fraction(sum(row), n)
variance = Fraction(sum((row[i] - row[j]) ** 2
for i in range(n) for j in range(i + 1, n)), n * n)
with localcontext() as context:
context.prec = DECIMAL_PRECISION
decimal = lambda q: Decimal(q.numerator) / Decimal(q.denominator)
denominator = decimal(variance + epsilon).sqrt()
output = tuple(float(decimal(Fraction(x) - mean) / denominator) for x in row)
return mean, variance, output
def close(a, b):
return len(a) == len(b) and all(isclose(x, y, rel_tol=REL_TOL, abs_tol=ABS_TOL)
for x, y in zip(a, b))
def rejects(rows, epsilon):
try:
normalize_features(rows, epsilon)
except ValueError:
return True
return False
def rounded(rows):
return [[round(x, 12) for x in row] for row in rows]
def run():
epsilon = Fraction(1, 4)
rows = ((1, 3), (2, 2))
observed = normalize_features(rows, epsilon)
checks = {}
checks['literal_two_feature_and_constant_row'] = (
close(observed[0], (-2 / sqrt(5), 2 / sqrt(5))) and observed[1] == (0.0, 0.0))
oracle_cases = 0
oracle_ok = True
for n in (2, 3, 4):
for row in product(range(-2, 3), repeat=n):
for eps in (Fraction(1, 4), Fraction(1), Fraction(4)):
reference = pairwise_fraction_oracle(row, eps)[2]
oracle_ok = oracle_ok and close(normalize_features((row,), eps)[0], reference)
oracle_cases += 1
checks['independent_pairwise_fraction_decimal_grid_ONE_group'] = oracle_ok and oracle_cases == 2325
source = (-3, 0, 2, 5)
baseline = normalize_features((source,), epsilon)[0]
orders = list(permutations(range(4)))
checks['feature_permutation_covariance_24_orders'] = all(
close(normalize_features((tuple(source[i] for i in p),), epsilon)[0],
tuple(baseline[i] for i in p)) for p in orders)
distractors = ((-32, 32), (1, 3), (0, 0))
mixed = normalize_features(distractors, epsilon)
checks['row_independence_and_batch_permutation'] = (
mixed[1] == observed[0] and all(normalize_features((r,), epsilon)[0] == y
for r, y in zip(distractors, mixed))
and normalize_features(tuple(reversed(distractors)), epsilon) == tuple(reversed(mixed)))
shifted = normalize_features(((8, 10),), epsilon)[0]
checks['common_offset_collision_loses_original_mean'] = (
shifted == observed[0] and pairwise_fraction_oracle((1, 3), epsilon)[0] == 2
and pairwise_fraction_oracle((8, 10), epsilon)[0] == 9)
doubled_fixed = normalize_features(((2, 6),), epsilon)[0]
doubled_scaled = normalize_features(((2, 6),), Fraction(1))[0]
negative_scaled = normalize_features(((-2, -6),), Fraction(1))[0]
checks['fixed_epsilon_scale_change_and_scaled_epsilon_equivalence'] = (
not close(doubled_fixed, observed[0]) and close(doubled_fixed, (-4 / sqrt(17), 4 / sqrt(17)))
and close(doubled_scaled, observed[0]) and close(negative_scaled, tuple(-x for x in observed[0])))
moment_rows = ((1, 3), (-3, 0, 2, 5), (4, 4), (-32, 0, 32))
moment_ok = True
for row in moment_rows:
y = normalize_features((row,), epsilon)[0]
exact_variance = pairwise_fraction_oracle(row, epsilon)[1]
moment_ok = moment_ok and isclose(fsum(y) / len(y), 0, abs_tol=ABS_TOL)
moment_ok = moment_ok and isclose(fsum(v * v for v in y) / len(y),
float(exact_variance / (exact_variance + epsilon)),
rel_tol=REL_TOL, abs_tol=ABS_TOL)
checks['zero_mean_variance_ratio_not_automatically_one'] = moment_ok
unbiased_wrong = (-1 / sqrt(2 + float(epsilon)), 1 / sqrt(2 + float(epsilon)))
outside_wrong = (-1 / (1 + float(epsilon)), 1 / (1 + float(epsilon)))
checks['unbiased_variance_and_outside_sqrt_epsilon_are_different_contracts'] = (
close(unbiased_wrong, (-2 / 3, 2 / 3)) and close(outside_wrong, (-0.8, 0.8))
and not close(observed[0], unbiased_wrong) and not close(observed[0], outside_wrong))
columns = tuple(zip(*rows))
wrong_axis = tuple(zip(*normalize_features(columns, epsilon)))
checks['feature_axis_not_batch_axis_literal_counterexample'] = (
close(wrong_axis[0], (-1 / sqrt(2), 1 / sqrt(2)))
and close(wrong_axis[1], (1 / sqrt(2), -1 / sqrt(2)))
and not close(wrong_axis[0], observed[0]) and not close(wrong_axis[1], observed[1]))
length_unit = (1 / sqrt(10), 3 / sqrt(10))
checks['centered_layernorm_is_not_L2_unit_length'] = (
not close(length_unit, observed[0]) and isclose(fsum(x * x for x in length_unit), 1,
rel_tol=REL_TOL, abs_tol=ABS_TOL)
and isclose(fsum(x * x for x in observed[0]), 8 / 5, rel_tol=REL_TOL, abs_tol=ABS_TOL))
a = normalize_features(((-2, 2),), epsilon)[0]
b = normalize_features(((2, 2),), epsilon)[0]
midpoint = normalize_features(((0, 2),), epsilon)[0]
checks['input_dependent_moments_break_a_single_fixed_affine_map'] = not close(
midpoint, tuple((u + v) / 2 for u, v in zip(a, b)))
class IntChild(int):
pass
class TupleChild(tuple):
pass
invalid = [([], epsilon), ((), epsilon), (((1,),), epsilon), (((1, 2), (3,)), epsilon),
(([1, 2],), epsilon), (TupleChild(((1, 2),)), epsilon),
(((True, 2),), epsilon), (((1.0, 2),), epsilon), (((IntChild(1), 2),), epsilon),
((('1', 2),), epsilon), (((float('nan'), 2),), epsilon),
(((float('inf'), 2),), epsilon), (((1 + 0j, 2),), epsilon),
(((33, 0),), epsilon), (((-33, 0),), epsilon), (((0,) * 9,), epsilon),
((((0, 0),) * 9), epsilon), (((0, 0),), True), (((0, 0),), 0.25),
(((0, 0),), 1), (((0, 0),), Fraction(0)), (((0, 0),), Fraction(-1)),
(((0, 0),), Fraction(1, 2048)), (((0, 0),), Fraction(17)),
(((0, 0),), Fraction(1025, 2048))]
checks['strict_finite_types_shape_and_epsilon_resource_policy'] = all(rejects(r, e) for r, e in invalid)
maxima = (tuple((-32, 32) * 4),) * 8
before = repr(maxima)
extreme_outputs = [normalize_features(maxima, eps) for eps in (Fraction(1, 1024), Fraction(16))]
checks['max_profile_finite_boundary_epsilon_and_input_preservation'] = (
repr(maxima) == before and all(len(batch) == 8 and all(len(r) == 8 for r in batch)
for batch in extreme_outputs)
and all(isfinite(x) for batch in extreme_outputs for row in batch for x in row))
assert all(type(value) is bool and value for value in checks.values()), checks
return {'python_version': platform.python_version(), 'implementation': platform.python_implementation(),
'dependencies': 'standard library only; PyTorch and Keras not executed',
'profile': {'rows': '1..8 exact tuple rows', 'features': '2..8 equal-width exact tuple columns',
'values': 'exact int [-32,32], unitless supplied features',
'epsilon': 'exact Fraction [1/1024,16], denominator <=1024',
'affine_stage': False},
'epsilon': str(epsilon), 'input_rows': rows, 'per_row_output_rounded_12': rounded(observed),
'wrong_batch_axis_output_rounded_12': rounded(wrong_axis),
'common_offset_row_output_rounded_12': rounded((shifted,)),
'doubled_fixed_epsilon_output_rounded_12': rounded((doubled_fixed,)),
'doubled_scaled_epsilon_output_rounded_12': rounded((doubled_scaled,)),
'output_biased_variance_for_first_row_exact': '4/5',
'output_L2_squared_for_first_row_exact': '8/5',
'oracle_case_count_inside_ONE_group': oracle_cases,
'invalid_case_count_inside_ONE_group': len(invalid),
'decimal_precision_digits': DECIMAL_PRECISION, 'absolute_tolerance': ABS_TOL,
'relative_tolerance': REL_TOL, 'unique_check_groups': len(checks), 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run(), ensure_ascii=False, indent=2, allow_nan=False))
مرجع، واریانس را از اختلافِ همهٔ جفتهای متفاوت میسازد: مجموعِ (xᵢ−xⱼ)² برای i<j، تقسیم بر N². این رابطه از بسطِ مربعها به دست میآید و حلقهٔ مربعِ فاصله تا میانگینِ تابعِ اصلی را تکرار نمیکند. میانگین و واریانس با Fraction دقیقاند؛ ریشه و تقسیم با Decimal و دقتِ ۷۰ رقم انجام میشوند و سپس برای مقایسه به float میروند.
تلورانسِ نسبی و مطلق هر دو 2e−12 هستند. تمامیِ سطرهای طولِ دو، سه و چهار با مقادیرِ منفیِ دو تا مثبتِ دو و سه انتخابِ اپسیلون، ۲۳۲۵ حالت میسازند؛ همه در یک گروهِ نامدار قرار دارند. شاهدهای محورِ غلط، برخوردِ انتقال، تفاوتِ طولِ واحد، مقیاس و نامعتبرها گروههای جدا و با هدفهای متفاوتاند. ۲۵ ورودیِ مردود نیز تنها یک گروهِ قراردادِ ورودی محسوب میشوند.
{
"python_version": "3.14.5",
"implementation": "CPython",
"dependencies": "standard library only; PyTorch and Keras not executed",
"profile": {
"rows": "1..8 exact tuple rows",
"features": "2..8 equal-width exact tuple columns",
"values": "exact int [-32,32], unitless supplied features",
"epsilon": "exact Fraction [1/1024,16], denominator <=1024",
"affine_stage": false
},
"epsilon": "1/4",
"input_rows": [
[
1,
3
],
[
2,
2
]
],
"per_row_output_rounded_12": [
[
-0.894427191,
0.894427191
],
[
0.0,
0.0
]
],
"wrong_batch_axis_output_rounded_12": [
[
-0.707106781187,
0.707106781187
],
[
0.707106781187,
-0.707106781187
]
],
"common_offset_row_output_rounded_12": [
[
-0.894427191,
0.894427191
]
],
"doubled_fixed_epsilon_output_rounded_12": [
[
-0.970142500145,
0.970142500145
]
],
"doubled_scaled_epsilon_output_rounded_12": [
[
-0.894427191,
0.894427191
]
],
"output_biased_variance_for_first_row_exact": "4/5",
"output_L2_squared_for_first_row_exact": "8/5",
"oracle_case_count_inside_ONE_group": 2325,
"invalid_case_count_inside_ONE_group": 25,
"decimal_precision_digits": 70,
"absolute_tolerance": 2e-12,
"relative_tolerance": 2e-12,
"unique_check_groups": 13,
"checks": {
"literal_two_feature_and_constant_row": true,
"independent_pairwise_fraction_decimal_grid_ONE_group": true,
"feature_permutation_covariance_24_orders": true,
"row_independence_and_batch_permutation": true,
"common_offset_collision_loses_original_mean": true,
"fixed_epsilon_scale_change_and_scaled_epsilon_equivalence": true,
"zero_mean_variance_ratio_not_automatically_one": true,
"unbiased_variance_and_outside_sqrt_epsilon_are_different_contracts": true,
"feature_axis_not_batch_axis_literal_counterexample": true,
"centered_layernorm_is_not_L2_unit_length": true,
"input_dependent_moments_break_a_single_fixed_affine_map": true,
"strict_finite_types_shape_and_epsilon_resource_policy": true,
"max_profile_finite_boundary_epsilon_and_input_preservation": true
}
}
سیزده گروهِ تألیفی موفق شدهاند؛ شمارِ حالتهای درونی را مقاله یا آزمونِ مستقلِ بیشتری حساب نمیکنیم. نتیجهٔ گردشده برای خواندن است و مقایسه با مقدارهای گردنشده انجام میشود. ورودیهای tuple تغییر نمیکنند و سقفِ هشت در هشت با مرزهای اپسیلون نیز بررسی شده است. سرعت، کیفیتِ مدل، دادهٔ واقعی و سازگاریِ عددیِ پشتوانههای سختافزاری سنجیده نشدهاند.
۵. چهار پرسش برای بازبینیِ نمایشِ نرمالشده

پرسش اول: آیا [2,2] و [7,7] باید خطای بردارِ صفر بدهند؟ پاسخ: خیر؛ هر دو پس از کمکردنِ میانگین صفر میشوند و ε مثبت مخرج را غیرصفر نگه میدارد. خروجیِ صفر مجاز است. این رفتار را نباید با تعریفنشدنِ کسینوسِ بردارِ صفر یکی گرفت؛ محاسبهٔ شباهت، قراردادی جدا میخواهد.
پرسش دوم: چرا دو برابر کردنِ [1,3] نمایش را تغییر داد؟ پاسخ: واریانس چهار برابر شد اما ε=1/4 ثابت ماند. با ε=1 برای [2,6]، خروجی به نمایشِ قبلی برمیگردد؛ با اپسیلونِ ثابت، چنین ناورداییِ دقیقی نداریم. این تغییرِ پارامتر بخشی از قرارداد است، نه دستکاریِ نتیجه برای موفق شدنِ آزمون.
پرسش سوم: آیا جایگزینیِ مخرجِ واریانس با N−1 اصلاحِ آماری است؟ پاسخ: در این مسئله خیر؛ تعریفِ موردِ بررسی تقسیم بر N است. در سطرِ دوویژگیِ مثال، واریانسِ جایگزین دو و اندازهٔ خروجی 2/3 میشود. برآوردگرِ دیگری ممکن است در مسئلهٔ آماریِ دیگری مناسب باشد، ولی اینجا تغییرِ رفتارِ رابط است.
پرسش چهارم: آیا موفقیتِ این کد، مناسب بودنِ LayerNorm برای جستوجو را ثابت میکند؟ پاسخ: خیر؛ فقط قراردادِ محاسباتیِ کوچک بررسی شده است. جابهجاییِ مشترک میتواند اطلاعاتِ مفید را حذف کند و تغییرِ هندسه بر شباهت اثر بگذارد. برای انتخابِ عملی باید نمایشِ اولیه، حفظِ اطلاعاتِ لازم و ارزیابیِ مرتبط با کاربرد جداگانه بررسی شوند. منابع در ۲۸ مهر ۱۴۰۵ خوانده شدند؛ این آموزش خبرِ تازه یا گزارشِ نتیجهٔ یک محصول نیست.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

