본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

03 파이토치 기초 (4) 데이터세트와 데이터로더, 모델 저장 및 불러오기

데이터세트와 데이터로더

데이터세트는 데이터의 집합을 의미하며, 입력값과 결괏값에 대한 정보를 제공하거나 일련의 데이터 묶음을 제공한다. 데이터세트의 구조는 일반적으로 데이터베이스의 테이블과 같은 형태로 구성되어 있다.

 

데이터의 구조나 패턴은 매우 다양하기 때문에 학습해야 하는 데이터가 파일 경로로 제공되거나 전처리 단계가 필요한 경우도 있다. 이처럼 데이터를 변형하고 매핑하는 코드를 학습 과정에 직접 반영하면 모듈화, 재사용성, 가독성을 떨어뜨리는 주요 원인이 된다. 이러한 현상을 방지하고 코드를 구조적으로 설계할 수 있도록 데이터세트와 데이터로더를 사용한다.

 

데이터세트

데이터세트 클래스의 기본형은 다음과 같다.

class Dataset:
  def __init__(self, data, *args **kwargs):
    self.data = data
  
  def __getitem__(self, index):
    return tuple(data[index] for data in data.tensors)
  
  def __len__(self):
    return self.data[0].size(0)
  • 초기화 메서드 : 입력된 데이터의 전처리 과정을 수행한다. 새로운 인스턴스가 생성될 때 학습에 사용될 데이터를 선언하고, 학습에 필요한 형태로 변형하는 과정을 진행한다.
  • 호출 메서드 : 입력된 index에 해당하는 데이터 샘플을 불러오고 반환한다.
  • 길이 반환 메서드 : 학습에 사용된 전체 데이터세트의 개수를 반환한다.

모델 학습을 위해 임의의 데이터세트를 구성할 때는 파이토치에서 지원하는 데이터세트 클래스를 상속받아 사용한다.

 

데이터로더

데이터로더는 데이터세트에 저장된 데이터를 어떠한 방식으로 불러와 활용할지 정의한다. 학습을 조금 더 원활하게 진행할 수 있도록 배치 크기(batch_size), 데이터 순서 변경(shuffle), 데이터 로드 프로세스 수(num_workers) 등의 기능을 제공한다.

 

다중 선형 회귀

데이터세트와 데이터로더를 활용해 지도 학습 중 하나인 다중 선형 회귀를 구현해보자.

# 기본 구조 선언
import torch
from torch.utils.data import TensorDataset, DataLoader

train_x = torch.FloatTensor([
    [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]
])
train_y = torch.FloatTensor([
    [0.1, 1.5], [1, 2.8], [1.9, 4.1], [2.8, 5.4], [3.7, 6.7], [4.6, 8]
])

# 데이터세트와 데이터로더
train_dataset = TensorDataset(train_x, train_y)
train_dataloader = DataLoader(train_dataset, batch_size=2, shuffle=True, drop_last=True)
  • 예시에서는 데이터세트를 재정의하지 않고 기본 데이터세트(Dataset) 클래스를 상속받아 재정의된 TensorDataset 클래스를 사용한다.
  • 종속 변수와 독립 변수 모두 (n, 2)의 shape을 가진다.
# 모델, 오차 함수, 최적화 함수 선언
model = torch.nn.Linear(2, 2, bias=True)
criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)

