Introduction

파이토치는 인공지능 분야에서 가장 사랑 받는 딥러닝 프레임워크이다. 텐서플로우나 JAX와 같이 다양한 머신러닝 프레임워크가 있지만, 현 시점 많은 인공지능 연구의 결과물이 파이토치로 구현되어 있다. 심지어 대부분의 오픈소스 LLM(e.g. Lamma, Deepseek) 또한 파이토치로 구현되어 있다. 따라서 인공지능을 공부하는데 있어 기본적인 파이토치 사용법을 반드시 익혀야 한다. 그래서 이 글에서는 기본적인 파이토치 사용법을 간단한 예제와 함께 알아보려고 한다.

파이토치

import torch
import torch.nn as nn # 혹은 from torch import nn
import torch.nn.functional as F
import torch.optim as optim

파이토치를 사용하기 위해 가장 먼저 import 해야하는 import문이다. 각 import문의 역할은 다음과 같다.

  • torch : 파이토치 핵심 기능 제공
  • torch.nn : 신경망 구성을 위한 모듈을 제공
  • torch.nn.functional : 신경망의 activation function 혹은 loss function을 제공
  • torch.optim : 모델학습의 옵티마이저를 제공

모델 정의

학습을 하기 위해서는 모델을 정의해야한다. 이는 torch.nn.Module을 상속한 클래스에 정의한다.

import torch.nn as nn

class ModelExample(nn.Module):
    def __init__(self):
        super(ModelExample, self).__init__()
        self.layer1 = nn.Linear(10, 20)
        self.layer2 = nn.ReLU()
        self.layer3 = nn.Linear(20, 1)

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

파이토치의 신경망 정의는 총 2가지 메소드로 구성된다.

  • __init__ : 신경망의 레이어와 필요한 요소들을 정의
  • forward : 실제 데이터가 연산되는 과정을 정의

이때 활성화함수의 경우 nn.ReLU() 혹은 F.ReLU() 두가지 방법으로 선언될 수 있다. 이는 객체로 선언해 사용하는지, 혹은 함수로 사용하는지에 차이가 있다. 따라서 일반적으로 __init__안에서 선언할때는 nn으로부터, forward에서 바로 사용할 때는 F에 있는 활성화 함수를 사용하면 된다.

모델을 선언할 때 레이어를 순차적으로 정의해야할 때, 좀 더 편리하게 작성하는 방법으로 Sequential이 있다.

layers = nn.Sequential(
	nn.Linear(10, 20),
	nn.ReLU(),
	nn.Linear(20, 5)
)

다음과 같이 __init__내부에 레이어를 묶어서 선언할 때 다음과 같은 방법으로 선언시, forward에서 좀 더 로직에 집중해 모델을 구성할 수 있게 된다.

데이터 셋과 데이터 로더

from torch.utils.data import Dataset, DataLoader

파이토치에서 데이터셋을 다루기 위해서 DatasetDataLoader를 제공한다.

  • Dataset : 파이토치에서 데이터셋을 정의하기 위해 사용하는 클래스
  • DataLoader : Dataset을 감싸 배치단위로 데이터를 제공. 샘플링 섞기 등의 기능도 제공

Dataset

우선 Dataset은 모델을 정의할 때와 마찬가지로 총 3개의 메소드를 정의해야 한다.

class CustomDataset(Dataset):
	def __init__(self, data, labels):
		self.data = data
		self.labels = labels
		
	def __getitem__(self, index):
		x = self.data[index]
		y = self.labels[index]
		return x, y
		
	def __len__(self):
		return len(self.data)
  • __init__: 데이터를 초기화하기 위한 메소드. 주로 파일 경로 로드 혹은 데이터 전처리에 사용
  • __getitem__ : 인덱스에 해당하는 데이터를 반환하는 메소드
  • __len__ : 데이터셋의 길이를 반환하는 메소드 이는 DataLoader와의 호환성을 유지하기 위한 인터페이스로, 반드시 지켜져야하는 규칙이다.

DataLoader

dataloader = DataLoader(dataset, batch_size=32, shuffle=True, drop_last = True)

DataLoader에서는 다음과 같이 위에서 선언한 Dataset을 감싸 사용한다. 자주 사용하는 매개변수는 다음과 같다.

  • dataset(필수) : 위에서 선언된 데이터셋을 데이터 로더에 제공한다.
  • batch_size : 한번에 처리할 데이터 샘플의 갯수를 설정(기본값 : 1)
  • shuffle : 데이터를 무작위로 섞을지 여부를 설정(기본값 : False)
  • drop_last : 마지막 배치에서 데이터가 부족할 시 제거할 지 여부를 선택 (기본값 : False)

