↓ 본문으로 건너뛰기

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

·5 분
nyan101
작성자
nyan101
목차
PyTorch - 이 글은 시리즈의 일부입니다.
[PyTorch] 02. nn.Module로 모델 클래스 만들기

학습의 각 단계를 추상화하기

앞서 그래디언트를 이용해 간단한 학습이 이루어지는 과정을 알아보았다. 이전 글의 마지막 코드를 다시 살펴보면 학습의 한 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)들을 각자 관리할 수 있지만, 복잡한 모델의 경우 이는 상당히 번거로운 과정이 될 수 있다. 흩어진 파라미터들을 통합해 관리한다면 어떤 형태가 가능할지 생각해 보자.

params = "통합된 파라미터들을 저장할 모음. list,dict,class,... 중 뭐가 좋을까?"

def init_params(params):
    params = "어떤 파라미터를 사용할 것인지 정의하고, 각 파라미터에 초기값 설정"
    pass

def predict(x, params):
    y_pred = "입력(=x)과 params으로부터 예측값 계산"
    return y_pred

def calculate_err(y_pred, y):
    err = "예측값(y_pred)과 실제값(y)간의 차이를 계산"
    return err

def update_params(params):
    "grad를 바탕으로 params를 조절한다"
    pass

def clear_grad(params):
    "params의 grad 값들을 전부 0으로 초기화한다"
    pass

# 추상화된 학습과정
init_params()
for i in range(num_adjust):
    # 1) 파라미터를 이용해 입력(xs)으로부터 예상 결과를 생성
    y_pred = predict(xs, params)
    # 2) 예상 결과(y_pred)와 실제 결과(ys)를 비교해 오차를 계산
    err = calculate_err(y_pres, ys)
    # 3) .backward()를 통해 각 파라미터가 오차에 관여하는 정도를 역산
    err.backward()
    # 4) 역산된 grad를 바탕으로 파라미터 수정 
    update_params(params)
    # 5) 파라미터들의 grad값을 0으로 초기화
    clear_grad(params)

대부분의 경우 학습과정은 이 형태를 크게 벗어나지 않을 것이다. 그런데 여기서 일부 함수들이 순수 함수(pure function)가 아닌, 내부 상태를 변화시키는 ‘동작’ 을 하는 프로시저(procedure)라는 사실이 눈에 들어온다. 일관성 있는 관리를 위해 각 단계들을 다시 역할에 따라 모아 클래스로 만들 수 있다.

  • 예측값 계산 담당
    • init_params(), predict()
  • 오차 계산 담당
    • calculate_err()
  • 파라미터 조정 담당
    • update_params(), clear_grad()

pytorch 모델은 기본적으로 이러한 분류를 따라 설계된다. 각각은 흔히 model(혹은 module), criterion, optimizer라는 이름으로 불리며, 이들 용어는 특별한 언급이 없는 한 이 블로그에서도 동일한 의미로 약속하자.

Model 클래스 만들기

pytorch 모델 클래스를 위한 기본 틀은 다음과 같다. 선형 함수는 너무 간단하니 3개의 파라미터(a, b, c)를 설정해 2차함수 근사를 위한 모델을 만들었다.

import torch
import torch.nn as nn

class MyModel(nn.Module): # nn.Module 클래스의 확장이 된다
    def __init__(self):
        # 부모 생성자 호출
        super(MyModel, self).__init__()
        # 파라미터들의 타입이 torch.tensor가 아니라 nn.Parameter임에 유의
        self.a = nn.Parameter(torch.tensor([1.]))
        self.b = nn.Parameter(torch.tensor([2.]))
        self.c = nn.Parameter(torch.tensor([3.]))
    
    def forward(self, x):
        y  = self.a * (x**2)
        y += self.b * (x**1)
        y += self.c
        return y

이렇게 정의된 모델 클래스는 다음과 같이 사용할 수 있다.

# fitting 대상 데이터
xs = [ 1.00,  2.00,  3.00,  4.00,  5.00,  6.00,  7.00,  8.00,  9.00 ]
ys = [ 9.15,  5.64, -0.21, -2.36, -0.91, -0.67,  2.65, 10.36, 17.13 ]

# 모델 객체 생성
model = MyModel()

# model.parameters()로 모델의 파라미터에 접근할 수 있다
# 파라미터들의 이름을 같이 보고싶다면 model.named_parameters()를 사용하자
for param in model.parameters():
    print(param)
    
y_pred = model(xs) # model.forward(xs)와 동일

처음 예측을 진행해보면 y_pred 는 (당연하게도) 전혀 들어맞지 않는 결과가 나온다.

이제 오차를 계산하고 파라미터를 조정해야 한다.

오차 계산하기

이전까지는 오차를 Mean Square Error(MSE)로 정의해 이를 코드상에서 계산했다. 그런데 꼭 이것만이 유일한 답일까? 오차를 정의하는 다른 방법들은 없을까?

