عملیات روی تنسورها در پایتورچ
بهنام خدا، سلام… در جلسه ششم آموزش پایتورچ رایگان، میخواهیم به آموزش عملیات روی تنسورها در پایتورچ بپردازیم. در این جلسه میخواهیم، نحوه جمع، ضرب، تفریق، تقسیم تنسورها در پایتورچ را به شما بیاموزیم. شاید به ظاهر عملیات روی تنسورها بسیار ساده باشد، اما از دو نکته غافل نشوید. اولا، این جلسه بسیار پرکاربرد است. دوما، وقتی ما تنسورهای چندبعدی داریم، عملیات روی تنسورها به سادگی ماتریس و بردار نخواهد بود. بنابراین، این جلسه را جدی بگیرید و با هوسم همراه باشید…
مقدمهای بر عملیات روی تنسورها در پایتورچ
در جلسههای قبل گفتیم که در یادگیری عمیق، بهشدت با تنسورهای چندبعدی (مثلا سهبعدی و چهاربعدی) درگیر هستیم. همچنین، وقتی به مقالات روز (2020) نگاه کنید، متوجه خواهید شد که دیگر مقالات بهاینصورت نیست که یک ورودی به شبکه عصبی بدهند و خروجی دریافت کنند و تمام… در مقالات امروزی، لازم است به دل شبکه بزنید و گاهی فیچرمپهای لایههای درون شبکه عصبی را دستکاری کنید. یکی از پرکاربردترین کارهایی که در این شرایط انجام میشود، عملیات روی تنسورها است. در این جلسه میخواهیم به شما آموزش دهیم که چگونه این عملیات را انجام دهید. این جلسه میتوانست شامل مباحث پیچیده هم باشد، اما فعلا مناسب ندیدیم که آنها را مطرح کنیم. شاید در آینده، دوباره دستورهای پیچیدهتری از دسته عملیات روی تنسورها در پایتورچ را به شما معرفی کنیم.
جمع در پایتورچ
جمع در پایتورچ را میتوان به دو دسته تقسیم کرد: جمع دو یا چند تنسور، جمع تنسور و عدد. جمع در هردو بسیار ساده هست. برای انجام جمع در پایتورچ، میتوانید از ()torch.add یا + استفاده کنید. ابتدا بیایید با استفاده از + دو تنسور را باهم جمع کنیم:
>>> a = torch.randn(2,2) >>> b = torch.rand(2,2) >>> c = a + b >>> print(c) tensor([[1.8789, 0.2206], [1.4695, 1.2994]])
همچنین، بهراحتی میتوانید یک عدد را با تنسور جمع کنید:
>>> a = torch.rand(1, 2, 3) >>> b = 2.5 >>> c = a + b >>> print(c) tensor([[[2.9457, 2.5377, 2.9965], [2.5358, 2.7575, 2.8567]]])
بسیار خب، هردو دسته جمع را گفتیم. اما بهنظر شما اگر بخواهیم دو تنسور با سایز مختلف را باهم جمع کنیم، چه اتفاقی میافتد؟ خطا؟ جواب این است که در جمع دو تنسور با سایز غیریکسان، همواره با خطا مواجه نخواهید شد. به ترفند زیر دقت کنید…
ترفند در جمع دو تنسور لازم نیست حتما سایز دو تنسور دقیقا یکسان باشد. بلکه، باید دو تنسور boradcastable یا قابل انتشار باشند. بهصورت خلاصه، منظور از broacastable این است که از بین دو تنسور، یک تنسور باید قابل انتشار روی تنسور دیگر باشد. یعنی چه؟ به تصویر زیر دقت کنید؛ دراینجا، تنسور دومی که تنها یک عدد اسکالر هست، قابلیت انتشار روی تنسور اول را دارد. یعنی میتوانیم تنسور دوم را روی هریک از درایههای تنسور اول حرکت دهیم.
احتمالا، ماجرا برای شما جا افتاده، اما اجازه دهید یک نوع دیگر هم مطلب بالا را بیان کنیم: در مفهوم broadcastable، یک تنسور باید بتواند روی تنسور دیگر راه برود! یا بهگونهای خود را توسعه دهد که همسایز تنسور دیگر شود. مثلا در بالا درایههای کمرنگ تنسور دوم نشان میدهد که توانستهایم این تنسور را توسعه دهیم تا همسایز تنسور اول شود. بیایید یک مثال دیگر را مرور کنیم؛ در تصویر زیر یک تنسور دوبعدی را با یک تنسور یکبعدی جمع کردهایم. بازهم تنسور دوم که بردار است توانسته روی تنسور اول راه برود یا خودش را توسعه دهد که همسایز تنسور اول شود.
حالا سوال اینجاست که یک تنسور 2×3 با 3×3 قابل جمع است؟ خیر، چگونه تنسور کوچکتر را توسعه دهیم که همسایز تنسور اول شود؟ نمیشود…
اما آیا قانونی برای تشخیص broadcastable بودن دو تنسور وجود دارد؟ بله. به موارد زیر دقت کنید:
- اولا، باید تنسورها حداقل یک بعد داشته باشند.
- دوما، وقتی از بُعد آخر به اول حرکت میکنیم، یا بعدهای دو تنسور باید نظیربهنظیر باهم برابر باشند، یا یکی از آنها 1 باشد یا یکی از آنها وجود نداشته باشد!
مثلا دو تنسور با ابعاد (5,7,3) و (5,7,3) را درنظر بگیرید. از بعد آخر (یعنی 3) شروع میکنیم و به سمت بعد اول (یعنی 5) میآییم. مشاهده میکنید که همه بعدها باهم برابر هستند. ساده بود!
آیا دو تنسور با ابعاد (5,3,4,1) و (3,1,1) را میتوانیم جمع کنیم؟ بله، از آخر به اول حرکت کنید؛ بُعد آخر هردو 1، بعد دوم 4 و 1 (طبق قانون بالا گفتیم که اگر بعد یکی برابر با 1 بود، مشکلی ندارد)، بعد سوم هردو 3 و درنهایت در بعد چهارم یک تنسور بُعد ندارد که بازهم طبق قانون درست هست. پس میتوانیم این دو تنسور را باهم جمع کنیم.
با مقایسه بُعدها از آخر به اول، مشخص میشود که این دو تنسور را میتوان باهم جمع کرد. تصویر زیر نیز نشان میدهد که چگونه این دو تنسور باهم جمع میشوند. همچنین، در زیر کد مربوط به جمع دو تنسور با سایز بالا را میتوانید مشاهده کنید:>>> a = torch.rand(3,1)
>>> b = torch.rand(1,3)
>>> c = a + b
>>> print(c)
tensor([[0.8277, 0.5650, 1.2449],
[0.4934, 0.2307, 0.9107],
[1.1400, 0.8773, 1.5572]])
تا اینجا، علاوهبر توضیح درباره +، مفهوم مهم broadcasting نیز توضیح داده شد. اما توضیح دستور ()torch.add باقیمانده است. استفاده از این دستور بسیار ساده است. کافی است دو ورودی تنسور و اسکالر/تنسور را وارد کنید:
>>> a = torch.rand(3,1) >>> b = torch.rand(1,3) >>> d = torch.add(a, b) >>> print(d) tensor([[0.8277, 0.5650, 1.2449], [0.4934, 0.2307, 0.9107], [1.1400, 0.8773, 1.5572]])
اما، این دستور یک ورودی دیگر بهنام α هم دارد. این ورودی که مقدار پیشفرض 1 دارد، بهعنوان ضریب ورودی دوم که اسکالر/تنسور است، استفاده میشود. یعنی:
out=input+alpha×other
input و other دو ورودی تنسور و اسکالر/تنسور هستند. البته، قطعا بهراحتی میتوانیم مقدار ()torch.add را هنگام استفاده از + هم بهکار ببریم. در مثال زیر، یک نمونه استفاده از + و add همراه با α نشان داده شده است:
>>> alpha = 0.3 >>> c = a + alpha * b >>> print(c) tensor([[0.5234, 0.4446, 0.6486], [0.1892, 0.1104, 0.3144], [0.8358, 0.7570, 0.9609]]) >>> d = torch.add(a, b, alpha) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: add() takes 2 positional arguments but 3 were given >>> d = torch.add(a, b, alpha=alpha) >>> print(d) tensor([[0.5234, 0.4446, 0.6486], [0.1892, 0.1104, 0.3144], [0.8358, 0.7570, 0.9609]])
>>> a = torch.randn(2,2)
>>> b = torch.randn(2)
>>> c = torch.add(a, b, alpha=2.5)
>>> print(c)
tensor([[-4.1967, 1.4073],
[-2.8068, 0.8149]])
>>> d = a + 2.5 * b
>>> print(d)
tensor([[-4.1967, 1.4073],
[-2.8068, 0.8149]])
تفریق در پایتورچ
برای تفریق در پایتورچ دو حالت استفاده از علامت – و دستور ()torch.add وجود دارد. بله ()torch.add، اشتباه تایپی نکردهایم. دستور تفریق در پایتورچ وجود ندارد! دلیلش هم ساده هست. طبیعتا برای تفریق دو تنسور در پایتورچ، کافی است یک تنسور را با علامت منفی، به دستور ()torch.add بدهیم. توضیح بیشتر از این لازم نیست. درادامه، یک نمونه مثال آورده شده است:
>>> a = torch.rand(3,1) >>> b = torch.rand(1,3) >>> c = a - b >>> print(c) tensor([[ 0.0348, -0.2630, -0.1034], [ 0.0577, -0.2401, -0.0805], [-0.2819, -0.5797, -0.4201]]) >>> d = torch.add(a, -b) >>> print(d) tensor([[ 0.0348, -0.2630, -0.1034], [ 0.0577, -0.2401, -0.0805], [-0.2819, -0.5797, -0.4201]])
>>> a = torch.randn(2,2)
>>> b = torch.randn(2)
>>> c = torch.add(a, -b, alpha=2.5)
>>> print(c)
tensor([[ 3.7338, -1.3585],
[ 5.1236, -1.9509]])
>>> d = a - 2.5 * b
>>> print(d)
tensor([[ 3.7338, -1.3585],
[ 5.1236, -1.9509]])
ضرب در پایتورچ
قطعا میدانید که ضرب ماتریسها در ریاضیات معمولا به دو روش انجام میشود: ضرب درایهای یا نقطهای و ضرب ماتریسی… البته اسمهای متفاوتی روی این دو دسته ضرب گذاشته میشود. ضرب در پایتورچ هم از این قاعده مستثنا نیست و دستورات متنوعی برای ضرب دارد.
ابتدا ضرب درایهای را بررسی کنیم؛ در ضرب درایهای مانند جمع و تفریق بالا، درایه به درایه دو ماتریس، آرایه یا تنسور درهم ضرب میشوند. البته که در این حالت دو تنسور باید یا ابعاد یکسانی داشته باشند. یا اینکه طبق توضیحات بالا boradcastable باشند. ضرب در پایتورچ بهشکل درایه به درایه به دو روش * و دستور ()torch.mul انجام میشود. بهتر است در مورد این بخش توضیح زیادی ندهیم، چون به بخشهای بالایی بسیار شبیه است. پس مثالهای زیر را ببینید:
>>> a = torch.randint(10, (4,2)) >>> b = torch.randint(10, (2,)) >>> c = a * b >>> print(c) tensor([[35, 8], [45, 5], [10, 1], [10, 1]]) >>> d = torch.mul(a, b) >>> print(d) tensor([[35, 8], [45, 5], [10, 1], [10, 1]])
>>> a = torch.randn(4, 1)
>>> print(a)
tensor([[ 1.1207],
[-0.3137],
[ 0.0700],
[ 0.8378]])
>>> b = torch.randn(1, 4)
>>> print(b)
tensor([[ 0.5146, 0.1216, -0.5244, 2.2382]])
>>> c = torch.mul(a, b)
>>> print(c)
tensor([[ 0.5767, 0.1363, -0.5877, 2.5083],
[-0.1614, -0.0382, 0.1645, -0.7021],
[ 0.0360, 0.0085, -0.0367, 0.1567],
[ 0.4312, 0.1019, -0.4394, 1.8753]])
روش دوم ضرب، ضرب ماتریسی است. برای این نوع ضرب در پایتورچ، دستورهای زیادی وجود دارد. دستورهایی مانند ()torch.mm و ()torch.bmm و ()torch.matmul و ()torch.mv مجموعه دستوراتی هستند که برای ضرب ماتریسی در پایتورچ قابل استفاده هستند. البته، اضافه کنیم که ما این مجموعه دستورات را از Torch API کشف کردهایم! ممکن است دستورات دیگری هم وجود داشته باشد. اما همه دستوراتی که برای ضرب ماتریسی در پایتورچ نوشتیم را فعلا فراموش کنید. فقط کافی است یک دستور را یاد بگیرید. برای ضرب ماتریسی در پایتورچ به دو روش علامت @ و دستور ()torch.matmul میتوانید عمل کنید. حتما میدانید که در ضرب ماتریسی، ابعاد دو تنسور باید باهم سازگار باشند (همان قانون ضرب ماتریسی). تصویر زیر:
بسیار خب، چندنمونه مثال زیر، بهخوبی به شما نحوه انجام ضرب ماتریسی در پایتورچ را نشان میدهد:
>>> a = torch.randn(4) >>> b = torch.randn(4) >>> torch.matmul(a, b) tensor(-0.1404) >>> a @ b tensor(-0.1404)
>>> # matrix x vector >>> a = torch.randn(3, 4) >>> b = torch.randn(4) >>> torch.matmul(a, b) tensor([-0.5707, 0.8581, 1.2449])
>>> # batched matrix x batched matrix >>> a = torch.randn(10, 3, 4) >>> b = torch.randn(10, 4, 5) >>> torch.matmul(a, b).size() torch.Size([10, 3, 5])
>>> # batched matrix x broadcasted matrix >>> a = torch.randn(10, 3, 4) >>> b = torch.randn(4, 5) >>> torch.matmul(a, b).size() torch.Size([10, 3, 5])
>>> a = torch.randn(10, 3, 4)
>>> b = torch.randn(4)
>>> torch.matmul(a, b)
tensor([[-1.2237, -0.5452, 0.0484],
[-0.0492, 1.7021, 0.6323],
[ 1.1875, 2.0880, 0.7118],
[ 2.4414, 0.5737, -1.3127],
[-2.4136, 0.9959, -1.7419],
[ 2.2359, -0.2203, -1.9648],
[ 4.2914, 0.4410, 2.7445],
[-2.6010, 1.0935, -1.4887],
[ 0.6410, 0.1908, 1.3521],
[-0.2242, 2.0771, 3.4459]])
>>> a @ b
tensor([[-1.2237, -0.5452, 0.0484],
[-0.0492, 1.7021, 0.6323],
[ 1.1875, 2.0880, 0.7118],
[ 2.4414, 0.5737, -1.3127],
[-2.4136, 0.9959, -1.7419],
[ 2.2359, -0.2203, -1.9648],
[ 4.2914, 0.4410, 2.7445],
[-2.6010, 1.0935, -1.4887],
[ 0.6410, 0.1908, 1.3521],
[-0.2242, 2.0771, 3.4459]])
>>> a = torch.randn(5,6)
>>> b = torch.randn(6,2)
>>> torch.matmul(a, b)
tensor([[ 0.0736, 1.3850],
[-3.5162, -1.9878],
[-0.2418, -1.4236],
[-2.7564, 1.0294],
[ 2.4741, 2.1556]])
تقسیم در پایتورچ
طبیعتا تقسیم در پایتورچ هم مشابه با ضرب در پایتورچ هست. تقسیم در پایتورچ هم میتواند به دو شکل تقسیم نقطهای یا درایه به درایه و تقسیم ماتریسی انجام شود. برای تقسیم نقطهای میتوان از علامت / یا دستور ()torch.div استفاده کرد. دقت کنید، در این حالت درایههای دو تنسور نظیر به نظیر در هم ضرب میشوند. مثالهای زیر را ببینید:
>>> a = torch.randn(5) >>> a tensor([ 0.3810, 1.2774, -0.2972, -0.3719, 0.4637]) >>> torch.div(a, 0.5) tensor([ 0.7620, 2.5548, -0.5944, -0.7439, 0.9275])
>>> a = torch.randn(4, 4) >>> a tensor([[-0.2609, 0.3033, 0.1021, 1.7529], [-0.7428, -0.1335, 0.9485, -0.3017], [ 0.2196, 1.1497, 0.2399, -0.2150], [-0.2181, 0.8113, 2.1083, 0.4822]]) >>> b = torch.randn(4, 4) >>> b tensor([[-0.1551, -0.6794, -0.5188, -0.4295], [-1.2571, -0.7163, 0.3778, -0.7557], [ 0.6118, 0.0753, 0.2168, 1.3910], [-1.4146, 0.0662, -0.9548, 0.6041]]) >>> torch.div(a, b) tensor([[ 1.6822, -0.4464, -0.1967, -4.0809], [ 0.5909, 0.1864, 2.5104, 0.3992], [ 0.3589, 15.2767, 1.1068, -0.1546], [ 0.1541, 12.2491, -2.2082, 0.7982]]) >>> a/b tensor([[ 1.6822, -0.4464, -0.1967, -4.0809], [ 0.5909, 0.1864, 2.5104, 0.3992], [ 0.3589, 15.2767, 1.1068, -0.1546], [ 0.1541, 12.2491, -2.2082, 0.7982]])
>>> a = torch.randn(4, 4) >>> a tensor([[-0.3711, -1.9353, -0.4605, -0.2917], [ 0.1815, -1.0111, 0.9805, -1.5923], [ 0.1062, 1.4581, 0.7759, -1.2344], [-0.1830, -0.0313, 1.1908, -1.4757]]) >>> b = torch.randn(4) >>> b tensor([ 0.8032, 0.2930, -0.8113, -0.2308]) >>> torch.div(a, b) tensor([[-0.4620, -6.6051, 0.5676, 1.2637], [ 0.2260, -3.4507, -1.2086, 6.8988], [ 0.1322, 4.9764, -0.9564, 5.3480], [-0.2278, -0.1068, -1.4678, 6.3936]]) >>> a/b tensor([[-0.4620, -6.6051, 0.5676, 1.2637], [ 0.2260, -3.4507, -1.2086, 6.8988], [ 0.1322, 4.9764, -0.9564, 5.3480], [-0.2278, -0.1068, -1.4678, 6.3936]])
>>> a = torch.randn(5, 1)
>>> b = torch.randn(1, 5)
>>> torch.div(a, b)
tensor([[-5.2483e-01, 1.8074e+00, -6.3639e-01, -3.3038e-01, 2.4895e+02],
[ 7.2512e-01, -2.4971e+00, 8.7926e-01, 4.5647e-01, -3.4395e+02],
[ 9.0805e-01, -3.1271e+00, 1.1011e+00, 5.7163e-01, -4.3072e+02],
[-6.6017e-02, 2.2735e-01, -8.0051e-02, -4.1558e-02, 3.1314e+01],
[ 1.9437e-01, -6.6935e-01, 2.3568e-01, 1.2236e-01, -9.2195e+01]])
>>> a/b
tensor([[-5.2483e-01, 1.8074e+00, -6.3639e-01, -3.3038e-01, 2.4895e+02],
[ 7.2512e-01, -2.4971e+00, 8.7926e-01, 4.5647e-01, -3.4395e+02],
[ 9.0805e-01, -3.1271e+00, 1.1011e+00, 5.7163e-01, -4.3072e+02],
[-6.6017e-02, 2.2735e-01, -8.0051e-02, -4.1558e-02, 3.1314e+01],
[ 1.9437e-01, -6.6935e-01, 2.3568e-01, 1.2236e-01, -9.2195e+01]])
برای تقسیم ماتریسی در پایتورچ دستوری وجود ندارد. اما تقسیم دو تنسور a و b را در نظر بگیرید. باتوجه به روابط زیر، واضح است که میتوانیم inverse تنسور b را حساب کنیم و سپس در تنسور a ضرب کنیم.
a/b = a @ inv(b)
پس نیاز داریم که b را معکوس کنیم. با استفاده از دستور ()torch.inverse و ()torch.pinverse در پایتورچ میتوانید بهترتیب یک تنسور مربعی و غیرمربعی را معکوس کنید. بیش از این وارد توضیحات تئوری مساله نمیشویم؛ در ادامه یک نمونه مثال آوردهایم که با استفاده از دو دستور ()torch.inverse و ()torch.matmul تقسیم ماتریسی در پایتورچ انجام دادهایم.
>>> a = torch.randn(5, 5) >>> b = torch.randn(5, 5) >>> torch.matmul(a, torch.inverse(b)) tensor([[-4.0638, -7.1006, 2.2321, -4.0767, 6.8057], [-0.6641, 2.2765, 0.2752, 0.2456, -1.1463], [-1.0924, -1.3587, 0.8429, -0.7122, 2.1099], [-2.6049, -4.1937, 0.8610, -1.9127, 3.0079], [-0.6232, 0.6519, -1.4855, 1.4112, -2.5137]]) >>> a @ torch.inverse(b) tensor([[-4.0638, -7.1006, 2.2321, -4.0767, 6.8057], [-0.6641, 2.2765, 0.2752, 0.2456, -1.1463], [-1.0924, -1.3587, 0.8429, -0.7122, 2.1099], [-2.6049, -4.1937, 0.8610, -1.9127, 3.0079], [-0.6232, 0.6519, -1.4855, 1.4112, -2.5137]])
تنسور (4,1) یک تنسور مربعی نیست، پس باید از pinverse استفاده شود. معکوس تنسور (4,1) یک تنسور (1,4) هست که با تنسور (3,4) سازگار نیست. حال، فرض کنید منظور ما تنسوری به ابعاد (1,4) بوده:>>> a = torch.randn(3, 4)
>>> b = torch.randn(4, 1)
>>> b = torch.inverse(b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: A must be batches of square matrices, but they are 1 by 4 matrices
>>> b = torch.pinverse(b)
>>> b.shape
torch.Size([1, 4])
>>> torch.matmul(a, b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: size mismatch, m1: [3 x 4], m2: [1 x 4] at C:\w\1\s\windows\pytorch\aten\src\TH/generic/THTensorMath.cpp:752
>>> a = torch.randn(3, 4)
>>> b = torch.randn(1, 4)
>>> b = torch.pinverse(b)
>>> torch.matmul(a, b)
tensor([[-0.6129],
[ 0.3608],
[ 0.6114]])
>>> a @ b
tensor([[-0.6129],
[ 0.3608],
[ 0.6114]])
منابع آموزش پایتورچ
این جلسه از آموزش پایتورچ براساس داکیومنت پایتورچ تهیه شده است. پیشنهاد میکنیم نگاهی به این داکیومنت پایتورچ بیندازید.
جلسه ششم آموزش پایتورچ هم به پایان رسید. در شش جلسه گذشته، هدف ما این بود که آرام شما را با پایتورچ آشنا کنیم. ابتدا شما را با دستوراتی همچون دستورات نامپای آشنا کردیم. در جلسه بعدی، مجموعهای از دستورات پرکاربرد مانند نامپای را معرفی خواهیم کرد. جلسه بعدی، آخرین جلسهای است که درباره دستورات مشابه نامپای در پایتورچ صحبت میکنیم. آماده شوید برای جلسههای کدنویسی شبکه های عصبی در پایتورچ…
دیدگاهتان را بنویسید