데이터셋 처리

데이터셋 처리 (Process)

🤗 Datasets은 데이터셋의 구조와 내용을 수정하는 많은 도구를 제공해요. 이 도구들은 데이터셋을 정리하고, 추가 컬럼을 만들고, features와 형식 사이를 변환하는 등에 중요합니다.

이 가이드에서는 다음을 다룰게요:

  • 행을 재정렬하고 데이터셋을 분할하기
  • 컬럼 이름 바꾸기·삭제하기와 그 밖의 일반적인 컬럼 연산
  • 데이터셋의 각 예시에 처리 함수 적용하기
  • 데이터셋 이어붙이기(concatenate)
  • 커스텀 포맷 변환 적용하기
  • 처리된 데이터셋 저장·내보내기

이 가이드의 예시는 MRPC 데이터셋을 사용하지만, 원하는 데이터셋을 아무거나 불러와 따라 해도 돼요.

>>> from datasets import load_dataset
>>> dataset = load_dataset("nyu-mll/glue", "mrpc", split="train")

이 가이드의 모든 처리 메서드는 새로운 Dataset 객체를 반환해요. 수정이 제자리(in-place)에서 일어나지 않으니까, 이전 데이터셋을 덮어쓰지 않도록 주의하세요!

정렬, 셔플, 선택, 분할, 샤드

데이터셋의 구조를 재배열하는 함수는 여러 가지가 있어요. 이 함수들은 원하는 행만 고르거나, train·test split을 만들거나, 아주 큰 데이터셋을 더 작은 청크로 샤딩(sharding)하는 데 유용합니다.

정렬 (Sort)

sort()로 컬럼 값을 수치에 따라 정렬할 수 있어요. 제공하는 컬럼은 NumPy 호환이어야 합니다.

>>> dataset["label"][:10]
[1, 0, 1, 0, 1, 1, 0, 1, 0, 0]
>>> sorted_dataset = dataset.sort("label")
>>> sorted_dataset["label"][:10]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> sorted_dataset["label"][-10:]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

내부적으로 이는 컬럼 값에 따라 정렬된 인덱스 리스트를 만드는 것이고, 이 인덱스 매핑을 사용해 밑에 있는 Arrow 테이블의 올바른 행에 접근해요.

셔플 (Shuffle)

shuffle() 함수는 컬럼 값을 무작위로 재배열해요. 데이터셋을 셔플하는 데 쓰이는 알고리즘을 더 제어하고 싶다면 generator 파라미터에 다른 numpy.random.Generator를 지정할 수 있어요.

>>> shuffled_dataset = sorted_dataset.shuffle(seed=42)
>>> shuffled_dataset["label"][:10]
[1, 1, 1, 0, 1, 1, 1, 1, 1, 0]

셔플링은 인덱스 리스트 [0:len(my_dataset)]를 가져다 셔플해 인덱스 매핑을 만듭니다. 그런데 Dataset에 인덱스 매핑이 생기는 순간 속도가 10배 느려질 수 있어요. 인덱스 매핑으로 읽을 행 인덱스를 얻는 추가 단계가 생기고, 무엇보다 연속된 데이터 청크를 읽지 않게 되기 때문이에요. 속도를 되찾으려면 Dataset.flatten_indices()로 전체 데이터셋을 다시 쓰면 되는데, 이는 인덱스 매핑을 제거해요. 또는 IterableDataset로 전환해 빠른 근사 셔플 IterableDataset.shuffle()을 활용할 수도 있어요:

>>> iterable_dataset = dataset.to_iterable_dataset(num_shards=128)
>>> shuffled_iterable_dataset = iterable_dataset.shuffle(seed=42, buffer_size=1000)

선택과 필터 (Select and Filter)

데이터셋의 행을 필터링하는 옵션은 select()filter() 두 가지예요.

  • select()는 인덱스 리스트에 따라 행을 반환해요.
>>> small_dataset = dataset.select([0, 10, 20, 30, 40, 50])
>>> len(small_dataset)
6
  • filter()는 지정한 조건에 맞는 행을 반환해요.
