فاز 1 · درس 12

عملیات‌های تنسوری: از چیدمان حافظه و برادکستینگ تا einsum و اتنشن

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک مکعب سه‌بعدی شفاف تشکیل‌شده از بلوک‌های کوچک‌تر شناور روی یک سطح آینه‌ای همراه با خطوط ماتریسی نورانی
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. تنسور چیست؟
  5. ابعاد تنسور در یادگیری عمیق
  6. چیدمان حافظه چگونه کار می‌کند؟
  7. قواعد برادکستینگ
  8. عملگر einsum: همه‌فن‌حریف عملیات‌های تنسوری
  9. پیاده‌سازی از صفر
  10. گام 1: فضای ذخیره‌سازی و گام‌های تنسور
  11. گام 2: تغییر شکل با reshape، squeeze و unsqueeze
  12. گام 3: ترانهاده و جابه‌جایی محورها (permute)
  13. گام 4: عملیات‌های عنصربه‌عنصر و تجمیعی (reductions)
  14. گام 5: برادکستینگ با NumPy
  15. گام 6: عملیات با einsum
  16. گام 7: سازوکار اتنشن با einsum
  17. استفاده از ابزارهای واقعی
  18. مقایسه‌ی پیاده‌سازی از صفر با NumPy
  19. مقایسه‌ی پیاده‌سازی از صفر با PyTorch
  20. تمام لایه‌های شبکه‌ی عصبی در قالب عملیات تنسوری
  21. خروجی این درس
  22. تمرین‌ها
  23. راهنمایی‌ها
  24. اصطلاحات کلیدی
  25. برای مطالعه‌ی بیشتر

اگر تا به حال در PyTorch یا TensorFlow مدلی پیاده کرده باشید، احتمالاً با خطاهای ناهماهنگی ابعاد (Shape Mismatch) روبه‌رو شده‌اید؛ خطاهایی مثل ضرب‌ناپذیر بودن دو ماتریس یا ناسازگاری ابعاد ورودی لایه‌ها. درک نادرست از ساختار تنسورها نه تنها باعث شکست کد هنگام اجرا می‌شود، بلکه گاهی با ایجاد خطاهای پنهان در برادکستینگ، محاسبات مدل را بی‌سروصدا منحرف می‌کند.

تنسورها سنگ‌بنای محاسبات یادگیری عمیق هستند؛ از تصاویر ورودی چندکاناله گرفته تا وزن‌ها و مقادیر QKV در سازوکار اتنشن ترنسفورمرها، همه‌چیز در قالب تنسور پردازش می‌شود. در این درس با پیاده‌سازی گام‌به‌گام یک کلاس تنسور از صفر، مفهوم گام‌های حافظه (Strides)، تغییر شکل، قواعد برادکستینگ و عملگر قدرتمند einsum را فرامی‌گیرید تا هرگز در خطاهای ابعادی سردرگم نشوید.

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

اهداف یادگیری

  • پیاده‌سازی یک کلاس تنسور از صفر؛ شامل ابعاد (shape)، گام‌ها (strides)، تغییر شکل (reshape)، ترانهاده (transpose) و عملیات‌های عنصربه‌عنصر (element-wise)
  • به‌کارگیری قواعد برادکستینگ (broadcasting) برای اجرای عملیات روی تنسورهایی با ابعاد متفاوت، بدون کپی کردن داده‌ها
  • نوشتن عبارت‌های جمع اینشتین (einsum) برای ضرب داخلی، ضرب ماتریسی، ضرب خارجی و عملیات‌های دسته‌ای (batched)
  • ردیابی دقیق ابعاد تنسورها در تک‌تک مراحل اتنشن چندسر (multi-head attention)

صورت مسئله

یک ترنسفورمر می‌سازید. مرحله‌ی رفت (forward pass) کاملاً تمیز و بی‌نقص به نظر می‌رسد. کد را اجرا می‌کنید و ناگهان با این خطا روبه‌رو می‌شوید: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768). به ابعاد خیره می‌شوید. یک ترانهاده امتحان می‌کنید. حالا می‌گوید: Expected 4D input (got 3D input). یک تابع unsqueeze اضافه می‌کنید؛ اما جای دیگری از کار می‌افتد.

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

