پرش به محتوای اصلی
پرش به محتوای مقاله

موتور Autograd در PyTorch مشتقات ریاضی را به‌جای تخمین محاسبه می‌کند

·۱۷ شهریور ۱۴۰۵۲۸ دقیقه مطالعه
راهنما
هفته هفتم: حلقه آموزش، نوشته شده دستی
هفته هفتم: حلقه آموزش، نوشته شده دستی
اشتراک‌گذاری
واقعاً چه چیز جدید است؟

اثبات ریاضی اینکه موتور Autograd در PyTorch به‌جای تخمین شیب (Numerical Approximation)، مشتقات دقیق قاعده زنجیره‌ای را محاسبه می‌کند و لایه‌های `nn.Linear` صرفاً پوشش‌هایی برای عملیات ساده تنسوری هستند.

تصور کنید تمام لایه‌های آماده‌ی PyTorch را کنار بگذارید و یک حلقه‌ی آموزش را کاملاً دستی بازسازی کنید. چنین ساختاری ثابت می‌کند که موتور Autograd (محاسبه خودکار مشتق) در این فریم‌ورک، مشتقات ریاضی دقیق را محاسبه می‌کند و نه تخمینی از شیب بر اساس تغییرات کوچک. این موضوع در یک تحلیل فنی در ۸ سپتامبر ۲۰۲۶ به تفصیل بررسی شد؛ نتایج نشان داد که اگر چرخه چهارمرحله‌ای آموزش حفظ شود، حذف انتزاع‌های سطح بالایی مثل nn.Linear و optim.SGD هیچ تغییری در نقطه پایان ریاضی مدل ایجاد نمی‌کند.

اکثر توسعه‌دهندگان به APIهای سطح بالایی تکیه می‌کنند که سازوکار پس‌انتشار (Backpropagation) را پنهان می‌کند. این موضوع باعث ایجاد یک «جعبه سیاه» می‌شود که در آن رابطه بین تابع زیان و به‌روزرسانی پارامترها مرموز به نظر می‌رسد. با حذف این ابزارهای کمکی، کل فرآیند به یک تمرین شفاف در حساب دیفرانسیل و دست‌کاری تنسورها تبدیل می‌شود.

برای درک بهتر، تصور کنید می‌خواهید دو پیچ را روی یک دستگاه بچرخانید تا به خروجی خاصی برسید. شما پیچ‌ها را می‌چرخانید، میزان خطا را بررسی می‌کنید و بر اساس اینکه خطا با چه شدتی تغییر می‌کند، تصمیم می‌گیرید پیچ‌ها را به کدام جهت بچرخانید. در یادگیری ماشین، آن پیچ‌ها همان وزن‌ها (Weights) و بایاس‌ها (Biases) هستند و آن «شدت تغییر»، همان گرادیان (Gradient) است.

مکانیسم Autograd

در قلب این فرآیند، گراف محاسباتی قرار دارد. وقتی یک تنسور با ویژگی requires_grad=True ایجاد می‌شود، PyTorch هر عملیاتی که روی آن انجام شود را ثبت می‌کند. این کار ردپایی از اشیای grad_fn ایجاد می‌کند، مانند AddBackward0 یا PowBackward0 که سیستم در مرحله‌ی بازگشتی (backward pass) آن‌ها را به‌صورت معکوس طی می‌کند.

در یک مثال ساده که مدل مقداری را بر اساس فرمول $y = wx + b$ پیش‌بینی می‌کند، سیستم جریان داده از ورودی‌ها تا تابع زیان نهایی را ردیابی می‌کند. برای مثال، اگر پیش‌بینی مدل ۷ باشد اما هدف ۱۰ باشد، زیان مربعی (Squared Loss) برابر ۹ خواهد بود. موتور Autograd سپس دقیقاً محاسبه می‌کند که یک تغییر کوچک در وزن ($w$) یا بایاس ($b$) چه تأثیری بر این زیان می‌گذارد.

