seminar

키워드

  • 텐서(tensor)
  • Design Simple Regression
  • Torch.Autograd
  • nn.Linear()

도입말

PyTorch 세미나는 PyTorch 공식 튜토리얼을 바탕으로 진행됩니다. PyTorch의 공식 튜토리얼은 다양한 예제와 PyTorch의 아키텍처에 대해서도 깊게 다루기 때문에 딥러닝 개발자라면 시간을 들여 내용 전체를 읽어보는 것을 추천드립니다. (공식 튜토리얼 리딩은 꾸준히 HW로 나갈 예정입니다.)


Tensor

PyTorch에서 텐서(tensor)는 n차원의 데이터를 저장하는 자료형이다. 아래와 같이 선언 및 초기화 할 수 있다.

data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)

또는 numpy 배열로도 가능하다.

np_array = np.array(data)
x_np = torch.from_numpy(np_array)

텐서를 생성하면 .shape, .dtype을 통해 차원과 데이터타입을 알 수 있다.

랜덤 텐서나 0 또는 1로 초기화된 텐서는 rand(), ones(), zeros() 함수로 생성할 수 있다.

shape = (2, 3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

.device는 텐서에서 체크해야 할 중요한 속성이다. 텐서를 어디에서 처리하는지에 대한 정보를 담고 있다.

x_data.device
# 출력
device(type='cpu')

텐서를 그냥 생성했다면 cpu device에 배정된다. gpu로 처리하기 위해서는 .to('cuda')를 통해 텐서를 gpu로 옮겨줘야 한다. 코랩에서 작업하고 있다면 “런타임 → 런타임 유형 변경 → GPU”로 설정해줘야 코랩의 GPU를 쓸 수 있다.

# We move our tensor to the GPU if available
if torch.cuda.is_available():
  x_data = x_data.to('cuda')
  print(f"Device tensor is stored on: {x_data.device}")
# 출력
Device tensor is stored on: cuda:0

PyTorch의 텐서는 Numpy 배열의 연산들(.mul(), *, matmul(), @)을 쓸 수 있다. 이 내용은 너무 쉬워서 패스하겠다.


In-place operations

텐서 연산의 뒤에 언더바(_)를 붙이면 in-place 연산이 된다.

x_data.t_()
x_data.copy_(y_data)
x_data.add_(5)

PyTorch 코드에 종종 등장하기에 그렇구나 하고 넘어가면 된다. 그러나 아래와 같은 이슈가 있기 때문에 권장하지는 않는다.

In-place operations save some memory, but can be problematic when computing derivatives because of an immediate loss of history. Hence, their use is discouraged.


Design Simple Regression

PyTorch 텐서로 간단한 회귀 모델을 만들어 보자. 먼저 데이터를 생성하자.

X = np.arange(100)
y = 2 * X + 1

print(X, y)

그리고 이걸 Tensor로 바꾼다.

X_data = torch.tensor(X)
y_data = torch.tensor(y)
print(X_data, y_data)

우리는 $y = w * x + b$ 꼴의 선형 회귀 모델을 만들 것이므로 아래와 같이 W, b 텐서도 생성한다. 이들은 모델의 파라미터(parameter)로 back-prop의 대상이 된다. 어떤 값으로 초기화 하든 상관 없으니 일단은 ones()로 초기화하자.

W = torch.ones(1)
b = torch.ones(1)
print(W, b)

그리고 모델을 코드로 표현한다. y_pred에 결과를 담자.

y_pred = W * X_data + b
y_pred

회귀 문제를 풀기 위해 loss를 정의한다.

loss = ((y_pred - y_data)**2).sum()
print(loss)

자! 여기까지 했으면 이제 back-prop를 수행하면 된다! 👏 그런데 back-prop을 어떻게 할까? computational graph를 구현해야 하나? 미분해를 구해야 하나? 걱정하지 마라 PyTorch에서 back-prop을 다 구현해뒀다. 그것이 바로 Torch.autograd이다.

Torch.autograd

이번 문단에서 다루는 autograd는 딥러닝의 Back-propagation을 코드로 구현한 PyTorch의 기능이다. back-propagation을 제대로 이해했다면 이 부분도 쉽게 이해할 수 있다.

완벽하게 알지 못해도 쓸 수는 있다. - 생활코딩

autograd에 대해 자세히 다루기 전에 앞의 회귀 문제를 먼저 해결하자.

먼저 위의 코드에서 W, b를 정의한 부분을 아래의 코드로 다시 쓰자.

W = torch.ones(1, requires_grad=True)
b = torch.ones(1, requires_grad=True)
print(W, b)

변수가 다시 정의되었으니 아래의 y_pred, loss도 다시 한번 계산한다.

y_pred = W * X_data + b
loss = ((y_pred - y_data)**2).sum()
print(loss)

loss의 출력을 보면 이전과는 다르게 grad_fn=<SumBackward0>이 생겼다! 궁금증은 뒤로 하고 계속 나아가자.

loss 변수에 .backward()함수를 호출하면 back-prop이 실행된다. 너무 간단한데?

loss.backward()

호출 후에 파라미터 W, b를 다시 살펴보자. .grad 값을 보면 어떤 값이 있는데, 이것이 parameter W를 갱신하는 gradient dW이다.

이제 GD의 방식대로 W, b를 갱신하면, learning rate $\eta$는 eta = 1e-6으로 설정하자.

eta = 1e-6
W = W - eta * W.grad
b = b - eta * b.grad

이제 이 과정을 반복하는 코드를 짜보자.

W = torch.ones(1)
b = torch.ones(1)
eta = 1e-6

for _ in range(10):
  W = torch.tensor(W, requires_grad = True)
  b = torch.tensor(b, requires_grad = True)
  y_pred = W * X_data + b
  loss = ((y_pred - y_data)**2).sum()
  print(loss)

  loss.backward()

  W = (W - eta * W.grad)
  b = (b - eta * b.grad)
UserWarning: To copy construct from a tensor, it is recommended to use 
sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), 
rather than torch.tensor(sourceTensor).