ماتریس‌ها فقط از پس روابط دوتایی بین دو مجموعه برمی‌آیند؛ اما داده‌های دنیای واقعی در دو بعد جا نمی‌شوند. یک دسته‌ی 32تایی از تصاویر RGB با ابعاد 224x224، یک تنسور 4بعدی است: (32, 3, 224, 224). سلف‌اتنشن با 12 سر نیز 4بعدی است: (batch, heads, seq_len, head_dim). شما به ساختار داده‌ای نیاز دارید که برای هر تعداد بعد تعمیم‌پذیر باشد و عملیات‌هایش روی تمام این ابعاد به‌شکل تمیزی با هم ترکیب شوند. این ساختار داده، همان «تنسور» (tensor) است. اگر روی عملیات‌های آن مسلط شوید، اشکال‌زدایی خطاهای ابعادی برایتان مثل آب خوردن می‌شود.

مفاهیم پایه

تنسور چیست؟

تنسور یک آرایه‌ی چندبعدی از اعداد با نوع داده‌ی یکسان است. تعداد ابعاد را مرتبه (rank یا order) می‌نامند. به هر بعد یک محور (axis) می‌گوییم. ابعاد یا شکل (shape) تاپلی است که اندازه‌ی تنسور را در راستای هر محور مشخص می‌کند.

اسکالررتبه 0شکل: ()برداررتبه 1شکل: (3,)ماتریسرتبه 2شکل: (2,3)تنسور سه‌بعدیرتبه 3شکل: (2,2,2)تنسور چهاربعدیرتبه 4شکل: (B,C,H,W)

تعداد کل عناصر = حاصل‌ضرب تمام اندازه‌ها. تنسوری با ابعاد (2, 3, 4) شامل 2⋅3⋅4=242 \cdot 3 \cdot 4 = 24 عنصر است.

ابعاد تنسور در یادگیری عمیق

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

وزن‌هااتنشنNLPبینایی(B, C, H, W)32, 3, 224, 224(B, T, D)16, 128, 768(B, H, T, D)16, 12, 128, 64Linear: (out, in)Conv2D: (out_c, in_c, kH, kW)امبدینگ: (vocab, dim)

فریم‌ورک PyTorch از فرمت NCHW (کانال اول) استفاده می‌کند؛ در حالی که پیش‌فرض TensorFlow فرمت NHWC (کانال آخر) است. ناهماهنگی در این چیدمان‌ها باعث افت شدید و نامحسوس سرعت یا بروز خطا می‌شود.

چیدمان حافظه چگونه کار می‌کند؟

یک آرایه‌ی 2بعدی در حافظه به‌صورت دنباله‌ای 1بعدی از بایت‌ها ذخیره می‌شود. گام‌ها (strides) مشخص می‌کنند که برای یک گام حرکت در امتداد هر محور، باید چند عنصر را در حافظه رد کنید.

ستون-محور (ترتیب F)سطر-محور (ترتیب C)a b c d e fگام‌ها: (3, 1)a d b e c fگام‌ها: (1, 2)

عملیات ترانهاده داده‌ها را جابه‌جا نمی‌کند؛ بلکه گام‌ها را با هم تعویض می‌کند. این کار باعث می‌شود تنسور غیرپیوسته (non-contiguous) شود؛ یعنی عناصر یک سطر دیگر پشت سر هم در حافظه قرار نخواهند داشت.

قواعد برادکستینگ

برادکستینگ (broadcasting) این امکان را می‌دهد که بدون کپی کردن داده‌ها، روی تنسورهایی با ابعاد متفاوت عملیات انجام دهید. برای این کار، ابعاد را از سمت راست تراز کنید. دو بعد در صورتی سازگارند که برابر باشند یا اندازه‌ی یکی از آن‌ها 1 باشد. ابعادی که کمتر هستند، از سمت چپ با 1 پر می‌شوند.

Tensor A:     (8, 1, 6, 1)
Tensor B:        (7, 1, 5)
Padded B:     (1, 7, 1, 5)
Result:       (8, 7, 6, 5)

عملگر einsum: همه‌فن‌حریف عملیات‌های تنسوری

