40 گام به سوی آینده‌ای هوشمند - مجموعه وبینارهای رایگان در حوزه هوش مصنوعی
Filter by دسته‌ها
chatGTP
آموزش هوش مصنوعی و انواع آن
آموزش‌های پایه‌ای هوش مصنوعی
اصول هوش مصنوعی
پایتون و ابزارهای یادگیری عمیق
کتابخانه‌ های یادگیری عمیق
یادگیری با نظارت
یادگیری بدون نظارت
یادگیری تقویتی
یادگیری عمیق
یادگیری نیمه نظارتی
آموزش‌های پیشرفته هوش مصنوعی
بینایی ماشین
پردازش زبان طبیعی
پردازش گفتار
چالش‌های عملیاتی
داده کاوی و بیگ دیتا
رایانش ابری و HPC
سیستم‌‌های امبدد
علوم شناختی
دیتاست
اخبار
تیتر یک
رسانه‌ها
آموزش پردازش زبان طبیعی
آموزش علوم داده
اینفوگرافیک
پادکست
ویدیو
رویدادها
کاربردهای هوش مصنوعی
کسب‌و‌کار
تحلیل بازارهای هوش مصنوعی
کارآفرینی
هوش مصنوعی در ایران
هوش مصنوعی در جهان
 آموزش گام به گام PyTorch

آموزش گام به گام PyTorch

در حال حاضر PyTorch بیشترین نرخ رشد را در میان چارچوب‌های کاری یادگیری عمیق دارد و در دوره‌های آموزشی Fast.ai، با عنوان «یادگیری عمیق برای برنامه‌نویس‌ها»، و کتابخانه آن مورد استفاده قرار می‌گیرد.

PyTorch به شدت پایتونی است، به عبارت دیگر برای برنامه نویسان Python، کار کردن با PyTorch بسیار راحت است.

به علاوه، به قول آندریج کارپاتی استفاده ازPyTorch حتی ممکن است به معنای برای سلامتی شما مفید باشد!

انگیزه نگارش مقاله آموزش PyTorch

مقاله‌ها و کتاب‌های آموزشی درباره PyTorch همه جا به وفور یافت می‌شود و راهنمای آن نسبتاً کامل و جامع است. هر گونه اطلاعاتی که در مورد تمام قابلیت‌های PyTorch بخواهید به راحتی در دسترس شماست. با این حال هنوز جای رویکردی ساختاری، فزاینده و بر گرفته از اصول اولیه PyTorch در میان این مطالب خالی است.

در این نوشتار توضیح می‌دهیم چرا ایجاد یک مدل یادگیری عمیق در Python بسیار ساده، آسان و واضح است. و انجام کارهایی از قبیل autograd، گراف محاسباتی پویا، کلاس‌های مدل و غیره به راحتی صورت می‌گیرد. همچنین به شما نشان خواهم داد که چگونه از بروز خطاها و مشکلات متداول پیش‌گیری کنید.

مسئله رگرسیون ساده

بیشتر مقاله‌های آموزشی، PyTorch را با مسئله ساده و جذاب «طبقه‌بندی تصویر» شرح می‌دهند. ظاهراً روش بسیار خوبی است امّا شما را از هدف اصلی یعنی درک سازوکار PyTorch دور می‌کند.

به همین دلیل در این مقاله آموزشی به یک موضوع ساده و متعارف می‌پردازیم: رگرسیون خطی تک متغیره (x). ساده‌ترین شکل  رگرسیون به صورت زیر است:

رگرسیون ساده در pytorch
مدل رگرسیون خطی ساده

تولید داده

آموزش را با تولید مقداری داده جعلی آغاز می‌کنیم: ابتدا یک بردار ۱۰۰ نقطه‌ای برای متغیر x داریم و سپس برچسب‌گذاری را به صورت a=1، b=2 انجام داده و پس از آن مقداری نویز گوسی در داده‌ها ایجاد می‌کنیم.

سپس دیتاست جعلی را به دو بخش دیتاست آموزشی و دیتاست اعتبارسنجی تقسیم می‌کنیم. برای این کار ابتدا آرایه اندیس‌ها را به هم می‌ریزیم و سپس ۸۰ نقطه بهم ریخته اول را به عنوان داده‌های آموزشی انتخاب می‌کنیم.

# Data Generation
np.random.seed(42)
x = np.random.rand(100, 1)
y = 1 + 2 * x + .1 * np.random.randn(100, 1)

# Shuffles the indices
idx = np.arange(100)
np.random.shuffle(idx)

# Uses first 80 random indices for train
train_idx = idx[:80]
# Uses the remaining indices for validation
val_idx = idx[80:]

# Generates train and validation sets
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]
تولید داده در pytorch
داده جعلی ، دیتاست‌های آموزشی و اعتبارسنجی

 

می‌دانیم که a = 1 و b = 2 است امّا اکنون باید بررسی کنیم که با استفاده از گرادیان کاهشی و ۸۰ نقطه داده‌ی آموزشی تا چه حد می‌توانیم به مقادیر واقعی نزدیک شویم.

گرادیان کاهشی

در صورتی که با عملکرد گرادیان کاهشی آشنا هستید، نیازی نیست این بخش را بخوانید. از طرفی توضیح مفصل و کامل عملکرد گرادیان کاهشی از حوصله این نوشتار خارج است. با این حال چهار گام اصلی محاسبه گرادیان کاهشی را توضیح می‌دهیم.

[irp posts=”17360″]

گام اول: محاسبه زیان

در مسائل رگرسیون ساده، زیان از طریق میانگین مربعات خطا (MSE) محاسبه می‌شود. به عبارت دیگر y (برچسب) را از a + bx (مقادیر پیش‌بینی‌شده) کم می‌کنیم و به توان ۲ می‌رسانیم؛ سپس از این مقادیر میانگین می‌گیریم.

اگر از تمام نقطه‌ داده‌های دیتاست آموزشی (یعنی N) برای محاسبه گرادیان کاهشی استفاده کنیم، در واقع «گرادیان کاهشی بسته‌ای» را اجرا نموده‌ایم. و در صورتی که هر مرتبه تنها یک نقطه را به کار بگیریم، «گرادیان کاهشی تصادفی» را اجرا نموده‌ایم. هر متد دیگری که اتخاذ کنیم، تعداد نقطه‌داده‌ها (n) در آن در بازه ۱ و N خواهد بود و «گرادیان کاهشی زیربسته‌ها» نام دارد.

محاسبه زیان در pytorch
زیان: میانگین مربعات خطا (MSE)

گام دوم: محاسبه گرادیان

گرادیان در واقع همان «مشتق جزئی» است. به این دلیل آن‌ را «جزئی» می‌نامیم که تنها با یک پارامتر (w.r.t) می‌توان آن را محاسبه کرد. از طرفی، رگرسیون خطی دو پارامتر دارد، a و b، لذا باید دو مشتق جزئی را محاسبه کنیم.