이런 Warn이 뜬다면 이대로 해줘도 좋은데 일단은 무시하고 넘어가자 😉

출력되는 loss 값을 확인하면 값이 점점 줄어들어 0에 수렴하는 것을 볼 수 있다 👏


자! 이제 Torch.Autograd란 무엇인지 살펴보자. 앞에서도 말했듯 Autograd는 back-prop을 구현한 기능이다. loss.backward() 호출 한번으로 back-prop을 계산할 수 있다.

이 기능을 사용하려면 텐서에 requires_grad=True 옵션을 줘야 한다. 이것은 이 텐서를 사용하는 모든 계산을 트래킹 하는 Computational Graph를 만들라는 옵션을 주는 것과 같다. 이 옵션을 주지 않는다면 Autograd로 back-prop을 할 수 없다.

앞에서 loss 텐서를 출력했을 때, grad_fn=<SumBackward0>라는 속성이 추가된 것을 볼 수 있었다. 이것은 이 텐서가 유도한 함수가 .sum()임을 저장하는 속성이다. Autograd는 몇가지 기본 함수에 대한 gradient 식을 미리 계산해 가지고 있는 것이다.


Disabling Gradient Tracking

기본적으로 텐서 계산식에 requires_grad=True인 텐서가 있다면 자동으로 Computation Graph를 만들어 Gradient Tracking을 수행한다. 그러나 몇몇 경우1에서는 강제로 Gradient Tracking을 끌 필요도 있다.

1. detach()를 사용하라.

z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)

텐서에 detach()를 사용하면 해당 텐서에 대해서는 더이상 Gradient Tracking을 하지 않는다.

2. torch.no_grad()를 사용하라.

z = torch.matmul(x, w)+b
print(z.requires_grad)

with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)