قرارداد جمع‌بندی اینشتین (einsum) هر محور را با یک حرف نام‌گذاری می‌کند. محورهایی که در ورودی آمده‌اند اما در خروجی نیستند، با هم جمع می‌شوند. محورهایی که در هر دو مشترکند، حفظ می‌شوند.

matmul: ik,kj - ijمجموع روی kمجموع روی kA(I,K)C(I,J)B(K,J)

الگوهای کلیدی: i,i-> (ضرب داخلی)، i,j->ij (ضرب خارجی)، ii-> (رد ماتریس یا trace)، ij->ji (ترانهاده)، bij,bjk->bik (ضرب ماتریسی دسته‌ای)، bhtd,bhsd->bhts (امتیازهای اتنشن).

برادکستینگ (BROADCASTING)دو شکل را بکشید
برادکستینگ (broadcasting) امکان ترکیب آرایه‌هایی با شکل‌های متفاوت را بدون کپی کردن داده‌ها فراهم می‌کند. شکل‌ها را از سمت راست تراز کنید؛ هر جفت از ابعاد باید برابر باشند، یا یکی از آن‌ها 1 باشد که در این صورت کشیده می‌شود. هرگونه ناهماهنگی دیگر منجر به خطا می‌شود. به همین دلیل است که یک بردار بایاس به‌راحتی به یک دسته (batch) کامل اضافه می‌شود.

پیاده‌سازی از صفر

کد کامل این بخش در فایل code/tensors.py قرار دارد. تمام مراحل به پیاده‌سازی همان فایل ارجاع می‌دهند.

گام 1: فضای ذخیره‌سازی و گام‌های تنسور

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

class Tensor:
    def __init__(self, data, shape=None):
        if isinstance(data, (list, tuple)):
            self._data, self._shape = self._flatten_nested(data)
        elif isinstance(data, np.ndarray):
            self._data = data.flatten().tolist()
            self._shape = tuple(data.shape)
        else:
            self._data = [data]
            self._shape = ()

        if shape is not None:
            total = reduce(lambda a, b: a * b, shape, 1)
            if total != len(self._data):
                raise ValueError(
                    f"Cannot reshape {len(self._data)} elements into shape {shape}"
                )
            self._shape = tuple(shape)

        self._strides = self._compute_strides(self._shape)

    @staticmethod
    def _compute_strides(shape):
        if len(shape) == 0:
            return ()
        strides = [1] * len(shape)
        for i in range(len(shape) - 2, -1, -1):
            strides[i] = strides[i + 1] * shape[i + 1]
        return tuple(strides)

برای ابعاد (3, 4)، گام‌ها برابر با (4, 1) خواهند بود؛ یعنی برای رفتن به سطر بعدی باید 4 عنصر، و برای رفتن به ستون بعدی 1 عنصر را رد کنید.

گام 2: تغییر شکل با reshape، squeeze و unsqueeze

عملیات reshape ابعاد را بدون تغییر دادن ترتیب عناصر عوض می‌کند. تعداد کل عناصر باید همیشه ثابت بماند. می‌توانید برای یکی از ابعاد مقدار -1 بگذارید تا اندازه‌ی آن به‌طور خودکار استخراج شود.

t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))

متد squeeze محورهایی با اندازه‌ی 1 را حذف می‌کند و unsqueeze یک محور جدید می‌افزاید. استفاده از unsqueeze برای برادکستینگ حیاتی است؛ مثلاً اگر بخواهید بردار بایاس با ابعاد (D,) را به یک دسته‌ی داده با ابعاد (B, T, D) اضافه کنید، باید با unsqueeze ابعاد آن را به (1, 1, D) برسانید.

t = Tensor(list(range(6)), shape=(1, 3, 1, 2))
s = t.squeeze()
v = Tensor([1, 2, 3])
u = v.unsqueeze(0)

گام 3: ترانهاده و جابه‌جایی محورها (permute)

عملیات transpose جای دو محور را با هم عوض می‌کند. متد permute همه‌ی محورها را دوباره مرتب می‌سازد. تبدیل بین دو فرمت NCHW و NHWC دقیقاً با همین کار انجام می‌شود.

mat = Tensor(list(range(6)), shape=(2, 3))
tr = mat.transpose(0, 1)

