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

۱. لاجیت، برچسب و قرارداد خروجی را جدا کنیم

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

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

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

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

۲. جابه‌جایی مشترک و خروجی لگاریتمی پایدار

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

در نمونهٔ اصلی، لاجیت‌ها به‌ترتیب ۱۰۰۰، ۱۰۰۱ و ۹۹۹ هستند و برچسب هدف دو، یعنی جایگاه سوم است. محاسبهٔ مستقیم نماییِ ۱۰۰۰ در اجرای شناورِ ثبت‌شده خطای OverflowError می‌دهد. مسئله این نیست که نسبتِ ریاضی تعریف نشده است؛ عددهای میانی از دامنهٔ نمایش بیرون رفته‌اند. تغییرِ مسیر محاسبه می‌تواند از ساختن همین عددهای بزرگ جلوگیری کند، بدون آنکه تعریف نسبت عوض شود.

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

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

۳. کد کامل و اعتبارسنجی ورودی محدود

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

کد با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. فایل را با نام logsoftmax_logloss.py ذخیره و دستور python logsoftmax_logloss.py را اجرا کنید. SciPy نصب نیست و هیچ نتیجه‌ای به اجرای آن نسبت داده نمی‌شود. ساختار خروجی از تاپل‌ها و عددهای شناور تشکیل شده است؛ ماژول، پارامتر مدل، فایل وزن یا ترتیب کلاس‌های یک سامانهٔ بیرونی را نمی‌خواند.

"""Bounded synthetic classifier logits; no trained model or calibration result."""
from dataclasses import dataclass
from decimal import Decimal, localcontext
from itertools import product
import json
import math
import platform

REL_TOL = 5e-13
ABS_TOL = 5e-14


@dataclass(frozen=True)
class LogitResult:
    probabilities: tuple[float, ...]
    log_probabilities: tuple[float, ...]
    negative_log_likelihood: float
    log_normalizer: float


def logit_result(logits, label):
    """One finite tuple, 2..8 classes, dimensionless logits bounded by 1e6."""
    if type(logits) is not tuple or not 2 <= len(logits) <= 8:
        raise ValueError("need an actual tuple with 2..8 logits")
    if any(type(x) not in (int, float) or not -1_000_000 <= x <= 1_000_000
           for x in logits):
        raise ValueError("logits must be finite actual int/float within +/-1e6")
    if type(label) is not int or not 0 <= label < len(logits):
        raise ValueError("label must be an actual integer class index")
    values = tuple(float(x) for x in logits)
    maximum = max(values)
    shifted = tuple(x - maximum for x in values)
    weights = tuple(math.exp(x) for x in shifted)
    total = math.fsum(weights)
    log_total = math.log(total)
    log_p = tuple(x - log_total for x in shifted)
    return LogitResult(
        tuple(x / total for x in weights), log_p,
        -log_p[label], maximum + log_total,
    )


def decimal_oracle(logits, label):
    """Independent raw exponential sum at precision80; no maximum shift/helper."""
    with localcontext() as context:
        context.prec = 80
        context.Emax = 999999
        context.Emin = -999999
        values = tuple(Decimal.from_float(float(x)) for x in logits)
        exponentials = tuple(x.exp() for x in values)
        total = sum(exponentials, Decimal(0))
        log_total = total.ln()
        return {
            "probabilities": tuple(float(x / total) for x in exponentials),
            "log_probabilities": tuple(float(x - log_total) for x in values),
            "negative_log_likelihood": float(log_total - values[label]),
        }


def close(a, b):
    return math.isclose(a, b, rel_tol=REL_TOL, abs_tol=ABS_TOL)


def agrees_with_oracle(logits, label):
    actual = logit_result(logits, label)
    expected = decimal_oracle(logits, label)
    return (
        all(close(a, b) for a, b in zip(actual.probabilities, expected["probabilities"], strict=True))
        and all(close(a, b) for a, b in zip(actual.log_probabilities, expected["log_probabilities"], strict=True))
        and close(actual.negative_log_likelihood, expected["negative_log_likelihood"])
    )


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


