Naver boostcamp -ai tech/week 02

PyTorch DataSet and DataLoader

끵뀐꿩긘 2022. 9. 30. 06:32

모델을 학습시키기 위해서 데이터를 공급해주는데 관여하는 데이터셋, 샘플러, 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,
                                    )