مشتق به شما می‌گوید که یک کمیّت مشخص با تغییر دیگر کمیّت‌ها چقدر تغییر می‌کند. در این مثال می‌خواهیم بدانیم در صورتی که هر یک از دو پارامتر a و b تغییر کند، زیان MSE چه تغییری خواهد کرد.

سمت راست معادله در تصویر زیر همان میانگین مربعات خطاست که در پیاده‌سازی گرادیان کاهشی برای رگرسیون خطی ساده استفاده می‌شود. برای اینکه بهتر متوجه شوید عبارت‌های پایانی از کجا آمده‌اند، در گام میانی، تمام عناصر بدست آمده از قانون مشتق زنجیره‌ای را نشان داده‌ایم.

گرادیان در pytorch
محاسبه گرادیان w.r.t برای ضرایب a و b

 

گام سوم: به‌روزرسانی پارامترها

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

هنوز باید یک پارامتر دیگر را نیز مد نظر قرار دهیم: نرخ یادگیری، که با حرف یونانی eta (η) نشان داده‌ می‌شود. نرخ یادگیری یک عامل ضرب‌پذیر است و در هنگام به‌روزرسانی پارامترها باید بر روی گرادیان اعمال شود.

به روز رسانی پارامترها
به‌روزرسانی ضرایب a و b با استفاده از گرادیان و نرخ یادگیری محاسبه شده

گام چهارم: تکرار مراحل!

اکنون با استفاده از پارامتر‌های به‌روزرسانی شده به گام اول باز می‌گردیم و پردازش را از ابتدا شروع می‌کنیم.

یک دوره، زمانی کامل می‌شود که تمام نقطه‌ها برای محاسبه زیان مورد استفاده قرار گیرند. در نظرگرفتنِ تمام نقاط در محاسبه گرادیان کاهشی بسته‌ای امری ساده و جزئی است زیرا این روش برای محاسبه زیان از تمام نقاط استفاده می‌کند، به عبارت دیگر یک دوره برابر است با یک به‌روزرسانی. در گرادیان کاهشی تصادفی منظور از یک دوره N تا به‌روزرسانی است، اما در گرادیان کاهشی زیربسته‌ها (با سایز n)، یک دوره برابر N/n به‌روزرسانی خواهد بود.

به طور خلاصه به تکرار این فرآیند به دفعات و در دوره‌های بیشمار آموزش مدل می‌گویند.

رگرسیون خطی در Numpy

اگر چه این مقاله آموزشی در مورد PyTorch است، اما بهتر است ابتدا به نحوه پیاده‌سازی مدل رگرسیون خطی در  Numpy بپردازیم. ذکر این نکته دو هدف عمده دارد: اول اینکه ساختارِ مسئله مورد بررسی را معرفی می‌کند (که در تقریباً همه موارد یکسان است) و دوم اینکه نشان می‌دهد نقاط آسیب کدام است و با یادگرفتن آن‌ها متوجه خواهید شد PyTorch چقدر کار شما را آسان‌تر می‌کند.

در آموزش مدل، مقداردهی اولیه دو مرحله دارد:

  • مقداردهی تصادفی پارامترها یا وزن‌ها (در این مثال تنها دو پارامتر a و b را داریم)؛ خط ۳ و ۴؛
  • مقداردهی اَبَرپارامترها (که در این مقاله تنها شامل نرخ یادگیری و تعداد دوره‌ها می‌شود)؛ خط ۹ و ۱۱؛

همیشه تابع مقداردهی اولیه را با random seed اجرا کنید تا در صورتی که دوباره اجرا شود به نتایج یکسانی برسد. طبق معمول random seed  را ۴۲، یعنی کمترین مقدار ممکن، قرار می‌دهیم.

هر دوره شامل چهار مرحله آموزشی است:

  • محاسبه پیش‌بینی‌های مدل: این قسمت forward pass نام دارد؛ خط ۱۵؛
  • محاسبه زیان: با استفاده از پیش‎بینی‌ها، برچسب‌ها و تابع زیانی که برای مسئله مورد نظر مناسب باشد زیان را محاسبه می‌کنیم؛ خط ۱۸ و ۲۰؛
  • محاسبه گرادیان‌ها برای تمام پارامترها: خط ۲۳ و ۲۴؛
  • به‌روزرسانی پارامترها: خط ۲۷ و ۲۸؛

به خاطر داشته باشید که اگر از گرادیان کاهشی بسته‌‌ای استفاده نمی‌کنید (در این مثال ما از این گرادیان استفاده می‌کنیم)، لازم است یک حقله درونی بنویسید تا این چهار گام آموزشی را یا برای تک تک نقطه‌ها (تصادفی) یا برای n تا نقطه (در گرادیان کاهشی زیربسته‌ها) اجرا کند. در ادامه نمونه کد مدنظر برای گرادیان کاهشی زیربسته‌ها را خواهید دید.

# Initializes parameters "a" and "b" randomly
np.random.seed(42)
a = np.random.randn(1)
b = np.random.randn(1)

print(a, b)

# Sets learning rate
lr = 1e-1
# Defines number of epochs
n_epochs = 1000

for epoch in range(n_epochs):
    # Computes our model's predicted output
    yhat = a + b * x_train
    
    # How wrong is our model? That's the error! 
    error = (y_train - yhat)
    # It is a regression, so it computes mean squared error (MSE)
    loss = (error ** 2).mean()
    
    # Computes gradients for both "a" and "b" parameters
    a_grad = -2 * error.mean()
    b_grad = -2 * (x_train * error).mean()
    
    # Updates parameters using gradients and the learning rate
    a = a - lr * a_grad
    b = b - lr * b_grad
    
print(a, b)

# Sanity Check: do we get the same results as our gradient descent?
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print(linr.intercept_, linr.coef_[0])

برای اطمینان از  اینکه در کدنویسی هیچ خطایی مرتکب نشده‌ایم، می‌توانیم برای برازش مدل از رگرسیون خطی Scikit-Learn استفاده کنیم و سپس و ضرایب همبستگی را مقایسه کنیم.

# a and b after initialization
[۰.۴۹۶۷۱۴۱۵] [-۰.۱۳۸۲۶۴۳]
# a and b after our gradient descent
[۱.۰۲۳۵۴۰۹۴] [۱.۹۶۸۹۶۴۱۱]
# intercept and coef from Scikit-Learn
[۱.۰۲۳۵۴۰۷۵] [۱.۹۶۸۹۶۴۴۷]

این مقادیر تا ۶ رقم اعشار یکسان هستند، بدین معنا که پیاده‌سازی مدل رگرسیون خطی در Numpy کاملاً موفقیت آمیز بوده است.

اکنون زمان آن فرا رسیده است به برازش مدل در PyTorch بپردازیم.

PyTorch

