PyTorch DataSet and DataLoader
모델을 학습시키기 위해서 데이터를 공급해주는데 관여하는 데이터셋, 샘플러, collate_fn과 데이터로더의 전체적인 큰 그림.
DataSet/ DataLoader가 필요한 이유
데이터와 코드 관리를 위해 데이터 처리 모듈과 학습/추론 모듈을 나누어 관리하는 것이 이상적이다.
-Custom DataSet/DataLoader
점점 많은 양의 data를 이용해서 딥러닝 모델을 학습시키는 일이 많아지면서 data를 불러오는데 시간이 오래걸리고 메모리가 부족해 RAM에 data가 다 올라오지 못하는 일이 발생한다. 그래서 데이터를 한번에 다 부르지 않고 하나씩 불러서 쓰는 방법을 택해야하는데, 이 때문에 데이터를 한번에 부르는 기존의 dataset이 아닌 custom dataset을 만들어야한다.
또한, batch를 묶는 방식, 길이와 만드는 형식 등 batch와 관련된 설정을 수정해야할 필요성이 있을 때 custom dataloader가 필요해진다.
DataSet
DataSet은 torch.utils.data.Dataset의 객체를 사용해야 하고, 두 가지의 스타일이 있다.
1. Map-style dataset:
index가 존재하여 data[index]로 데이터를 참조할 수 있음
2. Iterable-style dataset:
random으로 읽기에 어렵거나, data에 따라 batch size가 달라지는 데이터(dynamic batch size)(ex. stream data, real-time log)에 적합하다
next(iterable_dataset)로 데이터를 참조하므로 __iter__을 선언해주어야한다.
하지만, IterableDataSet은 samplier를 사용할 수 없으므로(왜 그런진 모르겠음), random shuffling을 하고 싶다면 미리 데이터셋을 shuffling한 이후에 불러오는 것이 좋다.
또한, DataLoader에서 IterableDataSet을 받았을 때, num_workers > 0의 조건에서 병목이 발생할 수 있다고 한다
https://inmoonlight.github.io/2021/02/21/PyTorch-IterableDataset/
PyTorch의 IterableDataset을 사용해서 데이터 불러오기
PyTorch 1.2 이상부터 torch.utils.data 에서는 크게 map-style dataset (torch.utils.data.Dataset) 과 iterable dataset (torch.utils.data.IterableDataset) 의 두 종류의 데이터 클래스를 지원하고 있다. 데이터 사이즈가 클 때는
inmoonlight.github.io
torchvision.MNIST와 같은 데이터에서는 데이터셋을 제공해주기도 한다. 하지만 여러가지 상황으로 인해 Custom DataSet을 만들어야하는 상황이 생긴다.
Custom DataSet은 torch.utils.data.Dataset을 상속하고
- __init__ 메서드
데이터의 위치나 파일명과 같은 초기화 작업을 위해 동작. 일반적으로 CSV파일이나 XML파일과 같은 데이터를 불러옴. 이렇게 함으로서 모든 데이터를 메모리에 로드하지 않고 효율적으로 사용할 수 있다. 여기에 이미지를 처리할 transforms들을 Compose해서 정의해둔다 - __len__ 메서드
Dataset의 최대 요소 수를 반환하는데 사용. 해당 메서드를 통해서 현재 불러오는 데이터의 인덱스가 적절한 범위 안에 있는지 확인할 수 있다 - __getitem__ 메서드
데이터셋의 idx번째 데이터를 반환하는데 사용. 일반적으로 원본 데이터를 가져와서 전처리하고 데이터 증강하는 부분이 진행된다, Tensor로 변환해서 데이터를 전달해야한다
가 모두 존재해야한다.
# 붓꽃 데이터의 DataSet
class IrisDataset(Dataset):
def __init__(self):
# 데이터를 가져오고, 입력형태를 정의한다
iris = load_iris()
self.X = iris['data']
self.y = iris['target']
self.feature_names = iris['feature_names']
self.target_names = iris['target_names']
def __len__(self):
len_dataset = None
# y는 추론에서는 없으므로 일반적으로 X의 길이를 반환하는 것이 안정적
len_dataset = len(self.X)
return len_dataset
def __getitem__(self, idx):
# idx가 주어질때 그에 맞는 X,y반환
X, y = None, None
X = torch.tensor(self.X[idx],dtype=torch.float)
y = torch.tensor(self.y[idx],dtype=torch.long)
return X, y
DataLoader
torch.utils.data.DataLoder를 사용해야하고, DataSet이 제공하는 데이터들을 받아 batch 기반의 딥러닝 학습을 위해 mini batch를 만들어주는 역할을 수행하는 생성자이다(next로 호출)
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
batch_sampler=None, num_workers=0, collate_fn=None,
pin_memory=False, drop_last=False, timeout=0,
worker_init_fn=None)
DataLoader는 위와 같이 많은 파라미터를 가지고 있다.
- batch_size
배치의 크기, 전달되는 데이터의 shape은 (batch_size, *(data.shape))이다.
- shuffle
데이터를 DataLoader에서 섞어서 사용하겠는지를 설정할 수 있다. 재현을 위해서 torch.manual_seed를 고정할 수 있다
(shuffle = True) == (sampler = RandomSampler)
- sampler
데이터의 idx를 컨트롤하는 방법 , sampler를 설정하려면 shuffle이 false여야한다--> 후술
- batch_sampler
샘플러와 비슷하지만, idx를 한번에 반환한다
- num_workers
데이터 로딩에 사용되는 subprocess 개수, 무작정 num_workers를 올린다고 해서 데이터로딩이 빨라지지는 않는다. (병목현상 때문)
https://jybaek.tistory.com/799
DataLoader num_workers에 대한 고찰
Pytorch에서 학습 데이터를 읽어오는 용도로 사용되는 DataLoader는 torch 라이브러리를 import만 하면 쉽게 사용할 수 있어서 흔히 공식처럼 잘 쓰고 있습니다. 다음과 같이 같이 사용할 수 있겠네요. fr
jybaek.tistory.com
- collate_fn
map-style 데이터셋에서 sample list를 batch 단위로 묶을 때 필요한 함수 정의 --> 후술
- pin_mermory
True로 선언하면, 데이터로더는 Tensor를 CUDA 고정 메모리에 올린다.
https://medium.com/naver-shopping-dev/top-10-performance-tuning-practices-for-pytorch-e6c510152f76
Top 10 Performance Tuning Practices for Pytorch
Pytorch 모델의 학습 및 추론을 가속화 할 수 있는 10가지 팁을 공유드립니다. 코드 몇 줄만 바꿈으로써 속도를 개선하고 모델의 품질 또한 유지할 수 있습니다.
medium.com
- drop_last
batch 단위로 데이터를 불러온다면, batch_size에 따라 마지막 batch의 길이가 달라질 수 있다.
예를 들어 data의 개수는 27개인데, batch_size가 5라면 마지막 batch의 크기는 2가 된다.
batch의 길이가 다른 경우에 따라 loss를 구하기 귀찮은 경우가 생기고, batch의 크기에 의존도가 높은 함수를 사용할 때 걱정이 되는 경우 마지막 batch를 사용하지 않을 수 있다
-time_out
DataLoader가 data를 불러오는데에 주어지는 제한시간
-worker_init_fn
num_worker가 개수라면, 이 파라미터는 어떤 worker를 불러올 것인가를 리스트로 전달
# dataloader 선언
dataloader = torch.utils.data.DataLoader(map_dataset)
for data in dataloader:
print(data['label'])
Sampler
커스텀 Sampler를 만들기 위해서는
- __iter__(): 데이터세트 요소의 인덱스를 반복하는 방법을 제공하는 메서드
- __len__(): 반환된 반복자의 길이를 반환하는 메서드
를 구현해야한다.
PyTorch에 구현된 Sampler
- SequentialSampler
idx와 같이 순차적으로 샘플링
- RandomSampler
랜덤으로 샘플링 == shuffle = True
- SubsetRandomSampler
지정된 인덱스 목록에서 대체 없이 무작위로 요소를 샘플링
- WeightedRandomSampler
주어진 확률(가중치)로 요소를 샘플링
- BatchSampler
다른 샘플러를 래핑하여 인덱스의 미니 배치를 생성
*stratifiead sampling(계층적 샘플링) for pytorch
count = [0] * nclasses
for item in images:
count[item[1]] += 1
weight_per_class = [0.] * nclasses
N = float(sum(count))
for i in range(nclasses):
weight_per_class[i] = N/float(count[i])
weight = [0] * len(images)
for idx, val in enumerate(images):
weight[idx] = weight_per_class[val[1]]
return weight
# And after this, use it in the next way:
dataset_train = datasets.ImageFolder(traindir)
# For unbalanced dataset we create a weighted sampler
weights = make_weights_for_balanced_classes(dataset_train.imgs, len(dataset_train.classes))
weights = torch.DoubleTensor(weights)
sampler = torch.utils.data.sampler.WeightedRandomSampler(weights, len(weights))
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=args.batch_size, shuffle = True,
sampler = sampler, num_workers=args.workers, pin_memory=True)
collate_fn
zero-padding이나 Variable Size 데이터 등 데이터 사이즈를 맞추기 위해 많이 사용
#Zeropadding
class ExampleDataset(Dataset):
def __init__(self, num):
self.num = num
def __len__(self):
return self.num
def __getitem__(self, idx):
return {"X":torch.tensor([idx] * (idx+1), dtype=torch.float32),
"y": torch.tensor(idx, dtype=torch.float32)}
dataset_example = ExampleDataset(10)
dataloader_example = torch.utils.data.DataLoader(dataset_example)
for d in dataloader_example:
print(d['X'])
'''
# print
tensor([[0.]])
tensor([[1., 1.]])
tensor([[2., 2., 2.]])
tensor([[3., 3., 3., 3.]])
tensor([[4., 4., 4., 4., 4.]])
tensor([[5., 5., 5., 5., 5., 5.]])
tensor([[6., 6., 6., 6., 6., 6., 6.]])
tensor([[7., 7., 7., 7., 7., 7., 7., 7.]])
tensor([[8., 8., 8., 8., 8., 8., 8., 8., 8.]])
tensor([[9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]])
'''
# batch가 2일 때 오류발생
dataloader_example = torch.utils.data.DataLoader(dataset_example, batch_size=2)
for d in dataloader_example:
print(d['X'])
'''
collate_fn을 완성하여 하나의 batch에는 동일한 길이를 반환할 수 있도록 만드세요.
하나의 batch에서 가장 길이가 긴 sample 기준으로 길이를 맞춥니다.
길이를 맞출 때는 비어있는 오른쪽을 0으로 패딩합니다.ex) 1 1 1 -> 1 1 1 0 0 0
'''
def my_collate_fn(samples):
collate_X = []
collate_y = []
######################################TODO######################################
max_len = max([len(sample['X']) for sample in samples])
for sample in samples:
diff = max_len-len(sample['X'])
if diff > 0:
zero_pad = torch.zeros(size=(diff,))
collate_X.append(torch.cat([sample['X'], zero_pad], dim=0))
else:
collate_X.append(sample['X'])
collate_y = [sample['y'] for sample in samples]
################################################################################
return {'X': torch.stack(collate_X),
'y': torch.stack(collate_y)}
torchvision.transforms
torchvision에서 제공하는 데이터 전처리 transform
- transforms.Resize
이미지 사이즈 변환
- transforms.RandomCrop
이미지 자르기
- transforms.RandomRotation
이미지 회전
- transforms.ToTensor()
이미지를 텐서로, 텐서로 바꿀때 정규화가 된다 (/225.를 수행한다)
- transforms.Compose
여러 transforms 묶기
transforms.Compose([transforms.Resize((224,224)),
transforms.RandomVerticalFlip(0.5),
transforms.CenterCrop(150)])(im)
아래의 예시와 같이 데이터셋을 가져올때 전처리로 쓰인다.
dataset_train_CIFAR10 = torchvision.datasets.CIFAR10(root='data/CIFAR10/', # 다운로드 경로 지정
train=True, # True를 지정하면 훈련 데이터로 다운로드
transform=transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))
]), # 텐서로 변환
download=True,
)
DataSet & DataLoader 예시
- CIFAR10
dataset_train_CIFAR10 = torchvision.datasets.CIFAR10(root='data/CIFAR10/', # 다운로드 경로 지정
train=True, # True를 지정하면 훈련 데이터로 다운로드
transform=transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))
]), # 이미지 전처리 텐서로 변환
download=True,
)
dataloader_train_CIFAR10 = DataLoader(dataset=dataset_train_CIFAR10,
batch_size=16, # 배치 사이즈 16
shuffle=True, # 섞는다
num_workers=4, # 4개의 서브 프로세스 활용
)
- Titanic
class TitanicDataset(Dataset):
def __init__(self, path, drop_features, train=True):
self.data = pd.read_csv(path) # 데이터 가져오기
# 범주형 데이터 처리
self.data['Sex'] = self.data['Sex'].map({'male':0, 'female':1})
self.data['Embarked'] = self.data['Embarked'].map({'S':0, 'C':1, 'Q':2})
self.train = train #훈련모드 여부
self.data = self.data.drop(drop_features, axis=1) #피처 삭제
# X,y값 지정
self.X = self.data.drop('Survived', axis=1)
self.y = self.data['Survived']
self.features = self.X.columns.tolist()
self.classes = ['Dead', 'Survived']
def __len__(self):
len_dataset=None
len_dataset = len(self.data) # 길이반환
return len_dataset
def __getitem__(self, idx):
X, y = None, None
# pandas로 연산
X = self.X.iloc[idx].values
# 학습모드면 y값반환 아니면 반환 x
if self.train:
y = self.y.iloc[idx]
return torch.tensor(X), torch.tensor(y) # 텐서로 바꿔줌
dataset_train_titanic = TitanicDataset('./data/titanic/train.csv',
drop_features=['PassengerId', 'Name', 'Ticket', 'Cabin'],
train=True)
dataloader_train_titanic = DataLoader(dataset=dataset_train_titanic,
batch_size=8,
shuffle=True,
num_workers=4,
)
-MNIST
class MyMNISTDataset(Dataset):
def __init__(self, path, transform, train=True):
self.path = path
#따로 함수를 지정해서 파일로 읽어옴
self.X = read_MNIST_images(self.path['image'])
self.y = read_MNIST_labels(self.path['label'])
# 클래스 지정
self.classes = ['0 - zero', '1 - one', '2 - two', '3 - three', '4 - four',
'5 - five', '6 - six', '7 - seven', '8 - eight', '9 - nine']
self.train = train #훈련모드
self.transform = transform # 전처리함수
self._repr_indent = 4 # __repr__에서 띄어쓰기 값
def __len__(self):
len_dataset = None
len_dataset = len(self.X) # 데이터의 길이
return len_dataset
def __getitem__(self, idx):
X,y = None, None
X = self.X[idx]
# 데이터 전처리
if self.transform:
X = self.transform(X)
# 훈련모드일때만 y값 반환
if self.train:
y = self.y[idx]
return torch.tensor(X, dtype=torch.double), torch.tensor(y, dtype=torch.long)
# 호출되었을 때 출력할 값
def __repr__(self):
head = "(PyTorch HomeWork) My Custom Dataset : MNIST"
data_path = self._repr_indent*" " + "Data path: {}".format(self.path['image'])
label_path = self._repr_indent*" " + "Label path: {}".format(self.path['label'])
num_data = self._repr_indent*" " + "Number of datapoints: {}".format(self.__len__())
num_classes = self._repr_indent*" " + "Number of classes: {}".format(len(self.classes))
return '\n'.join([head,
data_path, label_path,
num_data, num_classes])
dataset_train_MyMNIST = MyMNISTDataset(path=TRAIN_MNIST_PATH,
transform=transforms.Compose([
transforms.ToTensor()
]),
train=True
)
dataloader_train_MNIST = DataLoader(dataset=dataset_train_MyMNIST,
batch_size=16,
shuffle=True,
num_workers=4,
)