def run():
    logits = (1000.0, 1001.0, 999.0)
    result = logit_result(logits, 2)
    checks = {}
    equal = logit_result((0, 0), 0)
    checks["equal_two_class_hand_answer"] = (
        equal.probabilities == (0.5, 0.5)
        and equal.log_probabilities == (-math.log(2), -math.log(2))
        and equal.negative_log_likelihood == math.log(2)
    )
    checks["three_class_literal_hand_answers"] = (
        all(close(a, b) for a, b in zip(result.probabilities,
            (0.24472847105479764, 0.6652409557748219, 0.09003057317038046), strict=True))
        and close(result.negative_log_likelihood, 2.4076059644443804)
    )
    try:
        math.exp(1000.0)
    except OverflowError:
        naive_overflow = True
    else:
        naive_overflow = False
    checks["raw_float_exponential_overflows_for_main_fixture"] = naive_overflow
    extreme = logit_result((0, -1000), 1)
    checks["zero_probability_does_not_erase_finite_direct_log_loss"] = (
        extreme.probabilities[1] == 0.0
        and extreme.log_probabilities == (0.0, -1000.0)
        and extreme.negative_log_likelihood == 1000.0
        and agrees_with_oracle((0, -1000), 1)
    )
    checks["exactly_representable_common_offsets_preserve_outputs"] = all(
        logit_result(tuple(x + shift for x in logits), 2) ==
        LogitResult(result.probabilities, result.log_probabilities,
                    result.negative_log_likelihood, result.log_normalizer + shift)
        for shift in (-2048, 2048)
    )
    permutation = (2, 0, 1)
    permuted = logit_result(tuple(logits[i] for i in permutation), 0)
    checks["paired_class_permutation_and_label_mapping"] = (
        permuted.probabilities == tuple(result.probabilities[i] for i in permutation)
        and permuted.log_probabilities == tuple(result.log_probabilities[i] for i in permutation)
        and permuted.negative_log_likelihood == result.negative_log_likelihood
    )
    checks["probability_mass_order_and_ties"] = (
        close(math.fsum(result.probabilities), 1.0)
        and result.probabilities[1] > result.probabilities[0] > result.probabilities[2]
        and logit_result((3, 3, -2), 0).probabilities[0]
        == logit_result((3, 3, -2), 0).probabilities[1]
    )
    oracle_cases = []
    for values in product((-5.0, 0.5, 8.0), repeat=3):
        for offset in (-1024, 0, 1024):
            shifted = tuple(x + offset for x in values)
            for label in range(3):
                oracle_cases.append(agrees_with_oracle(shifted, label))
    checks["all_243_decimal_raw_exp_cases_one_group"] = all(oracle_cases)
    checks["small_differences_and_large_common_baseline"] = (
        agrees_with_oracle((1e-12, 0.0, -1e-12), 2)
        and agrees_with_oracle((1_000_000, 1_000_000, 999_999), 2)
        and logit_result((1e-12, 0.0, -1e-12), 2).probabilities[0]
        > logit_result((1e-12, 0.0, -1e-12), 2).probabilities[2]
    )
    endpoints = logit_result((-1_000_000, 1_000_000), 0)
    checks["bounded_domain_endpoints_and_eight_classes"] = (
        endpoints.negative_log_likelihood == 2_000_000.0
        and endpoints.probabilities == (0.0, 1.0)
        and agrees_with_oracle((-1_000_000, 1_000_000), 0)
        and agrees_with_oracle((0,) * 8, 7)
    )
    checks["log_output_matches_probability_on_moderate_inputs"] = all(
        close(math.exp(lp), p)
        for lp, p in zip(result.log_probabilities, result.probabilities, strict=True)
    )
    checks["reject_bad_logit_shape_type_finiteness_and_bounds"] = all(
        rejects(lambda values=values: logit_result(values, 0))
        for values in ([0, 1], (), (0,), (0,) * 9, (True, 0), ("1", 0),
                       (math.nan, 0), (math.inf, 0), (-math.inf, 0),
                       (1_000_001, 0), (-1_000_001, 0), (10**1000, 0))
    )
    checks["reject_label_coercion_and_outside_fixed_class_order"] = all(
        rejects(lambda label=label: logit_result(logits, label))
        for label in (True, 2.0, "2", -1, 3)
    )
    checks["input_tuple_unchanged"] = logits == (1000.0, 1001.0, 999.0)
    if not all(type(v) is bool and v for v in checks.values()):
        raise AssertionError(checks)
    return {
        "python": platform.python_version(), "dependencies": "standard library only",
        "logits_dimensionless": logits, "target_class_index": 2,
        "probabilities": result.probabilities, "log_probabilities": result.log_probabilities,
        "negative_log_likelihood_natural_log_units": result.negative_log_likelihood,
        "log_normalizer": result.log_normalizer,
        "raw_float_exp_1000_raised_OverflowError": naive_overflow,
        "extreme_fixture": {"logits": (0, -1000), "target": 1,
            "target_probability": extreme.probabilities[1],
            "target_log_probability": extreme.log_probabilities[1],
            "direct_negative_log_likelihood": extreme.negative_log_likelihood},
        "oracle": {"precision_decimal_digits": 80, "grid_cases_inside_one_group": len(oracle_cases),
                   "input_conversion": "Decimal.from_float of the actual float input"},
        "comparison_tolerance": {"relative": REL_TOL, "absolute": ABS_TOL,
            "scope": "chosen test comparisons, not a proven universal error bound"},
        "checks": checks, "check_count": len(checks), "all_passed": True,
        "limitations": ["No trained model, gradients, calibration, accuracy or empirical data",
                        "Tiny float probabilities may round to zero; no epsilon or clipping",
                        "Common-shift invariance is only tested for exactly representable fixtures",
                        "Reported log normalizer can lose low bits at a large baseline; loss uses shifted values directly"],
    }


