PyTorch - DataSet, DataLoader, Transform

DONGJIN IM·2022년 2월 20일
0
post-thumbnail

시작하기 앞서

데이터를 모델에 먹이는 순서

  1. Data 수집
  2. DataSet클래스에서 어떤 데이터를 불러올지와 데이터에 대한 정보를 반환함
  3. DataSet에서 데이터 개별의 처리방법을 정했다면, 처리된 Data로 DataLoader를 통해 Batch를 형성한 이후 Model에 데이터를 먹여줌

Dataset, DataLoader의 필요성

위 데이터를 모델에 먹이는 순서를 보면 "어떤 데이터를 불러올지" Dataset이 정해줌을 알 수 있다.
모델마다 먹고 싶은 데이터의 형식이 다른데, 이 다른 형식을 DataSet 선언을 통해 만들어 주는 것이다.
단지, Dataset은 단지 1개의 Data만 반환하기 때문에 Batch Size를 정해주어야 할 때 별도의 함수를 형성해야함을 알 수 있다.
그리고, 이런 별도 함수를 형성하지 않고 간단한 선언만으로 설정해 줄 수 있는 것이 DataLoader인 것이다.

  • from torch.utils.data import DataLoader, Dataset를 통해 모듈 호출 가능

DataSet

DataSet이란?

모델에 먹일 데이터의 입력 형태를 정의하는 클래스로, 데이터를 어떻게 모델에 입력시킬 것인지 표준화하는 클래스이다.

Image, Text, Audio 등 데이터 형태에 따라 정의를 다르게 해야 한다.

DataSet 스타일

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 형태이다.

Dataset 주의점

1. 모든 Data를 데이터 생성 시점에서 처리할 필요는 없다!
예를 들어 Image를 Tensor객체로 변환하는 과정은 Dataset으로 데이터를 변환하면서 바로 수행할 수도 있지만, 모델에 먹이기 직전에 바꾸는 경우도 많다.

즉, 데이터 변형은 최대한 Dataset으로의 변환 과정에서 수행하지 않고 모델에 데이터를 먹이기 직전 데이터를 꺼낼 때 변환해도 전혀 문제가 없는 것이다.

실제로, 데이터 변형 과정은 Dataset 생성 시점보다 모델에 데이터가 먹여지기 직전에 원하는 형식으로 변형되는 경우가 많다.

  • 데이터 (내가 원하는 형태로) 변형 : 나중에 배울 torch.nn.Model 모듈을 상속받는 내가 만든 모듈에서 forward() 메서드 수행 시 transforms 메서드를 통해 변형시킴
    • DataSet에서는 "데이터가 정형화된 형식을 따르도록" 변형시키는 것만 생각하자!

2. DataSet의 표준화
Dataset에서 데이터를 처리하는 방법을 표준화하는 것이다.
이 경우, 코드 리뷰를 할 때 나도 편해질 뿐더러 동료도 편해지는 효과를 가지고 온다.

  • 참고로, 최근에는 HuggingFace 등 표준화된 라이브러리에서 만들어준 Dataset을 활용하는 경우도 있다.

Code를 통한 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

DataLoader

DataLoader란?

DataSet으로 처리한 Data의 Batch를 생성해주는 클래스이다.
DataSet은 위에서 말했듯 Index로 접근하는 방식을 많이 활용하는데, 우리가 Batch Size를 지정하고 미니 배치로 데이터를 나누기 위해서는 Index를 통해 데이터를 쪼개는 함수를 만들 필요가 있다.
이 과정을 우리가 함수를 만드는 대신 수행해주는 클래스가 DataLoader이다.

학습 직전(GPU에 데이터를 Feed하기 직전) 데이터의 변환을 책임지는데, 다른 말로 Dataset의 forward() 과정을 수행하는 클래스이기도 한다.

주요 업무는 데이터의 변환 및 Batch 처리라고 할 수 있다.

DataLoader 클래스 분석

  • 매우 정리가 잘 된 사이트 : https://subinium.github.io/pytorch-

    DataLoader(dataset, batch_size, shufffle, sampler, batch_sampler, num_workers, collate_fn, pin_momery, drop_last, timeout, worker_init_fn, *, prefetch_factor, persistent_workers)

batch_size

  • Default : 1
  • 1개 Batch에 몇 개의 Data가 포함되어 있는지 지정해줌
  • (ex) Data가 52개이고 batch_size가 10이라면, 5개의 Batch에는 10개 Data가 포함되어 있고, 마지막 6번째 Batch에는 2개 Data가 포함되어 있음

drop_last

  • batch_size와 데이터 수에 따라 마지막 Batch의 길이가 달라질 수 있는데 이전에도 말했듯 길이가 다른 Batch는 위험성을 가져옴
  • drop_last=True : 마지막 Batch는 활용하지 않음(즉, 버림)