>>> start_with_ar = dataset.filter(lambda example: example["sentence1"].startswith("Ar"))
>>> len(start_with_ar)
6
>>> start_with_ar["sentence1"]
['Around 0335 GMT , Tab shares were up 19 cents , or 4.4 % , at A {@html ""} 4.57 .',
'Arison said Mann may have been one of the pioneers of the world music movement and he had a deep love of Brazilian music .',
'Arts helped coach the youth on an eighth-grade football team at Lombardi Middle School in Green Bay .',
'Around 9 : 00 a.m. EDT ( 1300 GMT ) , the euro was at $ 1.1566 against the dollar , up 0.07 percent on the day .',
"Arguing that the case was an isolated example , Canada has threatened a trade backlash if Tokyo 's ban is not justified on scientific grounds .",
'Artists are worried the plan would harm those who need help most - performers who have a difficult time lining up shows .'
]

filter()with_indices=True를 설정하면 인덱스로도 필터링할 수 있어요.

>>> even_dataset = dataset.filter(lambda example, idx: idx % 2 == 0, with_indices=True)
>>> len(even_dataset)
1834
>>> len(dataset) / 2
1834.0

유지할 인덱스 리스트가 연속적이지 않으면 이 메서드들도 내부적으로 인덱스 매핑을 만들어요.

분할 (Split)

train_test_split() 함수는 데이터셋에 train·test split이 아직 없다면 만들어 줘요. 각 split의 상대적 비율이나 절대 샘플 수를 조절할 수 있어요. 아래 예시는 test_size 파라미터로 원본 데이터셋의 10%인 test split을 만들고 있어요.

