레이블이 dataloader인 게시물을 표시합니다. 모든 게시물 표시
레이블이 dataloader인 게시물을 표시합니다. 모든 게시물 표시

2022년 7월 23일 토요일

pytorch 파일 기반(file based) DataLoader

시작하기전

앞서서 Pytorch 에서 Data를 읽어 오기 위한 DataLoader에 대해서 알아보았습니다. 이번글을 읽기전에 기본적으로 알아야 하는 내용입니다.

https://swlock.blogspot.com/2022/07/pytorch-dataset-dataloader.html

지난 posting의 dataloader는 반쪽짜리 입니다. dataloader는 결국 한번에 소모하는 RAM소비를 줄이기 위한 방법인데 data전체를 memory에 올리고 있는 상황이 되어 버립니다.

그래서 여기에서는 file 기반으로 변경해서 필요한 부분만 메모리에 올리도록 수정하였습니다.


numpy로 데이터 생성

이 부분은 numpy의 savetxt 를 이용합니다.

import numpy as np

####################################### 데이터 준비
np.random.seed(42)
# np.random.rand(m,n) n*m matrix 로 0~1 랜덤 생성
x = np.random.rand(100, 3)
'''
[[0.37454012 0.95071431 0.73199394]
 [0.59865848 0.15601864 0.15599452]
 [0.05808361 0.86617615 0.60111501]
 ...
'''
x_one_d = x[:,0:1] * x[:,1:2] * x[:,2:3]
'''
[[2.60648878e-01]  # <= [0.37454012 * 0.95071431 * 0.73199394]
 [1.45701819e-02]
 [3.02424805e-02]
 ...
'''

# y는 원하는 값 목표값인 label로 표현합니다.
y = 1 + 2 * x_one_d + .1 * np.random.randn(100, 1)
'''
[[1.52585494]
 [0.96398033]
 [1.27487937]
 ...
'''
# 저장전 확인
print(x.shape,type(x),x[0])
print(y.shape,type(y),y[0])

# 저장
np.savetxt("x_data.csv", x, delimiter=',')
np.savetxt("y_data.csv", y, delimiter=',')

# 저장된것 확인
x_data = np.loadtxt("x_data.csv", delimiter=',', skiprows=0, max_rows=1)
print(x_data.shape,type(x_data),x_data)
if x_data.shape==():
    x_data = np.array([x_data]) 
print(x_data.shape,type(x_data),x_data)
y_data = np.loadtxt("y_data.csv", delimiter=',', skiprows=0, max_rows=1)
print(y_data.shape,type(y_data),y_data)
if y_data.shape==():
    y_data = np.array([y_data]) 
print(y_data.shape,type(y_data),y_data)

실행 결과

(100, 3) <class 'numpy.ndarray'> [0.37454012 0.95071431 0.73199394]
(100, 1) <class 'numpy.ndarray'> [1.52585494]
(3,) <class 'numpy.ndarray'> [0.37454012 0.95071431 0.73199394]
(3,) <class 'numpy.ndarray'> [0.37454012 0.95071431 0.73199394]
() <class 'numpy.ndarray'> 1.5258549401766552
(1,) <class 'numpy.ndarray'> [1.52585494]


x_data.csv 생성된 파일 정보

3.745401188473624909e-01,9.507143064099161656e-01,7.319939418114050911e-01
5.986584841970366000e-01,1.560186404424365181e-01,1.559945203362026467e-01
5.808361216819946105e-02,8.661761457749351800e-01,6.011150117432088047e-01
7.080725777960454881e-01,2.058449429580244683e-02,9.699098521619943236e-01
8.324426408004217404e-01,2.123391106782761550e-01,1.818249672071006184e-01
1.834045098534338170e-01,3.042422429595377231e-01,5.247564316322378408e-01
4.319450186421157634e-01,2.912291401980419137e-01,6.118528947223794701e-01
1.394938606520418345e-01,2.921446485352181544e-01,3.663618432936917024e-01
4.560699842170359286e-01,7.851759613930135995e-01,1.996737821583597361e-01
5.142344384136116053e-01,5.924145688620424677e-01,4.645041271999772459e-02
6.075448519014383653e-01,1.705241236872915289e-01,6.505159298527951606e-02
9.488855372533332444e-01,9.656320330745593594e-01,8.083973481164611341e-01
3.046137691733706854e-01,9.767211400638386998e-02,6.842330265121568944e-01
....


y_data.csv 생성된 파일 정보

1.525854940176655239e+00
9.639803290492371390e-01
1.274879370024478487e+00
1.091665387286355093e+00
8.617645510296900735e-01
1.077207618958690771e+00
1.087757394417082413e+00
1.115103571175657615e+00
1.063752367405901955e+00
1.016827660818809154e+00
1.063977555024557597e+00
2.568002900244755082e+00
....


