یک پرسپترون ساده توانایی ترسیم مرزهای غیرخطی را ندارد و نمیتواند حتی مسائل پایهای مثل XOR را حل کند. برای حل مسائل واقعی مانند بینایی ماشین و پردازش زبان، نیازمند ساخت مرزهای تصمیمگیری پیچیده و منحنی هستیم. شبکههای چندلایه با چیدن نورونها و اعمال توابع فعالساز غیرخطی این محدودیت را برطرف میکنند.
در این درس، مکانیزم گذر رو به جلو (Forward Pass) را از صفر و تنها با استفاده از پایتون خالص بدون وابستگی به هیچ کتابخانهای پیادهسازی میکنید. یادگیری دقیق ضربهای ماتریسی و ردیابی ابعاد لایهها پایهایترین مهارتی است که برای دیباگ مدلهای پیچیده در فریمورکهایی مانند PyTorch نیاز خواهید داشت.
تمام کدهای این درس روی پردازندهی معمولی (CPU) بدون نیاز به کارت گرافیک اجرا میشوند و نیازی به ابزارهای ابری یا تنظیمات پیچیدهی محیطی ندارید.
یک نورون فقط یک خط میکشد. آنها را روی هم بچینید تا بتوانید هر شکلی را رسم کنید.
اهداف یادگیری
- پیادهسازی یک شبکه چندلایه از صفر با کلاسهای Layer و Network که یک گذر رو به جلو (forward pass) کامل را اجرا میکنند
- ردیابی ابعاد ماتریسها در تکتک لایههای شبکه و شناسایی عدم تطابق ابعاد (Shape Mismatch)
- توضیح چگونگی ایجاد مرزهای تصمیمگیری منحنی بهکمک قرار دادن توابع فعالساز غیرخطی روی یکدیگر
- حل مسئلهی XOR با معماری 2-2-1 و وزنهای سیگموئیدِ تنظیمشده بهصورت دستی
صورت مسئله
یک نورون منفرد فقط خط میکشد؛ همین و بس. تنها یک خط مستقیم میان دادههای شما رسم میکند. همهی مسائل واقعی دنیای هوش مصنوعی — از تشخیص تصویر و فهم زبان گرفته تا بازی گو (Go) — به خطوط منحنی نیاز دارند. چیدن نورونها در قالب چند لایه دقیقاً همان راه رسیدن به این منحنیهاست.
در سال 1969، مینسکی و پاپرت ثابت کردند که این محدودیت مرگبار است: یک شبکهی تکلایه از نظر ریاضی اصلاً نمیتواند XOR را یاد بگیرد (نه اینکه صرفاً «در یادگیری آن به مشکل بخورد»). جدول درستی XOR نقطههای [0,1] و [1,0] را در یک سمت و [0,0] و [1,1] را در سمت دیگر میگذارد. هیچ خط مستقیمی وجود ندارد که بتواند آنها را از هم جدا کند.
همین مسئله بودجههای تحقیقاتی شبکههای عصبی را بیش از یک دهه متوقف کرد. راهحل این بحران، هرچند بعدها بدیهی به نظر میرسید، ساده بود: دست از شبکهی تکلایه بردارید. نورونها را در چند لایه روی هم بچینید. بگذارید لایهی اول فضای ورودی را بشکند و ویژگیهای جدید بسازد؛ سپس لایهی دوم این ویژگیها را طوری ترکیب کند که تصمیمهایی فراتر از توانِ یک خط راست بگیرد.
به این چیدمان، شبکهی چندلایه میگویند. این شبکه پایهی تمام مدلهای یادگیری عمیق (deep learning) است که امروزه در محیطهای عملیاتی میبینید. گذر رو به جلو — یعنی حرکت داده از ورودی، عبور از لایههای پنهان و رسیدن به خروجی — اولین کدی است که باید پیاده کنید تا بقیهی بخشها معنا پیدا کنند.
مفهوم
لایهها: ورودی، پنهان، خروجی
یک شبکهی چندلایه از سه نوع لایه تشکیل میشود:
لایهی ورودی — در واقع یک لایهی واقعی نیست، بلکه دادههای خام شما را در خود نگه میدارد. داشتن دو ویژگی بهمعنای دو گره ورودی است. هیچ محاسبهای در این بخش انجام نمیشود.
لایههای پنهان — جایی که کار اصلی صورت میگیرد. هر نورون تمام خروجیهای لایهی قبلی را میگیرد، وزنها و یک بایاس را روی آنها اعمال میکند و در نهایت نتیجه را از یک تابع فعالساز عبور میدهد. به این خاطر به آنها «پنهان» میگویند که شما مقادیرشان را مستقیماً در دادههای آموزشی نمیبینید.
لایهی خروجی — پاسخ نهایی را تولید میکند. برای دستهبندی دوتایی، یک نورون مجهز به سیگموئید کفایت میکند؛ برای دستهبندی چندکلاسه، به ازای هر کلاس یک نورون قرار میگیرد.
این یک شبکهی 2-3-1 است: دو ورودی، سه نورون در لایهی پنهان و یک خروجی. هر اتصال یک وزن دارد و هر نورون (بهجز گرههای ورودی) دارای یک بایاس است.
هر لایه برداری از اعداد میسازد که به آن وضعیت پنهان (Hidden state) میگویند. در پردازش متن، وضعیتهای پنهان ابعاد را افزایش میدهند — مثلاً کدگذاری یک کلمه در قالب 768 عدد برای ثبت معنای آن. در تصاویر، ابعاد را کاهش میدهند و میلیونها پیکسل را در بازنمایی کوچکتری فشرده میکنند. اصلِ یادگیری درست در همین وضعیت پنهان رخ میدهد.
نورونها و توابع فعالساز
هر نورون سه کار انجام میدهد:
- تکتک ورودیها را در وزن متناظرشان ضرب میکند
- تمام این ضربها را با هم جمع میزند و مقدار بایاس را به آنها اضافه میکند
- حاصلجمع را از یک تابع فعالساز عبور میدهد
در حال حاضر، تابع فعالساز ما تابع سیگموئید (Sigmoid Function) است:
سیگموئید هر عددی را به بازهی (0, 1) فشرده میکند. ورودیهای مثبت و بزرگ به سمت 1 push میشوند. ورودیهای منفی و بزرگ به سمت 0 push میشوند. صفر هم به 0.5 تبدیل میشود. این خمیدگیِ نرم دقیقاً همان چیزی است که یادگیری را ممکن میکند؛ برخلاف تابع پلهای و خشنِ پرسپترون، سیگموئید در تمام نقاط خود دارای گرادیان (gradient) است.
گذر رو به جلو: جریان حرکت داده
در گذر رو به جلو، دادههای ورودی لایهبهلایه در شبکه push میشوند تا به خروجی برسند. در حین گذر رو به جلو هیچ یادگیریای اتفاق نمیافتد. این مرحله صرفاً محاسبهی محض است: ضرب کن، جمع بزن، فعالساز را اعمال کن و دوباره تکرار کن.
در هر لایه، سه عملیات به ترتیب رخ میدهد:
خروجیِ یک لایه ورودیِ لایهی بعدی میشود. کل گذر رو به جلو خلاصهاش همین است.
ابعاد ماتریسها
ردیابی ابعاد ماتریسها مهمترین مهارت دیباگ در یادگیری عمیق است. جدول ابعاد برای شبکهی 2-3-1 به این صورت است:
| گام | عملیات | ابعاد | ابعاد خروجی |
|---|---|---|---|
| ورودی | -- | (2,) | |
| بخش خطی پنهان | : (3, 2)، : (3,) | (3,) | |
| فعالساز پنهان | -- | (3,) | |
| بخش خطی خروجی | : (1, 3)، : (1,) | (1,) | |
| فعالساز خروجی | -- | (1,) |
قاعده این است: ماتریس وزن در لایهی ابعادی برابر با (neurons_in_layer_k, neurons_in_layer_k_minus_1) دارد. تعداد سطرها برابر با نورونهای لایهی فعلی و تعداد ستونها برابر با نورونهای لایهی قبلی است. اگر ابعاد همخوانی نداشته باشند، یعنی کدتان باگ دارد.
قضیهی تقریب عام
در سال 1989، جرج سیبنکو نکتهی شگفتانگیزی را اثبات کرد: یک شبکهی عصبی (Neural network) با یک لایهی پنهانِ منفرد و تعداد کافی نورون میتواند هر تابع پیوستهای را با هر دقت دلخواهی تقریب بزند.
این به آن معنا نیست که یک لایهی پنهان همیشه بهترین انتخاب است؛ بلکه نشان میدهد این معماری از نظر تئوری توانایی انجامش را دارد. در عمل، شبکههای عمیقتر (لایههای بیشتر و نورونهای کمتر در هر لایه) همین توابع را با پارامترهای کلّی بسیار کمتری نسبت به شبکههای عریض و کمعمق یاد میگیرند. راز موفقیت یادگیری عمیق در همین نکته نهفته است.
درک شهودی مسئله ساده است: هر نورون در لایهی پنهان یک «برآمدگی» یا ویژگی را یاد میگیرد. وقتی برآمدگیهای کافی را در جای مناسب قرار دهید، میتوانید هر منحنی همواری را تقریب بزنید. نورونهای بیشتر یعنی برآمدگیهای بیشتر و در نتیجه تقریب دقیقتر.
ترکیبپذیری
شبکههای عصبی قابلیت ترکیبشدن (composability) دارند. میتوانید آنها را روی هم بچینید، به هم زنجیر کنید یا بهطور موازی اجرا نمایید. مدلی مثل Whisper از یک شبکهی انکودر برای پردازش صوت و یک شبکهی دیکودر مجزا برای تولید متن استفاده میکند. مدلهای زبانی بزرگ مدرن فقط دیکودر هستند؛ BERT فقط انکودر است و T5 ساختار انکودر-دیکودر دارد. این انتخاب معماری است که مرز تواناییهای مدل را مشخص میکند.
پیادهسازی از صفر
پایتون خالص. بدون numpy. تمام محاسبات ماتریسی را از صفر پیاده میکنیم.
گام 1: تابع فعالساز سیگموئید
import math
def sigmoid(x):
x = max(-500.0, min(500.0, x))
return 1.0 / (1.0 + math.exp(-x))
محدود کردن اعداد به بازهی [-500, 500] جلوی خطای سرریز (overflow) را میگیرد. مقدار math.exp(500) عددی بسیار بزرگ اما متناهی است؛ در حالی که math.exp(1000) بینهایت میشود.
گام 2: کلاس Layer
مهمترین عملیات در کل یادگیری عمیق، ضرب ماتریسی است. هر لایه، هر هد اتنشن (attention) و هر گذر رو به جلو، سراسر ضرب ماتریسی است. یک لایهی خطی یک بردار ورودی میگیرد، آن را در یک ماتریس وزن ضرب میکند و بردار بایاس را با آن جمع میزند: . همین یک رابطهی ساده 90 درصد از کل بار پردازشی یک شبکهی عصبی را تشکیل میدهد.
یک لایه یک ماتریس وزن و یک بردار بایاس دارد. متد forward آن بردار ورودی را دریافت کرده و خروجیِ فعالشده را تحویل میدهد.
class Layer:
def __init__(self, n_inputs, n_neurons, weights=None, biases=None):
if weights is not None:
self.weights = weights
else:
import random
self.weights = [
[random.uniform(-1, 1) for _ in range(n_inputs)]
for _ in range(n_neurons)
]
if biases is not None:
self.biases = biases
else:
self.biases = [0.0] * n_neurons
def forward(self, inputs):
self.last_input = inputs
self.last_output = []
for neuron_idx in range(len(self.weights)):
z = sum(
w * x for w, x in zip(self.weights[neuron_idx], inputs)
)
z += self.biases[neuron_idx]
self.last_output.append(sigmoid(z))
return self.last_output
ابعاد ماتریس وزن برابر با (n_neurons, n_inputs) است. هر سطر نشاندهندهی وزنهای یک نورون به ازای تمام ورودیهاست. متد forward میان نورونها حلقه میزند، مجموع وزندار را بههمراه بایاس حساب میکند، سیگموئید را روی آن اعمال کرده و خروجیها را جمعآوری مینماید.
گام 3: کلاس Network
یک شبکه در اصل لیستی از لایههاست. در گذر رو به جلو این لایهها زنجیروار به هم وصل میشوند: خروجیِ لایهی وارد لایهی میشود.
class Network:
def __init__(self, layers):
self.layers = layers
def forward(self, inputs):
current = inputs
for layer in self.layers:
current = layer.forward(current)
return current
تمام گذر رو به جلو همین است؛ فقط چهار خط منطق کدنویسی. داده وارد میشود، از تکتک لایهها میگذرد و از سمت دیگر بیرون میآید.
گام 4: حل XOR با وزنهای تنظیمشده بهصورت دستی
در درس 01، مسئلهی XOR را با ترکیب پرسپترونهای OR ،NAND و AND حل کردیم. حالا همان کار را با کلاسهای Layer و Network خودمان پیاده میکنیم. معماری 2-2-1: دو ورودی، دو نورون پنهان و یک خروجی.
hidden = Layer(
n_inputs=2,
n_neurons=2,
weights=[[20.0, 20.0], [-20.0, -20.0]],
biases=[-10.0, 30.0],
)
output = Layer(
n_inputs=2,
n_neurons=1,
weights=[[20.0, 20.0]],
biases=[-30.0],
)
xor_net = Network([hidden, output])
xor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
for inputs, expected in xor_data:
result = xor_net.forward(inputs)
predicted = 1 if result[0] >= 0.5 else 0
print(f" {inputs} -> {result[0]:.6f} (rounded: {predicted}, expected: {expected})")
وزنهای بزرگ (20 و -20) باعث میشوند سیگموئید شبیه یک تابع پلهای رفتار کند. اولین نورون پنهان عملگر OR را تقریب میزند و دومی عملگر NAND را شبیهسازی میکند. در نهایت نورون خروجی آنها را با عملگر AND ترکیب میکند که نتیجهاش همان XOR است.
گام 5: دستهبندی دایره
یک مسئلهی چالشبرانگیزتر: تعیین اینکه نقاط دوبعدی داخل یا خارج دایرهای به شعاع 0.5 در مرکز مختصات هستند. حل این مسئله نیازمند یک مرز تصمیمگیری (Decision Boundary) منحنی است؛ کاری که برای یک پرسپترون منفرد غیرممکن به حساب میآید.
import random
import math
random.seed(42)
data = []
for _ in range(200):
x = random.uniform(-1, 1)
y = random.uniform(-1, 1)
label = 1 if (x * x + y * y) < 0.25 else 0
data.append(([x, y], label))
circle_net = Network([
Layer(n_inputs=2, n_neurons=8),
Layer(n_inputs=8, n_neurons=1),
])
شبکه با وزنهای تصادفی دستهبندی خوبی انجام نمیدهد؛ اما گذر رو به جلو بدون مشکل اجرا میشود. نکته دقیقاً همینجاست: گذر رو به جلو صرفاً محاسبه است. یادگیریِ وزنهای درست کارِ الگوریتم پسانتشار (backpropagation) است که در درس 03 به آن میرسیم.
correct = 0
for inputs, expected in data:
result = circle_net.forward(inputs)
predicted = 1 if result[0] >= 0.5 else 0
if predicted == expected:
correct += 1
print(f"Accuracy with random weights: {correct}/{len(data)} ({100*correct/len(data):.1f}%)")
وزنهای تصادفی دقت پایینی میدهند — که اغلب حتی از حدس زدن کلاس اکثریت هم بدتر است. بعد از آموزش (در درس 03)، همین معماری با 8 نورون پنهان یک مرز منحنی رسم میکند که نقاط داخل دایره را بهخوبی از بیرون جدا خواهد کرد.
استفاده از ابزارهای واقعی
فریمورک PyTorch تمام مراحل بالا را در چهار خط خلاصه میکند:
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 8),
nn.Sigmoid(),
nn.Linear(8, 1),
nn.Sigmoid(),
)
x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
output = model(x)
print(output)
کد nn.Linear(2, 8) دقیقاً همان کلاس Layer شماست: ماتریس وزنی با ابعاد (8, 2) و بردار بایاسی با ابعاد (8,). متد nn.Sigmoid() همان تابع سیگموئید شماست که بهصورت نظیربهنظیر (element-wise) اعمال میشود. کلاس nn.Sequential هم نقش کلاس Network را دارد که لایهها را به ترتیب پشت سر هم میچیند.
تفاوت اصلی در سرعت و مقیاسپذیری است. PyTorch روی کارتهای گرافیک (GPU) اجرا میشود، دستههای میلیونی داده را مدیریت میکند و گرادیانها را برای پسانتشار بهطور خودکار به دست میآورد. با این حال، منطق پایهایِ گذر رو به جلو دقیقاً همان کدی است که خودتان از صفر ساختید.
خروجی این درس
این درس یک پرامپت (Prompt) کاربردی برای طراحی معماری شبکهها به شما تحویل میدهد:
outputs/prompt-network-architect.md
هر زمان که خواستید برای یک مسئله تصمیم بگیرید چند لایه، چند نورون در هر لایه و چه توابع فعالسازی نیاز دارید، از این پرامپت استفاده کنید.
تمرینها
-
یک شبکهی 2-4-2-1 (با دو لایهی پنهان) بسازید و گذر رو به جلو را با وزنهای تصادفی روی دادههای XOR اجرا کنید. خروجیهای لایههای پنهان میانی را چاپ کنید تا ببینید بازنمایی دادهها در هر لایه چگونه دگرگون میشود.
-
تعداد نورونهای لایهی پنهان را در دستهبند دایره از 8 به 2 و سپس به 32 تغییر دهید. هر بار گذر رو به جلو را با وزنهای تصادفی اجرا کنید. آیا تغییر تعداد نورونهای پنهان، بازه یا توزیع خروجی را تغییر میدهد؟ علت آن چیست؟
-
متدی به نام
count_parametersبه کلاس Network اضافه کنید که تعداد کل وزنها و بایاسهای آموزشپذیر را برگرداند. آن را روی یک شبکهی 784-256-128-10 (معماری کلاسیک دیتای MNIST) تست کنید. این شبکه چند پارامتر دارد؟ -
گذر رو به جلو را برای یک شبکهی 3-4-4-2 بسازید. مقادیر رنگی RGB را (نرمالشده در بازهی 0 تا 1) به آن بدهید و دو خروجی شبکه را بررسی کنید. این ساختار، معماری پایهی یک دستهبند رنگ ساده با دو کلاس مختلف است.
-
تابع سیگموئید را با یک تابع پلهایِ دارای شیب جزئی (leaky step) جایگزین کنید: اگر z < 0 بود مقدار 0.01 * z را برگردانید و در غیر این صورت مقدار 1.0. گذر رو به جلو را روی مسئلهی XOR با همان وزنهای دستیِ گام 4 اجرا کنید. آیا شبکه هنوز کار میکند؟ چرا سیگموئید هموار به توابع با مرزهای تند و ناپیوسته ترجیح داده میشود؟
اصطلاحات کلیدی
| اصطلاح | در گفتگوهای عامیانه | معنای دقیق فنی |
|---|---|---|
| گذر رو به جلو (Forward pass) | «اجرای مدل» | عبور دادن ورودی از تمام لایهها — ضرب در وزنها، افزودن بایاس و اعمال تابع فعالساز — برای تولید خروجی |
| لایهی پنهان (Hidden layer) | «بخش میانی» | هر لایهای بین ورودی و خروجی که مقادیر آن مستقیماً در دادههای اصلی دیده نمیشود |
| شبکهی چندلایه (Multi-layer network) | «شبکه عصبی عمیق» | چیدمان پشتسرهم لایههایی از نورونها که خروجی هر لایه خوراک ورودی لایهی بعدی است |
| تابع فعالساز (Activation function) | «بخش غیرخطی» | تابعی که پس از تبدیل خطی اعمال میشود تا انحنا را به مرز تصمیمگیری وارد کند |
| سیگموئید (Sigmoid) | «منحنی Sشکل» | فرمول که هر عدد حقیقی را به بازهی (0,1) فشرده میکند و هموار و در تمام نقاط مشتقپذیر است |
| ماتریس وزن (Weight matrix) | «پارامترها» | ماتریس با ابعاد (current_layer_neurons, previous_layer_neurons) شامل اتصالات آموزشپذیر شبکه |
| بردار بایاس (Bias vector) | «مقدار آفست یا جابهجایی» | برداری که بعد از ضرب ماتریسی اضافه میشود تا نورونها حتی در صورت صفر بودن ورودیها بتوانند فعال شوند |
| تقریب عام (Universal approximation) | «شبکههای عصبی همهچیز را یاد میگیرند» | یک لایهی پنهان با تعداد کافی نورون میتواند هر تابع پیوستهای را تقریب بزند — البته این تعداد کافی گاهی میلیاردها نورون است |
| تبدیل خطی (Linear transformation) | «مرحلهی ضرب ماتریسی» | رابطهی ؛ محاسبات قبل از تابع فعالساز که ورودیها را به فضایی جدید نگاشت میدهد |
| مرز تصمیمگیری (Decision boundary) | «جایی که دستهبند رفتارش عوض میشود» | سطحی در فضای ورودی که خروجی شبکه در آن نقطه از آستانه (Threshold) دستهبندی عبور میکند |
برای مطالعهی بیشتر
- مایکل نیلسن، «Neural Networks and Deep Learning»، فصل 1 و 2 (http://neuralnetworksanddeeplearning.com/) — شفافترین آموزش رایگان دربارهی گذر رو به جلو و ساختار شبکهها، همراه با تصویرسازیهای تعاملی
- مقاله سیبنکو با عنوان «Approximation by Superpositions of a Sigmoidal Function» (سال 1989) — مقالهی اصلی اثبات قضیهی تقریب عام که متنی بسیار روان و خواندنی دارد
- ویدیوی کانال 3Blue1Brown با عنوان «?But what is a neural network» (در آدرس https://www.youtube.com/watch?v=aircAruvnKk) — مروری تصویری و 20 دقیقهای بر لایهها، وزنها و گذر رو به جلو که دیدگاه ذهنی درستی به شما میدهد
- گودفلو، بنجیو و کورویل، کتاب «Deep Learning»، فصل 6 (https://www.deeplearningbook.org/) — مرجع استاندارد و معتبر شبکههای چندلایه که نسخه آنلاین آن رایگان است