학습 진행

GPU 설정

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

인공지능 학습에 있어 gpu를 사용하면 학습 속도가 크게 향상된다. 따라서 다음과 같은 코드를 통해서 만약 gpu가 사용이 가능한다면 cuda를(nvidia의 경우), 사용가능하지 않다면 cpu를 통해 학습 할 수 있도록 다음과 같은 코드를 이용한다.

Loss Function 및 Optimizer 정의

criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

학습에 앞서 Loss Function와 Optimizer를 정의한다. 이때 nn.MSELoss()의 경우 위와 마찬가지로, 미리 선언한 후, 학습과정에서 criterion으로 loss를 계산해줄 수 있다. 만약 F.mse_loss를 사용할 경우 바로 함수를 선언할 때 F.mse_loss(ouput, label)과 같이 사용해주어야 한다.

optimizer의 경우 torch.optim이 제공하는 Adam을 사용하였다. optimizer는 모델 학습과정에서 파라미터 업데이트를 할때 사용한다. 대표적인 알고리즘인 SGD, Adam, RMSprop 등의 방법을 통해 파라미터를 조정할 수 있다. optimizer에는 매개변수로 업데이트할 모델의 파라미터와 learning rate를 설정해주면 된다.

모델 학습

epochs = 10

for epoch in range(epochs):
    model.train()  # 학습 모드로 변환
    running_loss = 0.0
    for inputs, labels in dataloader:
        inputs, labels = inputs.to(device), labels.to(device)
        
        # 옵티마이저 초기화
        optimizer.zero_grad()
        
        # 순전파
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        
        # 역전파 및 가중치 갱신
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
    
    print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(dataloader):.4f}")

이제 학습을 위한 준비가 되었으니 다음과 같이 모델을 학습을 진행할 수 있다.

우선 학습하기 전에 모델을 학습 모드로 변환 후 학습을 진행해야한다.

  • model.train() : 학습과정에서 드롭아웃, 배치 정규화 등을 활성화
  • model.eval() : 추론(평가)과정에서 드롭아웃 비활성화 및 배치정규화를 이동 평균으로 계산

그 후 dataloader에서 전에 dataset에서 정의한 __getitem__ 메소드에서 반환되는 값을 배치 단위로 가져온다.

그 다음으로 optimizer.zero_grad() 를 통해 누적된 gradient를 초기화해준다. 파이토치에서는 기본적으로 gradient를 누적한다. 단순히 보면 이게 비효율적이라 생각할 수 있지만 gpu크기가 작을 경우 여러 미니배치의 gradient를 누적하여 큰 배치를 시뮬레이션 할 수 있게 된다.마찬가지로 여러 gpu를 통해 학습할 때도 여러 gpu에서 계산된 gradient를 누적해 값을 업데이트 해나갈 수 있게 된다. 따라서 매 배치루프 시작전에 gradient를 초기화해줄 필요가 있다.

gradient 초기화를 해주었다면 다음 해야할 과정은 순전파, 즉 model의 forward 과정이다. 해당 과정에서 모델에 input값을 넣어 나온 결과 값을 실제 라벨과 비교한다. 그리고 해당 차이를 loss function에 넣어 loss를 계산하게 된다.

순전파 과정이 끝나면 다음 과정은 역전파 과정이다. loss.backward() 함수를 통해 손실값에 대한 gradient를 계산해나간다. 여기서 파이토치의 핵심기능인 autograd를 사용하게 된다. 그리고 optimizer.step()를 통해 계산된 gradient값을 바탕으로 파라미터를 업데이트한다. 이때 업데이트 되는 파라미터는 사전에 정의된 optimizer의 종류에 따라 달라지게 된다.

마지막으로 계산된 loss의 값들을 loss의 합으로 누적한다. 이는 한 에폭의 학습이 진행될때까지 계속 누적한다. 그리고 한 에폭이 끝난다면 해당 학습동안의 평균 loss를 출력해줌으로서 학습의 진행상황을 알 수 있게 된다.

모델 추론

model.eval()

all_outputs = []
all_labels = []

with torch.no_grad():
    for inputs, labels in test_dataloader:
        inputs, labels = inputs.to(device), labels.to(device)
        
        # 순전파
        outputs = model(inputs)
        
        all_outputs.append(outputs.cpu())
        all_labels.append(labels.cpu())

# 이후 추가 로직을 구현

