PyTorch

7 minute read

0. PyTorch 설치하기

PyTorch 설치 방법은 https://pytorch.org/get-started/locally/에서 안내받을 수 있다. 자신에게 맞는 조건들을 선택한 후 나타나는 명령어로 설치를 진행한다. GPU 없이 CPU만을 사용한다면 CUDA는 None으로 선택해도 된다.

1. 수를 표현하는 방식

Rank(차원) 이름 예시
0 scalar 1,2,3,4
1 vector [1,2,3,4]
2 matrix [[1,2],[3,4]]
3 tensor [[[1,2]]]

cf) shape

  • image
    (4, 1, 5, 5) = (Sample, Channel, Width, Height)
  • 시계열
    (4, 10, 16) = (Sample, Seq_len, Feature_Dim)

2. PyTorch에서의 Array

PyTorch에서 Array는 numpy에서 사용하는 것과 유사하게 사용할 수 있고 N차원 표현이 가능하다.
PyTorch에서는 FloatTensor와 LongTensor를 자주 사용하게 되며, 입력 X는 주로 FloatTensor로, 정답 Y는 주로 LongTensor로 표현한다.

t = torch.FloatTensor([0., 1., 2., 3., 4., 5., 6.])
print(type(t), t, t.dtype)
# <class 'torch.Tensor'> tensor([0., 1., 2., 3., 4., 5., 6.]) torch.float32

m = torch.LongTensor([1, 2, 3])
print(type(m), m, m.dtype)
# <class 'torch.Tensor'> tensor([1, 2, 3]) torch.int64

numpy와 유사한 활용법

  • .dim() : 차원 확인
  • .shape.size() : shape 확인
  • 인덱싱, 슬라이싱, 브로드캐스팅
  • .mean() : 평균 구하기. integer는 불가
  • .sum() : 합계
  • .max() : dimension이 지정되면 max와 argmax(인덱스)를 모두 반환
  • .cat() : Concatenation
  • .stack() : Stacking

numpy에서 Tensor로, Tensor에서 numpy로 변환할 수도 있다.

# numpy --> Tensor
k = np.array([0., 1., 2., 3., 4., 5., 6.])

t = torch.FloatTensor(k)

# Tensor --> numpy
t.numpy()

3. View

view는 numpy에서의 reshape과 유사하다. 다만, 실제 내부 동작방식에서는 차이가 있는데 view는 바뀐 것처럼 보여지게 하는 것이다.

t = np.array([[[0, 1, 2],
               [3, 4, 5]],

              [[6, 7, 8],
               [9, 10, 11]]])
ft = torch.FloatTensor(t)
print(ft.shape) # torch.Size([2, 2, 3])
print(ft.view([-1, 3]).shape) # torch.Size([4, 3])

4. Squeeze & Unsqueeze

squeeze와 unsqueeze를 사용해 차원을 줄이고 늘릴 수 있다.

Squeeze

  • (1, 2, 3) –> (2, 3)
  • (2, 1, 3) –> (2, 3)
  • (2, 3, 1) –> (2, 3)
  • (3, 1) –> (3,)
ft = torch.FloatTensor([[0], [1], [2]])
print(ft.shape) # torch.Size([3, 1])
print(ft.squeeze().shape) # torch.Size([3])

Unsqueeze

  • .unsqueeze(0) : (3,) –> (1, 3) / (2, 3) –> (1, 2, 3)
  • .unsqueeze(1) : (3,) –> (3, 1) / (2, 3) –> (2, 1, 3)
  • .unsqueeze(-1) : (3,) –> (3, 1) / (2, 3) –> (2, 3, 1)
ft = torch.Tensor([0, 1, 2])
print(ft.shape) # torch.Size([3])
print(ft.unsqueeze(0).shape) # torch.Size([1, 3])

5. 딥러닝 학습 (Linear Regression)

딥러닝에서 학습을 한다는 것은 X, Y가 주어졌을 때 Y = f(WX + b)에서 W와 b를 스스로 찾아내는 것이다.

