معاينة مختبر آمنة
Own Framework
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Own Framework
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
## البيرسيبترون (Perceptron) متعدد الطبقات
## بناء إطار شبكات عصبية خاص بنا
> هالدفتر جزء من منهج AI for Beginners. زوروا المستودع عشان تلقون مواد التعلّم كاملة.
بهالدفتر بنبني، خطوة خطوة، إطار شبكات عصبية خاص بنا يقدر يحل مهام **التصنيف متعدد الفئات** (Multi-Class Classification) ومسائل **الانحدار** (Regression) باستخدام البيرسيبترون متعدد الطبقات.
خلّونا أول شي نستورد المكتبات اللي نحتاجها.
# Matplotlib uses the pinned non-interactive runtime backend.
import matplotlib.pyplot as plt
from matplotlib import gridspec
from sklearn.datasets import make_classification
import numpy as np
# pick the seed for reproducibility - change it to explore the effects of random variations
np.random.seed(0)
import random## مجموعة البيانات التجريبية (Sample Dataset)
مثل قبل، بنبدأ بمجموعة البيانات التجريبية البسيطة وفيها معاملان.
n = 100
X, Y = make_classification(n_samples = n, n_features=2,
n_redundant=0, n_informative=2, flip_y=0.2)
X = X.astype(np.float32)
Y = Y.astype(np.int32)
# Split into train and test dataset
train_x, test_x = np.split(X, [n*8//10])
train_labels, test_labels = np.split(Y, [n*8//10])def plot_dataset(suptitle, features, labels):
# prepare the plot
fig, ax = plt.subplots(1, 1)
#pylab.subplots_adjust(bottom=0.2, wspace=0.4)
fig.suptitle(suptitle, fontsize = 16)
ax.set_xlabel('$x_i[0]$ -- (feature 1)')
ax.set_ylabel('$x_i[1]$ -- (feature 2)')
colors = ['r' if l else 'b' for l in labels]
ax.scatter(features[:, 0], features[:, 1], marker='o', c=colors, s=100, alpha = 0.5)
fig.show()plot_dataset('Scatterplot of the training data', train_x, train_labels)
plt.show()print(train_x[:5])
print(train_labels[:5])## مسألة تعلم الآلة (Machine Learning, ML)
نفترض إن عندنا مجموعة بيانات إدخال $\langle X,Y\rangle$، حيث $X$ مجموعة السمات و$Y$ التسميات المقابلة. في مسألة الانحدار تكون $y_i\in\mathbb{R}$، وفي التصنيف نمثّلها برقم الفئة $y_i\in\{0,\dots,n\}$.
نقدر نمثّل أي نموذج تعلم آلة بالدالة $f_\theta(x)$، حيث $\theta$ مجموعة **المعاملات** (parameters). هدفنا نلقى قيم $\theta$ اللي تخلّي النموذج يلائم مجموعة البيانات بأفضل شكل. نقيس هالشي باستخدام **دالة الخسارة** (loss function) $\mathcal{L}$، ونبي نلقى القيمة المثلى:
$$
\theta = \mathrm{argmin}_\theta \mathcal{L}(f_\theta(X),Y)
$$
دالة الخسارة تعتمد على المسألة اللي نحلّها.
### دوال الخسارة للانحدار
في الانحدار نستخدم غالبًا **الخطأ المطلق** $\mathcal{L}_{abs}(\theta) = \sum_{i=1}^n |y_i - f_{\theta}(x_i)|$، أو **متوسط الخطأ التربيعي** (mean squared error): $\mathcal{L}_{sq}(\theta) = \sum_{i=1}^n (y_i - f_{\theta}(x_i))^2$
# helper function for plotting various loss functions
def plot_loss_functions(suptitle, functions, ylabels, xlabel):
fig, ax = plt.subplots(1,len(functions), figsize=(9, 3))
plt.subplots_adjust(bottom=0.2, wspace=0.4)
fig.suptitle(suptitle)
for i, fun in enumerate(functions):
ax[i].set_xlabel(xlabel)
if len(ylabels) > i:
ax[i].set_ylabel(ylabels[i])
ax[i].plot(x, fun)
plt.show()x = np.linspace(-2, 2, 101)
plot_loss_functions(
suptitle = 'Common loss functions for regression',
functions = [np.abs(x), np.power(x, 2)],
ylabels = ['$\mathcal{L}_{abs}}$ (absolute loss)',
'$\mathcal{L}_{sq}$ (squared loss)'],
xlabel = '$y - f(x_i)$')### دوال الخسارة للتصنيف
خلّونا نفكّر شوي في التصنيف الثنائي. عندنا فئتان رقماهما 0 و 1، ومخرج الشبكة $f_\theta(x_i)\in [0,1]$ يحدّد احتمال اختيار الفئة 1.
**خسارة 0-1**
خسارة 0-1 تقابل حساب دقة النموذج؛ نحسب فيها عدد التصنيفات الصحيحة:
$$\mathcal{L}_{0-1} = \sum_{i=1}^n l_i \quad l_i = \begin{cases}
0 & (f(x_i)<0.5 \land y_i=0) \lor (f(x_i)<0.5 \land y_i=1) \\
1 & \mathrm{ otherwise}
\end{cases} \\
$$
لكن الدقة لحالها ما توضّح وش كثر ابتعدنا عن التصنيف الصح. قد يفوتنا اختيار الفئة الصحيحة بفارق بسيط، وهذا «أفضل» من فواتها بفارق كبير لأن الأوزان تحتاج تعديلًا أقل. لذلك نستخدم غالبًا **الخسارة اللوجستية**، وهي تراعي مقدار الفرق.
**الخسارة اللوجستية**
$$\mathcal{L}_{log} = \sum_{i=1}^n -y\log(f_{\theta}(x_i)) - (1-y)\log(1-f_\theta(x_i))$$
x = np.linspace(0,1,100)
def zero_one(d):
if d < 0.5:
return 0
return 1
zero_one_v = np.vectorize(zero_one)
def logistic_loss(fx):
# assumes y == 1
return -np.log(fx)plot_loss_functions(suptitle = 'Common loss functions for classification (class=1)',
functions = [zero_one_v(x), logistic_loss(x)],
ylabels = ['$\mathcal{L}_{0-1}}$ (0-1 loss)',
'$\mathcal{L}_{log}$ (logistic loss)'],
xlabel = '$p$')عشان نفهم الخسارة اللوجستية، ننظر إلى حالتين للمخرج المتوقع:
* إذا توقعنا المخرج 1 ($y=1$)، تكون الخسارة $-log f_\theta(x_i)$. تساوي الخسارة 0 إذا تنبأت الشبكة بالفئة 1 باحتمال 1، وتكبر كل ما صغر احتمال 1.
* إذا توقعنا المخرج 0 ($y=0$)، تكون الخسارة $-log(1-f_\theta(x_i))$. هنا $1-f_\theta(x_i)$ هو احتمال الفئة 0 اللي تنبأت به الشبكة، ومعنى الخسارة مثل الحالة السابقة.
## بنية الشبكة العصبية
سوّينا مجموعة البيانات لمسألة التصنيف الثنائي، لكن خلّونا نتعامل معها من البداية على إنها التصنيف متعدد الفئات؛ بهالطريقة نقدر لاحقًا نحوّل الكود بسهولة إلى أي عدد من الفئات. بهالحالة تكون بنية البيرسيبترون ذي الطبقة الوحدة كالتالي:
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
مخرجا الشبكة يقابلان الفئتين، ونختار الفئة اللي قيمة مخرجها أعلى بوصفها الحل الصحيح.
نعرّف النموذج كالتالي:
$$
f_\theta(x) = W\times x + b
$$
حيث $$\theta = \langle W,b\rangle$$ هي مجموعة المعاملات.
بنعرّف الطبقة الخطية كفئة برمجية في Python فيها دالة `forward` تنفّذ الحساب. تستقبل الدالة قيمة الإدخال $x$ وتنتج مخرج الطبقة. نخزّن المعاملين `W` و`b` داخل فئة الطبقة، ونهيّئهما عند الإنشاء بقيم عشوائية وأصفار، على الترتيب.
class Linear:
def __init__(self,nin,nout):
self.W = np.random.normal(0, 1.0/np.sqrt(nin), (nout, nin))
self.b = np.zeros((1,nout))
def forward(self, x):
return np.dot(x, self.W.T) + self.b
net = Linear(2,2)
net.forward(train_x[0:5])في حالات كثيرة يكون العمل على متجه من قيم الإدخال أكفأ من العمل على قيمة وحدة. وبما إننا نستخدم عمليات Numpy، نقدر نمرّر للشبكة متجه إدخالات فتعطينا متجه مخرجات.
## Softmax: تحويل المخرجات إلى احتمالات
مثل ما تشوفون، المخرجات الحالية مو احتمالات؛ تقدر تأخذ أي قيمة. وعشان نحوّلها إلى احتمالات، لازم نطبّع القيم عبر كل الفئات باستخدام دالة **Softmax**:
$$\sigma(\mathbf{z}_c) = \frac{e^{z_c}}{\sum_{j} e^{z_j}}, \quad\mathrm{for}\quad c\in 1 .. |C|$$
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
> نقدر نفسّر مخرج الشبكة $\sigma(\mathbf{z})$ على إنه توزيع احتمالي على مجموعة الفئات $C$: $q = \sigma(\mathbf{z}_c) = \hat{p}(c | x)$
وبنعرّف طبقة `Softmax` بالطريقة نفسها: فئة برمجية فيها الدالة `forward`:
class Softmax:
def forward(self,z):
zmax = z.max(axis=1,keepdims=True)
expz = np.exp(z-zmax)
Z = expz.sum(axis=1,keepdims=True)
return expz / Z
softmax = Softmax()
softmax.forward(net.forward(train_x[0:10]))الحين تشوفون إن المخرجات صارت احتمالات، ومجموع عناصر كل متجه مخرج يساوي 1 بالضبط.
وإذا كان عندنا أكثر من فئتين، أي أكثر من 2، تطبّع Softmax الاحتمالات على كل الفئات. هذا مخطط لبنية شبكة تنفّذ التصنيف على أرقام MNIST:
> **وصف الشكل:** مصنّف أرقام MNIST
## خسارة الانتروبيا المتقاطعة
دالة الخسارة في التصنيف تكون عادةً لوجستية، ونقدر نعمّمها إلى **خسارة الانتروبيا المتقاطعة** (cross-entropy loss). هالدالة تقيس التشابه بين أي توزيعين احتماليين. وتقدرون تقرؤون شرحًا أوسع عنها [في ويكيبيديا](https://en.wikipedia.org/wiki/Cross_entropy).
في حالتنا، التوزيع الأول هو مخرج الشبكة العصبية الاحتمالي. أما الثاني فهو توزيع **one-hot**، ويعطي الفئة المطلوبة $c$ احتمالًا يساوي 1، وبقية الفئات احتمالًا يساوي 0. بهالحالة نحسب خسارة الانتروبيا المتقاطعة بالشكل $-\log p_c$، حيث $c$ الفئة المتوقعة و$p_c$ الاحتمال اللي أعطته شبكتنا لهالفئة.
> إذا أعطت الشبكة الفئة المتوقعة احتمالًا يساوي 1، تكون خسارة الانتروبيا المتقاطعة 0. وكل ما اقترب احتمال الفئة الفعلية من 0، ارتفعت الخسارة، وقد توصل إلى ما لا نهاية.
def plot_cross_ent():
p = np.linspace(0.01, 0.99, 101) # estimated probability p(y|x)
cross_ent_v = np.vectorize(cross_ent)
f3, ax = plt.subplots(1,1, figsize=(8, 3))
l1, = plt.plot(p, cross_ent_v(p, 1), 'r--')
l2, = plt.plot(p, cross_ent_v(p, 0), 'r-')
plt.legend([l1, l2], ['$y = 1$', '$y = 0$'], loc = 'upper center', ncol = 2)
plt.xlabel('$\hat{p}(y|x)$', size=18)
plt.ylabel('$\mathcal{L}_{CE}$', size=18)
plt.show()def cross_ent(prediction, ground_truth):
t = 1 if ground_truth > 0.5 else 0
return -t * np.log(prediction) - (1 - t) * np.log(1 - prediction)
plot_cross_ent()بنعرّف خسارة الانتروبيا المتقاطعة بعد كطبقة مستقلة، لكن دالة `forward` تستقبل قيمتين: مخرج طبقات الشبكة السابقة `p`، والفئة المتوقعة `y`:
class CrossEntropyLoss:
def forward(self,p,y):
self.p = p
self.y = y
p_of_y = p[np.arange(len(y)), y]
log_prob = np.log(p_of_y)
return -log_prob.mean() # average over all input samples
cross_ent_loss = CrossEntropyLoss()
p = softmax.forward(net.forward(train_x[0:10]))
cross_ent_loss.forward(p,train_labels[0:10])> **مهم:** ترجع دالة الخسارة رقمًا يوضّح جودة أداء الشبكة. لازم ترجع رقمًا واحدًا لمجموعة البيانات كاملة، أو لجزء منها وهو الدفعة المصغّرة. لذلك، عقب ما نحسب خسارة الانتروبيا المتقاطعة لكل عنصر من متجه الإدخال، نأخذ متوسط العناصر أو مجموعها. هنا نسوي هذا باستدعاء `.mean`.
## الرسم البياني الحسابي
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
إلى هالنقطة عرّفنا فئات برمجية مختلفة لطبقات الشبكة. نقدر نمثّل تركيب هالطبقات على شكل **رسم بياني حسابي** (computational graph). والحين نقدر نحسب الخسارة لمجموعة تدريب معيّنة، أو لجزء منها، بالطريقة التالية:
z = net.forward(train_x[0:10])
p = softmax.forward(z)
loss = cross_ent_loss.forward(p,train_labels[0:10])
print(loss)## تقليل الخسارة وتدريب الشبكة
عقب ما نعرّف الشبكة على إنها $f_\theta$، ومع دالة الخسارة $\mathcal{L}(Y,f_\theta(X))$، نقدر نتعامل مع $\mathcal{L}$ كدالة في $\theta$ على مجموعة التدريب الثابتة: $\mathcal{L}(\theta) = \mathcal{L}(Y,f_\theta(X))$.
بهالحالة يصير تدريب الشبكة مسألة إيجاد قيمة $\theta$ اللي تقلّل $\mathcal{L}$:
$$
\theta = \mathrm{argmin}_{\theta} \mathcal{L}(Y,f_\theta(X))
$$
فيه طريقة معروفة لتحسين الدوال اسمها **الانحدار المتدرّج** (gradient descent). فكرتها إننا نحسب مشتقة دالة الخسارة بالنسبة إلى المعاملات، ونسمّي المشتقة متعددة الأبعاد **التدرّج**، وبعدها نغيّر المعاملات باتجاه يقلّل الخطأ.
يشتغل الانحدار المتدرّج كالتالي:
* نهيّئ المعاملات بقيم عشوائية $w^{(0)}$, $b^{(0)}$.
* نكرّر الخطوة التالية مرات كثيرة:
$$\begin{align}
W^{(i+1)}&=W^{(i)}-\eta\frac{\partial\mathcal{L}}{\partial W}\\
b^{(i+1)}&=b^{(i)}-\eta\frac{\partial\mathcal{L}}{\partial b}
\end{align}
$$
من حيث المبدأ، نحسب خطوات التحسين على مجموعة البيانات كاملة؛ تذكّروا إن الخسارة مجموع أو متوسط عبر كل عينات التدريب. لكن بالتطبيق العملي ناخذ أجزاء صغيرة اسمها **دفعات مصغّرة** (minibatches)، ونحسب التدرّجات على جزء من البيانات. ولأننا نختار هالجزء عشوائيًا كل مرة، نسمّي الطريقة **الانحدار المتدرّج العشوائي** (Stochastic Gradient Descent — SGD).
## الانتشار العكسي
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
$$\def\L{\mathcal{L}}\def\zz#1#2{\frac{\partial#1}{\partial#2}}
\begin{align}
\zz{\L}{W} =& \zz{\L}{p}\zz{p}{z}\zz{z}{W}\cr
\zz{\L}{b} =& \zz{\L}{p}\zz{p}{z}\zz{z}{b}
\end{align}
$$
عشان نحسب $\partial\mathcal{L}/\partial W$، نستخدم **قاعدة السلسلة** في اشتقاق الدالة المركّبة مثل ما توضّح الصيغ فوق. والفكرة كالتالي:
* نفترض إن إدخالًا معيّنًا أعطانا خسارة $\Delta\mathcal{L}$.
* لتقليلها، نعدّل مخرج Softmax $p$ بمقدار $\Delta p = (\partial\mathcal{L}/\partial p)\Delta\mathcal{L}$.
* هذا يقابل تغيير العقدة $z$ بمقدار $\Delta z = (\partial\mathcal{p}/\partial z)\Delta p$.
* ولتقليل الخطأ، نعدّل المعاملات وفقًا لذلك: $\Delta W = (\partial\mathcal{z}/\partial W)\Delta z$، ونسوي الشي نفسه مع $b$.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
توزّع هالعملية إشارة الخطأ من مخرج الشبكة راجعةً إلى معاملاتها؛ لذلك نسمّيها **الانتشار العكسي** (Backpropagation).
تتكوّن كل خطوة تدريب من جزأين:
* **التمرير الأمامي** (forward pass): نحسب فيه قيمة دالة الخسارة لدفعة الإدخال المصغّرة.
* **التمرير الخلفي** (backward pass): نقلّل فيه الخطأ بتوزيعه للخلف على معاملات النموذج عبر الرسم البياني الحسابي.
### تنفيذ الانتشار العكسي
* نضيف الدالة `backward` لكل عقدة عشان تحسب المشتقة وتنشر الخطأ أثناء التمرير الخلفي.
* ونحتاج بعد نطبّق تحديثات المعاملات حسب الخطوات اللي فوق.
نحسب مشتقات كل طبقة يدويًا. مثلًا للطبقة الخطية $z = x\times W+b$:
$$\begin{align}
\frac{\partial z}{\partial W} &= x \\
\frac{\partial z}{\partial b} &= 1 \\
\end{align}$$
إذا نبي نعوّض الخطأ $\Delta z$ عند مخرج الطبقة، نحدّث الأوزان وفقًا له:
$$\begin{align}
\Delta x &= \Delta z \times W \\
\Delta W &= \frac{\partial z}{\partial W} \Delta z = \Delta z \times x \\
\Delta b &= \frac{\partial z}{\partial b} \Delta z = \Delta z \\
\end{align}$$
**مهم:** ما نسوي الحسابات لكل عينة تدريب لحالها؛ نسويها على **دفعة مصغّرة** (minibatch) كاملة. نحسب تحديثَي المعاملات $\Delta W$ و$\Delta b$ على الدفعة كلها، وتكون أبعاد المتجهات المرتبطة كالتالي: $x\in\mathbb{R}^{\mathrm{minibatch}\, \times\, \mathrm{nclass}}$
class Linear:
def __init__(self,nin,nout):
self.W = np.random.normal(0, 1.0/np.sqrt(nin), (nout, nin))
self.b = np.zeros((1,nout))
self.dW = np.zeros_like(self.W)
self.db = np.zeros_like(self.b)
def forward(self, x):
self.x=x
return np.dot(x, self.W.T) + self.b
def backward(self, dz):
dx = np.dot(dz, self.W)
dW = np.dot(dz.T, self.x)
db = dz.sum(axis=0)
self.dW = dW
self.db = db
return dx
def update(self,lr):
self.W -= lr*self.dW
self.b -= lr*self.dbوبالطريقة نفسها نقدر نعرّف الدالة `backward` لبقية الطبقات:
class Softmax:
def forward(self,z):
self.z = z
zmax = z.max(axis=1,keepdims=True)
expz = np.exp(z-zmax)
Z = expz.sum(axis=1,keepdims=True)
return expz / Z
def backward(self,dp):
p = self.forward(self.z)
pdp = p * dp
return pdp - p * pdp.sum(axis=1, keepdims=True)
class CrossEntropyLoss:
def forward(self,p,y):
self.p = p
self.y = y
p_of_y = p[np.arange(len(y)), y]
log_prob = np.log(p_of_y)
return -log_prob.mean()
def backward(self,loss):
dlog_softmax = np.zeros_like(self.p)
dlog_softmax[np.arange(len(self.y)), self.y] -= 1.0/len(self.y)
return dlog_softmax / self.p## تدريب النموذج
الحين صرنا جاهزين نكتب **حلقة التدريب**. تمر الحلقة على مجموعة البيانات وتنفّذ التحسين دفعة مصغّرة عقب دفعة. ونسمّي المرور الكامل على مجموعة البيانات **حقبة تدريب** (epoch):
lin = Linear(2,2)
softmax = Softmax()
cross_ent_loss = CrossEntropyLoss()
learning_rate = 0.1
pred = np.argmax(lin.forward(train_x),axis=1)
acc = (pred==train_labels).mean()
print("Initial accuracy: ",acc)
batch_size=4
for i in range(0,len(train_x),batch_size):
xb = train_x[i:i+batch_size]
yb = train_labels[i:i+batch_size]
# forward pass
z = lin.forward(xb)
p = softmax.forward(z)
loss = cross_ent_loss.forward(p,yb)
# backward pass
dp = cross_ent_loss.backward(loss)
dz = softmax.backward(dp)
dx = lin.backward(dz)
lin.update(learning_rate)
pred = np.argmax(lin.forward(train_x),axis=1)
acc = (pred==train_labels).mean()
print("Final accuracy: ",acc)زين إننا قدرنا نرفع دقة النموذج من قرابة 50% إلى قرابة 80% في حقبة تدريب وحدة.
## فئة Net للشبكة
في حالات كثيرة تكون الشبكة العصبية مجرد تركيب من طبقات. لذلك نقدر نبني فئة برمجية تجمع الطبقات وتنفّذ التمريرين الأمامي والخلفي من دون ما نبرمج منطق الربط كل مرة. بنخزّن قائمة الطبقات داخل الفئة `Net`، ونستخدم الدالة `add` لإضافة طبقات جديدة:
class Net:
def __init__(self):
self.layers = []
def add(self,l):
self.layers.append(l)
def forward(self,x):
for l in self.layers:
x = l.forward(x)
return x
def backward(self,z):
for l in self.layers[::-1]:
z = l.backward(z)
return z
def update(self,lr):
for l in self.layers:
if 'update' in l.__dir__():
l.update(lr)مع الفئة `Net` يصير تعريف النموذج وتدريبه أرتب:
net = Net()
net.add(Linear(2,2))
net.add(Softmax())
loss = CrossEntropyLoss()
def get_loss_acc(x,y,loss=CrossEntropyLoss()):
p = net.forward(x)
l = loss.forward(p,y)
pred = np.argmax(p,axis=1)
acc = (pred==y).mean()
return l,acc
print("Initial loss={}, accuracy={}: ".format(*get_loss_acc(train_x,train_labels)))
def train_epoch(net, train_x, train_labels, loss=CrossEntropyLoss(), batch_size=4, lr=0.1):
for i in range(0,len(train_x),batch_size):
xb = train_x[i:i+batch_size]
yb = train_labels[i:i+batch_size]
p = net.forward(xb)
l = loss.forward(p,yb)
dp = loss.backward(l)
dx = net.backward(dp)
net.update(lr)
train_epoch(net,train_x,train_labels)
print("Final loss={}, accuracy={}: ".format(*get_loss_acc(train_x,train_labels)))
print("Test loss={}, accuracy={}: ".format(*get_loss_acc(test_x,test_labels)))## رسم عملية التدريب
خلّونا نشوف تدريب الشبكة بصريًا. بنعرّف الدالة `train_and_plot` لهالغرض، ونستخدم خريطة مستويات تمثّل القيم المختلفة لمخرج الشبكة بألوان مختلفة.
> لا تشيلون هم إذا ما وضحت لكم بعض تفاصيل كود الرسم تحت؛ الأهم إنكم تفهمون مفاهيم الشبكات العصبية اللي وراه.
def train_and_plot(n_epoch, net, loss=CrossEntropyLoss(), batch_size=4, lr=0.1):
fig, ax = plt.subplots(2, 1)
ax[0].set_xlim(0, n_epoch + 1)
ax[0].set_ylim(0,1)
train_acc = np.empty((n_epoch, 3))
train_acc[:] = np.NAN
valid_acc = np.empty((n_epoch, 3))
valid_acc[:] = np.NAN
for epoch in range(1, n_epoch + 1):
train_epoch(net,train_x,train_labels,loss,batch_size,lr)
tloss, taccuracy = get_loss_acc(train_x,train_labels,loss)
train_acc[epoch-1, :] = [epoch, tloss, taccuracy]
vloss, vaccuracy = get_loss_acc(test_x,test_labels,loss)
valid_acc[epoch-1, :] = [epoch, vloss, vaccuracy]
ax[0].set_ylim(0, max(max(train_acc[:, 2]), max(valid_acc[:, 2])) * 1.1)
plot_training_progress(train_acc[:, 0], (train_acc[:, 2],
valid_acc[:, 2]), fig, ax[0])
plot_decision_boundary(net, fig, ax[1])
fig.canvas.draw()
fig.canvas.flush_events()
return train_acc, valid_accimport matplotlib.cm as cm
def plot_decision_boundary(net, fig, ax):
draw_colorbar = True
# remove previous plot
while ax.collections:
ax.collections[0].remove()
draw_colorbar = False
# generate countour grid
x_min, x_max = train_x[:, 0].min() - 1, train_x[:, 0].max() + 1
y_min, y_max = train_x[:, 1].min() - 1, train_x[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
grid_points = np.c_[xx.ravel().astype('float32'), yy.ravel().astype('float32')]
n_classes = max(train_labels)+1
while train_x.shape[1] > grid_points.shape[1]:
# pad dimensions (plot only the first two)
grid_points = np.c_[grid_points,
np.empty(len(xx.ravel())).astype('float32')]
grid_points[:, -1].fill(train_x[:, grid_points.shape[1]-1].mean())
# evaluate predictions
prediction = np.array(net.forward(grid_points))
# for two classes: prediction difference
if (n_classes == 2):
Z = np.array([0.5+(p[0]-p[1])/2.0 for p in prediction]).reshape(xx.shape)
else:
Z = np.array([p.argsort()[-1]/float(n_classes-1) for p in prediction]).reshape(xx.shape)
# draw contour
levels = np.linspace(0, 1, 40)
cs = ax.contourf(xx, yy, Z, alpha=0.4, levels = levels)
if draw_colorbar:
fig.colorbar(cs, ax=ax, ticks = [0, 0.5, 1])
c_map = [cm.jet(x) for x in np.linspace(0.0, 1.0, n_classes) ]
colors = [c_map[l] for l in train_labels]
ax.scatter(train_x[:, 0], train_x[:, 1], marker='o', c=colors, s=60, alpha = 0.5)def plot_training_progress(x, y_data, fig, ax):
styles = ['k--', 'g-']
# remove previous plot
while ax.lines:
ax.lines[0].remove()
# draw updated lines
for i in range(len(y_data)):
ax.plot(x, y_data[i], styles[i])
ax.legend(ax.lines, ['training accuracy', 'validation accuracy'],
loc='upper center', ncol = 2)# Matplotlib uses the pinned non-interactive runtime backend.
net = Net()
net.add(Linear(2,2))
net.add(Softmax())
res = train_and_plot(5,net,lr=0.005)عقب تشغيل الخلية فوق، تقدرون تتابعون تفاعليًا وشلون يتغيّر الحد بين الفئات أثناء التدريب. لاحظوا إننا اخترنا معدل تعلّم صغيرًا جدًا عشان نشوف سير العملية.
## النماذج متعددة الطبقات
ركّبنا الشبكة فوق من أكثر من طبقة، لكن ما زال عندنا طبقة `Linear` وحدة هي اللي تنفّذ التصنيف الفعلي. وش يصير لو أضفنا أكثر من طبقة من هالنوع؟
المفاجأة إن الكود بيشتغل. لكن فيه نقطة مهمّة جدًا: لازم نحط بين الطبقات الخطية **دالة تنشيط غير خطية** (non-linear activation function)، مثل `tanh`. من دون اللاخطية، تكون قدرة عدة طبقات خطية على التعبير مثل قدرة طبقة وحدة؛ لأن تركيب الدوال الخطية يظل خطيًا.
class Tanh:
def forward(self,x):
y = np.tanh(x)
self.y = y
return y
def backward(self,dy):
return (1.0-self.y**2)*dyإضافة أكثر من طبقة لها فايدة؛ فبعكس الشبكة ذات الطبقة الوحدة، يقدر النموذج متعدد الطبقات يصنّف مجموعات ما تنفصل خطيًا. يعني إن النموذج متعدد الطبقات يكون **أغنى تمثيلًا**.
> يمكن إثبات إن نموذجًا بطبقتين وعدد كافٍ من العصبونات يقدر يصنّف أي مجموعة محدّبة من نقاط البيانات، وإن شبكة بثلاث طبقات تقدر تصنّف تقريبًا أي مجموعة.
رياضيًا، نمثّل البيرسيبترون متعدد الطبقات بدالة أعقد $f_\theta$ نحسبها على عدة خطوات:
* $z_1 = W_1\times x+b_1$
* $z_2 = W_2\times\alpha(z_1)+b_2$
* $f = \sigma(z_2)$
هنا $\alpha$ هي **دالة التنشيط غير الخطية**، و$\sigma$ دالة Softmax، و$\theta=\langle W_1,b_1,W_2,b_2\rangle$ هي المعاملات.
تظل خوارزمية الانحدار المتدرج نفسها، لكن حساب التدرّجات يصير أصعب. وباستخدام قاعدة السلسلة، نحسب المشتقات كالتالي:
$$\begin{align}
\frac{\partial\mathcal{L}}{\partial W_2} &= \color{red}{\frac{\partial\mathcal{L}}{\partial\sigma}\frac{\partial\sigma}{\partial z_2}}\color{black}{\frac{\partial z_2}{\partial W_2}} \\
\frac{\partial\mathcal{L}}{\partial W_1} &= \color{red}{\frac{\partial\mathcal{L}}{\partial\sigma}\frac{\partial\sigma}{\partial z_2}}\color{black}{\frac{\partial z_2}{\partial\alpha}\frac{\partial\alpha}{\partial z_1}\frac{\partial z_1}{\partial W_1}}
\end{align}
$$
لاحظوا إن بداية كل هالتعبيرات متطابقة؛ لذلك نقدر نواصل الانتشار العكسي عبر طبقات خطية أسبق، ونعدّل أوزانًا أبعد داخل الرسم البياني الحسابي.
الحين خلّونا نجرّب شبكة بطبقتين:
net = Net()
net.add(Linear(2,10))
net.add(Tanh())
net.add(Linear(10,2))
net.add(Softmax())
loss = CrossEntropyLoss()res = train_and_plot(5,net,lr=0.01)## ليه ما نستخدم دائمًا نموذجًا متعدد الطبقات؟
شفنا إن النموذج متعدد الطبقات *أقوى* و*أغنى تعبيرًا* من نموذج الطبقة الوحدة. ويمكن تسألون: ليه ما نستخدم دائمًا نموذجًا بطبقات كثيرة؟ السبب هو **فرط التكيّف** (overfitting).
بنفصّل المصطلح في أقسام جاية، لكن فكرته كذا: **كل ما زادت قدرة النموذج، صار أقدر على ملاءمة بيانات التدريب، واحتاج بيانات أكثر عشان يعمّم صح** على بيانات جديدة ما شافها قبل.
**النموذج الخطي:**
* غالبًا تكون خسارة التدريب عالية. نسمّي هذا **قصور التكيّف** (underfitting)، ويصير يوم ما تكون قدرة النموذج كافية لفصل كل البيانات صح.
* تكون خسارة التحقق قريبة من خسارة التدريب، وغالبًا يعمّم النموذج زين على بيانات الاختبار.
**النموذج المعقد متعدد الطبقات:**
* تكون خسارة التدريب منخفضة؛ لأن قدرة النموذج التعبيرية تخلّيه يلائم بيانات التدريب زين.
* قد تكون خسارة التحقق أعلى بكثير من خسارة التدريب، وقد تبدأ ترتفع أثناء التدريب؛ لأن النموذج «يحفظ» نقاط التدريب ويفقد «الصورة العامة».
> **وصف الشكل:** فرط التكيّف
> في الصورة تمثّل `x` بيانات التدريب، وتمثّل `o` بيانات التحقق. على اليسار نموذج خطي بطبقة وحدة يلائم طبيعة البيانات بشكل معقول. وعلى اليمين نموذج عنده فرط تكيّف: يلائم بيانات التدريب تقريبًا بشكل مثالي، لكنه ما يعطي نتائج معقولة على البيانات الثانية، ولذلك خطأ التحقق مرتفع جدًا.
## الزبدة
* النماذج البسيطة، بطبقات وعصبونات أقل وعدد معاملات قليل أو «سعة منخفضة»، أقل عرضة لفرط التكيّف.
* النماذج الأعقد، بطبقات وعصبونات أكثر وسعة عالية، أكثر عرضة لفرط التكيّف. لازم نراقب خطأ التحقّق ونتأكّد إنه ما يبدأ يرتفع مع استمرار التدريب.
* النماذج الأعقد تحتاج بيانات أكثر للتدريب.
* نقدر نعالج فرط التكيّف بإحدى طريقتين:
- نبسّط النموذج.
- نزيد كمية بيانات التدريب.
* تعبّر **مقايضة الانحياز والتباين** (bias-variance trade-off) عن التوازن المطلوب:
- بين قدرة النموذج وكمية البيانات.
- وبين فرط التكيّف وقصور التكيّف.
* ما فيه وصفة وحدة تحدّد عدد الطبقات أو المعاملات المناسبة؛ أفضل طريقة هي التجربة.
## الشكر والتقدير
هالدفتر جزء من منهج AI for Beginners.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.