بر اساس مستندات فنی، با استفاده از قاعده زنجیره‌ای (Chain Rule)، گرادیان وزن به صورت $2(pred - target) \times x$ محاسبه می‌شود. در آزمایش‌های انجام شده در این محیط آزمایشگاهی، این مقدار برای وزن ۱۸.۰- و برای بایاس ۶.۰- به دست آمد که با مشتقات دستی تا آخرین رقم اعشار مطابقت داشت. این موضوع تأیید می‌کند که Autograd شیب را با امتحان کردن تغییرات کوچک تخمین نمی‌زند، بلکه مشتق دقیق قاعده زنجیره‌ای را در قالب اعداد ممیز شناور محاسبه می‌کند.

ردیابی دقیق گرادیان‌ها

برای درک نحوه‌ی مدیریت این محاسبات توسط پایتورچ، بررسی ویژگی‌های خاص تنسورها در مسیر رفت (forward pass) ضروری است:

  • تنسورهای برگ (Leaf Tensors): تنسورهایی که مستقیماً توسط کاربر ایجاد شده‌اند (مثل وزن و بایاس) برگ نامیده می‌شوند. اگر requires_grad=True باشد، گرادیان‌های نهایی انباشته شده در فیلد .grad آن‌ها ذخیره می‌شود.
  • تنسورهای غیربرگ: تنسورهایی که حاصل یک عملیات هستند (مثل پیش‌بینی pred) برگ نیستند. این‌ها به‌جای مقدار .grad دارای یک grad_fn هستند که به عملیاتی که آن‌ها را ایجاد کرده اشاره می‌کند.
  • داده‌های ثابت: ورودی‌ها (مثل مقادیر سلسیوس) برگ هستند اما به‌طور پیش‌فرض requires_grad=False دارند. چون این‌ها داده‌های ثابت هستند، نیازی به محاسبه گرادیان ندارند و فیلد .grad آن‌ها None باقی می‌ماند.

در مدل تبدیل سلسیوس به فارنهایت، زنجیره عملیات به این صورت است: $w, x \rightarrow wx \rightarrow wx+b \rightarrow wx+b-10 \rightarrow ( \cdot )^2 \rightarrow L$. پایتورچ این زنجیره را معکوس طی می‌کند تا خطا را به پارامترها بازگرداند.

نقش نمونه‌های متعدد در محاسبه

هنگام آموزش روی یک دسته (Batch) — مثل ۶ نمونه دمایی استفاده شده در این آزمایش — زیان برابر با میانگین مربعات خطاست. در نتیجه، گرادیان نهایی نیز میانگین مشارکت هر نمونه است.

برای گرادیان وزن، هر نمونه جمله‌ای از $2(pred - target) \times x$ را اضافه می‌کند. در نقطه شروع تصادفی این آزمایش، ۶ مقدار مجزا برای مشارکت گرادیان وزن تولید شد: ۱۷۵۴.۶۶۱۹، ۵۹۴.۰۶۷۸، ۰.۰۰۰۰-، ۱۴۹۸.۹۴۰۳-، ۳۰۹۸.۸۶۶۷- و ۱۱۶۳۸.۷۶۳۷-. میانگین این شش مقدار برابر با ۲۳۱۴.۶۴۰۴- است که دقیقاً همان گرادیان وزنی است که توسط loss.backward() گزارش شد.

گرادیان بایاس نیز از همین منطق پیروی می‌کند، با این تفاوت که ضرب‌کننده ورودی $x$ با عدد ۱ جایگزین می‌شود. هر مشارکت صرفاً $2(pred - target)$ است و میانگین آن‌ها منجر به گرادیان بایاس گزارش‌شده یعنی ۷۳.۸۲۴۷- می‌شود.

حلقه‌ی آموزش چهارمرحله‌ای