torch..no_grad() 블록 내의 계산에 대해서는 Gradient Tracking을 하지 않는다. 보통 모델 학습 후 Demo run을 하는 코드에서 주로 사용한다. (나중에 다룰 예정이닷!)

nn.Linear

이번에는 다차원의 회귀 문제를 풀어보자! W 텐서를 (100, 5)의 shape으로 정의해서 앞의 과정을 그대로 해도 되기는 하는데 이번에는 nn.Linear()를 사용해서 회귀 문제를 풀어보자.

자! 일단 다차원 데이터부터 준비하자. 이번에는 GitHub에 있는 예시 데이터셋 50_Startups.csv를 사용할 것이다. 아래 명령어로 데이터셋을 colab으로 다운로드 할 수 있다.

!wget https://raw.githubusercontent.com/mahesh147/Multiple-Linear-Regression/master/50_Startups.csv

그리고 pandas를 이용해 csv 파일을 불러온다.

import pandas as pd

data = pd.read_csv('50_Startups.csv')
data.head()

X, y로 분리하자.

X = data[['R&D Spend', 'Administration', 'Marketing Spend']]
y = data[['Profit']]

print("X.shape", X.shape)
print("y.shape", y.shape)

이제 nn.Linear()를 사용 해보자. 아래와 같이 layer를 선언한다.

import torch.nn as nn

layer = nn.Linear(in_features=3, out_features=1)
print(layer)

nn.Linear()in_features, out_features 2가지 파라미터를 입력 받는다. 말그대로 피쳐 차원 수를 의미한다. 출력 결과를 보면 몇가지 정보를 더 확인할 수 있는데,

Linear(in_features=3, out_features=1, bias=True)

nn.Linear()에서는 알아서 bias 텀을 쓰도록 지정할 수 있다. 앞에서 W, b를 직접 선언해서 썼던 것과 다르게 좀더 편하게 layer를 만들 수 있다.

자! 이제 layer를 사용해보자.

X_data = torch.FloatTensor(X.values)
y_data = torch.FloatTensor(y.values)

layer(X_data)

이번에는 torch.FloatTensor()를 썼는데 데이터 타입을 맞춰주려고 사용했다. 별로 중요하지는 않다.

출력 결과를 보면 .grad_fn이 있는 것으로 보아 자동으로 Autograd가 적용된 것도 볼 수 있다.

이제 GD를 구축해보자.

X_data = torch.FloatTensor(X.values)
y_data = torch.FloatTensor(y.values)
layer = nn.Linear(in_features=3, out_features=1)

for _ in range(10):
  y_pred = layer(X_data)
  loss = ((y_pred - y_data)**2).sum()
  print(loss)

  loss.backward()

  eta = 1e-6
  with torch.no_grad():
    for p in layer.parameters():
        p.sub_(eta * p.grad)
        p.grad.zero_()

출력 결과를 보면 loss 값이 폭발🚀할 것이다. 이건 데이터를 정규화하지 않아서 인데 빠르게 정규화해보자.

from sklearn.preprocessing import MinMaxScaler

transformer = MinMaxScaler()
transformer.fit(X)
X = transformer.transform(X)

transformer = MinMaxScaler()
transformer.fit(y)
y = transformer.transform(y)
X_data = torch.FloatTensor(X)
y_data = torch.FloatTensor(y)
layer = nn.Linear(in_features=3, out_features=1)

for _ in range(10):
  y_pred = layer(X_data)
  loss = ((y_pred - y_data)**2).sum()
  print(loss)

  loss.backward()

  eta = 1e-2
  with torch.no_grad():
    for p in layer.parameters():
        p.sub_(eta * p.grad)
        p.grad.zero_()

자! 이것으로 nn.Linear()를 사용해 회귀 문제를 푸는 방법도 살펴봤다. 이 녀석은 앞으로 정말 자주 볼 예정이다! 👏


  1. Transfer Learning이라던가… fine-tuning이라던가…