Learning PyTorch with Examples


파이토치 기본 컨셉을 소개하는 튜토리얼이다


파이토치가 제공하는 2가지 메인 특징:


n-차원 텐서는 넘파이와 비슷하지만 GPU들에서 돌릴 수 있다

신경망 형성과 학습에서 자동으로 differentiation


fully-connected ReLU 네트워크를 예시로 사용할 것이며


1개의 히든레이어가 있고, gradient descent 로 학습하여 임의의 데이터를 정답 결과와의 유클리안 거리를 최소화하는 방법으로 학습할 것



Tensors


파이토치에 앞서, 넘파이를 이용한 네트워크에 대한 내용


넘파이는 n차원 배열 오브젝트로, 이런 배열들을 계산하는 함수를 제공함 딥러닝이나 기울기 등에 대한 직접적인 것을 포함하진 않지만


넘파이 계산들을 이용하여 2개 레이어의 네트워크를 만들 수 있음


import numpy as np

N, D_in, H, D_out = 64, 1000, 100, 10

x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)

w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)

learning_rate = 1e-6

for t in range(500):
#forward pass: predicted y 계산
h = x.dot(w1) #dot product? maybe
h_relu = np.maximum(h, 0) #normal ReLU
y_pred = h_relu.dot(w2)

#Compute and pritn loss
loss = np.square(y_pred - y).sum()
print(t, loss)

#backprop to compute gradients of w1 and w2 with respect to loss
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.T.dot(grad_y_pred)
grad_h_relu = grad_y_pred.dot(w2.T)
grad_h = grad_h_relu.copy()
grad_h[h < 0] = 0
grad_w1 = x.T.dot(grad_h)

#update weights
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2




PyTorch: Tensors

넘파이는 좋은 프레임웤이지만 GPU를 사용할 수 없음 최근 딥러닝 네트워크들은 GPU 사용시 50배 또는 그 이상 빨라지기도 하기때문에 넘파이 ㄴㄴ


파이토치 텐서는 넘파이랑 같음. n차원 배열이며 딥러닝에 대한 건 아니지만 배열에 대한 계산도 있고 쉽게 구현가능



하지만 넘파이와 달리 GPU를 이용하여 가속화할 수 있음 새로운 변수타입으로 cast하기만 하면 됨


다음은 텐서를 이용하여 2개 레이어 네트워크의 임의 데이터에 대한 학습을 하는 코드임


import torch

dtype = torch.float
device = torch.device("cpu")
# dtype = torch.device("cuda:0") #GPU로 돌리려면 주석 해제

# N은 배치사이즈 D_in은 인풋 차원
# H는 히든 디멘션 D_out은 아웃풋 차원
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random input and output data
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Randomly initialize weights
w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)

learning_rate = 1e-6
for t in range(500):
# Forward pass: compute predicted y
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)

# Compute and print loss
loss = (y_pred - y).pow(2).sum().item()
print(t, loss)

# Backprop to compute gradients of w1 and w2 with respect to loss
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)

# Update weights using gradient descent
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2




Autograd

위 코드에서 수동적으로 네트워크 학습을 진행시켰는데 좀만 복잡해져도 힘들어짐. 


다행히도 automatic differentiation 을 이용해서 신경망 backward pass를 자동으로 계산할 수 있음 파이토치에서는 autograd 패키지를 이용


네트워크 정방향 진행하면 computational 그래프가 정의됨. 그래프의 노드는 텐서들이며 엣지는 아웃풋 텐서를 만드는 계산식. Backpropagation


시 기울기 계산하기가 쉬워짐



복잡한 소리처럼 들리지만, 사용하기엔 간단함


x 텐서 변수가 x.requires_grad=True라면 x.grad는 이 x의 기울기값을 담는 텐서임


아래 코드는 이전 코드에서 필요한부분만 autograd 패키지를 이용하는쪽으로 수정한 코드임:


import torch

dtype = torch.float
device = torch.device("cpu")
# dtype = torch.device("cuda:0") #GPU로 돌리려면 주석 해제

# N은 배치사이즈 D_in은 인풋 차원
# H는 히든 디멘션 D_out은 아웃풋 차원
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random input and output data
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Randomly initialize weights
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
# # Forward pass: compute predicted y
# h = x.mm(w1)
# h_relu = h.clamp(min=0)
# y_pred = h_relu.mm(w2)
y_pred = x.mm(w1).clamp(min=0).mm(w2)

# Compute and print loss
#loss = (y_pred - y).pow(2).sum().item()
loss = (y_pred - y).pow(2).sum()
print(t, loss.item())