بهتر است قبل از شروع مدل‌سازی با برخی از مفاهیم اصلی آشنا باشید زیرا در غیر اینصورت ممکن است با مشکل مواجه شوید.

اولین مفهوی که باید با آن آشنا باشید «تنسورها» هستند. در یادگیری عمیق تنسورها همه جا به چشم می‌خورند. به همین دلیل است که چهارچوب کاری گوگل «تنسورفلو» نام دارد. در ادامه توضیح می‌دهیم تنسور چیست.

Tensor

ممکن است در Numpy آرایه‌ای با سه بعد داشته باشید؛ در اصطلاح تخصصی به این آرایه سه بعدی تنسور می‌گویند.

اسکالر (یا همان عدد) هیچ بُعدی ندارد؛ بردار یک بُعد دارد؛ ماتریس دو بُعد، و تنسور سه یا چند بُعد دارد.

امّا گاهی برای ساده نمودن مسائل، بردارها و ماتریس‌ها را نیز تنسور می‌نامیم. به عبارتی از این پس همه چیز یا اسکالر هستند یا تنسور.

برازش مدل در پای تورچ
تنسور ها در واقع ماتریس‌های چند بُعدی هستند.

 

بارگیری داده‌ها، دستگاه‌ها و CUDA

حال این سوال مطرح می‌شود که «چگونه از آرایه‌های Numpy به تنسور PyTorch برسیم؟» برای این کار می‌توانیم از تابع from_numpy استفاده کنیم؛ هر چند خروجی آن یک تنسور CPU است.

احتمالاً با خود می‌گوید «ولی من می‌خواستم از GPU مورد نظر خود استفاده کنم». نگران نباشید، تابع to()  اینجا به کمک شما می‌آید. این تابع تنسور شما را به هر دستگاهی که انتخاب کنید، از جمله GPU مورد نظر خودتان، ارسال می‌کند ( به این عملیات cuda یا cuda:0 می‌گویند).

باز هم ممکن است از خود بپرسید «اگر هیچ پردازنده گرافیکی‌ای در دسترس نباشد و لازم باشد کد را در CPU ذخیره کنید، چطور؟» نگران نباشید، PyTorch برای این شرایط نیز راهکار دارد. می‌توانید از تابع cuda.is_available() استفاده کرده و بررسی کنید که آیا GPU در دسترس دارید یا نه و بدین ترتیب دستگاه خود را انتخاب کنید.

با استفاده از تابع float() به راحتی می‌توانید دقت را بر روی مقادیر پایین‌تری قرار دهید (مثلا ۳۲-bit float).

import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot

device = 'cuda' if torch.cuda.is_available() else 'cpu'

# Our data was in Numpy arrays, but we need to transform them into PyTorch's Tensors
# and then we send them to the chosen device
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)

# Here we can see the difference - notice that .type() is more useful
# since it also tells us WHERE the tensor is (device)
print(type(x_train), type(x_train_tensor), x_train_tensor.type())

اگر نوع دو متغیر x و y را با هم مقایسه کنید مشاهده خواهید کرد که اولی numpy.ndarray و دومی torch.Tensor است.

امّا مشخص نیست تنسور کجا «ذخیره» شده است، در CPU یا GPU. تابع type()در PyTorch محل ذخیره تنسور را به شما نشان می‌دهد (torch.cuda.FloatTensor). در مثال این مقاله تنسور در GPU ذخیره شده است.

علاوه بر این می‌توانیم برعکس عمل کرده و با استفاده از numpy() تنسور را به آرایه Numpy بازگردانیم. باید به آسانیِ نوشتنِ کد x_train_tensor.numpy() باشد امّا در صورت اجرای این کد، با این پیغام خطا مواجه خواهید شد:

TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

متاسفانه در Numpy  نمی‌توانید تنسورهای واقع در  GPU را کنترل کنید و لازم است ابتدا با استفاده از cpu() آن‌ها را به تنسور CPU تبدیل کنید.

تعریف پارامترها

وجه تمایز تنسورِ داده‌، مثل تنسوری که ما ایجاد نموده‌ایم، با تنسورِ آموزش‌پذیر یعنی پارامتر یا وزن چیست؟

برای اینکه بتوانیم مقادیر آن‌ها (یعنی مقادیر پارامترها) را به‌روزرسانی کنیم، لازم است گرادیان تنسور آموزش‌پذیر را محاسبه نماییم. آرگومان requires_grad=True برای محاسبه گرادیان مناسب است. این آرگومان از PyTorch می‌خواهد گرادیان را محاسبه کند.

ممکن است بخواهید برای یک پارامتر تنسوری ساده ایجاد کرده و سپس آن را به دستگاه مورد نظر خود ارسال کنید، بهتر است عجله نکنید!

# FIRST
# Initializes parameters "a" and "b" randomly, ALMOST as we did in Numpy
# since we want to apply gradient descent on these parameters, we need
# to set REQUIRES_GRAD = TRUE
a = torch.randn(1, requires_grad=True, dtype=torch.float)
b = torch.randn(1, requires_grad=True, dtype=torch.float)
print(a, b)

# SECOND
# But what if we want to run it on a GPU? We could just send them to device, right?
a = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
b = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
print(a, b)
# Sorry, but NO! The to(device) "shadows" the gradient...

# THIRD
# We can either create regular tensors and send them to the device (as we did with our data)
a = torch.randn(1, dtype=torch.float).to(device)
b = torch.randn(1, dtype=torch.float).to(device)
# and THEN set them as requiring gradients...
a.requires_grad_()
b.requires_grad_()
print(a, b)

اولین قطعه کد دو تنسور مناسب برای همه چیز از جمله پارامترها و گرادیان‌‌ها ایجاد می‌کند. امّا این تنسورها، تنسور CPU هستند.

FIRST
tensor([-0.5531], requires_grad=True)
tensor([-0.7314], requires_grad=True)

قطعه کد دوم با رویکردی ساده تنسورها را به GPU ارسال می‌کند. اگر چه ارسال آن‌ها به یک دستگاه دیگر موفقیت آمیز است، امّا گرادیان تا حدی «از دست می‌رود».

SECOND
tensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)

در قطعه کد سوم ابتدا تنسورها را به دستگاه منتقل کرده و سپس با استفاده از تابع requires_grad_() پارامتر  requires_grad را برابر True قرار می‌دهیم.

THIRD
tensor([-0.8915], device='cuda:0', requires_grad=True) tensor([0.3616], device='cuda:0', requires_grad=True)

در PyTorch تمام توابعی که به خط تیره (_) ختم می‌شوند، تغییرات را در جای خود اعمال می‌کنند. به عبارت دیگر این توابع متغیر اصلی را اصلاح می‌کنند.

با اینکه رویکرد قبلی کارساز بود، بهتر است در همان لحظه‌ی ایجاد تنسور، آن را به یک دستگاه اختصاص دهیم.

