Datasets documentation

الفرق بين Dataset وIterableDataset

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v4.8.4).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

الفرق بين Dataset وIterableDataset

توجد نوعان من كائنات مجموعة البيانات، وهما Dataset و IterableDataset. يعتمد اختيار نوع مجموعة البيانات الذي ستستخدمه أو تنشئه على حجم مجموعة البيانات. بشكل عام، يُعد IterableDataset مثاليًا لمجموعات البيانات الكبيرة (فكّر في مئات الجيجابايت!) بسبب سلوكه الكسول ومزاياه من حيث السرعة، بينما يُعد Dataset رائعًا لكل ما عدا ذلك. ستقارن هذه الصفحة الفروق بين Dataset و IterableDataset لمساعدتك على اختيار كائن مجموعة البيانات المناسب لك.

التحميل والبث

عندما يكون لديك Dataset عادي، يمكنك الوصول إليه باستخدام my_dataset[0]. وهذا يوفر وصولاً عشوائياً إلى الصفوف. تُسمى هذه البيانات أيضاً ببيانات "map-style". على سبيل المثال، يمكنك تنزيل ImageNet-1k على النحو التالي والوصول إلى أي صف:

from datasets import load_dataset

imagenet = load_dataset("timm/imagenet-1k-wds", split="train")  # downloads the full dataset
print(imagenet[0])

لكن هناك تحذير واحد وهو أنه يجب أن يكون لديك كامل مجموعة البيانات مخزنة على القرص أو في الذاكرة، مما يمنعك من الوصول إلى مجموعات بيانات أكبر من سعة القرص. وبما أن هذا قد يصبح غير عملي لمجموعات البيانات الكبيرة، يوجد نوع آخر من مجموعات البيانات وهو IterableDataset. عندما يكون لديك IterableDataset، يمكنك الوصول إليه باستخدام حلقة for لتحميل البيانات تدريجياً أثناء التكرار على مجموعة البيانات. وبهذه الطريقة، يتم تحميل جزء صغير فقط من الأمثلة في الذاكرة، ولا تكتب أي شيء على القرص.

على سبيل المثال، يمكنك بث مجموعة بيانات ImageNet-1k دون تنزيلها على القرص:

from datasets import load_dataset

imagenet = load_dataset("timm/imagenet-1k-wds", split="train", streaming=True)  # will start loading the data when iterated over
for example in imagenet:
    print(example)
    break

يمكن لـ Streaming قراءة البيانات عبر الإنترنت دون كتابة أي ملف على القرص. على سبيل المثال، يمكنك بث مجموعات بيانات مكوّنة من عدة أجزاء، حيث يبلغ حجم كل جزء مئات الجيجابايت مثل C4 أو LAION-2B. تعرف على المزيد حول كيفية بث مجموعة بيانات في Dataset Streaming Guide.

لكن هذا ليس الفرق الوحيد، إذ إن السلوك “الكسول” الخاص بـIterableDataset حاضر أيضاً عند إنشاء مجموعة البيانات ومعالجتها.

إنشاء مجموعات بيانات بأسلوب الخريطة ومجموعات بيانات قابلة للتكرار

يمكنك إنشاء Dataset باستخدام قوائم أو قواميس، وتُحوَّل البيانات بالكامل إلى Arrow بحيث يمكنك الوصول إلى أي صف بسهولة:

my_dataset = Dataset.from_dict({"col_1": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]})
print(my_dataset[0])

لإنشاء IterableDataset من ناحية أخرى، يجب عليك توفير طريقة «كسولة» لتحميل البيانات. في بايثون، نستخدم بشكل عام دوال المولّدات. تُرجع هذه الدوال yield مثالاً واحداً في كل مرة، مما يعني أنك لا تستطيع الوصول إلى صف عبر تقطيعه كما تفعل مع Dataset عادي:

def my_generator(n):
    for i in range(n):
        yield {"col_1": i}

my_iterable_dataset = IterableDataset.from_generator(my_generator, gen_kwargs={"n": 10})
for example in my_iterable_dataset:
    print(example)
    break