# # Backprop to compute gradients of w1 and w2 with respect to loss
# grad_y_pred = 2.0 * (y_pred - y)
# grad_w2 = h_relu.t().mm(grad_y_pred)
# grad_h_relu = grad_y_pred.mm(w2.t())
# grad_h = grad_h_relu.clone()
# grad_h[h < 0] = 0
# grad_w1 = x.t().mm(grad_h)
loss.backward()

with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad

#수동으로 기울기를 0으로 다시 해줌
w1.grad.zero_()
w2.grad.zero_()

# # Update weights using gradient descent
# w1 -= learning_rate * grad_w1
# w2 -= learning_rate * grad_w2




PyTorch: Defining new autograd functions

각각의 오토그래드 기본 계산은 사실 텐서를 계산하는 두개의 함수이다. 


forward 함수는 인풋 텐서로부터 아웃풋 텐서를 계산한다.


backward 함수는 아웃풋 텐서의 기울기를 받아 인풋 텐서의 기울기를 계산한다.



torch.autograd.Function 의 서브클래스를 정의하고 forward와 backward 함수를 시행하여 우리는 우리만의 autograd 연산을 만들 수 있다. 


그리고 인스턴스를 생성하고 함수처럼 생성하여 우리의 새로운 연산을 이용해서 인풋데이터를 포함하는 텐서를 넘긴다



아래 예제에서 우리의 커스텀 오토그래드 함수를 정의하여 ReLU nonlinearity를 수행하고 2개 레이어 네트워크를 실행한다


# -*- coding: utf-8 -*-

import torch

class MyReLU(torch.autograd.Function):
"""
서브클래스를 생성하여 고유의 오토그래드 함수를 만듦
"""
@staticmethod
def forward(ctx, input):
"""
forward 패스에서 인풋이 담긴 텐서를 받아 아웃풋이 담긴 텐서를 반환한다. ctx는 context object이며 backward 계산을
위한 정보를 숨기는데 사용한다. ctx.save_for_backward를 이용해서 backward pass에 사용하기위해 임의의 오브젝트를 캐시
"""
ctx.save_for_backward(input)
return input.clamp(min=0)

@staticmethod
def backward(ctx, grad_output):
"""
backward 패스에서 아웃풋쪽 loss의 gradient를 담는 텐서를 받아 input쪽 기울기를 계산해야함
"""
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input < 0] = 0
return grad_input

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

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold input and outputs.
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Create random Tensors for weights.
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
# 우리가 만든 함수를 사용하기 위해 .apply를 사용함 우리는 이걸 relu라 부름
relu = MyReLU.apply

# forward 패스: y를 예상하여 계산함; 우리가 계산함
# ReLu 는 우리 고유 함수를 이용한것
y_pred = relu(x.mm(w1)).mm(w2)

# loss 계산하고 출력
loss = (y_pred - y).pow(2).sum()
print(t, loss.item())

loss.backward()

# gradient descent를 이용하여 weights 갱신
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad

w1.grad.zero_()
w2.grad.zero_()




TensorFlow: Static Graphs

파이토치는 텐서플로우와 매우 비슷함. 둘다 computational 그래프를 만들고, automatic differentiation 을 사용하여 기울기 계산함


가장 큰 차이점은 그래프가 텐서플로우는 static이고 파이토치는 dynamic



텐서플로우에서 그래프를 생성하고 이걸 계속 같은걸 쓰고 또 씀. 인풋데이터만 다르게 줘서말이지.


파이토치는 각각의 forward pass가 각각의 그래프를 생성함



static 그래프는 개발자가 optimize하는 측면에서 더 나이스함. 예를들어 프레임워크는 일부 그래프 연산을 효율을 위해 융합(fuse)시키거나


여러 GPU나 머신들에게 분배하는 전력을 쓸 수 있음. 만약 같은 그래프를 계속 반복해서 다시 사용한다면 이건 비용적으로 선행 optimization이


분할상환될 수 있음(이 부분 한정 직역)



static과 dynamic 그래프가 다른 것 중 하나는 control flow이다. 일부 모델들에서는 각각의 데이터 포인트에 따라 다른 계산을 하길 바랄 수도 있다.


예를들어 반복네트워크(Recurrent Network - RNN)에서 각각의 데이터포인트에서 시간 단계에 따라 다르기 때문에 펼쳐질 수 있다


이 펼치는건 loop 반복으로 시행될 수 있다. static 그래프에서 반복되는 부분은 그래프의 일부여야 한다. 이러한 이유에서 텐서플로우에서는 tf.scan 같은 연산을 제공하여 그래프에 내장된 루프를 시행한다.


dynamic 그래프에선 더 간단하다. 각각의 예시에 대해 그래프를 생성하기때문에 일반적인 긴요한 플로우 컨트롤을 사용하여 각각의 인풋에 맞게