t4d = Tensor(list(range(24)), shape=(1, 2, 3, 4))
perm = t4d.permute((0, 2, 3, 1))

پس از اجرای transpose یا permute، تنسور در حافظه دیگر پیوسته نخواهد بود. در PyTorch متد view روی تنسورهای غیرپیوسته کار نمی‌کند؛ بنابراین باید از reshape استفاده کنید یا اول متد .contiguous() را صدا بزنید.

گام 4: عملیات‌های عنصربه‌عنصر و تجمیعی (reductions)

عملیات‌های عنصربه‌عنصر (مانند جمع، ضرب و تفریق) مستقل از یکدیگر روی تک‌تک عناصر اعمال می‌شوند و ابعاد تنسور را حفظ می‌کنند. عملیات‌های تجمیعی (مانند sum، mean و max) یک یا چند محور را فرو می‌ریزند و جمع می‌کنند.

a = Tensor([[1, 2], [3, 4]])
b = Tensor([[10, 20], [30, 40]])
c = a + b
d = a * 2
s = a.sum(axis=0)

میانگین‌گیری سراسری (Global average pooling) در یک شبکه‌ی CNN: عبارت (B, C, H, W).mean(axis=[2, 3]) ابعاد (B, C) را تولید می‌کند. میانگین‌گیری روی دنباله در پردازش زبان طبیعی: عبارت (B, T, D).mean(axis=1) خروجی (B, D) می‌دهد.

گام 5: برادکستینگ با NumPy

تابع demo_broadcasting_numpy() در فایل tensors.py الگوهای اصلی را نشان می‌دهد.

activations = np.random.randn(4, 3)
bias = np.array([0.1, 0.2, 0.3])
result = activations + bias

images = np.random.randn(2, 3, 4, 4)
scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1)
result = images * scale

a = np.array([1, 2, 3]).reshape(-1, 1)
b = np.array([10, 20, 30, 40]).reshape(1, -1)
outer = a * b

محاسبه‌ی فاصله‌ی دوبه‌دو (pairwise distance) با برادکستینگ: ابتدا ابعاد (M, 2) را به (M, 1, 2) و ابعاد (N, 2) را به (1, N, 2) تغییر دهید، آن‌ها را از هم کم کنید، به توان دو برسانید، روی محور آخر جمع بزنید و جذر بگیرید. نتیجه تنسوری با ابعاد (M, N) خواهد بود.

گام 6: عملیات با einsum

توابع demo_einsum() و demo_einsum_gallery() تک‌تک الگوهای رایج را مرور می‌کنند.

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
dot = np.einsum("i,i->", a, b)

A = np.array([[1, 2], [3, 4], [5, 6]], dtype=float)
B = np.array([[7, 8, 9], [10, 11, 12]], dtype=float)
matmul = np.einsum("ik,kj->ij", A, B)

batch_A = np.random.randn(4, 3, 5)
batch_B = np.random.randn(4, 5, 2)
batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B)

هزینه‌ی محاسباتی یک انقباض تنسوری (contraction)، حاصل‌ضرب اندازه‌ی تمام اندیس‌هاست (چه حفظ‌شده‌ها و چه جمع‌زده‌ها). برای رابطه‌ی bij,bjk->bik با مقادیر B=32B=32، I=128I=128، J=64J=64 و K=128K=128، تعداد عملیات‌های ضرب و جمع برابر با 32⋅128⋅64⋅128=33,554,43232 \cdot 128 \cdot 64 \cdot 128 = 33{,}554{,}432 خواهد بود.

گام 7: سازوکار اتنشن با einsum

تابع demo_attention_einsum() سازوکار اتنشن چندسر را از ابتدا تا انتها پیاده‌سازی می‌کند.

B, H, T, D = 2, 4, 8, 16
E = H * D

X = np.random.randn(B, T, E)
W_q = np.random.randn(E, E) * 0.02

Q = np.einsum("bte,ek->btk", X, W_q)
Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3)

scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D)
weights = softmax(scores, axis=-1)
attn_output = np.einsum("bhts,bhsd->bhtd", weights, V)

concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E)
output = np.einsum("bte,ek->btk", concat, W_o)

