본문 바로가기

ML&DL/PyTorch

[PyTorch] Dataset과 Dataloader 설명 및 custom dataset & dataloader 만들기

Custom dataset/dataloader 가 필요한 이유

점점 많은 양의 data를 이용해서 딥러닝 모델을 학습시키는 일이 많아지면서 그 많은 양의 data를 한번에 불러오려면 시간이 오래걸리는 것을 넘어서서 RAM이 터지는 일이 발생한다. 데이터를 한번에 다 부르지 않고 하나씩만 불러서 쓰는 방식을 택하면 메모리가 터지지 않고 모델을 돌릴 수 있다. 그래서 모든 데이터를 한번에 불러놓고 쓰는 기존의 dataset말고 custom dataset을 만들어야할 필요가 있다. 또한 길이가 변하는 input에 대해서 batch를 만들때나 batch 묶는 방식을 우리가 정해주어야할 때 dataloader에서 batch를 만드는 부분을 수정해야할 필요가 있어 custom dataloader를 사용한다. 이번 포스팅에서는 dataset과 dataloader 클래스가 어떻게 구성되어있는 지 살펴보고, 내가 사용하는 음성데이터셋에 대해서 내가 어떤 식으로 custom dataset/dataloader를 정의하는지 예를 들며 설명해보겠다. 

Dataset

Dataset class는 전체 dataset을 구성하는 단계이다. (이 class를 상속받아서 나만의 dataset을 만들면 된다.) input으로는 전체 x(input feature)과 y(label)을 tensor로 넣어주면 된다. dataset의 구성은 아래와 같다.

 

  • __init__(self): 여기서 필요한 변수들을 선언한다. 전체 x_data와 y_data load하거나 파일목록을 load하자.
  • __get_item__(self, index): index번째 data를 return하도록 코드를 짜야한다. (여기서 tensor를 return해야함)
  • __len__(self): x_data나 y_data는 길이가 같으니까 아무 length나 return하면 된다.
import torch
import torch.utils.data as data


class BasicDataset(data.Dataset):
    def __init__(self, x_tensor, y_tensor):
        super(BasicDataset, self).__init__()

        self.x = x_tensor
        self.y = y_tensor
        
    def __getitem__(self, index):
        return self.x[index], self.y[index]

    def __len__(self):
        return len(self.x)
        
if __name__ == "__main__":
    train_x = torch.rand(500)
    train_y = torch.rand(500)
    tr_dataset = BasicDataset(train_x, train_y)

Custom Dataset for big dataset 

너무 큰 데이터셋이어서 RAM에 다 올릴 수 없을 때, 사용자정의 데이터셋을 정의해서 사용할 수 있다. 예시코드를 작성해보면 아래와 같고 한 파트씩 설명하겠다.

 

import torch
import torch.utils.data as data
import pandas as pd
import numpy as np

def file_load(opt):
    data_path = []
    f = open("{0}.txt".format(opt), 'r')
    while True:
        line = f.readline()
        if not line: break
        data_path.append(line[:-1])
    f.close()
    return data_path

class CustomDataset(data.Dataset):
    def __init__(self, opt_data):
        super(CustomDataset, self).__init__()

        """
        opt_data : 'train', 'validation'
        
        """
        self.file_list = file_load('E:/audio_data/train/tr')
        y = pd.read_csv('audio_data/train_answer.csv', index_col=0)
        self.y = y.values
        
    def __getitem__(self, index):
        
        x = np.load(self.file_list[index])
        self.x_data = torch.from_numpy(x).float()
        self.y_data = torch.from_numpy(self.y[index]).float()
        return self.x_data, self.y_data

    def __len__(self):
        return len(self.y)
        
if __name__ == "__main__":
    a = CustomDataset('train')

1) __init__(self):

    def __init__(self, opt_data):
        super(CustomDataset, self).__init__()

        """
        opt_data : 'train', 'validation'
        
        """
        self.file_list = file_load('E:/audio_data/{0}'.format(opt_data))
        y = pd.read_csv('audio_data/{0}_answer.csv'.format(opt_data), index_col=0)
        self.y = y.values

 

opt_data라는 매개변수를 통해 train인지 validation인지 선택할 수 있게 했다. 필요한 데이터는 x_data(input feature) 와 y_data(label 혹은 어떤 정답값)이다. x_data가 대규모 음성데이터라고 하면 데이터를 다 불러오는데 시간과 메모리가 너무 많이 들 수 있다. 그래서 x_data는 file name만 load해놓는 방식으로 구현했다. file name은 txt이나 json형식을 많이 사용하는 것 같다. y_data의 경우 그냥 csv 파일이기 때문에 한번에 불러서 y라는 변수에 저장하였다. 그리고 멤버함수에서 써야되는 변수들은 self.를 이용해서 멤버변수로 설정해야 다른 함수에서 불러 쓸 수 있다.

2) __get_item__(self, index):

 

    def __getitem__(self, index):
        
        x = np.load(self.file_list[index])
        self.x_data = torch.from_numpy(x).float()
        self.y_data = torch.from_numpy(self.y[index]).float()
        return self.x_data, self.y_data

 

여기서는 전체 x_data와 y_data 중에 해당 index번째의 샘플을 뽑아오는 단계이다. x_data의 경우 file_list에서 해당 index의 파일명을 불러와 이를 이용해서 index에 해당하는 샘플을 불러오면 된다. 이후 x와 y[index]를 tensor로 변환해주기 위해 from_numpy함수를 사용했다. 모델에서 weight들이 float형태이기 때문에 .float()을 이용해서 float형으로 변환해주어야 한다.

