در گامهای پیشین دیدید که چگونه میتوان اجزای یک شبکه عصبی را با پایتون خالص از صفر ساخت. با این حال، اجرای همان ساختارها در ابعاد واقعی و با حجم دادههای سنگین روی پایتون خام بهشدت کند است و ساعتها زمان میبرد. PyTorch با حفظ همان منطق و انتزاعهای ذهنی، محاسبات سنگین را به کرنلهای فوقالعاده بهینهی C++ و CUDA میسپارد تا آموزش مدلها صدها برابر سریعتر شود.
این فریمورک با فلسفهی اجرای مشتاقانه (eager execution) کار دیباگ را شبیه کدنویسی روزمره با پایتون کرده است. بدون نیاز به کامپایل گرافهای پیچیده، میتوانید در لحظه جریان داده و گرادیانها را بررسی کنید. یادگیری این ابزار، استاندارد صنعتی امروز برای ورود به دنیای مدلهای زبانی، پردازش تصویر و سیستمهای هوش مصنوعی مدرن است.
برای اجرای کدهای این درس و تمرینهای پایه، نیازی به کارت گرافیک خارجی یا اجارهی GPUهای گرانقیمت ابری ندارید؛ مدل نمونهی این آموزش روی همان پردازندهی اصلی (CPU) سیستم شخصی شما ظرف چند ده ثانیه بهراحتی اجرا میشود. اگر در نصب بستهها از PyPI با ناپایداری ارتباط مواجه شدید، استفاده از میرورهای داخلی پایتون روند دانلود را تسهیل میکند.
شما موتور را با پیستونها و میللنگ ساختید؛ حالا وقت آن است موتوری را یاد بگیرید که همه در دنیای واقعی با آن رانندگی میکنند.
اهداف یادگیری
- ساخت و آموزش شبکههای عصبی با استفاده از
nn.Module،nn.Sequentialو autograd در PyTorch - استفاده از تنسورهای PyTorch، شتابدهی GPU و حلقهی استاندارد آموزش (zero_grad، forward، loss، backward، step)
- تبدیل بخشهای مینیفریمورک دستساز خودتان به معادلهای آنها در PyTorch
- پروفایلینگ و مقایسهی سرعت آموزش بین فریمورک پایتون خالص خودتان و PyTorch روی یک مسئلهی یکسان
صورت مسئله
حالا یک مینیفریمورک کاملاً کاربردی در اختیار دارید: لایههای خطی، ReLU، دراپاوت (dropout)، نرمالسازی دستهای (batch norm)، بهینهساز Adam، یک DataLoader و یک حلقهی آموزش. این فریمورک با پایتون خالص، یک شبکهی 4 لایه را برای مسئلهی دستهبندی دایرهها آموزش میدهد.
اما همین فریمورک، روی مسئلهی یکسان 500 برابر کندتر از PyTorch عمل میکند.
مینیفریمورک شما هر بار یک نمونه را با حلقههای تودرتوی پایتون پردازش میکند. در مقابل، PyTorch همان عملیات را به کرنلهای (kernels) بهینهشدهی C++/CUDA میفرستد که روی GPU اجرا میشوند. روی یک کارت گرافیک NVIDIA A100، آموزش مدل ResNet-50 با 25.6 میلیون پارامتر روی مجموعهدادهی ImageNet با 1.28 میلیون تصویر در PyTorch حدود 6 ساعت زمان میبرد. فریمورک شما برای همین تسک تقریباً به 3,000 ساعت زمان نیاز خواهد داشت؛ تازه اگر قبل از آن حافظهاش تمام نشود و کرش نکند.
سرعت تنها تفاوت موجود نیست. فریمورک شما هیچ پشتیبانیای از GPU ندارد؛ از مشتقگیری خودکار (automatic differentiation) بیبهره است و برای تکتک ماژولها متد backward() را دستی نوشتید؛ قابلیت سریالسازی (serialization) ندارد؛ از آموزش توزیعشده پشتیبانی نمیکند؛ فاقد دقت ترکیبی (mixed precision) است؛ و راهی هم برای دیباگ جریان گرادیان بهجز دستورات print ندارد.
PyTorch تمام این کاستیها را برطرف میکند؛ آن هم در حالی که دقیقاً همان مدل ذهنی قبلی شما را حفظ کرده است: Module، forward()، parameters()، backward() و optimizer.step(). مفاهیم یکبهیک منتقل میشوند و سینتکس تقریباً یکی است. تفاوت فقط اینجاست که PyTorch یک دهه مهندسی سیستم را پشت همان رابط کاربری پیاده کرده که شما از صفر طراحی کرده بودید.
مفاهیم پایه
چرا PyTorch برنده شد؟
در سال 2015، TensorFlow از شما میخواست پیش از اجرای هر کاری، ابتدا یک گراف محاسباتی (computational graph) ایستا تعریف کنید. گراف را میساختید، کامپایل میکردید و سپس داده را از درونش عبور میدادید. دیباگ کردن یعنی زل زدن به تصویر گراف؛ تغییر معماری هم یعنی ساختن کل گراف از اول.
PyTorch در سال 2017 با فلسفهای متفاوت وارد میدان شد: اجرای مشتاقانه (eager execution). شما کد پایتون مینویسید و بلافاصله اجرا میشود. دستور y = model(x) واقعاً خروجی y را همین حالا محاسبه میکند، نه اینکه «یک گره به گرافی اضافه کند که بعداً y را حساب خواهد کرد». این یعنی ابزارهای استاندارد دیباگ پایتون بهراحتی کار میکردند؛ دستور print() جواب میداد، pdb کار میکرد و استفاده از دستورات شرطی if/else داخل گذر رو به جلو (forward pass) ممکن بود.
تا سال 2020، بازار حرف نهاییاش را زد. سهم PyTorch در مقالات پژوهشی یادگیری ماشین از 7% در سال 2017 به بالای 75% در سال 2022 رسید. شرکتهای Meta، Google DeepMind، OpenAI، Anthropic و Hugging Face همگی PyTorch را فریمورک اصلی خود کردهاند. در پاسخ به این موج، TensorFlow 2.x نیز اجرای مشتاقانه را به سیستم خود آورد؛ تاییدی ضمنی بر اینکه طراحی PyTorch از ابتدا درست بوده است.
درس ماجرا: تجربهی توسعهدهنده اثر فزاینده دارد. فریمورکی که در اجرا 10% کندتر اما دیباگ کردنش 50% سریعتر باشد، همیشه برنده است.
تنسورها (Tensors)
تنسور (tensor) یک آرایهی چندبعدی است که سه ویژگی بسیار مهم دارد: شکل (shape)، نوع داده (dtype) و دستگاه پردازشی (device).
import torch
x = torch.zeros(3, 4) # shape: (3, 4), dtype: float32, device: cpu
x = torch.randn(2, 3, 224, 224) # batch of 2 RGB images, 224x224
x = torch.tensor([1, 2, 3]) # from a Python list
شکل (Shape) همان ابعاد آرایه است. یک اسکالر شکل () دارد، یک بردار (n,)، یک ماتریس (m, n) و یک دسته از تصاویر به شکل (batch, channels, height, width) است.
نوع داده (Dtype) دقت عددی و مصرف حافظه را کنترل میکند.
| dtype | بیت | دامنه | کاربرد |
|---|---|---|---|
| float32 | 32 | حدود 7 رقم اعشار | آموزش پیشفرض |
| float16 | 16 | حدود 3.3 رقم اعشار | دقت ترکیبی |
| bfloat16 | 16 | دامنهی یکسان با float32، دقت کمتر | آموزش مدلهای زبانی بزرگ (LLM) |
| int8 | 8 | از -128 تا 127 | استنتاج کوانتیزهشده |
دستگاه (Device) مشخص میکند محاسبات در کجا انجام شوند.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = torch.randn(3, 4, device=device)
x = x.to("cuda")
x = x.cpu()
هر عملیاتی نیازمند این است که همهی تنسورها روی یک دستگاه مشترک باشند. این شایعترین خطای مبتدیان در PyTorch است: RuntimeError: Expected all tensors to be on the same device. برای حل آن، قبل از شروع محاسبات، همهی تنسورها را به یک دستگاه منتقل کنید.
تغییر شکل (Reshaping) با مرتبهی زمانی ثابت انجام میشود؛ چون فقط متادیتا را تغییر میدهد، نه خود دادهها را.
x = torch.randn(2, 3, 4)
x.view(2, 12) # reshape to (2, 12) -- must be contiguous
x.reshape(6, 4) # reshape to (6, 4) -- works always
x.permute(2, 0, 1) # reorder dimensions
x.unsqueeze(0) # add dimension: (1, 2, 3, 4)
x.squeeze() # remove size-1 dimensions
مشتقگیری خودکار (Autograd)
در مینیفریمورک خودتان مجبور بودید متد backward() را برای تکتک ماژولها بنویسید. در PyTorch نیازی به این کار نیست. PyTorch همهی عملیات روی تنسورها را در یک گراف جهتدار بدون دور (گراف محاسباتی) ثبت میکند و سپس با پیمایش معکوس این گراف، گرادیانها را بهطور خودکار به دست میآورد.
تفاوت اساسی با فریمورک شما: PyTorch از مشتقگیری خودکار مبتنی بر نوار (tape-based autodiff) استفاده میکند. هر عملیات در طول گذر رو به جلو به یک «نوار» اضافه میشود و با فراخوانی متد .backward()، این نوار در جهت عکس بازپخش خواهد شد.
x = torch.randn(3, requires_grad=True)
y = x ** 2 + 3 * x
z = y.sum()
z.backward()
print(x.grad) # dz/dx = 2x + 3
سه قاعدهی autograd:
- فقط تنسورهای برگ (leaf tensors) که
requires_grad=Trueدارند، گرادیان جمع میکنند - گرادیانها بهطور پیشفرض با هم جمع میشوند؛ پس قبل از هر گذر رو به عقب، حتماً
optimizer.zero_grad()را صدا بزنید - دستور
torch.no_grad()ردیابی گرادیان را غیرفعال میکند (که حین ارزیابی مدل کاربرد دارد)
ماژولهای شبکه (nn.Module)
کلاس nn.Module کلاس پایهی تمام اجزای شبکهی عصبی در PyTorch است. شما این انتزاع را در درس 10 ساختید. نسخهی PyTorch امکاناتی مثل ثبت خودکار پارامترها، کشف بازگشتی ماژولها، مدیریت دستگاه پردازشی و سریالسازی state dict را اضافه دارد.
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.layer1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
return x
وقتی یک nn.Module یا nn.Parameter را بهعنوان ویژگی در متد __init__ تعریف میکنید، PyTorch آن را بهطور خودکار ثبت میکند. متد model.parameters() تمام پارامترهای ثبتشده را بهصورت بازگشتی جمعآوری میکند. به همین دلیل هرگز نیازی نیست وزنها را بهصورت دستی جمع کنید؛ کاری که در مینیفریمورک ناچار به انجامش بودید.
بلوکهای ساختمانی اصلی:
| ماژول | عملکرد | پارامترها |
|---|---|---|
| nn.Linear(in, out) | in*out + out | |
| nn.Conv2d(in_ch, out_ch, k) | کانولوشن دوبعدی | in_chout_chk*k + out_ch |
| nn.BatchNorm1d(features) | نرمالسازی فعالسازیها | 2 * features |
| nn.Dropout(p) | صفر کردن تصادفی مقادیر | 0 |
| nn.ReLU() | 0 | |
| nn.GELU() | خطی با خطای گاوسی (Gaussian error linear) | 0 |
| nn.Embedding(vocab, dim) | جدول لوکآپ (امبدینگ) | vocab * dim |
| nn.LayerNorm(dim) | نرمالسازی به ازای هر نمونه | 2 * dim |
توابع زیان و بهینهسازها
PyTorch نسخههای آمادهی محیط پروداکشن از تمام ابزارهایی را که ساختید، در خود دارد.
توابع زیان (Loss functions) (از ماژول torch.nn):
| تابع زیان (loss) | تسک | ورودی |
|---|---|---|
| nn.MSELoss() | رگرسیون | هر شکلی |
| nn.CrossEntropyLoss() | دستهبندی چندکلاسه | لاجیتها (نه سافتمکس) |
| nn.BCEWithLogitsLoss() | دستهبندی دوکلاسه | لاجیتها (نه سیگموئید) |
| nn.L1Loss() | رگرسیون (مقاوم به دادههای پرت) | هر شکلی |
| nn.CTCLoss() | همترازی توالیها | لاگ احتمالها |
نکته: CrossEntropyLoss در داخل خود LogSoftmax و NLLLoss را ترکیب میکند. ورودی آن باید لاجیتهای خام (logits) باشد، نه خروجی سافتمکس. این یکی از خطاهای رایجی است که بیصدا گرادیانهای غلط تولید میکند.
بهینهسازها (Optimizers) (از ماژول torch.optim):
| بهینهساز | زمان استفاده | نرخ یادگیری معمول |
|---|---|---|
| SGD(params, lr, momentum) | مدلهای CNN، پایپلاینهای تنظیمشده | 0.01--0.1 |
| Adam(params, lr) | نقطهی شروع پیشفرض | 1e-3 |
| AdamW(params, lr, weight_decay) | مدلهای ترنسفورمر، فاینتیونینگ | 1e-4--1e-3 |
| LBFGS(params) | مقیاس کوچک، بهینهسازی مرتبه دوم | 1.0 |
حلقهی آموزش
هر حلقهی آموزشی در PyTorch از یک الگوی 5 مرحلهای همیشگی پیروی میکند. این مراحل را از درس 10 میشناسید.
الگوی استاندارد:
for epoch in range(num_epochs):
model.train()
for inputs, targets in train_loader:
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
پنج خط کد درون حلقهی دستهها؛ همان پنج خطی که پایهی آموزش مدلهایی مثل GPT-4، Stable Diffusion و LLaMA بودهاند. معماری تغییر میکند، داده عوض میشود، اما این پنج خط ثابت میمانند.
ساختار Dataset و DataLoader
کلاس Dataset در PyTorch یک کلاس انتزاعی با دو متد است: __len__ و __getitem__. کلاس DataLoader نیز با دستهبندی، بر زدن دادهها و بارگذاری چندپردازشی، آن را در بر میگیرد.
from torch.utils.data import Dataset, DataLoader
class MNISTDataset(Dataset):
def __init__(self, images, labels):
self.images = images
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return self.images[idx], self.labels[idx]
loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=4)
آرگومان num_workers=4 باعث میشود 4 پردازه بهطور موازی دادهها را بارگذاری کنند؛ درست همزمان با اینکه GPU روی دستهی فعلی آموزش میبیند. در تسکهایی که گلوگاه آنها دیسک است (مثل تصاویر حجیم یا فایلهای صوتی)، فقط همین پارامتر میتواند سرعت آموزش را دو برابر کند.
آموزش روی GPU
انتقال مدل به GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
این دستور تمام پارامترها و بافرهای مدل را بهصورت بازگشتی به GPU میبرد. سپس هنگام آموزش، باید هر دسته را هم منتقل کنید:
inputs, targets = inputs.to(device), targets.to(device)
دقت ترکیبی (Mixed precision) در کارتهای گرافیک مدرن (مثل A100، H100 و RTX 4090)، با اجرای محاسبات رفت و برگشت در قالب float16 و حفظ وزنهای اصلی در قالب float32، مصرف حافظه را نصف و خروجی پردازش را دو برابر میکند:
from torch.amp import autocast, GradScaler
scaler = GradScaler()
for inputs, targets in loader:
with autocast(device_type="cuda"):
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
مقایسه: مینیفریمورک در برابر PyTorch و JAX
| قابلیت | مینیفریمورک (درس 10) | PyTorch | JAX |
|---|---|---|---|
| مشتقگیری خودکار | نوشتن دستی backward() | مبتنی بر نوار (autograd) | تبدیلهای تابعی (Functional transforms) |
| نحوه اجرا | مشتاقانه (حلقههای پایتون) | مشتاقانه (کرنلهای C++) | ردیابیشده + کامپایل JIT |
| پشتیبانی از GPU | ندارد | دارد (CUDA, ROCm, MPS) | دارد (CUDA, TPU) |
| سرعت (MNIST روی MLP) | حدود 300 ثانیه در هر ایپاک | حدود 0.5 ثانیه در هر ایپاک | حدود 0.3 ثانیه در هر ایپاک |
| سیستم ماژولار | کلاس اختصاصی Module | nn.Module | توابع بدون وضعیت (Flax/Equinox) |
| دیباگ کردن | با دستور print() | با print()، pdb، breakpoint() | سختتر (ردیابی JIT مانع چاپ print میشود) |
| اکوسیستم | ندارد | Hugging Face، Lightning، timm | Flax، Optax، Orbax |
| منحنی یادگیری | خودتان ساختید | متوسط | شیب تند (رویکرد تابعی) |
| کاربرد در پروداکشن | مسائل کوچک آموزشی | Meta، OpenAI، Anthropic، HF | Google DeepMind، Midjourney |
پیادهسازی عملی
یک شبکهی پرسپترون چندلایه (MLP) سهلایه را روی MNIST، فقط با متدهای پایهای PyTorch آموزش میدهیم؛ بدون ابزارهای سطح بالا و بدون استفاده از torchvision.datasets. دادههای خام را خودمان دانلود و پارس میکنیم.
گام 1: بارگذاری دادههای خام MNIST
مجموعهدادهی MNIST در قالب 4 فایل فشرده با پسوند gz منتشر میشود: تصاویر آموزش (60,000 در ابعاد 28 در 28)، برچسبهای آموزش، تصاویر آزمون (10,000 در ابعاد 28 در 28) و برچسبهای آزمون. آنها را دانلود کرده و فرمت باینریشان را پارس میکنیم.
import torch
import torch.nn as nn
import struct
import gzip
import urllib.request
import os
def download_mnist(path="./mnist_data"):
base_url = "https://storage.googleapis.com/cvdf-datasets/mnist/"
files = [
"train-images-idx3-ubyte.gz",
"train-labels-idx1-ubyte.gz",
"t10k-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz",
]
os.makedirs(path, exist_ok=True)
for f in files:
filepath = os.path.join(path, f)
if not os.path.exists(filepath):
urllib.request.urlretrieve(base_url + f, filepath)
def load_images(filepath):
with gzip.open(filepath, "rb") as f:
magic, num, rows, cols = struct.unpack(">IIII", f.read(16))
data = f.read()
images = torch.frombuffer(bytearray(data), dtype=torch.uint8)
images = images.reshape(num, rows * cols).float() / 255.0
return images
def load_labels(filepath):
with gzip.open(filepath, "rb") as f:
magic, num = struct.unpack(">II", f.read(8))
data = f.read()
labels = torch.frombuffer(bytearray(data), dtype=torch.uint8).long()
return labels
گام 2: تعریف ساختار مدل
یک MLP سهلایه با ساختار: 784 -> 256 -> 128 -> 10. از تابع فعالساز ReLU و لایهی دراپاوت برای منظمسازی استفاده میکنیم؛ برای حفظ سادگی، خبری از نرمالسازی دستهای نیست.
class MNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10),
)
def forward(self, x):
return self.net(x)
لایهی خروجی، 10 لاجیت خام تولید میکند (یکی به ازای هر رقم). از سافتمکس استفاده نمیکنیم، چون CrossEntropyLoss این بخش را بهصورت داخلی مدیریت میکند.
تعداد کل پارامترها: . این عدد با معیارهای امروزی بسیار کوچک است؛ مثلاً نسخهی کوچک GPT-2 حدود 124 میلیون پارامتر دارد. به همین دلیل این مدل ظرف چند ثانیه آموزش میبیند.
گام 3: حلقهی آموزش
الگوی استاندارد: forward، سپس loss، بعد backward و در نهایت step.
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0
correct = 0
total = 0
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
به استفاده از torch.no_grad() در زمان ارزیابی دقت کنید. این کار مشتقگیری خودکار را غیرفعال کرده و مصرف حافظه را کاهش داده و سرعت استنتاج را بالا میبرد. بدون این دستور، PyTorch یک گراف محاسباتی میسازد که هرگز استفادهاش نمیکنید.
گام 4: اتصال تمام اجزا به یکدیگر
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
download_mnist()
train_images = load_images("./mnist_data/train-images-idx3-ubyte.gz")
train_labels = load_labels("./mnist_data/train-labels-idx1-ubyte.gz")
test_images = load_images("./mnist_data/t10k-images-idx3-ubyte.gz")
test_labels = load_labels("./mnist_data/t10k-labels-idx1-ubyte.gz")
train_dataset = torch.utils.data.TensorDataset(train_images, train_labels)
test_dataset = torch.utils.data.TensorDataset(test_images, test_labels)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=64, shuffle=True
)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=256, shuffle=False
)
model = MNISTModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
num_params = sum(p.numel() for p in model.parameters())
print(f"Device: {device}")
print(f"Parameters: {num_params:,}")
print(f"Train samples: {len(train_dataset):,}")
print(f"Test samples: {len(test_dataset):,}")
print()
for epoch in range(10):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device
)
test_loss, test_acc = evaluate(
model, test_loader, criterion, device
)
print(
f"Epoch {epoch+1:2d} | "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | "
f"Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.4f}"
)
torch.save(model.state_dict(), "mnist_mlp.pt")
print(f"\nModel saved to mnist_mlp.pt")
print(f"Final test accuracy: {test_acc:.4f}")
خروجی مورد انتظار پس از 10 ایپاک: حدود 97.8% دقت تست. زمان آموزش روی CPU: حدود 30 ثانیه؛ روی GPU: حدود 5 ثانیه؛ و روی مینیفریمورک دستساز خودتان با همین معماری: حدود 45 دقیقه.
استفادهی عملی
مقایسهی سریع: مینیفریمورک در برابر PyTorch
| مینیفریمورک (درس 10) | PyTorch |
|---|---|
model = Sequential(Linear(784, 256), ReLU(), ...) |
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), ...) |
pred = model.forward(x) |
pred = model(x) |
optimizer.zero_grad() |
optimizer.zero_grad() |
grad = criterion.backward() سپس model.backward(grad) |
loss.backward() |
optimizer.step() |
optimizer.step() |
| بدون GPU | model.to("cuda") |
| نوشتن دستی backward برای هر ماژول | سیستم Autograd همهچیز را مدیریت میکند |
همانطور که میبینید، رابط برنامهنویسی تقریباً یکسان است؛ تفاوت اصلی در سازوکارهای پشت صحنه نهفته است.
ذخیره و بارگذاری مدلها
torch.save(model.state_dict(), "model.pt")
model = MNISTModel()
model.load_state_dict(torch.load("model.pt", weights_only=True))
model.eval()
همیشه state_dict() (دیکشنری پارامترها) را ذخیره کنید، نه شیء کلی مدل را. ذخیرهی مستقیم شیء از pickle استفاده میکند که با تغییر ساختار کدها در آینده از کار میافتد، اما state dictها کاملاً قابلانتقال هستند.
زمانبندی نرخ یادگیری (Learning Rate Scheduling)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=10
)
for epoch in range(10):
train_one_epoch(model, train_loader, criterion, optimizer, device)
scheduler.step()
کتابخانهی PyTorch بیش از 15 نوع اسکدولر مختلف ارائه میدهد: StepLR، ExponentialLR، CosineAnnealingLR، OneCycleLR و ReduceLROnPlateau. همگی آنها به رابط استاندارد بهینهساز متصل میشوند.
تحویل پروژه
این درس دو خروجی تولید میکند:
outputs/prompt-pytorch-debugger.md-- پرامپتی برای عیبیابی خطاهای رایج آموزش در PyTorchoutputs/skill-pytorch-patterns.md-- یک مرجع مهارتی برای الگوهای آموزش در PyTorch
تمرینها
-
اضافه کردن نرمالسازی دستهای: لایهی
nn.BatchNorm1dرا بعد از هر لایهی خطی (و قبل از تابع فعالساز) قرار دهید. دقت تست و سرعت آموزش را با نسخهی صرفاً مبتنی بر دراپاوت مقایسه کنید. نرمالسازی دستهای باید در تعداد ایپاکهای کمتری به دقت بالای 98% برسد. -
پیادهسازی ابزار یافتن نرخ یادگیری (LR Finder): مدل را برای یک ایپاک با نرخ یادگیری که بهصورت نمایی افزایش مییابد (از 1e-7 تا 1.0) آموزش دهید. نمودار تابع زیان (loss) بر حسب نرخ یادگیری را رسم کنید. بهترین نرخ یادگیری، درست قبل از نقطهای است که در آن مقدار زیان شروع به بالا رفتن میکند. با این روش، نرخ بهینهتری برای مدل MNIST انتخاب کنید.
-
انتقال به GPU همراه با دقت ترکیبی: قابلیتهای
torch.amp.autocastوGradScalerرا به حلقهی آموزش اضافه کنید. توان پردازش (تعداد نمونه بر ثانیه) را روی GPU در دو حالت با و بدون دقت ترکیبی بسنجید. روی کارت A100، منتظر حدود 2 برابر افزایش سرعت باشید. -
ساخت یک Dataset سفارشی: مجموعهدادهی Fashion-MNIST را دانلود کنید (فرمت آن مشابه MNIST است اما تصاویر لباس دارد). یک کلاس به نام
FashionMNISTDataset(Dataset)بسازید و متدهای__getitem__و__len__را در آن پیاده کنید. همان شبکهی MLP را آموزش دهید و دقت آن را مقایسه کنید. مسئلهی Fashion-MNIST سختتر است؛ انتظار دقتی حدود 88% در برابر 98% داشته باشید. -
جایگزینی Adam با SGD به همراه تکانه (momentum): مدل را با
SGD(params, lr=0.01, momentum=0.9)آموزش دهید. منحنیهای یادگیری (learning curves) را مقایسه کنید. سپس یک اسکدولرCosineAnnealingLRاضافه کنید و ببینید آیا تا ایپاک 10، SGD میتواند خودش را به دقت Adam برساند یا خیر.
اصطلاحات کلیدی
| اصطلاح | اصطلاح عامیانه | معنای دقیق فنی |
|---|---|---|
| تنسور (Tensor) | «یک آرایهی چندبعدی» | آرایهای دارای نوع مشخص و آگاه از سختافزار (device) که پشتیبانی از مشتقگیری خودکار در تکتک عملیاتش گنجانده شده است |
| مشتقگیری خودکار (Autograd) | «بکپراپ خودکار» | سیستمی مبتنی بر نوار ضبط که عملیات گذر رو به جلو را ذخیره کرده و با بازپخش معکوس آنها، گرادیانهای دقیق را محاسبه میکند |
| ماژول (nn.Module) | «یک لایه» | کلاس پایه برای هر بلوک محاسباتی مشتقپذیر؛ پارامترها را ثبت میکند، از ماژولهای تودرتو پشتیبانی میکند و حالتهای train/eval را مدیریت میکند |
| دیکشنری وزنها (state_dict) | «وزنهای مدل» | یک شیء از نوع OrderedDict که نام پارامترها را به تنسورها متصل میکند؛ نمایشی استاندارد و قابل سریالسازی از یک مدل آموزشدیده |
| متد .backward() | «محاسبهی گرادیانها» | پیمایش معکوس گراف محاسباتی جهت محاسبه و انباشت گرادیانها برای همهی تنسورهای برگ با شرط requires_grad=True |
| متد .to(device) | «انتقال به GPU» | انتقال بازگشتی همهی پارامترها و بافرها به سختافزار مشخصشده (CPU، CUDA، MPS) |
| پایپلاین داده (DataLoader) | «پایپلاین داده» | یک پیمایشگر (iterator) که عملیات دستهبندی، بر زدن و بارگذاری دادهها از Dataset را به شکل اختیاری موازیسازی میکند |
| دقت ترکیبی (Mixed precision) | «استفاده از float16» | آموزش با فرمت float16 در رفت و برگشت به منظور افزایش سرعت، در حالی که وزنهای اصلی برای پایداری عددی با فرمت float32 نگه داشته میشوند |
| اجرای مشتاقانه (Eager execution) | «اجرای فوری» | اجرای بلادرنگ عملیات بلافاصله پس از فراخوانی، بدون موکول شدن به مرحلهی کامپایل بعدی؛ اصلیترین تفاوت طراحی PyTorch با نسخهی 1.x در TensorFlow |
| صفر کردن گرادیان (zero_grad) | «ریست کردن گرادیانها» | صفر کردن گرادیان همهی پارامترها پیش از گذر معکوس بعدی، زیرا PyTorch بهطور پیشفرض گرادیانها را انباشته میکند |
برای مطالعهی بیشتر
- مقالهی Paszke و همکاران، با عنوان «PyTorch: An Imperative Style, High-Performance Deep Learning Library» (سال 2019)؛ مقالهی اصلی که مصالحههای بهکاررفته در طراحی معماری PyTorch را توضیح میدهد
- آموزشهای رسمی PyTorch: صفحهی «Learning PyTorch with Examples» در (https://pytorch.org/tutorials/beginner/pytorch_with_examples.html)؛ مسیر رسمی حرکت از شناخت تنسورها تا یادگیری
nn.Module - راهنمای بهینهسازی عملکرد PyTorch در (https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html)؛ دقت ترکیبی، ورکرهای DataLoader، حافظهی پینشده (pinned memory) و سایر روشهای بهینهسازی در محیط پروداکشن
- مقالهی Horace He با عنوان «Making Deep Learning Go Brrrr» در (https://horace.io/brrr_intro.html)؛ بررسی دلایل سرعت بالای آموزش روی GPU به همراه ترفندهای اختصاصی بهینهسازی در PyTorch
