فاز 1 · درس 2

جبر خطی کاربردی: ساخت ماتریس‌ها و بردارها از صفر در پایتون

نوع
ساختنی
زمان
حدود 60 دقیقه
زبان‌ها
Python, Julia
شبکه‌ای هندسی از مکعب‌ها و نقاط درخشان با خطوط تبدیل فضایی و بردارها روی پس‌زمینه‌ی تاریک
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. بردارها: لیست‌های مرتبی از اعداد
  5. ماتریس‌ها: شبکه‌ای از اعداد
  6. چرا ابعاد اهمیت دارند؟
  7. نقشه‌ی عملیات جبری
  8. تفاوت ضرب نظیر‌به‌نظیر و ضرب ماتریسی
  9. برادکستینگ (Broadcasting)
  10. پیاده‌سازی از صفر
  11. گام 1: کلاس Vector
  12. گام 2: کلاس Matrix با عملیات پایه
  13. گام 3: مشاهده‌ی عملکرد در عمل
  14. گام 4: اتصال به شبکه‌های عصبی
  15. استفاده در عمل
  16. تحویل پروژه
  17. تمرین‌ها
  18. راهنمایی‌ها
  19. اصطلاحات کلیدی
  20. برای مطالعه‌ی بیشتر

بسیاری از توسعه‌دهندگانی که یادگیری عمیق را آغاز می‌کنند، خطوط کدی مانند `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) یک شبکه‌ی دوبعدی از سطرها و ستون‌هاست. یک ماتریس m×nm \times n دارای mm سطر و nn ستون است.

A=[123456]2x3 matrix (2 rows, 3 columns)A = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} \quad \text{2x3 matrix (2 rows, 3 columns)}

در شبکه‌های عصبی، ماتریس‌های وزن، بردارهای ورودی را به بردارهای خروجی تبدیل می‌کنند. لایه‌ای با 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)

تفاوت ضرب نظیر‌به‌نظیر و ضرب ماتریسی

این تمایز معمولاً افراد تازه‌کار را به اشتباه می‌اندازد.

ضرب نظیر‌به‌نظیر: درایه‌های متناظر در هم ضرب می‌شوند. ابعاد هر دو ماتریس باید دقیقاً یکسان باشد.

[1234]∗[5678]=[5122132]\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} * \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 5 & 12 \\ 21 & 32 \end{bmatrix}

ضرب ماتریسی: حاصل‌ضرب داخلی سطرها در ستون‌هاست. ابعاد درونی باید با هم بخوانند.

[1234]@[5678]=[1⋅5+2⋅71⋅6+2⋅83⋅5+4⋅73⋅6+4⋅8]=[19224350]\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} @ \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 1 \cdot 5 + 2 \cdot 7 & 1 \cdot 6 + 2 \cdot 8 \\ 3 \cdot 5 + 4 \cdot 7 & 3 \cdot 6 + 4 \cdot 8 \end{bmatrix} = \begin{bmatrix} 19 & 22 \\ 43 & 50 \end{bmatrix}

دو عملیات متفاوت، دو نتیجه‌ی متفاوت و دو قانون متفاوت.

برادکستینگ (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 |

تمام فریم‌ورک‌های مدرن این کار را خودکار انجام می‌دهند. درک این سازوکار مانع از سردرگمی شما در مواقعی می‌شود که ابعاد ظاهراً هم‌خوانی ندارند، اما کد بدون خطا اجرا می‌شود.

تصویر بردارزاویه‌ها را بکشید
VECTOR PROJECTIONBlue is a, grey is the direction of b, orange is the shadow a casts on b. The scalar projection |a|cos θ shrinks to zero when the vectors are perpendicular and goes negative when the angle passes 90°. The dashed line is the perpendicular dropped from a to its foot on b.
رنگ آبی بردار a، رنگ خاکستری جهت b، و رنگ نارنجی سایه‌ای است که a روی b می‌اندازد. تصویر اسکالر θ|a|cos با عمود شدن بردارها به صفر میل می‌کند و وقتی زاویه از 90° بیشتر شود، منفی می‌شود. خط‌چین نیز خط عمودی است که از a به پای عمود آن روی b رسم شده است.

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

گام 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 پیاده‌سازی خواهیم کرد.

تمرین‌ها

  1. بررسی معکوس ماتریس. حاصل A @ A.inverse_2x2() را محاسبه و تأیید کنید که ماتریس همانی به دست می‌آید. این کار را با سه ماتریس 2x2 مختلف امتحان کنید. وقتی دترمینان ماتریس صفر باشد چه اتفاقی می‌افتد؟

  2. پیاده‌سازی معکوس ماتریس 3x3. کلاس Matrix را طوری توسعه دهید که با روش ماتریس الحاقی (adjugate)، معکوس ماتریس‌های 3x3 را محاسبه کند. عملکرد آن را در مقایسه با np.linalg.inv در NumPy بسنجید.

  3. ساخت یک شبکه دو لایه. صرفاً با استفاده از کلاس 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) «برعکسش کن» جابه‌جا کردن سطرها و ستون‌ها. یک ماتریس m×nm \times n را به n×mn \times m تبدیل می‌کند؛ در پس‌انتشار نقش حیاتی دارد.
دترمینان (Determinant) «یک عددی از دل ماتریس» مقیاس تغییر مساحت (دوبعدی) یا حجم (سه‌بعدی) توسط ماتریس. صفر بودن آن یعنی تبدیل، یک بُعد را کاملاً فشرده و نابود می‌کند.
معکوس (Inverse) «خنثی کردن ماتریس» ماتریسی که تبدیل انجام‌شده را برمی‌گرداند. فقط زمانی وجود دارد که دترمینان صفر نباشد.
ماتریس همانی (Identity matrix) «ماتریس خسته‌کننده و بی‌اثر» معادل ضرب در عدد 1 در دنیای ماتریس‌ها. در اتصالات باقیمانده (شبکه‌های ResNet) کاربرد دارد.
برادکستینگ (Broadcasting) «جادوی اصلاح خودکار ابعاد» بسط دادن آرایه‌ی کوچک‌تر با تکرار آن در امتداد ابعاد ناموجود برای هم‌اندازه شدن با آرایه‌ی بزرگ‌تر.
نظیر‌به‌نظیر (Element-wise) «ضرب معمولی» ضرب درایه‌های متناظر در یکدیگر. هر دو آرایه باید هم‌اندازه باشند (یا قابلیت برادکست داشته باشند).

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

خودآزمایی

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

  1. پیش از درسبرای ضرب ماتریسی (m x n) @ (n x p)، چه شرطی در ابعاد آن‌ها باید برقرار باشد؟
  2. حین درسماتریس همانی چگونه تعریف می‌شود؟
  3. حین درستفاوت کلیدی بین ضرب درایه‌ای و ضرب ماتریسی چیست؟
  4. حین درسدر عبارت 'output = relu(W @ x + b)'، برادکستینگ (broadcasting) چه نقشی ایفا می‌کند؟
  5. پس از درسکدام ویژگی ناشی از دترمینان صفر است؟
  6. پس از درسعملیات ترانهاده چه تغییری در ماتریس ایجاد می‌کند؟

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

تفاوت ضرب نظیر‌به‌نظیر و ضرب ماتریسی چیست؟

در ضرب نظیر‌به‌نظیر (element-wise)، درایه‌های متناظر دو ماتریس هم‌ابعاد در یکدیگر ضرب می‌شوند. در مقابل، ضرب ماتریسی حاصل‌ضرب داخلی سطرها در ستون‌هاست و شرط اجرای آن، برابری تعداد ستون‌های ماتریس اول با تعداد سطرهای ماتریس دوم است.

برادکستینگ (Broadcasting) در پایتون و NumPy چه کاربردی دارد؟

هنگامی که ابعاد دو آرایه در عملیاتی مانند جمع یکسان نباشد (مثلاً جمع بردار بایاس با ماتریس خروجی لایه)، برادکستینگ آرایه‌ی کوچک‌تر را در امتداد بعدهای ناموجود تکرار می‌کند تا با آرایه‌ی بزرگ‌تر هم‌شکل شود و عملیات بدون خطا اجرا گردد.

چرا ضرب ماتریس‌ها در NumPy بسیار سریع‌تر از کدهای پایتون خام است؟

زیرا NumPy عملگر ضرب ماتریسی (`@`) را از طریق توابع بهینه‌سازی‌شده‌ی کتابخانه‌های BLAS در زبان‌های C و Fortran اجرا می‌کند و از قابلیت‌های پردازش برداری سخت‌افزار بهره می‌برد.

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

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