잘 알려진 MSE 외에도 Cross Entropy나 KL-divergence와 같은 다양한 방법들이 존재하며, 이들 대부분은 pytorch내부에 이미 구현되어 있다. 이번에는 torch.nn.MSELoss에 정의된 MSE를 그대로 사용하자. 설정할 수 있는 옵션들이 궁금한 사람을 위해 공식 문서가 준비되어 있다.

criterion = nn.MSELoss()     # 처음 한 번만 정의
----------
loss = criterion(y_pred, ys) # 매 라운드(epoch)마다. *앞으로는 err 대신 loss라는 용어를 사용한다
loss.backward()

파라미터 조정하기

loss.backward()까지 진행하면 각 파라미터들마다 계산된 .grad를 얻는다. 이제 이를 바탕으로 파라미터를 조정하자. 이전에는 아래와 같은 방식을 이용했다.

self.a -= 0.01 * self.a.grad
self.b -= 0.01 * self.b.grad
self.c -= 0.01 * self.c.grad
...

요약하면 모든 파라미터에 param -= (small_ratio) * param.grad 를 적용하는 것이라고 할 수 있다. 이런 방식을 Stochastic Gradient Descent(SGD)라고 하며, 이밖에도 Adam, ASGD, Rprop 등 다양한 업데이트 방식이 존재한다. 당연하게도 이런 업데이트 알고리즘 역시 pytorch의 torch.optim 내에 구현되어 있다. 여기서는 SGD를 그대로 사용하자. 업데이트 시 변화량에 곱해지는 ‘작은 수’를 lr(learning ratio)이라고 지칭한다.

import torch.optim as optim

optimzer = optim.SGD(model.parameters(), lr=1e-4) # 정의 시점에 model.parameters()를 넘겨준다
----------
loss.backward()
optim.step()      # 파라미터 업데이트
optim.zero_grad() # 파라미터들의 grad를 0으로 초기화

All-in-One

이제 전체 코드를 종합해보자.

# Model 클래스 구조
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.a = nn.Parameter(torch.tensor([1.]))
        self.b = nn.Parameter(torch.tensor([2.]))
        self.c = nn.Parameter(torch.tensor([3.]))
    
    def forward(self, x):
        y  = self.a * (x**2)
        y += self.b * (x**1)
        y += self.c
        return y

# fitting 대상 데이터
xs = torch.tensor([ 1.00,  2.00,  3.00,  4.00,  5.00,  6.00,  7.00,  8.00,  9.00 ])
ys = torch.tensor([ 9.15,  5.64, -0.21, -2.36, -0.91, -0.67,  2.65, 10.36, 17.13 ])

# model과 criterion, optimizer 선언
model = MyModel()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=1e-4)

# 학습
for epoch in range(100000):
    y_pred = model(xs)
    loss = criterion(y_pred, ys)
    optimizer.zero_grad() # .zero_grad()의 위치는 이곳과 .step() 이후 중 어느 곳이든 상관 없음
    loss.backward()       # 인터넷의 다른 예제들과의 통일성을 위해 loss.backward() 이전에 놓았다
    optimizer.step()
    
    if(epoch % 10000):
        print(f"Loss after {epoch} steps: {loss}")

학습 epoch이 증가함에 따라 점차 fitting하고자 하는 데이터에 가까워지는 것을 확인할 수 있다.


PyTorch - 이 글은 시리즈의 일부입니다.
[PyTorch] 02. nn.Module로 모델 클래스 만들기

관련 글

[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차원 이하만을 다룰 예정이지만 임의의 차원을 가질 수 있다는 점은 알아두자.

Möbius 함수의 정의와 활용

·1 분
얼마 전 코드포스에서 흥미로운 문제를 접했다. 대회 당시에는 풀지 못했는데, 에디토리얼을 읽어보던 중 뫼비우스 함수(Möbius function)에 대한 언급이 있어 이에 대해 좀더 자세히 알아보았다. Möbius function의 정의 뫼비우스 함수(Möbius function)는 자연수 n에 대해 다음과 같이 정의된다. (\(p\)는 소수)

[Coq 입문] Ch04. Polymorphism & Higher-Order Functions (2)

·5 분
High-Order Functions 다른 함수를 다룰 수 있는 함수를 고차 함수(higher-order function)라고 한다. Definition do3times {X:Type} (f:X->X) (a:X) : X := f (f (f a)). >> Check @do3times : @do3times : forall X : Type, (X -> X) -> X -> X 이 do3times는 함수 f와 인자 a를 받아 a에 f를 3번 적용한 결과를 돌려준다. 함수를 인자로 제공한다는게 어떤 의미인지 살펴보자 >> Definition doubleMe (n:nat) : nat := n+n. >> Compute do3times doubleMe 3. : 24 : nat Filter 이제 좀더 유용한 고차 함수들에 대해 알아보자. filter는 X타입의 리스트(list X)와 원소 X에 대한 판정함수(predicate, \(f:X\rightarrow bool\))를 받아 주어진 predicate를 만족하는(i.e. true를 리턴하는) 원소들만을 남긴다.