تک‌تک مراحل در واقع یک عملیات تنسوری است: تصویر خطی (ضرب ماتریسی با einsum)، تفکیک سرها (reshape + ترانهاده)، امتیازهای اتنشن (ضرب ماتریسی دسته‌ای با einsum)، مجموع وزندار (ضرب ماتریسی دسته‌ای با einsum)، ادغام سرها (ترانهاده + reshape)، و تصویر خروجی (ضرب ماتریسی با einsum).

استفاده از ابزارهای واقعی

مقایسه‌ی پیاده‌سازی از صفر با NumPy

عملیات پیاده‌سازی از صفر (کلاس Tensor) کتابخانه‌ی NumPy
ساخت Tensor([[1,2],[3,4]]) np.array([[1,2],[3,4]])
تغییر شکل (Reshape) t.reshape((3,4)) a.reshape(3,4)
ترانهاده (Transpose) t.transpose(0,1) a.T یا a.transpose(0,1)
حذف ابعاد واحد (Squeeze) t.squeeze(0) np.squeeze(a, 0)
مجموع (Sum) t.sum(axis=0) a.sum(axis=0)
جمع اینشتین (Einsum) N/A np.einsum("ij,jk->ik", a, b)

مقایسه‌ی پیاده‌سازی از صفر با PyTorch

import torch

t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
t.shape
t.stride()
t.is_contiguous()

t.reshape(3, 2)
t.unsqueeze(0)
t.transpose(0, 1)
t.transpose(0, 1).contiguous()

torch.einsum("ik,kj->ij", A, B)

کتابخانه‌ی PyTorch قابلیت‌های مشتق‌گیری خودکار (autograd)، پشتیبانی از GPU و کرنل‌های بهینه‌شده‌ی BLAS را اضافه می‌کند؛ اما معنای ابعاد در هر دو کاملاً یکسان است. اگر نسخه‌ی صفر تا صدی را متوجه شوید، خواندن و رفع خطاهای ابعادی PyTorch هم برایتان ساده خواهد بود.

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

عملیات فرم تنسوری فرم با Einsum
لایه‌ی خطی (Linear layer) Y = X @ W.T + b "bd,od->bo" + بایاس
ماتریس‌های QKV در اتنشن Q = X @ W_q "btd,dh->bth"
امتیازهای اتنشن Q @ K.T / sqrt(d) "bhtd,bhsd->bhts"
خروجی اتنشن softmax(scores) @ V "bhts,bhsd->bhtd"
نرمال‌سازی دسته‌ای (Batch norm) (X - mu) / sigma * gamma عنصربه‌عنصر + برادکستینگ (broadcasting)
تابع Softmax exp(x) / sum(exp(x)) عنصربه‌عنصر + تجمیع

خروجی این درس

این درس دو پرامپت آماده و کاربردی به شما می‌دهد:

  1. outputs/prompt-tensor-shapes.md -- یک پرامپت ساختاریافته برای اشکال‌زدایی سیستماتیک خطاهای عدم تطابق ابعاد (Shape Mismatch). این پرامپت شامل جدول‌های تصمیم‌گیری برای همه‌ی عملیات‌های متداول (matmul، برادکستینگ، cat، لایه‌های Linear، Conv2d، BatchNorm و softmax) به همراه جدول راهنمای سریع رفع خطا است.

  2. outputs/prompt-tensor-debugger.md -- یک پرامپت اشکال‌زدایی گام‌به‌گام که در زمان گیر افتادن در خطاهای ابعادی، می‌توانید آن را در هر دستیار هوش مصنوعی پیست کنید؛ پیام خطا و ابعاد تنسورهایتان را به آن بدهید تا راه‌حل دقیق را پس بگیرید.

