ژرفا — خودآموز یادگیری عمیق و مهندسی هوش مصنوعی (متوسط)

فصل ۱ از ۹

پیشرفت ترم
۰٪

ترم ۱ · مکانیزمِ یادگیری: PyTorch از پایه

`tensor`: چرا یک آرایهٔ دیگر لازم شد

فصل ۱پیش‌نمایش رایگان
۱۰ دقیقه مطالعه فصل ۱

در این فصل چه یاد می‌گیری#

numpy را بلدی و کارش را خوب انجام می‌دهد. پس چرا PyTorch یک نوعِ دادهٔ دیگر ساخت؟ جوابِ کوتاه: دو چیز که numpy ندارد و یادگیری عمیق بدونشان ممکن نیست — اجرا روی GPU، و ردیابیِ خودکارِ مشتق. در این فصل با tensor کار می‌کنی، تله‌های dtype و shape را می‌بینی، و پایه‌ای می‌سازی که کلِ دوره رویش بنا می‌شود.

دو جعبهٔ ابزارِ هم‌شکل که یکی‌شان دو ابزارِ اضافه دارد

آخر این فصل می‌توانی:

  • tensor بسازی و shape و dtype و device را بخوانی
  • بینِ numpy و torch رفت‌وبرگشت کنی و بدانی کِی حافظه مشترک است
  • شکلِ tensor را تغییر بدهی و خطای شکل را بخوانی
  • بگویی tensor دقیقاً چه چیزی به ndarray اضافه کرده

قبل از شروع#

از سرنخ ترمِ ۲: numpy، shape، عملیاتِ برداری، broadcasting.

سلولِ راه‌اندازی: هستهٔ تصادف را قفل می‌کند و laptops_clean.csv سرنخ را می‌سازد — در فصلِ ۹ لازمش داریم.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

💡 نکته: کلِ این فصل روی CPU در چند ثانیه اجرا می‌شود. GPU لازم نداری — و این قاعدهٔ کلِ ترمِ ۱ است.

۱. tensor یعنی آرایه، با دو چیزِ اضافه#

numpy.ndarray torch.Tensor
آرایهٔ چندبعدی
عملیاتِ برداری و broadcasting
اجرا روی GPU
ردیابیِ خودکارِ مشتق

همان دو ردیفِ آخر تمامِ دلیلِ وجودِ PyTorch است. بقیه‌اش تقریباً همان numpy است که می‌شناسی — و همین باعث می‌شود انتقالت آسان باشد.

import torch

scalar = torch.tensor(3.5)
vector = torch.tensor([1.0, 2.0, 3.0])
matrix = torch.tensor([[1.0, 2.0], [3.0, 4.0]])

for name, t in [("عدد", scalar), ("بردار", vector), ("ماتریس", matrix)]:
    print(f"{name}: shape={tuple(t.shape)} | ndim={t.ndim} | dtype={t.dtype}")
عدد: shape=() | ndim=0 | dtype=torch.float32
بردار: shape=(3,) | ndim=1 | dtype=torch.float32
ماتریس: shape=(2, 2) | ndim=2 | dtype=torch.float32

به shape=() نگاه کن: یک عددِ تنها، shape خالی دارد، نه (1,). این تفاوت بعداً واقعاً به دردسر می‌خورد — به‌ویژه وقتی loss که یک عددِ تنهاست را با یک بردارِ تک‌عضوی اشتباه بگیری.

🌱 ریشه‌اش کجاست: بردار و ماتریس اینجا فقط ظرفِ عدد نیستند؛ در ادامهٔ دوره تبدیلاند. اگر می‌خواهی بدانی چرا ضربِ ماتریسی این‌طور تعریف شده و چه ربطی به ترکیبِ دو تبدیل دارد، ریشه، ترم ۷ فصل ۱ — بردار از پایه شروع می‌کند و فصل ۵ همان ترم دقیقاً همین سؤال را جواب می‌دهد.

۲. dtype: اولین جایی که گیر می‌کنی#

from_int = torch.tensor([1, 2, 3])
from_float = torch.tensor([1.0, 2.0, 3.0])

print("از عدد صحیح:", from_int.dtype)
print("از اعشاری  :", from_float.dtype)
print("جمعشان     :", (from_int + from_float).dtype)
از عدد صحیح: torch.int64
از اعشاری  : torch.float32
جمعشان     : torch.float32

آن نقطهٔ کوچک بعد از 1 نوعِ کلِ tensor را عوض می‌کند. و این مهم‌ترین تفاوتِ عملیِ torch با numpy در روزِ اولت است: numpy پیش‌فرضش float64 است و torch پیش‌فرضش float32.

