')">

인공지능 모형 만들기

Custom Model: 맞춤형 인공지능 모형 만들기

Posted by Jong-June Jeon on June 25, 2024

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: 만는 함수를 실행할 때 작동하는 부분입니다.
우리는 다음 Sequential 로 만들어진 모형을 커스텀 모형으로 구현해볼 것입니다.

# 구현 모형
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__ 함수가 실행됩니다.

  1. 먼저 MyNeuralNetwork 는 인자 p = 784, h = 64, output_dim = 10 의 값을 __init__(self, p,h,output_dim)에 전달합니다.
  2. __init__ 함수가 실행됩니다. 이 함수는 self 에 변수와 내부함수를 생성합니다. 여기서 self 는 "model = MyNeuralNetwork(p,h,output_dim)" 구문에 의해 model 을 가리킵니다.
  3. model 에 내부함수인 layer1 이 만들어집니다. layer1은 Sequential 함수로 구성된 레이어 함수입니다.
여기서 한가지 알아야 하는 것은 forward 함수는 현재 실행되지 않는 다는 것입니다. 그러면 forward 함수는 언제 실행될까요? forward 함수는 model이 입력값을 받아 실행되는 순간에 작동합니다. 아래 코드에서 model 이 실행되는 곳을 찾아보세요.

# 커스텀 모형 실생
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. 을 써 주는것 꼭 기억하세요.