File DataLoader

전체 dataset의 크기를 알아야 합니다. 파일의 line수를 구하는 것과 같은데 잘못하면 파일의 전체 메모리를 올리게 됩니다. 이 부분은 아래와 같은 코드를 이용하면 일부 RAM을 시용하면서도 전체 라인 수를 구할 수 있습니다.

파일의 전체 라인 수 구하기

        with open(filename, 'r') as fp:
            for count, line in enumerate(fp):
                pass
        total_len = count + 1


전체 소스

나머지는 그렇게 어려운 부분은 없습니다.

train_dataset, val_dataset = random_split(dataset, [80, 20]) 부분이 보이는데 80,20은 전체 dataset에서 분해하기 위한 수량입니다. 입력 데이터가 100개라서 이와 같이 표현한 것이며 수량 합계가 다르다면 적절히 수정하도록 합니다.

import torch
import numpy as np
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split


class CustomDataset(Dataset):
    def __init__(self, x_tensor_filename, y_tensor_filename):
        self.x_fn = x_tensor_filename
        self.y_fn = y_tensor_filename
        with open(y_tensor_filename, 'r') as fp:
            for count, line in enumerate(fp):
                pass
        self.total_len = count + 1
    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        x_data = np.loadtxt(self.x_fn, delimiter=',', skiprows=index, max_rows=1)
        # [0.37454012 0.95071431 0.73199394]
        if x_data.shape==():
            x_data = np.array([x_data]) 
        y_data = np.loadtxt(self.y_fn, delimiter=',', skiprows=index, max_rows=1)
        # 1.5258549401766552
        if y_data.shape==():
            y_data = np.array([y_data]) 
        # [1.5258549401766552]
        return (x_data, y_data)
    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return self.total_len
        
dataset = CustomDataset("x_data.csv", "y_data.csv")

train_dataset, val_dataset = random_split(dataset, [80, 20])

train_loader = DataLoader(dataset=train_dataset, batch_size=9)
val_loader   = DataLoader(dataset=val_dataset, batch_size=9) 

n_epochs = 10

device = 'cuda' if torch.cuda.is_available() else 'cpu'

for epoch in range(n_epochs):
    print(f"epoch ##{epoch}")
    batch_no = 0
    for x_batch, y_batch in train_loader:
        print(f"train batch #{batch_no}, epoch #{epoch}")
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)
        print(x_batch,"\n",y_batch)
        batch_no += 1
    
    batch_no = 0    
    for x_val, y_val in val_loader:
        print(f"val batch #{batch_no}, epoch #{epoch}")
        x_val = x_val.to(device)
        y_val = y_val.to(device)
        print(x_val,"\n",y_val)
        batch_no += 1


'''
batch 의 값이 전체 dataset의 배수가 아니며 마지막 batch로 들어오는 크기는 줄어들 수 있음
epoch ##0
train batch #0, epoch #0
tensor([[0.3492, 0.7260, 0.8971],
        [0.8074, 0.8961, 0.3180],
        [0.3745, 0.9507, 0.7320],
        [0.1271, 0.5222, 0.7700],
        [0.2848, 0.0369, 0.6096],
        [0.9132, 0.5113, 0.5015],
        [0.4895, 0.9857, 0.2421],
        [0.0408, 0.5909, 0.6776],
        [0.0359, 0.4656, 0.5426]], device='cuda:0', dtype=torch.float64) 
 tensor([[1.4856],
        [1.5765],
        [1.5259],
        [1.1010],
        [1.0242],
        [1.6776],
        [1.1098],
        [1.0607],
        [1.0973]], device='cuda:0', dtype=torch.float64)
train batch #1, epoch #0

......

'''


파일 방식 구현과 기존 코드의 비교

기존 결과물과 비교해보면 차이가 없다는 것을 알 수 있습니다.


기존 방식 링크

2022년 7월 16일 토요일

pytorch Dataset과 DataLoader

DataLoader를 왜, 언제 사용하게 될까?

일반적인 예제 수준의 수량이 작은 data의 경우 안써도 무방합니다. 하지만 데이터 크기가 많은 경우 모두 메모리에 올릴 수 없는 상황이 벌어지게 됩니다.

DataLoader는 훈련에 필요한 데이터가 큰 경우 batch size 만큼 데이터를 가져와서 훈련 시키기 위해 사용합니다.

그리고 DataLoader를 사용하기 위해선 Dataset Class에 대해서 알아야 합니다.

- 검정/검증 : validation , test, val 모두 같은 의미로 사용합니다.

- 훈련 : train

- y : 목표값 label 로 표현하기도 합니다.

