فصل 20 -تنسورها با کتابخانه پایتورچ

20.0 مقدمه

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

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

20.1 ایجاد یک تنسور

مشکل

نیاز به ایجاد یک تنسور دارید.

راه‌حل

از PyTorch برای ایجاد یک تنسور استفاده کنید:

# Load library 
import torch

# Create a vector as a row
tensor_row = torch.tensor([1, 2, 3])

# Create a vector as a column
tensor_column = torch.tensor(
    [
        [1],
        [2],
        [3]
    ]
)

توضیحات

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

20.2 ایجاد تنسور از NumPy

مشکل

نیاز به ایجاد تنسورهای PyTorch از آرایه‌های NumPy دارید.

راه‌حل

از تابع from_numpy در PyTorch استفاده کنید:

# Import libraries
import numpy as np
import torch

# Create a NumPy array
vector_row = np.array([1, 2, 3])

# Create a tensor from a NumPy array
tensor_row = torch.from_numpy(vector_row)

توضیحات

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

20.3 ایجاد تنسور پراکنده(Sparse)

مشکل

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

راه‌حل

از تابع to_sparse در PyTorch استفاده کنید:

# Import libraries
import torch

# Create a tensor
tensor = torch.tensor(
 [
  [0, 0],
  [0, 1],
  [3, 0]
 ]
)

# Create a sparse tensor from a regular tensor
sparse_tensor = tensor.to_sparse()

توضیحات

تنسورهای پراکنده روش‌های کارآمدی از نظر حافظه برای نمایش داده‌هایی هستند که بیشتر شامل صفرها می‌شوند. در فصل اول، از scipy برای ایجاد یک ماتریس پراکنده فشرده‌شده به‌صورت ردیفی (CSR) استفاده کردیم که دیگر یک آرایه NumPy نبود.

کلاس torch.Tensor به ما امکان می‌دهد هم ماتریس‌های معمولی و هم ماتریس‌های پراکنده را با استفاده از یک شیء یکسان ایجاد کنیم. اگر نوع دو تنسوری که تازه ایجاد کردیم را بررسی کنیم، می‌بینیم که هر دو در واقع از یک کلاس یکسان هستند:

print(type(tensor)) 
print(type(sparse_tensor))

خروجی:

<class 'torch.Tensor'> 
<class 'torch.Tensor'>

20.4 انتخاب عناصر در یک تنسور

مشکل

نیاز به انتخاب عناصر خاصی از یک تنسور دارید.

راه‌حل

از ایندکس‌گذاری و برش‌دهی مشابه NumPy برای بازگرداندن عناصر استفاده کنید:

# Load library
import torch

# Create vector tensor
vector = torch.tensor([1, 2, 3, 4, 5, 6])

# Create matrix tensor
matrix = torch.tensor(
    [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ]
)

# Select third element of vector
vector[2]

خروجی:

tensor(3) 
# Select second row, second column
matrix[1,1] 

خروجی:

tensor(5) 

توضیحات

مانند آرایه‌های NumPy و تقریباً همه چیز در پایتون، تنسورهای PyTorch از ایندکس‌گذاری مبتنی بر صفر استفاده می‌کنند. هم ایندکس‌گذاری و هم برش‌دهی پشتیبانی می‌شوند. یک تفاوت کلیدی این است که ایندکس‌گذاری یک تنسور PyTorch برای بازگرداندن یک عنصر منفرد، همچنان یک تنسور برمی‌گرداند، نه مقدار خود شیء (که می‌توانست به‌صورت عدد صحیح یا اعشاری باشد).

نحو برش‌دهی نیز با NumPy هم‌خوانی دارد و در PyTorch اشیائی از نوع تنسور برمی‌گرداند:

# Select all elements of a vector 
vector[:]

خروجی:

array([1, 2, 3, 4, 5, 6])

همه چیز را تا عنصر سوم انتخاب کنید:

# Select everything up to and including the third element
vector[:3] 

خروجی:

tensor([1, 2, 3]) 

بعد از عنصر سوم همه چیز را انتخاب کنید:

# Select everything after the third element
vector[3:] 

خروجی:

tensor([4, 5, 6]) 

آخرین عنصر را انتخاب کنید:

# Select the last element
vector[-1] 

خروجی:

tensor(6) 

دو ردیف اول و تمام ستون های یک ماتریس را انتخاب کنید:

# Select the first two rows and all columns of a matrix
matrix[:2,:] 