딥러닝에서의 실제 구현은 Y = W*X + b가 아닌 Y = X*W + b (*는 Matrix Multiplication)로 되어 있으며, 두 가지 경우의 결과는 다르다.

Matrix Multiplication

Y = f(X.matmul(W) + b)

  • f: 활성화함수
  • X: 입력
  • Y: 출력
  • W: 가중치

image

image

  • H(x): 주어진 x 값에 대해 예측을 어떻게 할 것인가
  • cost(W, b): H(x) 가 y 를 얼마나 잘 예측했는가
    • MSE (Mean Squared Error)

Regression에서는 MSE를, Classification에서는 Cross Entropy Loss를 사용한다.

이를 실제로 구현하면 다음과 같다.

hypothesis = x_train * W + b 
cost = torch.mean((hypothesis - y_train) ** 2)

샘플 데이터로 학습시켜보자. 실행하면 W는 점차 1에 가까워지고 b는 점점 줄어들어 0에 가까워지는 것을 확인할 수 있다.

# 데이터 : 후에 (Dataset, DataLoader)를 이용해서 만들게 됨
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[1], [2], [3]])
# 모델 초기화
W = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# optimizer 설정
optimizer = optim.SGD([W, b], lr=0.01) # backpropagation 과정 (내부에서 미분하는 과정)

nb_epochs = 1000 # Mini-batch
for epoch in range(nb_epochs + 1):
    
    # H(x) 계산
    hypothesis = x_train * W + b
    
    # cost 계산
    cost = torch.mean((hypothesis - y_train) ** 2)

    # cost로 H(x) 개선
    optimizer.zero_grad() # pytorch의 차이점: optimizer 내부 미분 값을 누적 저장하게 되어 있다. zero reset
    cost.backward() # 미분 값을 계산
    optimizer.step() # model parameter 업데이트: W(or b) = W(or b) -lr * grad

    # 100번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} W: {:.3f}, b: {:.3f} Cost: {:.6f}'.format(
            epoch, nb_epochs, W.item(), b.item(), cost.item()
        ))

nn.Module을 상속받아 만든 Linear Regression 모델을 적용해보자.

class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1) # Linear 안에 weight matrix, bias 있음

    def forward(self, x): # 필수 구현
        return self.linear(x) # (3, 1) * (1, 1) ==> (3, 1) # Matrix Multiplication 연산
# 데이터 : 후에 (Dataset, DataLoader)를 이용해서 만들게 됨
x_train = torch.FloatTensor([[1], [2], [3]])
y_train = torch.FloatTensor([[1], [2], [3]])
# 모델 초기화
model = LinearRegressionModel()
# optimizer 설정
optimizer = optim.SGD(model.parameters(), lr=0.01)

nb_epochs = 1000
for epoch in range(nb_epochs + 1):
    
    # H(x) 계산
    prediction = model(x_train)
    
    # cost 계산
    cost = F.mse_loss(prediction, y_train)
    
    # cost로 H(x) 개선
    optimizer.zero_grad()
    cost.backward()
    optimizer.step()
    
    # 100번마다 로그 출력
    if epoch % 100 == 0:
        params = list(model.parameters())
        W = params[0].item()
        b = params[1].item()
        print('Epoch {:4d}/{} W: {:.3f}, b: {:.3f} Cost: {:.6f}'.format(
            epoch, nb_epochs, W, b, cost.item()
        ))

다른 딥러닝 방법론을 사용할 수도 있으나, 이번 글에서는 간단하게만 언급한다.

  • Logistic Classification
    • hypothesis = torch.sigmoid(x_train.matmul(W) + b)
    • cost = F.binary_cross_entropy(hypothesis, y_train)
  • Softmax
    • hypothesis = F.softmax(x_train.matmul(W) + b, dim=1)
    • cost = F.cross_entropy(x_train.matmul(W) + b, y_train)
    • 출력의 결과물로 확률을 반환하며, 이 확률값을 모두 다 더하면 1이 된다.

