데이터셋 처리
데이터셋 처리 (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를 받아요. 아래 예시는 ClassLabel과 Value 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")
출처: 공식문서