ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [PyTorchZerotoAll 내용정리] 08 DataLoader & Dataset
    ML&DL 이야기/Pytroch 2023. 9. 4. 12:23

    https://tutorials.pytorch.kr/beginner/basics/data_tutorial.html

     

    Dataset과 DataLoader

    파이토치(PyTorch) 기본 익히기|| 빠른 시작|| 텐서(Tensor)|| Dataset과 DataLoader|| 변형(Transform)|| 신경망 모델 구성하기|| Autograd|| 최적화(Optimization)|| 모델 저장하고 불러오기 데이터 샘플을 처리하는 코

    tutorials.pytorch.kr

    Manual..? data feeding-이전 코드

    이전 영상의 코드에서는 위와 같은 방식으로 data를 불러왔다. 

    보면 데이터를 pandas를 이용해 읽고, 이후 각 data를 numpy로 변환하였는데 해당 방식보다 효율적인 방식이 파이토치에는 존재한다. Dataset과 DataLoader이다. 

    영상이 5년전 영상이라 코드가 현재 사용되는 것과 다른 부분이 있어서 pytorch 공식문서를 참고해서 함께 작성하였다. 

    (추가로 코드에 관한 설명&예시코드는 https://honeyjamtech.tistory.com/68 을 다수 참조했다)

    DataSet 

    데이터 샘플을 처리하는 코드는 지저분(messy)하고 유지보수가 어려울 수 있습니다; 더 나은 가독성(readability)과 모듈성(modularity)을 위해 데이터셋 코드를 모델 학습 코드로부터 분리하는 것이 이상적입니다. PyTorch는 
    torch.utils.data.DataLoader와 
    torch.utils.data.Dataset의 두 가지 데이터 기본 요소를 제공하여 미리 준비해둔(pre-loaded) 데이터셋 뿐만 아니라 가지고 있는 데이터를 사용할 수 있도록 합니다. 
    Dataset은 샘플과 정답(label)을 저장하고, 
    DataLoader는 Dataset을 샘플에 쉽게 접근할 수 있도록 순회 가능한 객체(iterable)로 감쌉니다. - pytorch tutorial 중

    위에서 적힌 인용글 처럼, Datset은 위에서 우리가 샘플과 label을 분리했던 것을 더 깔끔하게 할 수 있게 바꾸어준다. 

    아래의 2 종류의 코드를 보자. 

    1. 정의된 Dataset 불러오기 

    import torch
    from torch.utils.data import Dataset
    from torchvision import datasets
    from torchvision.transforms import ToTensor
    import matplotlib.pyplot as plt
    
    
    training_data = datasets.FashionMNIST(
        root="data",
        train=True,
        download=True,
        transform=ToTensor()
    )
    
    test_data = datasets.FashionMNIST(
        root="data",
        train=False,
        download=True,
        transform=ToTensor()
    )

    위 코드는 TorchVision에서 제공하는 Fashion-MNIST dataset을 불러오는 예제로, 파이토치 내에서 제공하는 data를 불러올 때 사용하는 방식이다. 

    root는 저장할 디렉토리 공간, train은 test, trarin 종류 여부, transform은 feature와 label의 변형을 어떻게 할지 지정한다. 

    학습을 하려면 정규화(normalize)된 텐서 형태의 특징(feature)과 원-핫(one-hot)으로 부호화(encode)된 텐서 형태의 정답(label)이 필요하기에 위와 같은 변형이 필요하다. 

    2. Custom Dataset

    import torch
    from torch.utils.data import Dataset, DataLoader
    
    
    class SimpleDataset(Dataset):
        def __init__(self, t):
            self.t = t
    
        def __len__(self):
            return self.t
    
        def __getitem__(self, idx):
            return torch.LongTensor([idx])

    Custom Dataset, 즉, 우리가 직접 data를 다운받아서 사용하는 경우는 위와 같은 코드를 작성해야한다. 

    구조는 아래와 같다. 

    1. __init__(self, 인수들) : dataset을 처음 선언할 때, 즉 object가 선언될 때 자동으로 불리는 함수이다. 인수로는 우리가 원하는 인수를 입력받도록 만들 수 있다 (path, transform 혹은 기타 변수).
    2. __len__(self) : 데이터셋의 샘플의 개수를 반환한다. 만약 dataset을 선언한 뒤  len(a dataset)을 하면 내부적으로는 이 len 함수가 호출된다. len은 이후 DataLoader를 사용할 때 또 내부적으로 사용된다(순회하기 위해)
    3. __getitem__(self, idx) : idx에 해당하는 샘플을 데이터셋에서 불러오고 반환한다(idx = index), 즉, index에 맞추어 데이터를 하나씩 반환한다. 여기서 transform이 이루어진다(pytorch tutorial의 코드 기준)

    이후 해당 Class를 선언해서 사용하면 된다(선언시 __init__에 정의된 인수는 같이 작성해줘야한다)

     

    실제 코드 사용 예시는 아래와 같다(https://honeyjamtech.tistory.com/68 에 적혀있는 코드이다) 

    import glob
    import torch
    from torchvision import transforms
    from PIL import Image
    from torch.utils.data import Dataset, DataLoader
    
    
    class catdogDataset(Dataset):
        def __init__(self, path, train=True, transform=None):
            self.path = path
            if train:
                self.cat_path = path + '/cat/train'
                self.dog_path = path + '/dog/train'
            else:
                self.cat_path = path + '/cat/test'
                self.dog_path = path + '/dog/test'
                
            #find file end with png and make list
            self.cat_img_list = glob.glob(self.cat_path + '/*.png')
            self.dog_img_list = glob.glob(self.dog_path + '/*.png')
    
            self.transform = transform
    
            self.img_list = self.cat_img_list + self.dog_img_list
            #for the label
            self.class_list = [0] * len(self.cat_img_list) + [1] * len(self.dog_img_list) 
        
        def __len__(self):
            return len(self.img_list)
        
        def __getitem__(self, idx):
            img_path = self.img_list[idx]
            label = self.class_list[idx]
            img = Image.open(img_path)
    
            if self.transform is not None:
                img = self.transform(img)
    
            return img, label
    
    if __name__ == "__main__":
        transform = transforms.Compose(
            [
                transforms.ToTensor(),
            ]
        )
    
        dataset = catdogDataset(path='./cat_and_dog', train=True, transform=transform)
        dataloader = DataLoader(dataset=dataset,
                            batch_size=1,
                            shuffle=True,
                            drop_last=False)
    
        for epoch in range(2):
            print(f"epoch : {epoch} ")
            for batch in dataloader:
                img, label = batch
                print(img.size(), label)

    위 코드를 하나씩 보면 다음과 같다. 

    __init__ :

    path, train, transform의 종류를 입력받는다. 

    path와 train에 따라 파일의 경로를 확인하고, glob를 통해 폴더 내의 파일들을 찾는다(특정 확장자로 이루어진 파일을 찾기 위한 것으로 보인다, csv파일의 경우 해당 부분에서 glob 대신)

    추가로 abel을 반환하기 위해서 cat image와 dog image에 대응될 수 있게 cat image 개수만큼 0을, dog image 개수만큼의 1을 가지고 있는 class_list를 만들어 주었다

    __len__:

    img_lst의 개수를 반환해준다 

    __getitiem__:

    init에서 정의된 self를 이용하여 img와 label을 반환한다. 

     

    추가: 

    1. 아래 코드는 __init__하는 시점에서 메모리에 모두 올리는 방식이며 동일한 블로그에서 코드를 가져왔다. 해당 블로그에서 필자는 data하나의 크기가 매우커서(파일의 입출력 시간이 매우 긴 3d데이터라고 한다) init 시점에서 for문을 통해 self_image를 모두 돌면서 Image.open을 미리 해버린다. 

    class catdogDataset(Dataset):
        def __init__(self, path, train=True, transform=None):
            self.path = path
            if train:
                self.cat_path = path + '/cat/train'
                self.dog_path = path + '/dog/train'
            else:
                self.cat_path = path + '/cat/test'
                self.dog_path = path + '/dog/test'
            
            self.cat_img_list = glob.glob(self.cat_path + '/*.png')
            self.dog_img_list = glob.glob(self.dog_path + '/*.png')
    
            self.transform = transform
    
            self.img_list = self.cat_img_list + self.dog_img_list
    
            self.Image_list = []  # 바뀐부분!!!!!!!
            for img_path in self.img_list:  # 바뀐부분!!!!!!!
                self.Image_list.append(Image.open(img_path))  # 바뀐부분!!!!!!!
    
            self.class_list = [0] * len(self.cat_img_list) + [1] * len(self.dog_img_list) 
        
        def __len__(self):
            return len(self.img_list)
        
        def __getitem__(self, idx):
            img = self.Image_list[idx]  # 바뀐부분!!!!!!!
            label = self.class_list[idx]
    
            if self.transform is not None:
                img = self.transform(img)
    
            return img, label

     

     

    2. 아래 코드는 MovieLensDataset의 코드를 내가 작성한 것이다. 위 코드와 비교해서 보면 좋을 것 같아 추가로 올렸다. 

    csv파일 및 여러 변인들을 담는 부분을 보면 좋을 것 같다. 

    class MovieLensDataset(Dataset):
        #preporcessing
        """
        MovieLens dataset loading for CML
        """
        def __init__(self, data_file):
            self.data = pd.read_csv(data_file, sep='\t', header=None)
            # self.data.columns = ["user_id", "item_id", "rating", "timestamp"]
            #self.data = self.data.groupby(0).filter(lambda x: (x[2] == 1).sum() >= 10)        
            self.user = self.data[0]
            self.item = self.data[1]
            self.rating = self.data[2]
            self.timestamp = self.data[3]
    
        def __len__(self):
            return len(self.data)
    
        def __getitem__(self, idx):
            user = self.user[idx]
            item = self.item[idx]
            rating = self.rating[idx]
            timestamp = self.timestamp[idx]
    
            return user, item, rating, timestamp
    추가적인 고려사항들
    1.  데이터셋 getitem 에서 numpy array를 return해도 Dataloader를 사용하면 배치를 만드는 과정에서 자동으로 torch. tensor로 바꾼다. 0번째 차원을 추가하고 batch_size 개수의 item을 concatenate 하는 것도 추가된다고 적혀있다 -> colab을 통해 확인한 결과 + pytorch 공식문서를 읽어본 결과 해당 자료를 찾지 못했다, 혹은 글을 내가 잘못 이해한 거일 수도 있다(최종 return의 형태에 관해서만 말한 거 일수도?). 결국은 __getitem__에서 torch.tensor로 변환하는 과정이 필요할 것 같다(아래의 code implement 참고)

    2. glob을 사용하는 경우 파일 순서가 이상할 수 있다. img와 segmentation map img 와 같이 같이 pair 가 되어야하는데 각각이 다른 path에 있고 이름이 다른 경우에 glob의 output이 0.png, 1.png, 2.png 이 순서가 아니라서 img 랑 segmentation map 이 다른 순서로 sorting이 되어 있을 수 있다. 이 때 단순히 getitem에서 index 기준으로 그냥 이미지랑 map을 뽑는 식으로 짜면 학습할 때 img와 segmentation map이 페어가 맞지 않을 수도 있다고 한다.
    3. getitem에서 그냥 여러 데이터(이미지, label, etc.)를 쉼표로 리턴하는 게 아니라 이 데이터들을 딕셔너리로 리턴할 수도 있을 텐데 (return {'img' : self.img_list[idx], 'label' : self.class_list[idx]}), 이렇게 만들면 데이터로더가 나중에 합칠 때도 이걸 고려하여 데이터로더에서 나오는 batch가 {'img' : 뭐시기, 'label' : 뭐시기} 이렇게 나온다
    4. torchvision transforms 에서 Resize(128)로 해서 하면 이미지가 128, 128이 되는 것이 아니다. 만약 [128, 256] 짜리 이미지가 있고 Resize(128)로 transform을 만들어서 통과시켜도 [128, 256]이다. torchvision의 Resize가 비율을 유지해서 그렇다. 이는 여러 비율을 가지는 이미지들이 존재하면 위에서 잠깐 말한대로 이미지들의 크기가 달라서 concat이 안되어서 데이터로더에서 에러가 뜬다. 그러니까 Resize와 CenterCrop 같은 걸 함께 사용하자.

    DataLoader 

    Dataset은 데이터셋의 특징을 가져오고 하나의 샘플에 정답(label)을 지정한다. 모델을 학습할 때, 일반적으로 샘플들을 minibatch로 전달하고, 매 epoch마다 데이터를 다시 shuffle하여 overfit을 맞고, 데이터 처리 속도를 높이기도 한다. 

    from torch.utils.data import DataLoader
    
    train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
    test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
    for batch_idx, data in enumerate(self.data_loader):
                user = data[0].long().to(self.device)
                item = data[1].long().to(self.device)
                target = data[2].float().to(self.device)
                timestamp = data[3].long().to(self.device)

    Code Implementation

    https://colab.research.google.com/drive/1QB72fSRQTZihbzbp8qf7cf-jRWZFy4Ax#scrollTo=xWyz9OOPbsvJ

     

    Google Colaboratory Notebook

    Run, share, and edit Python notebooks

    colab.research.google.com

    class SimpleDataset(Dataset):
        def __init__(self, path):
            self.data = pd.read_csv(path, delimiter=',')
            self.x_data = torch.tensor(self.data.iloc[:, 0:-1].values, dtype=torch.float32)
            self.y_data = torch.tensor(self.data.iloc[:, [-1]].values, dtype=torch.float32)
    
        def __len__(self):
            return len(self.data)
    
        def __getitem__(self, idx):
            x = self.x_data[idx]
            y = self.y_data[idx]
            return x, y
    for epoch in range(10):
      for idx, data in enumerate(dataLoader):
        input, label = data
        output = model(input)
        loss = BCE_loss(output, label)
        print(f'Epoch: {epoch+1}/100,| Loss: {loss.item():.4f}')
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    ---

    코드를 약간 수정해서 getitem에서 tensor 변환을 하는 것으로 바꿨다

    class SimpleDataset(Dataset):
        def __init__(self, path):
            self.data = pd.read_csv(path, delimiter=',')
            self.x_data = self.data.iloc[:, 0:-1]
            self.y_data = self.data.iloc[:, [-1]]
    
        def __len__(self):
            return len(self.data)
    
        def __getitem__(self, idx):
            x = torch.tensor(self.x_data.iloc[idx].values, dtype=torch.float32)
            y = torch.tensor(self.y_data.iloc[idx].values, dtype=torch.float32)  
            return x, y
Designed by Tistory.