Hugging Face Datasets Library: Laden und Verarbeiten von Daten
1. Überblick
Wenn du mit NLP-Modellen oder beim Fine-Tuning von LLMs arbeitest, brauchst du immer einen vorherigen Schritt: Die Daten im richtigen Format vorbereiten, sie sauber machen und in die Training-Pipeline laden. Die datasets-Bibliothek von Hugging Face löst genau dieses Problem — sie bietet dir eine einheitliche API zum Laden von Datensätzen vom Hub, zur Batch-Verarbeitung mit map/filter und zum Speichern in einer optimalen Form für wiederholtes Lesen.
Nach dem Lesen dieses Artikels wirst du in der Lage sein, Datensätze vom Hugging Face Hub zu laden, die Struktur von Dataset/DatasetDict zu verstehen, Texte mit Tokenizern vorzuverarbeiten, große Datensätze mit Streaming zu verarbeiten und Tests für deine Datenverarbeitungs-Pipeline zu schreiben.
2. Installation
pip install datasets transformers
Die datasets-Bibliothek hat keine strikte Abhängigkeit von transformers, aber der Tokenisierungsteil in Abschnitt 6 setzt es voraus.
import datasets
print(datasets.__version__)
3.0.1
3. Datensätze vom Hugging Face Hub laden
3.1. load_dataset Grundlagen
from datasets import load_dataset
reviews = load_dataset("imdb")
print(reviews)
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
unsupervised: Dataset({
features: ['text', 'label'],
num_rows: 50000
})
})
load_dataset lädt das Skript/Metadaten des imdb-Datensatzes vom Hub, speichert die komprimierten Dateien lokal in ~/.cache/huggingface/datasets und gibt dann ein DatasetDict zurück — also ein Dictionary von Datasets nach Split (train, test, unsupervised).
3.2. Nur einen bestimmten Split laden
train_reviews = load_dataset("imdb", split="train")
print(type(train_reviews), len(train_reviews))
<class 'datasets.arrow_dataset.Dataset'> 25000
Wenn du split="train" übergibst, erhältst du direkt einen Dataset statt eines DatasetDict — praktisch, wenn du nur einen Split brauchst.
Hinweis: Wenn der Datensatz auf dem Hub ein Config benötigt (z.B.
glue), musst du zusätzlich den Config-Namen übergeben:load_dataset("glue", "mrpc"). Wenn du diesen Parameter vergisst, wirft es einen Fehler, der dich auffordert, ein Config auszuwählen.
4. Die Dataset-Struktur erkunden
sample = train_reviews[0]
print(sample.keys())
print(sample["label"], sample["text"][:60])
dict_keys(['text', 'label'])
0 I rented I AM CURIOUS-YELLOW from my video store because of...
Der Zugriff über einen Index gibt ein normales Python-Dictionary zurück. Dataset unterstützt auch Slicing, das ein Dictionary von Listen zurückgibt:
batch = train_reviews[0:3]
print(batch["label"])
[0, 0, 0]
Das Schema mit features anschauen:
print(train_reviews.features)
{'text': Value(dtype='string', id=None),
'label': ClassLabel(names=['neg', 'pos'], id=None)}
ClassLabel zeigt dir, dass 0 dem Label neg entspricht und 1 dem Label pos — hilfreich beim Mapping zurück zu Labelnamen.
5. Daten mit map, filter und select verarbeiten
5.1. filter zum Filtern von Zeilen
positive_only = train_reviews.filter(lambda row: row["label"] == 1)
print(len(positive_only))
12500
filter nimmt eine Funktion, die für jede Zeile True oder False zurückgibt, läuft durch den gesamten Datensatz und behält nur die Zeilen, die die Bedingung erfüllen.
5.2. map zur Datentransformation
def add_length(row):
row["text_length"] = len(row["text"])
return row
train_with_length = train_reviews.map(add_length)
print(train_with_length[0]["text_length"])
1640
map wendet eine Funktion auf jede Zeile an und gibt ein Dictionary mit neuen Feldern zurück (oder aktualisiert bestehende) — die Ergebnisse werden zurück in den ursprünglichen Datensatz eingefügt.
5.3. map in Batches zur Beschleunigung
def add_length_batched(batch):
batch["text_length"] = [len(t) for t in batch["text"]]
return batch
train_fast = train_reviews.map(add_length_batched, batched=True, batch_size=1000)
Mit batched=True erhält die Funktion ein komplettes Batch-Dictionary (jedes Feld ist eine Liste) statt einzelner Zeilen — das vermeidet 25.000 Python-Funktionsaufrufe und ist deutlich schneller bei großen Datensätzen.
5.4. select zum Abrufen einer Teilmenge
small_train = train_reviews.select(range(100))
print(len(small_train))
100
Verwende dies, wenn du die Pipeline schnell auf einer Teilmenge testen möchtest, bevor du den vollständigen Datensatz verarbeitest.
6. Textvorbereitung mit Tokenizer
Der häufigste praktische Anwendungsfall: Text tokenisieren, um ihn in das Modell einzugeben.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
def tokenize_batch(batch):
return tokenizer(batch["text"], truncation=True, max_length=256)
tokenized = train_reviews.map(tokenize_batch, batched=True)
print(tokenized.column_names)
['text', 'label', 'input_ids', 'attention_mask']
tokenize_batch nimmt einen Batch und gibt ein Dictionary mit input_ids und attention_mask zurück — map fügt diese neuen Felder automatisch in den Datensatz ein und behält die alten Felder. Um unnötige Felder zu entfernen (z.B. rohen text zur Speicherersparnis), verwende remove_columns:
tokenized = train_reviews.map(
tokenize_batch, batched=True, remove_columns=["text"]
)
print(tokenized.column_names)
['label', 'input_ids', 'attention_mask']
7. Splits, Shuffle und Sharding
7.1. Selbst train/test aufteilen
Wenn der Datensatz nur einen Split hat, teile ihn selbst mit train_test_split:
split_result = train_reviews.train_test_split(test_size=0.2, seed=42)
print(split_result)
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 20000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 5000
})
})
seed=42 stellt sicher, dass die Aufteilung zwischen den Ausführungen reproduzierbar ist.
7.2. Shuffle und Sharding
shuffled = train_reviews.shuffle(seed=42)
part = shuffled.shard(num_shards=4, index=0)
print(len(part))
6250
shard teilt den Datensatz in num_shards gleiche Teile — nützlich beim Verteilen von Daten auf mehrere Worker für verteiltes Training.
8. Streaming für große Datensätze
Wenn der Datensatz zu groß ist, um ihn vollständig auf die Festplatte/RAM zu laden, verwende streaming=True:
streamed = load_dataset("imdb", split="train", streaming=True)
first_three = list(streamed.take(3))
print(len(first_three))
3
Mit Streaming lädt datasets nicht die gesamte Datei herunter, sondern liest die Datensätze sequenziell über einen Iterator. Du kannst len() oder direkten Zugriff nicht verwenden, aber map/filter funktionieren weiterhin mit lazy evaluation (wird nur angewendet, wenn du iterierst).
Hinweis: Streaming eignet sich für Datensätze im Größenbereich von hunderten Gigabyte wie Pretraining-Sets. Bei kleineren Datensätzen (einige hundert Megabyte) ist normales Laden (
streaming=False) schneller, da du den Cache nutzen und zufällige Zugriffe durchführen kannst.
9. Datensätze speichern und neu laden
Speichere die Ergebnisse nach der Verarbeitung, um map nicht jedes Mal erneut ausführen zu müssen:
tokenized.save_to_disk("data/imdb_tokenized")
from datasets import load_from_disk
reloaded = load_from_disk("data/imdb_tokenized")
print(len(reloaded))
25000
save_to_disk schreibt den Datensatz im Arrow-Format — das Neuladen ist sehr schnell, da kein erneutes Parsing aus der Quelle erforderlich ist. Wenn du mit pandas arbeiten musst, konvertiere direkt:
df = tokenized.select(range(5)).to_pandas()
print(df.columns.tolist())
['label', 'input_ids', 'attention_mask']
10. Tests für die Datenverarbeitungs-Pipeline schreiben
Die Datenverarbeitungs-Pipeline sollte Tests haben, um sicherzustellen, dass die Logik von map/filter nicht unauffällig fehlerhaft wird, wenn du Code änderst. Hier sind Tests mit pytest.
import pytest
from datasets import Dataset
def add_length(row):
row["text_length"] = len(row["text"])
return row
def test_add_length_computes_correct_value():
data = Dataset.from_dict({"text": ["hello", "hi there"]})
result = data.map(add_length)
assert result[0]["text_length"] == 5
assert result[1]["text_length"] == 8
def test_filter_keeps_only_positive_labels():
data = Dataset.from_dict({"label": [0, 1, 1, 0]})
positive = data.filter(lambda row: row["label"] == 1)
assert len(positive) == 2
assert all(row["label"] == 1 for row in positive)
Dataset.from_dict erstellt einen kleinen Datensatz im Speicher, ohne etwas vom Hub zu laden — perfekt für Unit-Tests. Die beiden obigen Tests überprüfen das richtige Verhalten von map (neue Felder erstellen) und filter (korrekt filtern), ohne von echten Datensätzen aus dem Internet abhängig zu sein.
pytest test_dataset_pipeline.py -v
test_dataset_pipeline.py::test_add_length_computes_correct_value PASSED
test_dataset_pipeline.py::test_filter_keeps_only_positive_labels PASSED
11. Häufige Fehler und wann man es nicht verwenden sollte
batched=Truevergessen bei der Verarbeitung großer Datensätze:mapohne Batching läuft langsam, da die Python-Funktion für jede Zeile einzeln aufgerufen wird. Mit hundert tausend Zeilen solltest du immerbatched=Truebevorzugen, wenn die Logik Vektorisierung erlaubt.- Zufälliger Zugriff auf Streaming-Datensätze:
streamed[5]wirft einen Fehler, da Streaming-Datensätze Iteratoren sind und keinen zufälligen Zugriff unterstützen. Verwende nur.take(),.skip()oder iteriere sequenziell. seednicht setzen beishuffle/train_test_split: Die Ergebnisse sind zwischen den Ausführungen nicht reproduzierbar, was das Debugging erschwert, wenn man Metriken zwischen Experimenten vergleicht.- Unkontrollierte Cache-Aufblähung: Jedes Mal, wenn du
mapmit einer anderen Funktion aufrufst, schreibtdatasetseine neue Cache-Datei. Wenn dein Rechner wenig Speicherplatz hat, verwendeload_dataset(..., keep_in_memory=True)oder lösche regelmäßig~/.cache/huggingface/datasets. datasetsnicht für komplexe relationale Daten verwenden (mehrere Tabellen mit Joins, Transaktionen erforderlich). Dies ist eine Bibliothek, die für flache Tabellen-/Text-Daten in ML optimiert ist, kein Datenbankersatz.
12. Fazit
Die datasets-Bibliothek bietet dir eine saubere API, um vom „rohen Datensatz vom Hub” zum „tokenisierten, trainingsbereit” Datensatz zu gehen, alles mit nur wenigen Befehlen load_dataset, map, filter. Bei kleinen Datensätzen ladet du normal und nutzt den Arrow-Cache; bei riesigen Datensätzen schaltest du auf streaming=True um, um nicht alles auf die Festplatte laden zu müssen. Beim Aufbau einer Datenverarbeitungs-Pipeline für echte Projekte vergiss nicht, Tests für deine map/filter-Funktionen mit Dataset.from_dict zu schreiben — das hilft dir, frühzeitig zu erkennen, wenn die Vorverarbeitungslogik unbeabsichtigt verändert wird.