دلیلش هم صرفه‌جویی نیست، سرعت است: GPUها با float32 چند برابر سریع‌ترند و برای یادگیری عمیق آن دقت کاملاً کافی است.

⚠️ مواظب باش: torch.int64 در جای اشتباه یکی از رایج‌ترین خطاهای روزهای اول است. شبکه‌ها روی اعشار کار می‌کنند و دادنِ یک tensorِ صحیح به یک لایه خطای صریح می‌دهد. قاعدهٔ ساده: دادهٔ ورودیِ شبکه همیشه float32، برچسبِ طبقه‌بندی همیشه int64. این دو را حفظ کن؛ نودرصدِ خطاهای dtype همین‌جاست.

۳. numpy و torch: رفت‌وبرگشت#

import numpy as np

a = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(a)
print("numpy -> torch:", t, t.dtype)

a[0] = 99.0
print("بعد از تغییر numpy:", t)

back = t.numpy()
print("torch -> numpy:", back, type(back).__name__)
numpy -> torch: tensor([1., 2., 3.], dtype=torch.float64) torch.float64
بعد از تغییر numpy: tensor([99.,  2.,  3.], dtype=torch.float64)
torch -> numpy: [99.  2.  3.] ndarray

دو چیزِ مهم در همین شش خط:

  • dtype منتقل شد. numpy آرایه‌اش float64 بود، پس tensor هم float64 شد — نه float32 پیش‌فرض. اگر بعداً به یک لایه بدهی‌اش، خطا می‌گیری.
  • حافظه مشترک است. یک عدد را در numpy عوض کردیم و tensor هم عوض شد. torch.from_numpy کپی نمی‌کند.

🔧 اگر کار نکرد: اگر می‌خواهی نسخهٔ مستقل داشته باشی، torch.tensor(a) بنویس نه torch.from_numpy(a) — اولی کپی می‌کند. و برای تبدیلِ نوع، .float() یا .to(torch.float32) را بگذار: torch.from_numpy(a).float(). اشتراکِ حافظه سریع است، ولی اگر ندانی‌اش، باگی می‌سازد که ساعت‌ها دنبالش می‌گردی.

۴. shape: جایی که بیشترِ خطاها می‌آیند#

x = torch.arange(12.0)

print("شکل اولیه:", tuple(x.shape))
print("reshape(3,4):", tuple(x.reshape(3, 4).shape))
print("reshape(3,-1):", tuple(x.reshape(3, -1).shape))
print("unsqueeze(0):", tuple(x.unsqueeze(0).shape))

try:
    x.reshape(5, 3)
except RuntimeError as error:
    print("RuntimeError:", str(error)[:60])
شکل اولیه: (12,)
reshape(3,4): (3, 4)
reshape(3,-1): (3, 4)
unsqueeze(0): (1, 12)
RuntimeError: shape '[5, 3]' is invalid for input of size 12

سه ابزار که در کلِ دوره به‌کارت می‌آیند:

  • reshape شکل را عوض می‌کند، به شرطی که تعدادِ کلِ خانه‌ها ثابت بماند. می‌شود، نمی‌شود.
  • -1 یعنی «خودت حساب کن». reshape(3, -1) می‌گوید سه سطر بده و ستون‌ها را خودت دربیاور.
  • unsqueeze(0) یک بُعدِ به‌طولِ ۱ اضافه می‌کند. این را حفظ کن: مدل‌ها انتظارِ batch دارند، و برای دادنِ یک نمونهٔ تنها باید بُعدِ batch را دستی اضافه کنی.

چک کن: آن پیامِ خطا را حفظ کن — shape '[5, 3]' is invalid for input of size 12. در کلِ این دوره بارها می‌بینی‌اش، و همیشه یک معنا دارد: عددی که خواستی با تعدادِ واقعیِ خانه‌ها نمی‌خواند. اولین کاری که باید بکنی چاپ کردنِ shape قبل و بعدِ هر تبدیل است، نه حدس زدن.

۵. دو ضرب که با هم فرق دارند#

A = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
B = torch.tensor([[10.0, 20.0], [30.0, 40.0]])

print("A * B (خانه‌به‌خانه):\n", A * B)
print("A @ B (ضرب ماتریسی):\n", A @ B)
A * B (خانه‌به‌خانه):
 tensor([[ 10.,  40.],
        [ 90., 160.]])
A @ B (ضرب ماتریسی):
 tensor([[ 70., 100.],
        [150., 220.]])

