Dataloader 의 이해
파이토치를 이용한 모형학습에서 데이터의 크기가 큰 경우 한 번에 모든 데이터를 메모리로 올릴 수가 없습니다. 특히 GPU 연산을 하는 경우 GPU 메모리가 매우 제한되어 있어 메모리 관리 문제는 더욱 중요합니다. (80GB H100 GPU의 가격이 2,500만원!) 이 문제를 해결하기 위해 데이터 로더가 필요합니다. 데이터 로더는 메모리로 조금씩 올릴 수 있도록 데이터의 번호(인덱스)를 정해진 개수만큼 순차적으로 불러올 수 있는 기능을 제공합니다. 먼저 우리는 이미지 데이터 연구에 널리 사용된 MNIST (Modified National Institute of Standards and Technology) Dataset 을 이용합니다.
# MNIST 데이터셋 불러오기
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
train_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=transforms.ToTensor(),
download=True)
test_dataset = torchvision.datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor())
# 첫번째 데이터 확인
X, y = train_dataset[0]
X.shape
y
train_dataset[0]는 두 개의 원소로 된 tuple 로 되어 있는데, 첫 번째 원소를 X, 두 번째 원소를 y로 저장하였습니다. 다음 데이터 셋의 크기와 값을 확인합니다.
# 데이터 셋의 크기 확인
print(f'Number of training samples: {len(train_dataset)}')
print(f'Number of test samples: {len(test_dataset)}')
train_dataset의 크기가 6만, test_dataset 1만임을 확인할 수 있습니다. 아래는 이미지 데이터를 시각화하는 함수를 하나 정의하겠습니다.
# 시각화 함수
def imshow(img):
img = img / 2 + 0.5 # Unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
자 그럼 데이터 로더를 실행해봅시다. train_dataset은 길아가 60,000 이며 긱 원소는 튜블로 되어 있습니다. DataLoader는 60,000 의 인덱스에 번호를 매깁니다. shuffle=True 이므로 처음에 모든 데이터를 섞은 후에 batch_size=4 즉, 4개씩 묶어서 번호를 매길 것 입니다. 데이터 로더 객체인 train_loader 는 train_dataset의 개별 데이터를 4개씩 묶어서 순서대로 정렬해놓았다고 할 수 있습니다. 우리는 train_loader 가 가지고 있는 4개 묶음을 순서대로 불러올 수 있습니다. 많이 사용하는 방법이 for 구분, 혹은 반복자를 이용한 호출입니다.
# 데이터 로더
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=4,
shuffle=True)
아래는 반복자를 이용한 호출입니다. dataiter = iter(train_loader)는 train_loader를 반복자로 만들어 dataiter 저장한다는 뜻입니다. next(dataiter)를 실행하면 train_loader의 원소를 하나 씩 불러올 수 있습니다. 첫 번째 images, labels = next(dataiter)를 실행하면 train_loader의 첫 번째 원소가 나옵니다. 다시 images, labels = next(dataiter) 룰 실행하면 두 번째 원소가 나옵니다. 더 이상 원소를 불러올 수 없을 때까지 next() 를 실행할 수 있습니다. images 는 네 장의 사진을 가지고 있음을 확인하세요.
# 반복자
dataiter = iter(train_loader)
images, labels = next(dataiter)
아래 코드를 통해서 이미지 데이터를 시각화 해보겠습니다.
# 이미지와 라벨 시각화
print(' '.join(f'{labels[j].item()}' for j in range(4)))
imshow(torchvision.utils.make_grid(images))
아래 코드를 실행하여 결과를 확인하고 코드를 해석해봅시다.
# 이미지와 라벨 시각화
batchsize = 64
train_loader = torch.utils.data.DataLoader(train_dataset, batchsize)
i = 1
for data, target in train_loader:
print("#### ", i, 'th Batch')
print("data shape:", data.shape)
print("target shape:", target.shape)
i = i+1
if i>5:
break
Dataloader 를 이용한 모형의 적합
우리는 이미지 데이터를 입력값으로 이용하는 신경망 분류모형을 만들고자 합니다. 입력 데이터는 (28,28) 의 크기를 가졌는데 이것을 벡터로 풀어서 적합을 하려고 합니다. 다시 말해 이미지 데이터를 픽셀단위로 풀어서 784차원 (변수가 784개)인 데이터로 만듭니다. 이를 위해 nn.Flatten() 함수를 사용합니다. 아래에 있는 코드를 해석해 봅시다.
# 모형 설정
p = 28*28
h = 64
output_dim = 10
model = nn.Sequential(nn.Flatten(),
nn.Linear(p,h),
nn.ReLU(),
nn.Linear(h, h),
nn.ReLU(),
nn.Linear(h, h),
nn.ReLU(),
nn.Linear(h, output_dim))
learning_rate = 0.005
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
cross_entropy = nn.CrossEntropyLoss()
모형의 구성하였으므로 모형적합의 절차에 따라 코드를 구성합니다. for 문은 이중 반복문으로 구성되어 있는데 안쪽 반복문을 주목하십시오. for data, target in train_loader: 로 되어 있고 이 반복문은 train_loader가 가진 모든 원소를 다 불러올 때까지 실행됩니다. 앞서 train_loader를 설정한 부분을 확인하고 몇 개의 데이터를 묶어서 불어오는지 확인하세요.
train_loader가 가진 모든 원소를 불러와 모형이 적합되는 과정을 epoch 이라 부릅니다. 아래 예제는 몇 번에 epoch 가 반복될까요?
# 모형 적합
num_B = 50
loss_vec = np.zeros(num_B)
model.train()
for i in range(num_B):
val_vec = []
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = cross_entropy(output, target)
loss.backward()
optimizer.step()
predicted = torch.argmax(output.data,1)
v =((predicted == target).sum()).item()/len(target)
val_vec.append(v)
print ("Accuracy: {:.4f}".format(np.array(val_vec).mean()))
이제 GPU 를 이용한 연산을 해보겠습니다. GPU 연산을 위해선 다음을
GPU 메모리로 넘겨줘야 합니다.
- model
- data (tensor): 주로 Dataloader 에서 불러온 작은 배치 단위 데이터들
- loss 함수의 결과
아래 코드에서 to.(device) 로 표시된 부분을 찾아보세요. 여기서 device 는
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 에 의해
GPU 가 사용가능한 경우 'cuda' 그렇지 않은 경우 'cpu' 값이 들어갑니다.
colab 에서 작업을 하는 경우 런타임 유형변경을 클릭하여 GPU
(A100, L4 GPU, T4 GPU, TPU v2 중 하나) 를 선택해보세요
# 학습 후 모델 파라미터
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = nn.Sequential(nn.Flatten(),
nn.Linear(p,h),
nn.ReLU(),
nn.Linear(h, h),
nn.ReLU(),
nn.Linear(h, h),
nn.ReLU(),
nn.Linear(h, output_dim)).to(device)
learning_rate = 0.005
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
cross_entropy = nn.CrossEntropyLoss()
num_B = 50
loss_vec = np.zeros(num_B)
model.train()
for i in range(num_B):
val_vec = []
for data, target in train_loader:
optimizer.zero_grad()
data = data.to(device)
target = target.to(device)
output = model(data)
loss = cross_entropy(output, target).to(device)
loss.backward()
optimizer.step()
predicted = torch.argmax(output.data,1)
v =((predicted == target).sum()).item()/len(target)
val_vec.append(v)
print ("Accuracy: {:.4f}".format(np.array(val_vec).mean()))
커스텀 모형 만들기
신경망 모형은 구조가 복잡해질 경우 레이어를 직렬로 병합하는 nn.Sequential 로 모형을 만드는데 한계가 있습니다. 다양항 형태의 모형을 만들기 위해서 커스텀 모형을 만드는 방법을 알아보고자 합니다. 커스텀 모형은 nn.Module 을 통해 모형을 구성합니다. 모형을 구성하는 요소는 크게 두 가지입니다.
- __init__(self): 함수를 만들때 생성되는 변수와 내부 함수를 설정합니다
- forward: 만는 함수를 실행할 때 작동하는 부분입니다.
# 구현 모형
model = nn.Sequential(nn.Flatten(),
nn.Linear(p,h),
nn.ReLU(),
nn.Linear(h, output_dim))
# 커스텀 모형
class MyNeuralNetwork(nn.Module):
def __init__(self, p,h,output_dim):
super(MyNeuralNetwork, self).__init__()
self.layer1 = nn.Sequential(nn.Flatten(),
nn.Linear(p,h),
nn.ReLU(),
nn.Linear(h, output_dim))
def forward(self, x):
x = self.layer1(x)
return x
p = 28*28
h = 64
output_dim = 10
model = MyNeuralNetwork(p,h,output_dim)
여기서는 model = MyNeuralNetwork(p,h,output_dim) 이 실행되는 순간에 벌어 지는 일을 MyNeuralNetwork class 를 기준으로 설명하겠습니다. model 이 만들어 지는 순간에는 class 안쪽에서 정의한 __init__ 함수가 실행됩니다.
- 먼저 MyNeuralNetwork 는 인자 p = 784, h = 64, output_dim = 10 의 값을 __init__(self, p,h,output_dim)에 전달합니다.
- __init__ 함수가 실행됩니다. 이 함수는 self 에 변수와 내부함수를 생성합니다. 여기서 self 는 "model = MyNeuralNetwork(p,h,output_dim)" 구문에 의해 model 을 가리킵니다.
- model 에 내부함수인 layer1 이 만들어집니다. layer1은 Sequential 함수로 구성된 레이어 함수입니다.
# 커스텀 모형 실생
learning_rate = 0.005
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
cross_entropy = nn.CrossEntropyLoss()
num_B = 50
loss_vec = np.zeros(num_B)
model.train()
for i in range(num_B):
val_vec = []
for data, target in train_loader:
optimizer.zero_grad()
data = data.to(device)
target = target.to(device)
output = model(data)
loss = cross_entropy(output, target).to(device)
loss.backward()
optimizer.step()
predicted = torch.argmax(output.data,1)
v =((predicted == target).sum()).item()/len(target)
val_vec.append(v)
print ("Accuracy: {:.4f}".format(np.array(val_vec).mean()))
model 이 실행되는 곳은 output = model(data) 입니다. data 인자는 forward 함수의 x 전달되어 x = self.layer1(x) 를 실행시키고 결국 return x 로 출력되겠지요? 이상 간단한 커스텀 모형 만들기에 대해 알아보았습니다. 커스텀 모형을 통해 많은 레이어를 __init__ 에 정의하고 forward 에서 정의한 레이어 연산을 합성하여 복잡한 함수를 작성할 수 있습니다. forward 에서 미리 정의한 layer를 호출할 때, self 가 가진 내부함수 즉 self. 을 써 주는것 꼭 기억하세요.