# We can specify the device at the moment of creation - RECOMMENDED!
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True

این روش بسیار ساده‌تر است.

اکنون که با نحوه‌ی ایجاد تنسورهای مبتنی بر گرادیان آشنا شدید، به شما نشان می‌دهیم PyTorch چگونه آن‌ها را کنترل می‌کند. کنترل تنسورها در PyTorch توسط Autograd انجام می‌شود:

Autograd

Autograd همان «پکیج مشتق‌گیری خودکار» در PyTorch است. به لطف این پکیج دیگر نگران مشتق جزئی، قاعده زنجیره‌ای و مسائل شبیه به آن نخواهیم بود.

با استفاده از تابع backward() می‌توانیم از PyTorch بخواهیم که کار خود را انجام دهد و تمام گرادیان‌‌ها را محاسبه کند.

نقطه شروع محاسبه گرادیان  را به خاطر دارید؟ گرادیان را از «زیان» و همزمان با محاسبه مشتقات جزئی پارامترها (w.r.t.) محاسبه می‌کنیم. در نتیجه لازم است متد backward() را از متغیر متناظر آن در PyTorch، مثل loss.backward()، فرا بخوانیم.

می‌توانیم مقادیر حقیقی گرادیان‌‌ها را با بررسی ویژگی grad تنسور بدست آوریم.

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

امیدوارم به خاطر داشته باشید که منظور از خط تیره انتهای اسم تابع چیست (در غیر اینصورت به بخش قبل مراجعه کنید).

بهتر است محاسبه دستی گرادیان‌‌ها را فراموش کرده و در عوض از هر دو تابع backward() و zero_() استفاده کنیم.

کار تقریباً تمام است. امّا همیشه یک مشکل پنهان وجود دارد. و این بار مشکل پنهان در مورد به‌روزرسانی پارامترها است.

lr = 1e-1
n_epochs = 1000

torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor
    error = y_train_tensor - yhat
    loss = (error ** 2).mean()

    # No more manual computation of gradients! 
    # a_grad = -2 * error.mean()
    # b_grad = -2 * (x_tensor * error).mean()
    
    # We just tell PyTorch to work its way BACKWARDS from the specified loss!
    loss.backward()
    # Let's check the computed gradients...
    print(a.grad)
    print(b.grad)
    
    # What about UPDATING the parameters? Not so fast...
    
    # FIRST ATTEMPT
    # AttributeError: 'NoneType' object has no attribute 'zero_'
    # a = a - lr * a.grad
    # b = b - lr * b.grad
    # print(a)

    # SECOND ATTEMPT
    # RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
    # a -= lr * a.grad
    # b -= lr * b.grad        
    
    # THIRD ATTEMPT
    # We need to use NO_GRAD to keep the update out of the gradient computation
    # Why is that? It boils down to the DYNAMIC GRAPH that PyTorch uses...
    with torch.no_grad():
        a -= lr * a.grad
        b -= lr * b.grad
    
    # PyTorch is "clingy" to its computed gradients, we need to tell it to let it go...
    a.grad.zero_()
    b.grad.zero_()
    
print(a, b)

اگر اولین به‌روزرسانی ساختاری مشابه ساختار مورد استفاده در کد Numpy داشته باشد، با پیغام خطای زیر مواجه می‌شوید. البته با بررسی نمودن تنسور می‌توان متوجه شد که چه مشکلی پیش‌ آمده است: هنگام جایگذاریِ مجددِ نتایجِ به‌روزرسانی در پارامترهای مربوطه، دوباره گرادیان را «از دست داده‌ایم». در نتیجه ویژگی grad برابر none شده و خطا رخ می‌دهد.

FIRST ATTEMPT
tensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)
AttributeError: 'NoneType' object has no attribute 'zero_'

در دومین به‌روزرسانی به کمک ویژگی جایگذاری در محل (in-place)، آن را اندکی تغییر می‌دهیم. این بار هم PyTorch پیغام خطا می‌فرستد.

SECOND ATTEMPT
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.

علت این امر چیست؟ مشخص گردید که «بیش از حد مناسب بودن» باعث بروز مشکل شده است. مقصر این مشکل قابلیتPyTorch  در ایجاد «گراف محاسباتی پویا» از تمام عملیات‌های پایتون است که شامل نوعی تنسورِ محاسبه گرادیان یا وابستگی‌های آن می‌شود.

در بخش بعدی گراف محاسبه پویا را به تفصیل توضیح خواهیم داد.

با استفاده از torch.no_grad() می‌توانیم از PyTorch بخواهیم «عقب بیاستد» و اجازه بدهد خودمان بدون بهم ریختن گراف محاسباتی پویا پارامترها را به‌روزرسانی کنیم. این تابع امکان اجرای عملیات‌های متداول پایتون بر روی تنسور را، مستقل از نمودار محاسبه PyTorch، برای ما فراهم می‌کند.

بالاخره توانستیم مدل را با موفقیت اجرا نموده و پارامترهای نهایی را بدست‌آوریم. نتایج به‌دست‌آمده هم راستای نتایج حاصل از پیاده‌سازی مدل در Numpy بود.

THIRD ATTEMPT
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

گراف محاسباتی پویا

پکیج PyTorchViz و تابع make_dot(variable) این امکان را به ما می‌دهند که به آسانی گراف مرتبط با متغیر مد نظر خود را ترسیم کنیم.

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

torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()

با فراخوانی make_dot(yhat) گراف سمت چپ تصویر ۳ را بدست خواهید آورد:

pytorch
گراف محاسباتی تمام مراحلِ محاسبه MSE

مولفه‌های این گراف به شرح زیر است:

  • خانه‌های آبی رنگ: این خانه‌ها مبین تنسورهایی هستند که برای پارامترها استفاده کردیم؛ منظور تنسورهایی است که از PyTorch خواستیم گرادیان آن‌ها را محاسبه کند؛
  • خانه‌های خاکستری: این خانه‌ها عملیات پایتون را نشان می‌دهد که در برگیرنده تنسور محاسباتی گرادیان یا موارد وابسته به آن است؛
  • خانه‌ سبز: همانند خانه‌های خاکستری است با این تفاوت که این خانه‌ نقطه شروع محاسبه گرادیان کاهشی است (با فرض اینکه تابع backward() در متغیر مورد استفاده برای ترسیم گراف، فراخوانده شده باشد). در یک گراف، گرادیان‌‌ها از پایین به بالا محاسبه می‌شوند.

در صورتی که گراف متغیرهای خطا (گراف وسط) و زیان (گراف سمت راست) را ترسیم کنیم، همانطور که مشاهده می‌کنید تنها تفاوت بین آن‌ها و گراف سمت چپ تعداد گام‌های میانی (خانه‌های خاکستری) است.

[irp posts=”14391″]

