위 데이터를 모델에 먹이는 순서를 보면 "어떤 데이터를 불러올지" Dataset이 정해줌을 알 수 있다.
모델마다 먹고 싶은 데이터의 형식이 다른데, 이 다른 형식을 DataSet 선언을 통해 만들어 주는 것이다.
단지, Dataset은 단지 1개의 Data만 반환하기 때문에 Batch Size를 정해주어야 할 때 별도의 함수를 형성해야함을 알 수 있다.
그리고, 이런 별도 함수를 형성하지 않고 간단한 선언만으로 설정해 줄 수 있는 것이 DataLoader인 것이다.
from torch.utils.data import DataLoader, Dataset
를 통해 모듈 호출 가능모델에 먹일 데이터의 입력 형태를 정의하는 클래스로, 데이터를 어떻게 모델에 입력시킬 것인지 표준화하는 클래스이다.
Image, Text, Audio 등 데이터 형태에 따라 정의를 다르게 해야 한다.
1. Map-Style dataset
index가 존재하여 data[index]로 데이터 참조가 가능한 Dataset이다.
__getitem__(), __len__()을 선언해줘야 활용 가능하다.
주로 이 Map-Style Dataset을 많이 활용한다
2. Iterable-Style dataset
Iteration을 통해서만 데이터를 읽을 수 있는 Dataset이다.
Index로는 접근 할 수 없으며, Iterable 객체를 읽는 방법을 활용하여 데이터 접근이 가능하다.
__iter__()을 선언해줘야 활용 가능하다.
Random으로 읽기 어렵거나, data에 따라 batch size가 달라지는 dynamic batch size에 적합한 Dataset 형태이다.
1. 모든 Data를 데이터 생성 시점에서 처리할 필요는 없다!
예를 들어 Image를 Tensor객체로 변환하는 과정은 Dataset으로 데이터를 변환하면서 바로 수행할 수도 있지만, 모델에 먹이기 직전에 바꾸는 경우도 많다.
즉, 데이터 변형은 최대한 Dataset으로의 변환 과정에서 수행하지 않고 모델에 데이터를 먹이기 직전 데이터를 꺼낼 때 변환해도 전혀 문제가 없는 것이다.
실제로, 데이터 변형 과정은 Dataset 생성 시점보다 모델에 데이터가 먹여지기 직전에 원하는 형식으로 변형되는 경우가 많다.
2. DataSet의 표준화
Dataset에서 데이터를 처리하는 방법을 표준화하는 것이다.
이 경우, 코드 리뷰를 할 때 나도 편해질 뿐더러 동료도 편해지는 효과를 가지고 온다.
import torch
from torch.utils.data import Dataset
class CustomDataset(Dataset):
# Dataset Module을 상속받았으니, Data처리가 가능한 Module!
def __init__(self, text, labels):
"""
초기 Data 처리 방법을 지원
주로, text에는 Input, labels에는 실제 결과값을 입력하는 경우가 많은 것 같다
(ex) {"Happy", "Positive"}라는 Data가 있을 경우, "Happy"라는 입력으로
"Positive"라는 출력 값을 도출하고 싶을 때,
label = "Positive", data = "Happy"가 저장될 것
"""
self.labels = labels
self.data = text
def __len__(self): # 데이터 전체 길이
return len(self.labels)
def __getitem__(self, idx):
# index를 줬을 때 데이터를 어떻게 접근하고 어떤 형태로 반환할 것인가!
# 이 메서드는 나중에 배울 DataLoader 객체가 호출하게 됨
label = self.labels[idx]
text = self.data[idx]
sample = {"Text":text, "Class":label}
# 여기에서는 Dict-Type으로 Data를 처리하여 반환했다
return sample
DataSet으로 처리한 Data의 Batch를 생성해주는 클래스이다.
DataSet은 위에서 말했듯 Index로 접근하는 방식을 많이 활용하는데, 우리가 Batch Size를 지정하고 미니 배치로 데이터를 나누기 위해서는 Index를 통해 데이터를 쪼개는 함수를 만들 필요가 있다.
이 과정을 우리가 함수를 만드는 대신 수행해주는 클래스가 DataLoader이다.
학습 직전(GPU에 데이터를 Feed하기 직전) 데이터의 변환을 책임지는데, 다른 말로 Dataset의 forward() 과정을 수행하는 클래스이기도 한다.
주요 업무는 데이터의 변환 및 Batch 처리라고 할 수 있다.
DataLoader(dataset, batch_size, shufffle, sampler, batch_sampler, num_workers, collate_fn, pin_momery, drop_last, timeout, worker_init_fn, *, prefetch_factor, persistent_workers)
데이터 input의 size가 data마다 다를 수 있다. 예를 들어, N이라는 data N개로 이루어진 list가 입력값이라고 가정하자.
이 때, N이 1이면 data 길이가 1이지만, N이 10일 경우 data 길이가 10이 될 것이다.
만약 이런 데이터에 대하여 batch_size를 2이상으로 하면 Erorr 발생한다.
따라서 Batch로 묶일 모든 데이터를 공통된 길이로 묶어주는 함수가 필요하고, 이 함수를 지정해주는 Parameter가 collate_fn이 되는 것이다
(위에선 길이라고만 한정하여 설명하였지만, 길이 이외에도 collate_fn을 통해 Batch의 특성을 설정하는 등 많은 역할을 수행할 수 있다)
길이를 맞추거나 비어 있는 부분을 '0' 등으로 사용자 임의 지정 값으로 채운 이후(Padding) Batch로 데이터를 묶어 Error 없이 수행되게 하는 것이다.
# Data 길이가 다른 DataSet을 형성해보자
import torch
from torch.utils.data import Dataset, DataLoader
class MakeDataset(Dataset):
def __len__(self):
return 10
def __getitem__(self, idx):
return {"input":torch.tensor([idx] * (idx+1), dtype=torch.float32),
"label": torch.tensor(idx, dtype=torch.float32)}
dataset = MakeDataset()
"""
MakeDataset을 통해 만들어진 Data들은 모두 길이가 다르다
아래는 DataSet의 모든 input값을 출력한 것이다
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.]])
"""
# Case 1 : 위에서 만든 Data를 바로 DataLoader에 묶어보자
dataloader = DataLoader(dataset, batch_size = 3)
"""
결과 : RuntimeError 발생
이유를 보면 stack expects each tensor to be equal size라고 되어 있다
즉, Batch에 포함된 element들은 길이가 모두 같아야 Batch로 묶어줄 수 있는 것이다
참고로, batch_size=1일 경우에는 Error가 발생하진 않는다
"""
# Case 2 : collate_fn Parameter를 활용하여 Paddding을 통해 길이를 맞춰주자!
# 과정 1 : Padding 시켜주는 메서드 생성
# Batch size만큼 Data가 쪼개진 뒤 해당 메서드를 수행하게 됨
def make_batch(samples):
inputs = [sample['input'] for sample in samples]
labels = [sample['label'] for sample in samples]
padded_inputs = torch.nn.utils.rnn.pad_sequence(inputs,
batch_first = True)
return {'input: padded_inputs.contiguous(),
'label': torch.stack(labels).contiguous()}
dataloader = DataLoader(datasets, batch_size=3, collate_fn = make_batch)
for data in dataloader:
print(data['input'])
"""
결과
tensor([[0., 0., 0.],
[1., 1., 0.],
[2., 2., 2.]])
tensor([[3., 3., 3., 3., 0., 0.],
[4., 4., 4., 4., 4., 0.],
[5., 5., 5., 5., 5., 5.]])
tensor([[6., 6., 6., 6., 6., 6., 6., 0., 0.],
[7., 7., 7., 7., 7., 7., 7., 7., 0.],
[8., 8., 8., 8., 8., 8., 8., 8., 8.]])
tensor([[9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]])
Batch_Size = 3이므로 1개 Batch에 3개 Data가 저장되어 있음을 알 수 있다
원래 DataSet에 저장된 Data들과 비교하면, 길이를 맞추기 위해 0이
추가되었음을 알 수 있다
"""
"""
원래 Data(dataset)
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.]])
"""
def make_batch_false(samples): # batch_first = False인 Case
inputs = [sample['input'] for sample in samples]
labels = [sample['label'] for sample in samples]
padded_inputs = torch.nn.utils.rnn.pad_sequence(inputs,
batch_first=False)
return {'input': padded_inputs.contiguous(),
'label': torch.stack(labels).contiguous()}
def make_batch_true(samples): // batch_first = True인 Case
inputs = [sample['input'] for sample in samples]
labels = [sample['label'] for sample in samples]
padded_inputs = torch.nn.utils.rnn.pad_sequence(inputs,
batch_first=True)
return {'input': padded_inputs.contiguous(),
'label': torch.stack(labels).contiguous()}
dataloader_false = DataLoader(dataset, batch_size=3,
collate_fn=make_batch_false)
dataloader_true = DataLoader(dataset, batch_size=3,
collate_fn=make_batch_true)
print("Batch_First가 False일 경우")
for data in dataloader_false:
print(data['input'])
print("=======================")
print("Batch_First가 True일 경우")
for data in dataloader_true:
print(data['input'])
<결과>
Batch_First가 False일 경우
tensor([[0., 1., 2.],
[0., 1., 2.],
[0., 0., 2.]])
tensor([[3., 4., 5.],
[3., 4., 5.],
[3., 4., 5.],
[3., 4., 5.],
[0., 4., 5.],
[0., 0., 5.]])
tensor([[6., 7., 8.],
[6., 7., 8.],
[6., 7., 8.],
[6., 7., 8.],
[6., 7., 8.],
[6., 7., 8.],
[6., 7., 8.],
[0., 7., 8.],
[0., 0., 8.]])
tensor([[9.],
[9.],
[9.],
[9.],
[9.],
[9.],
[9.],
[9.],
[9.],
[9.]])
=======================
Batch_First가 True일 경우
tensor([[0., 0., 0.],
[1., 1., 0.],
[2., 2., 2.]])
tensor([[3., 3., 3., 3., 0., 0.],
[4., 4., 4., 4., 4., 0.],
[5., 5., 5., 5., 5., 5.]])
tensor([[6., 6., 6., 6., 6., 6., 6., 0., 0.],
[7., 7., 7., 7., 7., 7., 7., 7., 0.],
[8., 8., 8., 8., 8., 8., 8., 8., 8.]])
tensor([[9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]])
True일 경우 : Batch에 원래 존재하던 Data 변형 없이 값만 추가하여 Padding 진행
False일 경우(첫번째 Batch만 보자)
1. batch에 포함된 [0], [1,1], [2,2,2]를 처리해야 함
2. Batch에 포함된 Data 중 같은 index끼리 있는 값끼리 묶음
[0,1,2], [null, 1, 2], [null, null, 2]가 됨
3. 마지막으로 null에 우리가 지정한 padding_value(현재 0)을 넣어주면 끝
Data를 변형시키는 방법으로, Image의 Size를 변경시키거나 이미지를 자르거나, numpy를 torch 형식으로 변경하는 등의 작업을 수행하는 메서드이다.
from torchvision import datasets, transforms
를 통해 transforms 모듈을 불러와야 하며, transforms.Compose
를 통해 데이터를 변형시키는 일련의 과정을 묶어 한번에 처리할 수 있게 도와준다.
trsfm = transforms.Compose([
transforms.ToTensor(),
transforms.RandomGrayscale(p=0.5),
transforms.CenterCrop(100),
transforms.RandomHorizontalFlip(p=0.5)
])
y = trsfm(x)
"""
y는 x 이미지를 변형시킨 최종 (변형된) 이미지가 저장될 것이다
trsfm에 저장된 처리 과정은 총 4개이다.
설정한 tensor 변형, randomgrayscale, centercrop, randomhorizontalflip을
이미지 x에 대하여 차례대로 수행한 이후 결과를 반환하는 것이다
"""
from torchvision import datasets, transforms
self.dataset = dataset.MNIST(root, train, transform, download)