if __name__ == "__main__":
    print(json.dumps(run(), ensure_ascii=False, indent=2, allow_nan=False))

فقط نوع دقیق int یا float برای لاجیت پذیرفته می‌شود؛ bool، رشته و تبدیل ضمنی نداریم. مقدار باید داخل منفی یک میلیون تا مثبت یک میلیون باشد؛ NaN و بی‌نهایت نیز رد می‌شوند. برچسب باید نوع دقیق int و داخل دامنهٔ همان بردار باشد. این کران‌ها انتخاب آموزشی‌اند و محدودیت عمومی softmax نیستند. رد کردن ورودی نامعتبر، جای بررسی معنای امتیازهای تولیدشده توسط یک مدل واقعی را نمی‌گیرد.

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

۴. مرجع Decimal و شاهد صفرشدن احتمال

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

مرجع مستقل، بیشینه را کم نمی‌کند: نماییِ خامِ امتیازها را با Decimal، دقت هشتاد رقم و دامنهٔ نمای اعلام‌شده می‌سازد، جمع می‌کند و لگاریتم می‌گیرد. مطابق مستندات Decimal و تبدیل float، مقدارِ واقعاً واردشده را با from_float تبدیل می‌کنیم؛ آن را با یک رشتهٔ اعشاریِ ظاهراً مشابه جایگزین نمی‌کنیم. محاسبهٔ نمایی و لگاریتم همچنان گرد می‌شود؛ این مرجع حسابِ نمادینِ دقیق نیست.

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

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "logits_dimensionless": [
    1000.0,
    1001.0,
    999.0
  ],
  "target_class_index": 2,
  "probabilities": [
    0.24472847105479764,
    0.6652409557748218,
    0.09003057317038046
  ],
  "log_probabilities": [
    -1.4076059644443804,
    -0.4076059644443804,
    -2.4076059644443806
  ],
  "negative_log_likelihood_natural_log_units": 2.4076059644443806,
  "log_normalizer": 1001.4076059644444,
  "raw_float_exp_1000_raised_OverflowError": true,
  "extreme_fixture": {
    "logits": [
      0,
      -1000
    ],
    "target": 1,
    "target_probability": 0.0,
    "target_log_probability": -1000.0,
    "direct_negative_log_likelihood": 1000.0
  },
  "oracle": {
    "precision_decimal_digits": 80,
    "grid_cases_inside_one_group": 243,
    "input_conversion": "Decimal.from_float of the actual float input"
  },
  "comparison_tolerance": {
    "relative": 5e-13,
    "absolute": 5e-14,
    "scope": "chosen test comparisons, not a proven universal error bound"
  },
  "checks": {
    "equal_two_class_hand_answer": true,
    "three_class_literal_hand_answers": true,
    "raw_float_exponential_overflows_for_main_fixture": true,
    "zero_probability_does_not_erase_finite_direct_log_loss": true,
    "exactly_representable_common_offsets_preserve_outputs": true,
    "paired_class_permutation_and_label_mapping": true,
    "probability_mass_order_and_ties": true,
    "all_243_decimal_raw_exp_cases_one_group": true,
    "small_differences_and_large_common_baseline": true,
    "bounded_domain_endpoints_and_eight_classes": true,
    "log_output_matches_probability_on_moderate_inputs": true,
    "reject_bad_logit_shape_type_finiteness_and_bounds": true,
    "reject_label_coercion_and_outside_fixed_class_order": true,
    "input_tuple_unchanged": true
  },
  "check_count": 14,
  "all_passed": true,
  "limitations": [
    "No trained model, gradients, calibration, accuracy or empirical data",
    "Tiny float probabilities may round to zero; no epsilon or clipping",
    "Common-shift invariance is only tested for exactly representable fixtures",
    "Reported log normalizer can lose low bits at a large baseline; loss uses shifted values directly"
  ]
}

