Python/PyTorch 공부

[PyTorch] 3. 예제로 배우는 파이토치 - nn 모듈, 가중치 공유, 제어 흐름, 사용자 정의 nn 모듈

AI 꿈나무 2020. 12. 7. 16:03
반응형

공부 목적으로 PyTorch 튜토리얼 홈페이지를 변역해보았습니다.

 

Learning PyTorch with Examples — PyTorch Tutorials 1.7.0 documentation

Learning PyTorch with Examples Author: Justin Johnson This tutorial introduces the fundamental concepts of PyTorch through self-contained examples. At its core, PyTorch provides two main features: An n-dimensional Tensor, similar to numpy but can run on GP

pytorch.org

예제로 배우는 파이토치 - Learning PyTorch with examples

nn module

1. PyTorch : nn

 연산 그래프와 자동미분은 복잡한 연산을 정의하고 자동으로 미분을 하기 위한 강력한 패러다임입니다.

 하지만 큰 신경망에서 자동미분 그 자체만으로 낮은 수준일 수 있습니다.

 

 신경망을 구성할 때 종종 연산을 계상에 배열하는 것으로 생각하는데, 이 중 일부는 학습과정에서 최적화될 학습가능한 매개변수를 갖고 있습니다.

 

 TensorFlow에서 Keras, TensorFlow-Slimn, TFLear 같은 패키지는 연산 그래프 그 자체에 대해 신경망을 구성하기에 매우 유용한 높은 수준의 추상화를 제공합니다.

 

 PyTorch에서 nn 패키지는 동일한 목적으로 제공됩니다.

 nn 패키지는 대략적으로 신경망의 계층들과 동일한 Modules의 집합을 정의합니다.

 모듈은 입력 Tensor를 받고 결과 Tensor를 계산하지만 학습가능한 매개변수를 지닌 Tensor처럼 내부 상태를 갖습니다.

 nn 패키지는 또한 신경망을 학습할때 사용되는 흔한 유용한 손실 함수의 집합을 정의합니다.

 

 이번 예제에서 2계층 신경망을 구현하기 위해 nn 패키지를 사용하겠습니다.

 

# -*- coding: utf-8 -*-
import torch

dtype = torch.float
device = torch.device("cpu")

# N은 배치 크기이며, D_in은 입력의 차원입니다.
# H는 은닉층의 차원이며, D_out은 출력 차원입니다.
N, D_in, H, D_out = 64, 1000, 100, 10

# 입력과 출력을 저장하기 위해 무작위 값을 갖는 Tensor를 생성합니다.
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# nn 패키지를 사용하여 모델을 순차적 계층(sequence of layers)으로 정의합니다.
# nn.Sequential은 다른 Module들을 포함하는 Module로, 그 Module들을 순차적으로
# 적용하여 출력을 생성합니다. 각각의 Linear Module은 선형 함수를 사용하여
# 입력으로부터 출력을 계산하고, 내부 Tensor에 가중치와 편향을 저장합니다.
model = torch.nn.Sequential(
    torch.nn.Linear(D_in, H),
    torch.nn.ReLU(),
    torch.nn.Linear(H, D_out),
)

# 또한 nn 패키지에는 흔히 사용되는 손실 함수들에 대한 정의도 포함합니다.
# 여기에서는 평균 제곱 오차(MSE; Mean Squared Error)를 손실 함수로 사용하겠습니다.
loss_fn = torch.nn.MSELoss(reduction='sum')

learning_rate = 1e-4
for t in range(500):
    # 순전파 단계: 모델에 x를 전달하여 예상되는 y 값을 계산합니다. Module 객체는
    # __call__ 연산자를 덮어써(override) 함수처럼 호출할 수 있게 합니다.
    # 이렇게 함으로써 입력 데이터의 Tensor를 Module에 전달하여 출력 데이터의
    # Tensor를 생성합니다.
    y_pred = model(x)
    
    # 손실을 계산하고 출력합니다.
    # 예측한 y와 정답인 y를 갖는 Tensor들을 전달하고,
    # 손실 함수는 손실 값을 갖는 Tensor를 반환합니다.
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 역전파 단계를 실행하기 전에 변화도를 0으로 만듭니다.
    model.zero_grad()
    
    # 역전파 단계: 모델의 학습 가능한 모든 매개변수에 대해 손실의 변화도를
    # 계산합니다. 내부적으로 각 Module의 매개변수는 requires_grad=True 일 때
    # Tensor 내에 저장되므로, 이 호출은 모든 모델의 모든 학습 가능한 매개변수의
    # 변화도를 계산하게 됩니다.
    loss.backward()
    
    # 경사하강법(gradient descent)를 사용하여 가중치를 갱신합니다. 각 매개변수는
    # Tensor이므로 이전에 했던 것과 같이 변화도에 접근할 수 있습니다.
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad

2. PyTorch : optim

 지금까지 torch.no_grad()로 설정된 학습 가능한 매개변수들을 지닌 Tensor를 수동으로 변형하여 모델의 가중치를 갱신했었습니다.

 이것은 SGD(stochastic gradient)와 같은 간단한 최적화 알고리즘에게 큰 부담이 아닙니다.

 하지만 실제로 AdaGrad, RMSProp, Adam, 등등 같은 더 세련된 optimizer를 사용하여 신경망을 훈련합니다.

 

 PyTorch에서 optim 패키지는 최적화 알고리즘에 대한 아이디어를 추상화하고 흔하게 사용되는 최적화 알고리즘의 구현을 제공합니다.

 

 이번 예제에서 이전처럼 신경망을 정의하기 위해 nn 패키지를 사용하지만 optim 패키지에서 제공하는 RMSprop 알고리즘을 사용하여 모델을 최적화 하겠습니다.

 

# -*- coding: utf-8 -*-
import torch

# N은 배치 크기이며, D_in은 입력의 차원입니다;
# H는 은닉층의 차원이며, D_out은 출력 차원입니다.
N, D_in, H, D_out = 64, 1000, 100, 10

# 입력과 출력을 저장하기 위해 무작위 값을 갖는 Tensor를 생성합니다.
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# nn 패키지를 사용하여 모델과 손실 함수를 정의합니다.
model = torch.nn.Sequential(
    torch.nn.Linear(D_in, H),
    torch.nn.ReLU(),
    torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(reduction='sum')

# optim 패키지를 사용하여 모델의 가중치를 갱신할 Optimizer를 정의합니다.
# 여기서는 Adam을 사용하겠습니다; optim 패키지는 다른 다양한 최적화 알고리즘을
# 포함하고 있습니다. Adam 생성자의 첫번째 인자는 어떤 Tensor가 갱신되어야 하는지
# 알려줍니다.

learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
    # 순전파 단계: 모델에 x를 전달하여 예상되는 y 값을 계산합니다.
    y_pred = model(x)

    # 손실을 계산하고 출력합니다.
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 역전파 단계 전에, Optimizer 객체를 사용하여 (모델의 학습 가능한 가중치인)
    # 갱신할 변수들에 대한 모든 변화도를 0으로 만듭니다. 이렇게 하는 이유는
    # 기본적으로 .backward()를 호출할 때마다 변화도가 버퍼(buffer)에 (덮어쓰지 않고)
    # 누적되기 때문입니다. 더 자세한 내용은 torch.autograd.backward에 대한 문서를
    # 참조하세요.
    optimizer.zero_grad()
    
    # 역전파 단계: 모델의 매개변수에 대한 손실의 변화도를 계산합니다.
    loss.backward()

    # Optimizer의 step 함수를 호출하면 매개변수가 갱신됩니다.
    optimizer.step()

3. PyTorch : 사용자 정의 nn 모듈

 때때로 기존의 모듈의 구성(sequence)보다 더 복잡한 모듈을 지정하고 싶을 때가 있습니다.

 이 경우에 nn.Module의 서브클래스로 새로운 모듈을 정의하고 입력 Tensor를 받는 순전파와 다른 모듈 또는 다른Tensor에 대한 자동미분 연산을 사용하여 출력 Tensor를 생성하는 순전파를 정의하겠습니다.

 

 이번 예제에서 2계층 신경망을 직접 정의한 nn.Module 서브클래스로 구현하겠습니다.

 

# -*- coding: utf-8 -*-
import torch


class TwoLayerNet(torch.nn.Module):
    def __init__(self, D_in, H, D_out):
        '''
        생성자에서 2개의 nn.Linear 모듈을 생성하고, 멤버 변수로 지정합니다.
        '''
        super(TwoLayerNet, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)
    
    def forward(self, x):
        '''
        순전파 함수에서는 입력 Tensor를 받고 출력 Tensor를 반환해야 합니다.
        Tensor 상의 임의의 연산자뿐만 아니라 생성자에서 정의한 Module도 사용할 수 있습니다.
        '''
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred
        
# N은 배치 크기이며, D_in은 입력의 차원입니다;
# H는 은닉층의 차원이며, D_out은 출력 차원입니다.
N, D_in, H, D_out = 64, 1000, 100, 10

# 입력과 출력을 저장하기 위해 무작위 값을 갖는 Tensor를 생성합니다.
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# 앞에서 정의한 클래스를 생성하여 모델을 구성합니다.
model = TwoLayerNet(D_in, H, D_out)

# 손실 함수와 Optimizer를 만듭니다. SGD 생성자에 model.parameters()를 호출하면
# 모델의 멤버인 2개의 nn.Linear 모듈의 학습 가능한 매개변수들이 포함됩니다.
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
    # 순전파 단계: 모델에 x를 전달하여 예상되는 y 값을 계산합니다.
    y_pred = model(x)

    # 손실을 계산하고 출력합니다.
    loss = criterion(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())

    # 변화도를 0으로 만들고, 역전파 단계를 수행하고, 가중치를 갱신합니다.
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

  

4. PyTorch : 제어 흐름(Control Flow) + 가중치 공유(Weight Sharing)

 동적 그래프와 가중치 공유의 예시로 매우 이상한 모델을 구현하겠습니다.

 각 순전파 단계에서 많은 은닉 계층을 갖는 완전히 연결(fully-connected)된 ReLU 신경망이 무작위로 0~3 사이의 숫자를 선택하고, 가장 안쪽(innermosr)의 은닉층들을 계산하기 위해 동일한 가중치를 여러 번 재사용합니다.

 

 이 모델에서는 일반적인 Python 제어 흐름을 사용하여 반복(loop)을 구현할 수 있으며, 순전파 단계를 정의할 때 단지 동일한 Module을 여러번 재사용함으로써 내부(innermosr) 계층들 간의 가중치 공유를 구현할 수 있습니다.

 

 이러한 모델을 Module을 상속받는 서브클래스로 간단히 구현해보겠습니다.

 

# -*- coding: utf-8 -*-
import random
import torch


class DynamicNet(torch.nn.Module):
    def __init__(self, D_in, H, D_out):
        """
        생성자에서 순전파 단계에서 사용할 3개의 nn.Linear 인스턴스를 생성합니다.
        """
        super(DynamicNet, self).__init__()
        self.input_linear = torch.nn.Linear(D_in, H)
        self.middle_linear = torch.nn.Linear(H, H)
        self.output_linear = torch.nn.Linear(H, D_out)
        
    def forward(self, x):
        """
        모델의 순전파 단계에서, 무작위로 0, 1, 2 또는 3 중에 하나를 선택하고
        은닉층을 계산하기 위해 여러번 사용한 middle_linear Module을 재사용합니다.

        각 순전파 단계는 동적 연산 그래프를 구성하기 때문에, 모델의 순전파 단계를
        정의할 때 반복문이나 조건문과 같은 일반적인 Python 제어 흐름 연산자를 사용할
        수 있습니다.

        여기에서 연산 그래프를 정의할 때 동일 Module을 여러번 재사용하는 것이
        완벽히 안전하다는 것을 알 수 있습니다. 이것이 각 Module을 한 번씩만 사용할
        수 있었던 Lua Torch보다 크게 개선된 부분입니다.
        """
        h_relu = self.input_linear(x).clamp(min=0)
        for _ in range(random.randint(0,3)):
            h_relu = self.middle_linear(h_relu).clamp(min=0)
        y_pred = self.output_linear(h_relu)
        return y_pred
        
# N은 배치 크기이며, D_in은 입력의 차원입니다;
# H는 은닉층의 차원이며, D_out은 출력 차원입니다.
N, D_in, H, D_out = 64, 1000, 100, 10

# 입력과 출력을 저장하기 위해 무작위 값을 갖는 Tensor를 생성합니다.
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# 앞서 정의한 클래스를 생성(instantiating)하여 모델을 구성합니다.
model = DynamicNet(D_in, H, D_out)

# 손실함수와 Optimizer를 만듭니다. 이 이상한 모델을 순수한 확률적 경사 하강법
# (stochastic gradient decent)으로 학습하는 것은 어려우므로, 모멘텀(momentum)을
# 사용합니다.
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500):
    # 순전파 단계: 모델에 x를 전달하여 예상되는 y 값을 계산합니다.
    y_pred = model(x)

    # 손실을 계산하고 출력합니다.
    loss = criterion(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())

    # 변화도를 0으로 만들고, 역전파 단계를 수행하고, 가중치를 갱신합니다.
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
반응형