6. 하이퍼 파라미터 Optimization

전문가가 직접 입력해줘야 하는 파라미터를 ‘하이퍼 파라미터’라고 하며, 최적의 하이퍼 파라미터를 찾는 것을 ‘하이퍼 파라미터 Optimization’이라고 한다.

  • 하이퍼 파라미터
    • LR
    • Layer 개수
    • Layer 히든 유닛의 개수
    • 최적 아키텍처

7. Overfitting

딥러닝은 모델링을 통해 수집한 과거데이터로 현재를 예측하는 과정이다. 이 때, 다음과 같은 가정을 하고 진행하게 된다.

  • 과거 데이터는 현재 또는 미래 데이터를 대표할 가능성이 있다.
  • 과거 데이터는 현재 또는 미래에 일어날 가능성이 높은 데이터일 수 있다.

하지만, 문제점이 존재한다.

  • 과거 데이터를 수집할 때는 미래의 서비스에 입력될 데이터의 특성을 포함하고 있어야 한다.
  • 과거 모든 데이터를 수집하더라도 미래의 데이터를 위해 완벽하게 준비해내는 것은 불가능하다.
  • 학습 데이터가 적으면 예측 데이터의 분포를 잘 대표하지 못하는 문제가 있다.

너무 학습데이터에 한해 잘 학습하여 테스트 데이터에서는 좋은 성능을 내지 못하는 경우를 Overfitting이라고 하는데, 이를 방지하기 위한 방법은 크게 다음의 세 가지로 볼 수 있다.

  • 더 많은 학습 데이터
  • 더 적은 양의 feature (패턴인식의 차원의 저주 방지)
  • Regularization

8. 순환신경망 (Recurrent Neural Network, RNN)

Basic

은닉층의 노드에서 활성화 함수를 통해 나온 결과값을 출력층 방향으로도 보내면서, 다시 은닉층 노드의 다음 계산의 입력으로 보내는 방식을 취한다.

t1: h -> e (==> t1_state)
t2: e -> l (==> t2_state)
t3: l -> l (==> t3_state)
t4: l -> o (==> t4_state)
import torch
import numpy as np

h = [1, 0, 0, 0]
e = [0, 1, 0, 0]
l = [0, 0, 1, 0]
o = [0, 0, 0, 1]
input_data_np = np.array([[h, e, l, l, o], [e, o, l, l, l], [l, l, e, e, l]], dtype=np.float32) 

input_data_np의 shape = (3, 5, 4) = (Batch_Size (Sample), Seq_Len (T), Featrue_Dim)

  • DNN 입력 데이터 Shape = (Batch_Size, Feature_Dim)
  • CNN 입력 데이터 Shape = (Batch_Size, Channel(Color), Width, Height)
  • RNN 입력 데이터 Shape = (Batch_Size, Seq_Len, Featrue_Dim)

Torch Tensor로 바꾸고, RNN을 선언한다.

input_size = 4
hidden_size = 2

input_data = torch.Tensor(input_data_np)
rnn = torch.nn.RNN(input_size, hidden_size)

결과를 확인해보자.

outputs, _status = rnn(input_data)
print(outputs)
print(_status) # 마지막 상태 정보만 기억

CharSeq

import torch
import torch.optim as optim
import numpy as np

sample = " if you want you"
char_set = list(set(sample))
char_dic = {c: i for i, c in enumerate(char_set)}

dic_size = len(char_dic) # out-layer의 인식해야하는 클래스(카테고리)의 개수
hidden_size = len(char_dic)
learning_rate = 0.1

sample_idx = [char_dic[c] for c in sample]
x_data = [sample_idx[:-1]]
x_one_hot = [np.eye(dic_size)[x] for x in x_data]
y_data = [sample_idx[1:]]

X = torch.FloatTensor(x_one_hot) # X = (Batch_Size, Seq_Len, Feature_Dim)
Y = torch.LongTensor(y_data) # Y = (BS, Seq_Len)

