یک شبکهی عصبی ساده#
در قدم اول برای یادگیری شبکههای عصبی، یک پیاده سازی ساده از آن را بدون استفاده از کتابخانههای رایج انجام میدهیم.
این پیاده سازی با الهام از کتاب Neural Networks and Deep Learning انجام شده است. توضیحات بیشتر را میتوانید در آن کتاب پیدا کنید.
جزئیات پیاده سازی#
این راهنما به بررسی دقیق ساختار و منطق یک شبکهی عصبی ساده که برای طبقهبندی تصاویر مجموعه دادهی MNIST طراحی شده، میپردازد. هر بخش از کد با توضیحات کامل همراه است.
۱. ساختار کلی کلاس Network#
class Network(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
توضیح:#
class Network(object):
تعریف کلاس شبکهی عصبی. این کلاس شامل وزنها، بایاسها و توابع آموزش است.def __init__(self, sizes):
تابع سازنده که آرایهای به نامsizesمیگیرد، شامل تعداد نورونها در هر لایه.self.num_layers = len(sizes)
تعیین تعداد لایهها بر اساس طول آرایهیsizes.self.sizes = sizes
ذخیره لیست ساختار لایهها در ویژگیsizes.self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
مقداردهی اولیه بایاسها برای تمام لایههای غیر از ورودی.
بایاس هر نورون از توزیع نرمال استاندارد (mean=0, std=1) مقداردهی میشود.self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
مقداردهی اولیه وزنها بین هر دو لایهی متوالی.
وزنها ماتریسهایی با ابعاد(تعداد نورونهای لایهی فعلی × تعداد نورونهای لایهی قبلی)هستند.
۲. محاسبه تعداد پارامترها#
@property
def nParams(self):
return sum(w.size for w in self.weights) + sum(b.size for b in self.biases)
توضیح:#
این ویژگی تعداد کل پارامترهای قابل یادگیری (وزن + بایاس) در شبکه را بازمیگرداند.
w.sizeوb.sizeتعداد عناصر هر وزن و بایاس را محاسبه میکند.
۳. پیشخور (Feedforward)#
def feedforward(self, a, verbose=False):
for b, w in zip(self.biases, self.weights):
if verbose:
print("وزن:", w.shape, "بایاس:", b.shape)
a = sigmoid(w @ a + b)
return a
توضیح:#
aورودی اولیه شبکه (تصویر مسطح شده) است.در هر لایه:
ابتدا
z = w @ a + bمحاسبه میشود.سپس سیگموئید روی آن اعمال میشود.
aبرای لایه بعدی بهروز میشود.
۴. انتشار معکوس (Backpropagation)#
def backprop(self, x, y):
activation = x
activations = [x]
zs = []
for b, w in zip(self.biases, self.weights):
z = w.dot(activation) + b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].T)
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].T, delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].T)
return (nabla_b, nabla_w)
توضیح:#
مرحله feedforward را تکرار میکند و
zوactivationها را ذخیره میکند.در مرحله برگشتی:
گرادیان تابع هزینه محاسبه میشود.
سپس گرادیانها برای تمام وزنها و بایاسها در هر لایه بهدست میآید.
از مشتق تابع سیگموئید استفاده میشود.
۵. بهروزرسانی وزنها با Mini-Batch#
def update_mini_batch(self, mini_batch, eta):
for x, y in mini_batch:
delta_b, delta_w = self.backprop(x, y)
nabla_b = [nb + db for nb, db in zip(nabla_b, delta_b)]
nabla_w = [nw + dw for nw, dw in zip(nabla_w, delta_w)]
self.weights = [w - (eta / len(mini_batch)) * nw for w, nw in zip(self.weights, nabla_w)]
self.biases = [b - (eta / len(mini_batch)) * nb for b, nb in zip(self.biases, nabla_b)]
توضیح:#
برای هر mini-batch از دادههای آموزشی:
گرادیانها محاسبه میشود.
وزنها و بایاسها با میانگین گرادیانها و نرخ یادگیری
etaبهروزرسانی میشوند.
۶. توابع سیگموئید و مشتق آن#
def sigmoid(z): return 1.0 / (1.0 + np.exp(-z))
def sigmoid_prime(z): return sigmoid(z) * (1 - sigmoid(z))
توضیح:#
سیگموئید تابع فعالسازی استاندارد برای نورونهاست.
مشتق آن برای backpropagation ضروری است.
تابع cost_derivative#
def cost_derivative(self, output_activations, y):
return (output_activations - y)
گرادیان تابع هزینه نسبت به خروجی شبکه: [ rac{\partial C}{\partial a} = a - y ]
فرض شده که تابع هزینه از نوع Mean Squared Error است.
کد کامل کلاس شبکهی عصبی#
در زیر کد کامل کلاس را میتوانید به صورت یکجا ملاحظه و اجرا نمایید.
"""
یک ماژول برای پیادهسازی الگوریتم یادگیری گرادیان کاهش تصادفی برای یک شبکه عصبی پیشخور.
گرادیانها با استفاده از روش انتشار معکوس محاسبه میشوند.
"""
# کتابخانههای استاندارد
import random
# کتابخانههای لازم
import numpy as np
class Network(object):
"""کلاس شبکه عصبی"""
def __init__(self, sizes):
"""مقداردهی اولیه شبکه عصبی
پارامترها:
sizes -- لیستی که تعداد نورونهای هر لایه را مشخص میکند.
مثال:
[2, 3, 1] برای شبکهای با 2 نورون ورودی،
3 نورون در لایه پنهان و 1 نورون در لایه خروجی
"""
self.num_layers = len(sizes)
self.sizes = sizes
# مقداردهی تصادفی بایاسها با توزیع نرمال
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
# مقداردهی تصادفی وزنها با توزیع نرمال
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
def feedforward(self, a , verbose=False):
"""محاسبه خروجی شبکه برای ورودی داده شده
پارامترها:
a -- ورودی شبکه
برگشت:
خروجی شبکه
"""
for b, w in zip(self.biases, self.weights):
if verbose:
print("ورودی لایه:", a , a.shape)
a = sigmoid( w@a + b)
if verbose:
print("وزنها:", w , w.shape)
print("بایاسها:", b , b.shape)
print("خروجی لایه:", a , a.shape)
return a
def __call__(self, a):
"""اجرا کردن شبکه با ورودی داده شده
پارامترها:
a -- ورودی شبکه
برگشت:
خروجی شبکه
"""
return self.feedforward(a , False)
@property
def nParams(self):
"""محاسبه تعداد پارامترهای شبکه
برگشت:
تعداد کل پارامترها (وزنها و بایاسها)
"""
return sum(w.size for w in self.weights) + sum(b.size for b in self.biases)
def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):
"""آموزش شبکه با گرادیان کاهش تصادفی روی دستههای کوچک
پارامترها:
training_data -- لیست دادههای آموزشی به صورت (ورودی, خروجی مطلوب)
epochs -- تعداد دورههای آموزشی
mini_batch_size -- اندازه هر دسته کوچک
eta -- نرخ یادگیری
test_data -- دادههای آزمون (اختیاری)
"""
if test_data: n_test = len(test_data)
n = len(training_data)
for j in range(epochs):
random.shuffle(training_data)
# ایجاد دستههای کوچک
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
# آموزش روی هر دسته کوچک
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
# نمایش پیشرفت آموزش
if test_data:
print("دوره {0}: {1} / {2}".format(
j, self.evaluate(test_data), n_test))
else:
print("دوره {0} تکمیل شد".format(j))
def update_mini_batch(self, mini_batch, eta):
"""بهروزرسانی وزنها و بایاسها برای یک دسته کوچک
پارامترها:
mini_batch -- یک دسته کوچک از دادههای آموزشی
eta -- نرخ یادگیری
"""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
# محاسبه گرادیانها با انتشار معکوس
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# بهروزرسانی وزنها و بایاسها
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
def backprop(self, x, y):
"""الگوریتم انتشار معکوس برای محاسبه گرادیانها
پارامترها:
x -- ورودی آموزشی
y -- خروجی مطلوب
برگشت:
یک تاپل (nabla_b, nabla_w) که گرادیانهای تابع هزینه را نشان میدهد
"""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# مرحله پیشخور
activation = x
activations = [x] # ذخیره فعالسازیهای هر لایه
zs = [] # ذخیره ورودیهای هر لایه قبل از اعمال تابع فعالسازی
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# مرحله پسخور
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# محاسبه گرادیانها برای لایههای قبلی
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)
def evaluate(self, test_data):
"""ارزیابی عملکرد شبکه روی داده آزمون
پارامترها:
test_data -- دادههای آزمون
برگشت:
تعداد پیشبینیهای صحیح
"""
#test_results = [ (self.feedforward(x)-y)**2
# for (x, y) in test_data]
test_results = [(np.argmax(self.feedforward(x)), np.argmax(y) )
for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)
#return sum(test_results) / len(test_data)
def cost_derivative(self, output_activations, y):
"""محاسبه مشتق تابع هزینه
پارامترها:
output_activations -- فعالسازیهای لایه خروجی
y -- خروجی مطلوب
برگشت:
مشتق تابع هزینه نسبت به فعالسازیهای خروجی
"""
return (output_activations-y)
# توابع کمکی
def sigmoid(z):
"""تابع فعالسازی سیگموئید"""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""مشتق تابع سیگموئید"""
return sigmoid(z)*(1-sigmoid(z))
بارگذاری داده ها#
در زیر کدهایی که در بخش قبل برای دانلود دادههای MNIST نوشته بودیم را مجدد آورده ایم. برای درک این کد و نحوه ی استفاده از آن به درس جلسه ی گذشته رجوع کنید
#A simple code to load and show MNIST dataset
#### Libraries
# Standard library
import pickle
import gzip
# Third-party libraries
import numpy as np
import matplotlib.pyplot as plt
def download_dataset(url, filename):
"""
Download the dataset from the given URL if it is not already present.
"""
import os
import requests
# make sure the directory exists
os.makedirs(os.path.dirname(filename), exist_ok=True)
# Check if the file already exists
# If it does not exist, download it
# If it does exist, do nothing
if not os.path.exists(filename):
print(f"Downloading {filename}...")
response = requests.get(url)
with open(filename, 'wb') as f:
f.write(response.content)
print(f"Downloaded {filename}")
return True
def load_data():
"""
Load the MNIST dataset from a gzipped pickle file.
"""
# Download the dataset if it is not already present
dataset_url = 'https://github.com/unexploredtest/neural-networks-and-deep-learning/raw/refs/heads/master/data/mnist.pkl.gz' #'http://deeplearning.net/data/mnist/mnist.pkl.gz'
dataset_filename = '../data/mnist.pkl.gz'
download_dataset(dataset_url, dataset_filename)
f = gzip.open('../data/mnist.pkl.gz', 'rb')
u = pickle._Unpickler(f)
u.encoding = 'latin1'
training_data, validation_data, test_data = u.load()
f.close()
return (training_data, validation_data, test_data)
def show_data(data , index=0 , ax=None):
"""
Show the MNIST data.
"""
# Extract the first image and label from the training data
image, label = data[0][index], data[1][index] #loads the image and its label
# data is stored as a flat array of 784 pixels (28x28)
# Reshape the image to 28x28 pixels
image = image.reshape(28, 28)
# Display the image
ax.imshow(image, cmap='gray')
ax.set_title(f'Label: {label}')
ax.axis('off')
training_data, validation_data, test_data = load_data()
Downloading ../data/mnist.pkl.gz...
Downloaded ../data/mnist.pkl.gz
اجرا و آموزش مدل#
حال که داده ها آماده هستند لازم است شکل آنها را
yvalues = np.zeros( (50000 , 10))
yvalues[range(50000) , training_data[1] ] = 1
training_d = list(zip(training_data[0].reshape(-1 , 784 , 1), yvalues.reshape(-1 , 10 , 1)))
validation_d = list(zip(validation_data[0].reshape(-1 , 784 , 1), validation_data[1]))
yvalues_t = np.zeros( (10000 , 10))
yvalues_t[range(10000) , test_data[1] ] = 1
test_d = list(zip(test_data[0].reshape(-1 , 784 , 1), yvalues_t.reshape(-1 , 10 , 1)))
nn = Network([784, 30, 10])
nn.SGD(training_d, 5, 1000 , 1,
test_data=test_d)
دوره 0: 1049 / 10000
دوره 1: 1517 / 10000
دوره 2: 1878 / 10000
دوره 3: 2299 / 10000
دوره 4: 2638 / 10000