اگر خانه سبزِ گراف yhat (گراف سمت چپ) را با دقت بیشتری بررسی ‌کنید، مشاهده خواهید کرد که دو فلش به سمت آن وجود دارد زیرا این مولفه مقادیر دو متغیر، a و b*x، را با هم جمع می‌کند.

حال اگر خانه خاکستری همین گراف را بررسی کنید، می‌بینید که عمل ضرب انجام شده است، یعنی b * x؛ ولی تنها یک فلش به سمت آن کشیده شده است. این فلش از سمت خانه آبی که متناظر با پارامتر b است می‌آید.

سوال اینجاست که چرا در گراف yhat هیچ خانه‌ای متناظر با داده‌ی x وجود ندارد؟ علت این است که گرادیانِ داده‌ی x را محاسبه نمی‌کنیم. بنابراین باوجود اینکه تنسورهای بیشتری در عملیات‌های اجرای شده توسط گراف محاسباتی دخیل هستند، این گراف تنها تنسورهای محاسبه گرادیان و وابستگی‌های آن‌ها را به نمایش می‌گذارد.

اگر requires_grad  را در متغیر a برابر False قرار دهیم، گراف محاسباتی به شکل زیر در می‌آمد.

مولفه های گراف در پایتورچ
در این گراف گرادیان متغیر a محاسبه نشده است. با این حال هنوز هم این متغیر در محاسبه استفاده می‌شود.

همانطور که مشاهده می‌کنید دیگر خانه آبی متناظر با پارامتر a (که در گراف yhat وجود داشت) در این گراف ترسیم نشده است. دلیل آن بسیار ساده است: چون گرادیانی محاسبه نمی‌گردد، گرافی هم برای آن ترسیم نمی‌شود.

یکی از مزایای گراف محاسباتی پویا این است که هرچه قدر بخواهید می‌توانید به پیچیدگی آن بیافزایید. حتی می‌توانید برای کنترل جریان محاسبه گرادیان‌ها از عبارت‌های کنترل جریان از قبیل if استفاده کنید.

نمونه آن را در تصویر ۵ مشاهده می‌کنید. البته اعداد محاسبه شده در این مثال کاملاً بی‌معنا هستند.

آموزش پایتورچ
نمونه گراف محاسباتی پیچیده

بهینه‌سازها

تا اینجا یاد گرفتیم پارامترها را با استفاده از گرادیان محاسباتی و به طور دستی به‌روزرسانی کنیم. در صورتی که دو پارامتر وجود داشته باشد، کار دشواری نیست. امّا اگر تعداد پارامترها خیلی زیاد باشد باید از بهینه‌سازهای PyTorch مانند SGD و Adam استفاده کنیم.

بهینه‌سازT پارامتر مورد نظر (مثل نرخ یادگیری مورد استفاده یا هزاران اَبَرپارامتر احتمالی دیگر) را انتخاب نموده و با تابع step() آن را به‌روزرسانی می‌کند.

[irp posts=”3143″]

به علاوه، دیگر نیازی نیست مقدار تک تک گرادیان‌‌ها را صفر کنیم. تنها کافی است بهینه‌ساز zero_grad() را فرا بخوانیم.

کد زیر بهینه‌ساز گرادیان کاهشی تصادفی (SGD) برای پارامترهای a و b در مثالِ این مقاله را نشان می‌دهد. شایان ذکر است که ممکن است نام بهینه‌ساز شما را فریب دهد: در صورتی کل داده‌ی آموزشی را به یکباره برای بهینه‌سازی مورد استفاده قرار دهید، درست مانند کد مورد استفاده در این مقاله، بهینه‌ساز، علی رغم نام خود، گرادیان کاهشی بسته‌‌ای را اجرا خواهد کرد.

torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)

lr = 1e-1
n_epochs = 1000

# Defines a SGD optimizer to update the parameters
optimizer = optim.SGD([a, b], lr=lr)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor
    error = y_train_tensor - yhat
    loss = (error ** 2).mean()

    loss.backward()    
    
    # No more manual update!
    # with torch.no_grad():
    #     a -= lr * a.grad
    #     b -= lr * b.grad
    optimizer.step()
    
    # No more telling PyTorch to let gradients go!
    # a.grad.zero_()
    # b.grad.zero_()
    optimizer.zero_grad()
    
print(a, b)

فقط برای اطمینان از اینکه همه چیز هنوز خوب کار می‌کند، هر دو پارامتر را قبل و بعد از به‌روزرسانی بررسی می‌کنیم:

# BEFORE: a, b
tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
# AFTER: a, b
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

موفق شدیم فرآیند بهینه‌سازی را بهینه کنیم. در ادامه می‌پردازیم به بخش‌های باقی‌مانده.

زیان

همانطور که انتظار می‌رود، PyTorch محاسبه زیان را نیز پوشش می‌دهد. تعداد توابع زیانی که می‌توان از میان آن‌ها یکی را متناسب با مسئله موردنظر انتخاب کرد بسیار زیاد است. به دلیل اینکه مسئله مورد بررسی ما رگرسیون ساده است از تابع زیان میانگین مربعات خطا (MSE) استفاده می‌کنیم.

دقت داشته باشید که nn.MSELoss  فقط تابع زیان را برای ما ایجاد می‌کند و خود تابع زیان نیست. علاوه بر این می‌توانید یک متد کاهشی نیز تعیین کنید؛ متدی که نشان می‌دهد چگونه می‌خواهید نتایجِ نقاط جداگانه را کنار هم قرار دهید. برای مثال می‌توانید میانگین آن‌ها (reduction=’mean’) را محاسبه کنید یا اینکه صرفاً آن‌ها را باهم جمع کنید (reduction=’sum’).

بعداً و در خط ۲۰، از تابع زیان ایجاد شده برای محاسبه زیان بین پیش‌بینی‌ها و برچسب‌ها استفاده می‌کنیم.

کد ما به شکل زیر در می‌آید:

torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)

lr = 1e-1
n_epochs = 1000

# Defines a MSE loss function
loss_fn = nn.MSELoss(reduction='mean')

optimizer = optim.SGD([a, b], lr=lr)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor
    
    # No more manual loss!
    # error = y_tensor - yhat
    # loss = (error ** 2).mean()
    loss = loss_fn(y_train_tensor, yhat)

    loss.backward()    
    optimizer.step()
    optimizer.zero_grad()
    
print(a, b)

در این مرحله تنها باید یک تکه کد، یعنی کد پیش‌بینی‌ها، را تغییر داد. حال نوبت می‌رسد به ارائه تابع PyTorch برای پیاده‌سازی مدل.

مدل

بازنمایی مدل در PyTorch توسط یک کلاس معمولی پایتون انجام می‌شود. این کلاس بر گرفته از کلاس Module است.

اصلی‌ترین توابعی که باید به اجرا درآیند به شرح زیر هستند:

  • __init__(self): در این تابع بخش‌های مختلف مدل را تعریف می‌کنیم. در مثال ما دو پارامتر وجود دارد: a و b.