연산을 할 수 있다.



이전의 autograd 예시와 비교하기 위해 여기서는 텐서플로우를 이용하여 2개 레이어 네트워크를 학습하였다


# -*- coding: utf-8 -*-
import tensorflow as tf
import numpy as np

# First we set up the computational graph:

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create placeholders for the input and target data; these will be filled
# with real data when we execute the graph.
x = tf.placeholder(tf.float32, shape=(None, D_in))
y = tf.placeholder(tf.float32, shape=(None, D_out))

# Create Variables for the weights and initialize them with random data.
# A TensorFlow Variable persists its value across executions of the graph.
w1 = tf.Variable(tf.random_normal((D_in, H)))
w2 = tf.Variable(tf.random_normal((H, D_out)))

# Forward pass: Compute the predicted y using operations on TensorFlow Tensors.
# Note that this code does not actually perform any numeric operations; it
# merely sets up the computational graph that we will later execute.
h = tf.matmul(x, w1)
h_relu = tf.maximum(h, tf.zeros(1))
y_pred = tf.matmul(h_relu, w2)

# Compute loss using operations on TensorFlow Tensors
loss = tf.reduce_sum((y - y_pred) ** 2.0)

# Compute gradient of the loss with respect to w1 and w2.
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])

# Update the weights using gradient descent. To actually update the weights
# we need to evaluate new_w1 and new_w2 when executing the graph. Note that
# in TensorFlow the the act of updating the value of the weights is part of
# the computational graph; in PyTorch this happens outside the computational
# graph.
learning_rate = 1e-6
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)

# Now we have built our computational graph, so we enter a TensorFlow session to
# actually execute the graph.
with tf.Session() as sess:
    # Run the graph once to initialize the Variables w1 and w2.
    sess.run(tf.global_variables_initializer())

    # Create numpy arrays holding the actual data for the inputs x and targets
    # y
    x_value = np.random.randn(N, D_in)
    y_value = np.random.randn(N, D_out)
    for _ in range(500):
        # Execute the graph many times. Each time it executes we want to bind
        # x_value to x and y_value to y, specified with the feed_dict argument.
        # Each time we execute the graph we want to compute the values for loss,
        # new_w1, and new_w2; the values of these Tensors are returned as numpy
        # arrays.
        loss_value, _, _ = sess.run([loss, new_w1, new_w2],
                                    feed_dict={x: x_value, y: y_value})
        print(loss_value)





nn module

Computational graph나 autograd는 복잡한 연산을 정의하고 자동으로 기울기를 구하기위해 매우 파워풀한 패러다임. 하지만


거대한 신경망에서 autograd는 약간 low-level일 수 있음



텐서플로우에서 Keras, TensorFlow-Slim, 등과 같은 패키지는 좀더 높은 레벨(higher-level)의 관념을 제공하여 신경망 만들때 더 유용함



파이토치에서는 nn 패키지가 같은 목적으로 이용됨. Modules의 집합을 정의하며 이건 신경망의 레이어에 해당함. 하나의 모듈은 인풋 텐서를 받아


아웃풋 텐서를 계산하지만 학습할 파라미터를 포함한 텐서같은 것을 내부 state로 가지고있기도 함


nn 패키지는 또한 신경망 학습 시 일반적으로 많이 사용하는 loss 함수들도 정의함



아래 예시는 2개 레이어 네트워크를 nn패키지를 이용하여 구성한것


import torch

N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# nn 패키지를 이용하여 레이어를 나열하여 모델을 만듬. nn.Sequential 은 다른 모듈을 포함하는 모듈이고
# 이를 적용하여 output을 얻어냄. 각각의 선형 모듈은 인풋으로부터 선형 함수를 이용하여 아웃풋을 계산
# 그리고 weight과 bias를 홀드
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)

# nn 패키지는 유명한 몇개의 loss 함수도 갖고있음
loss_fn = torch.nn.MSELoss(size_average=False)

learning_rate=1e-4
for t in range(500):
# x를 모델로 통과시켜서 y를 예측하여 게산함. 모듈 오브젝트는 __call__ 연산을 오버라이드하여 함수처럼 호출하게함
# 인풋과 아웃풋은 텐서임
y_pred = model(x)

# 로스 계산과 출력. 계산된 y와 실제 y를 포함하는 텐서들을 넘기면 계산해줌
loss = loss_fn(y_pred, y)
print(t, loss.item())

# 기울기 0으로 만들기 backward pass 하기 전에
model.zero_grad()