3) __len__(self):

    def __len__(self):
        return len(self.y)

 

여기선 전체 dataset의 size를 return하면 되기 때문에 x나 y의 length 중 아무거나 return하면 된다. testset의 경우는 y값이 없기 때문에 x length를 return하는 것이 공용으로 dataset을 정의하기에는 더 좋다.

Dataloader

Dataloader class는 batch기반의 딥러닝모델 학습을 위해서 mini batch를 만들어주는 역할을 한다. dataloader를 통해 dataset의 전체 데이터가 batch size로 slice된다. 앞서 만들었던 dataset을 input으로 넣어주면 여러 옵션(데이터 묶기, 섞기, 알아서 병렬처리)을 통해 batch를 만들어준다. 서버에서 돌릴 때는 cpu num_worker를 조절해서 load속도를 올릴 수 있지만, PC에서는 default로 설정해야 오류가 안난다.

 

import torch.utils.data

tr_dataset = BasicDataset(train_x, train_y)
train_loader = data.DataLoader(dataset=tr_dataset, batch_size=128, num_workers=8, shuffle=True)

tt_dataset = BasicDataset(test_x, test_y)
test_loader = data.DataLoader(dataset=tt_dataset, batch_size=128, num_workers=8, shuffle=False)

 

[Dataloader의 주요 파라미터들]

  • shuffle: (default:False) Train이면 얘를 켜고 아니면 얘를 꺼줘야한다.
  • drop_last: (default: False) 얘를 켜면 맨 마지막에 나눠떨어지지 않는 batch를 버린다. (비효율적연산이 될 수 있기 때문) -> 학습할 때는 끄는게 나을지도?, 평가할 땐 켜야함..^^
  • num_workers: (default:0) cpu를 몇개 쓸지
  • collate_fn: batch를 만들어주는 함수

Custom Dataloader for variable length sequence

길이가 변하는 input을 처리하기 위해서 dataloader의 collate_fn을 사용자정의 함수로 다시 재정의하여 사용할 수 있다. 코드로 예시를 들어보자.

 

import torch
import torch.utils.data as data

class CustomDataLoader(data.DataLoader):
    def __init__(self, *args, **kwargs):
        super(CustomDataLoader, self).__init__(*args, **kwargs)
        self.collate_fn = _collate_fn


def _collate_fn(batch):
    
    """
    Args:
        batch: list, len(batch) = 1. See AudioDataset.__getitem__()
    Returns:
        mix_torch: B x ch x T, torch.Tensor
        ilens_torch : B, torch.Tentor
        src_torch: B x C x T, torch.Tensor
        
    ex)
    torch.Size([3, 6, 64000])
    tensor([64000, 64000, 64000], dtype=torch.int32)
    torch.Size([3, 2, 64000])
    """
    x_tensor=[]
    y_tensor=[]
    for i in batch[0][0]:
    	x = lbrosa.load("~.wav", sr)
        x = torch.from_numpy(pad_sequence(x))
        y = torch.from_numpy(np.load("~.npy"))
        x_tensor.append(x)
        y_tensor.append(y)
        
 
    return x_tensor, y_tensor

 

길이가 다른 input을 batch로 만들기 위해 배치를 만들어주는 collate_fn을 조작할 필요가 있다. 새로 정의한 _collate_fn을 아래와 같이 다시 dataloader의 collate_fn에 집어넣어주면 된다.

 

class CustomDataLoader(data.DataLoader):
    def __init__(self, *args, **kwargs):
        super(CustomDataLoader, self).__init__(*args, **kwargs)
        self.collate_fn = _collate_fn

 

 

그리고 새로 _collate_fn함수를 정의했다. 내부 구현은 간략하게 형식만 맞추었고 사용자 마음대로 설정할 수 있다. batch를 input으로 받기 때문에 batch_size =2일 때, [sample1, sample2] 이렇게 list형식으로 input이 들어온다. 내가 구현한 것 처럼 여기서 바로 data를 불러올 경우 dataset에서 파일 경로를 return하도록 코드를 짜면 되고 pad_sequence는 구현을 따로 넣지 않았지만 따로 구현해서 길이를 맞춰주면 된다. 이외에도 다양한 방식으로 output을 만들 수 있다.

 

def _collate_fn(batch):
    
    """
    Args:
        batch: list, len(batch) = 1. See AudioDataset.__getitem__()
    Returns:
        x_tensor : B, ch, T ; tensor
        y_tensor : B, K ; tensor

        
    ex)
    torch.Size([3, 6, 64000])
    torch.Size([3, 20])
    """
    x_tensor=[]
    y_tensor=[]
    for i in batch[0][0]:
    	x = lbrosa.load("~.wav", sr)
        x = torch.from_numpy(pad_sequence(x))
        y = torch.from_numpy(np.load("~.npy"))
        x_tensor.append(x)
        y_tensor.append(y)
        
 
    return x_tensor, y_tensor

Reference

discuss.pytorch.org/t/how-to-create-a-dataloader-with-variable-size-input/8278/8

pytorch.org/docs/stable/data.html#dataloader-collate-fn

towardsdatascience.com/understanding-pytorch-with-an-example-a-step-by-step-tutorial-81fc5f8c4e8e