تحميل الملفات المحلية بالكامل وبشكل تدريجي

من الممكن تحويل ملفات البيانات المحلية أو عن بُعد إلى مجموعة بيانات Dataset بصيغة Arrow باستخدام load_dataset():

data_files = {"train": ["path/to/data.csv"]}
my_dataset = load_dataset("csv", data_files=data_files, split="train")
print(my_dataset[0])

ومع ذلك، يتطلب هذا إجراءً لتحويل البيانات من صيغة CSV إلى صيغة Arrow، وهو ما يستغرق وقتاً ومساحة تخزين إذا كان حجم مجموعة البيانات كبيراً.

لحفظ مساحة القرص وتخطي خطوة التحويل، يمكنك تعريف IterableDataset عن طريق البث مباشرة من الملفات المحلية. بهذه الطريقة، يتم قراءة البيانات تدريجياً من الملفات المحلية أثناء تكرار عناصر مجموعة البيانات:

data_files = {"train": ["path/to/data.csv"]}
my_iterable_dataset = load_dataset("csv", data_files=data_files, split="train", streaming=True)
for example in my_iterable_dataset:  # this reads the CSV file progressively as you iterate over the dataset
    print(example)
    break

تُؤيَّد العديد من صيغ الملفات، مثل CSV وJSONL وParquet، بالإضافة إلى ملفات الصور والصوت. يمكنك العثور على مزيد من المعلومات في الدلائل المقابلة لتحميل مجموعات البيانات الجدولية، والنصية، والبصرية، والصوتية.

معالجة البيانات الفورية ومعالجة البيانات الكسولة

عند معالجة كائن Dataset باستخدام Dataset.map()، تتم معالجة مجموعة البيانات بأكملها فوراً وإعادتها. هذا يشبه طريقة عمل pandas على سبيل المثال.

my_dataset = my_dataset.map(process_fn)  # process_fn is applied on all the examples of the dataset
print(my_dataset[0])

من ناحية أخرى، بسبب الطبيعة “الكسولة” لمجموعة IterableDataset، فإن استدعاء IterableDataset.map() لا يطبّق دالة map الخاصة بك على كامل مجموعة البيانات. بل تُطبَّق دالة map الخاصة بك على نحوٍ فوري أثناء المعالجة.

بسبب ذلك، يمكنك ربط عدة خطوات معالجة معًا وستعمل جميعها في وقت واحد عند بدء التكرار على مجموعة البيانات:

my_iterable_dataset = my_iterable_dataset.map(process_fn_1)
my_iterable_dataset = my_iterable_dataset.filter(filter_fn)
my_iterable_dataset = my_iterable_dataset.map(process_fn_2)

# process_fn_1, filter_fn and process_fn_2 are applied on-the-fly when iterating over the dataset
for example in my_iterable_dataset:  
    print(example)
    break

الخلط الدقيق والتقريبي السريع

عندما تُخلط مجموعة بيانات Dataset باستخدام Dataset.shuffle()، فإنك تطبّق خلطًا دقيقًا لمجموعة البيانات. يعمل هذا عن طريق أخذ قائمة من الفهارس [0, 1, 2, ... len(my_dataset) - 1] وخلط هذه القائمة. ثم، عند الوصول إلى my_dataset[0]، تُعاد الصف والفهرس المحددان بواسطة العنصر الأول من إسقاط الفهارس الذي تم خلطه:

my_dataset = my_dataset.shuffle(seed=42)
print(my_dataset[0])

بما أننا لا نملك وصولاً عشوائياً إلى الصفوف في حالة IterableDataset، لا يمكننا استخدام قائمة عشوائية من الفهارس والوصول إلى صف في موضع عشوائي. يمنع هذا استخدام الخلط الدقيق. بدلاً من ذلك، يُستخدم خلط تقريبي سريع في IterableDataset.shuffle(). يستخدم مخزن خلط (shuffle buffer) لسحب أمثلة عشوائية بشكل تكراري من مجموعة البيانات. بما أن مجموعة البيانات تُقرأ بشكل تكراري لا يزال، فإنه يوفر أداءً سريعاً ممتازاً:

my_iterable_dataset = my_iterable_dataset.shuffle(seed=42, buffer_size=100)
for example in my_iterable_dataset:
    print(example)
    break

لكن استخدام مخزن خلط لا يكفي لتقديم خلط مرضٍ لتدريب نماذج تعلم الآلة. لذلك يقوم IterableDataset.shuffle() أيضًا بخلط شظايا مجموعة البيانات إذا كانت مجموعتك مكوّنة من ملفات أو مصادر متعددة. بالإضافة إلى ذلك، يملأ المخزن باستخدام ما يصل إلى max_buffer_input_shards شظية في كل مرة (الافتراضي هو 10)، مما يحسّن بشكل كبير من جودة الخلط:

# Stream from the internet
my_iterable_dataset = load_dataset("deepmind/code_contests", split="train", streaming=True)
my_iterable_dataset.num_shards  # 39

# Stream from local files
data_files = {"train": [f"path/to/data_{i}.csv" for i in range(1024)]}
my_iterable_dataset = load_dataset("csv", data_files=data_files, split="train", streaming=True)
my_iterable_dataset.num_shards  # 1024

# From a generator function
def my_generator(n, sources):
    for source in sources:
        for example_id_for_current_source in range(n):
            yield {"example_id": f"{source}_{example_id_for_current_source}"}

gen_kwargs = {"n": 10, "sources": [f"path/to/data_{i}" for i in range(1024)]}
my_iterable_dataset = IterableDataset.from_generator(my_generator, gen_kwargs=gen_kwargs)
my_iterable_dataset.num_shards  # 1024

إذا كان لديك مجموعة بيانات تحتوي على عدد قليل من الملفات، يمكنك تحسين جودة الخلط عن طريق استدعاء reshard() قبل الخلط. على سبيل المثال، في مجموعات بيانات Parquet، يعيد هذا التقسيم باستخدام مجموعات الصفوف بدلاً من وجود ملف واحد لكل شريحة، مما يمنحك عددًا أكبر بكثير من الشرائح للخلط منها.

فرق السرعة

تستند كائنات Dataset العادية إلى Arrow الذي يوفر وصولاً عشوائياً سريعاً إلى الصفوف. وبفضل خرائط الذاكرة (memory mapping) وكون Arrow صيغة داخل الذاكرة، فإن قراءة البيانات من القرص لا تتطلب استدعاءات نظام مكلفة أو فك تسلسل. كما يوفر تحميل بيانات أسرع بكثير عند التكرار باستخدام حلقة for من خلال التكرار على دفعات سجلات Arrow المتجاورة.

ومع ذلك، بمجرد أن يصبح لديك Dataset يحتوي على تعيين للفهارس (على سبيل المثال عبر Dataset.shuffle())، قد يصبح الأداء أبطأ بعشر مرات. وذلك لأن هناك خطوة إضافية للحصول على فهرس الصف المطلوب قراءته باستخدام تعيين الفهارس، والأهم من ذلك أنك لم تعد تقرأ مقاطع متتالية من البيانات. للاستعادة السرعة، ستحتاج إلى إعادة كتابة مجموعة البيانات بأكملها على القرص مرة أخرى باستخدام Dataset.flatten_indices()، والذي يزيل تعيين الفهارس. وقد يستغرق ذلك وقتاً طويلاً اعتماداً على حجم مجموعة بياناتك:

my_dataset[0]  # fast
my_dataset = my_dataset.shuffle(seed=42)
my_dataset[0]  # up to 10x slower
my_dataset = my_dataset.flatten_indices()  # rewrite the shuffled dataset on disk as contiguous chunks of data
my_dataset[0]  # fast again

في هذه الحالة، ننصح بالتبديل إلى IterableDataset والاستفادة من طريقة الخلط التقريبي السريع IterableDataset.shuffle(). إنها تخلط فقط ترتيب الشرائح وتضيف مخزن خلط إلى مجموعة البيانات الخاصة بك، مما يحافظ على سرعة مجموعة البيانات في المستوى الأمثل. يمكنك أيضًا إعادة خلط مجموعة البيانات بسهولة:

for example in enumerate(my_iterable_dataset):  # fast
    pass

shuffled_iterable_dataset = my_iterable_dataset.shuffle(seed=42, buffer_size=100)

for example in enumerate(shuffled_iterable_dataset):  # as fast as before
    pass

shuffled_iterable_dataset = my_iterable_dataset.shuffle(seed=1337, buffer_size=100)  # reshuffling using another seed is instantaneous

for example in enumerate(shuffled_iterable_dataset):  # still as fast as before
    pass

إذا كنت تستخدم مجموعة بياناتك على عدة فترات تدريب، فإن البذرة الفعلية لإعادة ترتيب شرائح البيانات داخل مخزن الخلط هي seed + epoch. هذا يجعل إعادة خلط مجموعة البيانات بين الفترات التدريبية أمرًا سهلًا:

for epoch in range(n_epochs):
    my_iterable_dataset.set_epoch(epoch)
    for example in my_iterable_dataset:  # fast + reshuffled at each epoch using `effective_seed = seed + epoch`
        pass

لإعادة تشغيل تكرار مجموعة بيانات بنمط الخريطة، يمكنك ببساطة تخطي الأمثلة الأولى:

my_dataset = my_dataset.select(range(start_index, len(dataset)))

لكن إذا كنت تستخدم DataLoader مع Sampler، فيجب عليك بدلاً من ذلك حفظ حالة المُعيّن (قد تكون قد كتبت مُعيّناً مخصصاً يتيح استئناف التدريب).

من ناحية أخرى، لا توفر مجموعات البيانات القابلة للتكرار وصولاً عشوائياً إلى فهرس مثال محدد للمتابعة منه. لكن يمكنك استخدام IterableDataset.state_dict() و IterableDataset.load_state_dict() للمتابعة من نقطة تفتيش بدلاً من ذلك، على غرار ما يمكنك فعله بالنسبة للنماذج والمُحسِّنات:

>>> iterable_dataset = Dataset.from_dict({"a": range(6)}).to_iterable_dataset(num_shards=3)
>>> # save in the middle of training
>>> state_dict = iterable_dataset.state_dict()
>>> # and resume later
>>> iterable_dataset.load_state_dict(state_dict)

في الخلفية، يحافظ مجموعة البيانات القابلة للتكرار على تتبع الشريحة الحالية التي يتم قراءتها وفهرس المثال داخل الشريحة الحالية، ويخزّن هذه المعلومات في state_dict.

للاستئناف من نقطة تفتيش، يتخطى مجموعة البيانات جميع الشرائح التي تم قراءتها سابقًا لإعادة البدء من الشريحة الحالية. ثم يقرأ الشريحة ويتخطى الأمثلة حتى يصل إلى المثال الدقيق من نقطة التفتيش.

لذلك، فإن إعادة تشغيل مجموعة البيانات سريع إلى حد كبير، إذ لن تعيد قراءة الشرائح التي تم المرور عليها بالفعل. ومع ذلك، فإن استئناف مجموعة البيانات ليس فوريًا بشكل عام، إذ يجب أن يعيد القراءة من بداية الشريحة الحالية ويتخطى الأمثلة حتى يصل إلى موقع نقطة التفتيش.

يمكن استخدام ذلك مع StatefulDataLoader من torchdata، راجع التدفق باستخدام PyTorch DataLoader.

التبديل من نمط الخريطة إلى نمط القابل للتكرار

إذا كنت ترغب في الاستفادة من السلوك «الكسول» الخاص بـ IterableDataset أو من مزاياها في السرعة، يمكنك تحويل مجموعة Dataset بنمط الخريطة لديك إلى IterableDataset:

my_iterable_dataset = my_dataset.to_iterable_dataset()

إذا كنت ترغب في خلط مجموعة البيانات الخاصة بك أو استخدامها مع PyTorch DataLoader، فننصح بتوليد IterableDataset مقسّم إلى أجزاء:

my_iterable_dataset = my_dataset.to_iterable_dataset(num_shards=1024)
my_iterable_dataset.num_shards  # 1024
Update on GitHub