AITech 학습정리-[DAY 17] basic of RNNs, LSTM, GRU
과거의 것들/AI Tech boostcamp

AITech 학습정리-[DAY 17] basic of RNNs, LSTM, GRU

===============================

학습내용

(3강) Recurrent Neural Network and Language Modeling

 

W는 t와 관계없이 어디에서나 같다. 입력인 xt와 ht-1 을 한 벡터로 연결하고 그걸 계산하는 W 로 생각할 수도 있고 나눠서 생각할 수도 있다. 현재 t 시간에서 출력값이 필요할 경우 W(hy)로 출력값 yt 만들 수 있음.

one to many의 경우 우린 지금까지 xt가 계속 있는걸로 배웠기 때문에 x에 입력값 0을 넣어주는 방식으로 한다.


이런식으로 피드백모델. 위에서 입력 "h"의 경우 target은 "e"지만 결과는 "o" 가 나온걸 알 수 있다. 그래서 loss func을 "e" 가 나오도록 잘 정의해야겠지.

모든 것을 하나의 loss에 두기엔 메모리가 부족하다. 그래서 저렇게 나눠서 학습하기도 한다.


이런식으로 기억 소자도 생긴다. 빨간색일수록 +1, 파란색일수록 -1이고 hidden state를 나타냄.

 

 


RNN 의 문제라면 계속 같은 W를 곱해주기 때문에 weight 값이 폭발하거나 사라지는 현상이 생긴다. 왜 그러나면 앞의 계수가 1 이 넘으면 계속 곱해주다 보니 폭발하고 1보다 작으면 사라진다. 위 그림에서 h3의 경우 h1에 대한 편미분을 하려고 보면 앞에 붙어있는 3을 계속 곱한걸 계산하게 되고 결과적으로 엄청커짐. 그래서 LSTM을 쓴다. 계수가 절대값 1 미만일때 밑의 그림을 보면 RNN은 금방 vanishing 되지만 LSTM은 오랫동안 살아있음.

즉 LSTM은 RNN의 문제인 vanishing과 explosion문제, 시간이 너무 오래 지나면 초기정보가 사라지는 문제를 보완하는 걸로 나왔다.


(4강) LSTM and GRU

 

xt와 ht-1을 받아 ht가 나온 vanilla RNN 과 달리 xt, Ct-1, ht-1 을 받아 Ct 와 ht를 내놓는다.

Ct에는 중요한 여러 정보를 가지고 있고 ht-1, xt와 결합해서 같이 계산한다. 

 


만약 x, h 둘다 크기가 h라고 하면, 두개 다 이어붙이기 때문에 W 열은 2h가 되고, 4개의 필터를 사용할 거라 4h 크기의 행을 가지게 된다. 각각을 h 크기로 나눠 필터로 사용할 거다. 그래서 필터는 Input gate, Forget gate, Output gate, Gate gate 4개가 있는데, i, f, o는 sigmoid를 사용해서 0~1의 값을 가지고 g는 tanh 를 사용해 -1~1의 값을 가진다. 그래서i, f, o는 얼마나 값을 가지고 갈지 배율을 통해 결정하고, g는 tanh를 사용하는게 RNN에서 선형 hidden state를 사용해 -1~1의 값을 가지게 해서 의미를 가져서 유의미한 정보를 가지게 해서 사용하는 것 같다.



 

Forget gate는 가지고 있던 ht-1에서 xt와 결합해 softmax로 0~1 값을 가지게 한다. 그래서 나온 gate 결과와 ht-1를 곱해서 나온 값으로 어느 값을 얼마나 잊어버릴 지 정한다.



어느 입력값을 강조해서 기억해야 할 것인지를 정한 it*Gtildat와 기존에 있던 것 중 어느정도 잊어버릴지 정한 ft*Ct-1을 더해서 Ct를 결정한다. 즉 잊어버릴건 잊어버리면서 현재 입력값과 전 ht-1으로 현재 입력을 어느정도 강조해서 기억할 것인지를 여기서 정하는 것 같다. 
 

output 이 필요하다면 계산된 Ct를 가지고 현재 계산을 내놓는다.

 
 

GRU는 위 LSTM의 단점인 메모리를 많이 먹는 것과 계산할게 많다는 점을 보완하기 위해 나온 것. Ct와 ht를 그냥 ht로 합치고 LSTM에서 Input gate와 Forget gate를 하나의 zt 값으로 만들어서 계산한다. 확실히 가볍고 빠르긴 한데 LSTM보다 잘 될때도 있고, 안될때도 있다고 한다.

 



 

 

실습

 

pack_padded_sequence, pad_packed_sequence 을 어떻게 쓰는지

language token 은 순차적으로 token을 만드는것. 원래 목적이 앞으로 어떤 단어를 input으로 넣을지 모르는데 다음 단어를 예측하는 거니까.

근데 여기서 본 force tagging 이나 entity recognition는 이미 모든 token을 알고 수행하는 것. rnn이 각각의 token을 알아서 넣어주는 것. 즉 rnn 자체가 rnn cell 하나를 문장 길이만큼 for문으로 호출하는 class가 수행하는 거라고 보면 된다. 근데 실제로 language model은 그렇지 않으니까 직접 for문을 돌아 각 rnn 셀에 한 단어씩 input을 넣어주고 그렇게 나온 결과를 다시 input으로 정해서 넣어주는 과정을 구현해줘야 함.

 

 

##3. RNN 1. 주어진 데이터를 RNN에 넣을 수 있는 형태로 만듭니다. 2. 기본적인 RNN 사용법 및 적용법을 익힙니다. 3. PackedSquence의 필요성에 대해 배우고 적용법을 실습합니다.

 
 
 
 
 

필요 패키지 import

 
 
 
 
[1]
 
 
 
from tqdm import tqdm
from torch import nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
 
import torch
 
 
 
 
 
 
 
 
 
 

데이터 전처리

 
 
 
 
 

아래의 sample data를 확인해봅시다.
전체 단어 수와 pad token의 id도 아래와 같습니다.

 
 
 
 
[2]
 
 
 
vocab_size = 100
pad_id = 0
 
data = [
  [85,14,80,34,99,20,31,65,53,86,3,58,30,4,11,6,50,71,74,13],
  [62,76,79,66,32],
  [93,77,16,67,46,74,24,70],
  [19,83,88,22,57,40,75,82,4,46],
  [70,28,30,24,76,84,92,76,77,51,7,20,82,94,57],
  [58,13,40,61,88,18,92,89,8,14,61,67,49,59,45,12,47,5],
  [22,5,21,84,39,6,9,84,36,59,32,30,69,70,82,56,1],
  [94,21,79,24,3,86],
  [80,80,33,63,34,63],
  [87,32,79,65,2,96,43,80,85,20,41,52,95,50,35,96,24,80]
]
 
 
 
 
 
 
 
 
 
 
 