تمرین‌ها

  1. ساده -- رفت و برگشت با Reshape. یک تنسور با ابعاد (2, 3, 4) بسازید. شکل آن را به (6, 4)، سپس به (24,) و در نهایت دوباره به (2, 3, 4) تغییر دهید. با چاپ کردن داده‌های مسطح (flat)، بررسی کنید که ترتیب عناصر در هر مرحله حفظ شده باشد.

  2. متوسط -- پیاده‌سازی برادکستینگ. کلاس Tensor را با افزودن متد broadcast_to(shape) ارتقا دهید؛ به‌طوری که بعدهایی با اندازه‌ی 1 را متناسب با ابعاد هدف گسترش دهد. سپس متد _elementwise_op را طوری بازنویسی کنید که قبل از محاسبات، خودبه‌خود برادکستینگ را انجام دهد. عملکرد آن را با دو تنسور به ابعاد (3, 1) و (1, 4) برای ساخت خروجی (3, 4) آزمایش کنید.

  3. سخت -- ساخت einsum از صفر. یک تابع اولیه‌ی einsum(subscripts, *tensors) بنویسید که دست‌کم این حالت‌ها را پوشش دهد: ضرب داخلی (i,i->)، ضرب ماتریسی (ij,jk->ik)، ضرب خارجی (i,j->ij) و ترانهاده (ij->ji). رشته‌ی اندیس‌ها را پردازش کنید، اندیس‌های منقبض‌شده (حذف‌شده در خروجی) را بشناسید و با حلقه زدن روی تمام ترکیب‌های اندیس، خروجی را بسازید. نتایج خود را با خروجی np.einsum مقایسه کنید.

  4. سخت -- ردیاب ابعاد در مکانیسم اتنشن. تابعی بنویسید که مقادیر batch_size، seq_len، embed_dim و num_heads را به عنوان ورودی بگیرد و ابعاد دقیق تنسورها را در تک‌تک مراحل اتنشن چندسر چاپ کند: ورودی، تصاویر خطی Q/K/V، تفکیک سرها، امتیازهای اتنشن، وزن‌های سافت‌مکس، مجموع وزندار، merge سرها و تصویر خروجی. درستی نتایج را با خروجی تابع demo_attention_einsum() بررسی کنید.

راهنمایی‌ها

تمرین 1. تنسور اولیه را با list(range(24)) بسازید تا هر عنصر مقدار موقعیت خودش را نشان دهد. بعد از هر فراخوانی reshape، خروجی flatten().to_list() را چاپ کنید و مطمئن شوید اعداد همچنان به‌ترتیب از 0 تا 23 هستند.

تمرین 2. عملیات برادکستینگ روی یک بعد تنها زمانی ممکن است که اندازه‌ی آن در هر دو تنسور برابر باشد یا اندازه‌ی یکی از آن‌ها 1 باشد؛ اول این شرط را بررسی کنید و اگر برقرار نبود، خطا پرتاب کنید. برای ابعاد (3, 1) و (1, 4)، بررسی کنید که عنصر [i][j] در نتیجه دقیقاً برابر با a[i][0] + b[0][j] باشد و کل خروجی را با NumPy بسنجید.

تمرین 3. رشته‌ی اندیس‌ها را بر اساس -> جدا کنید. حروفی که در بخش ورودی هستند اما در بخش خروجی وجود ندارند، اندیس‌های جمع‌بندی هستند؛ بنابراین روی مقادیر تمام این حروف حلقه بزنید و حاصل را در موقعیت خروجی جمع کنید. هر یک از چهار حالت را با آرایه‌های تصادفی کوچک بسازید و با دستور np.allclose(mine, np.einsum(...)) درستی آن را بسنجید.

تمرین 4. کار را با اندازه‌ی هر سر یعنی embed_dim // num_heads شروع کنید و با دستور assert مطمئن شوید که embed_dim % num_heads == 0 است. امتیازهای اتنشن ابعادی برابر با (batch_size, num_heads, seq_len, seq_len) دارند؛ شکل‌های چاپ‌شده را با این ابعاد و همچنین با خروجی‌های تابع demo_attention_einsum() تطبیق دهید.

اصطلاحات کلیدی