rnn = torch.nn.RNN(dic_size, hidden_size, batch_first=True)

criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.Adam(rnn.parameters(), learning_rate)

for i in range(50):
    optimizer.zero_grad()
    outputs, _ = rnn(X) 
    # 상태정보를 이용하지 않을 때에는 _status 대신 _를 사용
    # outputs.shape = (BS, Seq_Len, hidden_size(=10))
    loss = criterion(outputs.view(-1, dic_size), Y.view(-1))
    loss.backward()
    optimizer.step()

    result = outputs.data.numpy().argmax(axis=2)
    result_str = ''.join([char_set[c] for c in np.squeeze(result)])
    print(i, "loss: ", loss.item(), "prediction: ", result, "true Y: ", y_data, "prediction str: ", result_str)
  • DNN
    • outputs_shape = (BS, Output_Size)
    • Y_shape = (BS, )
  • RNN
    • outputs_shape = (BS, Seq_Len, Output_Size (Hidden_Size)) –> (BS*Seq_Len, Output_Size (Hidden_Size)) (.view()에서 일어남)
    • Y_shape = (BS, Seq_Len) –> (BS*Seq_Len, )

9. Padding vs. Packing

PyTorch 라이브러리의 다음 함수들을 사용해 Padding과 Packing을 쉽게 수행할 수 있다.

  • pad_sequence
  • pack_sequence
  • pack_padded_sequence
  • pad_packed_sequence
import torch
import numpy as np
from torch.nn.utils.rnn import pad_sequence, pack_sequence, pack_padded_sequence, pad_packed_sequence

data = ['hello world',
        'midnight',
        'calculation',
        'path',
        'short circuit']

char_set = ['<pad>'] + list(set(char for seq in data for char in seq)) # Get all characters and include pad token
char2idx = {char: idx for idx, char in enumerate(char_set)} # Constuct character to index dictionary
print('char_set:', char_set)
# char_set: ['<pad>', 'i', 'l', 'w', 'a', 'h', 'p', 'd', ' ', 'o', 'e', 'r', 'n', 't', 'c', 'u', 'm', 'g', 's']
print('char_set length:', len(char_set))
# char_set length: 19

X = [torch.LongTensor([char2idx[char] for char in seq]) for seq in data]
lengths = [len(seq) for seq in X] # [11, 8, 11, 4, 13]

(1) (Zero) Padding

속도가 빠르며, 일반적으로 가장 많이 쓰이는 방법이며, PaddedSequence는 sequence중에서 가장 긴 sequence와 길이를 맞추어주기 위해 padding을 추가한 일반적인 Tensor를 말한다. padding_value=0.0이 기본이지만, 원한다면 바꿀 수 있다.

padded_sequence = pad_sequence(X, batch_first=True)

(2) Packing

PyTorch에서는 PackedSequence를 통해 padding 없이도 정확히 필요한 부분까지 병렬 계산을 수행하게 만들 수 있다. 주의해야 할 점은, 주어지는 Tensor 리스트가 길이별 내림차순으로 정렬되어 있어야 한다는 것이다.

sorted_idx = sorted(range(len(lengths)), key=lengths.__getitem__, reverse=True)
sorted_X = [X[idx] for idx in sorted_idx]

packed_sequence = pack_sequence(sorted_X)
print(packed_sequence)
PackedSequence(data=tensor([ 8, 15, 14,  1, 12, 15, 10,  9,  2,  9, 17,  4,  4,  3,  7,  6,  4, 14,
         5, 15,  7, 17, 13,  2, 11, 11,  4, 18, 14, 16,  9, 15,  2, 17,  7,  7,
         6,  6,  2, 14,  4, 17, 13,  3,  5,  2,  7]), batch_sizes=tensor([5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 1, 1]))

t1, t2, t3, t4에는 5개, t5, t6, t7, t8에는 4개, t9, t10, t11에는 3개, t12, t13에는 1개를 가져와서 처리한다.

Leave a comment