Padding 처리를 해주면서 padding 전 길이도 저장합니다.

 
 
 
 
[3]
 
 
 
max_len = len(max(data, key=len))
print(f"Maximum sequence length: {max_len}")
 
valid_lens = []
for i, seq in enumerate(tqdm(data)):
  valid_lens.append(len(seq))
  if len(seq) < max_len:
    data[i] = seq + [pad_id] * (max_len - len(seq))
 
 
 
 
 
 
100%|██████████| 10/10 [00:00<00:00, 4905.62it/s]
Maximum sequence length: 20
 
 
 
 
[4]
 
 
 
print(data)
print(valid_lens)
 
 
 
 
 
[[85, 14, 80, 34, 99, 20, 31, 65, 53, 86, 3, 58, 30, 4, 11, 6, 50, 71, 74, 13], [62, 76, 79, 66, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [93, 77, 16, 67, 46, 74, 24, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [19, 83, 88, 22, 57, 40, 75, 82, 4, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [70, 28, 30, 24, 76, 84, 92, 76, 77, 51, 7, 20, 82, 94, 57, 0, 0, 0, 0, 0], [58, 13, 40, 61, 88, 18, 92, 89, 8, 14, 61, 67, 49, 59, 45, 12, 47, 5, 0, 0], [22, 5, 21, 84, 39, 6, 9, 84, 36, 59, 32, 30, 69, 70, 82, 56, 1, 0, 0, 0], [94, 21, 79, 24, 3, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [80, 80, 33, 63, 34, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [87, 32, 79, 65, 2, 96, 43, 80, 85, 20, 41, 52, 95, 50, 35, 96, 24, 80, 0, 0]] [20, 5, 8, 10, 15, 18, 17, 6, 6, 18]
 
 
 
 

위 데이터를 하나의 batch로 만들어 실습에 이용하겠습니다.

 
 
 
 
[5]
 
 
 
# B: batch size, L: maximum sequence length
batch = torch.LongTensor(data)  # (B, L)
batch_lens = torch.LongTensor(valid_lens)  # (B)
 
 
 
 
 
 
 
 
 
 
 

RNN 사용해보기

 
 
 
 
 

RNN에 넣기 전 word embedding을 위한 embedding layer를 만듭니다.

 
 
 
 
[6]
 
 
 
embedding_size = 256
embedding = nn.Embedding(vocab_size, embedding_size)
 
# d_w: embedding size
batch_emb = embedding(batch)  # (B, L, d_w)
 
 
 
 
 
 
 
 
 
 

아래와 같이 RNN 모델 및 초기 hidden state를 정의합니다.

 
 
 
 
[7]
 
 
 
hidden_size = 512  # RNN의 hidden size
num_layers = 1  # 쌓을 RNN layer의 개수
num_dirs = 1  # 1: 단방향 RNN, 2: 양방향 RNN
 
rnn = nn.RNN(
    input_size=embedding_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    bidirectional=True if num_dirs > 1 else False
)
 
h_0 = torch.zeros((num_layers * num_dirs, batch.shape[0], hidden_size))  # (num_layers * num_dirs, B, d_h)
 
 
 
 
 
 
 
 
 
 
 

RNN에 batch data를 넣으면 아래와 같이 2가지 output을 얻습니다.

  • hidden_states: 각 time step에 해당하는 hidden state들의 묶음.
  • h_n: 모든 sequence를 거치고 나온 마지막 hidden state.
 
 
 
 
[8]
 
 
 
hidden_states, h_n = rnn(batch_emb.transpose(0, 1), h_0)
# 우리는 위에서 (B, L, d_w) 로 정의했지만 원래 rnn 함수에 넣을 땐 (L, B, d_w) 로 넣어야 함.
# rnn을 처음 정의할 때 arg로 b_first=True 로 하면 자기가 알아서 transpose 해주지만 
# 지금은 그냥 해보자.
 
# d_h: hidden size, num_layers: layer 개수, num_dirs: 방향의 개수
print(hidden_states.shape)  # (L, B, d_h)
print(h_n.shape)  # (num_layers*num_dirs, B, d_h) = (1, B, d_h)
 
 
 
 
 
torch.Size([20, 10, 512]) torch.Size([1, 10, 512])
 
 
 
 

RNN 활용법

 
 
 
 
 

마지막 hidden state를 이용하여 text classification task에 적용할 수 있습니다.

 
 
 
 
[9]
 
 
 
num_classes = 2
classification_layer = nn.Linear(hidden_size, num_classes)
 
# C: number of classes
output = classification_layer(h_n.squeeze(0))  # (1, B, d_h) => (B, C)
print(output.shape)
 
 
 
 
 
torch.Size([10, 2])
 
 
 
 

각 time step에 대한 hidden state를 이용하여 token-level의 task를 수행할 수도 있습니다.

 
 
 
 
[10]
 
 
 
num_classes = 5
entity_layer = nn.Linear(hidden_size, num_classes)
 
# C: number of classes
output = entity_layer(hidden_states)  # (L, B, d_h) => (L, B, C)
print(output.shape)
 
 
 
 
 
 
torch.Size([20, 10, 5])
 
 
 
 

PackedSequence 사용법

 
 
 
 
 

앞서 padding 처리했던 데이터를 다시 확인해봅시다.

 
 
 
 
[11]
 
 
 
data
 
 
 
 
 
 
[[85, 14, 80, 34, 99, 20, 31, 65, 53, 86, 3, 58, 30, 4, 11, 6, 50, 71, 74, 13],
 [62, 76, 79, 66, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [93, 77, 16, 67, 46, 74, 24, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [19, 83, 88, 22, 57, 40, 75, 82, 4, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [70, 28, 30, 24, 76, 84, 92, 76, 77, 51, 7, 20, 82, 94, 57, 0, 0, 0, 0, 0],
 [58, 13, 40, 61, 88, 18, 92, 89, 8, 14, 61, 67, 49, 59, 45, 12, 47, 5, 0, 0],
 [22, 5, 21, 84, 39, 6, 9, 84, 36, 59, 32, 30, 69, 70, 82, 56, 1, 0, 0, 0],
 [94, 21, 79, 24, 3, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [80, 80, 33, 63, 34, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [87, 32, 79, 65, 2, 96, 43, 80, 85, 20, 41, 52, 95, 50, 35, 96, 24, 80, 0, 0]]
 
 
 
 

아래 그림과 같이 불필요한 pad 계산이 포함됩니다.

 
 
 
 
 

데이터를 padding전 원래 길이 기준으로 정렬합니다.

 
 
 
 
[12]
 
 
 
sorted_lens, sorted_idx = batch_lens.sort(descending=True)
sorted_batch = batch[sorted_idx]
 
print(sorted_batch)
print(sorted_lens)
 
 
 
 
 
 
tensor([[85, 14, 80, 34, 99, 20, 31, 65, 53, 86, 3, 58, 30, 4, 11, 6, 50, 71, 74, 13], [58, 13, 40, 61, 88, 18, 92, 89, 8, 14, 61, 67, 49, 59, 45, 12, 47, 5, 0, 0], [87, 32, 79, 65, 2, 96, 43, 80, 85, 20, 41, 52, 95, 50, 35, 96, 24, 80, 0, 0], [22, 5, 21, 84, 39, 6, 9, 84, 36, 59, 32, 30, 69, 70, 82, 56, 1, 0, 0, 0], [70, 28, 30, 24, 76, 84, 92, 76, 77, 51, 7, 20, 82, 94, 57, 0, 0, 0, 0, 0], [19, 83, 88, 22, 57, 40, 75, 82, 4, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [93, 77, 16, 67, 46, 74, 24, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [94, 21, 79, 24, 3, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [80, 80, 33, 63, 34, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [62, 76, 79, 66, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) tensor([20, 18, 18, 17, 15, 10, 8, 6, 6, 5])
 
 
 
 

아래와 같은 padding 무시 효과를 얻을 수 있습니다.

 
 
 
 
 

pack_padded_sequence를 이용하여 PackedSquence object를 사용합니다.

 
 
 
 
[13]
 
 
 
sorted_batch_emb = embedding(sorted_batch)
packed_batch = pack_padded_sequence(sorted_batch_emb.transpose(0, 1), sorted_lens)
 
print(packed_batch)
print(packed_batch[0].shape)
 
 
 
 
 
PackedSequence(data=tensor([[ 0.2625, -1.3331, 0.7854, ..., 1.8971, -0.2850, 0.9487], [ 0.6874, 0.2910, -0.0022, ..., -0.0657, -1.2294, 0.6583], [ 0.3701, -0.2177, 1.9384, ..., -0.7533, -0.5143, 0.4756], ..., [ 0.4872, -0.7720, 0.5177, ..., -0.8946, 0.3043, -0.3325], [ 0.1690, -0.4358, -0.4312, ..., 0.2710, -1.3817, -0.1278], [ 0.8931, 0.7309, 1.1370, ..., -0.0119, -1.9230, -1.4908]], grad_fn=<PackPaddedSequenceBackward>), batch_sizes=tensor([10, 10, 10, 10, 10, 9, 7, 7, 6, 6, 5, 5, 5, 5, 5, 4, 4, 3, 1, 1]), sorted_indices=None, unsorted_indices=None) torch.Size([123, 256])
 
 
 
[14]
 
 
 
packed_outputs, h_n = rnn(packed_batch, h_0)
 
print(packed_outputs)
print(packed_outputs[0].shape)
print(h_n.shape)
 
 
 
 
 
PackedSequence(data=tensor([[-0.5275, -0.2683, 0.2338, ..., 0.2864, 0.3521, -0.4516], [ 0.1298, 0.0473, 0.3081, ..., -0.3221, 0.0603, -0.5091], [-0.6747, -0.1912, 0.1777, ..., -0.1541, -0.2706, 0.0324], ..., [ 0.5282, 0.4590, 0.4602, ..., -0.0996, -0.3980, -0.6492], [-0.4430, -0.4270, -0.0434, ..., -0.5053, 0.1069, 0.3328], [-0.2188, -0.3435, 0.2749, ..., 0.0753, -0.1203, 0.5254]], grad_fn=<CatBackward>), batch_sizes=tensor([10, 10, 10, 10, 10, 9, 7, 7, 6, 6, 5, 5, 5, 5, 5, 4, 4, 3, 1, 1]), sorted_indices=None, unsorted_indices=None) torch.Size([123, 512]) torch.Size([1, 10, 512])
 
 
 
 

packed_output은 PackedSquence이므로 원래 output 형태와 다릅니다.
이를 다시 원래 형태로 바꿔주기 위해 pad_packed_sequence를 이용합니다.

 
 
 
[15]
 
 
 
outputs, outputs_lens = pad_packed_sequence(packed_outputs)
 
print(outputs.shape)  # (L, B, d_h)
print(outputs_lens)
 
 
 
 
 
torch.Size([20, 10, 512]) tensor([20, 18, 18, 17, 15, 10, 8, 6, 6, 5])

 

 


teacher forcing 은 맨 처음 학습할 땐 맞추는게 불가능에 가까운데 그렇게 틀린 결과를 다음 input에 넣고 또 틀린걸 다음 input에 넣고... 하니까 끝이 없고 그냥 개판난다. 그래서 처음엔 연결을 끊고 알맞게 출력했다고 가정하며 input으로 넣는 cheating을 해줌. 그 다음부터는 지가 어찌어찌 알아서 잘 해주겠지.
 
근데 지금은 이런거 없이 그냥 넣는거다.

 

 

양방향 layer는 이렇게 정방향, 역방향 두개 다 해서 각 hidden state를 concat을 해서 하나의 context로 쓰겠다.

 

##4. LSTM, GRU 1. 기존 RNN과 다른 부분에 대해서 배웁니다. 2. 이전 실습에 이어 다양한 적용법을 배웁니다.

 
 
 
 
 

필요 패키지 import

 
 
 
 
[1]
 
 
 
from tqdm import tqdm
from torch import nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
 
import torch
 
 
 
 
 
 
 
 
 
 

데이터 전처리

 
 
 
 
 

아래의 sample data를 확인해봅시다.
이전 실습과 동일합니다.

 
 
 
 
[2]
 
 
 
vocab_size = 100
pad_id = 0
 
data = [
  [85,14,80,34,99,20,31,65,53,86,3,58,30,4,11,6,50,71,74,13],
  [62,76,79,66,32],
  [93,77,16,67,46,74,24,70],
  [19,83,88,22,57,40,75,82,4,46],
  [70,28,30,24,76,84,92,76,77,51,7,20,82,94,57],
  [58,13,40,61,88,18,92,89,8,14,61,67,49,59,45,12,47,5],
  [22,5,21,84,39,6,9,84,36,59,32,30,69,70,82,56,1],
  [94,21,79,24,3,86],
  [80,80,33,63,34,63],
  [87,32,79,65,2,96,43,80,85,20,41,52,95,50,35,96,24,80]
]
 
 
 
 
 
 
 
 
 
 
[3]
 
 
 
max_len = len(max(data, key=len))
print(f"Maximum sequence length: {max_len}")
 
valid_lens = []
for i, seq in enumerate(tqdm(data)):
  valid_lens.append(len(seq))
  if len(seq) < max_len:
    data[i] = seq + [pad_id] * (max_len - len(seq))
 
 
 
 
 
 
100%|██████████| 10/10 [00:00<00:00, 50655.85it/s]
Maximum sequence length: 20
 
 
 
 
[4]
 
 
 
# B: batch size, L: maximum sequence length
batch = torch.LongTensor(data)  # (B, L)
batch_lens = torch.LongTensor(valid_lens)  # (B)
 
batch_lens, sorted_idx = batch_lens.sort(descending=True)
batch = batch[sorted_idx]
 
print(batch)
print(batch_lens)
 
 
 
 
 
 
tensor([[85, 14, 80, 34, 99, 20, 31, 65, 53, 86, 3, 58, 30, 4, 11, 6, 50, 71, 74, 13], [58, 13, 40, 61, 88, 18, 92, 89, 8, 14, 61, 67, 49, 59, 45, 12, 47, 5, 0, 0], [87, 32, 79, 65, 2, 96, 43, 80, 85, 20, 41, 52, 95, 50, 35, 96, 24, 80, 0, 0], [22, 5, 21, 84, 39, 6, 9, 84, 36, 59, 32, 30, 69, 70, 82, 56, 1, 0, 0, 0], [70, 28, 30, 24, 76, 84, 92, 76, 77, 51, 7, 20, 82, 94, 57, 0, 0, 0, 0, 0], [19, 83, 88, 22, 57, 40, 75, 82, 4, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [93, 77, 16, 67, 46, 74, 24, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [94, 21, 79, 24, 3, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [80, 80, 33, 63, 34, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [62, 76, 79, 66, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) tensor([20, 18, 18, 17, 15, 10, 8, 6, 6, 5])
 
 
 
 

LSTM 사용

 
 
 
 
 

LSTM에선 cell state가 추가됩니다.
Cell state의 shape는 hidden state의 그것과 동일합니다.

 
 
 
 
[5]
 
 
 
embedding_size = 256
hidden_size = 512
num_layers = 1
num_dirs = 1
 
embedding = nn.Embedding(vocab_size, embedding_size)
lstm = nn.LSTM(
    input_size=embedding_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    bidirectional=True if num_dirs > 1 else False
)
 
h_0 = torch.zeros((num_layers * num_dirs, batch.shape[0], hidden_size))  # (num_layers * num_dirs, B, d_h)
c_0 = torch.zeros((num_layers * num_dirs, batch.shape[0], hidden_size))  # (num_layers * num_dirs, B, d_h)
 
 
 
 
 
 
 
 
 
 
[6]
 
 
 
# d_w: word embedding size
batch_emb = embedding(batch)  # (B, L, d_w)
 
packed_batch = pack_padded_sequence(batch_emb.transpose(0, 1), batch_lens)
 
packed_outputs, (h_n, c_n) = lstm(packed_batch, (h_0, c_0))
print(packed_outputs)
print(packed_outputs[0].shape)
print(h_n.shape)
print(c_n.shape)
 
 
 
 
 
 
PackedSequence(data=tensor([[ 0.1090, -0.1299, 0.0556, ..., -0.0710, -0.0076, -0.0486], [-0.0112, 0.0427, -0.0232, ..., 0.0016, 0.0092, 0.0440], [ 0.0997, 0.0043, 0.1436, ..., 0.1419, 0.0427, 0.0734], ..., [-0.0553, -0.1232, 0.1145, ..., -0.0268, 0.0378, 0.2137], [ 0.2144, -0.0137, -0.1451, ..., 0.1717, 0.0581, 0.1913], [ 0.0496, 0.0183, 0.0408, ..., 0.1653, 0.0407, 0.2150]], grad_fn=<CatBackward>), batch_sizes=tensor([10, 10, 10, 10, 10, 9, 7, 7, 6, 6, 5, 5, 5, 5, 5, 4, 4, 3, 1, 1]), sorted_indices=None, unsorted_indices=None) torch.Size([123, 512]) torch.Size([1, 10, 512]) torch.Size([1, 10, 512])
 
 
 
[7]
 
 
 
outputs, output_lens = pad_packed_sequence(packed_outputs)
print(outputs.shape)
print(output_lens)
 
 
 
 
 
 
torch.Size([20, 10, 512]) tensor([20, 18, 18, 17, 15, 10, 8, 6, 6, 5])
 
 
 
 

GRU 사용

 
 
 
 
 

GRU는 cell state가 없어 RNN과 동일하게 사용 가능합니다.
GRU를 이용하여 LM task를 수행해봅시다.

 
 
 
 
[8]
 
 
 
gru = nn.GRU(
    input_size=embedding_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    bidirectional=True if num_dirs > 1 else False
)
 
 
 
 
 
 
 
 
 
 
[9]
 
 
 
output_layer = nn.Linear(hidden_size, vocab_size)
 
 
 
 
 
 
 
 
 
 
[10]
 
 
 
input_id = batch.transpose(0, 1)[0, :]  # (B)
hidden = torch.zeros((num_layers * num_dirs, batch.shape[0], hidden_size))  # (1, B, d_h)
 
 
 
 
 
 
 
 
 
 
 

Teacher forcing 없이 이전에 얻은 결과를 다음 input으로 이용합니다.

 
 
 
 
[11]
 
 
 
for t in range(max_len):
  input_emb = embedding(input_id).unsqueeze(0)  # (1, B, d_w)
  output, hidden = gru(input_emb, hidden)  # output: (1, B, d_h), hidden: (1, B, d_h)
 
  # V: vocab size
  output = output_layer(output)  # (1, B, V)
  probs, top_id = torch.max(output, dim=-1)  # probs: (1, B), top_id: (1, B)
 
  print("*" * 50)
  print(f"Time step: {t}")
  print(output.shape)
  print(probs.shape)
  print(top_id.shape)
 
  input_id = top_id.squeeze(0)  # (B)
 
 
 
 
 
************************************************** Time step: 0 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 1 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 2 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 3 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 4 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 5 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 6 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 7 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 8 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 9 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 10 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 11 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 12 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 13 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 14 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 15 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 16 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 17 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 18 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10]) ************************************************** Time step: 19 torch.Size([1, 10, 100]) torch.Size([1, 10]) torch.Size([1, 10])
 
 
 
 

max_len만큼의 for 문을 돌면서 모든 결과물의 모양을 확인했지만 만약 종료 조건(예를 들어 문장의 끝을 나타내는 end token 등)이 되면 중간에 생성을 그만둘 수도 있습니다.

 
 
 
 
 

양방향 및 여러 layer 사용

 
 
 
 
 

이번엔 양방향 + 2개 이상의 layer를 쓸 때 얻을 수 있는 결과에 대해 알아봅니다.

 
 
 
 
[12]
 
 
 
num_layers = 2
num_dirs = 2
dropout=0.1
 
gru = nn.GRU(
    input_size=embedding_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    dropout=dropout,
    bidirectional=True if num_dirs > 1 else False
)
 
 
 
 
 
 
 
 
 
 

Bidirectional이 되었고 layer의 개수가 22로 늘었기 때문에 hidden state의 shape도 (4, B, d_h)가 됩니다.

 
 
 
 
[13]
 
 
 
# d_w: word embedding size, num_layers: layer의 개수, num_dirs: 방향의 개수
batch_emb = embedding(batch)  # (B, L, d_w)
h_0 = torch.zeros((num_layers * num_dirs, batch.shape[0], hidden_size))  # (num_layers * num_dirs, B, d_h) 
= (4, B, d_h)
# 4가 무슨 의미냐면 1번째 layer의 forward direction, 1번째 layer의 backward direction,
# 2번째 layer의 forward direction, 2번째 layer의 backward direction
# 이 순으로 hidden state들이 붙어서 들어가있음.
# 그래서 사용방법은 똑같다.
 
packed_batch = pack_padded_sequence(batch_emb.transpose(0, 1), batch_lens)
 
packed_outputs, h_n = gru(packed_batch, h_0)
print(packed_outputs)
print(packed_outputs[0].shape)
print(h_n.shape)
 
 
 
 
 
PackedSequence(data=tensor([[-0.0293, -0.0791, 0.0114, ..., -0.1287, 0.1300, 0.0617], [-0.0713, 0.0404, 0.1020, ..., -0.0592, -0.0856, 0.2827], [-0.0472, 0.0160, -0.1003, ..., -0.1792, 0.0181, -0.0359], ..., [ 0.0696, 0.2049, -0.2546, ..., -0.0113, 0.0962, 0.0464], [ 0.0664, 0.1775, -0.0219, ..., 0.0920, 0.0142, 0.2569], [-0.0896, 0.2183, 0.1000, ..., 0.0058, 0.0432, 0.1318]], grad_fn=<CatBackward>), batch_sizes=tensor([10, 10, 10, 10, 10, 9, 7, 7, 6, 6, 5, 5, 5, 5, 5, 4, 4, 3, 1, 1]), sorted_indices=None, unsorted_indices=None) torch.Size([123, 1024]) torch.Size([4, 10, 512])
 
 
 
[14]
 
 
 
outputs, output_lens = pad_packed_sequence(packed_outputs)
 
print(outputs.shape)  # (L, B, num_dirs*d_h)
# 순방향으로 나온 hidden state와 역방향으로 나온 hidden state들이 붙었기 때문에
# *2 한게 나온다.
print(output_lens)
 
 
 
 
 
 
torch.Size([20, 10, 1024]) tensor([20, 18, 18, 17, 15, 10, 8, 6, 6, 5])
 
 
 
 

각각의 결과물의 shape는 다음과 같습니다.

outputs: (max_len, batch_size, num_dir * hidden_size)
h_n: (num_layers*num_dirs, batch_size, hidden_size)

 
 
 
[15]
 
 
 
batch_size = h_n.shape[1]
print(h_n.view(num_layers, num_dirs, batch_size, hidden_size))
print(h_n.view(num_layers, num_dirs, batch_size, hidden_size).shape)
 
 
 
 
 
tensor([[[[-0.3416, -0.0641, 0.1698, ..., -0.2974, -0.1405, -0.2110], [-0.3102, 0.2515, 0.0087, ..., 0.3465, -0.3247, -0.3052], [ 0.1320, -0.1247, -0.0655, ..., 0.3011, 0.6126, 0.2865], ..., [ 0.0639, -0.0599, -0.1331, ..., -0.1711, -0.3999, -0.5071], [-0.5416, -0.3079, 0.1389, ..., -0.1878, 0.0021, -0.4033], [ 0.3566, 0.0381, -0.2197, ..., -0.0346, 0.0959, -0.2303]], [[-0.3532, -0.3261, -0.1105, ..., -0.0232, -0.0691, -0.3212], [-0.3202, 0.0325, 0.0515, ..., -0.1766, -0.2185, 0.1328], [ 0.1408, 0.1922, -0.5685, ..., 0.2021, 0.3038, 0.0785], ..., [-0.1995, 0.0693, 0.1303, ..., 0.0381, 0.0419, 0.3044], [-0.0500, -0.2331, -0.4630, ..., 0.0698, 0.1738, -0.4147], [ 0.3631, 0.2410, -0.0755, ..., -0.0823, 0.5553, 0.1109]]], [[[-0.0896, 0.2183, 0.1000, ..., 0.1592, 0.1052, 0.1090], [ 0.0517, 0.0794, 0.1425, ..., -0.1243, -0.0613, -0.0130], [ 0.0696, 0.2049, -0.2546, ..., -0.1444, 0.1694, 0.0151], ..., [ 0.1095, 0.1303, -0.0909, ..., -0.1538, 0.1073, -0.1053], [ 0.2312, -0.0721, 0.0277, ..., 0.2068, -0.0436, 0.0206], [-0.1834, -0.0824, -0.1628, ..., 0.1385, -0.0802, -0.1213]], [[-0.0396, 0.0614, 0.0782, ..., -0.1287, 0.1300, 0.0617], [-0.0946, 0.0567, -0.1073, ..., -0.0592, -0.0856, 0.2827], [-0.0462, -0.1590, -0.1344, ..., -0.1792, 0.0181, -0.0359], ..., [-0.2835, -0.2083, 0.1522, ..., -0.0210, -0.0277, -0.1216], [-0.1895, -0.0309, 0.1750, ..., -0.0695, 0.2357, 0.3276], [-0.2289, 0.0339, -0.0078, ..., -0.1842, 0.0536, -0.1016]]]], grad_fn=<ViewBackward>) torch.Size([2, 2, 10, 512])

 

 

 

=============================

과제 / 퀴즈

 

Natural Language Processing

Assignment 2: 번역 모델 전처리

0. Introduction

  • 본 과제의 목적은 영어-한글 번역 모델을 학습하기 위해 영어-한글 번역 데이터셋을 전처리하는 방법을 학습하는 것입니다. 이번 과제에서는 번역 모델의 입/출력을 만들기 위해 자주 사용되는 여러가지 자연어 전처리 기술을 익히게 됩니다. 번역 모델은 번역하고자 하는 문장(Source)을 입력으로 받고, 번역 결과(Target)을 출력합니다.

  • 미완성된 함수 3개가 있습니다. (preprocess, collate_fn, bucketing) 아래의 지시 사항과 각 함수의 docstring을 참고하여 함수 3개를 완성하여 Data Loader를 완성해주세요. test case를 모두 통과해야 합니다.
 
 
 
 
[1]
 
 
 
from typing import List, Dict, Tuple, Sequence, Any
from collections import Counter, defaultdict, OrderedDict
from itertools import chain
import random
random.seed(1234)
 
import torch
 
 
 
 
 
 
 
 
 
[2]
 
 
 
class Language(Sequence[List[str]]):
    PAD_TOKEN = '<PAD>'
    PAD_TOKEN_IDX = 0
    UNK_TOKEN = '<UNK>'
    UNK_TOKEN_IDX = 1
    SOS_TOKEN = '<SOS>'
    SOS_TOKEN_IDX = 2
    EOS_TOKEN = '<EOS>'
    EOS_TOKEN_IDX = 3
 
    def __init__(self, sentences: List[str]) -> None:
        self._sentences: List[List[str]] = [sentence.split() for sentence in sentences]
 
        self.word2idx: Dict[str, int] = None
        self.idx2word: List[str] = None
    
    def build_vocab(self, min_freq: int=1) -> None:
        SPECIAL_TOKENS: List[str] = [Language.PAD_TOKEN, Language.UNK_TOKEN, Language.SOS_TOKEN, 
Language.EOS_TOKEN]
        self.idx2word = SPECIAL_TOKENS + [word for word, count in Counter(chain(*self._sentences)).items() 
if count >= min_freq]
        self.word2idx = {word: idx for idx, word in enumerate(self.idx2word)}
    
    def set_vocab(self, word2idx: Dict[str, int], idx2word: List[str]) -> None:
        self.word2idx = word2idx
        self.idx2word = idx2word
    
    def __getitem__(self, index: int) -> List[str]:
        return self._sentences[index]
    
    def __len__(self) -> int:
        return len(self._sentences)
 
 
class NMTDataset(Sequence[Tuple[List[int], List[int]]]):
    def __init__(self, src: Language, tgt: Language, max_len: int=30) -> None:
        assert len(src) == len(tgt)
        assert src.word2idx is not None and tgt.word2idx is not None
 
        self._src = src
        self._tgt = tgt
        self._max_len = max_len
 
    def __getitem__(self, index: int) -> Tuple[List[str], List[str]]:
        return preprocess(self._src[index], self._tgt[index], self._src.word2idx, self._tgt.word2idx, 
self._max_len)
 
    def __len__(self) -> int:
        return len(self._src)
 
 
 
 
 
 
 
 
 
 
 

1. Sentence Preprocessor

  • NLP 모델에 자연어 정보를 전달하기 위해서는 적절한 형태로의 전처리가 필요합니다. 주어진 데이터셋은 Source, Target(한->영 번역의 경우 source는 한국어 문장, target은 영어 문장이 됩니다.) 각각 하나의 문장으로 이루어져 있고 모델에 해당 정보를 전달하기 위해서는 하나의 문장을 여러 단어로 분리하고 각각의 단어를 index로 바꿔줄 수 있는 word2idx dictionary가 필요합니다(동일한 단어 = 동일한 index).

해당 과정은 가장 간단한 수준의 tokenization이며 거의 모든 자연어 전처리 과정에서 사용됩니다. 주어진 문장쌍(Source, Target)을 단어 index 단위로 바꾸어주는 preprocess 함수를 docstring을 참고하여 완성해주세요.

  • 추가사항: 번역 모델에서 Target 문장에는 sos(start of sentence), eos(end of sentence) token이 추가되고 각각은 문장의 시작과 끝을 알려주는 token으로 사용됩니다. (그림 참고)
 
 
 
 
[5]
 
 
 
def preprocess(
    raw_src_sentence: List[str],
    raw_tgt_sentence: List[str],
    src_word2idx: Dict[str, int],
    tgt_word2idx: Dict[str, int],
    max_len: int
) -> Tuple[List[int], List[int]]:
    """ Sentence preprocessor for neural machine translation
 
    Preprocess Rules:
    1. All words should be converted into their own index number by word2idx.
    2. If there is no matched word in word2idx, you should replace the word as <UNK> token.
    3. You have to use matched word2idx for each source/target language.
    4. You have to insert <SOS> as the first token of the target sentence.
    5. You have to insert <EOS> as the last token of the target sentence.
    6. The length of preprocessed sentences should not exceed max_len.
    7. If the lenght of the sentence exceed max_len, you must truncate the sentence.
 
    Arguments:
    raw_src_sentence -- raw source sentence without any modification
    raw_tgt_sentence -- raw target sentence without any modification 
    src_word2idx -- dictionary for source language which maps words to their unique numbers
    tgt_word2idx -- dictionary for target language which maps words to their unique numbers
    max_len -- maximum length of sentences
 
    Return:
    src_sentence -- preprocessed source sentence
    tgt_sentence -- preprocessed target sentence
 
    """
    # Special tokens, use these notations if you want
    UNK = Language.UNK_TOKEN_IDX
    SOS = Language.SOS_TOKEN_IDX
    EOS = Language.EOS_TOKEN_IDX
 
    ### 아래에 코드 빈칸(None)을 완성해주세요
    src_sentence = []
    tgt_sentence = []
    for word in raw_src_sentence:
        if word in src_word2idx: # src dictionary에 현재의 word가 있는 경우
            src_sentence.append(src_word2idx[word])
        else:
            src_sentence.append(UNK) # src dictionary에 현재의 word가 없는 경우
    
    for word in raw_tgt_sentence:
        if word in tgt_word2idx: # tgt dictionary에 현재의 word가 있는 경우
            tgt_sentence.append(tgt_word2idx[word])
        else:
            tgt_sentence.append(UNK) # tgt dictionary에 현재의 word가 없는 경우
 
    src_sentence = src_sentence[:max_len] # max_len까지의 sequence만
    tgt_sentence = [SOS] + tgt_sentence[:max_len-2] + [EOS] # SOS, EOS token을 추가하고 max_len까지의 
sequence만
 
    # [선택] try, except을 활용해서 조금 더 빠르게 동작하는 코드를 작성해보세요.
 
    # [선택] List Comprehension을 활용해서 짧은 코드를 작성해보세요. (~2 lines)
 
    ### 코드 작성 완료
    return src_sentence, tgt_sentence
 
 
 
 
 
 
 
 
 
 
 

1-1. Preprocess 테스트 케이스 확고

 
 
 
 
[6]
 
 
 
def test_preprocess():
    print("======Preprocessing Test Case======")
    eng_sentences = ["I study for the rest of the time", "We always try and show our love for each other"]
    kor_sentences = ["나는 남는 시간에 공부를 한다", "우리는 언제나 서로에게 사랑을 보여주려 노력해요"]
    english = Language(eng_sentences)
    korean = Language(kor_sentences)
    english.build_vocab()
    korean.build_vocab()
 
    # First test
    src_sentence, tgt_sentence = preprocess(english[0], korean[0], english.word2idx,
                                            korean.word2idx, max_len=100)
    assert src_sentence == [4, 5, 6, 7, 8, 9, 7, 10] and \
           tgt_sentence == [2, 4, 5, 6, 7, 8, 3], \
           "결과가 일치하지 않습니다"
    print("첫번째 테스트 케이스를 통과했습니다!")
 
    # Second test
    src_sentence, tgt_sentence = preprocess(english[1], korean[1], english.word2idx,
                                            korean.word2idx, max_len=5)
    assert src_sentence == [11, 12, 13, 14, 15] and \
           tgt_sentence == [2, 9, 10, 11, 3], \
           "max length를 고려한 결과가 일치하지 않습니다"
    print("두번째 테스트 케이스를 통과했습니다!")
    
    # Third test
    raw_src_sentence = "Why don't you study using your spare time efficiently ?".split()
    raw_tgt_sentence = '남는 시간을 효율적으로 활용해서 공부를 해보면 어떨까요 ?'.split()
    src_sentence, tgt_sentence = preprocess(raw_src_sentence, raw_tgt_sentence, english.word2idx,
                                            korean.word2idx, max_len=12)
    assert src_sentence == [1, 1, 1, 5, 1, 1, 1, 10, 1, 1] and \
           tgt_sentence == [2, 5, 1, 1, 1, 7, 1, 1, 1, 3], \
           "Out of vocabulary (OOV) 는 <UNK> token으로 대체되어야 합니다"
    print("세번째 테스트 케이스를 통과했습니다!")
 
    print("Preprocess의 모든 테스트 케이스를 통과했습니다!")
    
test_preprocess()
 
 
 
 
 
 
======Preprocessing Test Case====== 첫번째 테스트 케이스를 통과했습니다! 두번째 테스트 케이스를 통과했습니다! 세번째 테스트 케이스를 통과했습니다! Preprocess의 모든 테스트 케이스를 통과했습니다!
 
 
 
 

2. Bucketing

  • Bucketing은 주어진 문장의 길이에 따라 데이터를 그룹화하여 padding을 적용하는 기법입니다. 이 기법은 모델의 학습 시간을 단축하기 위해 고안되었습니다. 아래 그림과 같이 bucketing을 적용하지 않은 경우, batch별 pad token의 개수가 늘어나 학습하는 데에 오랜 시간이 걸립니다. 주어진 문장들을 문장 길이를 기준으로 나누어 bucketed_batch_indices 함수를 완성해주세요.

 

Figure 1. Bucketing을 적용하지 않은 경우


 

Figure 2. Bucketing을 적용한 경우
 
 
 
 
[13]
 
 
 
def bucketed_batch_indices(
    sentence_length: List[Tuple[int, int]],
    batch_size: int,
    max_pad_len: int
) -> List[List[int]]:
    """ Function for bucketed batch indices
    Although the loss calculation does not consider PAD tokens,
    it actually takes up GPU resources and degrades performance.
    Therefore, the number of <PAD> tokens in a batch should be minimized in order to maximize GPU 
utilization.
    Implement a function which groups samples into batches that satisfy the number of needed <PAD> tokens 
in each sentence is less than or equals to max_pad_len.
    
    Note 1: several small batches which have less samples than batch_size are okay but should not be many. 
If you pass the test, it means "okay".
 
    Note 2: you can directly apply this function to torch.utils.data.dataloader.DataLoader with 
batch_sampler argument.
    Read the test codes if you are interested in.
 
    Arguments:
    sentence_length -- list of (length of source_sentence, length of target_sentence) pairs.
    batch_size -- batch size
    max_pad_len -- maximum padding length. The number of needed <PAD> tokens in each sentence should not 
exceed this number.
 
    return:
    batch_indices_list -- list of indices to be a batch. Each element should contain indices of 
sentence_length list.
 
    Example:
    If sentence_length = [7, 4, 9, 2, 5, 10], batch_size = 3, and max_pad_len = 3,
    then one of the possible batch_indices_list is [[0, 2, 5], [1, 3, 4]]
    because [0, 2, 5] indices has simialr length as sentence_length[0] = 7, sentence_length[2] = 9, and 
sentence_length[5] = 10.
    """
    
    ### 아래에 코드 빈칸(None)을 완성해주세요
    batch_map = defaultdict(list)
    batch_indices_list = []
    
    src_len_min = min(sentence_length, key=lambda x: x[0])[0] # 첫번째 인덱스인 src의 min length
    tgt_len_min = min(sentence_length, key=lambda x: x[1])[1] # 두번째 인덱스인 tgt의 min length
 
    for idx, (src_len, tgt_len) in enumerate(sentence_length):
        src = (src_len - src_len_min + 1) // (max_pad_len) # max_pad_len 단위로 묶어주기 위한 몫 (그림에서는 
5)
        tgt = (tgt_len - tgt_len_min + 1) // (max_pad_len) # max_pad_len 단위로 묶어주기 위한 몫 (그림에서는 
5)
        batch_map[(src, tgt)].append(idx)
 
    for key, value in batch_map.items():
        batch_indices_list += [value[i: i+batch_size] for i in range(0, len(value), batch_size)]
 
    ### 코드 작성 완료
 
    # Don't forget shuffling batches because length of each batch could be biased
    random.shuffle(batch_indices_list)
 
    return batch_indices_list
 
 
 
 
 
 
 
 
 
 
 

2-1. Bucketing 테스트 케이스 확인

 
 
 
 
[14]
 
 
 
def test_bucketing():
    print ("======Bucketing Test Case======")
    dataset_length = 50000
    min_len = 10
    max_len = 30
    batch_size = 64
    max_pad_len = 5
 
    sentence_length = [(random.randint(min_len, max_len), random.randint(min_len, max_len))
                       for _ in range(dataset_length)]
    batch_indices = bucketed_batch_indices(sentence_length, batch_size=batch_size,
                                           max_pad_len=max_pad_len)
    
    # the first test
    assert sorted(chain(*batch_indices)) == list(range(0, dataset_length)), \
        "중복되거나 빠진 샘플이 존재합니다."
    print("첫번째 테스트 케이스를 통과했습니다!")
    
    # the second test
    assert sum(1 for batch in batch_indices if len(batch) < batch_size) < 30, \
        "Batch size보다 작은 batch들이 너무 많습니다."
    print("두번째 테스트 케이스를 통과했습니다!")
 
    # the third test
    for batch in batch_indices:
        src_length, tgt_length = zip(*list(sentence_length[idx] for idx in batch))
        assert max(src_length) - min(src_length) <= max_pad_len and max(tgt_length) - min(tgt_length) <= 
max_pad_len, \
            "max_pad_len보다 더 많은 <PAD> token이 있는 문장이 있습니다."
    print("세번째 테스트 케이스를 통과했습니다!")
 
    print("모든 테스트 케이스를 통과했습니다!")
test_bucketing()
 
 
 
 
 
 
======Bucketing Test Case====== 첫번째 테스트 케이스를 통과했습니다! 두번째 테스트 케이스를 통과했습니다! 세번째 테스트 케이스를 통과했습니다! 모든 테스트 케이스를 통과했습니다!
 
 
 
 

3. Collate Function

  • Collate function은 주어진 데이터셋을 원하는 형태의 batch로 가공하기 위해 사용되는 함수입니다. Batch 단위별로 max sequence length에 맞게 pad token을 추가하고 내림차순으로 정렬하는 collate_fn 함수를 완성해주세요.
 
 
 
 
[21]
 
 
 
def collate_fn(
    batched_samples: List[Tuple[List[int], List[int]]]
) -> Tuple[torch.Tensor, torch.Tensor]:
    """ Collate function
    Because each sentence has variable length, you should collate them into one batch with <PAD> tokens.
    Implement collate_fn function which collates source/target sentence into source/target batchs 
appending <PAD> tokens behind
    Meanwhile, for the convenience of latter implementations, you should sort the sentences within a batch 
by its source sentence length in descending manner.
 
    Note 1: if you are an expert on time-series data, you may know a tensor of [sequence_length, 
batch_size, ...] is much faster than [batch_size, sequence_length, ...].
    However, for simple intuitive understanding, let's just use batch_first this time.
 
    Note 2: you can directly apply this function to torch.utils.data.dataloader.DataLoader with collate_fn 
argument.
    Read the test codes if you are interested in.
 
    Hint: torch.nn.utils.rnn.pad_sequence would be useful
 
    Arguments:
    batched_samples -- list of (source_sentence, target_sentence) pairs. This list should be converted to 
a batch
 
    Return:
    src_sentences -- batched source sentence
                        in shape (batch_size, max_src_sentence_length)
    tgt_sentences -- batched target sentence
                        in shape (batch_size, max_tgt_sentence_length)
 
    """
    PAD = Language.PAD_TOKEN_IDX
    batch_size = len(batched_samples)
 
    ### 아래에 코드 빈칸을 완성해주세요
    batched_samples = sorted(batched_samples, key=lambda x: len(x[0]), reverse=True) # 0번째 요소의 길이를 
기준으로 내림차순 정렬
    
    src_sentences = []
    tgt_sentences = []
    for src_sentence, tgt_sentence in batched_samples:
        src_sentences.append(torch.tensor(src_sentence))
        tgt_sentences.append(torch.tensor(tgt_sentence))
 
    src_sentences = torch.nn.utils.rnn.pad_sequence(src_sentences, batch_first=True) # batch x longest 
seuqence 순으로 정렬 (링크 참고)
    tgt_sentences = torch.nn.utils.rnn.pad_sequence(tgt_sentences, batch_first=True) # batch x longest 
seuqence 순으로 정렬 (링크 참고)
 
    ### 코드 작성 완료
 
    assert src_sentences.shape[0] == batch_size and tgt_sentences.shape[0] == batch_size
    assert src_sentences.dtype == torch.long and tgt_sentences.dtype == torch.long
    return src_sentences, tgt_sentences
 
 
 
 
 
 
 
 
 
 

3-1. Collate function 테스트 케이스 확인

 
 
 
 
[22]
 
 
 
def test_collate_fn():
    print("======Collate Function Test Case======")
    
    # the first test
    batched_samples = [([1, 2, 3, 4], [1]), ([1, 2, 3], [1, 2]), ([1, 2], [1, 2, 3]), ([1], [1, 2, 3, 4])]
    src_sentences, tgt_sentences = collate_fn(batched_samples)
    assert (src_sentences == torch.Tensor([[1, 2, 3, 4], [1, 2, 3, 0], [1, 2, 0, 0], [1, 0, 0, 0]])).all() 
and \
           (tgt_sentences == torch.Tensor([[1, 0, 0, 0], [1, 2, 0, 0], [1, 2, 3, 0], [1, 2, 3, 4]])).all(),
 \
           "Your collated batch does not math expected result."
    print("첫번째 테스트 케이스를 통과했습니다!")
 
    # the second test
    batched_samples = [([1], [1, 2, 3, 4]), ([1, 2], [1, 2, 3]), ([1, 2, 3], [1, 2]), ([1, 2, 3, 4], [1])]
    src_sentences, tgt_sentences = collate_fn(batched_samples)
    assert (src_sentences == torch.Tensor([[1, 2, 3, 4], [1, 2, 3, 0], [1, 2, 0, 0], [1, 0, 0, 0]])).all() 
and \
           (tgt_sentences == torch.Tensor([[1, 0, 0, 0], [1, 2, 0, 0], [1, 2, 3, 0], [1, 2, 3, 4]])).all(),
 \
           "Your collated batch should be sorted in descending manner by its source sentence length."
    print("두번째 테스트 케이스를 통과했습니다!")
 
    print("모든 테스트 케이스를 통과했습니다!")
 
test_collate_fn()
 
 
 
 
 
======Collate Function Test Case====== 첫번째 테스트 케이스를 통과했습니다! 두번째 테스트 케이스를 통과했습니다! 모든 테스트 케이스를 통과했습니다!
 
 
 
 

3-2. 전처리 테스트 케이스 확인

 
 
 
 
[23]
 
 
 
def test_dataloader():
    print("======Dataloader Test======")
    english = Language(['고기를 잡으러 바다로 갈까나', '고기를 잡으러 강으로 갈까나', '이 병에 가득히 
넣어가지고요'])
    korean = Language(['Shall we go to the sea to catch fish ?', 'Shall we go to the river to catch fish', 
'Put it in this bottle'])
    english.build_vocab()
    korean.build_vocab()
    dataset = NMTDataset(src=english, tgt=korean)
 
    batch_size = 4
    max_pad_len = 5
    sentence_length = list(map(lambda pair: (len(pair[0]), len(pair[1])), dataset))
 
    bucketed_batch_indices(sentence_length, batch_size=batch_size, max_pad_len=max_pad_len)
    dataloader = torch.utils.data.dataloader.DataLoader(dataset, collate_fn=collate_fn, num_workers=2, \
                                                        batch_sampler=bucketed_batch_indices
(sentence_length, batch_size=batch_size, max_pad_len=max_pad_len))
 
    src_sentences, tgt_sentences = next(iter(dataloader))
    print("Tensor for Source Sentences: \n", src_sentences)
    print("Tensor for Target Sentences: \n", tgt_sentences)
 
    print("모든 전처리 과제를 완료했습니다 고생하셨습니다 :)")
test_dataloader()
 
 
 
 
 
======Dataloader Test====== Tensor for Source Sentences: tensor([[4, 5, 6, 7], [4, 5, 8, 7]]) Tensor for Target Sentences: tensor([[ 2, 4, 5, 6, 7, 8, 9, 7, 10, 11, 12, 3], [ 2, 4, 5, 6, 7, 8, 13, 7, 10, 11, 3, 0]]) 모든 전처리 과제를 완료했습니다 고생하셨습니다 :)
 
 
[-]
 
 
 
 
 
 
 
 

 

 

==========================

피어세션

그냥 저저번주 금요일꺼부터 복습해봄.

질문은 긍정적으로 하는게 좋다.

 

======================

후기

오늘 과제는 풀긴 풀었는데 하나도 이해가 안된다. 그러니까 코드 자체도 그렇고 코드와 수업에서 배운 개념과의 괴리감이 강함. 풀이를 꼭 봐야 겠다.