Pytorch에서 데이터를 로딩하고 전처리하기 위한 torch.utils.data 모듈에 대해 살펴보자.
이를 항상 대충 넘어가서, 이미지 데이터 등을 로드하는 단계부터 막힐 때가 많았다.
잘 짚고 넘어가자.
torch.utils.data 모듈은 PyTorch에서 데이터 처리를 효율적으로 수행하기 위한 도구들을 제공한다.
데이터셋을 정의하고, 데이터 로더를 통해 데이터를 효율적으로 불러올 수도 있다.
데이터셋을 정의하기 위한 기본 클래스이다.
모든 사용자정의 데이터 셋은 Dataset 클래스를 우선 상속 받아야 한다.
Dataset 클래스를 상속을 받는 이유는 다양한 유형의 데이터(이미지, 텍스트, 시계열 등)를 일관된 방식으로 다룰 수 있고, Dataset 클래스를 정의함으로써 데이터 전처리, 증강, 로딩 등의 작업을 쉽게 추가할 수 있는 확장성 때문이다.
그 안에 __init__ 메소드가 구현됨은 물론, __len__, __getitem__ 메소드를 구현해야 한다. (len 메소드는 선택적이라고 공식 문서에 적혀있기는 하나, DataLoader 사용 시 문제가 될 수 있어 권장되는 거의 필수적인 메소드다.)
예시 코드는 아래와 같다.
from torch.utils.data import Dataset
class CustomDataset(Dataset): # Dataset의 상속
def __init__(self, data, labels):
self.data = data
self.labels = labels
def __len__(self): # 필수 메소드1 len
return len(self.data)
def __getitem__(self, idx): # 필수 메소드2 getitem
sample = self.data[idx]
label = self.labels[idx]
return sample, label
왜 len, getitem이라는 메소드를 굳이 정의하는지?
여기서 왜 len이란 메소드를 굳이 정의하는지 궁금해서 알아보았다. (매직메소드)
Python의 내장 함수인 len()은 기본적으로 객체의 길이를 반환하는 함수이다.
하지만, 우리가 클래스로 지정하는 새로운 클래스 객체는 len이라는 내장함수가 기본적으로 인식할 수 있는 객체에 해당하지 않는 것이다.
이 경우, 해당 클래스 객체에 대해서 len이라는 메소드를 따로 구현하고 이를 호출하도록 해줘야 한다.
만약 이러한 클래스 객체에 대해서 len 메소드를 구현하지 않고 해당 객체에 len 함수를 사용한다면 TypeError가 발생한다.
(TypeError: object of type 'MyDataset' has no len())
__getitem__ 이라는 매직 메소드도 마찬가지로, 커스텀한 Dataset 클래스에 인덱싱을 가능토록 하는 기능을 부여한다.
인덱싱을 구현함으로써 batch 샘플을 로딩하는 속도를 높일 수도 있다.
</div>
또한, IterableDataset이라는 클래스도 있는데, 이 클래스는 데이터셋의 크기를 미리 알 수 없거나, 인덱싱을 할 수 없는 경우에 사용되는 클래스다.
주로, 스트리밍 데이터, 로그 파일, 센서 데이터 등 계속해서 생성되는 데이터를 다룰 때 사용된다.
Dataset 클래스를 정의할 때 __init__ 메소드에서 초기화에 사용하는 속성으로는 다음과 같은 내용이 있다.
데이터: self.data
레이블/타겟: self.labels
변환: self.transform
파일 경로/이름: self.file_paths
기타
import torch
from torch.utils.data import Dataset
from PIL import Image
import os
class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
"""
데이터셋 초기화 메소드.
Args:
annotations_file (str): 이미지 파일과 레이블이 기록된 CSV 파일 경로
img_dir (str): 이미지 파일이 저장된 디렉토리 경로
transform (callable, optional): 입력 이미지에 적용할 변환 함수
target_transform (callable, optional): 타겟 레이블에 적용할 변환 함수
"""
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
self.target_transform = target_transform
def __len__(self):
"""
데이터셋의 샘플 수를 반환합니다.
"""
return len(self.img_labels)
def __getitem__(self, idx):
"""
주어진 인덱스에 해당하는 샘플을 반환합니다.
Args:
idx (int): 데이터 샘플 인덱스
"""
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = Image.open(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
return image, label
Dataset 객체를 입력으로 받아 데이터를 mini-batch 단위로 로드하고 효율적인 처리를 돕는 클래스
data loader는 데이터셋과 sampler를 결합하고 주어진 데이터셋을 반복할 수 있는 기능을 제공한다.
데이터셋의 반복, shuffle, 병렬 로딩 등을 지원한다.
map-style, iterable-style dataset을 모두 지원하며, 단일/병렬 처리 로딩, 로딩 순서 커스터마이징 등도 지원한다.
주요 parameter로는 아래가 있다. (data 빼고는 전부 optional)
data: 로딩하고자 하는 데이터를 Dataset 객체로 받아야 한다.
batch_size: batch당 몇 개의 sample이 들어가야할지를 정의한다.
batch_size를 설정할 경우, data loader는
shuffle: 매 epoch마다 데이터를 다시 섞는 설정 (default: False) -> 모델이 데이터 순서에 의존하지 않도록 도움
num_workers: 데이터를 병렬로 로드할 수 있도록 한다. (default = 0) -> 데이터 로딩 속도를 크게 향상시킴
drop_last: 데이터셋의 크기가 배치 크기로 나누어 떨어지지 않을 때, 마지막 mini-batch를 버리는 기능
pin_memory: 이 설정이 True로 설정되면 데이터를 GPU로 전송하기 전에 고정된 메모리 영역에 저장하여 전송 속도를 향상시킬 수 있음
(기타 parameter)
collate_fn: 사용자 정의 배치 함수를 사용하기 위한 기능
sampler: 데이터 샘플링 방식을 커스터마이징 하는 기능
이처럼 DataLoader를 사용하면 1. 데이터셋을 로드하고, 2. 사용한 sampler에 따라 인덱스 번호를 붙여서 갖고 있음
만약, batch_size 등을 입력한 경우에는 data loader가 처리한 내용은 배치 단위로 저장되어 있다.
그래서 dataloader = DataLoader(dataset, batch_size = 4, shuffle = True) 를 써서 저장된 dataloader는 반복문으로 돌려서 batch 단위로 데이터를 꺼내서 쓸 수 있게 되는 것이다.
Sampler 클래스는 데이터셋에서 샘플을 어떻게 선택할지를 정의하는 데에 사용되는 클래스다.
sampler는 데이터셋의 인덱스를 생성하여 데이터 로딩 과정에서 사용할 인덱스를 명시하고, 이 인덱스를 기준으로 mini-batch에서 사용할 sample들을 선택한다.
데이터를 로드할 때 어떤 순서로 샘플을 선택할지를 결정
샘플링 방식을 커스터마이징 할 수도 있다.
1. SequentialSampler
데이터셋을 순서대로 샘플링함
데이터를 섞지 않고 순차적으로 접근할 때 사용
shuffle = False 설정인 경우 DataLoader가 사용하는 Sampler
2. RandomSampler
랜덤하게 샘플링
shuffle = True 설정인 경우 DataLoader가 사용하는 Sampler
3. SubsetRandomSampler
데이터셋의 부분 데이터셋을 랜덤하게 샘플링
4. WeightedRandomSampler
가중치를 기반으로 샘플링하여 각 샘플의 선택 확률을 다르게 설정할 때 사용
e.g. 클래스 불균형 문제를 해결하기 위해 특정 클래스의 샘플을 더 자주 선택할 때
5. BatchSampler
mini-batch를 생성하기 위해 샘플링
배치 단위로 샘플을 선택함
batch_size = int로 설정한 경우 DataLoader가 사용하는 Sampler
Dataset 클래스를 사전에 정의해주어야 하고, 이 객체를 DataLoader에 집어넣을 수 있다.
Dataset 클래스를 정의하면서 __init__ 메소드 안에는 속성으로 data, label, transform, file_path 등을 지정해주는 경우가 많다.
DataLoader는 batch를 쓰면 배치 단위로 나뉘어진 데이터를 돌려주어 이를 iterable하게 확인할 수 있다.
shuffle을 쓰면 Random Sampling, 안 쓰면 SequentialSampling shuffle을 쓰고, batch_size를 쓰면 batch마다 랜덤하게 샘플링 된 배치가 생성되어서, epoch마다 배치에 들어간 데이터가 달라진다.