اصطلاح باور اشتباه مفهوم واقعی
تنسور (Tensor) «همان ماتریس با ابعاد بیشتر» آرایه‌ای چندبعدی با نوع داده‌ی یکسان، ابعاد مشخص، گام‌های معین و عملیات‌های تعریف‌شده
مرتبه (Rank) «همان تعداد بعد» تعداد محورها؛ یک ماتریس مرتبه‌ی 2 دارد، نه اینکه مرتبه‌اش برابر با رتبه‌ی ماتریسی در جبر خطی باشد
ابعاد (Shape) «اندازه‌ی تنسور» تاپلی که اندازه‌ی هر محور را نشان می‌دهد؛ مثلاً (2, 3) یعنی 2 سطر و 3 ستون
گام (Stride) «نحوه‌ی چیدمان حافظه» تعداد عناصری که برای یک خانه جلو رفتن در امتداد هر محور، باید در حافظه رد شوند
برادکستینگ (Broadcasting) «هماهنگ شدن خودکار ابعاد متفاوت» مجموعه‌ای دقیق از قواعد: تراز از راست، و الزام به برابر بودن یا 1 بودن اندازه‌ها در هر بعد
پیوسته (Contiguous) «تنسور مرتب و معمولی» عناصری که پشت سر هم و بدون فاصله در حافظه قرار گرفته‌اند، بدون هیچ انحرافی نسبت به چیدمان منطقی
جمع اینشتین (Einsum) «روشی شیک برای ضرب ماتریسی» نمادگذاری کلی برای بیان هر نوع انقباض تنسوری، ضرب خارجی، رد ماتریس یا ترانهاده در یک خط
دید (View) «همان عملیات reshape» تنسوری با بافر حافظه‌ی مشترک اما با متادیتای متفاوت ابعاد و گام‌ها، که روی داده‌های غیرپیوسته کار نمی‌کند
انقباض (Contraction) «جمع زدن روی یک اندیس» عملیات کلی ضرب و جمع روی اندیس‌های مشترک بین دو تنسور که خروجی‌ای با مرتبه‌ی پایین‌تر تولید می‌کند
فرمت NCHW / NHWC «تفاوت سلیقه‌ی PyTorch و TensorFlow» استانداردهای چیدمان حافظه برای تنسورهای تصویر؛ NCHW کانال‌ها را قبل از ابعاد فضایی و NHWC کانال‌ها را بعد از آن‌ها قرار می‌دهد

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درس«شکل» (shape) یک تنسور چه چیزی را توصیف می‌کند؟
  2. پیش از درسدر PyTorch، یک تنسور دسته‌ای از تصاویر به‌طور پیش‌فرض از چه چیدمانی (layout) استفاده می‌کند؟
  3. پس از درسشکل حاصل از برادکستینگ تنسورهایی با ابعاد (8, 1, 6, 1) و (7, 1, 5) چیست؟
  4. پس از درسدر عبارت einsum با فرمت 'bhtd,bhsd->bhts'، چه اتفاقی برای اندیس 'd' می‌افتد؟
  5. پس از درسچرا فراخوانی ‎.view()‎ روی یک تنسور ترانهاده‌شده (transposed) در PyTorch با شکست مواجه می‌شود؟

سؤالات متداول

تفاوت بین view و reshape در کتابخانه‌هایی مثل PyTorch چیست؟

متد view داده جدیدی تولید نمی‌کند و فقط متادیتای شکل و گام‌ها را عوض می‌کند؛ به همین دلیل اگر تنسور در حافظه پیوسته (contiguous) نباشد خطا می‌دهد. در مقابل، reshape در صورت نیاز یک کپی پیوسته از داده می‌سازد و سپس شکل را تغییر می‌دهد.

قواعد برادکستینگ (Broadcasting) چگونه کار می‌کنند؟

ابعاد دو تنسور از سمت راست به چپ مقایسه می‌شوند؛ در هر بعد، اندازه‌ها یا باید با هم برابر باشند یا یکی از آن‌ها 1 باشد. در ابعاد کمتر، سمت چپ ابعاد تنسور به شکل ضمنی با اندازه 1 پر می‌شود.

چرا استفاده از einsum در پیاده‌سازی اتنشن ترنسفورمرها توصیه می‌شود؟

عملگر einsum به شما اجازه می‌دهد عملیات‌های پیچیده‌ای مثل ضرب ماتریسی دسته‌ای و انقباض تنسور را بدون نیاز به چندین مرحله ترانهاده (transpose) و تغییر شکل (reshape) در یک خط کد خوانا و استاندارد بنویسید.

ما رو توی گوگل بیشتر ببین

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