بسیاری از برنامهنویسان یادگیری عمیق را با فریمورکهای سطح بالا شروع میکنند؛ مدلهایی چندمیلیون پارامتری که با چند خط کد ساخته و آموزش داده میشوند، اما سازوکار پشت صحنهی آنها مثل یک جعبهی سیاه باقی میماند. برای فهم عمیق شبکههای عصبی، باید به نقطهی آغاز برگردیم: پرسپترون، سادهترین واحد پردازشی که تمام شبکههای مدرن از کنار هم قرار گرفتن آن ساخته شدهاند.
در این درس یاد میگیرید که چطور یک پرسپترون را از پایه با پایتون بنویسید، چرا این ساختار ساده نمیتواند مسئلهی معروفی مثل XOR را حل کند و چگونه چیدن پرسپترونها در قالب شبکههای چندلایه راه را برای هوش مصنوعی امروزی هموار کرده است. کدها کاملاً سبک و بدون نیاز به سختافزار خاص یا پردازندهی گرافیکی هستند و بهراحتی روی هر سیستمی اجرا میشوند.
پرسپترون اتمِ شبکههای عصبی (neural networks) است؛ اگر بشکافیدش، به وزنها، بایاس و یک تصمیم میرسید.
اهداف یادگیری
- پیادهسازی پرسپترون از صفر در Python، شامل قاعدهی بهروزرسانی وزنها و تابع فعالسازی پلهای
- توضیح اینکه چرا یک پرسپترونِ تنها فقط مسائل تفکیکپذیر خطی را حل میکند و نمایش نمونهی شکست در مسئلهی XOR
- ساخت یک پرسپترون چندلایه با ترکیب گیتهای OR، NAND و AND برای حل مسئلهی XOR
- آموزش یک شبکهی دولایه با تابع فعالسازی سیگموئید (sigmoid) و پسانتشار خطا (backpropagation) برای یادگیری خودکار XOR
صورت مسئله
با بردارها و ضرب داخلی (dot product) آشنا هستید. میدانید که ماتریس ورودیها را به خروجیها تبدیل میکند. اما یک ماشین چطور یاد میگیرد که از کدام تبدیل استفاده کند؟
پرسپترون پاسخ این پرسش است؛ سادهترین ماشین یادگیرندهی ممکن: چند ورودی میگیرد، آنها را در وزنها ضرب میکند، بایاس را میافزاید و تصمیمی دودویی میگیرد. سپس خود را تنظیم میکند. همهچیز همین است. هر شبکهی عصبی که تاکنون ساخته شده، چیدنِ لایهبهلایهی همین ایده روی یکدیگر است.
درک پرسپترون یعنی درک معنای واقعی «یادگیری» در دنیای کد: تغییردادن اعداد تا زمانی که خروجی با واقعیت تطبیق پیدا کند.
مفاهیم پایه
یک نورون، یک تصمیم
پرسپترون ورودی میگیرد، هر کدام را در یک وزن ضرب میکند، حاصل را با هم جمع میزند، بایاس را میافزاید و نتیجه را از یک تابع فعالسازی (activation function) عبور میدهد.
تابع پلهای (step function) رفتار قاطعی دارد: اگر مجموع وزندار بهاضافهی بایاس باشد، خروجی 1 است؛ در غیر این صورت خروجی 0 خواهد بود.
این یک دستهبند خطی (linear classifier) است. وزنها و بایاس خطی (یا در ابعاد بالاتر، ابرصفحهای) تعریف میکنند که فضای ورودی را به دو ناحیه تقسیم میکند.
مرز تصمیمگیری
برای دو ورودی، پرسپترون خطی در فضای دوبعدی رسم میکند:
x2
┤
│ Class 1 /
│ (0) /
│ /
│ / w1·x1 + w2·x2 + b = 0
│ /
│ / Class 2
│ / (1)
┼───────────/──────────── x1
هر نقطهای در یک سمتِ خط خروجی 0 و در سمت دیگر خروجی 1 میگیرد. فرایند آموزش، این خط را آنقدر جابهجا میکند تا کلاسها بهدرستی از هم تفکیک شوند.
قاعدهی یادگیری
قاعدهی یادگیری پرسپترون ساده است:
For each training example (x, y_true):
y_pred = predict(x)
error = y_true - y_pred
For each weight:
w_i = w_i + learning_rate * error * x_i
bias = bias + learning_rate * error
اگر پیشبینی درست باشد ()، چیزی تغییر نمیکند. اگر 0 پیشبینی شود ولی مقدار واقعی 1 باشد، وزنها افزایش مییابند. اگر 1 پیشبینی شود ولی مقدار واقعی 0 باشد، وزنها کاهش پیدا میکنند. نرخ یادگیری (learning rate) بزرگیِ هر بار تنظیم را مشخص میکند.
مسئلهی XOR
اینجا نقطهای است که کار به بنبست میرسد. این گیتهای منطقی را ببینید:
AND gate: OR gate: XOR gate:
x1 x2 out x1 x2 out x1 x2 out
0 0 0 0 0 0 0 0 0
0 1 0 0 1 1 0 1 1
1 0 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 0
گیتهای AND و OR تفکیکپذیر خطی (linearly separable) هستند: میتوان با یک خط راست صفرها را از یکها جدا کرد. اما XOR اینطور نیست؛ هیچ تکخطی نمیتواند [0,1] و [1,0] را از [0,0] و [1,1] جدا کند.
AND (separable): XOR (not separable):
x2 x2
1 ┤ 0 1 1 ┤ 1 0
│ / │
0 ┤ 0 / 0 0 ┤ 0 1
┼──/──────── x1 ┼──────────── x1
line works! no single line works!
این یک محدودیت بنیادین است: یک پرسپترونِ تنها فقط از پسِ مسائل تفکیکپذیر خطی برمیآید. مینسکی و پاپرت در سال 1969 این موضوع را اثبات کردند و همین باعث شد پژوهشهای شبکههای عصبی نزدیک به یک دهه عملاً متوقف شود.
راهحل: چیدن پرسپترونها در لایههای مختلف. پرسپترون چندلایه با ترکیب دو تصمیم خطی و تبدیل آنها به یک تصمیم غیرخطی، مسئلهی XOR را حل میکند.
پیادهسازی از صفر
گام 1: کلاس Perceptron
class Perceptron:
def __init__(self, n_inputs, learning_rate=0.1):
self.weights = [0.0] * n_inputs
self.bias = 0.0
self.lr = learning_rate
def predict(self, inputs):
total = sum(w * x for w, x in zip(self.weights, inputs))
total += self.bias
return 1 if total >= 0 else 0
def train(self, training_data, epochs=100):
for epoch in range(epochs):
errors = 0
for inputs, target in training_data:
prediction = self.predict(inputs)
error = target - prediction
if error != 0:
errors += 1
for i in range(len(self.weights)):
self.weights[i] += self.lr * error * inputs[i]
self.bias += self.lr * error
if errors == 0:
print(f"Converged at epoch {epoch + 1}")
return
print(f"Did not converge after {epochs} epochs")
گام 2: آموزش روی گیتهای منطقی
and_data = [
([0, 0], 0),
([0, 1], 0),
([1, 0], 0),
([1, 1], 1),
]
or_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 1),
]
not_data = [
([0], 1),
([1], 0),
]
print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
print(f" {inputs} -> {p_and.predict(inputs)}")
print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
print(f" {inputs} -> {p_or.predict(inputs)}")
print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
print(f" {inputs} -> {p_not.predict(inputs)}")
گام 3: مشاهدهی شکست در XOR
xor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
result = p_xor.predict(inputs)
status = "OK" if result == expected else "WRONG"
print(f" {inputs} -> {result} (expected {expected}) {status}")
مدل هرگز همگرا نمیشود. این اثبات عینی ناتوانی یک پرسپترون تنها در یادگیری XOR است.
گام 4: حل XOR با دو لایه
ترفند ماجرا: . سه پرسپترون را با هم ترکیب میکنیم:
def xor_network(x1, x2):
or_neuron = Perceptron(2)
or_neuron.weights = [1.0, 1.0]
or_neuron.bias = -0.5
nand_neuron = Perceptron(2)
nand_neuron.weights = [-1.0, -1.0]
nand_neuron.bias = 1.5
and_neuron = Perceptron(2)
and_neuron.weights = [1.0, 1.0]
and_neuron.bias = -1.5
hidden1 = or_neuron.predict([x1, x2])
hidden2 = nand_neuron.predict([x1, x2])
output = and_neuron.predict([hidden1, hidden2])
return output
print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
result = xor_network(inputs[0], inputs[1])
print(f" {inputs} -> {result} (expected {expected})")
هر چهار حالت بهدرستی جواب دادند. وقتی پرسپترونها را لایهبندی میکنید، مرزهای تصمیمگیریای شکل میگیرند که ساختنشان از عهدهی هیچ تکپرسپترونی برنمیآید.
گام 5: آموزش یک شبکهی دولایه
در گام 4 وزنها را دستی چیدیم. این کار برای XOR جواب میدهد، اما در مسائل واقعی که وزنهای درست را از پیش نمیدانیم شدنی نیست. راهحل: تابع پلهای را با سیگموئید جایگزین کنیم و وزنها را خودکار از طریق پسانتشار خطا یاد بگیریم.
class TwoLayerNetwork:
def __init__(self, learning_rate=0.5):
import random
random.seed(0)
self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.b_output = random.uniform(-1, 1)
self.lr = learning_rate
def sigmoid(self, x):
import math
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def forward(self, inputs):
self.inputs = inputs
self.hidden_outputs = []
for i in range(2):
z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
self.hidden_outputs.append(self.sigmoid(z))
z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
self.output = self.sigmoid(z_out)
return self.output
def train(self, training_data, epochs=10000):
for epoch in range(epochs):
total_error = 0
for inputs, target in training_data:
output = self.forward(inputs)
error = target - output
total_error += error ** 2
d_output = error * output * (1 - output)
saved_w_output = self.w_output[:]
hidden_deltas = []
for i in range(2):
h = self.hidden_outputs[i]
hd = d_output * saved_w_output[i] * h * (1 - h)
hidden_deltas.append(hd)
for i in range(2):
self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
self.b_output += self.lr * d_output
for i in range(2):
for j in range(len(inputs)):
self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
self.b_hidden[i] += self.lr * hidden_deltas[i]
net = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
result = net.forward(inputs)
predicted = 1 if result >= 0.5 else 0
print(f" {inputs} -> {result:.4f} (rounded: {predicted}, expected {expected})")
دو تفاوت کلیدی با گام 4 وجود دارد. اول اینکه سیگموئید جای تابع پلهای را میگیرد؛ تابعی هموار است، در نتیجه گرادیانها (gradients) تعریف شدهاند. دوم اینکه متد train خطا را از خروجی به لایهی پنهان به عقب میفرستد و هر وزن را متناسب با سهمش در خطا اصلاح میکند. این یعنی پسانتشار خطا در 20 خط کد.
اینجا پل ورود به درس 03 است. ریاضیِ پشتِ d_output و hidden_deltas همان قاعدهی زنجیرهای (chain rule) روی گراف شبکه است که در درس بعدی بهدقت اثباتش میکنیم.
استفاده از ابزارهای واقعی
تمام آنچه از صفر پیاده کردید، تنها با یک دستور import در دسترس است:
from sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])
clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])
تنها پنج خط کد؛ درحالیکه کلاس 30خطیِ Perceptron شما هم دقیقاً همین کار را میکند. نسخهی sklearn قابلیتهایی مثل بررسی همگرایی، چندین تابع زیان (loss) و پشتیبانی از ورودیهای تنک (sparse) را افزوده است؛ اما حلقهی اصلی کاملاً یکسان است: مجموع وزندار، تابع پلهای، و بهروزرسانی وزنها در صورت بروز خطا.
شکاف واقعی در مقیاسهای بزرگ آشکار میشود؛ تغییراتی که در شبکههای عملیاتی رخ میدهد:
- تابع پلهای جای خود را به سیگموئید، ReLU یا دیگر توابع هموار میدهد
- وزنها بهطور خودکار از طریق پسانتشار خطا یاد گرفته میشوند (درس 03)
- لایهها عمیقتر میشوند: 3، 10، 100 لایه یا بیشتر
- اصل ماجرا تغییر نمیکند: هر لایه از خروجیهای لایهی قبلی ویژگیهای جدیدی میسازد
یک پرسپترونِ تنها فقط خط راست میکشد؛ آنها را روی هم بگذارید، تا هر شکلی را برایتان رسم کنند.
خروجی این درس
این درس خروجی زیر را تولید میکند:
outputs/skill-perceptron.md- مهارتی در زمینهی شناخت زمان نیاز به معماریهای تکلایه در برابر چندلایه
تمرینها
- یک پرسپترون را روی گیت NAND آموزش دهید (گیت عمومی که هر مدار منطقی را میتوان با آن ساخت). بررسی کنید که وزنها و بایاس آن مرز تصمیمگیری معتبری تشکیل دهند.
- کلاس Perceptron را طوری تغییر دهید که در هر دوره (epoch) مرز تصمیمگیری () را ثبت کند. جابهجایی خط را در طول آموزش روی گیت AND در خروجی چاپ کنید.
- پرسپترونی با 3 ورودی بسازید که خروجی آن فقط وقتی 1 باشد که دستکم 2 ورودی از 3 ورودی، 1 باشند (تابع رأیگیری اکثریت). آیا این مسئله تفکیکپذیر خطی است؟ چرا؟
اصطلاحات کلیدی
| اصطلاح | در گفتوگوهای عامیانه | معنای واقعی و دقیق |
|---|---|---|
| پرسپترون (Perceptron) | «یک نورون مصنوعی» | یک دستهبند خطی: ضرب داخلیِ ورودیها و وزنها، بهاضافهی بایاس، که از یک تابع پلهای عبور میکند |
| وزن (Weight) | «میزان اهمیت یک ورودی» | ضریبی که اثر هر ورودی را در تصمیمگیری نهایی مقیاسبندی میکند |
| بایاس (Bias) | «آستانه (threshold)» | مقداری ثابت که مرز تصمیمگیری را جابهجا میکند و اجازه میدهد پرسپترون حتی با ورودیهای صفر هم فعال شود |
| تابع فعالسازی (Activation function) | «چیزی که مقادیر را فشرده میکند» | تابعی که پس از مجموع وزندار اعمال میشود؛ در پرسپترون تابع پلهای، و در شبکههای نوین سیگموئید یا ReLU |
| تفکیکپذیر خطی (Linearly separable) | «میشود بینشان خط کشید» | مجموعهدادهای که در آن یک ابرصفحه میتواند کلاسها را کاملاً از هم جدا کند |
| مسئلهی XOR (XOR problem) | «کاری که پرسپترون نمیتواند بکند» | اثباتی بر اینکه شبکههای تکلایه نمیتوانند توابع تفکیکناپذیر خطی را یاد بگیرند |
| مرز تصمیمگیری (Decision boundary) | «جایی که دستهبند تغییر حالت میدهد» | ابرصفحهی که فضای ورودی را به دو کلاس تفکیک میکند |
| پرسپترون چندلایه (Multi-layer perceptron) | «یک شبکهی عصبی واقعی» | پرسپترونهایی که در چند لایه چیده شدهاند؛ بهطوریکه خروجی هر لایه، ورودیِ لایهی بعدی میشود |
برای مطالعهی بیشتر
- Frank Rosenblatt, "The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain" (1958) -- مقالهی اصلی که همهچیز از آنجا آغاز شد.
- Minsky & Papert, "Perceptrons" (1969) -- کتابی که اثبات کرد XOR با شبکههای تکلایه حلنشدنی است و تحقیقات شبکههای عصبی را برای یک دهه به محاق برد.
- Michael Nielsen, "Neural Networks and Deep Learning", Chapter 1 (http://neuralnetworksanddeeplearning.com/) -- رایگان و آنلاین؛ بهترین توضیح بصری دربارهی چگونگی ترکیب پرسپترونها برای ساخت شبکهها.
