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

۱. مسئلهٔ برق را با واحد و مقیاس مشخص تعریف کنید

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

چهار جریان مصنوعی صفر، ۰٫۰۱، ۰٫۰۲ و ۰٫۰۳ آمپر و چهار ولتاژ متناظر ۰٫۱۰، ۰٫۳۱، ۰٫۴۹ و ۰٫۷۲ ولت داریم. این اعداد را برای تمرین انتخاب کرده‌ایم؛ خروجی ابزار آزمایشگاهی نیستند. ابتدا جریان را بر مقیاس ثابت ۰٫۰۱ آمپر و ولتاژ را بر یک ولت تقسیم می‌کنیم. در نتیجه، ورودی مدل x و هدف y بی‌بعد می‌شوند. مقیاس‌ها بخشی از قرارداد هستند و باید همراه داده و پارامتر ذخیره شوند؛ تقسیم بر یک عدد بدون ثبت واحد، مسئله را بازتولیدپذیر نمی‌کند.

مدل بی‌بعد را به‌صورت prediction = w*x + b می‌نویسیم. پارامترهای آغازین w=0.20 و b=0.05 هستند. برای برگشت به واحد فیزیکی، شیب جریان‌ـ‌ولتاژ برابر w ضرب‌در نسبت مقیاس ولتاژ به مقیاس جریان است؛ بنابراین شیب آغازین معادل ۲۰ اهم و عرض از مبدأ معادل ۰٫۰۵ ولت می‌شود. این صرفاً تعبیر مدل خطی است، نه شناسایی مقاومت یک قطعهٔ واقعی. تابع هزینه را میانگین مربع باقیمانده‌های بی‌بعد تعریف می‌کنیم؛ مقدار آن نیز بی‌بعد است.

۲. مشتق تحلیلی را با تفاضل مرکزی مقایسه کنید

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

باقیماندهٔ هر نمونه را پیش‌بینی منهای هدف می‌گیریم. مشتق هزینه نسبت به b، دو برابر میانگین باقیمانده‌ها و مشتق نسبت به w، دو برابر میانگین حاصل‌ضرب باقیمانده در x است. این رابطه مستقیم از مشتق مربع و خطی بودن پیش‌بینی به دست می‌آید. اگر هزینه را نصف میانگین مربع تعریف کرده بودیم، ضریب دو تغییر می‌کرد؛ پس فرمول مشتق را باید با تعریف دقیق هزینه مقایسه کرد، نه با نامی مانند MSE در یک یادداشت قدیمی.

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

همهٔ ابزارهای بررسی مشتق دقیقاً یک روش ندارند. مرجع scipy.optimize.check_grad از تقریب تفاضل پیشرو استفاده می‌کند. بنابراین نباید خروجی تابع سفارشی این مقاله را خروجی همان API معرفی کرد. روش عددی، اندازهٔ گام و معیار مقایسه باید کنار نتیجه ثبت شوند. اینجا هر دو پارامتر بی‌بعدند و مؤلفه‌ها با تلورانس نسبی ۱۰ به توان منفی هفت و مطلق ۱۰ به توان منفی نه بررسی می‌شوند؛ این حدود، قرارداد همین تمرین‌اند.

۳. کد کامل پایتون و خطاهای عمدی برای کنترل آزمون

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

برنامهٔ زیر فقط کتابخانهٔ استاندارد پایتون را لازم دارد و با Python 3.14.5 اجرا شده است. گام پیش‌فرض تفاضل مرکزی ۱۰ به توان منفی پنج است. دو نسخهٔ نادرست از گرادیان درست می‌سازیم: یکی با علامت وارونه و دیگری با حذف ضریب دو. آزمونی که حتی این دو اشتباه شناخته‌شده را تشخیص ندهد، شاهد مناسبی برای درستی مشتق اصلی نیست. نتیجهٔ هر بررسی به‌صورت جداگانه در JSON چاپ می‌شود تا رد شدن یک شرط پشت یک پیام کلی پنهان نشود.

ورودی باید دقیقاً دو پارامتر و زوج‌های دادهٔ هم‌تعداد، غیرخالی و متناهی داشته باشد. علاوه بر بررسی گام مثبت، کنترل می‌کنیم که جمع یا تفریق گام واقعاً نمایش شناور پارامتر را تغییر دهد. گام ۱۰ به توان منفی سی در نقطهٔ مرجع این شرط را نقض می‌کند و پذیرفته نمی‌شود. این کنترل همهٔ مسائل دقت عددی را حل نمی‌کند، اما از مقایسهٔ دو نقطهٔ عملاً یکسان جلوگیری می‌کند. فایل را با نام gradient_check.py ذخیره و با پایتون اجرا کنید.

