↓ 본문으로 건너뛰기

[PyTorch] 04. validation phase를 추가해 train_model() 함수 작성하기

·4 분
nyan101
작성자
nyan101
목차
PyTorch - 이 글은 시리즈의 일부입니다.
[PyTorch] 04. validation phase를 추가해 train_model() 함수 작성하기

굉장히 오랜만에 이어지는 포스팅이다… 핑거스냅에 당해서 블립(blip) 당함

모델 학습에서의 Training & Validation

이전 글에서 작성했던 마지막 코드를 다시 살펴보자. 이전 글에서는 아래 5가지 단계를 거쳤다.

  1. nn.Module을 상속받은 모델 클래스 작성
  2. torchvision.datasets 라이브러리에서 유명 데이터셋(MNIST) 다운로드
  3. torch.utils.data.Dataloader 사용법
  4. 모델의 1 epoch 학습 진행
  5. 모델의 test 성능 측정

각 단계별로 주요 로직을 정리해 하나의 코드로 합친 결과는 다음과 같다.

# 모델 생성 및 설정
net = MyModel()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters())

# 데이터 로드
train_data = torchvision.datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor())
test_data = torchvision.datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())

# dataloader에 적재
train_loader = torch.utils.data.DataLoader(train_data, shuffle=True, batch_size=50)
test_loader = torch.utils.data.DataLoader(test_data)