با حذف بهینه‌سازهای سطح بالا، حلقه‌ی آموزش به یک توالی سخت‌گیرانه چهار مرحله‌ای تبدیل می‌شود:

  • پیش‌بینی و امتیازدهی: مدل یک مسیر رفت را طی می‌کند تا پیش‌بینی‌ها را تولید کرده و میانگین مربعات خطا (زیان) را محاسبه کند. برای وظیفه تبدیل سلسیوس به فارنهایت، این شامل ۶ نمونه با ورودی‌های خام سلسیوس است. زیان، میانگین خطاهای مربعی در کل دسته است.
  • محاسبه گرادیان‌ها: فراخوانی loss.backward() گراف محاسباتی را طی می‌کند تا فیلدهای .grad تنسورهای قابل آموزش را پر کند. این فرآیند همان پس‌انتشار است.
  • به‌روزرسانی پارامترها: پارامترها در جهت مخالف گرادیان و با ضریب نرخ یادگیری (Learning Rate) تغییر می‌کنند (مثلاً: $param = param - lr \times grad$). این همان قاعده‌ی ریاضی ساده‌ی SGD است.
  • پاک‌سازی گرادیان‌ها: با استفاده از .zero_()، بافرهای گرادیان صفر می‌شوند تا از انباشت گرادیان‌ها در طول دوره‌های مختلف (Epochs) جلوگیری شود.

حفاظ‌های حیاتی: no_grad و zero_

دو عملیات برای جلوگیری از کرش کردن یا واگرایی مدل اجباری است. نخست، به‌روزرسانی پارامترها باید حتماً داخل بلوک torch.no_grad() رخ دهد. چون خودِ به‌روزرسانی یک عملیات تنسوری است، پایتورچ در غیر این صورت سعی می‌کند این به‌روزرسانی را هم در گراف گرادیان ثبت کند که منجر به خطای RuntimeError در مورد عملیات‌های in-place روی متغیرهای برگ می‌شود. متغیر برگ، تنسوری است که مستقیماً ایجاد شده و نتیجه‌ی یک محاسبه ردیابی شده نیست.

دوم، پاک‌سازی صریح گرادیان‌هاست. برخلاف برخی فریم‌ورک‌ها، پایتورچ گرادیان‌ها را با افزودن مقادیر جدید به مقادیر موجود در بافر .grad انباشته می‌کند. بدون فراخوانی .zero_()، گرادیان در هر دوره دو برابر می‌شود. برای مثال، در تستی که گرادیان واقعی ۳.۰ است، فراخوانی دوم backward() بدون پاک‌سازی، مقدار ۶.۰ را می‌خواند که منجر به به‌روزرسانی‌های عظیم و ناپایدار شده و باعث واگرایی مدل می‌گردد.

مقایسه گردش‌کار دستی در برابر خودکار

در یک تست رودررو، یک حلقه دستی برای آموزش تبدیل سلسیوس به فارنهایت ($F = C \times 1.8 + 32$) با استفاده از ۶ نمونه به کار گرفته شد. این حلقه با وزن‌های تصادفی (وزن: ۱.۵۴۰۹۹۶، بایاس: ۰.۲۹۳۴۲۹-) شروع شد و برای ۲۰,۰۰۰ دوره با نرخ یادگیری ۰.۰۰۰۳ اجرا گردید.

نتایج با استفاده از nn.Linear و optim.SGD کاملاً یکسان بود. هر دو روش به وزن ۱.۸۰۰۰ و بایاس ۳۱.۹۹۸۱ هم‌گرا شدند. برای ورودی ۲۵ درجه سلسیوس، هر دو مدل مقدار ۷۶.۹۹۸۵ فارنهایت را پیش‌بینی کردند. زیان نهایی بدون گرد کردن تقریباً ۰.۰۰۰۰۰۲۹۳۷۱ بود.

اگرچه وزن‌های شروع متفاوت بود (زیرا nn.Linear و torch.randn از قوانین متفاوتی برای تبدیل Seed به عدد استفاده می‌کنند)، اما هر دو اجرا به یک نقطه پایان رسیدند چون هر دو یک زیان یکسان را روی داده‌های خطی یکسان بهینه کردند. این ثابت می‌کند که لایه‌ی سطح بالای nn.Linear صرفاً یک پوشش (Wrapper) برای همان ضرب ماتریسی و جمع بایاس است که در حلقه دستی استفاده شد.