"""Authored finite-difference tests of a normalized electrical regression loss."""
import json
import math
import platform
import random


def validate(theta, x, y):
    if len(theta) != 2 or not x or len(x) != len(y):
        raise ValueError('Two parameters and nonempty aligned samples required')
    if any(type(v) not in (int, float) or not math.isfinite(v)
           for v in [*theta, *x, *y]):
        raise ValueError('Finite real values required')


def loss(theta, x, y):
    validate(theta, x, y)
    w, b = theta
    return math.fsum((w * xi + b - yi) ** 2 for xi, yi in zip(x, y)) / len(x)


def gradient(theta, x, y):
    validate(theta, x, y)
    w, b = theta
    residual = [w * xi + b - yi for xi, yi in zip(x, y)]
    return [2 * math.fsum(r * xi for r, xi in zip(residual, x)) / len(x),
            2 * math.fsum(residual) / len(x)]


def central_difference(theta, x, y, eps=1e-5):
    validate(theta, x, y)
    if type(eps) not in (int, float) or not math.isfinite(eps) or eps <= 0:
        raise ValueError('Positive finite step required')
    result = []
    for j in range(2):
        plus, minus = list(theta), list(theta)
        plus[j] += eps
        minus[j] -= eps
        if plus[j] == theta[j] or minus[j] == theta[j]:
            raise ValueError('Step is too small at this floating-point magnitude')
        result.append((loss(plus, x, y) - loss(minus, x, y)) / (2 * eps))
    return result


def close(a, b):
    return all(math.isclose(u, v, rel_tol=1e-7, abs_tol=1e-9) for u, v in zip(a, b))


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


def run_example():
    current_a = [0., .01, .02, .03]
    voltage_v = [.10, .31, .49, .72]  # synthetic, not measured
    current_scale_a, voltage_scale_v = .01, 1.
    x = [i / current_scale_a for i in current_a]
    y = [v / voltage_scale_v for v in voltage_v]
    theta = [.20, .05]  # dimensionless w,b; equivalent slope20 ohm, offset0.05V
    analytic = gradient(theta, x, y)
    numeric = central_difference(theta, x, y)
    rng = random.Random(31)
    points = [[rng.uniform(-.5, .5), rng.uniform(-.5, .5)] for _ in range(24)]
    errors = [max(abs(a - n) for a, n in zip(gradient(t, x, y),
              central_difference(t, x, y))) for t in points]
    wrong_sign = [-g for g in analytic]
    missing_two = [g / 2 for g in analytic]
    shifted_y = [v + .4 for v in y]
    shifted_theta = [theta[0], theta[1] + .4]
    repeated_x, repeated_y = x + x, y + y
    learning_rate = .01
    after = [t - learning_rate * g for t, g in zip(theta, analytic)]
    sweep = [{'eps': e, 'numeric': central_difference(theta, x, y, e)}
             for e in [1e-2, 1e-4, 1e-6]]
    checks = {
        'hand_calculated_loss_and_gradient':
            math.isclose(loss(theta, x, y), .00315) and close(analytic, [-.175, -.11]),
        'central_difference_matches_at_reference': close(analytic, numeric),
        '24_fixed_seed_parameter_points_agree':
            all(close(gradient(t, x, y), central_difference(t, x, y)) for t in points),
        'deliberate_sign_and_factor_faults_detected':
            not close(analytic, wrong_sign) and not close(analytic, missing_two),
        'paired_permutation_preserves_loss_and_gradient':
            math.isclose(loss(theta, x, y), loss(theta, x[::-1], y[::-1])) and
            close(analytic, gradient(theta, x[::-1], y[::-1])),
        'duplicate_dataset_preserves_mean_loss_and_gradient':
            math.isclose(loss(theta, x, y), loss(theta, repeated_x, repeated_y)) and
            close(analytic, gradient(theta, repeated_x, repeated_y)),
        'joint_target_offset_shift_preserves_residuals':
            math.isclose(loss(theta, x, y), loss(shifted_theta, x, shifted_y)) and
            close(analytic, gradient(shifted_theta, x, shifted_y)),
        'small_step_reduces_this_fixture_loss': loss(after, x, y) < loss(theta, x, y),
        'invalid_inputs_and_unrepresentable_step_rejected': all([
            rejects(lambda: loss(theta, [], [])),
            rejects(lambda: loss(theta, x, y[:-1])),
            rejects(lambda: gradient([1.], x, y)),
            rejects(lambda: loss([float('nan'), 1.], x, y)),
            rejects(lambda: central_difference(theta, x, y, 0)),
            rejects(lambda: central_difference(theta, x, y, 1e-30)),
        ]),
    }
    if not all(checks.values()):
        raise AssertionError(checks)
    return {'python': platform.python_version(), 'dependencies':'standard library only',
        'data_origin':'authored synthetic current/voltage pairs, not a device measurement',
        'scales':{'current_a':current_scale_a,'voltage_v':voltage_scale_v},
        'parameter_order':['w_dimensionless','b_dimensionless'],
        'loss':loss(theta, x, y),'analytic_gradient':analytic,'numeric_gradient':numeric,
        'max_abs_component_error_at_reference':max(abs(a-n) for a,n in zip(analytic,numeric)),
        'max_abs_component_error_at_24_points':max(errors),'step_sweep':sweep,
        'loss_after_one_step':loss(after, x, y),'parameters_after_step':after,
        'checks':checks,
        'limitations':['No training benchmark or model generalization estimate',
                       'Finite fixtures, not proof for all inputs',
                       'No PyTorch, SciPy, GPU or MATLAB execution']}


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

