معاينة مختبر آمنة
Intro Py Torch
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Intro Py Torch
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
## مقدمة إلى PyTorch
> هالدفتر جزء من منهج الذكاء الاصطناعي للمبتدئين. زوروا صفحة المنهج عشان تلقون مواد التعلّم كاملة.
### أطر الشبكات العصبية
تعلمنا إن تدريب الشبكات العصبية يحتاج منّا:
* نضرب المصفوفات (الموترات) بسرعة
* نحسب التدرجات عشان ننفّذ تحسين الانحدار المتدرج
وش تخلّيكم أطر الشبكات العصبية تسوون:
* تشغّلون عمليات الموترات على وحدة الحوسبة المتوفرة، سواءً CPU أو GPU أو حتى TPU
* تحسبون التدرجات آليًا؛ لأن مشتقات كل دوال الموترات المدمجة مبرمجة داخل الإطار
وتعطيكم—اختياريًا:
* أداة لبناء الشبكة العصبية أو API عالية المستوى تصف الشبكة كسلسلة طبقات
* دوال تدريب بسيطة مثل `fit` في Scikit Learn
* عدة خوارزميات تحسين غير الانحدار المتدرج
* تجريدات للتعامل مع البيانات، والمفروض إنها تشتغل على GPU بعد
### مسارا الإطار في هالدورة
تستخدم هالدورة PyTorch في مسار وTensorFlow مع Keras 3 في المسار الثاني. PyTorch يوفر تنفيذًا مباشرًا وتفاضلًا تلقائيًا وبناء نماذج مرنًا. اعتماديات الدفتر مثبتة في ملف التشغيل، لذلك ما نثبّت حزمًا من الخلايا.
import torch
torch.__version__## المفهوم الأساسي: الموتر
**الموتر** مصفوفة متعددة الأبعاد، ويفيدنا بتمثيل أنواع مختلفة من البيانات:
* 400x400 - صورة بالأبيض والأسود
* 400x400x3 - صورة ملوّنة
* 16x400x400x3 - دفعة مصغّرة من 16 صورة ملوّنة
* 25x400x400x3 - ثانية وحدة من فيديو بمعدل 25 إطارًا بالثانية
* 8x25x400x400x3 - دفعة مصغّرة من 8 مقاطع، مدة كل واحد منها 1 ثانية
### موترات بسيطة
نقدر ننشئ موترات بسيطة بسهولة من قوائم مصفوفات np، أو نولّد موترات عشوائية:
a = torch.tensor([[1,2],[3,4]])
print(a)
a = torch.randn(size=(10,3))
print(a)نقدر نستخدم العمليات الحسابية على الموترات، وتتنفّذ عنصرًا بعنصر مثل numpy. وإذا احتجنا بُعدًا إضافيًا، تتوسع الموترات آليًا إلى الأبعاد المطلوبة. ولاستخراج مصفوفة numpy من موتر، نستخدم `.numpy`:
print(a-a[0])
print(torch.exp(a)[0].numpy())## العمليات داخل المكان وخارجه
عمليات الموترات مثل `+`/`add` ترجع موترات جديدة. لكن أحيانًا نحتاج نعدّل الموتر الموجود في مكانه. أغلب العمليات لها نسخة تنفّذ التعديل في المكان نفسه، وتنتهي بـ `_`:
u = torch.tensor(5)
print("Result when adding out-of-place:",u.add(torch.tensor(3)))
u.add_(torch.tensor(3))
print("Result after adding in-place:", u)هذي طريقة مباشرة وبسيطة نحسب بها مجموع كل الصفوف في المصفوفة:
s = torch.zeros_like(a[0])
for i in a:
s.add_(i)
print(s)لكن الأفضل بكثير نستخدم:
torch.sum(a,axis=0)تقدرون تقرون أكثر عن موترات PyTorch في [التوثيق الرسمي](https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html)
## حساب التدرجات
عشان ننفّذ الانتشار العكسي نحتاج نحسب التدرجات. نقدر نضبط الخاصية `requires_grad` لأي موتر PyTorch على `True`، ووقتها يتتبع PyTorch كل العمليات على هالموتر لحساب التدرجات. ولحسابها نستدعي الدالة `backward`، وبعدها يصير التدرج متوفرًا في الخاصية `grad`:
a = torch.randn(size=(2, 2), requires_grad=True)
b = torch.randn(size=(2, 2))
c = torch.mean(torch.sqrt(torch.square(a) + torch.square(b))) # Do some math using `a`
c.backward() # call backward() to compute all gradients
# What's the gradient of `c` with respect to `a`?
print(a.grad)للتوضيح بشكل أدق، PyTorch **يراكم** التدرجات آليًا. إذا حددنا `retain_graph=True` وقت استدعاء `backward`، يحتفظ برسم الحسابات ويضيف التدرج الجديد إلى الحقل `grad`. وإذا بغينا نعيد حساب التدرجات من الصفر، لازم نصفر الحقل `grad` بوضوح إلى 0 عن طريق استدعاء `zero_`:
c = torch.mean(torch.sqrt(torch.square(a) + torch.square(b)))
c.backward(retain_graph=True)
c.backward(retain_graph=True)
print(a.grad)
a.grad.zero_()
c.backward()
print(a.grad)عشان يحسب PyTorch التدرجات، ينشئ **رسم حسابات** ويحافظ عليه. ولكل موتر خاصيته `requires_grad` مضبوطة على `True`، يحتفظ PyTorch بدالة خاصة اسمها `grad_fn` تحسب مشتقة التعبير بحسب قاعدة السلسلة في الاشتقاق:
print(c)هنا حُسب `c` باستخدام الدالة `mean`، ولذلك تشير `grad_fn` إلى دالة اسمها `MeanBackward`.
في أغلب الحالات نبي PyTorch يحسب تدرج دالة قياسية مثل دالة الخسارة. لكن إذا بغينا نحسب تدرج موتر بالنسبة إلى موتر ثاني، يخلّينا PyTorch نحسب حاصل ضرب مصفوفة جاكوبي في متجه معيّن.
لنفترض إن عندنا دالة متجهة $\vec{y}=f(\vec{x})$، حيث
$\vec{x}=\langle x_1,\dots,x_n\rangle$ و
$\vec{y}=\langle y_1,\dots,y_m\rangle$، وقتها نعرّف تدرج $\vec{y}$ بالنسبة إلى $\vec{x}$ باستخدام **مصفوفة جاكوبي**:
$$
\begin{align}J=\left(\begin{array}{ccc}
\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\
\vdots & \ddots & \vdots\\
\frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}
\end{array}\right)\end{align}
$$
بدل ما يعطينا PyTorch مصفوفة جاكوبي كاملة، يحسب حاصل الضرب $v^T\cdot J$ بينها وبين المتجه
$v=(v_1 \dots v_m)$. عشان نسوي هذا، نستدعي ``backward`` ونمرر `v` كوسيط. لازم يكون حجم `v` مثل حجم الموتر الأصلي اللي نحسب التدرج بالنسبة له.
c = torch.sqrt(torch.square(a) + torch.square(b))
c.backward(torch.eye(2)) # eye(2) means 2x2 identity matrix
print(a.grad)بتلقون تفاصيل أكثر عن حساب مصفوفات جاكوبي في PyTorch داخل [التوثيق الرسمي](https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html)
# المثال 0: التحسين باستخدام الانحدار المتدرج
خلونا نستخدم الاشتقاق الآلي لإيجاد أصغر قيمة لدالة بسيطة بمتغيرين $f(x_1,x_2)=(x_1-3)^2+(x_2+2)^2$. نخلي الموتر `x` يحمل الإحداثيات الحالية للنقطة. نبدأ من نقطة ابتدائية $x^{(0)}=(0,0)$، ثم نحسب النقطة التالية في المتتالية بصيغة الانحدار المتدرج:
$$
x^{(n+1)} = x^{(n)} - \eta\nabla f
$$
هنا $\eta$ هو **معدل التعلّم**—وبنرمز له في الكود بـ `lr`—و$\nabla f = (\frac{\partial f}{\partial x_1},\frac{\partial f}{\partial x_2})$ هو تدرج $f$.
بالبداية نعرّف القيمة الابتدائية لـ `x` والدالة `f`:
x = torch.zeros(2,requires_grad=True)
f = lambda x : (x-torch.tensor([3,-2])).pow(2).sum()
lr = 0.1الحين بننفّذ 15 تكرارًا من الانحدار المتدرج. في كل تكرار نحدّث إحداثيات `x` ونطبعها عشان نتأكد إننا نقترب من نقطة الحد الأدنى (3,-2):
for i in range(15):
y = f(x)
y.backward()
gr = x.grad
x.data.add_(-lr*gr)
x.grad.zero_()
print("Step {}: x[0]={}, x[1]={}".format(i,x[0],x[1]))## المثال 1: الانحدار الخطي
الحين صار عندنا أساس كافي لحل مسألة **الانحدار الخطي** المعروفة. خلونا نولّد مجموعة البيانات الاصطناعية الصغيرة هذي:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification, make_regression
from sklearn.model_selection import train_test_split
import randomnp.random.seed(13) # pick the seed for reproducibility - change it to explore the effects of random variations
train_x = np.linspace(0, 3, 120)
train_labels = 2 * train_x + 0.9 + np.random.randn(*train_x.shape) * 0.5
plt.scatter(train_x,train_labels)نعرّف الانحدار الخطي بخط مستقيم $f_{W,b}(x) = Wx+b$، حيث $W, b$ معاملات النموذج اللي نحتاج نلقاها. ونقدر نعرّف الخطأ على مجموعة البيانات $\{x_i,y_u\}_{i=1}^N$—ويسمّى بعد **دالة الخسارة**—بمتوسط مربع الخطأ:
$$
\mathcal{L}(W,b) = {1\over N}\sum_{i=1}^N (f_{W,b}(x_i)-y_i)^2
$$
خلونا نعرّف النموذج ودالة الخسارة:
input_dim = 1
output_dim = 1
learning_rate = 0.1
# This is our weight matrix
w = torch.tensor([100.0],requires_grad=True,dtype=torch.float32)
# This is our bias vector
b = torch.zeros(size=(output_dim,),requires_grad=True)
def f(x):
return torch.matmul(x,w) + b
def compute_loss(labels, predictions):
return torch.mean(torch.square(labels - predictions))بندرّب النموذج على سلسلة من الدفعات المصغّرة. وبنستخدم الانحدار المتدرج ونعدّل معاملات النموذج بالمعادلات هذي:
$$
\begin{array}{l}
W^{(n+1)}=W^{(n)}-\eta\frac{\partial\mathcal{L}}{\partial W} \\
b^{(n+1)}=b^{(n)}-\eta\frac{\partial\mathcal{L}}{\partial b} \\
\end{array}
$$
def train_on_batch(x, y):
predictions = f(x)
loss = compute_loss(y, predictions)
loss.backward()
w.data.sub_(learning_rate * w.grad)
b.data.sub_(learning_rate * b.grad)
w.grad.zero_()
b.grad.zero_()
return lossخلونا نبدأ التدريب. بنمر على مجموعة البيانات عدة مرات—وكل مرة نسمّيها **حقبة**—ونقسّمها إلى دفعات مصغّرة، ثم نستدعي الدالة اللي عرّفناها فوق:
# Shuffle the data.
indices = np.random.permutation(len(train_x))
features = torch.tensor(train_x[indices],dtype=torch.float32)
labels = torch.tensor(train_labels[indices],dtype=torch.float32)batch_size = 4
for epoch in range(10):
for i in range(0,len(features),batch_size):
loss = train_on_batch(features[i:i+batch_size].view(-1,1),labels[i:i+batch_size])
print('Epoch %d: last batch loss = %.4f' % (epoch, float(loss)))الحين حصلنا على القيم المحسّنة للمعاملين $W$ و$b$. لاحظوا إن قيمهم قريبة من القيم الأصلية اللي استخدمناها لما ولّدنا مجموعة البيانات ($W=2, b=1$)
w,bplt.scatter(train_x,train_labels)
x = np.array([min(train_x),max(train_x)])
with torch.no_grad():
y = w.numpy()*x+b.numpy()
plt.plot(x,y,color='red')## التنفيذ على GPU
عشان نستخدم GPU في الحسابات، يدعم PyTorch نقل الموترات إلى GPU وبناء رسم الحسابات عليه. المعتاد إننا نعرّف في بداية الكود وحدة الحوسبة المتوفرة داخل المتغير `device`—وتكون إما `cpu` أو `cuda`—ثم ننقل كل الموترات لها باستدعاء `.to(device)`. ونقدر بعد ننشئ الموترات من البداية على الوحدة المحددة بتمرير المعامل `device=...` إلى كود الإنشاء. بهالشكل يشتغل الكود نفسه على CPU وGPU من دون تعديلات:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Doing computations on '+device)
### Changes here: indicate device
w = torch.tensor([100.0],requires_grad=True,dtype=torch.float32,device=device)
b = torch.zeros(size=(output_dim,),requires_grad=True,device=device)
def f(x):
return torch.matmul(x,w) + b
def compute_loss(labels, predictions):
return torch.mean(torch.square(labels - predictions))
def train_on_batch(x, y):
predictions = f(x)
loss = compute_loss(y, predictions)
loss.backward()
w.data.sub_(learning_rate * w.grad)
b.data.sub_(learning_rate * b.grad)
w.grad.zero_()
b.grad.zero_()
return loss
batch_size = 4
for epoch in range(10):
for i in range(0,len(features),batch_size):
### Changes here: move data to required device
loss = train_on_batch(features[i:i+batch_size].view(-1,1).to(device),labels[i:i+batch_size].to(device))
print('Epoch %d: last batch loss = %.4f' % (epoch, float(loss)))## المثال 2: التصنيف
الحين بناخذ مسألة تصنيف ثنائي. مثال مناسب عليها: التصنيف بين ورم خبيث وورم حميد بناءً على حجمه وعمره.
النموذج الأساسي قريب من نموذج الانحدار، لكننا نحتاج دالة الخسارة تكون مختلفة. نبدأ بتوليد بيانات تجريبية:
np.random.seed(0) # pick the seed for reproducibility - change it to explore the effects of random variations
n = 100
X, Y = make_classification(n_samples = n, n_features=2,
n_redundant=0, n_informative=2, flip_y=0.1,class_sep=1.5)
X = X.astype(np.float32)
Y = Y.astype(np.int32)
split = [ 70*n//100, (15+70)*n//100 ]
train_x, valid_x, test_x = np.split(X, split)
train_labels, valid_labels, test_labels = np.split(Y, split)def plot_dataset(features, labels, W=None, b=None):
# prepare the plot
fig, ax = plt.subplots(1, 1)
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)
if W is not None:
min_x = min(features[:,0])
max_x = max(features[:,1])
min_y = min(features[:,1])*(1-.1)
max_y = max(features[:,1])*(1+.1)
cx = np.array([min_x,max_x],dtype=np.float32)
cy = (0.5-W[0]*cx-b)/W[1]
ax.plot(cx,cy,'g')
ax.set_ylim(min_y,max_y)
fig.show()plot_dataset(train_x, train_labels)## تدريب البيرسيبترون (Perceptron) بطبقة واحدة
خلونا نستخدم آلية PyTorch لحساب التدرجات وندرّب البيرسيبترون بطبقة واحدة.
بيكون تركيب الشبكة العصبية من مدخلين 2 ومخرج واحد 1. حجم مصفوفة الأوزان $W$ بيكون $2\times1$، وحجم متجه التحيز $b$ بيكون $1$.
عشان يكون الكود مرتب أكثر، خلونا نجمع كل المعاملات داخل فئة وحدة:
class Network():
def __init__(self):
self.W = torch.randn(size=(2,1),requires_grad=True)
self.b = torch.zeros(size=(1,),requires_grad=True)
def forward(self,x):
return torch.matmul(x,self.W)+self.b
def zero_grad(self):
self.W.grad = None
self.b.grad = None
def update(self, lr=0.1):
with torch.no_grad():
self.W -= lr * self.W.grad
self.b -= lr * self.b.grad
net = Network()> لاحظوا إننا نستخدم `W.data.zero_` بدل `W.zero_`. السبب إننا ما نقدر نعدّل مباشرة موترًا يتتبعه نظام *Autograd*.
النموذج الأساسي هو نفسه في المثال السابق، لكن دالة الخسارة هنا خسارة لوجستية. عشان نطبّقها، نحتاج ناتج الشبكة العصبية يكون قيمة **احتمال**؛ يعني نجيب الناتج $z$ إلى المجال [0,1] باستخدام دالة التنشيط `sigmoid`: $p=\sigma(z)$.
إذا طلع لنا الاحتمال $p_i$ لقيمة الإدخال رقم i، وكانت فئتها الفعلية $y_i\in\{0,1\}$، نحسب الخسارة كالتالي: $\mathcal{L_i}=-(y_i\log p_i + (1-y_i)log(1-p_i))$.
في PyTorch نقدر ننفّذ الخطوتين—تطبيق sigmoid ثم حساب الخسارة اللوجستية—باستدعاء واحد للدالة `binary_cross_entropy_with_logits`. وبما إننا ندرّب الشبكة بدفعات مصغّرة، نحتاج نحسب متوسط الخسارة لكل عناصر الدفعة؛ والدالة `binary_cross_entropy_with_logits` تسوي هذا آليًا بعد.
> الاستدعاء `binary_crossentropy_with_logits` يكافئ استدعاء `sigmoid` ثم استدعاء `binary_crossentropy`
def train_on_batch(net, x, y):
z = net.forward(x).flatten()
loss = torch.nn.functional.binary_cross_entropy_with_logits(input=z,target=y)
net.zero_grad()
loss.backward()
net.update()
return lossعشان نمر على البيانات، بنستخدم آلية PyTorch المدمجة لإدارة مجموعات البيانات. تعتمد على مفهومين:
* **Dataset** هي المصدر الأساسي للبيانات، وتكون إما **Iterable** أو **Map-style**
* **Dataloader** مسؤول عن تحميل البيانات من مجموعة البيانات وتقسيمها إلى دفعات مصغّرة
في مثالنا بنعرّف مجموعة البيانات اعتمادًا على موتر، ثم نقسمها إلى دفعات مصغّرة فيها 16 عنصرًا. كل دفعة فيها موتران: بيانات الإدخال بحجم=16x2، والتسميات في متجه طوله 16 ونوعه عدد صحيح يمثل رقم الفئة.
# Create a tf.data.Dataset object for easy batched iteration
dataset = torch.utils.data.TensorDataset(torch.tensor(train_x),torch.tensor(train_labels,dtype=torch.float32))
dataloader = torch.utils.data.DataLoader(dataset,batch_size=16)
list(dataloader)[0]الحين نقدر نمر على مجموعة البيانات كاملة وندرّب الشبكة لمدة 15 حقبة:
for epoch in range(15):
for (x, y) in dataloader:
loss = train_on_batch(net,x,y)
print('Epoch %d: last batch loss = %.4f' % (epoch, float(loss)))المعاملات اللي حصلنا عليها:
print(net.W,net.b)عشان نتأكد إن التدريب اشتغل، خلونا نرسم الخط اللي يفصل بين الفئتين. معادلة خط الفصل هي $W\times x + b = 0.5$
plot_dataset(train_x,train_labels,net.W.detach().numpy(),net.b.detach().numpy())الحين خلونا نحسب الدقة على مجموعة البيانات الخاصة بالتحقق:
pred = torch.sigmoid(net.forward(torch.tensor(valid_x)))
torch.mean(((pred.view(-1)>0.5)==(torch.tensor(valid_labels)>0.5)).type(torch.float32))خلونا نفصّل وش يصير هنا:
* `pred` هو متجه الاحتمالات المتنبأ بها لمجموعة البيانات الخاصة بالتحقق كاملة. نحسبه بتمرير بيانات التحقق الأصلية `valid_x` في الشبكة، ثم نطبّق `sigmoid` عشان نحصل على الاحتمالات
* `pred.view(-1)` ينشئ عرضًا مسطحًا للموتر الأصلي. الدالة `view` تشبه الدالة `reshape` في numpy
* `pred.view(-1)>0.5` يرجع موترًا منطقيًا لقيمة الصحة يوضح الفئة المتنبأ بها؛ False = الفئة 0 وTrue = الفئة 1
* وبالطريقة نفسها، ينشئ `torch.tensor(valid_labels)>0.5)` موترًا منطقيًا لقيم الصحة في تسميات التحقق
* نقارن الموترين عنصرًا بعنصر، ونحصل على موتر منطقي جديد؛ `True` يعني تنبؤًا صحيحًا و`False` يعني تنبؤًا خطأ
* نحوّل هالموتر إلى أعداد عشرية، ثم نأخذ متوسطه باستخدام `torch.mean`، وهذي قيمة الدقة المطلوبة
## الشبكات العصبية والمحسّنات
في PyTorch فيه وحدة خاصة اسمها `torch.nn.Module` لتمثيل الشبكة العصبية. وعندنا طريقتان نعرّف بها شبكتنا:
* **Sequential**، وفيها نحدد قائمة الطبقات اللي تكوّن الشبكة
* نعرّفها كـ **فئة** ترث من `torch.nn.Module`
الطريقة الأولى مناسبة للشبكات القياسية اللي طبقاتها متسلسلة، أما الثانية فمرنة أكثر وتخلّينا نعبّر عن شبكات ببُنى معقدة كيف ما احتجنا.
داخل الوحدات نقدر نستخدم **طبقات** قياسية مثل:
* `Linear` - طبقة خطية كثيفة تكافئ البيرسيبترون بطبقة واحدة، وبنيتها نفس بنية الشبكة اللي عرّفناها فوق
* `Softmax` و`Sigmoid` و`ReLU` - طبقات تقابل دوال التنشيط
* وفيه طبقات ثانية لأنواع شبكات خاصة، مثل الالتفافية والمتكررة وغيرها. وبنرجع لكثير منها لاحقًا في الدورة
> أغلب تطبيقات دالة التنشيط ودوال الخسارة في PyTorch متوفرة بصيغتين: **دالة** داخل النطاق `torch.nn.functional`، و**طبقة** داخل النطاق `torch.nn`. غالبًا يكون استخدام العناصر الوظيفية من `torch.nn.functional` أسهل لدوال التنشيط؛ لأننا ما نحتاج ننشئ كائن طبقة مستقل.
إذا بغينا ندرّب البيرسيبترون بطبقة واحدة، نستخدم طبقة `Linear` المدمجة مباشرة:
net = torch.nn.Linear(2,1) # 2 inputs, 1 output
print(list(net.parameters()))مثل ما تشوفون، ترجع الدالة `parameters` كل المعاملات اللي نحتاج نعدّلها أثناء التدريب. وهي تقابل مصفوفة الأوزان $W$ والتحيز $b$. وتلاحظون إن الخاصية `requires_grad` مضبوطة على `True`؛ لأننا نحتاج نحسب التدرجات بالنسبة إلى هالمعاملات.
وفي PyTorch بعد **محسّنات** مدمجة تطبّق طرق تحسين مثل **الانحدار المتدرج**. وهذي طريقة تعريف **محسّن الانحدار المتدرج العشوائي**:
optim = torch.optim.SGD(net.parameters(),lr=0.05)باستخدام المحسّن، تصير حلقة التدريب بالشكل هذا:
val_x = torch.tensor(valid_x)
val_lab = torch.tensor(valid_labels)
for ep in range(10):
for (x,y) in dataloader:
z = net(x).flatten()
loss = torch.nn.functional.binary_cross_entropy_with_logits(z,y)
optim.zero_grad()
loss.backward()
optim.step()
acc = ((torch.sigmoid(net(val_x).flatten())>0.5).float()==val_lab).float().mean()
print(f"Epoch {ep}: last batch loss = {loss}, val acc = {acc}")> يمكن لاحظتوا إننا نقدر نطبّق الشبكة على بيانات الإدخال باستخدام `net(x)` بدل `net.forward(x)`؛ لأن `nn.Module` يطبّق دالة Python المسماة `__call__`
وبناءً على هذا، نقدر نعرّف دالة `train` عامة:
def train(net, dataloader, val_x, val_lab, epochs=10, lr=0.05):
optim = torch.optim.Adam(net.parameters(),lr=lr)
for ep in range(epochs):
for (x,y) in dataloader:
z = net(x).flatten()
loss = torch.nn.functional.binary_cross_entropy_with_logits(z,y)
optim.zero_grad()
loss.backward()
optim.step()
acc = ((torch.sigmoid(net(val_x).flatten())>0.5).float()==val_lab).float().mean()
print(f"Epoch {ep}: last batch loss = {loss}, val acc = {acc}")
net = torch.nn.Linear(2,1)
train(net,dataloader,val_x,val_lab,lr=0.03)## تعريف الشبكة كسلسلة طبقات
الحين خلونا ندرّب بيرسيبترون متعدد الطبقات. نعرّفه ببساطة عن طريق تحديد سلسلة الطبقات. والكائن الناتج بيرث آليًا من `Module`؛ يعني بيكون عنده بعد الدالة `parameters` اللي ترجع كل معاملات الشبكة.
net = torch.nn.Sequential(torch.nn.Linear(2,5),torch.nn.Sigmoid(),torch.nn.Linear(5,1))
print(net)نقدر ندرّب هالشبكة متعددة الطبقات باستخدام الدالة `train` اللي عرّفناها فوق:
train(net,dataloader,val_x,val_lab)## تعريف الشبكة كفئة
استخدام فئة ترث من `torch.nn.Module` طريقة أوسع مرونة؛ لأننا نقدر نعرّف داخلها أي حسابات نحتاجها. تسوي `Module` أشياء كثيرة آليًا؛ مثلًا تتعرف على المتغيرات الداخلية اللي تمثل طبقات PyTorch وتجمع معاملاتها للتحسين. اللي عليكم بس تعرّفون كل طبقات الشبكة كأعضاء داخل الفئة:
class MyNet(torch.nn.Module):
def __init__(self,hidden_size=10,func=torch.nn.Sigmoid()):
super().__init__()
self.fc1 = torch.nn.Linear(2,hidden_size)
self.func = func
self.fc2 = torch.nn.Linear(hidden_size,1)
def forward(self,x):
x = self.fc1(x)
x = self.func(x)
x = self.fc2(x)
return x
net = MyNet(func=torch.nn.ReLU())
print(net)train(net,dataloader,val_x,val_lab,lr=0.005)**المهمة 1**: ارسموا منحنيات دالة الخسارة والدقة على بيانات التدريب وبيانات التحقق أثناء التدريب
**المهمة 2**: جرّبوا تحلون مسألة تصنيف MNIST باستخدام هالكود. تلميح: استخدموا `crossentropy_with_logits` كدالة الخسارة.
## تعريف الشبكة كوحدة PyTorch Lightning
خلونا نغلّف كود نموذج PyTorch اللي كتبناه داخل وحدة PyTorch Lightning. كذا نقدر نتعامل مع النموذج براحة ومرونة أكبر باستخدام أدوات Lightning المختلفة للتدريب واختبار الدقة.
أول شيء نحتاج نثبّت PyTorch Lightning ونستورده. نسوي هذا بالأمر:
```
pip install pytorch-lightning
```
أو
```
conda install -c conda-forge pytorch-lightning
```
import pytorch_lightning as plعشان يشتغل الكود في Lightning، نحتاج نسوي التالي:
1. ننشئ فئة فرعية من `pl.LightningModule`، ونضيف لها بنية النموذج في الدالة `__init__` ومسار التمرير الأمامي في الدالة `forward`.
2. ننقل المحسّن المستخدم إلى الدالة `configure_optimizers`.
3. نعرّف عمليتي التدريب والتحقق داخل الدالتين `training_step` و`validation_step` بالترتيب.
4. نطبّق—اختياريًا—عملية الاختبار بالدالة `test_step` وعملية التنبؤ بالدالة `predict_step`.
ومهم تعرفون إن PyTorch Lightning ينقل النماذج تلقائيًا بين وحدات الحوسبة بحسب مكان البيانات الواردة من `DataLoaders`. لذلك لازم نحذف كل استدعاءات `.cuda` أو `.to(device)` من الكود.
class MyNetPL(pl.LightningModule):
def __init__(self, hidden_size = 10, func = torch.nn.Sigmoid()):
super().__init__()
self.fc1 = torch.nn.Linear(2,hidden_size)
self.func = func
self.fc2 = torch.nn.Linear(hidden_size,1)
self.val_epoch_num = 0 # for logging
def forward(self, x):
x = self.fc1(x)
x = self.func(x)
x = self.fc2(x)
return x
def training_step(self, batch, batch_nb):
x, y = batch
y_res = self(x).view(-1)
loss = torch.nn.functional.binary_cross_entropy_with_logits(y_res, y)
return loss
def configure_optimizers(self):
optimizer = torch.optim.SGD(self.parameters(), lr = 0.005)
return optimizer
def validation_step(self, batch, batch_nb):
x, y = batch
y_res = self(x).view(-1)
val_loss = torch.nn.functional.binary_cross_entropy_with_logits(y_res, y)
print("Epoch ", self.val_epoch_num, ": val loss = ", val_loss.item(), " val acc = ",((torch.sigmoid(y_res.flatten())>0.5).float()==y).float().mean().item(), sep = "")
self.val_epoch_num += 1خلونا نضيف `Dataset` و`DataLoader` لبيانات التحقق بعد:
valid_dataset = torch.utils.data.TensorDataset(torch.tensor(valid_x),torch.tensor(valid_labels,dtype=torch.float32))
valid_dataloader = torch.utils.data.DataLoader(valid_dataset, batch_size = 16)الحين نموذجنا جاهز للتدريب. في PyTorch Lightning تُنفّذ العملية عن طريق كائن من الفئة `Trainer`، وهو يربط النموذج بمجموعات البيانات اللي نعطيه إياها ويدير التدريب.
net = MyNetPL(func=torch.nn.ReLU())
trainer = pl.Trainer(
max_epochs=2,
log_every_n_steps=1,
accelerator="auto",
devices="auto",
)
trainer.fit(
model=net,
train_dataloaders=dataloader,
val_dataloaders=valid_dataloader,
)## أهم النقاط
* يخلّيكم PyTorch تتعاملون مع الموترات على مستوى منخفض، وهنا يكون عندكم أكبر قدر من المرونة
* فيه أدوات مريحة للتعامل مع البيانات، مثل Datasets وDataloaders
* تقدرون تعرّفون بُنى الشبكات العصبية بصياغة `Sequential`، أو عن طريق فئة ترث من `torch.nn.Module`
* وإذا تبون طريقة أبسط لتعريف الشبكة وتدريبها، شوفوا PyTorch Lightning
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.