حالت‌های ماژول و استنتاج

در حالی که تنسورهای ساده حالت آموزش ندارند، ماژول‌های پایتورچ از .train() و .eval() استفاده می‌کنند. این موضوع برای لایه‌هایی مثل دراپ‌اوت (Dropout) — که برای جلوگیری از بیش‌برازش (Overfitting) برخی مقادیر را تصادفی صفر می‌کند — حیاتی است. در تستی با $p=0.5$، دراپ‌اوت پنج مقدار از هشت مقدار را صفر کرد و بازماندگان را در ضریب $1/(1-p)=2$ ضرب کرد تا میانگین خروجی ثابت بماند. در حالت ارزیابی، این رفتار باید متوقف شود و تمام مقادیر بدون تغییر عبور کنند.

تفاوت بین حالت ماژول و ثبت گرادیان را باید به دقت درک کرد:

  • .eval(): رفتار لایه‌های حساس به حالت (مثل Dropout) را برای استنتاج (Inference) تغییر می‌دهد اما روی ثبت گرادیان اثر ندارد.
  • torch.no_grad(): ساخت گراف محاسباتی را به‌طور کامل متوقف می‌کند تا حافظه و محاسبات کاهش یابد، اما حالت ماژول را تغییر نمی‌دهد.

برای استنتاج در محیط عملیاتی، استفاده هم‌زمان از هر دو استاندارد است تا خروجی قطعی و بهره‌وری حافظه به حداکثر برسد. یک ماژول در حالت .eval() که داخل بلوک no_grad() قرار دارد، استاندارد استقرار (Deployment) است.

نقاط بازرسی و جداسازی

ذخیره‌ی دستی مدل شامل ذخیره‌ی دیکشنری‌ای از تنسورهاست. برای ذخیره‌ی مقادیر یادگرفته‌شده بدون کل تاریخچه‌ی آموزش، از متد .detach() استفاده می‌شود. این کار کپی‌ای از تنسور ایجاد می‌کند که حافظه را به اشتراک می‌گذارد اما ردیابی گرادیان ندارد و منجر به یک فایل نقطه بازرسی (Checkpoint) بسیار کوچک می‌شود که فقط شامل تنسورهای وزن و بایاس است. این یک نقطه بازرسی پارامتر است، نه وضعیت کامل آموزش، زیرا فاقد وضعیت بهینه‌ساز یا شمارنده دوره‌هاست.

هنگام بارگذاری این نقاط بازرسی، استفاده از weights_only=True در torch.load توصیه می‌شود تا انواع بارگذاری شده به تنسورها و داده‌های ساده محدود شود و ریسک‌های امنیتی مربوط به بارگذاری اشیای نامعتبر پایتون کاهش یابد. در آزمایش‌ها، مدل بارگذاری‌شده با موفقیت مقدار ۷۶.۹۹۸۵ فارنهایت را برای ۲۵ درجه سلسیوس با استفاده از تنسورهای ذخیره شده و فرمول پیش‌بینی محاسبه کرد.

خلاصه نتایج

مورد مقدار تأیید شده
وظیفه سلسیوس به فارنهایت ($F = C \times 1.8 + 32$)
مدل دو تنسور ساده (بدون nn.Linear و بهینه‌ساز)
سخت‌افزار CPU
پیش از آموزش وزن: ۱.۵۴۰۹۹۶ / بایاس: ۰.۲۹۳۴۲۹-
نخستین گرادیان (backward) وزن: ۲۳۱۴.۶۴۰۴- / بایاس: ۷۳.۸۲۴۷-
گرادیان دستی وزن: ۲۳۱۴.۶۴۰۴- / بایاس: ۷۳.۸۲۴۷-
پس از آموزش وزن: ۱.۸۰۰۰ / بایاس: ۳۱.۹۹۸۱
استنتاج (۲۵ درجه) ۷۶.۹۹۸۵ درجه فارنهایت
ابرپارامترها نرخ یادگیری: ۰.۰۰۰۳ / دوره‌ها: ۲۰,۰۰۰
زیان نهایی (بدون گرد کردن) ۲.۹۳۷۱۲۵۱۴۵۷۶۳۲۷۵۲e-۰۶