با وجود اینکه در تعریف پارامترها هیچ محدودیتی وجود ندارد، می‌توانید مدل‌های دیگر یا لایه‌های دیگر را به عنوان یک صفت در مدل خود جای دهید. به زودی نمونه این مدل‌ها را نیز خواهیم دید.

  • forward(self, x): این تابع محاسبات اصلی را انجام می‌دهد، یعنی ورودی x را دریافت کرده و خروجی را تولید می‌کند.

با این حال نباید متد forward(x) را فرا بخوانید. بلکه برای اجرای forward pass و برآورد به پیش‌بینی‌ها باید تمام مدل را به عنوان model(x) فراخوانی کنید.

برای مسئله رگرسیون مورد بررسی در این مقاله یک مدل مناسب (و ساده) می‌سازیم. کد آن به صورت زیراست:

class ManualLinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
# To make "a" and "b" real parameters of the model, we need to wrap them with nn.Parameter
        self.a = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
        self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
        
    def forward(self, x):
        # Computes the outputs / predictions
        return self.a + self.b * x

در متد __init__  دو پارامتر a و b را با استفاده از کلاس Parameter() تعریف می‌کنیم. به این ترتیب به PyTorch می‌گوییم پارامترهای a و b باید پارامترهای همان مدلی باشند که صفت آن هستند.

با انجام این کار (به جای اینکه لیستِ پارامترها را به طور دستی بسازیم) می‌توانیم با استفاده از متد parameters()  یک پیمایش‌گر را بازیابی کرده و بر روی تمام پارامترهای مدل پیاده کنیم؛ این تابع حتی پارامترهای مدل‌های تودرتو را نیز پیمایش می‌کند. سپس این پارامتر‌ها را به بهینه‌ساز می‌دهیم.

علاوه بر این، با استفاده از تابع state_dict() می‌توانیم مقادیر فعلی تمام پارامترها را بدست آوریم.

نکته مهم: مدل باید به همان دستگاهی که داده در آن قرار دارند، فرستاده شود. اگر داده‌ها‌ از تنسورهای GPU تولید شده باشند، مدل نیز در GPU قرار می‌گیرد.

برای تغییر کد می‌توانیم متد‌های کاربردی زیر را استفاده کنید:

torch.manual_seed(42)

# Now we can create a model and send it at once to the device
model = ManualLinearRegression().to(device)
# We can also inspect its parameters using its state_dict
print(model.state_dict())

lr = 1e-1
n_epochs = 1000

loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=lr)

for epoch in range(n_epochs):
    # What is this?!?
    model.train()

    # No more manual prediction!
    # yhat = a + b * x_tensor
    yhat = model(x_train_tensor)
    
    loss = loss_fn(y_train_tensor, yhat)
    loss.backward()    
    optimizer.step()
    optimizer.zero_grad()
    
print(model.state_dict())

خروجی متد به شرح زیر است (به دلیل اینکه مقادیر پایانی پارامترهای a و b تغییری نکرده است، پس همه چیز خوب است!):

OrderedDict([('a', tensor([0.3367], device='cuda:0')), ('b', tensor([0.1288], device='cuda:0'))])
OrderedDict([('a', tensor([1.0235], device='cuda:0')), ('b', tensor([1.9690], device='cuda:0'))])

توجه شما را به یک عبارت خاص در کد جلب می‌کنم: model.train().

مدل‌ها در PyTorch یک متد train() دارند که متاسفانه هیچ گام آموزشی را اجرا نمی‌کند. تنها هدف آن قرار دادن مدل در حالت آموزش است. این متد از این جهت حائز اهمیت است که برخی از مدل‌ها ممکن است مکانیزیم‌هایی مثل Dropout یا حذف تصادفی داشته باشند که باعث می‌شود در مراحل آموزش و اعتبارسنجی رفتاری کاملاً متفاوت از خود نشان دهند.

مدل‌های تودروتو

اگر به خاطر داشته باشید، در ساخت مدلِ این مقاله برای اجرای رگرسیون خطی به طور دستی دو پارامتر ایجاد نمودیم. در این مرحله با استفاده از مدل خطی PyTorch به عنوان یکی از صفات مدل اصلی، یک مدل تودرتو می‌سازیم.

با اینکه این مثال کاملاً ساختگی است و در آن مدل اصلی را تقریباً بدون اضافه کردن مقوله‌ای مفید برنامه نویسی می‌کنیم، به خوبی مفهوم مدل تودرتو را نشان می‌دهد.

در متد __init__  صفتی تعریف نمودیم که شامل شبکه خطی تودرتو می‌شود.

در متد forward() مدل تودرتو را برای اجرای forward pass فرامی‌خوانیم (دقت داشته باشید که self.linear.forward(x) را فراخوانی نمی‌کنیم).

class LayerLinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        # Instead of our custom parameters, we use a Linear layer with single input and single output
        self.linear = nn.Linear(1, 1)
                
    def forward(self, x):
        # Now it only takes a call to the layer to make predictions
        return self.linear(x)

اکنون اگر متد parameters() این مدل را فرابخوانید، PyTorch پارامترهای صفات را به صورت بازگشتی محاسبه می‌کند. با اجرای کد[*LayerLinearRegression().parameters()] شما هم می‌توانید این روش را امتحان کنید و لیست تمام پارامترها بدست آورید. علاوه بر این می‌توانید صفات خطی جدید نیز اضافه نمایید، صفاتی که حتی اگر در forward pass مورد استفاده قرار نگیرند، بازهم در لیستی تحت عنوان parameters() قرار می‌گیرند.

مدل‌های توالی

مدل این مقاله بسیار ساده بود. ممکن است بپرسید «چرا زحمت ساخت کلاس برای مدلی به این سادگی را به خودمان بدهیم؟» به نکته خوبی اشاره می‌کنید.

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

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

Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)

گام آموزش

تا کنون بهینه‌ساز، تابع زیان و مدل را تعریف کرده‌ایم. برای اطلاعات بیشتر می‌توانید مطالب بالا را مرور کرده و نگاهی به کدهای داخل حلقه‌ بیاندازید. اکنون این سوال مطرح می‌شود که اگر از یک بهینه‌ساز، تابع زیان یا حتی مدل دیگری استفاده می‌کردیم، چه تفاوتی ایجاد می‌شد؟ در ادامه توضیح می‌دهیم چگونه می‌توان مدل را عمومی‌تر کرد.

با توجه به سه عنصر بهینه‌ساز، تابع زیان و مدل، و همچنین ویژگی‌ها و برچسب‌ها، تمام خطوط این کد، یک گام آموزشی را اجرا می‌کنند.

