Python/PyTorch 공부

[PyTorch] 2. 예제로 배우는 파이토치 - 자동미분(Autograd)

AI 꿈나무 2020. 12. 7. 14:47
반응형

공부 목적으로 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

자동 미분 - Autograd

PyTorch : 텐서와 자동미분

 이전 포스팅에서 신경망을 통과하는 순전파와 역전파를 수동으로 구현했습니다.

 작은 2계층 신경망에서 역전파를 수동으로 구현하는 것은 큰일은 아니지만 크고 복잡한 신경망에서는 매우 빠르게 아슬아슬해집니다.

 

 다행이도, 신경망에서 역전파의 계산을 자동화하기 위한 자동 미분을 사용합니다.

 PyTorch에서 autograd 패키지는 정확하게 이 기능을 제공합니다.

 자동미분을 사용할 때, 신경망의 순전파는 연산 그래프를 정의할 것입니다.

 그래프에서 node는 Tensor이고 edge는 입력 Tensor로부터 출력 Tensor를 생산하는 함수입니다.

 이 그래프를 통해 역전파하면 손쉽게 변화도를 계산할 수 있습니다.

 

 복잡하게 들리지만 이것은 실제로 사용하기 매우 간단합니다.

 각각의 Tensor는 연산 그래프에서 노드로 표현됩니다.

 만약 xx.requires_grad=True인 Tensor이면 x.grad는 어떤 scalar 값에 대한 x의 변화도를 갖고 있는 또다른 Tensor입니다.

 

 여기서 3차 polynomial를 sine 파동에 맞추는 것을 구현하기 위해 PyTorch Tensor와 자동미분을 사용하겠습니다.

 이제 더이상 신경망을 통과하는 역전파를 수동으로 구현할 필요가 없습니다.

 

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

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0")  # Uncomment this to run on GPU

# 입력과 추력을 갖기 위한 Tensor 생성하기
# 기본값으로 requires_grad=False를 합니다.
# 이는 역전파 동안 Tensor에 관한 변화도를 계산하지 않는다는 의미입니다.
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 가중치에 대한 무작위 Tensor를 생성합니다.
# 3차 polynomial에서 4개의 가중치가 필요합니다.
# y = a + b x + c x^2 + d x^3
# 역전파 동안 Tensor에 대한 변화도를 계산하는 것을 나타내는
# requires_grad=True로 설정합니다.
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(2000):
    # 순전파 : Tensor 연산을 사용해서 Y를 예측합니다.
    y_pred = a + b * x + c * x ** 2 + d * x ** 3
    
    # Tensor 연산을 사용해서 손실을 계산하고 출력합니다.
    # 이제 손실은 (1,) 형태의 Tensor입니다.
    # loss.item()은 손실이 갖고 있는 scalar 값을 얻습니다.
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())
        
    # 역전파를 계산하기 위해 자동미분을 사용합니다.
    # 이는 requires_grad=True인 모든 Tensor에 대한 변화도를 계산합니다.
    # 이것을 호출한 뒤에 a.grad, b.grad, c.grad, d.grad는 각각의 a,b,c,d에 대한
    # 손실의 변화도를 갖고 있는 Tensor입니다.
    loss.backward()
    
    # 경사 하강법을 사용해서 수동으로 가중치를 갱신합니다.
    # torch.no_grad()로 감싸는 이유는 가중치는 requires_True를 갖고 있지만,
    # 자동미분은 이것을 추적할 필요가 없기 때문입니다.
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad
        
        # 가중치를 갱신한 후에 수동으로 변화도를 0으로 만듭니다.
        a.grad.zero_()
        b.grad.zero_()
        c.grad.zero_()
        d.grad.zero_()
        
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')

 