dataset에 있어서 epoch, batch_size 의 기본은 아래 그림을 참고 바랍니다.


앞서 DataLoader를 사용하기 위해서는 Dataset에 대해서 알아야 한다고 했었습니다.

Dataset 과 DataLoader class 모두 torch.utils.data 패키지 내에 있습니다.


from torch.utils.data import Dataset

from torch.utils.data import DataLoader

Dataset

Dataset을 사용하기 위해서는 해당 class를 상속해서 기본적으로 3가지 정도 함수 작업을 해줍니다.

class CustomDataset(Dataset):
    def __init__(self, x_tensor, y_tensor):
        self.x = x_tensor
        self.y = y_tensor
        
    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        return (self.x[index], self.y[index])

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return len(self.x)

__init__() 는 생성될때 호출되는 함수 입니다. 여기에서는 tensor 데이터를 모두 x, y에 넣는 구조이지만 결국 x,y 모두 메모리에 넣는 상태라 이렇게 해버리면 사실 dataset 을 사용하는 이유가 사라집니다. 여기에서는 예제로서 이렇게 사용한 것입니다.

__get_item__(self, index): 데이터셋을 인덱싱 할 수 있으므로 목록( dataset[i]) 처럼 작동할 수 있습니다 . 요청된 데이터 포인트에 해당 하는 튜플(입력, 레이블)을 리턴 해야 합니다.

__len__(self): 전체 데이터 세트의 크기를 반환합니다.


여기 이미지 데이터의 훌륭한 예제가 있습니다. 그러나 이해하기가 어려울 수 있습니다.

https://pytorch.org/tutorials/beginner/data_loading_tutorial.html#dataset-class

설명에서 y는 label과 같은 의미입니다.


Dataset 분리하기

전체 dataset에 대해서 train용과 valid(검정,검증)용 같은 데이터로 분리하는 경우가 있습니다. 대부분 많이들 분리해서 사용합니다.

이때 random_split를 사용하고 몇개를 train 용으로 사용할지 valid용으로 사용할지 인자로 넘겨줍니다. (아무래도 random이 좋습니다.)

아래와 같은 샘플 코드로 사용이 가능합니다.

from torch.utils.data.dataset import random_split

train_dataset, val_dataset = random_split(dataset, [80, 20])


DataLoader

이제 DataLoader를 사용할 차례입니다. DataLoader는 dataset에서 일정 조각(batch)크기 만큼 로딩할 수 있는 기능입니다.

여기에서는 임의로 batch_size = 9로 넣었습니다. 이 정도 하면 모든 준비가 끝났고 훈련할 준비가 완료되었습니다.

train_loader = DataLoader(dataset=train_dataset, batch_size=9)
val_loader   = DataLoader(dataset=val_dataset, batch_size=9) 

훈련단에서는 어떻게 사용해야할까요? 

for 문에서 간단하게 이 정도로 사용이 가능합니다.

for epoch in range(n_epochs):
    for x_batch, y_batch in train_loader:
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)
        ## model train


전체 예제

import torch
import numpy as np
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split

####################################### 데이터 준비
np.random.seed(42)
# np.random.rand(m,n) n*m matrix 로 0~1 랜덤 생성
x = np.random.rand(100, 3)
'''
[[0.37454012 0.95071431 0.73199394]
 [0.59865848 0.15601864 0.15599452]
 [0.05808361 0.86617615 0.60111501]
 ...
'''
x_one_d = x[:,0:1] * x[:,1:2] * x[:,2:3]
'''
[[2.60648878e-01]  # <= [0.37454012 * 0.95071431 * 0.73199394]
 [1.45701819e-02]
 [3.02424805e-02]
 ...
'''

# y는 원하는 값 목표값인 label로 표현합니다.
y = 1 + 2 * x_one_d + .1 * np.random.randn(100, 1)
'''
[[1.52585494]
 [0.96398033]
 [1.27487937]
 ...
'''
################################################

x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
'''
x_tensor :
tensor([[0.3745, 0.9507, 0.7320],
        [0.5987, 0.1560, 0.1560],
        [0.0581, 0.8662, 0.6011],
        ...
y_tensor:
tensor([[1.5259],
        [0.9640],
        [1.2749],
        ...
'''

class CustomDataset(Dataset):
    def __init__(self, x_tensor, y_tensor):
        self.x = x_tensor
        self.y = y_tensor
        
    def __getitem__(self, index):
        """
        주어진 인덱스 index 에 해당하는 샘플을 데이터셋에서 불러오고 반환합니다.
        """
        return (self.x[index], self.y[index])

    def __len__(self):
        """
        데이터셋의 샘플 개수를 반환합니다.
        """
        return len(self.x)
        
dataset = CustomDataset(x_tensor, y_tensor)

