Learning PyTorch with Examples (2) – Autograd
http://pytorch.org/tutorials/beginner/pytorch_with_examples.html#autograd
Learning PyTorch with Examples
Autograd
PyTorch: Variables and autograd
앞의 예제에서는 신경망의 순전파와 역전파 과정을 직접 작성하였습니다. 2층을 가지는 신경망이라 역전파 과정을 손으로 작성하는 것이 어렵지는 않았지만, 크고 복잡한 망이라면 짜는게 쉽지 않을 것입니다.
고맙게도 자동 미분 기능이 있어 역전파 과정을 자동으로 계산할 수 있습니다. PyTorch의 autograd 패키지가 바로 그 기능을 제공합니다. autograd를 사용하면, 순전파를 실행할 때 계산 그래프를 정의합니다. Tensor가 그래프의 노드가 될 것이고, 입력 Tensor를 받아 출력 Tensor를 내놓는 함수가 그래프의 에지가 될 것입니다. 이 그래프를 통해 역전파를 하면 쉽게 그래디언트를 계산할 수 있습니다.
설명만 보면 꽤 복잡해 보이지만, 실제 사용은 매우 쉽습니다. PyTorch Tensor를 Variable 객체로 감싸면 됩니다. Variable은 계산 그래프의 노드를 나타냅니다. x
가 Variable이라면, x.data
는 Tensor이고 x.grad
는 어떤 한 스칼라 값에 대한 x
의 그래디언트를 가지는 또다른 Variable입니다.
PyTorch Variable은 PyTorch Tensor와 동일한 API를 가집니다. Tensor에 적용할 수 있는 거의 모든 연산이 Variable에도 작동합니다. 다른 부분은 Variable을 사용할 경우 자동으로 그래디언트를 계산하기 위해 계산 그래프를 만든다는 것 뿐입니다.
이제 PyTorch Variable과 autograd를 이용한 2층 신경망을 보겠습니다. 이제 더 이상 역전파 과정을 손으로 구현할 필요가 없습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# -*- coding: utf-8 -*- import torch from torch.autograd import Variable dtype = torch.FloatTensor # dtype = torch.cuda.FloatTensor # Uncomment this to run on GPU # 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, and wrap them in Variables. # Setting requires_grad=False indicates that we do not need to compute gradients # with respect to these Variables during the backward pass. x = Variable(torch.randn(N, D_in).type(dtype), requires_grad=False) y = Variable(torch.randn(N, D_out).type(dtype), requires_grad=False) # Create random Tensors for weights, and wrap them in Variables. # Setting requires_grad=True indicates that we want to compute gradients with # respect to these Variables during the backward pass. w1 = Variable(torch.randn(D_in, H).type(dtype), requires_grad=True) w2 = Variable(torch.randn(H, D_out).type(dtype), requires_grad=True) learning_rate = 1e-6 for t in range(500): # Forward pass: compute predicted y using operations on Variables; these # are exactly the same operations we used to compute the forward pass using # Tensors, but we do not need to keep references to intermediate values since # we are not implementing the backward pass by hand. y_pred = x.mm(w1).clamp(min=0).mm(w2) # Compute and print loss using operations on Variables. # Now loss is a Variable of shape (1,) and loss.data is a Tensor of shape # (1,); loss.data[0] is a scalar value holding the loss. loss = (y_pred - y).pow(2).sum() print(t, loss.data[0]) # Use autograd to compute the backward pass. This call will compute the # gradient of loss with respect to all Variables with requires_grad=True. # After this call w1.grad and w2.grad will be Variables holding the gradient # of the loss with respect to w1 and w2 respectively. loss.backward() # Update weights using gradient descent; w1.data and w2.data are Tensors, # w1.grad and w2.grad are Variables and w1.grad.data and w2.grad.data are # Tensors. w1.data -= learning_rate * w1.grad.data w2.data -= learning_rate * w2.grad.data # Manually zero the gradients after updating weights w1.grad.data.zero_() w2.grad.data.zero_() |
PyTorch: Defining new autograd functions
PyTorch를 뜯어보면, autograd의 각 연산은 실제로 2개의 함수가 Tensor에서 작동하고 있습니다. forward 함수는 입력 Tensor로부터 출력 Tensor를 계산합니다. backward 함수는 출력 Tensor로부터 어떤 스칼라 값에 대한 그래디언트를 받고, 그 스칼라 값에 대한 입력 Tensor에 대한 그래디언트를 계산합니다.
PyTorch에서는 torch.autograd.Function
의 서브클래스를 정의하고 forward
와 backward
함수를 구현하기만 하면 원하는 autograd 연산을 쉽게 만들 수 있습니다. 이렇게 만들어진 새로운 autograd의 인스턴스를 만들고 함수를 부르는 것처럼 입력 데이터가 담긴 Variable를 넘겨주어 우리가 원하는 연산을 할 수 있습니다.
아래 예제에서는 사용자 정의 ReLU nonlinearity를 계산하는 autograd 함수를 정의하고, 그걸 2층 신경망에 사용해보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# -*- coding: utf-8 -*- import torch from torch.autograd import Variable class MyReLU(torch.autograd.Function): """ We can implement our own custom autograd Functions by subclassing torch.autograd.Function and implementing the forward and backward passes which operate on Tensors. """ @staticmethod def forward(ctx, input): """ In the forward pass we receive a Tensor containing the input and return a Tensor containing the output. ctx is a context object that can be used to stash information for backward computation. You can cache arbitrary objects for use in the backward pass using the ctx.save_for_backward method. """ ctx.save_for_backward(input) return input.clamp(min=0) @staticmethod def backward(ctx, grad_output): """ In the backward pass we receive a Tensor containing the gradient of the loss with respect to the output, and we need to compute the gradient of the loss with respect to the input. """ input, = ctx.saved_tensors grad_input = grad_output.clone() grad_input[input < 0] = 0 return grad_input dtype = torch.FloatTensor # dtype = torch.cuda.FloatTensor # Uncomment this to run on GPU # 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, and wrap them in Variables. x = Variable(torch.randn(N, D_in).type(dtype), requires_grad=False) y = Variable(torch.randn(N, D_out).type(dtype), requires_grad=False) # Create random Tensors for weights, and wrap them in Variables. w1 = Variable(torch.randn(D_in, H).type(dtype), requires_grad=True) w2 = Variable(torch.randn(H, D_out).type(dtype), requires_grad=True) learning_rate = 1e-6 for t in range(500): # To apply our Function, we use Function.apply method. We alias this as 'relu'. relu = MyReLU.apply # Forward pass: compute predicted y using operations on Variables; we compute # ReLU using our custom autograd operation. y_pred = relu(x.mm(w1)).mm(w2) # Compute and print loss loss = (y_pred - y).pow(2).sum() print(t, loss.data[0]) # Use autograd to compute the backward pass. loss.backward() # Update weights using gradient descent w1.data -= learning_rate * w1.grad.data w2.data -= learning_rate * w2.grad.data # Manually zero the gradients after updating weights w1.grad.data.zero_() w2.grad.data.zero_() |
TensorFlow: Static Graphs
PyTorch의 autograd는 TensorFlow와 꽤 비슷합니다. 두 프레임워크 모두 계산 그래프를 정의하고, 자동으로 그래디언트를 계산하는 기능이 들어가 있습니다. 둘의 가장 큰 차이는 TensorFlow의 계산 그래프는 정적이고 PyTorch는 동적이라는 점입니다.
TensorFlow에서는 계산 그래프를 한번 정의하고 나면 그래프에 들어가는 입력 데이터만 다르게 할 수 있을 뿐이고 같은 그래프만을 실행할 수 있습니다. 하지만 PyTorch는 각 순전파마다 새로운 계산 그래프를 정의하여 이용합니다.
정적 그래프는 그래프의 초기 부분을 최적화 할 때에는 좋습니다. 예를 들어, 프레임워크는 효율성을 위해서 몇개의 그래프 연산을 조합하거나, 그래프를 여러 GPU나 컴퓨터로 나누어 실행할고자할 때가 있습니다. 만약 어떤 그래프를 계속적으로 사용해야 할 경우, 이에 따른 연산 부담이 가중될 수 있는데, 같은 그래프를 계속 재실행할 경우 이런 부담을 낮출 수 있습니다.
정적 그래프와 동적 그래프의 다른 한 차이는 제어 흐름입니다. 어떤 모델에서는 각 데이터 포인트마다 다른 계산을 하고 싶을 수 있습니다. 예를 들어 recurrent network는 각 데이터 포인트에대해 서로 다른 여러 타임 스텝에 대해서 언롤(unroll)됩니다. 이 언롤링은 루프로 구현되게 되는데, 정적 그래프를 사용하면 이 루프 자체가 그래프의 한 부분으로 구현되어 있어야 합니다. 이런 이유로 TensorFlow는 tf.scan
와 같은 연산으로 그래프 내에 루프를 집어 넣습니다. 동적 그래프를 사용하면 이런 상황이 매우 간단해집니다. 각 상황에 따라 그래프를 on-the-fly로 다시 만들기 때문에, 각 입력에 따라 달라지는 계산을 평범한 명령형 흐름 제어(imperative flow control)을 이용할 수 있습니다.
위에서 PyTorch autograd를 사용한 것과 아래의 TensorFlow로 작성한 코드를 비교해봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# -*- 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) |