# 데이터로더 적용
for epoch in range(20000):
  cost = 0.0

  for batch in train_dataloader:
    x, y = batch
    output = model(x)

    loss = criterion(output, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    cost += loss

  cost /= len(train_dataloader)

  if (epoch + 1) % 1000 == 0:
    print(f"Epoch : {epoch+1:4d}, Model : {list(model.parameters())}, Cost : {cost:.3f}\n")
  • 에폭마다 오차를 다시 계산하기 위해 에폭이 시작할 때 cost를 0으로 초기화 한다.
  • 배치 크기로 데이터를 학습하기 때문에 오차가 아닌 손실(loss)를 계산한다.
  • dataloader는 반복하여 batch를 반환한다.
  • 손실 값을 계산하고 배치마다 오차에 손실 값을 누적해서 더한 뒤 배치 학습이 종료된 후 오차의 평균값을 계산하기 위해 dataloader의 길이만큼 나눈다.

 

모델/데이터세트 분리

파이토치의 모델은 인공신경망 모듈을 활용해 구현된다. 모델은 데이터에 대한 연산을 수행하는 계층을 정의하고, 순방향 연산을 수행한다.

 

모델 구현은 신경망 패키지의 모듈(Module) 클래스를 활용한다. 새로운 모델 클래스를 생성하려면 모듈 클래스를 상속받아 임의의 서브 클래스를 생성한다.

 

모듈 클래스

모듈 클래스는 초기화 메서드와 순방향 메서드를 재정의하여 활용한다. 

 

모듈 클래스 기본형

class Model(nn.Module):
  def __init__(self):
    super().__init__()
    self.conv1 = nn.Conv2d(1, 20, 5)
    self.conv2 = nn.Conv2d(20, 20, 5)
  
  def forward(self, x):
    x = F.relu(self.conv1(x))
    x = F.relu(self.conv2(x))
    return x
  • 초기화 메서드 : 신경망에 사용될 계층을 정의하기 전에 super 함수로 모듈 클래스의 속성을 초기화한다. super 함수로 부모 클래스를 초기화하면 서브 클래스인 모델에서 부모 클래스의 속성을 사용할 수 있다.
  • 순방향 메서드 : 초기화 메서드에서 선언한 모델 매개변수를 활용해 신경망 구조를 설계한다. 모듈 클래스는 호출 가능한 형식(Callable Type)으로 모델의 인스턴스를 호출하는 순간에 호출 메서드(__call__)가 순방향 메서드를 실행한다.
  • 파이토치의 자동 미분 기능인 Autograd에서 모델의 매개변수를 역으로 전파해 자동으로 기울기 또는 변화도를 계산해주기 때문에 별도의 역전파 메서드를 구성하지 않아도 된다.

 

비선형 회귀

데이터셋

non_linear.csv 파일의 데이터로 비선형 회귀를 구현해보자.

import torch
import pandas as pd
from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
  def __init__(self, file_path):
    df = pd.read_csv(file_path)
    self.x = df.iloc[:, 0].values
    self.y = df.iloc[:, 1].values
    self.length = len(df)

  def __getitem__(self, index):
    x = torch.FloatTensor([self.x[index] ** 2, self.x[index]])
    y = torch.FloatTensor([self.y[index]])
    return x, y

  def __len__(self):
    return self.length
  • 데이터세트 클래스를 상속받아 사용자 정의 데이터세트(CustomDataset)를 정의한다.
  • 초기화 메서드에서 csv 파일의 경로를 입력받을 수 있게 file_path를 정의한다.
  • csv 파일을 불러온 후 self.x와 self.y에 각각 x값과 y값을 할당한다.
  • 호출 메서드에서 x 값과 y값을 반환한다.
  • 길이 반환 메서드로 초기화 메서드에서 선언한 self.length를 반환한다.
class CustomModel(torch.nn.Module):
  def __init__(self):
    super().__init__()
    self.layer = torch.nn.Linear(2, 1, bias=True)

  def forward(self, x):
    x = self.layer(x)
    return x
  • 모듈 클래스를 상속받아 사용자 정의 모델을 정의한다.
train_dataset = CustomDataset(DATA_PATH / "non_linear.csv")
train_dataloader = DataLoader(train_dataset, batch_size=128, shuffle=True, drop_last=True)
  • 사용자 정의 데이터세트와 데이터로더를 통해 데이터세트, 데이터로더 인스턴스를 생성한다.
device = "cuda" if torch.cuda.is_available() else "cpu"
model = CustomModel().to(device)
criterion = torch.nn.MSELoss().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)
  • GPU 연산 적용을 위해 모델과 오차 함수 클래스에 to 메서드로 device를 설정한다. 그런 다음 최적화 함수에 모델의 파라미터를 전달한다.