# backward pass: 모델의 학습가능한 모든 파라미터에 대해 기울기 계산하고, 각각 모듈(레이어)의 파라미터는 텐서에
#저장되어있음 requires_grad=True 속성을 호출하면 모든 기울기를 계산할것임
loss.backward()

# gradient descent 이용하여 weight들 업데이트함
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad




PyTorch: optim

파이토치의 optim 패키지는 optimization 알고리즘의 아이디어를 추상화하고 널리 사용되는 알고리즘을 제공한다.


아래 코드는 nn패키지로 모델을 정의하고 optim패키지가 제공하는 Adam 알고리즘으로 optimize 하는것이다


import torch

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Use the nn package to define our model and loss function.
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(size_average=False)

# optim 패키지의 Adam 알고리즘을 이용하여 optimize함. 다른 알고리즘도 많음
# Adam 알고리즘의 첫번째 인자는 어떤 텐서를 아담이 업데이트해야하는지 알려줌

learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
# Forward pass: compute predicted y by passing x to the model.
y_pred = model(x)

# Compute and print loss.
loss = loss_fn(y_pred, y)
print(t, loss.item())

# backward pass 하기 전에, 모든 기울기를 0으로 만들어야하는데 왜냐하면 .backward()가 호출이되면 기본값으로
# 기울기값들은 버퍼에 축적되기때문
optimizer.zero_grad()

# backward pass: 모델 파라미터에 따른 loss 기울기 계산
loss.backward()

#step 함수는 파라미터를 갱신함
optimizer.step()




PyTorch: Custom nn Modules

기존에 존재하는 모듈 외에 좀더 복잡한 모델을 원할 경우가 있을텐데 이때는 nn.Module이라는 서브클래스로 고유한 모듈을 만들 수 있다. 


import torch

class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
2개의 nn.Linear 모듈(레이어)를 초기화하고 멤버 변수 할당화
:param D_in:
:param H:
:param D_out:
"""
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)

def forward(self, x):
"""
인풋 텐서를 받아서 아웃풋 텐서를 반환해야함 생성자에 정의된 모듈이나 텐서의 임의 연산을 사용할 수 있음
:param x:
:return:
"""
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Construct our model by instantiating the class defined above
model = TwoLayerNet(D_in, H, D_out)

# Loss 함수와 Opimizer를 정의함. model.parameters()를 호출하여 SGD를 사용하면 모델의 레이어들의 학습해야할 파라미터가 넘겨짐
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
# forward pass: y 예상값을 계산함 x를 모델에 통과시켜서
y_pred = model(x)

loss = criterion(y_pred, y)
print(t, loss.item())

# 기울기 0, backward pass, weights 갱신
optimizer.zero_grad()
loss.backward()
optimizer.step()




PyTorch: Control Flow + Weight Sharing

dynamic 그래프와 weight 공유의 예로 상당히 이상한 모델을 돌려봅시다. fully-connected ReLU 네트워크이며 각각의 패스에서 1부터 4 사이의 숫자를 고르고 그 숫자만큼의(?) 히든레이어를 사용하며 같인 weights를 여러번 사용하여 가장 안쪽에있는 히든레이어를 계산함


루프를 돌리기 위해 일반적인 파이썬 플로우 컨트롤을 사용하며 forward pass 선언 시 같은 모듈을 여러번 사용함으로써 weights를 공유함


모듈 서브클래스를 이용하면 쉽게 시행할 수 있음



import random
import torch

class DynamicNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
생성자에서 3개 nn.Linear 인스턴스를 생성하여 forward pass에서 사용할것
:param D_in:
:param H:
:param D_out:
"""
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):
"""
forward 패스에서 0~3까지 골라 그 숫자만큼 중간 레이어를 반복하여 히든레이어를 계산함
각각의 pass가 그래프를 생성하므로 파이썬의 반복문이나 조건문 등의 일반적인 컨트롤 플로우 연산자를
사용하여 모델의 forward 패스를 구현 가능함

또한 computational graph를 생성하는데 있어 같은 모듈을 여러번 사용해도 안전함 LuaTorch 대비 가장 발전한 부분
:param x:
:return:
"""
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 is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Construct our model by instantiating the class defined above
model = DynamicNet(D_in, H, D_out)

# 이 이상한 모델을 vanilla stochastic gradient descent로 하기엔 너무 터프하니 momentum 이용
# SGD - stochastic gradient descent
# 최적화 관련 용어 : http://ikaros0909.tistory.com/3
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500):
# Forward pass: Compute predicted y by passing x to the model
y_pred = model(x)

# Compute and print loss
loss = criterion(y_pred, y)
print(t, loss.item())

# Zero gradients, perform a backward pass, and update the weights.
optimizer.zero_grad()
loss.backward()
optimizer.step()


+ Recent posts