خروجی:

tensor([[1, 2, 3], [4, 5, 6]]) 

همه ردیف ها و ستون دوم را انتخاب کنید:

# Select all rows and the second column
matrix[:,1:2] 

خروجی:

tensor([[2], [5], [8]]) 

یک تفاوت مهم این است که تنسورهای PyTorch هنوز از گام‌های منفی (negative steps) در برش‌دهی پشتیبانی نمی‌کنند. بنابراین، تلاش برای معکوس کردن یک تنسور با استفاده از برش‌دهی منجر به خطا می‌شود:

# Reverse the vector
vector[::-1] 

خروجی:

ValueError: step must be greater than zero 

به‌جای آن، اگر بخواهیم یک تنسور را معکوس کنیم، می‌توانیم از متد flip استفاده کنیم:

vector.flip(dims=(-1,)) 

خروجی:

tensor([6, 5, 4, 3, 2, 1]) 

20.5 توصیف یک تنسور

مشکل

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

راه‌حل

ویژگی‌های shape، dtype، layout و device تنسور را بررسی کنید:

# Load library 
import torch

# Create a tensor
tensor = torch.tensor([[1,2,3], [1,2,3]])

# Get the shape of the tensor
tensor.shape

خروجی:

torch.Size([2, 3]) 

نوع داده موارد موجود در تانسور را دریافت کنید:

# Get the data type of items in the tensor
tensor.dtype 

خروجی:

torch.int64 

قالب تانسور را دریافت کنید

# Get the layout of the tensor
tensor.layout 

خروجی:

torch.strided 

دستگاه مورد استفاده تانسور را دریافت کنید:

# Get the device being used by the tensor
tensor.device 

خروجی:

device(type='cpu') 

توضیحات

تنسورهای PyTorch ویژگی‌های مفیدی برای جمع‌آوری اطلاعات درباره یک تنسور ارائه می‌دهند، از جمله:

- شکل (Shape): ابعاد تنسور را نشان می‌دهد.

- نوع داده (Dtype): نوع داده اشیاء درون تنسور را مشخص می‌کند.

- قالب (Layout): نحوه چیدمان حافظه را نشان می‌دهد (رایج‌ترین آن strided است که برای تنسورهای متراکم استفاده می‌شود).

- دستگاه (Device): سخت‌افزاری که تنسور روی آن ذخیره شده است (CPU یا GPU) را مشخص می‌کند.

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

20.6 اعمال عملیات بر روی عناصر

مشکل

می‌خواهید یک عملیات را بر روی تمام عناصر یک تنسور اعمال کنید.

راه‌حل

از قابلیت پخش (broadcasting) در PyTorch استفاده کنید:

# Load library 
import torch

# Create a tensor
tensor = torch.tensor([1, 2, 3])

# Broadcast an arithmetic operation to all elements in a tensor
tensor * 100

خروجی:

tensor([100, 200, 300]) 

توضیحات

عملیات‌های پایه در PyTorch از پخش (broadcasting) بهره می‌برند تا این عملیات‌ها را با استفاده از سخت‌افزارهای شتاب‌دهنده مانند GPU به‌صورت موازی انجام دهند. این موضوع برای عملگرهای ریاضی پشتیبانی‌شده در پایتون (+، -، ×، /) و سایر توابع داخلی PyTorch صدق می‌کند. برخلاف NumPy، PyTorch شامل متد vectorize برای اعمال یک تابع به تمام عناصر یک تنسور نیست. بااین‌حال، PyTorch تمامی ابزارهای ریاضی لازم برای توزیع و شتاب‌دهی به عملیات‌های معمول موردنیاز در جریان‌های کاری یادگیری عمیق را فراهم می‌کند.

20.7 یافتن مقادیر حداکثر و حداقل

مشکل

نیاز به یافتن مقدار حداکثر یا حداقل در یک تنسور دارید.

راه‌حل

از متدهای max و min در PyTorch استفاده کنید:

# Load library
import torch

# Create a tensor
torch.tensor([1,2,3])

# Find the largest value
tensor.max()

خروجی:

tensor(3) 

یافتن کوچکترین مقدار:

# Find the smallest value
tensor.min() 

خروجی:

tensor(1) 

توضیحات

متدهای max و min یک تنسور به ما کمک می‌کنند تا بزرگ‌ترین یا کوچک‌ترین مقادیر موجود در آن تنسور را پیدا کنیم. این متدها به همان شکل برای تنسورهای چندبعدی نیز کار می‌کنند:

# Create a multidimensional tensor
tensor = torch.tensor([[1,2,3],[1,2,5]])

# Find the largest value
tensor.max() 

خروجی:

tensor(5) 

20.8 تغییر شکل تنسورها

مشکل

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

راه‌حل

از متد reshape در PyTorch استفاده کنید:

# Load library
import torch

# Create 4x3 tensor
tensor = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])

# Reshape tensor into 2x6 tensor
tensor.reshape(2, 6) 

خروجی:

tensor([[ 1, 2, 3,  4,  5,  6], 
        [ 7, 8, 9, 10, 11, 12]])

توضیحات

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

20.9 ترانهاده کردن یک تنسور

مشکل

نیاز به ترانهاده کردن یک تنسور دارید.

راه‌حل

از متد mT استفاده کنید:

# Load library
import torch

# Create a two-dimensional tensor
tensor = torch.tensor([[[1,2,3]]])

# Transpose it
tensor.mT 

خروجی:

tensor([[1],
        [2],
        [3]])

توضیحات

ترانهاده کردن در PyTorch کمی متفاوت از NumPy است. متد T که برای آرایه‌های NumPy استفاده می‌شود، در PyTorch تنها برای تنسورهای دوبعدی پشتیبانی می‌شود و در زمان نگارش این متن، برای تنسورهای با اشکال دیگر منسوخ شده است. متد mT که برای ترانهاده کردن دسته‌ای از تنسورها استفاده می‌شود، ترجیح داده می‌شود، زیرا برای ابعاد بیشتر از دو مقیاس‌پذیر است.

روش دیگری برای ترانهاده کردن تنسورهای PyTorch با هر شکلی، استفاده از متد permute است:

tensor.permute(*torch.arange(tensor.ndim - 1, -1, -1))

خروجی:

tensor([[1], [2], [3]]) 

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

20.10 مسطح کردن یک تنسور

مشکل

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

راه‌حل

از متد flatten استفاده کنید:

# Load library
import torch

# Create tensor
tensor = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# Flatten tensor
tensor.flatten() 

خروجی:

tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])

توضیحات

مسطح کردن یک تنسور تکنیکی مفید برای تبدیل یک تنسور چندبعدی به یک‌بعدی است.

20.11 محاسبه ضرب داخلی

مشکل

نیاز به محاسبه ضرب داخلی دو تنسور دارید.

راه‌حل

از متد dot استفاده کنید:

# Load library
import torch

# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])

# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])

# Calculate the dot product of the two tensors
tensor_1.dot(tensor_2) 

خروجی:

tensor(32)

توضیحات

محاسبه ضرب داخلی دو تنسور عملیاتی رایج است که هم در حوزه یادگیری عمیق و هم در فضای بازیابی اطلاعات کاربرد دارد. شاید به خاطر بیاورید که در بخش‌های قبلی کتاب، از ضرب داخلی دو بردار برای انجام جستجوی مبتنی بر شباهت کسینوسی استفاده کردیم. انجام این کار در PyTorch روی GPU (به‌جای استفاده از NumPy یا scikit-learn روی CPU) می‌تواند بهبود عملکرد چشمگیری در مسائل بازیابی اطلاعات به همراه داشته باشد.

20.12 ضرب تنسورها

مشکل

نیاز به ضرب دو تنسور دارید.

راه‌حل

از عملگرهای حسابی پایه پایتون استفاده کنید:

# Load library
import torch

# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])

# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])

# Multiply the two tensors
tensor_1 * tensor_2 

خروجی:

tensor([ 4, 10, 18])

توضیحات

PyTorch از عملگرهای حسابی پایه مانند ×، +، - و / پشتیبانی می‌کند. اگرچه ضرب تنسورها احتمالاً یکی از رایج‌ترین عملیات‌ها در یادگیری عمیق است، دانستن این نکته مفید است که تنسورها همچنین می‌توانند جمع، تفریق یا تقسیم شوند.

- جمع یک تنسور با تنسور دیگر:

tensor_1 + tensor_2

خروجی:

 tensor([5, 7, 9]) 

- تفریق یک تنسور از تنسور دیگر:

tensor_1 - tensor_2

خروجی:

 tensor([-3, -3, -3]) 

- تقسیم یک تنسور بر تنسور دیگر:

tensor_1 / tensor_2

خروجی:

tensor([0.2500, 0.4000, 0.5000])