Dataset وDataLoader
درس 5 من 8
5Dataset وDataLoader
الصورة الكبيرةالنموذج لا يرى بياناتك كلها مرة واحدة، يراها دفعات. Dataset يعرف كيف يعطيك مثالًا واحدًا، وDataLoader يجمع الأمثلة في دفعات ويخلطها ويحمّلها بالتوازي. وحجم الدفعة ليس تفصيلًا تقنيًا، بل يغيّر كيف يتعلّم النموذج.
ماذا تشاهد في الرسم: الدورة الواحدة تُقطع إلى دفعات، وكل دفعة خطوة تدريب. خمسة أمثلة بدفعات من اثنين تعطي ثلاث دفعات، والأخيرة فيها مثال واحد إلا إن طلبت drop_last=True.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
لا تطعم النموذج كل البيانات دفعة واحدة، كما لا تأكل الطبق كله بلقمة. تقسّمها لقيمات (دفعات)، وتخلط ترتيبها كل مرة حتى لا يحفظ النموذج الترتيب، وتجهّز اللقمة التالية بينما يمضغ الحالية. هذه وظيفة Dataset وDataLoader.
Dataset يعرّف __len__ و__getitem__ لإعادة مثال واحد، وDataLoader يجمع الأمثلة في دفعات batch_size مع shuffle=True وnum_workers للتحميل المتوازي. الدفعة الكبيرة أسرع وأنعم ميلًا لكنها تستهلك ذاكرة أكثر وقد تحتاج معدل تعلّم أكبر.
قبل أن تبدأ
⚙️حلقة تدريب بيدك
تعرف أن كل خطوة تدريب تعمل على دفعة، وأن الدورة مرور كامل على بيانات التدريب.
🧊الموتر
تعرف أن البُعد الأول في الشكل هو حجم الدفعة غالبًا، مثل (2, 1) هنا.
الفكرة الجوهرية
النموذج لا يرى كل البيانات مرة واحدة، بل دفعات. في هذا الدرس تبني Dataset يعطي مثالًا واحدًا، ثم DataLoader يجمعها في دفعات، ثم ترى أثر حجم الدفعة، ثم لماذا نخلط.
1Dataset: مثال واحد في كل مرة
Dataset أي صنف فيه دالتان: __len__ تقول كم مثالًا عندك، و__getitem__ تعيد المثال رقم i كموترات. انظر إلى الرسم: رقم يدخل، ومثال جاهز يخرج.
في __init__ تحفظ قائمة المسارات فقط، فلا تُحمَّل كل البيانات في الذاكرة. والتعديل العشوائي للصور يحدث داخل __getitem__ أيضًا.
2DataLoader: دفعات وخلط وتوازٍ
DataLoader يحوّل Dataset إلى دفعات، ويخلط الترتيب في كل دورة إن طلبت shuffle=True. انظر إلى الرسم: كل دورة تبدأ بترتيب جديد، ثم تمرّ على الدفعات واحدة بعد الأخرى.
هذا الكود يحتاج PyTorch على جهازك، فالناتج المعروض مسجَّل من تشغيل حقيقي. ثبّته بـpip install torch وشغّله بنفسك.
import torch
from torch.utils.data import DataLoader, Dataset
class Reviews(Dataset):
def __init__(self, texts, labels):
self.texts = texts
self.labels = labels
def __len__(self):
return len(self.texts)
def __getitem__(self, i):
length = torch.tensor([len(self.texts[i])], dtype=torch.float32)
return length, torch.tensor(self.labels[i])
data = Reviews(["ممتاز", "سيئ جدا", "رائع وسريع", "لا أنصح", "جيد"], [1, 0, 1, 0, 1])
print("size:", len(data))
print("item 1:", data[1])
torch.manual_seed(0)
for x, y in DataLoader(data, batch_size=2, shuffle=True):
print(tuple(x.shape), y.tolist())
خمسة أمثلة ودفعات من اثنين، فالدفعة الأخيرة فيها مثال واحد. السمة هنا بدائية (طول النص) لأن الهدف هو الشكل: كل دفعة (2, 1) من السمات ومعها قائمة الفئات. في الواقع __getitem__ هو المكان الذي تقرأ فيه صورة من القرص أو ترمّز جملة.
مع num_workers=4 تجهّز أربع عمليات الدفعات القادمة بينما تتدرّب البطاقة على الحالية. وdrop_last=True يحذف الدفعة الأخيرة الصغيرة إن أزعجتك.
3حجم الدفعة يغيّر الميل
دفعة صغيرة تعني خطوات أكثر وميلًا أكثر ضجيجًا. دفعة كبيرة تعني ميلًا أدق وخطوات أقل وذاكرة أكبر. انظر إلى الرسم: تشتّت الميل بين الدفعات لنفس النموذج.
نحسب ميل نفس النموذج (عند وزن صفر) على كل دفعة في دورة واحدة، ونقيس كم يتفاوت بين الدفعات.
import numpy as np
rng = np.random.default_rng(0)
X = rng.uniform(-2, 2, size=1024)
y = 3 * X - 1 + rng.normal(scale=0.5, size=1024)
for batch_size in [8, 128, 1024]:
grads = []
for start in range(0, 1024, batch_size):
xb, yb = X[start:start + batch_size], y[start:start + batch_size]
grads.append(2 * ((0.0 * xb - yb) * xb).mean())
print(f"batch={batch_size:4} steps per epoch={len(grads):3} gradient spread={np.std(grads):.3f}")
بدفعة 8: 128 خطوة في الدورة، وكل ميل يختلف عن غيره كثيرًا (تفاوت 2.8). بدفعة 128: ثماني خطوات وتفاوت 0.69. بالبيانات كلها: خطوة واحدة دقيقة تمامًا. الضجيج في الدفعات الصغيرة ليس عيبًا دائمًا، فهو يساعد النموذج أحيانًا على الخروج من مناطق مسطّحة، لكنه يتطلّب معدّل تعلّم أصغر.
4لماذا نخلط بيانات التدريب
البيانات المرتّبة، حسب الفئة أو الزمن، تعطي دفعات متحيّزة. الخلط في كل دورة يجعل كل دفعة صورة مصغّرة من البيانات كلها.
import random
labels = [0] * 6 + [1] * 6
batch_size = 4
print("no shuffle:")
for start in range(0, len(labels), batch_size):
print(" ", labels[start:start + batch_size])
random.seed(1)
order = list(range(len(labels)))
random.shuffle(order)
print("shuffled:")
for start in range(0, len(order), batch_size):
print(" ", [labels[i] for i in order[start:start + batch_size]])
بلا خلط، الدفعة الأولى كلها فئة 0 والأخيرة كلها فئة 1. النموذج يتعلّم «قل 0 دائمًا» ثم يُجبر على «قل 1 دائمًا»، ويتأرجح. بعد الخلط كل دفعة فيها الفئتان، فيرى الميل الصورة كاملة في كل خطوة.
اخلط التدريب فقط. التحقّق والاختبار يبقيان بترتيب ثابت، حتى تكون النتائج قابلة للمقارنة وكل تنبّؤ يعود إلى صفّه.
كيف يعمل، خطوة بخطوة: كيف تصل البيانات من القرص إلى النموذج
Dataset يحفظ قائمة المسارات فقط في __init__، ويعرف عددها من __len__.
__getitem__(i) يقرأ المثال رقم i من القرص ويحوّله إلى موترات، وهنا يحدث التوسيع العشوائي.
DataLoader يخلط الترتيب في كل دورة إن كان shuffle=True، ويجمع الأمثلة في دفعات.
مع num_workers أكبر من صفر تجهّز عمليات منفصلة الدفعات القادمة بينما تدرّب البطاقة على الدفعة الحالية.
مثال محلول
Dataset في PyTorch أي صنف فيه دالتان: __len__ تقول كم مثالًا عندك، و__getitem__ تعطي المثال رقم i كموترات. هذا كل ما يحتاجه DataLoader.
__init__. يعمل مع ألف صورة، وينهار مع مئة ألف. احفظ في __init__ قائمة المسارات فقط، واقرأ الصورة نفسها في __getitem__.
backward قبل step واحد، وستراه في درس الضبط.
جرّب بلا كود
drop_last=True؟ الجواب: 15 دفعة كاملة فيها 960 مثالًا ودفعة أخيرة فيها 40، أي 16 خطوة، ومع drop_last=True تصير 15.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| حجم الدفعة تفصيل تقني يحدّده حجم الذاكرة فقط. | يغيّر طريقة التعلّم. بدفعة 8 كان انتشار الميل 2.8 وبدفعة 128 كان 0.69، ومع تكبيرها كثيرًا تحتاج غالبًا معدّل تعلّم أكبر. |
| الخلط مفيد دائمًا، فاخلط كل البيانات. | اخلط التدريب فقط. التحقّق والاختبار بترتيب ثابت حتى تكون النتائج قابلة للمقارنة وتستطيع ربط كل تنبّؤ بصفّه. |
| التدريب البطيء يعني أن النموذج كبير. | كثيرًا ما تنتظر البطاقة القرص. إن كان استخدامها منخفضًا والمعالج مشغولًا، فالحل في num_workers وتحضير الصور، لا في النموذج. |
جرّب بنفسك
أي DataLoader يجب أن يكون shuffle=False؟
في التقييم لا توجد خطوات تدريب تتأثّر بالترتيب، والترتيب الثابت يجعل النتائج قابلة للمقارنة بين التشغيلات ويسهّل ربط كل تنبّؤ بصفّه الأصلي.
استخدام بطاقة الرسوميات 20% فقط أثناء التدريب، والمعالج 100%. ما أول ما تجرّبه؟
البطاقة تنتظر البيانات. المعالج هو عنق الزجاجة، فوزّع التحميل على عمليات أكثر، أو جهّز الصور مسبقًا بحجمها النهائي. معدّل التعلّم لا علاقة له بالسرعة.
بطاقات مراجعة سريعة
__len__ تعيد عدد الأمثلة، ودالة __getitem__ تعيد المثال رقم i كموترات. التحميل الثقيل يحدث في الثانية حتى لا تُحمّل كل البيانات في الذاكرة.num_workers، وكل ذلك دون أن تكتب حلقة بنفسك.تحقق من فهمك
ضاعفت حجم الدفعة من 32 إلى 64 وتركت كل شيء آخر كما هو. لماذا قد يصير التدريب أبطأ في الوصول لنفس الخسارة رغم أن كل خطوة أدق؟
لأن عدد الخطوات في الدورة انخفض للنصف، والميل الأدق لا يعوّض عن نصف الخطوات بنفس معدّل التعلّم. القاعدة التقريبية المشهورة: عند مضاعفة الدفعة، ارفع معدّل التعلّم بنسبة مشابهة. والحد العملي أن الدفعات الكبيرة جدًّا تحتاج إحماءً لمعدّل التعلّم حتى لا تنفجر في البداية.
بيانات مرتّبة: ستة أمثلة من الفئة 0 ثم ستة من الفئة 1، بدفعات من 4 وبلا خلط. ماذا يحدث للتدريب؟
الدفعة الأولى كلها 0 والأخيرة كلها 1، فيتعلّم النموذج «قل 0 دائمًا» ثم يُجبر على «قل 1 دائمًا» ويتأرجح بينهما. الخلط يجعل كل دفعة تحمل الفئتين.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.