احتمال‌های مثال اصلی حدود ۰٫۲۴۴۷۲۸، ۰٫۶۶۵۲۴۱ و ۰٫۰۹۰۰۳۱ و زیانِ جایگاه سوم حدود ۲٫۴۰۷۶۰۶ است. در شاهدِ جداگانهٔ صفر و منفی هزار، احتمالِ کلاس دوم در اجرای float صفر می‌شود، ولی خروجی لگاریتمیِ آن منفی هزار و زیان مستقیم هزار می‌ماند. راهنمای log_softmax نیز تفاوت مسیر مستقیم با لگاریتمِ احتمال اشباع‌شده را مطرح می‌کند؛ شاهد و کد حاضر، خروجی واقعیِ برنامهٔ خودمان هستند.

۵. چهار پرسش و مرز تفسیر زیان

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

پرسش اول: برای دو لاجیت مساویِ صفر، احتمال و زیان هدف چیست؟ پاسخ: هر دو احتمال یک‌دوم و زیان هر برچسب، لگاریتم طبیعیِ دو است؛ حدود ۰٫۶۹۳۱۴۷. تساویِ امتیاز، شاهدی دربارهٔ توازن واقعی کلاس‌ها یا کیفیت مدل نیست.

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

پرسش سوم: با جابه‌جایی کلاس سوم به اول، برچسب چه می‌شود؟ پاسخ: برای ترتیب تازهٔ سوم، اول، دوم، شاخص هدف صفر می‌شود. احتمال‌ها و لگاریتم‌ها هم باید با همان جایگشت مقایسه شوند؛ حفظ شاخص دو، هدف دیگری انتخاب می‌کند.

پرسش چهارم: عبور از آزمون یعنی احتمال‌ها کالیبره‌اند؟ پاسخ: خیر؛ هیچ آموزش یا دادهٔ مستقل نداریم. آزمون مستقل گرادیان نیز محاسبه را از کیفیت کاربرد جدا می‌کند. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ مستندات SciPy نسخهٔ 1.18.0 و Python نسخهٔ 3.14.8 از اجرای واقعیِ 3.14.5 جدا هستند. تاریخ اصلی انتشار این صفحات معلوم نیست و این مطلب خبر تازه معرفی نمی‌شود.

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

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