بسیاری از توسعهدهندگانی که یادگیری عمیق را آغاز میکنند، خطوط کدی مانند `output = relu(W @ x + b)` را صرفاً کپی میکنند، بدون آنکه بدانند پشت عملگر `@` چه اتفاقی رخ میدهد. این ناآگاهی هنگام برخورد با خطاهای رایج عدم تطابق ابعاد (Shape Mismatch) در فریمورکهایی مثل PyTorch به یک مانع کلافهکننده تبدیل میشود.
کلید ساخت مدلهای یادگیری عمیق، درک شهودی عملیات برداری و ماتریسی است. هر تصویر، ورودی متنی، و لایهی تبدیل در شبکه، ساختاری از همین ماتریسهاست. در این درس، مفاهیم را بهجای فرمولهای خشک دانشگاهی، از صفر و با نوشتن یک کلاس اختصاصی پایتون پیاده میکنیم تا جادوی پشت این عملگرها آشکار شود.
کدهای این بخش کاملاً پایهای و سبک هستند و بدون نیاز به سختافزار ابری یا پردازندهی گرافیکی، مستقیماً روی CPU سیستم شما اجرا میشوند؛ بنابراین بدون هیچ دغدغهی زیرساختی میتوانید منطق ریاضی شبکه عصبی را لمس کنید.
هر شبکه عصبی صرفاً همان ضرب ماتریسی است، با چند مرحلهی اضافه.
اهداف یادگیری
- ساخت کلاس Matrix با پشتیبانی از عملیات نظیربهنظیر (element-wise)، ضرب ماتریسی، ترانهاده (transpose)، دترمینان (determinant) و معکوس (inverse)
- تشخیص تفاوت ضرب نظیربهنظیر و ضرب ماتریسی، همراه با درک کاربرد هرکدام
- پیادهسازی یک لایهی متراکم (dense layer) در شبکه عصبی (
relu(W @ x + b)) صرفاً با استفاده از کلاس Matrix که از صفر ساختهاید - درک و تشریح قوانین برادکستینگ (broadcasting) و نحوهی کارکرد جمع بایاس (bias) در فریمورکهای شبکه عصبی
صورت مسئله
میخواهید یک شبکه عصبی بسازید. کد را باز میکنید و با چنین خطی روبهرو میشوید:
output = activation(weights @ input + bias)
عملگر @ همان ضرب ماتریسی است. متغیر weights یک ماتریس است و input یک بردار. اگر ندانید این عملگرها چه میکنند، این سطر برایتان شبیه جادو خواهد بود؛ اما اگر بدانید، متوجه میشوید که کل گذر رو به جلو (forward pass) یک لایه تنها در سه عملیات خلاصه شده است.
هر تصویری که مدل پردازش میکند، ماتریسی از مقادیر پیکسلهاست. هر امبدینگ (embedding) کلمه، یک بردار است. هر لایه از هر شبکه عصبی، در اصل یک تبدیل ماتریسی است. همانطور که بدون شناخت متغیرها نمیتوان کد نوشت، بدون تسلط بر عملیات ماتریسی هم نمیتوانید سیستمهای هوش مصنوعی بسازید.
در این درس، این تسلط را از پایه شکل میدهیم.
مفاهیم پایه
بردارها: لیستهای مرتبی از اعداد
بردار (vector) لیستی از اعداد است که جهت و اندازه (magnitude) دارد. در هوش مصنوعی، بردارها نقاط داده، ویژگیها یا پارامترها را نشان میدهند.
v = [3, 4] -- a 2D vector
w = [1, 0, -2] -- a 3D vector
بردار دوبعدی [3, 4] به مختصات (3, 4) روی صفحه اشاره میکند. طول (اندازه) آن برابر 5 است (همان مثلث معروف 3-4-5).
ماتریسها: شبکهای از اعداد
ماتریس (matrix) یک شبکهی دوبعدی از سطرها و ستونهاست. یک ماتریس دارای سطر و ستون است.
در شبکههای عصبی، ماتریسهای وزن، بردارهای ورودی را به بردارهای خروجی تبدیل میکنند. لایهای با 784 ورودی و 128 خروجی، از یک ماتریس وزن 128x784 استفاده میکند.
چرا ابعاد اهمیت دارند؟
ضرب ماتریسی یک قانون سفتوسخت دارد: (m x n) @ (n x p) = (m x p). ابعاد درونی باید دقیقاً با هم برابر باشند.
(128 x 784) @ (784 x 1) = (128 x 1)
weights input output
Inner dimensions: 784 = 784 -- valid
اگر در PyTorch با خطای عدم تطابق ابعاد (Shape Mismatch) روبهرو شدید، دلیلش دقیقاً همین است.
نقشهی عملیات جبری
| عملیات | عملکرد | کاربرد در شبکه عصبی |
|---|---|---|
| جمع | ترکیب نظیربهنظیر | افزودن بایاس به خروجی |
| ضرب اسکالر | تغییر مقیاس تمام درایهها | نرخ یادگیری * گرادیانها |
| ضرب ماتریسی | تبدیل بردارها | گذر رو به جلوی لایه |
| ترانهاده | جابهجایی سطرها و ستونها | پسانتشار (backpropagation) |
| دترمینان | خلاصهسازی در قالب یک عدد | بررسی معکوسپذیری |
| معکوس | بازگرداندن تبدیل به حالت اول | حل دستگاههای خطی |
| ماتریس همانی | ماتریس خنثی (بدون تغییر) | مقداردهی اولیه، اتصالات باقیمانده (residual connections) |
تفاوت ضرب نظیربهنظیر و ضرب ماتریسی
این تمایز معمولاً افراد تازهکار را به اشتباه میاندازد.
ضرب نظیربهنظیر: درایههای متناظر در هم ضرب میشوند. ابعاد هر دو ماتریس باید دقیقاً یکسان باشد.
ضرب ماتریسی: حاصلضرب داخلی سطرها در ستونهاست. ابعاد درونی باید با هم بخوانند.
دو عملیات متفاوت، دو نتیجهی متفاوت و دو قانون متفاوت.
برادکستینگ (Broadcasting)
وقتی یک بردار بایاس را با ماتریسی از خروجیها جمع میکنید، ابعادشان یکسان نیست. برادکستینگ (broadcasting) آرایهی کوچکتر را کِش میآورد تا هماندازهی آرایهی بزرگتر شود.
| 1 2 3 | + [10, 20, 30]
| 4 5 6 |
Broadcasting stretches the vector across rows:
| 1 2 3 | | 10 20 30 | | 11 22 33 |
| 4 5 6 | + | 10 20 30 | = | 14 25 36 |
تمام فریمورکهای مدرن این کار را خودکار انجام میدهند. درک این سازوکار مانع از سردرگمی شما در مواقعی میشود که ابعاد ظاهراً همخوانی ندارند، اما کد بدون خطا اجرا میشود.
پیادهسازی از صفر
گام 1: کلاس Vector
class Vector:
def __init__(self, data):
self.data = list(data)
self.size = len(self.data)
def __repr__(self):
return f"Vector({self.data})"
def __add__(self, other):
return Vector([a + b for a, b in zip(self.data, other.data)])
def __sub__(self, other):
return Vector([a - b for a, b in zip(self.data, other.data)])
def __mul__(self, scalar):
return Vector([x * scalar for x in self.data])
def dot(self, other):
return sum(a * b for a, b in zip(self.data, other.data))
def magnitude(self):
return sum(x ** 2 for x in self.data) ** 0.5
گام 2: کلاس Matrix با عملیات پایه
class Matrix:
def __init__(self, data):
self.data = [list(row) for row in data]
self.rows = len(self.data)
self.cols = len(self.data[0])
self.shape = (self.rows, self.cols)
def __repr__(self):
rows_str = "\n ".join(str(row) for row in self.data)
return f"Matrix({self.shape}):\n {rows_str}"
def __add__(self, other):
return Matrix([
[self.data[i][j] + other.data[i][j] for j in range(self.cols)]
for i in range(self.rows)
])
def __sub__(self, other):
return Matrix([
[self.data[i][j] - other.data[i][j] for j in range(self.cols)]
for i in range(self.rows)
])
def scalar_multiply(self, scalar):
return Matrix([
[self.data[i][j] * scalar for j in range(self.cols)]
for i in range(self.rows)
])
def element_wise_multiply(self, other):
return Matrix([
[self.data[i][j] * other.data[i][j] for j in range(self.cols)]
for i in range(self.rows)
])
def matmul(self, other):
return Matrix([
[
sum(self.data[i][k] * other.data[k][j] for k in range(self.cols))
for j in range(other.cols)
]
for i in range(self.rows)
])
def transpose(self):
return Matrix([
[self.data[j][i] for j in range(self.rows)]
for i in range(self.cols)
])
def determinant(self):
if self.shape == (1, 1):
return self.data[0][0]
if self.shape == (2, 2):
return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0]
det = 0
for j in range(self.cols):
minor = Matrix([
[self.data[i][k] for k in range(self.cols) if k != j]
for i in range(1, self.rows)
])
det += ((-1) ** j) * self.data[0][j] * minor.determinant()
return det
def inverse_2x2(self):
det = self.determinant()
if det == 0:
raise ValueError("Matrix is singular, no inverse exists")
return Matrix([
[self.data[1][1] / det, -self.data[0][1] / det],
[-self.data[1][0] / det, self.data[0][0] / det]
])
@staticmethod
def identity(n):
return Matrix([
[1 if i == j else 0 for j in range(n)]
for i in range(n)
])
گام 3: مشاهدهی عملکرد در عمل
A = Matrix([[1, 2], [3, 4]])
B = Matrix([[5, 6], [7, 8]])
print("A + B =", (A + B).data)
print("A @ B =", A.matmul(B).data)
print("A^T =", A.transpose().data)
print("det(A) =", A.determinant())
print("A^-1 =", A.inverse_2x2().data)
I = Matrix.identity(2)
print("A @ A^-1 =", A.matmul(A.inverse_2x2()).data)
گام 4: اتصال به شبکههای عصبی
import random
inputs = Matrix([[0.5], [0.8], [0.2]])
weights = Matrix([
[random.uniform(-1, 1) for _ in range(3)]
for _ in range(2)
])
bias = Matrix([[0.1], [0.1]])
def relu_matrix(m):
return Matrix([[max(0, val) for val in row] for row in m.data])
pre_activation = weights.matmul(inputs) + bias
output = relu_matrix(pre_activation)
print(f"Input shape: {inputs.shape}")
print(f"Weight shape: {weights.shape}")
print(f"Output shape: {output.shape}")
print(f"Output: {output.data}")
این یک لایهی متراکم واحد است: output = relu(W @ x + b). تکتک لایههای متراکم در تمام شبکههای عصبی دقیقاً همین کار را انجام میدهند.
استفاده در عمل
NumPy تمام کارهای بالا را با خطوط کد کمتر و چندین برابر سریعتر انجام میدهد.
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print("A + B =\n", A + B)
print("A * B (element-wise) =\n", A * B)
print("A @ B (matrix multiply) =\n", A @ B)
print("A^T =\n", A.T)
print("det(A) =", np.linalg.det(A))
print("A^-1 =\n", np.linalg.inv(A))
print("I =\n", np.eye(2))
inputs = np.random.randn(3, 1)
weights = np.random.randn(2, 3)
bias = np.array([[0.1], [0.1]])
output = np.maximum(0, weights @ inputs + bias)
print(f"\nNeural network layer: {weights.shape} @ {inputs.shape} = {output.shape}")
print(f"Output:\n{output}")
عملگر @ در پایتون متد __matmul__ را فرامیخواند. پیادهسازی NumPy بر پایهی روتینهای بهینهشدهی BLAS است که با C و Fortran نوشته شدهاند. همان ریاضیات، اما 100 برابر سریعتر.
برادکستینگ در NumPy:
matrix = np.array([[1, 2, 3], [4, 5, 6]])
bias = np.array([10, 20, 30])
print(matrix + bias)
کتابخانهی NumPy بردار یکبعدی بایاس را بهطور خودکار روی هر دو سطر کپی (برادکست) میکند. جمع بایاس در تمام فریمورکهای شبکه عصبی دقیقاً به همین شیوه کار میکند.
تحویل پروژه
خروجی این درس یک پرامپت (prompt) برای آموزش عملیات ماتریسی با استفاده از شهود هندسی است؛ فایل outputs/prompt-matrix-operations.md را بررسی کنید.
کلاس Matrix که اینجا ساختیم، پایهی فریمورک جمعوجور شبکه عصبی ما خواهد بود؛ فریمورکی که در فاز 3، درس 10 پیادهسازی خواهیم کرد.
تمرینها
-
بررسی معکوس ماتریس. حاصل
A @ A.inverse_2x2()را محاسبه و تأیید کنید که ماتریس همانی به دست میآید. این کار را با سه ماتریس 2x2 مختلف امتحان کنید. وقتی دترمینان ماتریس صفر باشد چه اتفاقی میافتد؟ -
پیادهسازی معکوس ماتریس 3x3. کلاس Matrix را طوری توسعه دهید که با روش ماتریس الحاقی (adjugate)، معکوس ماتریسهای 3x3 را محاسبه کند. عملکرد آن را در مقایسه با
np.linalg.invدر NumPy بسنجید. -
ساخت یک شبکه دو لایه. صرفاً با استفاده از کلاس Matrix خودتان (بدون کمک گرفتن از NumPy)، یک شبکه عصبی دو لایه بسازید: ورودی (3) -> لایه پنهان (4) -> خروجی (2). وزنها را تصادفی مقداردهی کنید، یک گذر رو به جلو اجرا نمایید و مطمئن شوید همهی ابعاد درست باشند.
راهنماییها
تمرین 1. حاصل A @ A.inverse_2x2() را برای سه ماتریس به دست آورید و برابری هر درایه با ماتریس همانی را با دقت 1e-9 بسنجید. برای ماتریسی با دترمینان 0، مثل [[1, 2], [2, 4]]، متد inverse_2x2 خطای ValueError پرتاب میکند و این دقیقاً همان رفتار مورد انتظار است.
تمرین 2. ماتریس همسازه (cofactor) ماتریس 3x3 را بسازید، آن را ترانهاده کنید تا ماتریس الحاقی به دست آید و سپس هر درایه را بر determinant() تقسیم کنید. بررسی کنید که حاصل A @ A_inv ماتریس همانی باشد و مقدار هر درایه با دقت 1e-9 با خروجی np.linalg.inv(A) مطابقت داشته باشد.
تمرین 3. پیش از ضرب، ابعاد هر ماتریس را بنویسید: همانطور که در تابع demo_neural_network_layer دیدید، ابعاد W1 برابر 4x3 و W2 برابر 2x4 است. ورودی 3x1 باید یک بردار پنهان 4x1 و در نهایت خروجی 2x1 تولید کند.
اصطلاحات کلیدی
| اصطلاح | در گفتوگوهای عامیانه | مفهوم دقیق و فنی |
|---|---|---|
| بردار (Vector) | «یک فِلِش» | لیستی مرتب از اعداد. در هوش مصنوعی: نقطهای در فضایی چندبعدی. |
| ماتریس (Matrix) | «جدولی از اعداد» | یک تبدیل خطی. بردارها را از فضایی به فضای دیگر نگاشت میکند. |
| ضرب ماتریسی (Matrix multiply) | «فقط اعداد رو در هم ضرب کن» | حاصلضرب داخلی تکتک سطرهای ماتریس اول در تکتک ستونهای ماتریس دوم. ترتیب ضرب مهم است. |
| ترانهاده (Transpose) | «برعکسش کن» | جابهجا کردن سطرها و ستونها. یک ماتریس را به تبدیل میکند؛ در پسانتشار نقش حیاتی دارد. |
| دترمینان (Determinant) | «یک عددی از دل ماتریس» | مقیاس تغییر مساحت (دوبعدی) یا حجم (سهبعدی) توسط ماتریس. صفر بودن آن یعنی تبدیل، یک بُعد را کاملاً فشرده و نابود میکند. |
| معکوس (Inverse) | «خنثی کردن ماتریس» | ماتریسی که تبدیل انجامشده را برمیگرداند. فقط زمانی وجود دارد که دترمینان صفر نباشد. |
| ماتریس همانی (Identity matrix) | «ماتریس خستهکننده و بیاثر» | معادل ضرب در عدد 1 در دنیای ماتریسها. در اتصالات باقیمانده (شبکههای ResNet) کاربرد دارد. |
| برادکستینگ (Broadcasting) | «جادوی اصلاح خودکار ابعاد» | بسط دادن آرایهی کوچکتر با تکرار آن در امتداد ابعاد ناموجود برای هماندازه شدن با آرایهی بزرگتر. |
| نظیربهنظیر (Element-wise) | «ضرب معمولی» | ضرب درایههای متناظر در یکدیگر. هر دو آرایه باید هماندازه باشند (یا قابلیت برادکست داشته باشند). |
برای مطالعهی بیشتر
- 3Blue1Brown: جوهرهی جبر خطی - درک شهودی و تصویری تکتک عملیاتهایی که در این درس بررسی شدند
- مستندات NumPy دربارهی برادکستینگ - قوانین دقیق برادکستینگ در پکیج NumPy
- مرور جبر خطی CS229 استنفورد - مرجعی فشرده از مباحث ضروری جبر خطی برای یادگیری ماشین
