تصور کنید تمام لایههای آمادهی 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 مراجعه کنید.




گفتگو