# 학습(training) 진행
st = time.time()
print(f"training with {len(train_data)} data... ", end='')
for epoch in range(1):
    for x,y in train_loader:
        y_pred = net(x)
        loss = criterion(y_pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
print(f"done (elapsed {time.time() - st}s)")

# 검증(test) 진행
with torch.no_grad():
    acc, tot = 0, 0
    for x, y in test_loader:
        y_pred = net(x)
        acc += (y==y_pred.argmax(1)).sum()
        tot += len(y)
    print(f"test accuracy : {acc}/{tot} ({100*acc/tot}%)")

모델의 학습과정을 보면 최종 검증 직전까지도 아무런 중간점검 없이 전체 epoch의 학습이 진행되는 것을 볼 수 있다. 그러나 모델을 본격적으로 학습시키기 위해서는 학습 과정에서 training loss를 계산하거나 주기적으로 validation을 진행하는 등, 학습의 진행도를 추적하기 위해 다양한 절차를 추가하는 경우가 많다. 이번 글에서는 학습 과정에 다음 두 작업을 추가해보자.

  • validation phase 추가
  • train/val 단계마다 평균 손실(loss와)과 평균 정확도(acc) 계산

dataloaders_dict 작성

이전에는 train, test용 dataloader를 각각 따로 만들어 관리했다. 그러나 복잡한 코드에서는 관리의 편의성을 위해 둘을 dict로 모아 관리하는 방법이 자주 사용된다.1 다음 작업을 수행하자.

  • train_data에서 일정 비율(80%)을 분리해 train용으로, 나머지를 val용으로 나눈다
  • 각 데이터셋을 data.Dataloader를 이용해 dataloader로 만든다
  • 두 dataloader를 적절한 키(’train’, ‘val’)와 함께 dictionary에 등록한다

이를 코드로 작성하면 다음과 같다.

import torch.utils.data as data
import torchvision
from torchvision import transforms

# 본 포스팅에서는 RGB채널이 모두 있는 CIFAR10 데이터셋을 사용한다
train_data = torchvision.datasets.CIFAR10(
  root='./data', download=True, train=True, transform=transforms.ToTensor()
)

train_size = int(len(train_data)*0.8) # 전체의 80%를 학습용으로 사용
val_size = len(train_data) - train_size
train_dataset, val_dataset = data.dataset.random_split(train_data, [train_size, val_size])

dataloaders_dict = {
  'train' : data.DataLoader(train_dataset, shuffle=True, batch_size=50),
  'val' : data.DataLoader(val_dataset, batch_size=50)
}

이제 dataloaders_dict['train'], dataloaders_dict['val']를 통해 각 phase용 dataloader에 접근할 수 있다.

train_model() 함수 작성

이제 validation phase를 추가해 train_model() 함수를 작성하자. 수행할 작업은 다음과 같다

  • 사용 가능한 device를 인식하고 모델을 변환

  • 각 epoch마다 train, val 과정 수행

    • phase(’train’, ‘val’)에 따라 model.train(), model.eval() 모드 변경
    • train phase에서는 파라미터 업데이트를 위해 set_grad_enabled 활성화
    • val phase에서는 불필요한 계산 방지를 위해 set_grad_enabled 비활성화
  • 각 epoch, phase마다 loss, acc 출력

이를 코드로 작성하면 다음과 같다.

def train_model(net, criterion, optimizer, dataloaders_dict, num_epochs):
    # 사용 가능한 device 인식(GPU가 있으면 GPU 사용)
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    net.to(device)
    print(f"Using device: {device}")
    
    # train & val 전체 진행루틴
    for epoch in range(num_epochs):
        print("-----------------")
        print(f"> Epoch {epoch+1}/{num_epochs}")
        # 각 epoch마다 'train', 'val'을 모두 진행
        for phase in ['train', 'val']:
            # 현재 phase에 따라 모델의 상태 변경
            torch.set_grad_enabled(phase == 'train')
            if phase == 'train':
                net.train()
            else:
                net.eval()
            # 첫 epoch에서는 validation만 수행(초기 상태의 성능 측정 목적)
            if (epoch == 0) and (phase == 'train'):
                continue
            # 매 epoch마다 loss(손실), correct(맞은 개수)를 기록
            epoch_loss = 0.0
            epoch_corrects = 0
            # tqdm : 진행바(progress bar)를 표시하기 위한 라이브러리
            for x,y in tqdm(dataloaders_dict[phase]):
                x, y = x.to(device), y.to(device)
                output = net(x)
                loss = criterion(output, y)
                # train phase일 때만 loss값을 기반으로 파라미터 갱신
                if phase == 'train':
                    optimizer.zero_grad()
                    loss.backward()
                    optimizer.step()
                # loss, correct 계산
                _, y_pred = torch.max(output, 1)
                epoch_loss += loss.item() * x.size(0) # size(0): 해당 batch의 size
                epoch_corrects += torch.sum(y_pred == y.data)
            # epoch이 끝난 후 해당 epoch에서의 평균 손실, 정확도 계산 및 출력
            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
            epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
            print(f"{phase} Loss: {epoch_loss:.4f}  Acc: {epoch_acc:.4f} ({epoch_corrects}/{len(dataloaders_dict[phase].dataset)})")

모델 학습 진행

앞서 작성한 함수들을 이용해 모델 학습을 진행해보자. 편의상 별도의 모델 클래스를 만드는 대신 torchvision.models의 resnet18을 사용했다.

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision
from torchvision import models, transforms

"""
앞서 작성한 dataloaders_dict, train_model 코드 생략
"""

# resnet18 모델에서 마지막 output layer만 변경한다(총 10개의 label)
net = models.resnet18()
net.fc = nn.Linear(in_features=net.fc.in_features, out_features=10)

# criterion, optimizer 설정
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(params = net.parameters())

# 학습 진행
train_model(net, criterion, optimizer, dataloaders_dict, num_epochs=5)

이를 실행하면 다음과 같은 출력을 볼 수 있다. 함께 출력되는 진행바는tqdm의 효과이다.

Using device: cuda
-----------------
> Epoch 1/5
100%|██████████| 200/200 [00:05<00:00, 34.14it/s]
val Loss: 2.3033  Acc: 0.1043 (1043/10000)
-----------------
> Epoch 2/5
100%|██████████| 800/800 [00:59<00:00, 13.52it/s]
train Loss: 1.4675  Acc: 0.4736 (18944/40000)
100%|██████████| 200/200 [00:04<00:00, 42.41it/s]
val Loss: 1.2823  Acc: 0.5465 (5465/10000)
-----------------
> Epoch 3/5
100%|██████████| 800/800 [00:59<00:00, 13.53it/s]
train Loss: 1.0625  Acc: 0.6243 (24970/40000)
100%|██████████| 200/200 [00:04<00:00, 42.35it/s]
val Loss: 1.1661  Acc: 0.5980 (5980/10000)
-----------------
> Epoch 4/5
100%|██████████| 800/800 [00:59<00:00, 13.52it/s]
train Loss: 0.8876  Acc: 0.6887 (27547/40000)
100%|██████████| 200/200 [00:04<00:00, 42.33it/s]
val Loss: 1.0737  Acc: 0.6208 (6208/10000)
-----------------
> Epoch 5/5
100%|██████████| 800/800 [00:59<00:00, 13.50it/s]
train Loss: 0.7564  Acc: 0.7349 (29394/40000)
100%|██████████| 200/200 [00:04<00:00, 42.28it/s]
val Loss: 0.9722  Acc: 0.6625 (6625/10000)


  1. 이는 이후 다른 글에서 다룰 torchvision.transforms의 활용에 있어서도 동일하다. ↩︎

PyTorch - 이 글은 시리즈의 일부입니다.
[PyTorch] 04. validation phase를 추가해 train_model() 함수 작성하기

관련 글

[PyTorch] 03. torch.nn 모듈 조립으로 CNN 만들기

·7 분
예제 dataset 가져오기 지금까지는 \(y=ax+b\)나 \(y=ax^2+bx+c\) 처럼 간단한 형태의 숫자→숫자 함수만을 모델링했으니 이제 좀더 흥미로운 데이터를 살펴보자. 딥러닝, 특히 이미지 인식 관련 예제를 보면 MNIST나 CIFAR-10같은 이름이 많이 등장한다. 이 둘은 Image Classification 문제의 대표적인 예시로 각각 손글씨와 물체 사진 이미지셋에 해당하며, 흔히 모델의 벤치마크 용도로 활용되고 있다. 이번 글에서는 좀더 단순한 MNIST를 사용하기로 한다. MNIST 데이터는 아래와 같이 색이 없는 1채널(=gray scale) 28x28픽셀 이미지들의 모음으로 0에서 9까지 총 10개의 라벨을 가지고 있다.

[PyTorch] 02. nn.Module로 모델 클래스 만들기

·5 분
학습의 각 단계를 추상화하기 앞서 그래디언트를 이용해 간단한 학습이 이루어지는 과정을 알아보았다. 이전 글의 마지막 코드를 다시 살펴보면 학습의 한 round를 크게 다음과 같은 단계들로 추상화시킬 수 있다. for i in range(num_adjust): # 1) 파라미터를 이용해 입력(xs)으로부터 예상 결과를 생성 y_pred = a*xs + b # 2) 예상 결과(y_pred)와 실제 결과(ys)를 비교해 오차를 계산 err = ((y_pred - ys)**2).mean() # 3) .backward()를 통해 각 파라미터가 오차에 관여하는 정도를 역산 err.backward() # 4) 역산된 grad를 바탕으로 파라미터 수정 a.data.sub_(0.01 * a.grad) b.data.sub_(0.01 * b.grad) # 5) 파라미터들의 grad값을 0으로 초기화 a.grad.zero_() b.grad.zero_() 그런데 이번과 같이 모델이 \(y=ax+b\) 처럼 간단한 형태인 경우 파라미터(a, b)들을 각자 관리할 수 있지만, 복잡한 모델의 경우 이는 상당히 번거로운 과정이 될 수 있다. 흩어진 파라미터들을 통합해 관리한다면 어떤 형태가 가능할지 생각해 보자.

[PyTorch] 01. PyTorch의 기본 구조

·7 분
요새는 뭘 찾아봐도 딥러닝이 나온다.. 성향이 continuous, gradient 같은 거랑은 안 맞아서 최대한 피해보려고 했지만 결국 시대의 흐름에 거스르지 못하고 어떻게 사용하는지 정도는 알아두는게 좋을 것 같아서 정리해봤다. 요구사항(prerequisite) python에 대한 기초지식 (ex: ABC.xyz()는 클래스/객체 ABC 안에 있는 xyz 라는 함수를 호출하는 것이다) 함수에 대한 개념(규칙에 따라 input → output으로 이어지는 블랙박스로 이해해도 된다) 머릿속 이미지화를 위한 약간의 상상력(?) tensor 클래스 tensor는 \(n\_1 \times n\_2 \times \cdots \times n\_d \) 개의 숫자를 \(d\)차원으로 정렬한 객체이다. 편의상 예시는 2차원 이하만을 다룰 예정이지만 임의의 차원을 가질 수 있다는 점은 알아두자.