새로운 자동미분 함수 정의하기 - Defining new autograd functions

 내부적으로, 각각의 기본 자동미분 연산은 Tensor를 조작하는 두 개의 함수입니다.

 순전파 함수는 입력 Tensor로부터 출력 Tensor를 계산합니다.

 역전파 함수는 어떤 scalar 값에 대한 출력 Tensor의 변화도를 얻고 같은 scalar값에 대한 입력 Tensor의 변화도를 계산합니다.

 

 PyTorch에서 torch.autograd.Function의 서브클래스를 정의하고 순전파와 역전파 함수를 구현함으로써 손쉽게 자동 미분 연산자를 정의할 수 있습니다.

 그리고 나서, 인스턴스(instance)를 구축함으로써 새로운 자동미분 연산을 사용할 수 있습니다.

 이것을 함수처럼 호출하면 입력 데이터를 지닌 Tensor를 전달합니다.

 

 이번 예제에서 모델을 y = a + b * P3(c + d * x)로 정의합니다.

 P3은 3차 Legender polynomial이며 0.5(5$x^3$-3x)로 정의됩니다.

 P3의 순전파와 역전파를 계산하는 사용자 정의 자동미분 함수를 호출하고 모델을 구현하기 위해 이것을 사용하겠습니다.

 

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


class LegendrePolynomial3(torch.autograd.Function):
    '''
    torch.autograd.Function을 상속받아 사용자 정의 자동미분 함수를 구현하고
    Tensor 연산을 하는 순전파와 역전파를 구현하겠습니다.
    '''
    
    @staticmethod
    def forward(ctx, input):
    '''
    순전파에서 입력을 지닌 Tensor를 받고 결과값을 지닌 Tensor를 반환합니다.
    ctx는 역전파를 위한 정보를 저장하기 위해 사용되는 컨텍스트 객체(context object) 입니다.
    ctx.save_for_backward를 사용하여 역전파단계에서 사용되는 속성 객체를 cache에 저장할 수 있습니다.
    '''
    ctx.save_for_backward(input)
    return 0.5 * (5 input ** 3 - 3 * input)
    
    @staticmethod
    def backward(ctx, grad_output):
    '''
    역전파 단계에서 결과값과 관련된 손실의 변화도를 지닌 Tensor를 받습니다.
    그리고 입력값과 관련된 손실의 변화도를 계산합니다.
    '''
    input, = ctx.saved_tensors
    return grad_output * 1.5 * (5 * input ** 2 - 1)
    
dtype = torch.float
device = torch.device('cpu')

# 입력과 출력을 저장하기 위한 Tensor를 생성합니다.
# 기본값으로 requries_grad=False로 설정합니다.
# 이는 역전파 단계 동안 이 Tensor에 관련된 변화도를 계산할 필요가 없다는 의미입니다.
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 가중치에 대한 무작위 Tensor를 생성합니다.
# 이 예제에서 4개의 가중치가 필요합니다. y = a + b * P3(c + d * x)
# 이 가중치들은 수렴하기 위한 올바른 결과로부터 너무 멀리 떨어지지 않게 초기화 해야 합니다.
# requires_grad=True로 설정합니다.
# 이는 역전파 단계에서 이 Tensor에 관련된 변화도를 계산하겠다는 의미입니다.
a = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)
c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)

learning_rate = 5e-6
for t in range(2000):
    # 함수를 적용하기 위해 Function.apply 방법을 사용합니다.
    # 이것을 'P3'이라는 변수명을 사용합니다.
    P3 = LegendrePolynomial3.apply
    
    # 순전파 : 연산을 사용해서 예측된 y를 계산합니다.
    # 자동미분 연산을 사용해서 P3을 계산합니다.
    y_pred = a + b * P3(c + d * x)
    
    # 손실을 계싼하고 출력합니다.
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())
        
    # 역전파를 계산하기 위해 자동미분을 사용합니다.
    loss.backward()
    
    # 경사 하강법을 사용해서 가중치를 갱신합니다.
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad

        # 가중치를 갱신하고 수동으로 변화도를 0으로 만듭니다.
        a.grad = None
        b.grad = None
        c.grad = None
        d.grad = None

print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
반응형