این تمرین نشان می‌دهد که وقتی یک فرآیند آموزش واگرا می‌شود یا گرادیان‌ها محو می‌شوند، مشکل همیشه در یکی از این چهار مرحله است. درک اینکه backward() همان حساب دیفرانسیل ساده است، به توسعه‌دهندگان اجازه می‌دهد مدل‌های پیچیده را با تجسم جریان گرادیان‌ها از زیان به سمت وزن‌ها عیب‌یابی کنند.

گام بعدی شما

  • برای درک عمیق‌تر، یک مدل رگرسیون ساده را بدون استفاده از nn.Module و فقط با تنسورها پیاده‌سازی کنید.
  • تفاوت رفتار loss.backward() را در حالت‌هایی که requires_grad=False است برای ورودی‌ها بررسی کنید.
  • اثر تغییر نرخ یادگیری (Learning Rate) را روی سرعت هم‌گرایی در یک حلقه دستی مشاهده کنید.

اما داستان سخت‌افزاری این محاسبات حتی شگفت‌انگیزتر است — به تحلیل ما درباره‌ی تراشه‌های Blackwell مراجعه کنید.

چرا این موضوع مهم است؟

این یافته با تکیه بر اعتبار ریاضیات دیفرانسیل، ابهامات مربوط به موتور Autograd را برطرف کرده و ثابت می‌کند که دقت مدل‌ها وابسته به محاسبات دقیق است نه تخمین‌های آماری. این موضوع برای مهندسانی که نیاز به بهینه‌سازی شدید حافظه و سرعت استنتاج دارند، حیاتی است.

تأثیر برای ایران

این تحلیل برای برنامه‌نویسان و پژوهشگران ایرانی که با محدودیت‌های سخت‌افزاری (GPU) مواجه‌اند، اهمیت دارد؛ زیرا درک لایه‌های زیرین به آن‌ها اجازه می‌دهد مدل‌های بهینه‌تر و سبک‌تری را بدون تکیه بر لایه‌های سنگین فریم‌ورک طراحی کنند.

·نگاه ما
تحریریه دات‌هوش

این تحلیل ثابت می‌کند که انتزاع‌های سطح بالای PyTorch هیچ جادویی در محاسبات ریاضی ندارند و صرفاً برای افزایش سرعت توسعه‌اند. وقتی توسعه‌دهنده متوجه شود که `backward()` دقیقاً همان مشتقات کتاب‌های ریاضی است، عیب‌یابی پدیده‌هایی مثل محوشدن گرادیان از یک حدس مهندسی به یک تحلیل ریاضی تبدیل می‌شود. این رویکرد، مرز بین کدنویسی مدل و درک ریاضیات زیربنایی را از بین می‌برد.

منابع

این گزارش با خط‌لولهٔ خودکار دات‌هوش از منابع معتبر جهانی تدوین و زیر نظر تحریریه منتشر شده است. روش کار ما

گفتگو

پنج‌شنبه‌های هوش‌محور

بسته‌ی هفتگی دات‌هوش

۵ خبر، ۲ ابزار، ۱ پرامپت در هر شماره. به‌زودی راه‌اندازی می‌شود — هر پنج‌شنبه صبح.

خبر کلیدی
ابزار کاربردی
پرامپت حرفه‌ای
تحلیل پژوهش
به‌زودی
زاویه‌ی ایرانی
به‌زودی
تمرین این هفته
به‌زودی

راهنماهای دات‌هوش

راهنماهای کاربردیِ دات‌هوش برای کار با هوش مصنوعی — از همین‌جا شروع کنید:

دات‌هوش

راهنمای فارسی هوش مصنوعی — با نگاه به ایران

اخبار روزانه، معرفی ابزارها و مدل‌ها، و آموزشِ کار با هوش مصنوعی؛ همیشه با این پرسش که از ایران چه چیزی کار می‌کند و چه چیزی نه.