۴. خروجی اجرا و سه ویژگی مستقل از جواب عددی

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

در نقطهٔ مرجع، هزینه تقریباً ۰٫۰۰۳۱۵ و گرادیان به‌ترتیب w و b تقریباً منفی ۰٫۱۷۵ و منفی ۰٫۱۱ است. بیشترین اختلاف مطلق مؤلفه‌ای میان مشتق تحلیلی و تفاضل مرکزی در این اجرا حدود ۱٫۷۵ ضرب‌در ده به توان منفی سیزده شد. در ۲۴ نقطهٔ پارامتر ساخته‌شده با بذر ثابت ۳۱، بیشترین اختلاف حدود ۳٫۱۹ ضرب‌در ده به توان منفی یازده بود. این مقادیر به همین داده، نقاط و اجرای شناور وابسته‌اند و کران خطا برای همهٔ ورودی‌ها نیستند.

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

یک گام به‌روزرسانی با نرخ ۰٫۰۱، پارامترها را به ۰٫۲۰۱۷۵ و ۰٫۰۵۱۱ و هزینه را تقریباً به ۰٫۰۰۲۷۴۰۴۵۴ می‌رساند. کاهش هزینه فقط برای همین نقطه و گام بررسی شده است؛ انتخاب بهینهٔ نرخ یادگیری یا همگرایی را ثابت نمی‌کند. برای ثبت بذر، محیط و تمایز تغییر داده از تغییر روش، آموزش تکرارپذیری آزمایش پردازش سیگنال را نیز ببینید. تمام نه گروه بررسی این برنامه موفق شدند؛ این عدد شمار گروه‌هاست، نه شمار حالت‌های ممکن مسئله.

۵. پاسخ تمرین‌ها و محدودیت تعمیم به شبکه‌های عصبی

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

تمرین اول: چرا حذف ضریب دو در این مثال خطاست؟ پاسخ: هزینه، میانگین مربع کامل است و مشتق مربع دو برابر باقیمانده دارد. برای هزینهٔ نصف میانگین مربع، همان حذف ممکن است درست باشد؛ قرارداد تابع تعیین‌کننده است. تمرین دوم: آیا کوچک‌تر کردن گام همیشه پاسخ را دقیق‌تر می‌کند؟ پاسخ: خیر. اختلاف دو مقدار نزدیک می‌تواند دقت رقم‌های معنادار را از بین ببرد و گام بسیار کوچک حتی پارامتر شناور را تغییر ندهد. برنامه سه گام ۰٫۰۱، ۰٫۰۰۰۱ و ۰٫۰۰۰۰۰۱ را نیز گزارش می‌کند؛ در این تابع درجه‌دو، نباید انتظار داشت خطا با یک الگوی ساده در همهٔ آن‌ها کاهش یابد.

تمرین سوم: شیب فیزیکی پس از گام آموزشی چقدر است؟ پاسخ: ۰٫۲۰۱۷۵ ضرب‌در یک ولت تقسیم بر ۰٫۰۱ آمپر، یعنی ۲۰٫۱۷۵ اهم؛ عرض از مبدأ نیز ۰٫۰۵۱۱ ولت است. تمرین چهارم: آیا تکرار مجموعه همان افزایش اطلاعات آزمایش است؟ پاسخ: خیر. ویژگی دوم فقط قرارداد میانگین‌گیری را بررسی می‌کند و دادهٔ مستقل جدیدی نمی‌سازد. برای مقایسهٔ روش عددی با جواب مستقل در مسئله‌ای دیگر، اعتبارسنجی عددی مدار RC نمونهٔ مکمل مناسبی است.

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

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

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