آموزش pytorch رایگان
بهنام خدا، سلام… با یک پست بسیار طولانی در خدمت شما هستم. میخواهم در یک پست به شما پایتورچ را آموزش دهم! در آموزش pytorch رایگان نمیخواهم تمام جزئیات پایتورچ را به شما آموزش دهم، اما تلاش میکنم که ملاقات اولتان با پایتورچ دلنشین باشد. این آموزش را براساس این پست خوب نوشتم، اما مطمئن باشید صرفا ترجمه نیست و وقت زیادی برای نگارش گذاشتهام. با هوسم همراه باشید…
انگیزه نگارش پست آموزش pytorch
پایتورچ در سالهای گذشته بسیار پیشرفت کرده و در بین محققان به یک فریمورک یادگیری عمیق محبوب تبدیل شده است. بهگونهای که آماروارقام نشان میدهد که فریمورک پایتورچ نسبت به تنسورفلو از محبوبیت بیشتری در بین محققان برخوردار هست (نگاهی به آخرین مقالات در paperswithcode.com بیندازید). به دو دلیل تصمیم گرفتم که یک پست جامع آموزش pytorch بنویسم. اول اینکه، در ایران هنوز ارزش پایتورچ درک نشده است. دوم اینکه، پست آموزش pytorch فارسی خوب در اینترنت ندیدم. بنابراین، لازم دیدم که دینم را به پایتورچ عزیز و دوستداشتنی ادا کنم و یک پست کامل در شان پایتورچ بنویسم. ادعای من این هست که اگر با شبکه عصبی MLP و پایتون آشنا باشید، با این پست بهراحتی ساختار کدنویسی در پایتورچ را درک خواهید کرد و احتمالا شیفتهاش میشوید!
پیش نیازهای آموزش pytorch رایگان
توصیه میکنم موارد زیر را قبل از شروع آموزش مدنظر داشته باشید:
- همزمان با خواندن این آموزش، بخشهای کدنویسی را تمرین کنید. لطفا کپی پیست نکنید، خودتان کدها را بنویسید!
- اگر هم پایتورچ را نصب نکردهاید، این پست را بخوانید تا نصب پایتورچ را سریع انجام دهید.
- در این پست گفته شده که بجای نصب پایتورچ، از گوگل کولب هم میتوانید استفاده کنید.
پس لطفا برای این آموزش وقت بگذارید تا نتیجه مثبتی داشته باشد.
رگرسیون خطی (Linear Regression)
بهتر هست آموزش pytorch رایگان را با یک مثال پیش ببریم. برخلاف بسیاری از آموزشها در حوزه یادگیری عمیق که با دستهبندی تصاویر مانند mnist و cifar شروع میکنند، دراینجا من رگرسیون خطی را انتخاب کردهام. میخواهیم یک دانه نورون را در پایتورچ پیادهسازی کنم و یک دیتاست به آن بدهم و آموزشش دهم. چرا یک شبکه عصبی چند لایه و مثال دستهبندی تصاویر انتخاب نکردم؟ اول اینکه، مولف این پست با رگرسیون آموزش را جلو برده است! 😉 دوم اینکه، این پست مربوط به آموزش pytorch هست. اگر شبکه عصبی بزرگی را برای آموزش انتخاب میکردم، ممکن بود آموزش منحرف شود. چون تمرکز ما در این پست باید دقیقا روی پایتورچ و قابلیتهایش باشد. حاشیه کافی است، برویم سراغ متن…
رابطه رگرسیون خطی را در زیر مشاهده میکنید:
y = a + bx (رابطه 1)
رابطه بالا معادل یک نورون است. a معادل بایاس و b هم همان وزن در نورون است. یک نورون داریم و میخواهیم با همین مثال تا انتهای آموزش pytorch پیش برویم.
پیاده سازی رگرسیون خطی در پایتورچ
پیاده سازی رگرسیون خطی در پایتورچ شامل مراحل زیر هست:
- تولید داده
- ساخت نورون یا پیادهسازی رابطه (1)
- محاسبه خروجی نورون براساس داده ورودی و مدل رابطه (1)
- محاسبه لاس یا اتلاف
- محاسبه گرادیان ها
- بروزرسانی پارامترهای نورون
در فرآیند بالا، مراحل 3 تا 6 باید آنقدر تکرار شود که اتلاف حداقل شود. شکل 1 دقیقا ساختار کلی یک شبکه عصبی را نشان میدهد.
خیلی فکر کردم و دیدم بهتر است همین ابتدای کار کدهای کامل رگرسیون خطی در پایتورچ را به شما نشان بدهم. هدفم این است که به یک آشنایی کلی با کدهای پایتورچ برسید.
# parameters of the Linear regression model 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) lr = 1e-1 n_epochs = 1000 # MSE loss function loss_fn = nn.MSELoss(reduction='mean') # SGD optimizer optimizer = optim.SGD([a, b], lr=lr) for epoch in range(n_epochs): # Compute output of the regression model yp = a + b * x # Calc. loss L = loss_fn(yt, yp) # Compute gradient of parameters (dL/da , dL/db) L.backward() # Update parameters (a , b) optimizer.step() # Reset gradients optimizer.zero_grad()
خب، مراحل زیر را بخوانید و به شکل 1 و کد بالا نگاه کنید:
- ابتدا، یک مجموعه داده داریم. در کدها، دادهها با متغیر x نشان داده شدهاند. فعلا نیازی نیست که بفهمید این دادهها از کجا آمدهاند.
- در مرحله دوم، دادهها را به شبکه یا مدل دادهایم. فرقی نمیکند شبکه چه باشد! چه یک نورون چه یک شبکه عصبی کانولوشنی بزرگ… خروجی شبکه yp را گرفتهایم. در کدها در خط 17، به مدل ورودی x دادهایم و خروجی yp را گرفتهایم.
- حال باید ببینیم این خروجی چقدر از مقدار ایدهآل فاصله دارد. بله، مطابق شکل 1، نوبت محاسبه تابع اتلاف براساس مقدار ایدهآل yt و خروجی پیش بینی شبکه yp است. مقدار اتلاف با استفاده از loss_fn در خط 20 حساب شده و در L ذخیره شده است.
- حالا باید این مقدار اتلاف را پس انتشار کنیم (پس انتشار خطا یا back propagation). پس انتشار خطا را با استفاده از ()loss.backward انجام میدهیم. میبینید که در خط 23 به سادگی گفتهایم loss را backward کن. خروجی این مرحله این است که مقدار گرادیان تمامی پارامترهای موجود در مدل (L/∂a∂ و L/∂b∂) محاسبه میشود.
- بسیارخب، خطا پس انتشار شد و حالا باید پارامترهای شبکه آپدیت شوند. آپدیت شدن پارامترها را یک بهینه ساز انجام میدهد. بنابراین، در خط 26، با ()optimizer.step فرآیند آپدیت پارامترها انجام میشود. مشاهده میکنید که یکسری از مراحل در حلقه for هست و باید آنقدر تکرار شود تا به بهترین پارامترها برسیم و اتلاف کم شود.
- در خط 29، ()optimizer.zero_grad چیست؟ فعلا نادیده بگیرید، بعدا توضیح میدهم…
خب، اگر متوجه توضیحات این بخش نشدید، مشکلی نیست. درادامه بیشتر توضیح خواهم داد. این توضیحها بیشتر برای آشنایی بود… حال، بیایید تک تک مراحل بالا را با جزئیات بررسی کنیم. من در هر بخش، به شما توضیح میدهم که چگونه با استفاده از قابلیتهای پایتورج، پیادهسازی را انجام دهید. پس تازه پست آموزش pytorch رایگان شروع شده است…
تولید داده در یادگیری ماشین (داده ساختگی)
قبل از هرچیز باید تکلیف دادههای ورودی را مشخص کنیم. دراینجا، بهتر هست کار را پیچیده نکنیم و یکسری داده مصنوعی بسازیم. میخواهم یک بردار با 100 نقطه یا نمونه بسازم که روی خط y=1+2x+n با مقداری نویز گوسی (n) توزیع شده باشند. این کار به راحتی با استفاده از نامپای (numpy) انجام میشود. کافی است همان رابطه بالا (y=1+2x+n) را با نامپای پیادهسازی کنیم.
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)
طبیعتا، همواره اینطور هست که دو دسته داده (train و val) داریم. بنابراین، با استفاده از کدهای زیر میتوانیم دیتاست بالا را به دو دسته جداگانه train و val تقسیم کنیم:
# 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]
حالا بیایید با استفاده از matplotlib نمودار دادههای train و val را رسم کنیم:
plt.plot(x_train, y_train, 'o') plt.title('Generated Data (Train)') plt.grid() plt.xlabel('x_train') plt.ylabel('y_train')
بسیارخب، یکسری نقطه با توزیع y=1+2x+n با کمی نویز (n) در این بخش ساختیم. حالا میخواهیم با استفاده از رگرسیون خطی، بهترین خط را برروی این دادهها برازش یا فیت کنیم. خب میدانیم که مقدار a و b در رابطه (1) باید بهترتیب برابر با 1 و 2 باشد. اما میخواهیم با استفاده از گرادیان کاهشی و داده آموزشی مقدار a و b را بدست آوریم. لازم هست کمی درباره گرادیان کاهشی صحبت کنم، پس برویم سراغ گرادیان کاهشی…
گرادیان کاهشی در پایتورچ (gradient descent)
در این بخش میخواهم درباره گرادیان کاهشی و نحوه آپدیت شدن پارامترهای مدل رگرسیون خطی صحبت کنم. ممکن هست شما با مباحث این بخش آشنا باشید، بنابراین راحت باشید و از این بخش آموزش pytorch رایگان بپرید.
آپدیت شدن پارامترها و رسیدن به مقادیر بهینه شامل 5 مرحله اصلی است که در بلوک دیاگرام شکل 1 نیز قابل مشاهده هست. این 5 گام عبارتنداز:
- محاسبه خروجی پیشبینی شبکه
- محاسبه اتلاف یا loss
- محاسبه گرادیان
- آپدیت پارامترها
- تکرار چهار مرحله بالا (1 تا 4)، تا زمانیکه خطا یا اتلاف بسیار کم شود.
در ادامه درمورد هریک از چهار مرحله بالا توضیح میدهم.
گام 1: محاسبه خروجیهای پیش بینی رگرسیون خطی براساس دادههای ورودی
در گام اول، ابتدا تمامی دادههای ورودی x را به مدل رگرسیون خطی (y=a+bx) میدهیم و خروجی y را بدست میآوریم. خب مساله صرفا جایگذاری است و ساده است. تنها نکته، مقادیر a و b است که نداریم و باید آنها را بدست آوریم. بنابراین، مقدار آنها را بهصورت تصادفی (رندوم) انتخاب میکنیم. خروجیهای y برای همه دادهها (مثلا 80 نمونه آموزشی) بدست آمد. پس کد این بخش بهصورت زیر میشود:
np.random.seed(42) a = np.random.randn(1) b = np.random.randn(1) print(a, b) # Computes our model's predicted output y = a + b * x_train print(y.shape)
[0.49671415] [-0.1382643] (80, 1)
گام 2: محاسبه اتلاف یا loss
درکل توابع اتلاف متنوعی با کارکردهای مختلف در یادگیری ماشین و شبکه عصبی وجود دارد. برای مساله رگرسیون، یکی از پرکاربردترینها تابع اتلاف Mean Square Error (MSE) هست. یعنی رابطه زیر:
طبق رابطه بالا، MSE عبارتنداز میانگینِ مربعِ اختلاف بین مقدار هدف یا واقعی (y) و مقدار پیشبینیشده توسط مدلی مانند رگرسیون خطی (a+bx). رابطه بالا در نامپای بهصورت زیر پیادهسازی میشود:
# How wrong is our model? That's the error! error = (y_train - y) # It is a regression, so it computes mean squared error (MSE) loss = (error ** 2).mean() print(loss)
2.7421577700550976
گام 3: محاسبه گرادیان
گرادیان یعنی مشتقگیری آنهم از نوع جزئی! مشتقگیری از کی نسبت به کی؟! ما باید از تابع اتلاف نسبت به تک تک پارامترهای مدل رگرسیون خطی (یعنی a و b) مشتق بگیریم. یعنی L/∂a∂ و L/∂b∂ . بهتر هست در این پست زیاد وارد مباحث تئوری نشویم. در دو رابطه زیر، مشتقهای L/∂a∂ و L/∂b∂ را مشاهده میکنید که براساس قاعده زنجیرهای مشتق و تابع MSE محاسبه شده است:
پیادهسازی دو رابطه بالا در نامپای بهصورت زیر است:
# Computes gradients for both "a" and "b" parameters a_grad = -2 * error.mean() b_grad = -2 * (x_train * error).mean() print('a_grad: ', a_grad) print('b_grad: ', b_grad)
a_grad: -3.044811379650508 b_grad: -1.8337537171510832
گام 4: آپدیت پارامترهای مدل رگرسیون خطی
بعد از محاسبه مقدار مشتق L/∂a∂ و L/∂b∂، از این گرادیان ها برای آپدیت پارامترها استفاده میکنیم. ما به دنبال حداقل کردن اتلاف هستیم، بنابراین باید به سمت دره حرکت کنیم نه قله! به شکل زیر دقت کنید:
برای حرکت به سمت دره، ابتدا علامت گرادیان را برعکس میکنیم و بعد در نرخ یادگیری (η) ضرب و درنهایت با پارامتر اصلی جمع میکنیم. یعنی به شکل زیر:
احتمالا با نرخ یادگیری هم آشنا هستید. ضریبی که در میزان تغییر پارامترها تاثیرگذار هست. یعنی:
- اگر مقدار نرخ یادگیری بزرگ باشد، میزان تغییر مقدار پارامترها زیاد خواهد بود.
- اگر هم مقدار نرخ یادگیری کوچک باشد، میزان تغییر مقدار پارامترها کم خواهد بود.
پیادهسازی آپدیت پارامترها در نامپای بهصورت زیر است:
lr = 1e-1 # Updates parameters using gradients and the learning rate a = a - lr * a_grad b = b - lr * b_grad print('a: ', a) print('b: ', b)
a: [0.80119529] b: [0.04511107]
خب انتخاب مقادیر نرخ یادگیری خود یک پست جداگانه هست و بهتر هست در اینجا به آن نپردازیم.
گام 5: تکرار فرآیند بهینه سازی
با مراحل 1 تا 4 همه پارامترها یک بار آپدیت شدند. حالا باید به مرحله 1 برگردیم و دوباره فرآیند بهینه سازی را تکرار کنیم. یعنی با پارامترهای جدیدی که برای a و b بدست آمده، بازهم به شبکه ورودی بدهیم و خروجی پیش بینی مدل را بدست آوریم. بعد هم محاسبه اتلاف، محاسبه گرادیان، آپدیت پارامترها و …
بنابراین کدهای چند گام بالا را باید در یک حلقه قرار دهیم تا آنقدر تکرار شود که مقدار اتلاف بسیار کم شود. در زیر تمام کدهای بالا را یکجا آوردم و یک حلقه و کمی کد اضافه هم قرار دادم. حلقه 1000 بار تکرار میشود و انتظار دارم قبل از 1000 تکرار به اتلاف کم برسم و مقدار بهینه برای a و b بدست آید:
# Initializes parameters "a" and "b" randomly np.random.seed(42) a = np.random.randn(1) b = np.random.randn(1) print('Initial value of a: ', a) print('Initial value of b: ', 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('Final value of a: ', a) print('Final value of b: ', b)
seed را برای این درنظر گرفتیم که اعداد تصادفی a و b با هربار اجرای برنامه تغییر نکند.
این هم نتیجه:
Initial value of a: [0.49671415] Initial value of b: [-0.1382643] Final value of a: [1.02354094] Final value of b: [1.96896411]
مشاهده میکنید که مقادیر a و b حدودا برابر با 1 و 2 شده است که انتظارش را داشتیم. حال بیایید خط y=a+bx را روی دادههای ولید بیندازیم و نتیجهاش را ببینیم.
plt.plot(x_val, y_val, 'ro') plt.title('Validation data and reult of the trained regression model') plt.grid() plt.xlabel('x_val') plt.ylabel('y_val') # Final regression model yhat = a + b * x_val plt.plot(x_val, yhat, 'g')
به نظر میرسد توانستیم خودمان یک مدل رگرسیون خطی را پیاده سازی کنیم و آموزش دهیم. نتیجه هم که خوب است…
بسیارخب، بخش تئوری تمام شد. البته، من صرفا مروری بر مباحث تئوری داشتم و هدفم آموزش تئوری بهصورت کامل نبود. چنانچه میخواهید بیشتر درمورد تئوری شبکه عصبی MLP و نحوه آموزش دیدن آن بدانید، میتوانید پستهای اینجا را مطالعه کنید. اما فرآیند تئوری بالا را در نامپای بدون هیچگونه دستور آمادهای پیادهسازی کردم. چرا با نامپای پیادهسازی کردم؟! پس پایتورچ کجاست؟! به دو دلیل تصمیم گرفتم که پیاده سازی با نامپای را قبل از پایتورچ بیاورم. اول اینکه، بعد از تئوری یک لیوان نامپای میچسبد! یعنی حسابی تئوری را برای شما جا میاندازد. حتی امروزه عدهای در کتابهایشان در کنار روابط ریاضی، معادل نامپای را هم مینویسند، چون معتقدند کدهای نامپای در درک بهتر روابط ریاضی کمک میکند. دوم اینکه، در این مرحله کمی سختی بکشید، تا در مرحله بعدی که وارد پایتورچ شدیم بفهمید پایتورچ چقدر زندگی را شیرین میکند… 😀
کم کم به بخشهای مهم پست آموزش pytorch رایگان میرسیم. حالا میخواهم به دو روش، رگرسیون خطی را با پایتورچ پیاده سازی کنم. قبل از اینکه پیاده سازی رگرسیون خطی با پایتورچ را انجام دهم، باید مقدمات پایتورچ را به شما توضیح دهم.
تنسور در پایتورچ
روش اول پیاده سازی رگرسیون خطی در پایتورچ، روشی مشابه نامپای هست. دقت کنید، پایتورچ مجموعه دستورات کاملی مشابه با نامپای دارد. شما به راحتی میتوانید کارهای مشابه در نامپای را با خود پایتورچ انجام دهید. مثلا در پایتورچ تنسور داریم. تنسورها تعریفی مشابه با همان array در نامپای را دارند. در واقع ما به هر آرایهای با هر بُعدی تنسور میگوییم. مثلا یک اسکالر هم یک تنسور صفربعدی محسوب میشود. بردار، یک تنسور یک بعدی و ماتریس یک تنسور دو بعدی است و الی آخر… بنابراین یک آرایه صفر بعدی تا چندبعدی همه را تنسور مینامیم. برای اطلاعات بیشتر درباره انواع تنسورها پست تعریف تنسور در پایتورچ را مطالعه کنید. اما یک نمونه تنسور سه بعدی در ادامه تعریف کرده ام:
torch.tensor([[[1, 2, 8, 2], [3, 0, 9, 9], [7, 2, 0, 6]], [[0, 9, 8, 2], [1, 8, 5, 5], [2, 8, 1, 7]]])
نکته برخلاف نامپای، در پایتورچ میتوانید تمامی محاسبات جبری خود را روی CPU یا GPU انجام دهید. درحالیکه در نامپای چنین اتفاقی ممکن نیست. به صورت پیش فرض، تنسورها روی CPU تعریف میشوند. برای تعریف روی GPU کافی است بنویسید:
torch.tensor([[[1, 2, 8, 2], [3, 0, 9, 9], [7, 2, 0, 6]], [[0, 9, 8, 2], [1, 8, 5, 5], [2, 8, 1, 7]]], device=torch.device('cuda:0'))
در کد بالا، اگر برای ورودی device چیزی ننویسیم، مستقیم روی CPU اجرا میکند، برای اجرا روی GPU، با دستور ()torch.device باید دقیقا تعیین کنم که باید روی GPU اجرا شود. داخل پرانتز نوشتهام: ‘cuda:0’. کلمه cuda که هیچ، اما 0: چیست؟ من مشخص کردهام که این تنسور باید روی GPU شماره 0 اجرا شود. بله، فریمورک پایتورچ قابلیت کار با چند device پردازشی همزمان را دارد. ممکن است چند GPU داشته باشم. روی کدام GPU تنسور باید ساخته شود؟ بسیار خب، حالا خروجی تنسور را ببینیم:
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-6-1d92b8510f7f>
in <module>()
5 [[0, 9, 8, 2],
6 [1, 8, 5, 5],
----> 7 [2, 8, 1, 7]]], device=torch.device('cuda'))
/usr/local/lib/python3.6/dist-packages/torch/cuda/__init__.py
in _lazy_init()
188 raise AssertionError(
189 "libcudart functions unavailable. It looks like you have a broken build?")
--> 190 torch._C._cuda_init()
191 # Some of the queued calls may reentrantly call _lazy_init();
192 # we need to just return without initializing in that case.
RuntimeError: cuda runtime error (100) : no CUDA-capable device is detected at /pytorch/aten/src/THC/THCGeneral.cpp:47
خطا! در این مواقع بسیاری از دوستان دست از کار میکشند، چون با خطا مواجه شدهاند! تعداد خطوط خطا هم که بسیار زیاد هست، پس هیچی… اگر شما جزو این دسته افراد هستید، کافی است به خط آخر خطا نگاه کنید. همانجایی که RuntimeError با رنگ قرمز نوشته شده است. اول گفته خطا از cuda هست. بعد از : گفته no CUDA-capable device is detected، با اندکی انگلیسی دست و پا شکسته یا حتی در بدترین شرایط استفاده از گوگل ترنسلیت، به راحتی میتوان فهمید که منظور پیغام این است: “هیچ دیوایس سازگار با کودا پیدا نکردم”. بله، اصلا GPU پیدا نکرده است. من از گوگل کولب استفاده میکنم و یادم رفته بود که دیوایس را روی GPU قرار دهم. حالا اینکار را انجام میدهم و دوباره برنامه را اجرا میکنم. خروجی را ببینید:
tensor([[[1, 2, 8, 2], [3, 0, 9, 9], [7, 2, 0, 6]], [[0, 9, 8, 2], [1, 8, 5, 5], [2, 8, 1, 7]]], device='cuda:0')
راستی، وقتی فقط یک GPU دارید، میتوانید بجای ‘cuda:0’، فقط بنویسید ‘cuda’.
ترفند با استفاده از دستور ()torch.cuda.is_available میتوانید چک کنید که GPU شما در دسترس هست یا خیر…
ترفند با استفاده از دستور torch.cuda.get_device_properties(‘cuda:0’) میتوانید مشخصات GPU را مشاهده کنید. این دستور در گوگل کولب خیلی کاربرد دارد. خروجی این دستور را در کولب ببینید:
_CudaDeviceProperties(name='Tesla K80', major=3, minor=7, total_memory=11441MB, multi_processor_count=13)
نکته برای تغییر دیتاتایپ یک تنسور کافی است از اتریبیوتهای دیتاتایپ استفاده کنید. مثلا، برای تبدیل یک تنسور از int به یک تنسور float کافی است بنویسید:
a = torch.randint(10, (2, 3), device=torch.device('cuda')) print('a: ', a) print('a: ', a.dtype) print(50*'-') b = a.float() print('b: ', b) print('b: ', b.dtype)
a: tensor([[0, 2, 2], [9, 6, 3]], device='cuda:0') a: torch.int64 -------------------------------------------------- b: tensor([[0., 2., 2.], [9., 6., 3.]], device='cuda:0') b: torch.float32
نکته آیا راهی برای تبدیل نامپای به تنسور یا برعکس وجود دارد؟ بله. برای تبدیل یک نامپای میتوانید از دستور ()torch.from_numpy استفاده کنید:
np_array = np.random.randint(0, 10, (2, 3)) print(np_array) print(50*'-') tensor = torch.from_numpy(np_array) print(tensor)
[[7 7 6] [2 4 0]] -------------------------------------------------- tensor([[7, 7, 6], [2, 4, 0]])
برای تبدیل یک تنسور به نامپای کافی است بنویسید:
tensor = torch.from_numpy(np_array) print(tensor) print(50*'-') np_array = tensor.numpy() print(np_array)
tensor([[7, 7, 6], [2, 4, 0]]) -------------------------------------------------- [[7 7 6] [2 4 0]]
هشدار دقت کنید، نامپای از GPU پیشتیانی نمیکند. بنابراین، اگر قرار باشد یک تنسور در GPU را به نامپای تبدیل کنید با چنین خطایی مواجه خواهید شد:
TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
برای حل مشکل، ابتدا تنسور را از GPU به CPU انتقال دهید و سپس به نامپای تبدیل کنید. برای انتقال هم میتوانید به سادگی از ()to. استفاده کنید. اینگونه:
a = torch.randint(10, (2, 3), device=torch.device('cuda')) print(a) print(50*'-') # Method 1 b = a.to('cpu').numpy() print(b) print(50*'-') # Method 2 c = a.cpu().numpy() print(c)
tensor([[7, 4, 4], [8, 7, 2]], device='cuda:0') -------------------------------------------------- [[7 4 4] [8 7 2]] -------------------------------------------------- [[7 4 4] [8 7 2]]
بسیار خب، با یادگیری اندک نکاتی درباره تنسور در پایتورچ، میتوانیم کد نامپای رگرسیون خطی را یک بار دیگر با پایتورچ پیاده سازی کنیم. توضیح زیادی دراین باره نمیدهم، چون خودتان میبینید که چندان فرقی با نامپای ندارد.
# Converts numpy arrays to tensor x_train = torch.from_numpy(x_train) y_train = torch.from_numpy(y_train) # Initializes parameters "a" and "b" randomly torch.manual_seed(42) a = torch.randn(1) b = torch.randn(1) print('Initial value of a: ', a) print('Initial value of b: ', 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('Final value of a: ', a) print('Final value of b: ', b)
من میتوانم کد بالا را روی GPU هم اجرا کنم. اینجا محاسبات زیاد نیست، بنابراین نمیتوان یک مقایسه معنیدار بین سرعت اجرا روی CPU و GPU انجام داد.
اما مزیت اصلی پایتورچ را هنوز نگفتهام. اینکه بتوانیم محاسبات جبری را روی GPU انجام دهیم، مزیت هست. ولی مزیت بزرگ پایتورچ، امکانات فراوان در آموزش شبکه های عصبی است. تا اینجا همیشه من رابطه آپدیت شدن پارامترها و گرادیان اتلاف نسبت به پارامترها را نوشتهام. اما پایتورچ کار را راحت کرده و میگوید کار مشتقگیری و بروزرسانی را به من بسپارید! در بخش بعدی آموزش pytorch رایگان با مشتق گیری خودکار در پایتوررچ آشنا خواهید شد.
مشتق گیری در پایتورچ
پایتورچ یک پکیج مشتق گیری خودکار به نام اتوگراد autograd دارد. با این autograd دوست داشتنی نیازی نیست نگران مشتق گیری باشید. من میخواهم از این ویژگی اتوگراد استفاده کنم تا برایم گرادیان پارامترهای مدل رگرسیون خطی را خودکار حساب کند. یعنی نیاز نباشد که من از رابطه (3) برای محاسبه گرادیان استفاده کنم. پایتورچ چه دستوری برای مشتق گیری خودکار دارد؟ backward
مردم یادتان میآید که برای محاسبه گرادیان از چه رابطه ای مشتق گرفتیم و چه مشقتی کشیدیم؟! به رابطه (3) نگاه کنید؛ از تابع اتلاف نسبت به تک تک پارامترها باید مشتق گرفته شود. حالا برای محاسبه گرادیان همه پارامترهای یک مدل یا شبکه عصبی، کافی است بنویسید:
loss.backward()
تمام! به همین سادگی با دستور بالا، گرادیان همه پارامترها محاسبه میشود. چطور مشتق میگیرد؟ بیایید یک مثال ساده حل کنیم. من در کد زیر یک متغیر تصادفی تعریف کردهام. همچنین y را براساس x تعریف کردهام. میدانیم که مشتق y نسبت به x میشود 2. برای بدست آوردن این عدد کافی است بنویسید:
x = torch.randn(1, requires_grad=True) print('x: ', x) y = x**2 print('y: ', y) y.backward()
و خروجی:
x: tensor([-0.4310)
y: tensor([0.1858])
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-3-afc8455384e1> in <module>()
3 print(y)
4
----> 5 y.backward()
1 frames
/usr/local/lib/python3.6/dist-packages/torch/autograd/__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables)
125 Variable._execution_engine.run_backward(
126 tensors, grad_tensors, retain_graph, create_graph,
--> 127 allow_unreachable=True) # allow_unreachable flag
128
129
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
نشد! با خطا مواجه شدیم. چرا؟ دلیلش ساده هست. پایتورچ میداند که باید از y مشتق بگیرد. اما نسبت به چه مشتق بگیرد؟ ببینید، از نظر backward فرقی بین یک عدد رندوم x با عددی ثابت مانند 2 وجود ندارد. یعنی میخواهم بگویم که آنرا به چشم عدد ثابت میبیند. باید با روشی به backward بفهمانیم که این عدد ثابت نیست و یک پارامتر هست. برای این کار کافی است از ویژگی requires_grad استفاده کنید. از نامش مشخص هست که میگوید میخواهم گرادیان این متغیر حساب شود. اتفاقا به RuntimeError نگاه کنید؛ دقیقا گفته شده که تنسور با require grad ندارد.
اما چطور requires_grad را برای متغیر x فعال کنیم؟ این عبارت یکی از ورودیهای ()torch.tensor است. یعنی باید تنسور x را بهاینصورت تعریف کنیم:
x = torch.randn(1, requires_grad=True)
و حالا مشتق میگیرم:
y = x**2 y.backward()
درست شد. اما چگونه مقدار مشتق y/∂x∂ را ببینیم. با استفاده از grad. مقدار گرادیان هر پارامتر را ببینید. این هم خروجی:
x.grad
tensor([-0.8621])
مقدار بالا، دقیقا همان 2x است و بهدرستی محاسبه شده است.
ترفند راستی، به روش زیر هم میتوانید requires_grad در تنسور دلخواهتان را True/False کنید:
x = torch.randn(1) x.requires_grad_(True) print(x)
مدل رگرسیون خطی ما فقط دو پارامتر دارد، به همین خاطر شاید اهمیت مشتق گیری خودکاری درک نشود. اما تصور کنید، یک شبکه عصبی چند لایه کانولوشنی مانند VGG دارید که 130 میلیون پارامتر دارد. حالا شما باید بهصورت دستی مشتق هریک از این پارامترها را محاسبه کنید.
تمرین 3: از عبارت (y=x^3+e^x+5/z) نسبت به x مشتق بگیرید.
بهینه سازی در پایتورچ
بسیارخب، تا الان ما با استفاده از رابطه (4) پارامترها را آپدیت کردهایم. برای آپدیت کردن، از گرادیان استفاده کردهایم. اتفاقا گرادیان را هم با رابطه (3) به صورت دستی استفاده کرده بودیم. اما در بخش قبلی یاد گرفتید که چگونه با استفاده از backward گرادیان پارامترها را محاسبه کنید. دراین بخش میخواهم به ویژگی جدیدی اشاره کنم که کار آپدیت کردن پارامترها را برای ما انجام دهد. یعنی از دست رابطه (4) هم خلاص شویم.
خلاصه کار بهینه ساز در پایتورچ بهاینصورت است که پارامترها را از ما میگیرد و براساس الگوریتمهایی مانند SGD، Adam و غیره و همچنین هایپرپارامترها، پارامترها را بروزرسانی میکند. پس، اول باید یک بهینه ساز تعریف کنیم (مثلا Adam SGD و غیره). من در دستور زیر، یک بهینه ساز بنام SGD ساختهام.
torch.manual_seed(42) a = torch.randn(1, requires_grad=True) b = torch.randn(1, requires_grad=True) optimizer = torch.optim.SGD([a, b], lr=0.01) print(optimizer)
SGD ( Parameter Group 0 dampening: 0 lr: 0.01 momentum: 0 nesterov: False weight_decay: 0 )
در دستور بالا، ورودی اول پارامترها را از من میخواهد. پارامترهای من a و b است. ورودی دوم از من نرخ یادگیری را میخواهد که خب کافی است به رابطه (4) نگاه کنید تا یادتان بیاید که خیلی مهم است. بسیار خب، optimizer یک آبجکت است که بعدا با آن میخواهیم پارامترها را آپدیت کنیم. برای آپدیت کردن کافی است بنویسید:
optimizer.step()
همین؟ بله، منظور از دستور بالا این است که optimizer (براساس الگوریتم SGD و هایپرپارامترهایی که برایت تعریف کردیم) لطفا یک بار بروزرسانی پارامترها را انجام بده.
نکته در بخش torch.optim میتوانید انواع بهینه سازهای پایتورچ را مشاهده کنید.
بسیار خب، میخواهم مشتق گیری خودکار و بهینه ساز را به کد پایتورچ اضافه کنم. پس بخشهایی از مشتق گیری دستی و بروزرسانی پارامترها را حذف کردهام و جایگزینی راحت برای آنها قرار دادهام. ببینید:
# Converts numpy arrays to tensor x_train = torch.from_numpy(x_train) y_train = torch.from_numpy(y_train) # Initializes parameters "a" and "b" randomly torch.manual_seed(42) a = torch.randn(1, requires_grad=True) b = torch.randn(1, requires_grad=True) print('Initial value of a: ', a) print('Initial value of b: ', b) # Sets learning rate lr = 1e-1 # Defines optimizer optimizer = torch.optim.SGD([a, b], lr=lr) # 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() loss.backward() # simple! # Updates parameters using gradients and the learning rate # a = a - lr * a_grad # b = b - lr * b_grad optimizer.step() # simple! # Resets gradients! optimizer.zero_grad() print('Final value of a: ', a) print('Final value of b: ', b)
کد بالا را ببینید؛ این کد جدیدی نیست. اما اجازه دهید بهصورت مختصر در مورد هر خط توضیح دهم:
- در خط 2 و 3، دادههای ورودی و لیبل از نامپای به تنسور تبدیل شدهاند.
- در خط 7 و 8، دو پارامتر مدل رگرسیون خطی را تعریف کردهام. نکته جدید نسبت به قبل این هست که هردو requires_grad=True اضافه کردم. چون بتوانم از آنها مشتق بگیرم.
- در خط 14، مقدار نرخ یادگیری را تعریف کردهام.
- در خط 17، بهینه ساز از نوع SGD تعریف کردهام. ورودی اول پارامترهای مدل رگرسیون خطی و ورودی دوم نرخ یادگیری است. این دستور جدید هست و قبلا ندیده بودید.
- در خط 20، تعداد تکرار حلقه تعریف شده است.
- در خط 22، وارد حلقه for برای انجام فرآیند آموزش میشویم!
- در خط 24، ورودی آموزش را به مدل رگرسیون خطی میدهم و خروجی yhat را میگیرم.
- در خط 27 و 29، مقدار اتلاف محاسبه میشود. این خط هم قبلا وجود داشت.
- دو خط 32 و 33 را کامنت کردم و قبلا وجود داشت. بجای این دو خط میخواهم از قابلیت خفن پایتورچ استفاده کنم.
- در خط 34، دستور جایگزین دو خط 32 و 33 را نوشتهام. دستور ()loss.backward مقدار گرادیان را محاسبه میکند.
- دو خط 37 و 38 را کامنت کردم و دستور جایگزین ()optimizer.step را نوشتم. با این دستور، پارامترها آپدیت میشود.
- اما برسیم به خط 42! تا الان درباره این خط توضیح ندادم. خلاصه بگویم که این خط گرادیان های محاسبه شده برای پارامترهای (a,b) را ریست یا صفر میکند. اما درادامه یک مثال خوب آوردم که کارکرد zero_grad را متوجه شوید.
در زیر من یک تنسور با مقدار 1.0 و گرادیان روشن تعریف کردهام.
x = torch.tensor(1., requires_grad=True)
حالا یک تابع y تعریف میکنم و از آن نسبت به x مشتق میگیرم:
# First try y = x**3 y.backward() print('Gradient: ', x.grad)
خب واضح است که مشتق 3x^2 میشود و بنابراین مقدار مشتق برابر با 3 خواهد بود:
Gradient: tensor(3.)
تا اینجا همه چیز درست است. اما، بیایید برای بار دوم، کد بالا را اجرا میکنم:
# Second try y = x**3 y.backward() print('Gradient: ', x.grad)
حالا بازهم نتیجه 3 میشود؟ ببینید:
Gradient: tensor(6.)
خب 6 شد! چرا؟ این عدد 6 معادل با دو عدد 3 است! یعنی گرادیان اولین بار هنوز پاک نشده و بعد که گرادیان دومین بار را گرفتم، با اولی جمع شد. یعنی ما ویژگی گرادیان تجمعی داریم. مثلا برای بار سوم گرادیان بگیرم، مقدار برابر با 9 خواهد شد. اگر 100 بار گرادیان بگیرم، مقدار 300 میشود. به همین دلیل، از zero_grad استفاده میکنیم که گرادیانها را بعد از هربار آپدیت وزنها صفر یا ریست کند.
نکته چرا اصلا باید گرادیانها را جمع کند؟ خب این zero_grad را هم داخل همان ()optimizer.step میگذاشتند که ما ننویسیم! پایتورچ ریست و صفر کردن گرادیان ها را به شما میسپارد، به شما توانایی انجام کارهای پیچیدهتر را میدهد. اما توضیح بیشتر در مورد کاربردش در این مقال نمیگنجد!
اگر به کد آموزش pytorch نگاه کنید، بخشی از آنها را ساده کردیم. اما هنوز تابع اتلاف ما دستی پیاده سازی شده. سوال این است نمیتوان برای آن هم از یک تابع آماده در پایتورچ استفاده کرد؟ میشود، بخش بعدی آموزش pytorch رایگان به تابع اتلاف در پایتورچ اختصاص دارد.
تابع اتلاف در پایتورچ
اتلاف یکی دیگر از بخشهای مهم در شبکه عصبی است. به شکل 1 دقت کنید؛ از تابع اتلاف برای بررسی میزان فاصله بین خروجی شبکه و خروجی ایدهآل استفاده میشود. توابع اتلاف زیادی مانند MSE MAE و غیره وجود دارد. خوشبختانه در پایتورچ طیف وسیعی از توابع اتلاف پیاده سازی شده و به راحتی میتوانیم از آنها استفاده کنیم. مساله ما در اینجا، رگرسیون است و به همین دلیل میخواهم از MSE استفاده کنم.
تمامی مجموعه توابع اتلاف پایتورچ در مسیر torch.nn وجود دارد. به عنوان نمونه، با استفاده از دستور torch.nn.MSELoss یک آبجکت تابع اتلاف MSE میسازم و از آن در محاسبه اتلاف مدل رگرسیون خطی استفاده میکنم. در کد زیر یک تابع اتلاف تعریف کردهام و دو تنسور تصادفی ساختم. براساس تابع اتلاف و تنسورها، مقدار اتلاف را محاسبه کردهام:
loss = torch.nn.MSELoss() input = torch.randn(3, 5, requires_grad=True) target = torch.randn(3, 5) output = loss(input, target) print(output)
این هم مقدار اتلاف:
tensor(2.8259, grad_fn=<MseLossBackward>)
حالا میخواهم کد آموزش pytorch را آپدیت کنم. بخش اتلاف دستی را حذف کنم و به تابع اتلاف پایتورچی آماده اضافه کنم. ببینید:
# Converts numpy arrays to tensor x_train = torch.from_numpy(x_train) y_train = torch.from_numpy(y_train) # Initializes parameters "a" and "b" randomly torch.manual_seed(42) a = torch.randn(1, requires_grad=True) b = torch.randn(1, requires_grad=True) print('Initial value of a: ', a) print('Initial value of b: ', b) # Sets learning rate lr = 1e-1 # Defines optimizer optimizer = torch.optim.SGD([a, b], lr=lr) # Defines MSE loss loss_fn = torch.nn.MSELoss() # 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() loss = loss_fn(yhat, y_train) # Computes gradients for both "a" and "b" parameters # a_grad = -2 * error.mean() # b_grad = -2 * (x_train * error).mean() loss.backward() # simple! # Updates parameters using gradients and the learning rate # a = a - lr * a_grad # b = b - lr * b_grad optimizer.step() # simple! # Resets gradients! optimizer.zero_grad() print('Final value of a: ', a) print('Final value of b: ', b)
در کد بالا، در خط 20 تابع اتلاف MSE را تعریف کردهام. در خط 33 هم از آن استفاده کردهام و دو ورودی شامل خروجی پیش بینی yhat و لیبل y_train را به عنوان ورودی به آن دادهام. این هم خروجی:
Initial value of a: tensor([0.3367], requires_grad=True) Initial value of b: tensor([0.1288], requires_grad=True) Final value of a: tensor([1.0235], requires_grad=True) Final value of b: tensor([1.9690], requires_grad=True)
خب یک بخش دیگر را هم با دستورات پایتورچ ساده کردیم. اما مدل رگرسیون خطی ما هم دستی نوشته شده است. نمیتوان با پایتورچ یک نورون ساخت؟ میشود، در بخش بعدی آموزش pytorch رایگان به موضوع مهم ساخت و تعریف مدل پرداختهام.
تعریف مدل در پایتورچ
وقتی صحبت از تعریف مدل رگرسیون خطی است، جای نگرانی نیست و در یک خط بدون استفاده از دستور آمادهای این کار را انجام میدهیم. اما ما قرار است از مدلهای بزرگ استفاده کنیم، بنابراین باید بیاموزیم که چگونه میتوان از قابلیتهای تعریف مدل در پایتورچ استفاده کرد.
دو روش رایج برای تعریف مدل در پایتورچ وجود دارد:
- nn.Sequential
- nn.Module
همین اول بگویم که sequential صرفا برای تعریف مدلهای ساده، کوچک با لایههای ترتیبی است. گاهی از همین squential برای تعریف یک ماژول در روش module استفاده میشود. خلاصه اینکه، اگر میخواهید چند لایه فولی کانکتد، کانولوشنی و غیره پشت سر هم بچینید، sequential بد نیست. در ادامه، در مورد هر دو روش sequential و module توضیح دادم.
-
تعریف مدل با پایتورچ با استفاده از nn.Sequential
برای شما یک نمونه تمپلیت از نحوه تعریف مدل در پایتورچ با squential آوردم. ببینید:
model = torch.nn.Sequential( nn.ABC(n_inputs, param1), nn.DEF(), nn.GHI() )
در کد بالا ABC DEF GHI یکسری لایه هستند که پشت سر هم قرار گرفتهاند. الان بهتر میتوانید درک کنید که این لایهها صرفا ترتیبی چیده شدهاند و هیچ ارتباطی بین ABC و GHI نمیتوان ایجاد کرد. اما شبکههای امروزی شبیه ResNext علاوه بر عمق در راستای عرض هم چندشاخهای شدهاند. چنین شبکههایی را نمیتوان با sequential پیاده سازی کرد.
برای ساخت یک لایه فولی کانکتد در پایتورچ میتوان از دستور torch.nn.Linear استفاده کرد. در این دستور باید حداقل دو پارامتر تعداد ورودی و تعداد خروجی (تعداد نورون) مشخص شود. در زیر کد یک مدل دولایه را آوردم. همچنین activation function هم بعد هر لایه قرار دادم.
model = nn.Sequential( nn.Linear(10, 30), nn.Sigmoid(), nn.Linear(30, 2), nn.Softmax() ) print(model)
یک توضیح مختصر درباره کد بالا:
- در خط 1، nn.Sequential پرانتز باز!
- در خط 2، تعریف یک لایه فولی کانکتد با 10 ورودی و 30 خروجی (نورون). بنابراین، سایز ورودی مدل ما 10 است.
- در خط 3، از سیگموید بهعنوان activation function استفاده شده است.
- در خط 4، بازهم از لایه فولی کانکتد با 30 ورودی و 2 خروجی (نورون) استفاده شده است. واضح است که تعداد ورودی باید 30 باشد، خروجی لایه قبلی 30 بوده است. از طرفی تعداد خروجی مدل 2 است. مثلا، میتواند یک مدل کلاسیفایر با 10 ورودی و 2 خروجی (کلاس) باشد.
- در خط 5، softmax برای نرمالیزه کرده خروجی و …
بسیارخب، برویم سراغ مدل رگرسیون خطی؛ قطعا این مدل ساده را با sequential و nn.Linear میتوان پیاده کرد. به همین سادگی:
lreg_model = torch.nn.Sequential( torch.nn.Linear(1, 1) ) print(lreg_model)
Sequential( (0): Linear(in_features=1, out_features=1, bias=True) )
یک ورودی و یک خروجی و تمام… اما چطور این مدل را در کدهایمان قرار دهیم و آن مدل دستی قبلی را حذف کنیم؟ به کد زیر نگاه کنید:
# Converts numpy arrays to tensor x_train = torch.from_numpy(x_train).float() y_train = torch.from_numpy(y_train).float() # Initializes parameters "a" and "b" randomly # torch.manual_seed(42) # a = torch.randn(1, requires_grad=True) # b = torch.randn(1, requires_grad=True) # print('Initial value of a: ', a) # print('Initial value of b: ', b) # Sets learning rate lr = 1e-1 # Defines model with nn.Sequential lreg_model = torch.nn.Sequential( torch.nn.Linear(1, 1) ) lreg_model.train() # Defines optimizer optimizer = torch.optim.SGD(lreg_model.parameters(), lr=lr) # Defines MSE loss loss_fn = torch.nn.MSELoss() # Defines number of epochs n_epochs = 1000 for epoch in range(n_epochs): # Computes our model's predicted output # yhat = a + b * x_train yhat = lreg_model(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() loss = loss_fn(yhat, y_train) # Computes gradients for both "a" and "b" parameters # a_grad = -2 * error.mean() # b_grad = -2 * (x_train * error).mean() loss.backward() # simple! # Updates parameters using gradients and the learning rate # a = a - lr * a_grad # b = b - lr * b_grad optimizer.step() # simple! # Resets gradients! optimizer.zero_grad() # print('Final value of a: ', a) # print('Final value of b: ', a) print(lreg_model.state_dict())
اما توضیحات کد بالا:
- من با استفاده از امکانات پایتورچ مدل ساختم، بنابراین دیگر نیازی به seed و a و b ندارم. به همین دلیل، خطوط 6 الی 11 را کامنت کردم.
- در خط 17، مدل رگرسیون خطی را با استفاده از sequential ساختم. یک دانه نورون! نام مدلی که ساختم lreg_model است.
- در خط 23، بجای تعریف [a b] به عنوان پارامترها، از دستور ()lreg_model.parameters استفاده کردم. طبیعی است، باید پارامترهای مدل را به بهینه ساز بدهم نه [a b]…
- خط 33 را کامنت کردم (مدل قبلی).
- خط 34، مدل جدید را فراخوانی کردم و بسیار ساده به آن یک ورودی دادهام.
- خطوط 55 و 56 را کامنت کردم. در خط 57 پارامترهای مدل را پرینت کردم. برای مشاهده پارامترهای مدل کافی است از ()lreg_model.state_dict استفاده کنید. ببینید چه خفن خروجی را نمایش میدهد: 😎
OrderedDict([('0.weight', tensor([[1.9690]])), ('0.bias', tensor([1.0235]))])
طبیعتا میدانید که شبکه های عصبی امروزی بزرگتر از این حرفها هستند و مثلا یک شبکه عصبی چندلایه ای چطور باید ساخت؟ شما میتوانید شبکه های عصبی پیچیده را با روش module بسازید. در بخش بعدی در این مورد توضیح دادم.
-
تعریف مدل با پایتورچ با استفاده از nn.Module
برای ساخت یک مدل به روش module باید یک کلاس پایتونی بنویسید. یک کلاس که دو تابع اصلی init و forward دارد. نترسید، بسیار ساده است. در زیر یک تمپلیت قرار دادم. شما میتوانید این تمپلیت را داشته باشید و همیشه کدهایتان را در همین مدل ساده بنویسید.
class ABC(nn.Module): def __init__(self, param1, param2, param3): # execute super class's __init__() super().__init__() # Instanciate nn.Module class and assign as a member def forward(self, x): # write the sequence of layers and processes return x
با جزئیات در مورد تمپلیت بالا توضیح میدهم. اما من به دانشجوهایم در کلاس میگویم: تعریف مدل با کلاس بالا شبیه آشپزی است. آشپزی شامل دو مرحله است: اول، تهیه مواد اولیه و دوم، پختن و ترکیب مواد اولیه! مشابه با این مثال، در کلاس بالا هم شما ابتدا تمامی ماژولها و لایههای لازم را در تابع __init__ تعریف میکنید و میسازید. سپس، در تابع forward این ماژولها و لایهها را کنار هم میچینیم و بعد خروجی نهایی شبکه را return میکنیم.
اما توضیح در مورد تمپلیت بالا:
- بعد از class، نام دلخواهتان را بنویسید.
- داخل پرانتز جلوی ABC، باید nn.Module باشد. همیشه!
- در خط 2، تابع __init__ را برای تعریف لایه و ماژول بنویسید. همیشه __init__! اولین ورودی همیشه self هست و سایر ورودیها، ورودی دلخواه شماست.
- خط بعدی super مربوط به nn.Module است و نیازی نیست جدی بگیرید. همانطور که نوشته شده، بگذارید بماند!
- بعد از کامنت، لایه و ماژولهایتان را تعریف میکنید.
- در تابع forward، ماژول و لایههای مدنظر را به هر شکلی که میخواهید بچینید.
- ورودی تابع forward، معمولا همان دادههای ورودی (آموزش یا ارزیابی) است.
- بعد از کامنت میتوانید شروع کنید به چینش لایه و ماژولها…
همان مدل دو لایه MLP که با sequential ساختم را در کد زیر با module ساختهام.
class NeuralNetwork(torch.nn.Module): def __init__(self, n_input, n_unit1, n_output): super().__init__() # Inputs to 1st hidden layer linear transformation self.hidden = torch.nn.Linear(n_input, n_unit1) self.sigmoid = torch.nn.Sigmoid() # Output layer self.output = torch.nn.Linear(n_unit1, n_output) self.softmax = torch.nn.Softmax(dim=1) def forward(self, x): x = self.hidden(x) x = self.sigmoid(x) x = self.output(x) x = self.softmax(x) return x
اما توضیحات کد بالا:
- در تابع __init__، دو لایه فولی کانکتد و activation function تعریف کردم. برای هرکدام یک اسم مانند hidden output و غیره ساختم. اما برای اینکه بتوانم در تابع دیگری در همین کلاس این لایهها را فراخوانی کنم، از self. استفاده کردم.
- در بخش forward، خیلی ساده اینها را پشت سر هم چیدم. فکر میکنم خیلی واضح است.
حال چگونه از این کلاس استفاده کنم؟ طبیعتا وقتی کلاس میسازیم، اول باید یک آبجکت تعریف کنیم. این از تعریف آبجکت:
nn_model = NeuralNetwork(10, 30, 2) print(nn_model)
NeuralNetwork( (hidden): Linear(in_features=10, out_features=30, bias=True) (sigmoid): Sigmoid() (output): Linear(in_features=30, out_features=2, bias=True) (softmax): Softmax(dim=1) )
حالا باید به این آبجکت ورودی بدهم. پس یک ورودی تصادفی x میسازم و به مدل بالا میدهم تا خروجی را مشاهده کنم.
x = torch.randn(20, 10) y = nn_model(x) print(y.shape)
دقت کنید، اگر یک شبکه با 10 لایه هم داشته باشید، به راحتی در بخش init، ابتدا 10 لایه را بسازید. سپس، در بخش forward آنها را بهم بچسبانید. هرطوری که دلتان میخواهد آنها را بهم بچسبانید.
در زیر مدل رگرسیون خطی را با استفاده از module ساختهام. توضیح خاصی ندارد، یک لایه Linear ساختهام و در بخش forward به آن ورودی میدهم.
class LinRegModel(torch.nn.Module): def __init__(self): super().__init__() # Inputs to 1st hidden layer linear transformation self.neuron = torch.nn.Linear(1, 1) def forward(self, x): y = self.neuron(x) return y
lreg_model2 = LinRegModel() print(lreg_model2)
LinRegModel( (neuron): Linear(in_features=1, out_features=1, bias=True) )
حالا با کد بالا میخواهم کدهای پایتورچ را آپدیت کنم. بخشهای sequential را حذف کردم و بجای آن کدهای بالا را گذاشتم.
# Converts numpy arrays to tensor x_train = torch.from_numpy(x_train).float() y_train = torch.from_numpy(y_train).float() # Initializes parameters "a" and "b" randomly # torch.manual_seed(42) # a = torch.randn(1, requires_grad=True) # b = torch.randn(1, requires_grad=True) # print('Initial value of a: ', a) # print('Initial value of b: ', b) # Sets learning rate lr = 1e-1 # Defines model with nn.Sequential class LinRegModel(torch.nn.Module): def __init__(self): super().__init__() # Inputs to 1st hidden layer linear transformation self.neuron = torch.nn.Linear(1, 1) def forward(self, x): y = self.neuron(x) return y lreg_model2 = LinRegModel() lreg_model2.train() # Defines optimizer optimizer = torch.optim.SGD(lreg_model2.parameters(), lr=lr) # Defines MSE loss loss_fn = torch.nn.MSELoss() # Defines number of epochs n_epochs = 1000 for epoch in range(n_epochs): # Computes our model's predicted output # yhat = a + b * x_train yhat = lreg_model2(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() loss = loss_fn(yhat, y_train) # Computes gradients for both "a" and "b" parameters # a_grad = -2 * error.mean() # b_grad = -2 * (x_train * error).mean() loss.backward() # simple! # Updates parameters using gradients and the learning rate # a = a - lr * a_grad # b = b - lr * b_grad optimizer.step() # simple! # Resets gradients! optimizer.zero_grad() # print('Final value of a: ', a) # print('Final value of b: ', a) print(lreg_model2.state_dict())
در مورد کدهای بالا توضیحی نمیدهم، چون بسیار شبیه حالت sequential هست.
بسیارخب، اگرچه آموزش pytorch رایگان تمام شد، اما واقعا تمام نشده! چون مثلا موارد زیر را هنوز توضیح ندادم:
- نحوه ساخت دیتاست سفارشی (custom dataset)
- نحوه استفاده از دیتالودر
- نحوه بچبندی و بهمریختن دادهها
- نحوه ارزیابی مدل
- نحوه ذخیره مدل آموزش دیده
خیلی طولانی شد و فعلا خسته هستم! اما اگر این آموزش موردتوجه شما قرار گرفت و فیدبک دادید، برمیگردم و ادامه این آموزش را مینویسم. تلاشم را کردم که با استفاده از منابع موجود و فکر زیاد، آموزشی آماده کنم که مفید باشد. امیدوارم که اینطور باشد…
.
مطالب زیر را حتما مطالعه کنید
دستورهای GPU در پایتورچ که همه باید بدانند
آموزش پردازش زبان طبیعی در پایتورچ
عملیات روی تنسورها در پایتورچ
اندیس گذاری در پایتورچ
تنسور تصادفی در پایتورچ
تنسورهای خاص در پایتورچ
68 دیدگاه
به گفتگوی ما بپیوندید و دیدگاه خود را با ما در میان بگذارید.
خیلی عالی بود. خیلی خیلی ممنونم
اگر براتون مقدور هست، ادامه آموزش هم برامون بذارید.
فقط میتونم بگم خدا خیرتون بده
بینظیر بود
عاااالی بود
سلام.
عالی بود . خیلی بکارم اومد. ممنون از وقتی که گذاشتین 🙂
سلام،
آموزش بسیار روان و عالی بود. بخصوص اینکه روش های مختلف کدنویسی با یکدیگر مقایسه شده اند. بسیار از شما ممنونم.
فقط به نظر بنده ترجمه gradient descent باید کاهش گرادیانی باشد، نه گرادیان کاهشی. چرا که به در واقع با استفاده از گرادیان به سمت نقطه ی کمینه حرکت می کنیم پس کاهش گرادیانی انجام می دهیم. باز هم از شما ممنونم
سلام
ما این عبارت رو ترجمه نکردیم. در منابع فارسی، مرسوم هست که به Gradient Descent گرادیان کاهشی و به Gradient Ascent گرادیان افزایشی گفته میشه.
خیلی آموزش روان و خوبی بود. شاید بهترین آموزشی که دیده بودم!
دستتون درد نکنه.
موفق باشید.
بی نظیر بود آموزش مرسی بابت وقت و حوصله ای که میزارید. منکه لذت بردم
با سلام
بسیار عالی
خداوند به شما خیر و برکت عطا کند
سپاسگزارم.
انصافا و بدون اغراق آموزش خیلی خوبی بود. بسیار روان ترجمه کردین و کاملا مشخصه که تلاش زیادی داشتین تا مطالب تکمیلی رو اضافه کنین.
جالبه که در انتهای صفحه آدرس شما رو دیدم و متوجه شدم هم استانی هستیم. بهتون تبریک میگم.
ممنون از حسن توجه شما 🙏
موفق باشید 🌹
حقیقتا به ایرانی بودن شما افتخار کردم .کار پر زحمتی است .ولی برای ما که میخونیم لذت بخشه .خدا قوت و دستمریزاد
عالی بود واقعا. خدا خیرتون بده. انشالله که آموزش هاتون ادامه داشته باشه.
بسیار عالی و حرفه ای بود.
خیلی ممنون از این که مطالب و تجاربتون رو به صورت صادقانه و با دقت ارائه می دهید .
بسیار عالی بود
متشکرم
تشکر خیلی مفید بود
سلام
بسیار عالی توضیح دادید
من همه کدها رو خودم پیاده سازی کردم همگی بدون خطا اجرا شدند
کمتر پیش میاید که کدهای آموزشی بدون خطا باشند
خیلی وقت گذاشتید
ان شاا… ادامه دهید
آقا بهترین آموزشی بود که دیدم توی نت
تشکر
عالی
سلام به نویسنده گرامی
خدا قوت…
مشتی هستی.
دم شما گرم با این توضیحات روان و ساده
ممنون 🌹🙏
سلام
وقتتون بخیر
یه خطا در کدهای پایتورچ دارم می تونید راهنماییم کنید.
invalid index of a 0-dim tensor. Use tensor.item() to convert a 0-dim tensor to a Python number
کجا می تونم این سوال رو بپرسم؟
سلام
متغیر شما یک تنسور صفربعدی هست. یعنی یک اسکالر هست و به همین خاطر، شما نمیتونید به این متغیر اندیس بدید. اگر مثلا میخوایید از مقدار متغیر a استفاده کنید، باید ()a.item بنویسید.
سلام
خیلی خیلی عالی بود. حتما در قسمت تقدیر و تشکر پایان نامه اسم سایت شما را نیز خواهم آورد. خدا اجرتون بده
یاعلی
سلام
خیلی عالی 😍
خیلی علاقهمندیم تصویر این صفحه رو داشته باشیم. برای ما خیلی خاص و باارزش هست. 😊🙏
درود، با سپاس از آموزش خوب شما، ارجمندانه نیازمند ادامه آموزش شما هستم، سپاسگزار میشم که این آموزش را ادامه دهید، در پناه پروردگار خرسند و تندرست باشید.
سلام
ممنون از شما بابت کامنت 🌹🙏
انشالله آموزش رو ادامه میدیم.
سلام بسیار عالی و مفید بود حتی یکسری مشکلات پاییه ای از شبکه عصبی و رگرسیون داشتم تا حد زیادی حل شد. ضمن اینکه برای آشنایی با پایتورچ شروع فوق العادهای داشتم.
موفق و پایدار باشید.
سلام
ممنون بابت فیدبک 🌹🙏
امیدواریم این شروع خوب استمرار داشته باشه و به پایتورچ مسلط بشید. ✌
سلام بسیار آموزش خوبی بود. یه سوال برام مطرح شد. ()lreg_model2.train چه کاری انجام میده؟
سلام
ما در پایتورچ دو تا مد ()train و ()eval داریم. وقتی میخواییم مدل رو ترین کنیم اول میبریم تو مدل ()train و بعد ترین رو شروع میکنیم.
منابع خارجی روهم خوندم ولی به این منظمی این مطالب نبود امیدوارم ادامه پیدا کنه ممنون از زحماتتون
سپاس 🌹🙏
سلام. ممنون از آموزش خوبتون
یه سوالی داشتم اینکه توی بعضی از مقالات یک مدل رو روی چندین دیتاست امتحان میکنند. درواقع میشه مدلی رو روی یک دیتاست ترین کرد و بعد از ذخیره اون از این مدل از قبل ترین شده برای دیتاست های دیگه استفاده کرد؟ در اینصورت دقت مدل کم نمیشه؟ من مدلی که داشتم رو روی دیتاست دیگه ای آزمایش کردم ولی دقتم کاملا افت کرده. به نظر شما تحت چه شرایطی این کار درست انجام میشه؟
سلام
بله این رایج هست که روش پیشنهادی رو روی چند دیتاست امتحان کنن. اما، معمولا اینطور هست که به صورت جداگانه شبکه رو روی هر دیتاست آموزش میدن و بعد هم روی همون دیتاست ارزیابی میکنن. مثلا، شبکه Faster R-CNN در تشخیص اشیا، به صورت جداگانه روی دیتاست PASCAL VOC و COCO آموزش و ارزیابی میشه.
اما موضوعی بهنام Domain Adaptation داریم که هدف این هست که مثلا همون شبکه Faster R-CNN روی دیتاست COCO آموزش ببینه ولی درعین حال روی دیتاستی مثل PASCAL VOC هم عملکرد خوبی داشته باشه. بنابراین، جواب سوال شما این هست: بله، آموزش روی یک دیتاست و بعد ارزیابی روی دیتاست دیگه باعث افت عملکرد شبکه میشه که طبیعی هست. چون دو دیتاست مربوط به دو Domain مختلف هستن.
موفق باشید
خیلی ممنون بابت پاسخگویی کامل شما.
کارتون عالیه و حرف نداره. خیلی ازتون سپاسگزارم.
سلام
ممنون از شما 🌹🙏
سلام و وقت بخیر. بعد از اموزش شبکه خودرمزنگار در پایتورچ نیاز دارم خروجی لایه ی اخر را بگیرم و یک سطر را با سطر بالایی اش عوض کنم اما بعد از اعمال تغییرات در اخرین لایه ی شبکه کل تصویرم تغییر می کند در صورتی که باید تنها قسمت کوچکی از تصویر تغییر پیدا کند! اگر لطف کنید راهنمایی بفرمایید ممنون می شوم. کد قسمت تست به شکل زیر است:
b=model (ConvAutoencoder().conv6.weight)
a0=b[0,0,0,:]
a1=b[0,0,1,:]
a0=a1
model.conv6.weight = nn.Parameter(b)
def test(model):
outputs1 = []
with torch.no_grad():
for epoch in range(10):
for data1 in test_loader:
img1, _ = data1
recon1 = model(img1)
outputs1.append((epoch, img1, recon1),)
return outputs1
outputs1 = test(model)
for k in range(0, 10, 9):
plt.figure(figsize=(10, 2))
imgs1 = outputs1[k][1].detach().numpy()
recon1 = outputs1[k][2].detach().numpy()
for i, item in enumerate(imgs1):
if i >= 10: break
plt.subplot(2, 10, i+1)
#plt.imshow(item[0])
plt.imshow(item[0].reshape(28,28), cmap=”gray”)
for i, item in enumerate(recon1):
if i >= 10: break
plt.subplot(2, 10, 10+i+1)
plt.imshow(item[0].reshape(28,28), cmap=”gray”)
سلام
به نظر میرسه سوال شما به این پست آموزش pytorch رایگان مرتبط نیست. همچنین، به دلایل متعددی خدمات بررسی کد نداریم.
موفق باشید 🌹🙏
سلام
آموزشتون عالی بود
خیلی استفاده کردم و مشتاقانه منتظر ادامه اش هستم.
با تشکر فراوان.
سلام
سپاس 🌹🙏
چشم، انشالله ادامه آموزش رو آماده میکنیم.
بسیار عالی مشتاقانه منتظر ادامه این پست هستیم.
سلام
چشم، برای نگارش ادامه آموزش pytorch برنامهریزی میکنیم.
سپاس 🌹🙏
سلام
واقعا عالی بود دستتون درد نکنه
سلام
سپاس 🌹🙏
سلام. خیلی ممنون از آموزش بی نظیرتون. ممنون میشم این رو هم توضیح بدین که دستور view توی پایتورچ برای چی هست؟ توی داکیومنت پایتورچ متوجه نشدم چرا و چه زمانی ازش استفاده میشه.
سلام
سپاس 🌹🙏
دستور view مشابه reshape هست. تصور کنید یک تنسور بنام a به ابعاد (1, 1, 512, 10) دارید. میتونید با دستور view (مثلا a.view(10, -1))، تنسور a رو به یک تنسور با ابعاد (512, 10) تبدیل کنید.
بسیار عالی ……………………….
سپاسگزار
سلام
سپاس کاربر هوسم 😊🌹🙏
با سلام و احترام،
نوع نگارش به نحوی است که به نظر یک بچه دبستانی هم می تواند موضوع را متوجه شود. شیوایی متن خیلی مهم هست که شما حقیقتا رعایت کردید، خیلی ممنون.
بکارگیری کنایه ها و … مانند “مردم یادتان هست…” و “ارجاع به متن اصلی در تووارد دیتاساینس” طولانی بودن متن را کاملا تعدیل می نماید.
موفق باشید.
سلام
سپاس 🌹🙏
بی نهایت ممنونم،حقیقتا لذت بردم از تدریستون و این شیوه که اتخاذ کردید
سلام
سپاس 🌹🙏
ما هم از کامنت زیبای شما لذت بردیم و انرژی گرفتیم.
با سلام و تشکر– واقعا طرز بیانیون عالیه-لطفا ادامه بدید
سلام
سپاس 🌹
پرانرژی ادامه خواهیم داد 💪
بسیار اموزنده وعالی
خواهشن ادامه مطالب رو هم قرار بدهید
سلام
استقبال خوبی از این پست شده و انشالله مدرس ادامه خواهند داد.
البته، وبینار رایگان آشنایی با پایتورچ رو که احتمالا یکی دو هفته آینده برگزار میشه رو از دست ندید.
سپاس 🌹
سلام
عالی بود. مطالبتون واقعا خوبه .
من چند تا سوال از این پست دارم . چطوری میتونم بپرسم مدرستون جواب بده؟
سلام
میتونید همینجا بپرسید، در حد توان بهشون جواب میدم. البته، میتونید از طریق ایمیل هم سوالات رو بپرسید.
بسیاار عالی
خسته نباشید
سلام
سپاس، سلامت باشید…
خیلی مفید بود. تشکر.
سلام
خوشحالیم که برای شما مفید بوده، سپاس 🌹
بسیار عالی مثل همیشه .
سپاس 🌹
خیلی خوب بود ممنون.
سپاس 🙏