>>> dataset.train_test_split(test_size=0.1)
{'train': Dataset(schema: {'sentence1': 'string', 'sentence2': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 3301),
'test': Dataset(schema: {'sentence1': 'string', 'sentence2': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 367)}
>>> 0.1 * len(dataset)
366.8

split은 기본적으로 셔플되지만, shuffle=False로 설정하면 셔플을 막을 수 있어요.

샤드 (Shard)

🤗 Datasets은 아주 큰 데이터셋을 미리 정한 개수의 청크로 나누는 샤딩을 지원해요. shard()num_shards 파라미터로 데이터셋을 몇 개의 샤드로 나눌지 정하고, 반환할 샤드를 index 파라미터로 지정하면 됩니다.

예를 들어 stanfordnlp/imdb 데이터셋은 25000개의 예시가 있어요.

>>> from datasets import load_dataset
>>> dataset = load_dataset("stanfordnlp/imdb", split="train")
>>> print(dataset)
Dataset({
    features: ['text', 'label'],
    num_rows: 25000
})

데이터셋을 네 개의 청크로 샤딩하면 첫 번째 샤드는 6250개 예시만 가집니다.

>>> dataset.shard(num_shards=4, index=0)
Dataset({
    features: ['text', 'label'],
    num_rows: 6250
})
>>> print(25000/4)
6250.0

이름 바꾸기, 삭제, 캐스트, 플래튼

다음 함수들은 데이터셋의 컬럼을 수정하게 해 줘요. 컬럼 이름을 바꾸거나 삭제하고, 컬럼을 새 features 집합으로 바꾸고, 중첩 컬럼 구조를 플래튼(flatten)하는 데 유용합니다.

이름 바꾸기 (Rename)

데이터셋 컬럼 이름을 바꿔야 할 때 rename_column()을 사용해요. 원래 컬럼과 연관된 features는 실제로 제자리에서 원래 컬럼을 대체하는 게 아니라 새 컬럼 이름 아래로 옮겨져요.

rename_column()에 원래 컬럼 이름과 새 컬럼 이름을 제공하세요.

>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'label', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.rename_column("sentence1", "sentenceA")
>>> dataset = dataset.rename_column("sentence2", "sentenceB")
>>> dataset
Dataset({
    features: ['sentenceA', 'sentenceB', 'label', 'idx'],
    num_rows: 3668
})

삭제 (Remove)

하나 이상의 컬럼을 제거해야 할 때는 remove_columns() 함수에 제거할 컬럼 이름을 제공해요. 여러 컬럼을 제거하려면 컬럼 이름 리스트를 제공하면 됩니다.

>>> dataset = dataset.remove_columns("label")
>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.remove_columns(["sentence1", "sentence2"])
>>> dataset
Dataset({
    features: ['idx'],
    num_rows: 3668
})

반대로 select_columns()는 유지할 하나 이상의 컬럼을 선택하고 나머지는 제거해요. 이 함수는 컬럼 이름 하나 또는 리스트를 받습니다.

>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'label', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.select_columns(['sentence1', 'sentence2', 'idx'])
>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.select_columns('idx')
>>> dataset
Dataset({
    features: ['idx'],
    num_rows: 3668
})

캐스트 (Cast)

cast() 함수는 하나 이상의 컬럼 feature 타입을 변환해요. 이 함수는 인자로 우리의 새 Features를 받아요. 아래 예시는 ClassLabelValue features를 어떻게 바꾸는지 보여줍니다.

>>> dataset.features
{'sentence1': Value('string'),
'sentence2': Value('string'),
'label': ClassLabel(names=['not_equivalent', 'equivalent']),
'idx': Value('int32')}

>>> from datasets import ClassLabel, Value
>>> new_features = dataset.features.copy()
>>> new_features["label"] = ClassLabel(names=["negative", "positive"])
>>> new_features["idx"] = Value("int64")
>>> dataset = dataset.cast(new_features)
>>> dataset.features
{'sentence1': Value('string'),
'sentence2': Value('string'),
'label': ClassLabel(names=['negative', 'positive']),
'idx': Value('int64')}

캐스팅은 원래 feature 타입과 새 feature 타입이 호환될 때만 동작해요. 예를 들어 원래 컬럼에 0과 1만 들어 있다면 Value("int32") feature 타입의 컬럼을 Value("bool")로 캐스팅할 수 있습니다.

플래튼 (Flatten)

중첩된 데이터 구조는 flatten()으로 펼칠 수 있어요. 가령 중첩 컬럼 translations가 있다면, 이 함수는 컬럼의 각 하위 필드를 고유한 컬럼으로 분해해요: translations.en, translations.de, translations.fr.

>>> dataset.features
{'translations': {'de': Value(dtype='string', id=None),
  'en': Value(dtype='string', id=None),
  'fr': Value(dtype='string', id=None)},
 'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], id=None)}
>>> dataset = dataset.flatten()
>>> dataset.features
{'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], id=None),
 'translations.de': Value(dtype='string', id=None),
 'translations.en': Value(dtype='string', id=None),
 'translations.fr': Value(dtype='string', id=None)}

전처리 함수 적용

데이터셋의 각 예시에 전처리 함수를 적용하려면 map()을 사용해요. 이는 토큰화, 데이터 증강, 피처 정규화와 같은 작업에 가장 유용합니다.

map()은 데이터셋의 각 예시에 함수를 적용해요. 전처리 함수는 데이터셋의 각 예시에서 데이터를 어떻게 처리할지 정의합니다.

>>> def add_prefix(example):
...     example["sentence1"] = 'My sentence: ' + example["sentence1"]
...     return example

map()을 호출하면 데이터셋의 각 예시 하나하나에 add_prefix 함수가 적용됩니다.

>>> updated_dataset = dataset.map(add_prefix)
>>> updated_dataset["sentence1"][0]
'My sentence: Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .'

배치

map()은 기본적으로 예시를 하나씩 처리해요. 하지만 처리 속도를 크게 높이려면 함수가 전체 배치를 처리하는 batched=True를 사용하는 걸 추천해요. 이는 특히 GPU나 batch_size 등과 함께 쓸 때 효율적입니다.

batched=True로 설정하면 함수는 데이터셋 각 행 단위가 아니라 데이터셋 배치를 입력으로 받아요. 즉 한 번에 여러 예시를 처리할 수 있어요.

>>> def add_prefix_batched(batch):
...     batch["sentence1"] = ['My sentence: ' + s for s in batch["sentence1"]]
...     return batch

>>> updated_dataset = dataset.map(add_prefix_batched, batched=True)

데이터 셔플

데이터를 무작위로 섞는 shuffle()은 앞서 살펴본 것과 동일하게 동작해요. seed를 지정하면 재현 가능한 결과를 얻을 수 있습니다.

>>> shuffled_dataset = dataset.shuffle(seed=42)

데이터 이어붙이기

concatenate_datasets()로 두 개 이상의 Dataset 객체를 이어붙일 수 있어요. 두 데이터셋은 동일한 features 타입을 가져야 합니다.

>>> from datasets import concatenate_datasets

>>> concatenated_dataset = concatenate_datasets([dataset, updated_dataset])

커스텀 포맷 변환

set_transform()으로 데이터셋 형식을 커스텀 변환 함수로 지정할 수 있어요. 이 함수는 데이터셋의 각 예시에 즉석에서 적용됩니다. 특히 오디오 데이터를 디코딩할 때 유용해요. 예를 들어 pydub로 AMR 파일을 디코드하는 예시입니다.

...         samples = [s.get_array_of_samples() for s in channel_sounds]
...         fp_arr = np.array(samples).T.astype(np.float32)
...         fp_arr /= np.iinfo(samples[0].typecode).max
...         return fp_arr
...
...     batch["audio"] = [pydub_decode_file(audio_path) for audio_path in batch["audio"]]
...     return batch

>>> audio_dataset_amr.set_transform(decode_audio_with_pydub)

저장 (Save)

데이터셋이 준비되면 Parquet 형식의 Hugging Face Dataset으로 저장해서 load_dataset()으로 나중에 다시 쓸 수 있어요.

push_to_hub()에 데이터셋을 저장할 Hugging Face 데이터셋 저장소 이름을 제공해서 저장하세요.

encoded_dataset.push_to_hub("username/my_dataset")

여러 프로세스로 병렬 업로드를 할 수도 있는데, 특히 과정을 가속화하고 싶을 때 유용해요.

dataset.push_to_hub("username/my_dataset", num_proc=8)

load_dataset() 함수로 데이터셋을 다시 불러와요 (스트리밍 모드든 아니든).

from datasets import load_dataset
reloaded_dataset = load_dataset("username/my_dataset", streaming=True)

또는 Arrow 형식으로 로컬 디스크에 저장할 수도 있어요. Parquet과 비교해 Arrow는 압축되지 않아 다시 불러오기 훨씬 빠릅니다. 로컬 디스크 사용과 임시 캐시에 아주 좋아요. 하지만 더 크고 메타데이터가 적어서 Parquet보다 업로드/다운로드/쿼리가 느리고 장기 보관에는 덜 적합합니다.

save_to_disk()load_from_disk() 함수로 디스크에서 데이터셋을 다시 불러와요.

>>> encoded_dataset.save_to_disk("path/of/my/dataset/directory")
>>> # 나중에
>>> from datasets import load_from_disk
>>> reloaded_dataset = load_from_disk("path/of/my/dataset/directory")

내보내기 (Export)

🤗 Datasets은 내보내기도 지원해서 다른 애플리케이션에서 데이터셋을 다룰 수 있어요. 아래 표는 현재 지원되는 내보내기 파일 형식이에요.

파일 형식 내보내기 메서드
CSV Dataset.to_csv()
JSON Dataset.to_json()
Parquet Dataset.to_parquet()
SQL Dataset.to_sql()
메모리 내 파이썬 객체 Dataset.to_pandas(), Dataset.to_polars(), 혹은 Dataset.to_dict()

예를 들어 데이터셋을 CSV 파일로 내보내려면 이렇게 해요.

>>> encoded_dataset.to_csv("path/of/my/dataset.csv")

Hugging Face의 데이터셋 저장소Storage Bucket으로 내보내려면 hf:// 경로를 사용해요.

>>> encoded_dataset.to_csv("hf://datasets/username/dataset_name/path/of/my/dataset.csv")
>>> encoded_dataset.to_csv("hf://buckets/username/raw_data_bucket/path/of/my/dataset.csv")

출처: 공식문서