for epoch in range(10000):
  cost = 0.0

  for x, y in train_dataloader:
    x = x.to(device)
    y = y.to(device)

    output = model(x)

    loss = criterion(output, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    cost += loss

  cost /= len(train_dataloader)

  if (epoch + 1) % 1000 == 0:
    print(f"Epoch : {epoch+1:4d}, Model : {list(model.parameters())}, Cost : {cost:.3f}\n")
  • GPU 연산을 적용하므로 학습에 사용되는 x와 y 변수에도 to 메서드를 통해 device를 설정해준다.
with torch.no_grad():
  model.eval()
  inputs = torch.FloatTensor(
      [
          [1 ** 2, 1],
          [5 ** 2, 5],
          [11 ** 2, 11],
      ]
  ).to(device)
  outputs = model(inputs)
  print(outputs)
  • 검증세트나 테스트세트로 모델을 확인하거나 평가할 때는 torch.no_grad 클래스를 활용한다. no_grad 클래스는 기울기 계산을 비활성화하는 클래스로써 자동 미분 기능을 사용하지 않도록 설정해 메모리 사용량을 줄여 추론에 적합한 상태로 변경한다.
  • 그런 다음 모델 평가 모드는 eval 메서드로 변경한다. 모델을 평가 모드로 변경하지 않으면 일관성 없는 추론 결과를 반환하므로 평가 시 항상 선언해야 한다.

데이터세트 분리

from torch.utils.data import random_split

dataset = CustomDataset(DATA_PATH / "non_linear.csv")
dataset_size = len(dataset)
train_size = int(dataset_size * 0.8)
validation_size = int(dataset_size * 0.1)
test_size = dataset_size - train_size - validation_size

train_dataset, validation_dataset, test_dataset = random_split(dataset, [train_size, validation_size, test_size])
  • 데이터 세트를 분리하기 위해 torch.utils.data 모듈에서 random_split 함수를 사용한다.
  • 데이터세트 인스턴스와 분리하기 위한 각 데이터셋의 사이즈를 정수형 시퀀스로 전달하여 사용할 수 있다.

검증용 데이터세트를 통한 평가는 다음과 같이 할 수 있다.

with torch.no_grad():
  model.eval()
  for x, y in validation_dataloader:
    x = x.to(device)
    y = y.to(device)

    outputs = model(x)
    print(f"X : {x}")
    print(f"Y : {y}")
    print(f"Outputs : {outputs}")
    print("--------------------")

 

모델 저장 및 불러오기

파이토치 모델을 직렬화와 역직렬화를 통해 객체를 저장하고 불러올 수 있다.

 

모델을 저장하려면 파이썬의 피클을 활용해 파이썬 객체 구조를 바이너리 프로토콜로 직렬화 한다.

 

모델을 불러오려면 저장된 객체 파일을 역직렬화해 현재 프로세스의 메모리에 업로드한다.

 

모델 저장 및 불러오기

torch.save(model, SAVE_PATH / "model.pt")
model = torch.load(SAVE_PATH / "model.pt", map_location=device)
  • 모델을 불러올 때는 모델 학습 상태가 GPU인지 CPU인지 확인하지 않고 모델을 활용할 수 있게 map_location 매개변수로 device를 설정한다.
  • 모델을 불러오는 경우에도 동일한 형태의 클래스가 선언되어 있어야 한다. 여기서는 CustomModel 클래스가 동일한 구조로 선언되어 있어야 한다.

모델 상태 저장 및 불러오기

# 모델 상태 저장
torch.save(model.state_dict(), SAVE_PATH / "model_state_dict.pt")

# 모델 상태 불러오기
model = CustomModel().to(device)
model_state_dict = torch.load(SAVE_PATH / "model_state_dict.pt", map_location=device)
model.load_state_dict(model_state_dict)
  • state_dict 메서드는 모델에서 학습이 가능한 매개변수를 순서가 있는 딕셔너리(OrderedDict) 형식으로 반환한다.
  • 모델 상태를 불러올 때는 먼저 모델 인스턴스를 생성하고 device를 설정한다. 그런 다음 load 메서드를 통해 모델 상태를 불러오고 load_state_dict 메서드를 통해 모델 인스턴스에 모델 상태를 반영한다.

 

체크포인트 저장/불러오기

체크포인트는 학습 과정의 특정 지점마다 저장하는 것을 의미한다. 데이터의 개수가 많고 깊은 구조의 모델을 학습하면 오랜 시간이 소요되는데, 이러한 경우 예기치 못하게 오류가 발생하거나 시스템 리소스 과부하 등으로 학습이 정상적으로 마무리되지 않을 수 있다.

 

다음은 체크포인트 저장하는 예시이다.

checkpoint = 1
for epoch in range(10000):
  cost = 0.0

  for x, y in train_dataloader:
    x = x.to(device)
    y = y.to(device)

    output = model(x)
    loss = criterion(output, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    cost += loss

  cost = cost / len(train_dataloader)

  if (epoch + 1) % 1000 == 0:
    torch.save(
        {
            "model":"CustomModel",
            "epoch":epoch,
            "model_state_dict":model.state_dict(),
            "optimizer_state_dict":optimizer.state_dict(),
            "cost":cost,
            "description":f"CustomModel checkpoint-{checkpoint}",
        },
        SAVE_PATH / f"checkpoint-{checkpoint}.pt"
    )
    checkpoint += 1
  • 체크포인트도 save 메서드를 활용해 저장할 수 있다. 단, 다양한 정보를 저장하기 위해 딕셔너리 형식으로 값을 할당한다.
  • 학습을 이어서 진행하기 위한 목적이므로 에폭, 모델 상태, 최적화 상태 등은 필수로 포함돼야 한다.

체크포인트 불러오기

model = CustomModel().to(device)
criterion = torch.nn.MSELoss().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)

checkpoint = torch.load(SAVE_PATH / "checkpoint-6.pt")
model.load_state_dict(checkpoint["model_state_dict"])
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
checkpoint_epoch = checkpoint["epoch"]
checkpoint_description = checkpoint["description"]
  • 체크포인트를 불러온 후 model과 optimizer에 저장되어있던 상태를 반영한 후 다시 재학습을 시작할 수 있다.