# پاسخ یک ترفند جادویی نیست
مدل را با یک چرخه بسیار ساده اما در مقیاس بسیار بزرگ آموزش میدهیم:
training_loop.txt
همین چرخه، هزاران یا میلیونها بار تکرار میشود. مدل در هر مرحله کمی کمتر اشتباه میکند و وزنهایش کمی تغییر میکنند. آموزش شبکههای عصبی در PyTorch نیز دقیقاً بر همین اجزای اصلی بنا شده است: داده، مدل، محاسبه خطا، انتشار گرادیان و بهروزرسانی پارامترها.
# مدل در اولین لحظه چه چیزی میداند؟
یک تصور رایج این است که وقتی میگوییم «یک مدل را آموزش دادیم»، انگار اطلاعاتی را داخل آن ذخیره کردهایم. اما مدل به شکل یک کتابخانه از جملهها کار نمیکند. در یک شبکه عصبی، چیزی که در طول آموزش تغییر میکند عمدتاً پارامترهای عددی مدل است.
initial_weights.py — python3
import torch
from torch import nn
# A freshly created layer — before any training:
layer = nn.Linear(3, 3)
print(layer.weight.data)
print(layer.bias.data)
▶ tensor([[ 0.12, -0.71, 0.33],
[ 0.52, 0.08, -0.41],
[ -0.29, 0.44, 0.17]])
▶ tensor([0.15, -0.04, 0.22])
این اعداد در ابتدای آموزش با روشهای Initialization مناسب تولید میشوند — نه کاملاً تصادفیِ بیخبر، بلکه با توزیعهایی که یادگیری را پایدارتر میکنند (مثل Kaiming/He برای شبکههای با ReLU). مدل هنوز چیزی را «بلد» نیست.
چرا Initialization مهم است؟ اگر همه وزنها صفر یا یکسان باشند، همه نورونها گرادیان یکسانی میگیرند و عملاً یک نورون تکراری میشوند — شبکه هرگز یاد نمیگیرد. اگر هم خیلی بزرگ باشند، سیگنالها در لایهها منفجر میشوند. به همین دلیل توزیع اولیه وزنها یکی از جزئیات مهم طراحی شبکه است.
# اولین مرحله: داده
برای آموزش، به مدل مثال میدهیم. فرض کنید میخواهیم مدلی بسازیم که تصاویر لباس را دستهبندی کند. در یادگیری ماشین معمولاً با دو چیز سروکار داریم: Input (چیزی که مدل میبیند) و Label (جواب درست).
dataset.txt
# Input → Label
image_001.png → T-shirt
image_002.png → Shoe
image_003.png → Bag
image_004.png → Dress
PyTorch برای مدیریت چنین دادههایی از
Dataset و DataLoader استفاده میکند. Dataset نمونهها و برچسبهای آنها را نگه میدارد و DataLoader آنها را به شکل Batch در اختیار مدل قرار میدهد.
چرا همه دادهها را یکجا به مدل نمیدهیم؟
فرض کنید ۶۰٬۰۰۰ تصویر داریم. لازم نیست هر بار همه را وارد شبکه کنیم. دادهها به Batch تقسیم میشوند:
batches.txt
# Dataset: 60,000 samples, batch_size = 64
Batch 1 → 64 samples
Batch 2 → 64 samples
Batch 3 → 64 samples
...
Batch 938 → 32 samples # last batch (60000 % 64 = 32)
▶ 60000 / 64 ≈ 938 batches per epoch
این روش هم از نظر حافظه عملیتر است و هم بخش مهمی از روش آموزش مدلهای امروزی است: بهروزرسانی وزنها بر اساس تخمین گرادیان از هر Batch (Mini-Batch Gradient Descent) هم سریعتر از نمونهبهنمونه است و هم نویز مفیدی دارد که از گیرافتادن در نقاط بد کمک میکند.
# حالا مدل باید حدس بزند — و اشتباهش را اندازه بگیریم
فرض کنید تصویر یک کفش را به شبکه دادهایم. مدل در اولین مراحل آموزش احتمالاً چنین جوابی میدهد:
predict.py — early training
# Input: image of a shoe (label = Shoe)
# Model output after softmax (for intuition):
T-shirt 0.42
Shoe 0.15
Bag 0.33
Dress 0.10
✗ wrong prediction — but how wrong exactly?
مدل اشتباه کرده است. اما یک سؤال مهمتر وجود دارد: از کجا بفهمیم چقدر اشتباه کرده؟ اینجاست که مفهوم Loss وارد میشود؛ عددی که میگوید خروجی مدل چقدر با هدف فاصله دارد. برای مسائل Classification، یکی از توابع بسیار رایج
CrossEntropyLoss است:
loss.txt — over training
# Cross-Entropy Loss (lower = better)
▶ start of training: Loss = 2.41
▶ after a while: Loss = 0.83
▶ later: Loss = 0.21
▶ near the end: Loss = 0.07
نکته فنی — Logit خام: در PyTorch،
nn.CrossEntropyLoss در واقع ترکیب LogSoftmax و NLLLoss است. یعنی خروجی خام آخرین لایه (Logit) را مستقیماً به آن میدهیم و نیازی نیست خودمان قبلش Softmax اجرا کنیم. اگر Softmax را دستی اعمال کنیم، عملاً دوبار اعمال شده و آموزش خراب میشود.
# اما مدل از کجا بفهمد کدام Weight را تغییر دهد؟
اینجا یکی از مهمترین بخشهای یادگیری عمیق وارد میشود: Backpropagation. فرض کنید مدل از هزاران یا میلیونها وزن تشکیل شده و در انتها به
Loss = 1.82 رسیده است. حالا باید بفهمیم کدام وزنها باعث افزایش این خطا شدهاند و هر کدام چقدر باید تغییر کنند. برای این کار گرادیان را محاسبه میکنیم. به زبان ساده، گرادیان به ما میگوید: «اگر این پارامتر کمی تغییر کند، Loss چه تغییری خواهد کرد؟»
gradients.py — python3
# After loss.backward():
# each parameter carries its own gradient
∂Loss/∂W1 = 0.72 # increasing W1 raises loss → decrease it
∂Loss/∂W2 = -0.14 # increasing W2 lowers loss → increase it
∂Loss/∂W3 = 0.03 # almost no effect → tiny change
این اعداد جهت و اندازه تغییر موردنیاز را مشخص میکنند. PyTorch این محاسبات مشتقگیری را از طریق سیستم Automatic Differentiation (autograd) انجام میدهد: هر عملیات روی Tensorها در یک گراف محاسباتی ثبت میشود و
loss.backward() با پیمایش معکوس آن، گرادیان همه پارامترها را با قاعده زنجیرهای محاسبه میکند.
# Gradient Descent؛ حالا وزنها را تغییر بده
یکی از سادهترین شکلهای بهروزرسانی وزن این است:
update.py — python3
# W_new = W_old - learning_rate × gradient
W_old = 0.80
gradient = 0.20
learning_rate = 0.10
W_new = 0.80 - (0.10 × 0.20)
▶ W_new = 0.78
در یک شبکه واقعی، چنین محاسبهای برای تعداد بسیار زیادی پارامتر انجام میشود. اما یک سؤال حیاتی باقی میماند: چقدر؟ این دقیقاً جایی است که Learning Rate تعیینکننده میشود:
learning_rate.txt
اگر
lr = 0.000001 باشد، تغییرات بسیار کوچک خواهند بود و آموزش ممکن است بسیار کند شود. اما اگر lr = 1.0 باشد، ممکن است مدل بیش از حد بزرگ حرکت کند، از نقطه مناسب عبور کند و Loss نوسان کند یا اصلاً همگرا نشود. بنابراین آموزش فقط «داده دادن» نیست؛ انتخاب Hyperparameterها نیز بخش مهمی از فرایند است.
فراتر از SGD ساده: در عمل معمولاً از Optimizerهای هوشمندتری مثل
Adam استفاده میشود که برای هر پارامتر اندازه گام تطبیقی محاسبه میکند (با نگهداشتن میانگین متحرک گرادیانها و مربع آنها). با این حال، ایده اصلی همه آنها همان است: حرکت در خلاف جهت گرادیان.
# بیایید واقعاً یک هوش مصنوعی کوچک آموزش دهیم
اینجا دیگر فقط تئوری نیست. با Python و PyTorch یک شبکه عصبی واقعی میسازیم و آموزش میدهیم. از FashionMNIST استفاده میکنیم؛ مجموعهدادهای شامل ۶۰٬۰۰۰ تصویر آموزشی و ۱۰٬۰۰۰ تصویر تست ۲۸×۲۸ از اقلام پوشیدنی. اول نصب:
terminal — bash
pip install torch torchvision
مرحله اول: Device و داده
train.py — part 1
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
# 1) Device: GPU if available, else CPU
if torch.cuda.is_available():
device = torch.device("cuda")
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
# 2) Dataset + DataLoader
train_data = datasets.FashionMNIST(
root="data", train=True,
download=True, transform=ToTensor()
)
train_loader = DataLoader(
train_data, batch_size=64, shuffle=True
)
▶ Using: cuda (or 'cpu' on machines without GPU)
shuffle=True باعث میشود ترتیب نمونههای آموزش در هر epoch تصادفی شود — این کار از یادگیری ترتیب دادهها جلوگیری میکند و گرادیانهای هر Batch نمایندهتری از کل داده میسازند.
مرحله دوم: ساخت شبکه عصبی
train.py — part 2
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.network = nn.Sequential(
nn.Linear(28 * 28, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 10)
)
def forward(self, x):
x = self.flatten(x)
return self.network(x)
model = NeuralNetwork().to(device)
architecture.txt
nn.Linear(28*28, 128) یعنی ورودی ۷۸۴ ویژگی داریم و خروجی این لایه ۱۲۸ ویژگی است. شبکه در انتها این مقادیر را به ۱۰ خروجی تبدیل میکند؛ یکی برای هر کلاس. این خروجیها Logit هستند، نه Probability — و همانطور که گفتیم، CrossEntropyLoss مستقیماً همین Logitهای خام را میگیرد.
مرحله سوم: Loss و Optimizer و حلقه آموزش
train.py — part 3
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
epochs = 10
for epoch in range(epochs):
model.train()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
prediction = model(X) # Forward
loss = loss_fn(prediction, y) # Loss
optimizer.zero_grad() # Clear old grads
loss.backward() # Backprop
optimizer.step() # Update weights
سه خط شاید از همه مهمتر باشند:
the_core.txt
▼ optimizer.zero_grad() — گرادیانهای قبلی را پاک کن
▼ loss.backward() — گرادیان جدید را محاسبه کن
▼ optimizer.step() — وزنها را بهروزرسانی کن
✓ // this exact trio powers everything from this tiny MLP to billion-parameter LLMs
چرا zero_grad() لازم است؟ در PyTorch گرادیانها بهصورت پیشفرض انباشته (accumulate) میشوند؛ یعنی هر
backward() گرادیان جدید را روی قبلی میافزاید. اگر قبل از هر Batch گرادیانها را صفر نکنیم، گرادیانهای چند Batch مخلوط میشوند و بهروزرسانی وزنها غلط میشود. این یکی از رایجترین باگهای آموزش در PyTorch است.
مرحله چهارم: ارزیابی مدل
مدل را نباید فقط روی دادههایی که با آنها آموزش دیده آزمایش کنیم؛ چون ممکن است دادهها را بیش از حد حفظ کرده باشد. برای همین بخشی از داده (اینجا: ۱۰٬۰۰۰ تصویر تست) را کنار میگذاریم:
evaluate.py — python3
model.eval() # disable training-only behavior
with torch.no_grad(): # no gradient graph → faster, less memory
for X, y in test_loader:
prediction = model(X)
predicted_class = prediction.argmax(dim=1)
correct += (predicted_class == y).sum().item()
torch.no_grad() باعث میشود هنگام ارزیابی گراف گرادیان ساخته نشود و حافظه و محاسبات اضافه کاهش پیدا کند. model.eval() هم رفتارهای مخصوص آموزش (مثل Dropout) را خاموش میکند.
# هنگام اجرای برنامه چه اتفاقی میافتد؟
train.py — output
$ python train.py
▶ Epoch 1/10 | Loss: 0.52 | Accuracy: 0.812
▶ Epoch 2/10 | Loss: 0.39 | Accuracy: 0.848
▶ Epoch 5/10 | Loss: 0.29 | Accuracy: 0.876
▶ Epoch 10/10 | Loss: 0.23 | Accuracy: 0.889
عددهای دقیق به شرایط اجرا، نسخه کتابخانه، سختافزار و تنظیمات بستگی دارند؛ چیزی که مهم است روند است. این نمودار شکل کلی آموزش همین شبکه را نشان میدهد — Loss بهصورت نزولی آرام میشود و Accuracy بهصورت صعودی به سقف میرسد:
training_curves.txt
مقادیر نمودار صرفاً آموزشیاند: منحنیها شکل واقعبینانهی آموزش یک MLP روی FashionMNIST را نشان میدهند (دقت تست ~۸۸–۸۹٪ برای این معماری ساده طبیعی است)، اما اعداد دقیق اجرای شما بسته به سختافزار و seed متفاوت خواهد بود.
Epoch یعنی چه؟
وقتی مدل تمام نمونههای آموزشی را یک بار مشاهده کند، یک Epoch کامل شده است. با ۶۰٬۰۰۰ نمونه و Batch های ۶۴تایی، هر Epoch حدود ۹۳۸ مرحله Batch دارد. اگر
epochs = 10 باشد، مدل تقریباً ۱۰ بار کل داده آموزشی را میبیند و در طول آن، حدود ۹٬۳۸۰ بار پارامترهایش بهروزرسانی میشود.
# Overfitting؛ وقتی مدل بیش از حد درس میخواند
فرض کنید مدل روی داده آموزشی بسیار عالی شده است اما روی دادهای که قبلاً ندیده ضعیف عمل میکند:
overfitting_check.txt
# A classic overfitting signature:
Training Accuracy 99.8%
Test Accuracy 72.0%
✗ gap = 27.8% → the model memorized, it didn't generalize
احتمالاً مدل به جای یادگیری الگوهای قابل تعمیم، بیش از حد به ویژگیهای مجموعه آموزشی وابسته شده است. به این وضعیت Overfitting میگوییم. برای مقابله با آن، بسته به مسئله میتوان از داده بیشتر، Data Augmentation، Regularization، Dropout، Early Stopping یا تنظیم معماری استفاده کرد.
به همین دلیل در پروژههای جدی، دادهها را به چند بخش تقسیم میکنند: Training برای یادگیری پارامترها، Validation برای تصمیمگیری درباره تنظیمات و انتخاب مدل، و Test برای ارزیابی نهایی روی دادهای مستقل. اگر فقط Training Loss را نگاه کنیم، ممکن است فکر کنیم مدل دارد بهتر میشود، در حالی که بعد از نقطهای Validation Loss شروع به بالا رفتن میکند — یعنی تعمیم مدل در حال خراب شدن است.
# قسمت جذابتر: آموزش یک مدل زبانی
شبکهای که ساختیم میتوانست بگوید «این تصویر احتمالاً کفش است». اما مدلهای زبانی مثل LLMها مسئله بسیار بزرگتری دارند. هدف اصلی یک مدل زبانی Autoregressive را میتوان به شکل زیر نوشت:
objective.py
# The objective of an autoregressive language model:
P(xₜ | x₁, x₂, ..., xₜ₋₁)
# "Given all previous tokens, how likely is each possible next token?"
یعنی: «با توجه به توکنهای قبلی، توکن بعدی چه احتمالی دارد؟» — همان ایدهای که در مقالهی توهم مدلهای زبانی به آن پرداختیم: مدل به جای داشتن یک «ماژول حقیقتسنج» در هسته تولید، بر اساس الگوهای آموختهشده توکن بعدی را پیشبینی میکند.
اول متن باید Token شود
tokenizer.py — python3
# "The cat is sleeping."
tokens = tokenizer.encode("The cat is sleeping.")
print(tokens)
▶ [464, 2415, 318, 7421, 13]
// a token is not necessarily a whole word —
// "unbelievable" may split into smaller pieces
مدل دیگر مستقیماً با حروف کار نمیکند؛ با Tensorها و نمایشهای عددی کار میکند. در مرحله آموزش، جواب واقعی (توکن بعدی) را هم داریم، پس میتوانیم خروجی مدل را با آن مقایسه کنیم، Loss حساب شود، گرادیان محاسبه شود و وزنها تغییر کنند — دقیقاً همان چرخهای که برای تشخیص کفش دیدیم.
چرا Transformer؟
برای مدلهای زبانی مدرن، معماری Transformer نقش بسیار مهمی دارد. مقالهی معروف Attention Is All You Need در سال ۲۰۱۷ نشان داد که میتوان از سازوکار Attention برای پردازش توالیها بدون تکیه بر ساختارهای بازگشتی سنتی استفاده کرد. ایده بسیار ساده اما قدرتمند است: «هنگام پردازش یک Token، به کدام بخشهای دیگر ورودی باید بیشتر توجه کنم؟»
attention.txt
"Ali put the book on the table because it was heavy."
# What does "it" refer to?
book 0.91
table 0.06
Ali 0.02
// attention lets the model link "it" to "book" across the sequence
اما آموزش یک LLM بسیار بزرگتر از مثال FashionMNIST است. تعداد پارامترها و مقدار داده آنقدر زیاد میشود که آموزش دیگر روی یک کامپیوتر معمولی عملی نیست و از Multiple GPUها، ماشینهای متعدد و Distributed Training استفاده میشود.
چهار مفهومی که نباید اشتباه بگیریم
pipeline.txt
در کار معروف InstructGPT، پژوهشگران از نمونههای نوشتهشده توسط انسان برای Supervised Fine-Tuning استفاده کردند و سپس خروجیهای مدل را با ترجیحات انسانی مقایسه کردند تا رفتارش بهتر با خواسته انسانها هماهنگ شود. البته این تنها روش ممکن نیست و روشهای دیگری مانند Preference Optimization نیز وجود دارند.
# مهمترین بخشی که کمتر دربارهاش صحبت میشود: داده
فرض کنید بهترین معماری جهان را داریم، اما دادهها تکراری، سوگیرانه، نادرست یا پرنویز باشند. مدل چه چیزی یاد میگیرد؟ به احتمال زیاد بخشی از همین مشکلات را. این همان دلیل معروف Garbage In, Garbage Out است: در سیستمهای یادگیری ماشین، کیفیت Pipeline داده میتواند به اندازه معماری مدل مهم باشد.
data_pipeline.txt
آیا مدل فقط «حفظ» میکند؟
نه به این سادگی. یک آزمایش ساده: اگر داده آموزشی فقط این سه نمونه باشد:
generalization_test.txt
# Training data:
2 + 2 = 4
2 + 3 = 5
2 + 4 = 6
# A model that only memorized:
17 + 23 = ??? # never seen → fails
# A model that learned the structure:
17 + 23 = 40 # generalizes to new inputs
گاهی مدل ممکن است بخشی از دادههای آموزشی را حفظ کند، اما یادگیری ماشین را نمیتوان صرفاً به حفظ کردن تقلیل داد. مدل هنگام آموزش پارامترهایی پیدا میکند که الگوهای موجود در داده را مدل کنند — و همین تفاوت بین Memorization و Generalization یکی از موضوعات بسیار مهم این حوزه است.
# پس دقیقاً چه چیزی یاد گرفته شد؟
پاسخ کوتاه است: Weightهای مدل تغییر کردند. از وزنهای تصادفی اولیه (W₀) شروع کردیم، و در هر مرحله آموزش، وزنها کمی در خلاف جهت گرادیان حرکت کردند — W₁، W₂، W₃ — تا در نهایت به W_final رسیدیم؛ مجموعهای از پارامترها که Loss را بهتر میکنند.
یک شبکه کوچک ممکن است چند صد هزار پارامتر داشته باشد؛ یک مدل بزرگ میتواند میلیاردها پارامتر داشته باشد. اما اصل داستان همچنان همان است:
universal_loop.txt
در یک طرف، فقط چند خط کد داریم:
prediction = model(X)، loss = loss_fn(prediction, y) و آن سه خط طلایی zero_grad / backward / step. در طرف دیگر، یکی از پیچیدهترین سامانههای محاسباتی ساختهشده توسط انسان قرار دارد. این تضاد شاید جذابترین بخش ماجرا باشد: پیچیدگی نتیجه، لزوماً از پیچیدگی ایده اولیه نمیآید؛ گاهی از مقیاسی میآید که آن ایده را در آن اجرا میکنیم.
takeaway.txt
هوش مصنوعی با اعداد شروع میشود، نه با هوش.
حدس میزند، خطا میکند، وزنهایش اصلاح میشود —
و این چرخه میلیاردها بار تکرار میشود.
«یادگیری» یعنی همین:
هر بار کمی بهتر از دفعه قبل.
حدس میزند، خطا میکند، وزنهایش اصلاح میشود —
و این چرخه میلیاردها بار تکرار میشود.
«یادگیری» یعنی همین:
هر بار کمی بهتر از دفعه قبل.