모델 추론의 경우에도 model.eval()를 통해 모델을 통해 추론모드로 변경하여 진행한다. 그리고 with torch.no_grad()를 통해 gradient 계산을 비활성화 한상태에서 추론을 최적화 한다. 이 과정은 필수는 아니지만, 학습에는 필요하지만 추론과정에서는 필요없는 연산을 하지 않음으로서 메모리와 추론 속도 측면에서 이점을 볼 수 있다. (하지만 사용하는게 권장된다) 해당 코드에서는 테스트셋에서 테스트하는 코드이다.

모델 저장

모델 저장의 경우 전체 모델을 저장하는 방법과 가중치만 저장하는 방법이 존재한다.

전체 모델 저장
torch.save(model, "model_full.pt")
model = torch.load("model_full.pt")

모델을 저장할 때 모델 전체를 저장할 경우, 모델을 불러올 때 모델의 정의를 할 필요가 없다는 장점이 있다.

모델 가중치 저장
torch.save(model.state_dict(), "model_weights.pt")

# 모델의 가중치를 받아오기 위해 모델을 불러오는 과정 필요 
model = ModelExample()
model.load_state_dict(torch.load("model_weights.pt"))
# model.load_state_dict(torch.load("model_weights.pt", strict=False))

두번째 방식은 모델의 가중치만을 저장하고, 해당 모델을 불러오는 과정이다. 위의 전체 모델을 저장할 때와 달리 모델 구조를 선언해줘야한다. 언뜻보면 전체 모델을 저장할 때보다 더 불편한 방식처럼 보이지만, 더 선호되는 방식이다. 이는 모델의 일부분이 변경되었을 때에도 일부 레이어의 파라미터를 재활용할 수 있기 때문이다. 모델의 파라미터를 로드할 때 strict = False로 주어 기존의 일부 파라미터를 재활용할 수 있다.

기타 라이브러리

컴퓨터 비전

import cv2
import torchvision
import torchvision.transforms as transforms
  • cv2(OpenCV) : 이미지 처리를 위한 라이브러리로, 이미지를 읽거나 객체추적같은 기능을 제공
  • torchvision : 파이토치에서 컴퓨터비전을 사용하기 위한 모듈
  • torchvision.transforms : 이미지데이터의 전처리 및 증강을 위한 모듈

자연어 처리

from torchtext.data import Field, TabularDataset, BucketIterator
from torchtext.vocab import GloVe
from transformers import AutoModel, AutoTokenizer
  • torchtext : 파이토치의 자연어 처리 라이브러리
  • 예시 모듈 설명
    • Field : 데이터 전처리를 위한 클래스
    • TabularDataset : CSV, JSON등의 파일을 테이블형식으로 로드하는 클래스
    • BucketIterator : 비슷한 길이의 시퀀스를 하나의 배치로 묶어 패딩 작업을 최소화
    • GloVe : 사전 학습된 임베딩 벡터 로드
  • transformer : 허깅페이스의 트랜스포머 라이브러리는 사전 정의된 언어 모델을 제공함

데이터 처리

import numpy as np
import pandas as pd
import math
import time
import copy
import wandb
from sklearn.model_selection import train_test_split
from tqdm import tqdm # 쥬피터 노트북에서는 tqdm.notebook import tqdm으로 사용

해당 라이브러리는 파이토치와 같이 자주 사용되는 라이브러리를 모아둔 것이다.

  • numpy : 수치 계산을 위한 라이브러리로 주로 행렬 연산을 위해 사용한다.
  • pandas : 데이터 분석 및 조작을 위한 라이브러리로, CSV같은 Tabular데이터를 데이터프레임으로 관리한다.
  • math : 파이썬의 내장 라이브러리로, 삼각함수 로그 등 다양한 수학적 연산을 제공함
  • time : 파이썬 내장 라이브러리로 시간 및 시간의 간격을 측정할 때 사용함
  • copy : 파이썬의 객체를 복사하는 라이브러리로 주로 깊은 복사를 할 때 사용
  • wandb : 모델 학습시 각 실험을 시각화하고 로깅할 때 주로 사용하는 라이브러리
  • sklearn : 머신러닝 라이브러리로서 데이터 전처리, 분할 및 딥러닝 이전의 기본 머신러닝 알고리즘이 구현되어있음
  • tqdm : 루프 진행상황을 시각적으로 보여주는 라이브러리

시각화

import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
  • matplotlib.pyplot : 파이썬의 가장 기본적인 데이터 시각화 라이브러리
  • seaborn : matplotlib 기반으로 제작된 고급 데이터 시각화 라이브러리
  • plotly : 유저 인터랙션이 가능한 시각화를 제공하는 라이브러리