اکنون می‌توانیم یک تابع جدید بنویسیم که با این سه عنصر یک تابع دیگر می‌سازد، تابع خروجی گام آموزشی را با در نظرگرفتن مجموعه‌ای از ویژگی‌ها، برچسب‌ها و استدلال‌ها اجرا کرده و زیان متناطر با هر یک را محاسبه می‌کند.

سپس می‌توانیم با کمک این تابع همه‌کاره یک تابع train_step() بسازیم و آن را به درون حلقه آموزش فرابخوانیم. اکنون کد ما به شکل زیر خواهد بود. ملاحظه می‌کنید که حلقه آموزش کوچکتر شده است:

def make_train_step(model, loss_fn, optimizer):
    # Builds function that performs a step in the train loop
    def train_step(x, y):
        # Sets model to TRAIN mode
        model.train()
        # Makes predictions
        yhat = model(x)
        # Computes loss
        loss = loss_fn(y, yhat)
        # Computes gradients
        loss.backward()
        # Updates parameters and zeroes gradients
        optimizer.step()
        optimizer.zero_grad()
        # Returns the loss
        return loss.item()
    
    # Returns the function that will be called inside the train loop
    return train_step

# Creates the train_step function for our model, loss function and optimizer
train_step = make_train_step(model, loss_fn, optimizer)
losses = []

# For each epoch...
for epoch in range(n_epochs):
    # Performs one train step and returns the corresponding loss
    loss = train_step(x_train_tensor, y_train_tensor)
    losses.append(loss)
    
# Checks model's parameters
print(model.state_dict())

فعلاً حلقه آموزش را کنار گذاشته و به داده‌‌ها می‌پردازیم. تاکنون تنها آرایه‌های Numpy را به تنسورهای PyTorch تبدیل نموده‌ایم. امّا بهتر از این هم می‌توانیم عمل کنیم و یک دیتا‌ست بسازیم.

دیتاست

دیتاست در PyTorch به صورت کلاس معمولی پایتونی بازنمایی می‌شود که ویژگی‌های کلاس Dataset را به ارث برده است. Dataset را می‌توانید لیستی از تاپل‌های پایتون در نظر بگیرید به طوری که هر تاپل متناظر با یک نقطه (ویژگی یا برچسب) است.

مهم‌ترین متد‌های مورد نیاز آن به شرح زیر هستند:

  • __init__(self): این متد تمام آرگومان‌های مورد نیاز برای ایجاد فهرست تاپل‌ها را در بر می‌گیرد و ممکن است حاوی نام فایل CSV باشد که باید بارگذاری و پردازش شود، یا حاوی دو تنسور باشد که یکی برای ویژگی‌ها و دیگری برای برچسب‌ها، و یا هر چیز دیگری ساخته شده است.

نیازی نیست کل دیتاست را در واحد سازنده (__init__) بارگذاری کنید. اگر دیتاست شما بزرگ است (مثلاً شامل ده‌ها هزار عکس می‌شود) بارگذاری یکباره آن برای حافظه مشکل‌ساز خواهد شد. پیشنهاد می‌کنیم آن‌ها را بر حسب نیاز بارگذاری کنید (یعنی هر زمانی که__get_item__  فراخوانی  می‌شود).

  • __get_item__(self, index): این متد امکان اندیس‌گذاری دیتاست را فراهم می‌کند و در نتیجه این عمل، دیتاست شبیه یک لیست رفتار می‌کند (dataset[i])، یعنی متناسب با نقطه داده درخواستی، خروجی آن باید یک تاپل (ویژگی‌ها، برچسب) باشد. به این ترتیب، می‌توان قطعه‌های مورد نظر از دیتاستی که قبلاً بارگذاری‌شده‌ یا تنسورها خروجی بگیریم یا همانطور که قبلاً مطرح شد، آن‌ها را برحسب نیاز بارگذاری کنیم (مانند این مثال).
  • __len__(self): خروجی این متد صرفاً سایز کل دیتاست است. لذا، هر گاه که از دیتاست نمونه‌برداری می‌شود، اندیس‌گذاری آن به سایز اصلی محدود می‌گردد.

در ادامه دیتاستی ساده و سفارشی می‌سازیم که آرگومان‌های آن از دو تنسور تشکیل شده است: یکی برای ویژگی و دیگری برای برچسب. برای هر یک از اندیس‌ها، خروجی کلاس دیتاست ما قطعه متناظرِ آن در هر یک از تنسورها خواهد بود. کد نویسی این تابع به شرح زیر است:

from torch.utils.data import Dataset, TensorDataset

class CustomDataset(Dataset):
    def __init__(self, x_tensor, y_tensor):
        self.x = x_tensor
        self.y = y_tensor
        
    def __getitem__(self, index):
        return (self.x[index], self.y[index])

    def __len__(self):
        return len(self.x)

# Wait, is this a CPU tensor now? Why? Where is .to(device)?
x_train_tensor = torch.from_numpy(x_train).float()
y_train_tensor = torch.from_numpy(y_train).float()

train_data = CustomDataset(x_train_tensor, y_train_tensor)
print(train_data[0])

train_data = TensorDataset(x_train_tensor, y_train_tensor)
print(train_data[0])

اگر دیتاست تنها متشکل از چند تنسور باشد، می‌توانیم با استفاده از کلاس TensorDataset موجود در PyTorch به راحتی این مسئله را حل کنیم، که تقریباً مشابه کاری است که برای ایجاد دیتاست سفارشی انجام دادیم.

شایان ذکر است که تنسورهای آموزشی را از آرایه‌های Numpy بدست آوردیم، ولی آن‌ها را به هیچ دستگاهی منتقل نکردیم. بنابراین، این تنسورها، تنسورهای CPU هستند.

تنسورها را منتقل نکردیم زیرا نمی‌خواهیم مانند تنسورهای GPU تمام داده‌ی آموزشی به دستگاه منتقل شود؛ انتقال کل داده‌ی آموزشی فضای زیادی از RAM کارت گرافیکی را اشغال خواهد کرد.

پاسخ این سوال که «چرا دیتاست می‌سازیم» این است که می‌خواهیم از یک DataLoader استفاده کنیم.

[irp posts=”6228″]

DataLoader

تا این بخش از مقاله در هر گام آموزشی کل داده‌ی آموزشی را به‌کار می‌بردیم. همچنین در تمام مدت از گرادیان کاهشی بسته‌‌ای استفاده می‌کردیم. این امر برای دیتاست‌ کوچک و بی‌معنای مورد استفاده در این مقاله هیچ مشکلی ایجاد نکرد امّا در پروژه‌های جدی باید گرادیان کاهشی زیربسته‌ها را به کار بگیریم. بنابراین لازم است زیربسته داشته باشیم و به طَبَع آن باید دیتاست را به چند قطعه تقسیم کنیم. به طور قطع پیشنهاد نمی‌کنیم این کار را به طور دستی انجام دهید.