* هر خانه را در خانهٔ متناظرش ضرب می‌کند. @ ضربِ ماتریسیِ واقعی است: سطر در ستون، و جمع.

تقریباً هر لایهٔ یک شبکهٔ عصبی یک @ است. وقتی در فصلِ ۴ به nn.Linear برسیم، خواهی دید که کارِ اصلی‌اش دقیقاً همین یک عملیات است.

۶. broadcasting، همان‌طور که در numpy بود#

rows = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
bias = torch.tensor([10.0, 20.0, 30.0])

print("broadcast:\n", rows + bias)
print("shape:", tuple((rows + bias).shape))
broadcast:
 tensor([[11., 22., 33.],
        [14., 25., 36.]])
shape: (2, 3)

بردارِ سه‌تایی روی هر دو سطر پخش شد. این دقیقاً همان قاعدهٔ سرنخ ترمِ ۲ است و در torch بی‌تغییر کار می‌کند.

و کاربردش اینجا خیلی مشخص است: یک لایهٔ شبکه به همهٔ نمونه‌های batch یک bias مشترک اضافه می‌کند — و همین broadcasting است که آن را در یک خط ممکن می‌کند.

۷. device: جایی که tensor زندگی می‌کند#

device = "cuda" if torch.cuda.is_available() else "cpu"
print("دستگاه:", device)

big = torch.randn(3, 3)
print("روی:", big.device)
دستگاه: cpu
روی: cpu

خروجیِ خطِ اول به ماشینِ تو بستگی دارد — روی Colab بدونِ GPU، cpu است و با GPU، cuda.

آن خطِ اول را حفظ کن؛ الگوی استانداردِ کلِ دوره است. به‌جای اینکه در کد بنویسی cuda (که روی ماشینِ بدونِ GPU می‌شکند)، می‌پرسی و بعد تصمیم می‌گیری. و tensorها پیش‌فرض روی CPU ساخته می‌شوند؛ برای بردنشان روی GPU باید صریح بگویی big.to(device).

⚠️ مواظب باش: دو tensor که روی دو دستگاهِ متفاوت‌اند با هم کار نمی‌کنند و خطای Expected all tensors to be on the same device می‌دهند. در ترمِ ۲ می‌بینی که این رایج‌ترین خطای اولین آموزشِ GPUدار است، و همیشه یک علت دارد: مدل را منتقل کرده‌ای و داده را نه، یا برعکس.

۸. تکرارپذیری، از همین اولین فصل#

torch.manual_seed(0)
first = torch.randn(2, 2)

torch.manual_seed(0)
second = torch.randn(2, 2)

print("دو بار با همان seed یکی است:", bool(torch.equal(first, second)))
print(first)
دو بار با همان seed یکی است: True
tensor([[ 1.5410, -0.2934],
        [-2.1788,  0.5684]])

سلولِ راه‌اندازیِ این ترم همین کار را برایت کرده، و برای همین است که عددهای بالا با عددهای تو یکی درآمد.

📏 اندازه بگیر: شبکه‌های عصبی از وزن‌های تصادفی شروع می‌کنند، پس دو اجرای بدونِ seed نتیجهٔ متفاوت می‌دهند — و برخلافِ سرنخ که تفاوتش کوچک بود، اینجا می‌تواند تفاوتِ «آموزش دید» و «اصلاً همگرا نشد» باشد. از همین فصل، هیچ‌وقت بدونِ seed آزمایش نکن. ترمِ ۲ فصلِ ۶ کلِ ماجرا را باز می‌کند، از جمله جایی که حتی seed هم کافی نیست.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
tensor تنسور آرایهٔ چندبعدیِ PyTorch، با GPU و مشتقِ خودکار
dtype دی‌تایپ نوعِ عددیِ خانه‌ها (float32، int64، …)
shape شیپ اندازهٔ هر بُعد
device دیوایس جایی که tensor در آن ذخیره و پردازش می‌شود
reshape ری‌شیپ تغییرِ شکل بدونِ تغییرِ محتوا
broadcasting برادکستینگ پخشِ خودکارِ یک tensorِ کوچک روی یک بزرگ‌تر

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

tensor را داری. حالا آن قابلیتِ دومی که در جدولِ بخشِ ۱ بود: autograd. یک خط کد می‌نویسی و PyTorch مشتقِ کلِ محاسبه‌ات را — هر چقدر هم پیچیده — خودش حساب می‌کند. و می‌بینی که هیچ جادویی در کار نیست، فقط قاعدهٔ زنجیره‌ایِ ریشه ترمِ ۸.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.