shuffle

  • Default : False
  • 데이터 순서를 섞어서 활용할지, 똑같은 순서로 계속해서 활용할지 결정
  • shuffle=True : Data가 랜덤한 Batch 중 한 곳에 들어감
    • DataSet에서 initalize할 때 Data를 섞어서 같은 효과를 낼 수 있음

sample(batch_sampler와 거의 동일)

  • index를 컨트롤 하는 방법
    • index를 컨트롤하여 Data에 대한 접근 방법을 바꾸는 것
  • Data index를 조정하는 방법으로, shuffle은 무조건 "False"로 지정되어야 함
    • map-style Dataset에서 활용됨
    • 미리 선언된 Sampler들
      • SequentialSampler : 항상 같은 순서
      • RandomSampler : 랜덤
      • SubsetRandomSampler : 랜덤 리스트
      • WeightRandomSampler : 가중치에 따른 확률
      • BatchSampler : Batch 단위로 Sampling 가능
      • DistributedSampler : 분산 처리

num_workers

  • Default : 0
  • Data Loading에 활용되는 subprocess 개수(MultiProcessing)
  • num_workers가 많아지면 Multi Process가 늘어나니 무조건 많으면 좋을까?
    • 정답인 No이다.
    • 데이터를 GPU에서 처리해야 하므로, Multi Processing 과정이 많아 CPU와 GPU 사이 교류가 너무 많아지면 병목 현상이 발생할 수 있어 오히려 성능이 하락할 수 있음

pin_memory

  • True일 경우, Tensor를 CUDA 고정 메모리에 올림
  • 속도 향상에 도움을 주지만, GPU size가 충분할 때만 활용해야 함

time_out

  • Default : 0
  • DataLoader가 data를 불러오는 제한시간

worker_init_fn

  • Default : None
  • num_workers가 정해졌을 때, 어떤 worker를 불러올지 리스트로 전달

collate_fn

collate_fn이란?

데이터 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 없이 수행되게 하는 것이다.

코드를 통한 collate_fn 이해

# 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이 
추가되었음을 알 수 있다
"""
  • torch.nn.utils.rnn.pad_sequence(sequences, batch_first, padding_value)
    • 길이가 다른 Data의 길이를 맞춰주는 Padding을 위한 메서드
    • Sequences : 가변 길이 Data 목록(List)
    • batch_first : Boolean 값으로 설정
      • True : batch끼리 묶어 Padding을 수행. 즉, "Batch에 포함된 Data"들끼리 Padding 진행
      • False : Batch에 포함된 Data들을 "먼저 묶고", 같은 위치에 존재하는 원소값들끼리 묶음. 이 때, 길이가 부족한 List에 padding_value를 추가해 길이를 맞춤
      • 즉, True는 Batch에 포함된 각각의 Data들의 길이를 모두 맞추는 것이고, False일 때는 Data의 같은 index에 존재하는 값끼리 묶은 이후, List 길이를 맞춰주는 것이다.
    • padding_value : Padding을 위해(길이를 맞춰주기 위해) 추가할 Data
"""
원래 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)을 넣어주면 끝

Transform

Transform이란?

Data를 변형시키는 방법으로, Image의 Size를 변경시키거나 이미지를 자르거나, numpy를 torch 형식으로 변경하는 등의 작업을 수행하는 메서드이다.

from torchvision import datasets, transforms를 통해 transforms 모듈을 불러와야 하며, transforms.Compose를 통해 데이터를 변형시키는 일련의 과정을 묶어 한번에 처리할 수 있게 도와준다.

transforms 활용 방법

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에 대하여 차례대로 수행한 이후 결과를 반환하는 것이다
"""

참고 : PyTorch에서 제공해주는 DataSet

  • 위에서는 우리가 Dataset을 직접 만들었지만, PyTorch에서는 개발자들이 Dataset 몇 개를 미리 형성해 놓았음
  • from torchvision import datasets, transforms
    • torchvision에 저장된 dataset, transform 모듈을 활용하여 미리 생성된 dataset을 활용할 수 있으며, 데이터 변형도 가능
  • 이미 만들어진 DataSet 같은 경우 transform Parameter를 통해 위에서 지정해줬던 transforms.Compose()를 통해 만들어진 객체를 바로 넣어줄 수 있음
    • 자동으로 Transform 과정이 수행됨
  • self.dataset = dataset.MNIST(root, train, transform, download)
    • root : 데이터 경로
    • train : True일 경우 테스트용 데이터, False일 경우 학습용 데이터를 가지고 옴
    • transform : 위에서 설정한 transforms.Compose 설정을 Dataset에 먹여줌
      • 위에 설정한 대로 Input Data를 Model에 먹일 수 있음
    • download : True일 경우, MNIST 데이터가 없으면 다운 받음
profile
개념부터 확실히!

0개의 댓글

관련 채용 정보