برای تقسیم نمودن دیتاست از کلاس DataLoader موجود در PyTorch استفاده می‌کنیم. دیتاست مورد استفاده در این مثال، همان دیتاستی است که در بخش قبلی ایجاد نمودیم. به این منظور تنها کافی است سایز زیربسته‌ها و بهم ریختن یا مرتب نگه‌داشتن آن‌ها را برای کلاس DataLoader مشخص ‌کنیم.

loader مانند یک پیمایش‌گر عمل می‌کند و در نتیجه می‌توانیم روی آن حلقه ایجاد کنیم و در هر اجرا یک زیربسته دیگر را فراخوانی کنیم.

from torch.utils.data import DataLoader

train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)

برای بازیابی یک زیربسته نمونه می‌توانید کد دستوری زیرا اجرا نمایید؛ با اجرای این کد لیستی شامل دو تنسور که یکی مربوط به ویژگی و دیگری مربوط به برچسب است، نمایش داده می‌شود.

next(iter(train_loader))

حال این سوال  مطرح می‌شود که این کد چگونه حلقه آموزشی را تغییر می‌دهد.

losses = []
train_step = make_train_step(model, loss_fn, optimizer)

for epoch in range(n_epochs):
    for x_batch, y_batch in train_loader:
        # the dataset "lives" in the CPU, so do our mini-batches
        # therefore, we need to send those mini-batches to the
        # device where the model "lives"
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)
        
        loss = train_step(x_batch, y_batch)
        losses.append(loss)
        
print(model.state_dict())

اجرای این کد دو تفاوت عمده ایجاد می‌کند: اول اینکه نه تنها برای بارگذاری هر یک از زیربسته‌های موجود در DataLoader یک حلقه داخلی داریم، بلکه اکنون می‌توانیم به طور مجزا فقط یک زیربسته را به دستگاه ارسال کنیم.

برای دیتاست‌های بزرگ‌تر اگر داده‌ را به صورت نمونه به نمونه (بر روی تنسور CPU) و از طریق تابع __get_item__  در  کلاس Dataset بارگذاری کرده و سپس به یکباره تمام نمونه‌های همان زیربسته را به GPU (یا دستگاه) مورد نظر ارسال کنید، می‌توانید از RAM کارت گرافیکی به بهترین شکل ممکن استفاده کنید.

به علاوه در صورتی که تعداد GPUهای مورد استفاده برای آموزش مدل زیاد باشد، بهتراست دیتاست را در حالت «agnostic» نگه دارید و بسته‌‌ها را حین آموزش در GPUهای مختلف جایگذاری نمایید.

مطالب مطرح شده تا این بخش از مقاله آموزشی تنها در مورد دیتاست آموزشی بود. یک دیتاست تولید و سپس برای آن یک dataloader ایجاد کردیم. می‌توانیم برای اعتبارسنجی داده‌ نیز همین کار را انجام دهیم و با همان روشی که ابتدای مقاله مطرح کردیم داده‌ها را به دو قسمت تقسیم کنیم یا از تابع random_split استفاده کنیم.

Random Split

در PyTorch تابعی به نام random_split() وجود دارد که روشی آسان و متداول برای تقسیم کردن دیتاست و تبدیل آن به دو دیتاست آموزشی و اعتبار سنجی است. شایان ذکر است که در این مثال لازم است این تابع را بر روی تمام دیتاست اعمال کنیم (و نه صرفاً بر روی دیتاست آموزشی‌ای که در بخش‌های قبل ایجاد نمودیم).

سپس در ازای هر یک زیرمجموعه‌های دیتاست یک تابع DataLoader می‌سازیم. کد آن به شرح زیر خواهد بود:

from torch.utils.data.dataset import random_split

x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()

dataset = TensorDataset(x_tensor, y_tensor)

train_dataset, val_dataset = random_split(dataset, [80, 20])

train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)

اکنون برای دیتاست اعتبارسنجی نیز یک DataLoader داریم.

اعتبارسنجی

اعتبارسنجی آخرین بخش این مقاله است؛ برای اینکه بتوانیم مدل را ارزیابی کنیم، یا به عبارتی «زیان اعتبارسنجی» را محاسبه کنیم، لازم است حلقه آموزش را تغییر دهیم. اولین گام شامل اضافه نمودن یک حلقه درونی دیگر است تا زیربسته‌های حاصل از validation loader را مدیریت کند و آن‌ها را به همان دستگاهی ارسال کند که مدل روی آن قرار دارد. سپس با کمک مدل پیش‌بینی را انجام می‌دهیم (خط ۲۳) و زیان هر یک را محاسبه می‌کنیم (خط ۲۴).

تقریباً تمام است امّا جای دارد دو نکته کوچک ولی مهم را مد نظر قرار دهید:

  • no_grad(): اگر چه این تابع تغییرات زیادی در مدل‌ ساده ما ایجاد نخواهد کرد، امّا بستنِ حلقه درونی مرحله اعتبارسنجی با این مدیریت‌کننده محتوا روش مناسبی است برای غیر فعال نمودن تمام محاسبات گرادیانی که ممکن است ناخواسته انجام شوند؛ منظور گرادیان‌های آموزشی است نه گرادیان‌های مرحله اعتبارسنجی.
  • eval(): تنها عملیاتی که این تابع انجام می‌دهد قرار دادن مدل در مرحله اعتبارسنجی است. درست مانند همتای خود یعنی تابع train() در دیتاست آموزشی. درنتیجه مدل رفتار خود را متناسب با عملیات‌هایی مثل Dropout یا حذف تصادفی تعدیل خواهد کرد.

در نهایت حلقه آموزش به شرح زیر خواهد بود:

losses = []
val_losses = []
train_step = make_train_step(model, loss_fn, optimizer)

for epoch in range(n_epochs):
    for x_batch, y_batch in train_loader:
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)

        loss = train_step(x_batch, y_batch)
        losses.append(loss)
        
    with torch.no_grad():
        for x_val, y_val in val_loader:
            x_val = x_val.to(device)
            y_val = y_val.to(device)
            
            model.eval()

            yhat = model(x_val)
            val_loss = loss_fn(y_val, yhat)
            val_losses.append(val_loss.item())

print(model.state_dict())

البته راه‌های دیگری برای بهبود یا تغییر مدل وجود دارد. همیشه می‌توانید به مدل یک مولفه‌ دیگر اضافه کنید، برای مثال می‌توانید از تابع برنامه‌ریز نرخ یادگیری (learning rate scheduler) استفاده کنید. به دلیل طولانی شدن این نوشتار توضیحات را همین جا به پایان می‌رسانیم.

برای دریافت کد کامل با تمام جزئیات آن اینجا کلیک کنید.

میانگین امتیاز / ۵. تعداد ارا :

مطالب پیشنهادی مرتبط

اشتراک در
اطلاع از
0 نظرات
بازخورد (Feedback) های اینلاین
مشاهده همه دیدگاه ها
[wpforms id="48325"]