train_dataset, val_dataset = random_split(dataset, [80, 20])

train_loader = DataLoader(dataset=train_dataset, batch_size=9)
val_loader   = DataLoader(dataset=val_dataset, batch_size=9) 

n_epochs = 10

device = 'cuda' if torch.cuda.is_available() else 'cpu'

for epoch in range(n_epochs):
    print(f"epoch ##{epoch}")
    batch_no = 0
    for x_batch, y_batch in train_loader:
        print(f"train batch #{batch_no}, epoch #{epoch}")
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)
        print(x_batch,"\n",y_batch)
        batch_no += 1
    
    batch_no = 0    
    for x_val, y_val in val_loader:
        print(f"val batch #{batch_no}, epoch #{epoch}")
        x_val = x_val.to(device)
        y_val = y_val.to(device)
        print(x_val,"\n",y_val)
        batch_no += 1


'''
batch 의 값이 전체 dataset의 배수가 아니며 마지막 batch로 들어오는 크기는 줄어들 수 있음
epoch ##0
train batch #0, epoch #0
tensor([[0.3636, 0.9718, 0.9624],
        [0.5701, 0.0972, 0.6150],
        [0.7025, 0.3595, 0.2936],
        [0.3745, 0.9507, 0.7320],
        [0.3411, 0.1135, 0.9247],
        [0.8074, 0.8961, 0.3180],
        [0.2440, 0.9730, 0.3931],
        [0.0937, 0.3677, 0.2652],
        [0.2865, 0.5908, 0.0305]], device='cuda:0') 
 tensor([[1.6872],
        [1.1006],
        [1.2078],
        [1.5259],
        [1.0126],
        [1.5765],
        [1.1275],
        [1.1460],
        [1.0727]], device='cuda:0')
train batch #1, epoch #0

......

train batch #8, epoch #0
tensor([[0.5467, 0.1849, 0.9696],
        [0.5568, 0.9362, 0.6960],
        [0.7751, 0.9395, 0.8948],
        [0.8353, 0.3208, 0.1865],
        [0.1834, 0.3042, 0.5248],
        [0.3887, 0.2713, 0.8287],
        [0.9083, 0.2396, 0.1449],
        [0.6452, 0.1744, 0.6909]], device='cuda:0') 
 tensor([[1.3725],
        [1.6946],
        [2.3438],
        [1.1588],
        [1.0772],
        [1.3870],
        [1.2217],
        [1.1346]], device='cuda:0')
val batch #0, epoch #0
tensor([[0.5487, 0.6919, 0.6520],
        [0.6335, 0.5358, 0.0903],
        [0.1866, 0.8926, 0.5393],
        [0.2419, 0.0931, 0.8972],
        [0.3867, 0.9367, 0.1375],
        [0.5107, 0.4174, 0.2221],
        [0.7958, 0.8900, 0.3380],
        [0.6376, 0.8872, 0.4722],
        [0.8180, 0.8607, 0.0070]], device='cuda:0') 
 tensor([[1.5698],
        [1.0461],
        [1.0360],
        [0.9711],
        [1.0503],
        [1.1409],
        [1.3574],
        [1.5283],
        [0.9116]], device='cuda:0')
......
        
val batch #1, epoch #9
tensor([[0.3702, 0.0155, 0.9283],
        [0.2848, 0.0369, 0.6096],
        [0.8324, 0.2123, 0.1818],
        [0.4319, 0.2912, 0.6119],
        [0.5026, 0.5769, 0.4925],
        [0.0243, 0.6455, 0.1771],
        [0.8774, 0.7408, 0.6970],
        [0.6233, 0.3309, 0.0636],
        [0.7081, 0.0206, 0.9699]], device='cuda:0') 
 tensor([[1.0920],
        [1.0242],
        [0.8618],
        [1.0878],
        [1.2654],
        [1.1154],
        [1.9157],
        [1.1037],
        [1.0917]], device='cuda:0')
val batch #2, epoch #9
tensor([[0.5613, 0.7710, 0.4938],
        [0.8172, 0.5552, 0.5297]], device='cuda:0') 
 tensor([[1.3249],
        [1.5163]], device='cuda:0')

'''

위 예제에서 model train 관련 코드는 빠져있습니다. 훈련시 데이터가 몇개나 어떤 형태로 올라오는지에 대한 예제입니다. batch, epoch 관계를 보다 이해하기 쉬울것이라 생각됩니다.


Pytorch 링크

pytorch Dataset과 DataLoader

pytorch 파일 기반(file based) DataLoader

Machine Learning(머신러닝) 기초, Pytorch train 기본

Machine Learning(머신러닝) 기초, Pytorch 영상 분류, 영상 불량 검출, 개 고양이 분류 (image classification)