Word2Vec 구현

Word2Vec을 pytorch를 통해서 구현해보겠습니다. 파이토치 공식홈에도 유사한 예제가 있으니 관심있으신 분들은 공식홈에 있는 내용을 읽어보시는 것이 도움이 되시리라 생각됩니다.

먼저 아래와 같이 필요한 라이브러리들을 임포트합니다. 마지막에 임포트한 matplotlib의 경우는 시각화를 위한 것으로 단어들이 어떤 상관성을 가지는지 확인해보기 위함입니다.

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

import numpy as np
import pandas as pd

아래와 같은 텍스트를 선언합니다. 몇개의 단어로 구성된 문장이고 중복된 문장들을 복사해서 붙여 넣었습니다. Word2Vec을 구현하는데 여러 방식이 있지만 이번 예제에서는 Skip-Gram 방식을 사용합니다.

위 구조에서 핵심은 가중치행렬 WW, W′W′ 두 개입니다. Word2Vec의 학습결과가 이 두 개의 행렬입니다. 그림을 자세히 보시면 입력층-은닉층, 은닉층-출력층을 잇는 가중치 행렬의 모양이 서로 전치(transpose)한 것과 동일한 것을 볼 수 있습니다. 그런데 전치하면 그 모양이 같다고 해서 완벽히 동일한 행렬은 아니라는 점에 주의할 필요가 있습니다. 물론 두 행렬을 하나의 행렬로 취급(tied)하는 방식으로 학습을 진행할 수 있고, 학습이 아주 잘되면 WW와 W′W′ 가운데 어떤 걸 단어벡터로 쓰든 관계가 없다고 합니다.

또 다른 방법은 COBOW(Continuous Bag-of-Words) 방식이 있습니다. 이 방식은 Skip-Gram과 반대의 방식입니다.
CBOW는 주변에 있는 단어들을 가지고, 중간에 있는 단어들을 예측하는 방법입니다. 반대로, Skip-Gram은 중간에 있는 단어로 주변 단어들을 예측하는 방법입니다. 메커니즘 자체는 거의 동일하기 때문에 이해하는데 어렵지는 않습니다.

보통 딥러닝이라함은, 입력층과 출력층 사이의 은닉층의 개수가 충분히 쌓인 신경망을 학습할 때를 말하는데 Word2Vec는 입력층과 출력층 사이에 하나의 은닉층만이 존재합니다. 이렇게 은닉층(hidden Layer)이 1개인 경우에는 일반적으로 심층신경망(Deep Neural Network)이 아니라 얕은신경망(Shallow Neural Network)이라고 부릅니다. 또한 Word2Vec의 은닉층은 일반적인 은닉층과는 달리 활성화 함수가 존재하지 않으며 룩업 테이블이라는 연산을 담당하는 층으로 일반적인 은닉층과 구분하기 위해 투사층(projection layer)이라고 부르기도 합니다.

corpus = [
    'he is a king',
    'she is a queen',
    'he is a man',
    'she is a woman',
    'warsaw is poland capital',
    'berlin is germany capital',
    'paris is france capital',
    'seoul is korea capital', 
    'bejing is china capital',
    'tokyo is japan capital',
]

def tokenize_corpus(corpus):
    tokens = [x.split() for x in corpus]
    return tokens

tokenized_corpus = tokenize_corpus(corpus)

단어들의 중복을 제거하여 vocabulary 리스트를 만들고 word2idx, idx2word dict를 만듭니다.

vocabulary = []
for sentence in tokenized_corpus:
    for token in sentence:
        if token not in vocabulary:
            vocabulary.append(token)

word2idx = {w: idx for (idx, w) in enumerate(vocabulary)}
idx2word = {idx: w for (idx, w) in enumerate(vocabulary)}

vocabulary_size = len(vocabulary)

Skip-Gram이나 CBOW 모두 window_size 가 필요합니다. 해당 파라메터는 주변의 단어를 몇개까지 학습에 이용할 것인가를 결정해주는 파라메터입니다. 이번 예제에서는 2개의 단어만 학습에 활용하도록 하겠습니다.

window_size = 2
idx_pairs = []

for sentence in tokenized_corpus:
    indices = [word2idx[word] for word in sentence]
    for center_word_pos in range(len(indices)):
        for w in range(-window_size, window_size + 1):
            context_word_pos = center_word_pos + w
            if context_word_pos < 0 or context_word_pos >= len(indices) or center_word_pos == context_word_pos:
                continue
            context_word_idx = indices[context_word_pos]
            idx_pairs.append((indices[center_word_pos], context_word_idx))

idx_pairs = np.array(idx_pairs) 

위와 같은 과정을 통해서 idx_pairs를 만들 수 있습니다. array에서 10개만 출력해보면 아래와 같은 배열을 볼 수 있습니다.

이것은 “he is a man”이라는 단어를 학습 할 때에 [he, is],[he,a],[is, he],[is,a],[is,man] … 형태의 학습데이터입니다. COBOW 방식은 주변의 단어들을 통해서 목적단어를 예측하는 형태라면 skip-gram 방식은 목적단어를 통해서 주변에 나올 수 있는 단어 [is, a]를 예측하는 방법으로 학습이 진행됩니다.

print(idx_pairs[0:10])
array([[0, 1],
       [0, 2],
       [1, 0],
       [1, 2],
       [1, 3],
       [2, 0],
       [2, 1],
       [2, 3],
       [3, 1],
       [3, 2]])

입력 데이터를 One-Hot 형태로 변경합니다. 참고로 One-Hot 형태를 사용하지 않고 nn.Embedding()을 통해서 룩업테이블(Look-Up Table)을 만들어 사용해도 무방합니다. nn.Embedding()을 사용하는 법은 이전 글에서 다뤘기 때문에 자세한 내용은 해당 게시물을 참조하시기 바랍니다.

def get_input_layer(word_idx):
    return np.eye(vocabulary_size)[word_idx]

X = []
y = []
for data, target in idx_pairs:
    X.append(get_input_layer(data))
    y.append(target)
    
X = torch.FloatTensor(np.array(X))
y = torch.Tensor(np.array(y)).long()

이제 신경망 모듈을 아래와 같이 생성합니다. 입력과 출력 사이에 2차원의 벡터형태로 정보를 압축하게됩니다.

class Word2VecModel(nn.Module):
    def __init__(self,inout_dim):
        super().__init__()
        self.linear1 = nn.Linear(inout_dim,2)
        self.linear2 = nn.Linear(2,inout_dim)
        
    def forward(self,x):
        return self.linear2(self.linear1(x))
    
model = Word2VecModel(X.size(dim=-1))

아래와 같이 데이터를 훈련합니다. 예측치(prediction)와 실제 값(y)를 통해서 cost를 계산하고 이를 출력해줍니다.

# optimizer 설정
optimizer = optim.Adam(model.parameters())

nb_epochs = 100
for epoch in range(nb_epochs + 1):

    # H(x) 계산
    prediction = model(X)

    # cost 계산
    cost = F.cross_entropy(prediction, y)

    # cost로 H(x) 개선
    optimizer.zero_grad()
    cost.backward()
    optimizer.step()
    
    # 20번마다 로그 출력
    if epoch % 100 == 0:
        print('Epoch {:4d}/{} Cost: {:.6f}'.format(
            epoch, nb_epochs, cost.item()
        ))

훈련이 완료된 후에 생성된 weight 정보를 출력해봅니다.

vector = model.state_dict()['linear2.weight'] + model.state_dict()['linear2.bias'].view(-1,1)
w2v_df = pd.DataFrame(vector.numpy(), columns = ['x1', 'x2'])
w2v_df['word'] = vocab
w2v_df = w2v_df[['word','x1','x2']]
w2v_df
ano = w2v_df['word'].values
x1 = w2v_df['x1'].values
x2 = w2v_df['x2'].values

fig, ax = plt.subplots(figsize=(5,5))
ax.scatter(x1, x2)

for i, txt in enumerate(ano):
    ax.annotate(txt, (x1[i], x2[i]))

2차원 벡터를 통해서 아래와 같이 시각화해봅니다.

Reference

[1]https://ratsgo.github.io/from%20frequency%20to%20semantics/2017/03/30/word2vec/
[2]https://towardsdatascience.com/nlp-101-word2vec-skip-gram-and-cbow-93512ee24314
[3]https://wikidocs.net/22660

Word2Vec 시각화

Word2Vec은 간단을 간단히 말하면 “문장안에 있는 여러 단어들을 벡터 형태로 표현하는 것” 말 그대로 Word to Vector라고 할 수 있습니다. 워드라는 말은 쉽게 이해할 수 있지만 벡터(Vector)는 어떤 뜻일까요?

물리학이나 수학에서 약간씩 차이가 있지만 공통적으로 어떤 공간에서 위치와 방향성을 가지는 값을 표현하는 것이라고 할 수 있습니다. 그러니까 문장에 많은 Word를 어떤 공간에 위치값을 표시할 뿐만아니라 이 값들이 어떤 방향성이 있는지를 표시하는 기법이 Word2Vec이라고 하겠습니다.

위의 이미지는 word2vec의 가장 유명한 그림 중에 하나입니다. 각 단어들을 보면 어떤 방향성이 있고 숫자 값을 가지고 있습니다. 그렇기 때문에 유사도를 계산 할 수도 있고 각 단어의 관계에 대한 연산이 가능합니다.
예를 들어서 “KING-MAN+WOMAN=QUEEN”이라는 관계가 나온다는 것이죠.
또 “한국-서울+도쿄=일본”라는 관계를 추출할 수 있습니다. 아래 링크를 방문해보시고 다양한 케이스를 테스트해보시기 바랍니다.
https://word2vec.kr/search/

이것은 전통적인 방법인 One-Hot-Encoding을 통해서 단어를 표현하는 것의 문제점을 극복할 수 있는 아주 유용한 방법입니다. 이렇게 단어들을 벡터로 바꾸는 것을 워드 임베딩(Word-Embedding)이라고 하고 그중에서 가장 대표적인 모델이 Word2Vec 모델로 해당 단어와 함께 자주 등장하는 단어는 비슷한 단어일것이라는 가정으로 출발합니다.

본 예제는 Word2Vec의 원리와 이론을 소개하는 것은 아니고 실제로 단어를 2차원 공간에 표시하는 방법에 대한 예제코드이기 때문에 해당 이론이 궁금하신 분들은 인터넷에 공개된 많은 예제들이 있으니 참고해보시기 바랍니다.

예제를 실해하기 위해서 먼저 필요한 라이브러리를 import합니다.
분석할 데이터는 인터넷 쇼핑몰의 마우스를 구매한 후에 남긴 후기들을 모은 것입니다. 예를 들어 제품의 이름을 선택했을 경우에 해당 단어와 가장 거리가 가까운 단어들이 긍정의 단어들이라면 제품의 평가가 좋을 것일테고 반대로 제품이 부정적인 단어들과 거리가 가깝다면 반대의 경우라고 생각할 수 있겠습니다.

import pandas as pd
import numpy as np

df_r = pd.read_excel("./mouse_review.xlsx")
df_r.head()

파일을 읽어온 뒤에 pandas의 head() 함수로 상위 5개의 데이터를 추출해봅니다.
데이터는 사용자, 작성일, 리뷰 내용, 별점, 제품명 정보가 있습니다.

이번에 사용할 text 정보는 리뷰 내용입니다. 리뷰에 보면 여러가지 특수기호, 영문자 등이 있기 때문에 정규식을 통해서 한글 외에 나머지 데이터를 걸러냅니다. 걸러낸 데이터는 review_train 컬럼을 만들어서 원본 데이터와 별도로 저장해둡니다.

df_r['review_train'] = df_r['review'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
df_r.head()

1차로 정규화를 끝낸 텍스트 데이터를 통해서 문장을 형태소별로 분리해줍니다. 또 사용하지 않는 단어들의 사전을 모아서 불용단어를 걸러냅니다. 분석하고자 하는 상황에 맞춰서 불용어를 등록해줍니다.

from konlpy.tag import Okt

stop_words = ['가','요','변','을','수','에','문','제','를','이','도','은','다','게','요','한','일','할','인데','거','좀','는데','ㅎㅎ','뭐','까','있는','잘','습니다','다면','했','주려','지','있','못','후','중','줄']

okt = Okt()
tokenized_data = []
for sentence in df_r['review_train']:
    temp_X = okt.morphs(sentence, stem=True) # 토큰화
    temp_X = [word for word in temp_X if not word in stop_words] 
    tokenized_data.append(temp_X)

이제 시각화를 위한 준비를 해줍니다. 시각화는 matplolib을 사용합니다.
한글화를 위해서 폰트를 설정해줍니다.
본 예제는 Mac OS환경에서 테스트 되었기 때문에 폰트의 위치는 Window 사용자와 틀릴 수 있으니 테스트 환경에 맞게 폰트 정보를 변경해줍니다.

import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
font_name = font_manager.FontProperties(fname='/System/Library/Fonts/Supplemental/AppleGothic.ttf').get_name()
rc('font', family=font_name)

리뷰 텍스트의 정보들을 간단히 표시해줍니다.
학습에 필요한 단계는 아니니 데이터에 대한 정보를 보고자 하지 않는다면 그냥 넘어가셔도 되겠습니다. 본 예제에 사용된 데이터는 대부분 길이가 0~50글자 사이의 비교적 짧은 문장들이라는 것을 알 수 있습니다.

print('리뷰의 최대 길이 :',max(len(l) for l in tokenized_data))
print('리뷰의 평균 길이 :',sum(map(len, tokenized_data))/len(tokenized_data))
plt.hist([len(s) for s in tokenized_data], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

이제 Word2Vec 모델을 생성할 차례입니다.
Word2Vec 모델은 가장 잘 알려진 gensim 라이브러리를 활용해보겠습니다.

사용한 파라메터의 자세한 정보는 아래 링크를 참조해보시기 바랍니다.
본 모델은 좌우 5개의 단어를 참조하는 100차원의 워드 벡터를 만드는 모델로 cobow 알고리즘을 사용하고 최소 5번 이하로 등장하는 단어들은 제외하겠습니다. worker는 thread의 갯수로 테스트하는 하드웨어의 성능에 따라서 조정할 수 있습니다.
https://radimrehurek.com/gensim/models/word2vec.html

from gensim.models import Word2Vec
model = Word2Vec(sentences = tokenized_data, size = 100, window = 5, min_count = 5, workers = 4, sg = 0)

학습 데이터가 많지 않기 대문에 학습 시간은 오래 걸리지 않습니다.
학습이 끝난 후에 단어들을 추출해서 벡터 리스트를 생성합니다. 해당 리스트 하나를 출력해보면 아래와 같은 데이터가 표시됩니다.

vocabs = model.wv.vocab.keys()
word_vocab_list = [model.wv[v] for v in vocabs]
array([ 0.45729467, -0.45482287,  0.2776271 , -0.38435346,  0.4311736 ,
       -0.36617622,  0.12129851, -0.309033  , -0.09569103, -0.27311006,
        0.28018764, -0.13276236,  0.13590969,  0.0521839 , -0.01882668,
        0.13234554, -0.02577238,  0.43111804, -0.6007069 ,  0.52846146,
        0.01065135, -0.20410554,  0.08504212, -0.5189065 ,  0.06219423,
       -0.10900757,  0.19578645, -0.01295294, -0.20757432, -0.17270625,
        0.08728364,  0.4751571 , -0.06208701, -0.3829262 ,  0.4810491 ,
       -0.27205822, -0.16547562, -0.2804698 ,  0.1357591 ,  0.16740464,
        0.53618526, -0.17420012,  0.06363445,  0.655636  ,  0.05952126,
       -0.6312642 ,  0.11448789, -0.00824977, -0.26018238, -0.33553734,
        0.18489622,  0.03913857, -0.5856825 , -0.08111028,  0.6696569 ,
        0.4201213 , -0.2061224 , -0.03785964, -0.0813726 ,  0.0297378 ,
       -0.5556496 , -0.0006753 ,  0.25876167,  0.08983239, -0.10351149,
        0.24005203,  0.21328437,  0.0797505 , -0.23059952, -0.32846287,
       -0.0017608 ,  0.51077896,  0.36693272,  0.2767188 , -0.47870687,
       -0.3036568 , -0.06708886, -0.4789917 , -0.08152916,  0.19817959,
        0.07031752, -0.34857494,  0.5963662 ,  0.02050934,  0.29983994,
        0.07854129,  0.40096822,  0.00098353, -0.26964054, -0.12954848,
        0.33181033, -0.07866482,  0.40206903, -0.37808138, -0.10669091,
       -0.15223539, -0.01180514, -0.13499472,  0.31345636,  0.08265099],
      dtype=float32)

Word2Vec에서 제공하는 함수인 most_similar()를 통해서 입력하는 단어와 가장 가까운 단어 정보를 표시해봅니다. “클릭”과 가장 가까운 단어는 “버튼”,”소리” 등의 순서로 각 단어간의 연관성이 매우 높다는 것을 알 수 있습니다.

아마도 마우스라는 제품의 특징상 클릭이라는 단어와 함께 버튼, 소리, 게임, 느낌 등의 단어가 많이 등장했다는 것을 알 수 있습니다.

print(model.wv.most_similar("클릭"))
[('버튼', 0.9999016523361206), ('소리', 0.9998948574066162), ('게임', 0.9998838305473328), ('느낌', 0.9998810291290283), ('아니다', 0.9998774528503418), ('되다', 0.9998745918273926), ('이나', 0.9998740553855896), ('만', 0.9998738765716553), ('재질', 0.9998738169670105), ('누르다', 0.9998728036880493)]

이제 각 단어와의 관계를 그래프로 나타내보겠습니다. 해당 데이터는 100차원의 데이터이고 그려보고자 하는 것은 2차원에 표시되는 그래프이기 때문에 차원을 축소할 필요가 있습니다.

잘알려진 차원축소 알고리즘으로 PCA기법이 있습니다.

PCA(Principal Component Analysis)는 차원축소(dimensionality reduction)와 변수추출(feature extraction) 기법으로 널리 쓰이고 있는 기법으로 데이터의 분산(variance)을 최대한 보존하면서 서로 직교하는 새 기저(축)를 찾아, 고차원 공간의 표본들을 선형 연관성이 없는 저차원 공간으로 변환하는 기법입니다. 
https://ratsgo.github.io/machine%20learning/2017/04/24/PCA/

from sklearn.decomposition import PCA
pca = PCA(n_components=2)
xys = pca.fit_transform(word_vocab_list)
xs = xys[:,0]
ys = xys[:,1]

#plt.figure(figsize=(10 ,10))
plt.scatter(xs, ys, marker = 'o')
plt.xlim(0,1), plt.ylim(0,0.01)
for i, v in enumerate(vocabs):
    plt.annotate(v, xy=(xs[i], ys[i]))

해당 기법을 통해서 아래와 같은 그래프를 그렸습니다. 해당 그래프는 전체 그래프에서 일부 구간(xlim, ylim)을 표시한 것으로 전체 데이터는 아닙니다.

높은 차원의 데이터를 평면으로 축소하면서 데이터의 구간이 많이 겹치는 것을 알 수 있습니다. 이러한 문제는 데이터를 더 높은 차원의 공간(3차원)에 표시한다던가 아니면 의미 없는 데이터들을 추출해서 데이터의 수를 줄여서 표시할 수도 있습니다.

RNN Time-Series 예측(2)

본 예제는 모두를 위한 딥러닝 시즌2의 데이터(data-02-stock_daily.csv)와 모델을 제외한 소스 코드를 참고했습니다.

# Reference
# 모두를 위한 딥러닝 시즌 2 - PyTorch
# Lab-11-4 RNN timeseries

필요한 라이브러리를 임포트 합니다.

import torch
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler

모델에 사용할 파라메터를 셋팅해줍니다. seq_length는 입력 시퀀스 정보, data_dim은 입력 데이터의 차원, hidden_dim은 출력 데이터의 차원, output_dim은 최종 예측 데이터의 차원 입니다.

# hyper parameters
seq_length = 7
data_dim = 5
hidden_dim = 30
output_dim = 1
learning_rate = 0.01
iterations = 501

해당 데이터는 주가 데이터로 개장 포인트, 가장 높은 포인트, 가장 낮은 포인트, 폐장시 포인트와 거래량으로 5개 변수 데이터를 가지고 있습니다. 본 예측은 학습 데이터를 7일로 분리하여 다음 폐장 포인트를 예측하는 모델입니다.

# load data
xy = np.loadtxt("data-02-stock_daily.csv", delimiter=",")
xy = xy[::-1]  # reverse order

학습용 데이터와 테스트용 데이터를 분리하는 내용입니다. 학습 데이터와 검증 데이터는 7:3 비율로 분리합니다.

# split train-test set
train_size = int(len(xy) * 0.7)
train_set = xy[0:train_size]
test_set = xy[train_size - seq_length:]
train_set.shape, test_set.shape

입력한 데이터를 학습에 사용하기 위해서는 정규화 과정이 필요합니다.
정규화를 왜 해야 하는지에 대해서는 아래의 그래프를 참고하시기 바랍니다.

해당 데이터셋은 총 5개로 구성되어 있습니다. 그중 4개의 데이터는 단위가 비슷하기 때문에 그래프를 통해 보면 유사한 형태를 보이고 있습니다. 그러나 Volume 이라는 컬럼을 같이 표현하고자 한다면 입력 단위의 차이가 매우 크기 때문에 아래의 그림과 같이 나머지 데이터는 식별이 불가능하게 됩니다.

  • 단위의 차이로 인해서 Volume 데이터를 시각화 하는데 한계가 있음

그러나 MinMaxScaler를 활용하여 정규화 하게 되면 모든 데이터를 0,1의 범위 안에 표현할 수 있기 때문에 모든 그래프를 한번에 그릴 수 있습니다. 그리고 이렇게 표현한 데이터는 다시 원래 단위의 형태로 복원 할 수 있습니다.

학습에서 MinMaxScaler를 사용하는 이유는 다차원 데이터값을 비교 분석하기 쉽게 만들어주고 자료의 오버플로우나 언더플로우를 방지해주고 최적과 과정에서 안정성 및 수렴 속도를 향상 시키기 위함입니다.

MinMaxScaler 수행후 데이터 시각화
scaler = MinMaxScaler()
scaler.fit(train_set)
print(scaler.n_samples_seen_, scaler.data_min_, scaler.data_max_, scaler.feature_range)
train_set = scaler.transform(train_set)
scaler.fit(test_set)
print(scaler.n_samples_seen_, scaler.data_min_, scaler.data_max_, scaler.feature_range)
test_set = scaler.transform(test_set)

build_dataset 함수는 RNN 학습을 위해서 입력 텐서를 만들어 주는 부분입니다.
time_series[0:7,], time_series[7,[-1]] 형식으로 되어 있습니다.

# make dataset to input
def build_dataset(time_series, seq_length):
    dataX = []
    dataY = []
    for i in range(0, len(time_series) - seq_length):
        _x = time_series[i:i + seq_length, :]
        _y = time_series[i + seq_length, [-1]]  # Next close price
        #print(_x, "->", _y)
        dataX.append(_x)
        dataY.append(_y)
    return np.array(dataX), np.array(dataY)
# make train-test dataset to input
trainX, trainY = build_dataset(train_set, seq_length)
testX, testY = build_dataset(test_set, seq_length)
print(trainX.shape, trainY.shape)

# convert to tensor
trainX_tensor = torch.FloatTensor(trainX)
trainY_tensor = torch.FloatTensor(trainY)

testX_tensor = torch.FloatTensor(testX)
testY_tensor = torch.FloatTensor(testY)

이제 학습 데이터를 통해서 데이터를 학습하는 모델을 만듭니다. 본 예제에서는 BiLSTM 방식으로 4개의 층을 쌓아 올린 형태입니다.

위의 그림은 해당 데이터에 대한 입력 데이터 형태와 입력와 출력의 형태는 아래 그림과 같습니다. 일단 벡터는 (n,7,5) -> (n,7,30) 형태로 나옵니다. 그러나 BiLSTM 모델을 사용했기 때문에 마지막 output은 30*2의 형태가 됩니다.

class Net(torch.nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, layers):
        super(Net, self).__init__()
        self.rnn = torch.nn.LSTM(input_dim, hidden_dim, num_layers=layers, batch_first=True, bidirectional=True)
        self.layers = torch.nn.Sequential(
            torch.nn.Linear(hidden_dim*2, 20),
            torch.nn.Linear(20, 10),
            torch.nn.Linear(10, output_dim)
        )

    def forward(self, x):
        x, (hidden, cell) = self.rnn(x)
        x = self.layers(x[:, -1, ])
        return x

net = Net(data_dim, hidden_dim, output_dim, 4)

이제 학습을 수행합니다.

# loss & optimizer setting
criterion = torch.nn.MSELoss()
optimizer = optim.Adam(net.parameters(), lr=learning_rate)

# start training
for i in range(iterations):
    outputs = net(trainX_tensor)
    loss = criterion(outputs, trainY_tensor)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if i%50 == 0:
        print(i, loss.item())

학습이 완료된 후 테스트를 수행합니다.

net.eval()
predict_data = net(testX_tensor).data.numpy()
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(testY)
plt.plot(predict_data, color='red')
plt.legend(['original', 'prediction'])
plt.show()

학습된 결과와 원본 데이터를 통해 비교해보면 예측이 비교적 잘됐음을 알 수 있습니다.

RNN Time-Series 예측(1)

해당 예측 모델의 원본 링크는 아래와 같습니다.
본 예제는 아래에 구현된 링크와 동일한 데이터를 사용했고 RNN의 모델과 학습 부분의 로직을 수정했습니다.

https://stackabuse.com/time-series-prediction-using-lstm-with-pytorch-in-python/

필요한 라이브러리를 임포트 합니다.

import torch
import torch.nn as nn
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

seaborn에 샘플 데이터 중에서 flights 정보를 로드합니다.
seaborn 패키지에는 flights 외에도 다양한 샘플 데이터가 있습니다.

sns.get_dataset_names()
['anscombe', 'attention', 'brain_networks', 'car_crashes','diamonds', 'dots', 'exercise', 'flights','fmri', 'gammas', 'geyser', 'iris', 'mpg','penguins', 'planets', 'tips', 'titanic']

flights 데이터를 DataFrame 형태로 입력 받아서 상위 5개 데이터를 출력해봅니다. 해당 데이터프레임은 year, month, passengers 컬럼이 있습니다. 데이터의 형식은 해당 년도에 월별로 승객의 수가 등록되어 있습니다. 데이터는 1949~1960년까지의 143개 데이터입니다. 참고로 df.head()로는 상위 5개 데이터를 df.tail()로는 하위 5개의 데이터를 출력합니다.

df = sns.load_dataset('flights')
df.head()
idxyearmonthpassengers
01949January112
11949February118
21949March132
31949April129
41949May121
샘플 데이터

데이터셋의 결측치를 다음과 같이 확인해보고 예측에 사용할 컬럼인 passengers가 어떻게 변화하는지 추이 정보를 출력합니다.

학습에 앞서 훈련용 데이터와 검증용 데이터를 분리하겠습니다.
학습용 데이터는 59~60년도 데이터를 제외한 나머지 데이터입니다. 해당 모델을 통해서 2개년도의 승객 추이를 예측해보겠습니다.

data = df['passengers'].values.astype(float)
valid_data_size = 24 
train_data = data[:-valid_data_size]
valid_data = data[-valid_data_size:]

데이터를 분리한 후 MinMaxScaler를 통해서 데이터를 0~1 사이의 값으로 변환합니다.
스케일링 작업을 통해 다차원 데이터의 값들을 비교 분석하기 쉽게 만들어주고 자료의 오버플로우나 언더플로우를 방지해주고 최적화 과정에서 안정성 및 수렴 속도를 향상시켜줍니다.

from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
train_data_norm = scaler.fit_transform(train_data.reshape(-1,1))

학습용 데이터는 아래와 같은 방법으로 생성합니다. 학습 데이터셋은 1월-12월 데이터를 통해 다음해 1월의 승객 수를 예측하고 2월-다음해 1월 데이터를 통해 2월의 승객 수를 예측하는 형태로 구성되어 있습니다.

sequence_length = 12 # monthly
def make_batch(input_data, sl):
    train_x = []
    train_y = []
    L = len(input_data)
    for i in range(L-sl):
        train_seq = input_data[i:i+sl]
        train_label = input_data[i+sl:i+sl+1]
        train_x.append(train_seq)
        train_y.append(train_label)
    return train_x, train_y

Array 형태의 데이터를 파이토치 텐서로 변환해줍니다.

train_x, train_y = make_batch(train_data_norm, sequence_length)
tensor_x = torch.Tensor(train_x)
tensor_y = torch.Tensor(train_y)

학습을 위한 데이터의 최종 형태는 아래와 같은 형태가 됩니다.
RNN 입력 자료의 특성상 배치사이즈, 타임 시퀀스, 입력 벡터의 형태를 가지게 됩니다.

tensor_x.size(), tensor_y.size()
output : (torch.Size([108, 12, 1]), torch.Size([108, 1, 1]))

이제 학습을 위한 모델 클래스를 만듭니다. 모델은 LSTM을 사용합니다.
모델의 초기화를 위해서 입력 벡터, 입력 시퀀스 정보를 각각 설정합니다. LSTM의 출력 벡터는 100으로 주었고 단층이 아닌 4개 층으로 구성했습니다.

아래와 같은 모델을 통해 구성하면 입력 시퀀스가 12이기 때문에 최종 LSTM 출력의 벡터는 (N, 12, 100)의 형태로 만들어집니다. 해당 모델에서는 12개의 시퀀스에서 나오는 데이를 사용하지 않고 마지막 스텝에서 나오는 시퀀스 정보만 사용하게 되기 때문에 RNN의 모델 중에서 Many-to-One에 해당한다고 할 수 있습니다.

class RNN(nn.Module):
    
    def __init__(self):
        super().__init__()
        self.input_vector = 1
        self.sequence_length = 12
        self.output_vector = 100
        self.num_layers = 4
        
        self.lstm = nn.LSTM(input_size=self.input_vector, hidden_size=self.output_vector, num_layers=self.num_layers, batch_first=True)
        self.linear = nn.Sequential(
            nn.Linear(self.output_vector, 50),
            nn.Linear(50, 30),
            nn.Linear(30, 10),
            nn.Linear(10,1)
        )
        
    def forward(self, x):
        output, _ = self.lstm(x) #(hidden, cell) 데이터는 사용하지 않음
        return self.linear(output[:,-1,:])

model = RNN()
RNN(
  (lstm): LSTM(1, 100, num_layers=4, batch_first=True)
  (linear): Sequential(
    (0): Linear(in_features=100, out_features=50, bias=True)
    (1): Linear(in_features=50, out_features=30, bias=True)
    (2): Linear(in_features=30, out_features=10, bias=True)
    (3): Linear(in_features=10, out_features=1, bias=True)
  )
)

해당 모델의 학습을 수행합니다.

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()

epochs = 501

for i in range(epochs):
    model.train()
    
    output = model(tensor_x)
    loss = criterion(output, tensor_y.view(-1,1))
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if i%25 == 0:
        print('Epoch {}, Loss {:.5f}'.format(i, loss.item()))

학습이 완료되면 해당 모델이 원본 데이터와 비교하여 얼마나 추이를 잘 나타내는지 확인하는 과정이 필요합니다. 이 과정을 위해서 사전에 분리한 valid 데이터를 사용합니다.

valid_data_norm = train_data_norm[-valid_data_size:]
valid_x, _ = make_batch(valid_data_norm, sequence_length)

valid 데이터 역시 학습과 동일한 과정을 수행합니다.
다만 학습이 일어나는 것은 아니기 때문에 loss를 계산하거나 역전파와 같은 프로세스는 수행하지 않습니다.
또한 해당 데이터는 0,1 사이 값으로 변환한 데이터이기 때문에 이 값을 다시 scaler를 통해 원래 값의 형태로 변경해줍니다.

model.eval()
with torch.no_grad():
    valid_tensor = torch.Tensor(valid_x)
    predict = model(valid_tensor)
predict = predict.data.numpy()
actual_predictions = scaler.inverse_transform(predict)

이렇게 변경한 데이터를 통해서 원본 데이터와 그래프를 그려봅니다. blue 라인이 원본 데이터이고 red 라인이 예측한 데이터입니다.

x = np.arange(120,132,1)
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(df['passengers'][0:132])
plt.plot(x,actual_predictions)
plt.show()
sequence_length=12

참고로 아래는 Sequence_Length=6으로 예측한 결과 입니다.

Sequence_length=6

CNN Filter 변환 결과

입력 이미지가 CNN(Convolutional Neural Network) 필터를 통과하면서 이미지의 변화가 어떻게 되는지 알아보고자 합니다. CNN 필터는 기존의 머신러닝 기반의 Feature 추출 방식과는 달리 자동으로 Feature를 학습합니다.

https://www.researchgate.net/figure/Architecture-of-our-unsupervised-CNN-Network-contains-three-stages-each-of-which_283433254

CNN 네트워크는 위와 같은 네트워크를 통과 하며 스스로 이미지에 대한 feature 정보를 습득하게 됩니다. 본 코드는 하나의 각 필터를 통과하며 이미지가 어떻게 변화되는지에 대한 정보를 보여주는 코드입니다.

테스트를 위해 필요한 라이브러리를 import합니다.

import cv2
import torch
import numpy as np
import matplotlib.pyplot as plt

위의 이미지는 cv2.imread로 읽으면 (225, 400, 3) 형태의 shape 정보를 가집니다. 우선 데이터의 자료형을 바꾸고 정규화를 수행합니다.

또 하나 작업해야 할 것은 pytorch에서 데이터를 처리하기 편하도록 dimension을 변경하는 작업을 수행합니다. pytorch cnn에서 데이터를 읽기 위한 데이터 타입은 아래와 같습니다.

conv2d input, output data shape

conv2d의 입력 형태는 (N, C, H, W) 형태이기 때문에 이에 맞게 shape 정보를 변경하고 plt.imshow() 함수를 통해서 이미지를 표시해봅니다.

_image = cv2.imread('./lena.jpg')
_image = _image.astype(np.float32)
_image = np.multiply(_image, 1.0 / 255.0)

image = torch.from_numpy(_image)
image = image[np.newaxis, :] 
image = image.permute(0, 3, 1, 2)

plt.imshow(image[0,2,:], cmap='gray')

이미지를 출력하기 위해서 간단한 출력용 함수를 정의합니다.

def imagegrid(args, output):
    width=args[0]
    height=args[1]
    rows = args[2]
    cols = args[3]
    axes=[]
    fig=plt.figure()
    fig.set_size_inches((20,14))
    for i in range(rows*cols):
        img = output[0,i,:]
        axes.append( fig.add_subplot(rows, cols, i+1) )
        plt.imshow(img)
    fig.tight_layout()    
    plt.show()

첫번째 conv2d는 입력 데이터를 받아서 12 Channel로 출력값을 나타내는 함수입니다. 이때 kernel 사이즈를 5로 정의했습니다. conv2d를 통해서 출력되는 이미지 정보는 아래와 같습니다. 해당 이미지들은 CNN에서 생성한 필터를 통과한 이미지 들입니다. 이러한 방식을 계속하며 곡선, 직선, 대각선 등 이미지의 특징 정보를 추출하게 됩니다.

conv1 = torch.nn.Conv2d(3,12,5)
output = conv1(image)
conv1_output = output.detach().numpy()
imagegrid([100,100,3,4], conv1_output)

위와 같은 방법으로 conv2d 네트워크를 한번 더 통과해봅니다. 이번에는 24개의 이미지를 출력하게 됩니다. conv2d를 통과하면서 점점 이미지의 크기는 작아지고 원본 이미지 형태는 필터를 지나며 점점 Feature의 특징 정보가 드러나게 됩니다.

conv2 = torch.nn.Conv2d(12,24,5)
output = conv2(output)
conv2_output = output.detach().numpy()
imagegrid([100,100,6,4], conv2_output)

한번 더 conv2d를 수행해서 48개의 이미지 데이터를 만들어 냅니다.

conv3 = torch.nn.Conv2d(24,48,5)
output = conv3(output)
conv3_output = output.detach().numpy()
imagegrid([100,100,6,8], conv3_output)

이런 방식으로 여러 차례 conv2d를 통과 하면서 이미지의 특징들일 추출해 내는 과정을 반복하게 됩니다. CNN 은 마지막에 이렇게 만들어진 정보들을 Linear 모듈을 통과하면서 최종 적으로 Classification 하게 됩니다.

예를 들어 현재의 이미지가 (60,97) 사이즈라면 Linear 모듈의 입력은 48*60*97을 입력으로 받고 임의의 크기의 출력 값을 얻을 수 있습니다. 이러한 Linear 과정을 통과하면서 ReLU, BatchNorm과 같은 함수를 사용하면 학습이 더 효율적일 수 있습니다.

CNN Fashion-MNIST 테스트 (PyTorch)

Fashion-MNIST에 대한 설명은 아래 링크로 대신하겠습니다.

Fashion-MNIST is a dataset of Zalando‘s article images—consisting of a training set of 60,000 examples and a test set of 10,000 examples. Each example is a 28×28 grayscale image, associated with a label from 10 classes. We intend Fashion-MNIST to serve as a direct drop-in replacement for the original MNIST dataset for benchmarking machine learning algorithms. It shares the same image size and structure of training and testing splits.

본 예제 코드는 데이터셋을 학습해서 입력되는 이미지가 어떤 분류에 속하는지를 예측해보는 것입니다. Fashion-MNIST 데이터셋을 벡터 공간에 표시하면 위와 같은 이미지로 분류할 수 있습니다.

이제 학습을 위해 해당 데이터셋을 다운로드합니다.

# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.5,), (0.5,))])

# Download and load the training data
train_loader = torch.utils.data.DataLoader(datasets.FashionMNIST('../F_MNIST_data/', download=True, train=True, transform=transform), batch_size=128, shuffle=True)

# Download and load the test data
test_loader = torch.utils.data.DataLoader(datasets.FashionMNIST('../F_MNIST_data/', download=True, train=False, transform=transform), batch_size=128, shuffle=True)

다운로드한 데이터가 어떤 이미지가 있는지 살펴보기 위해서 랜덤하게 몇개의 샘플을 추출해서 표시해보겠습니다. 해당 이미지들은 10개 [‘t-shirt’, ‘trouser’, ‘pullover’, ‘press’, ‘coat’, ‘sandal’, ‘shirt’, ‘sneaker’, ‘bag’, ‘ankleboot’]로 분류할 수 있는 패션 아이템들입니다.

x_train, y_train = next(iter(train_loader))
x_valid, y_valid = next(iter(test_loader))

fig, ax = plt.subplots(5,5)
fig.set_size_inches((20,14))
for i in range(5):
    for j in range(5):
        idx = numpy.random.randint(128)
        ax[i][j].imshow(x_train[idx,0,:])
        ax[i][j].set_xlabel(label[y_train[idx].item()])
        ax[i][j].set_xticklabels([])
        ax[i][j].set_yticklabels([])

학습을 위한 모델을 선언합니다. 이전 MNIST 데이터셋을 테스트했을 때와 같은 모델을 재활용했습니다.

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.convs = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=3), # input_channel, output_channel, kernel_size
            nn.ReLU(),
            nn.BatchNorm2d(10),
            nn.Conv2d(10, 20, kernel_size=3, stride=2),
            nn.ReLU(),
            nn.BatchNorm2d(20),
            nn.Conv2d(20, 40, kernel_size=3, stride=2)
        )
        
        self.layers = nn.Sequential(
            nn.Linear(40*5*5, 500),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.BatchNorm1d(500),
            nn.Linear(500,250),
            nn.Linear(250,100),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.BatchNorm1d(100),
            nn.Linear(100,50),
            nn.Linear(50, 10),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        x = self.convs(x)
        x = x.view(-1, 40*5*5)
        return self.layers(x)
    
cnn = Net().to(DEVICE)

이전 MNIST 코드는 하나의 mini batch 데이터만 학습했다면 이번에는 전체 데이터를 대상으로 학습을 진행합니다. 많은 학습을 거친다면 모델의 정확도가 높아지겠지만 성능을 높이는 테스트가 아니기 때문에 최소한의 학습 epcohs만 수행합니다.

optimizer = optim.Adam(cnn.parameters())
criterion = nn.CrossEntropyLoss()

hist_loss = []
hist_accr = []

epochs = 30

for epoch in range(epochs):

    for idx, (data, label) in enumerate(train_loader):
        data, label = data.to(DEVICE), label.to(DEVICE)
        output = cnn(data)
        loss = criterion(output, label)
        
        predict = torch.argmax(output, dim=-1) == label
        accuracy = predict.float().mean().item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        hist_loss.append(loss.item())
        hist_accr.append(accuracy)

        if idx % 100 == 0:
            print('Epoch {}, idx {}, Loss : {:.5f}, Accuracy : {:.5f}'.format(epoch, idx, loss.item(), accuracy))

학습이 완료되고 학습의 진행이 어떻게 되었는지 알기 위해서 사전에 정의한 hist_loss와 hist_accr을 사용해서 시각화 해보겠습니다.

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12,8))

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

학습이 완료된 후에 테스트 데이터를 사용해서 모델의 정확도를 확인해보았고 결과 값으로 Accuracy : 0.93750를 얻었습니다.

cnn.eval()

with torch.no_grad():
    for idx, (data, label) in enumerate(test_loader):
        data, label = data.to(DEVICE), label.to(DEVICE)
        output = cnn(data)
        loss = criterion(output, label)
    
        predict = torch.argmax(output, dim=-1) == label
        accuracy =  predict.float().mean().item()
    
        print('Accuracy : {:.5f}'.format(accuracy))

CNN MNIST 테스트 (PyTorch)

CNN 알고리즘을 MNIST 데이터셋을 활용해서 테스트해봅니다.

CNN 알고리즘에 대한 다양한 많은 설명이 있으니 자세한 내용은 아래의 강의를 참고하시기 바랍니다. 비록 작은 부분의 차이들은 있을 수 있지만 본 예제 역시 인터넷에 많은 소스 코드와 다르지 않습니다.
단, 아래의 영상은 텐서플로우로 설명하는 영상이지만 본 예제는 파이토치로 구현되어 있으며 학습도 전체 데이터를 대상으로 하지 않고 첫번째 미니배치만 학습하는 것으로 작성했습니다.

필요한 라이브러리를 임포트하고 GPU 사용 설정하는 부분과 MNIST 데이터를 로드하는 부분에 대해서는 자세한 설명을 하지 않고 지나가겠습니다. GPU 설정이 필요 없는 CPU 상에서 예제를 구동하는 경우는 device 설정을 하지 않고 넘어가셔도 무방합니다.

GPU 서버가 없는 경우는 무료로 Colab을 이용하시는 것도 추천합니다.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets

import matplotlib.pyplot as plt

USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")

BATCH_SIZE = 128

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data',
                   train=True,
                   download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data',
                   train=False, 
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=BATCH_SIZE, shuffle=True)

MNIST 데이터는 DataLoader로 불러왔습니다. 데이터는 (128,1,28,28) 형태로 149개로 분리되어 있습니다. 테스트 데이터 역시 마찬가지입니다. 128은 배치사이즈, 1은 채널 사이즈, (28*28)은 이미지의 크기입니다.

본 테스트에서는 전체 149개 데이터를 모두 학습하지 않고 1세트만 301회 학습을 수행합니다. 더 높은 정확도 얻고자 하시는 분은 전체 데이터를 통해 더 많은 학습을 해보시기 바랍니다.

dataset = next(iter(train_loader)) # 학습 데이터
x_data = dataset[0] # x 데이터
y_data = dataset[1] # label 데이터

dataset = next(iter(test_loader)) # 검증 데이터
x_test = dataset[0].to(DEVICE) # x 데이터
y_test = dataset[1].to(DEVICE) # label 데이터

각 데이터 셋에 어떤 이미지가 있는지 확인해보기 위해서 아래와 같은 코드를 수행합니다. MNIST 데이터 셋은 동일한 크기의 손글씨 이미지가 들어있기 때문에 각각의 이미지를 표시해보면 0-9까지의 손글씨 이미지가 저장되어 있는 것을 확인 할 수 있습니다.
아래와 같이 0번째 배열에 숫자 5가 있는 것을 확인 할 수 있습니다.

plt.imshow(x_data[0,0,:])

참고로 CNN의 입력 데이터의 Shape은 아래의 그림과 같습니다. 입력 데이터는 4개의 차원으로 구성되어 있습니다. 가장 먼저는 Batch_Size로 해당 이미지의 갯수를 의미합니다. 다음에 나오는 것은 이미지의 Channel입니다. 3인경우는 RGB 값을 가지고 1인 경우는 대부분 단일 색상으로 표현하는 값입니다. 그리고 나오는 값은 Height, Width 값입니다.

이런 상태에서 [0,0,:]의 의미는 0번째 이미지에서 첫번째 채널의 이미지 데이터를 가지고 온다는 의미가 됩니다.

이제 학습을 위한 모델을 구성합니다. 본 모델을 크게 두부분으로 되어 있습니다. self.convs는 convolution을 수행하는 부분으로 원본 이미지에서 특징정보를 추출하는 부분입니다. 이때 중요한 것은 각 Conv2d를 수행하며 어떤 형태의 아웃풋이 나오는지 확인하는 것이 중요합니다.

예를 들어 28*28 이미지를 kernel_size 3으로 계산하면 출력되는 이미지 사이즈는 (26*26)입니다. 어떻게 이렇게 나오는지는 아래 식에서 확인하실 수 있습니다.

https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html

하지만 매번 위의 식을 통해서 계산하는 것은 좀 귀찮고 힘든 입니다. 위의 공식을 간단한 함수로 구현한 내용을 공유해드립니다.

def conv_output_shape(h_w, kernel_size=1, stride=1, pad=0, dilation=1):
    from math import floor
    if type(kernel_size) is not tuple:
        kernel_size = (kernel_size, kernel_size)
    h = floor( ((h_w[0] + (2 * pad) - ( dilation * (kernel_size[0] - 1) ) - 1 )/ stride) + 1)
    w = floor( ((h_w[1] + (2 * pad) - ( dilation * (kernel_size[1] - 1) ) - 1 )/ stride) + 1)
    return h, w

위와 같은 식을 거쳐서 self.convs 레이어의 최종 output_shape은 총 5*5 이미지 사이즈를 가진 40장의 이미지 데이터를 얻을 수 있습니다.

이렇게 얻은 데이터는 self.layers를 거치다 보면 최종 0~9까지의 숫자 정보를 얻을 수 있습니다.

PyTorch의 Conv2d 패키지는 프로그래머가 간단히 Convolution Layer를 구성할 수 있도록 해줍니다. 프로그래머는 간단히 입력 채널의 수와 출력 채널의 수 그리고 커널 사이즈와 스트라이드 정보만 맞춰주면 자동으로 이미지를 구성해줍니다.

아래의 경우는 최초 28×28 이미지를 입력하고 커널을 3으로 맞춰서 앞선 함수를 통해서 출력 shape을 보면 26×26의 이미지를 출력한다는 것을 확인 할 수 있습니다. 또 다음 레이어는 커널을 3, 스트라이드를 2로 정의하고 이전에 입력된 이미지의 크기를 입력하면 출력 이미지는 12×12로 표시되는 것을 확인 할 수 있습니다. 이런 방법으로 마지막 이미지가 출력되는 크기는 5×5의 이미지가 됩니다.

그렇게 되면 마지막의 fully-connected layer에 들어가는 값은 40×5×5의 입력 값이 됩니다. 그리고 맨 마지막까지 Linear Layer를 거치게 되면 10개의 값으로 출력되고 이에 Softmax를 취하면 0-9 중에 하나의 값을 예측하게 됩니다.

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.convs = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=3), # input_channel, output_channel, kernel_size
            nn.ReLU(),
            nn.BatchNorm2d(10),
            nn.Conv2d(10, 20, kernel_size=3, stride=2),
            nn.ReLU(),
            nn.BatchNorm2d(20),
            nn.Conv2d(20, 40, kernel_size=3, stride=2)
        )
        
        self.layers = nn.Sequential(
            nn.Linear(40*5*5, 500),
            nn.ReLU(),
            nn.BatchNorm1d(500),
            nn.Linear(500,250),
            nn.Linear(250,100),
            nn.ReLU(),
            nn.BatchNorm1d(100),
            nn.Linear(100,50),
            nn.Linear(50, 10),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        x = self.convs(x)
        x = x.view(-1, 40*5*5)
        return self.layers(x)
    
cnn = Net().to(DEVICE)

이제 학습을 위한 모든 준비가 완료되었고 아래와 같이 학습을 수행합니다. 예제에서는 간단히 301회 학습을 수행했습니다. 학습이 진행되면서 loss와 accuracy 정보의 변화를 기록하기 위해서 list 변수를 각각 선언해줍니다.

또 100회 학습이 완료될 때마다 변화되는 loss와 accuracy 값을 화면에 출력해줍니다.

optimizer = optim.Adam(cnn.parameters())
criterion = nn.CrossEntropyLoss()

hist_loss = []
hist_accr = []

epochs = 301
for epoch in range(epochs):
    cnn.train()
    output = cnn(x_data)
    loss = criterion(output, y_data)
    
    predict = torch.argmax(output, dim=-1) == y_data
    accuracy = predict.float().mean().item()
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    hist_loss.append(loss)
    hist_accr.append(accuracy)
    
    if epoch % 100 == 0:
        print('epoch{}, {:.5f}, {:.5f}'.format(epoch, loss.item(), accuracy))

학습이 완료되고 loss와 accuracy 값을 그래프로 그려줍니다.
그래프를 보니 학습의 곡선이 완만하게 내려가고 정확도는 1에 가까운 값을 나타내어 학습이 잘이뤄지는 것을 확인할 수 있습니다.

그러나 training 데이터를 통한 학습정확도이기 때문에 검증용 데이터를 통해서 정확도 계산을 다시 할 필요가 있습니다.

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12,8))

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

검증용 데이터셋(test_dataloader)를 통해서 한 배치 정보를 얻어서 방금 수행한 모델의 정확도를 테스트해봅니다. 테스트 결과 0.7109375 값을 얻을 수 있었습니다. 높은 값은 아니지만 전체 469개 미니배치 중에서 1개 데이터셋만 테스트 했기 때문에 전체 데이터를 대상으로 테스트하면 보다 높은 정확도를 얻을 수 있을 것입니다.

cnn.eval()

with torch.no_grad():
    output = cnn(x_test)
    loss = criterion(output, y_test)
    
    predict = torch.argmax(output, dim=-1) == y_test
    accuracy =  predict.float().mean().item()
    
    print(accuracy)

Google Colab GPU Text-classification

Colaboratory(혹은 Colab)를 사용하면 브라우저에서 Python을 작성하고 실행할 수 있습니다. 장점이라면 별도의 구성이 필요 없고 무료로 GPU를 사용할 수 있다는 장점이 있습니다. 또 만든 코드를 간단하게 공유할 수도 있습니다.

감성분석(Text Classification)에 사용한 데이터는 네이버에서 공개한 영화 평점 정보입니다. 해당 데이터는 아래 링크에서 받을 수 있습니다.
https://github.com/e9t/nsmc

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import io

본 예제에서 사용할 konlpy를 Colab에 설치합니다.

!pip install konlpy
from konlpy.tag import Okt
okt = Okt()

Colab에서 사용할 파일을 사용자 계정의 구글 드라이브에 업로드합니다. 그리고 업로드한 파일 정보를 Colab에서 읽을 수 있도록 필요한 python 라이브러리를 등록해야합니다. 아래 코드를 실행하면 구글 계정에 인증할 수 있는 정보가 나오고 키값을 입력하면 드라이브에 접근할 수 있습니다.

from google.colab import drive
drive.mount('/content/gdrive')

구글 드라이브에 접근이 완료되면 파일이 있는 디렉토리 위치로 이동합니다.

from google.colab import drive
drive.mount('/content/gdrive')
%cd gdrive/My\ Drive/Colab\ Notebooks

해당 위치로 이동한 후에 %ls 명령을 실행시켜보면 해당 폴더에 있는 파일 리스트를 표시해줍니다. 파일 중에서 학습에 사용할 파일을 open하면 됩니다.

%ls
def read_data(filename):
    with io.open(filename, 'r',encoding='utf-8') as f:
        data = [line for line in f.read().splitlines()]
        data = data[1:]
    return data 

sentences = []
# 테스트를 위해 길이가 30 이하인 문장을 읽음
for sentence in read_data('./ratings_test.txt'):
  if len(sentence) <= 30:
    sentences.append(sentence)

해당 파일을 읽어보면 아래와 같은 형태로 데이터가 구성되어 있습니다. 이전 예제에서 설명했던 것처럼 1은 긍정적인 답변을 0은 부정적인 답변을 의미합니다.

['6270596|굳 ㅋ|1',
 '7898805|음악이 주가 된, 최고의 음악영화|1',
 '6315043|진정한 쓰레기|0',
 '7462111|괜찮네요오랜만포켓몬스터잼밌어요|1',
 '10268521|소위 ㅈ문가라는 평점은 뭐냐?|1' ...
class Vocab():
    def __init__(self):
        self.vocab2index = {'<pad>':0,'<unk>':1} # padding 0, unkown 1
        self.index2vocab = {0:'<pad>',1:'<unk>'} # 0 padding, 1 unkown
        self.vocab_count = {}
        self.n_vocab = len(self.vocab2index)
        
    def add_vocab(self, sentence):
        for word in sentence:
            if word not in self.vocab2index:
                self.vocab2index[word] = self.n_vocab
                self.index2vocab[self.n_vocab] = word
                self.vocab_count[word] = 1
                self.n_vocab += 1
            else:
                self.vocab_count[word] += 1

vo = Vocab()

def charStrip(s):
    s = s.replace('"','').replace('「','').replace('」','').replace('“','').replace('?','').replace('”','')
    s = s.replace('(',' ').replace(')',' ').replace('‘','').replace('’','').replace('□','').replace('◆','').replace('◇','')
    s = s.replace('[',' ').replace(']',' ').replace('○','').replace('△','').replace('◎','').replace('▣','').replace('◇','')
    s = s.replace('.',' ').replace('*',' ').replace('.',' ').replace('~',' ')
    
    return s

x = [] # text sentence
y = [] # label
for sentence in sentences:
    arr = sentence.split('|')
    if(len(arr) == 3):
      sentence = okt.morphs(charStrip(arr[1]))
      
      vo.add_vocab(sentence)
      x.append(sentence) 
      y.append(float(arr[2])) 
MAX_SEQUENCE_LENGTH = 0

for sentence in x:
    if MAX_SEQUENCE_LENGTH < len(sentence): MAX_SEQUENCE_LENGTH = len(sentence)

MAX_SEQUENCE_LENGTH

데이터 중에서 가장 긴 문장을 확인해봅니다. 이 문장의 크기가 Sequence Length가 됩니다. 이 문장의 길이보다 작은 문장의 경우 빈칸은 <unk> 값으로 채워줍니다.

def tensorize(vocab, sentence):
    idx = [vocab.vocab2index[word] for word in sentence]
    #return torch.Tensor(idx).long().item()
    return idx

tmp_tensor = []
for sentence in x:
    tmp = tensorize(vo, sentence)
    tmp_zero = np.zeros(MAX_SEQUENCE_LENGTH)
    
    for i,val in enumerate(tmp):
        tmp_zero[i] = val
        
    tmp_tensor.append(tmp_zero)
    
x_data = torch.Tensor(tmp_tensor).long()
y_data = torch.Tensor([float(t) for t in y])
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
output : device(type='cuda')

구글 Colab에서는 GPU를 사용할 수 있기 때문에 필요한 설정을 해줍니다. CPU로 연산할 때보다 훨씬 빠른 계산속도를 보여줍니다.

학습용 데이터는 8/2로 학습용 Train Data / Valid Data 데이터로 나눠줍니다.

DATA_LENGTH = x_data.size(0)

train_cnt = int(DATA_LENGTH*.8)
valid_cnt = DATA_LENGTH - train_cnt
print('train_cnt, valid_cnt = ',train_cnt,valid_cnt)

idx = torch.randperm(DATA_LENGTH)
x_train = torch.index_select(x_data, dim=0, index=idx).to(device).split([train_cnt, valid_cnt], dim=0)
y_train = torch.index_select(y_data, dim=0, index=idx).to(device).split([train_cnt, valid_cnt], dim=0)

각 데이터셋은 pytorch의 Dataset, DataLoader를 사용해서 배치사이즈로 나눠줍니다. 학습할 데이터가 많은 경우에 많은 데이터를 한번에 읽으면 메모리 부족현상이 발생하는데 Dataset을 배치사이즈로 분리해서 로딩하면 메모리를 적게 사용하게 되어 큰 데이터도 학습할 수 있습니다.

from torch.utils.data import Dataset, DataLoader

class SimpanDataset(Dataset):
    
    def __init__(self, data, label):
        super().__init__()
        self.data = data
        self.labels = label
        
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]
    
# train_loader
train_loader = DataLoader(dataset=SimpanDataset(x_train[0], y_train[0]), batch_size=250, shuffle=True)
valid_loader = DataLoader(dataset=SimpanDataset(x_train[1], y_train[1]), batch_size=250, shuffle=False)

학습용 모델을 아래와 같이 설정합니다. 학습 모델은 Embedding -> BiLSTM -> Softmax 레이어를 통과하면서 최종 output을 만들어냅니다.
단, output은 모든 Sequence의 데이터를 사용하지 않고 마지막 시퀀스의 값만 사용하며 LSTM의 모델에서 bidirectional을 True로 설정했기 때문에 output*2의 값이 리턴됩니다. 학습에 최종 결과물은 0과 1이기 때문에 hidden_size는 2입니다.

class SimpanClassificationModel(nn.Module):
    
    def __init__(self, input_size, hidden_size):
        super().__init__()
        
        self.embedding = nn.Embedding(input_size, 300)
        
        self.rnn = nn.LSTM(input_size=300, hidden_size=100, num_layers=4, batch_first=True, bidirectional=True)
        
        self.layers = nn.Sequential(
            nn.ReLU(),
            nn.Linear(100*2,100),
            nn.Linear(100,30),
            
            nn.Linear(30, hidden_size),
        )
        
        self.softmax = nn.Softmax(dim=-1)
        
    def forward(self, x):
        y = self.embedding(x)
        y,_ = self.rnn(y)
        y = self.layers(y)
        
        return self.softmax(y[:,-1,:])
    
input_size = vo.n_vocab
hidden_size = torch.unique(y_train[1]).size(dim=-1)

model = SimpanClassificationModel(input_size, hidden_size)
model = model.cuda()
# loss & optimizer setting
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

hist_loss = []
hist_accr = []

epochs = 501
# start training
model.train()

for epoch in range(epochs):
    epoch_loss = 0
    for x,y in train_loader:
      x, y = x.to(device), y.to(device)
      output = model(x)
      loss = criterion(output, y.long())
        
      optimizer.zero_grad()
      loss.backward()
      optimizer.step()
        
      epoch_loss += loss.item()
      accuracy = (torch.argmax(output, dim=-1) == y).float().mean().item()
    
      
    hist_loss.append(epoch_loss)
    hist_accr.append(accuracy)
        
    print('Cost: {:.6f}, Accuracy : {:.6f}'.format(loss.item(),accuracy))
    print('--{}--'.format(epoch))

학습의 진행상황을 기록하기 위해서 2개의 배열(hist_loss, hist_accr)을 사용합니다.

hist_loss는 loss값의 변화를 기록하는 배열이며 hist_accr은 해당 모델의 정확도 정보를 얻기 위해서 만든 배열입니다. 학습이 진행되며 해당 배열에 데이터가 기록되고 matplotlib.pyplot을 사용해서 그래프를 그려봅니다.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12, 8)) 

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

plt.show()
model.eval()
    
for xv, yv in valid_loader:
    output = model(x)
    
    accuracy = (torch.argmax(output, dim=-1) == y).float().mean().item()

    hist_loss.append(loss.item())
    hist_accr.append(accuracy)

    print('Accuracy : {:.6f}'.format(accuracy))

모델의 학습이 완료된 후 valid data를 통해서 학습 모델의 정확도를 알아봅니다.

Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667

인공신경망을 이용한 분류

인공신경망을 이용하여 데이터를 분류하는 문제를 테스트해보고자합니다.

필요한 라이브러리들을 다음과 같이 로드합니다.

import numpy as np

import torch
import torch.nn as nn
import torch.optim as optim

분류 문제 해결을 위해 사용한 데이터는 zoo 데이터셋입니다. 해당 데이터는 동물의 16가지 특징 정보들 예를 들어 깃털이 있는지 알을 낳는지 비행이 가능한지 등을 체크해서 이 동물이 어느 분류에 들어가는지를 0-7까지의 중에 하나로 분류한 데이터 셋입니다.

xy = np.loadtxt('./zoo.csv', delimiter=',', dtype=np.float32)

해당 데이터의 shape을 보면 (101,17)로 되어 있습니다. 총 16개의 특징(1은 분류)을 가진 101개의 데이터라는 의미입니다.

x_data = xy[:,0:-1]
y_data = xy[:,-1]

x_data = torch.Tensor(x_data)
y_data = torch.Tensor(y_data).long()
x_data.size(), y_data.size() # (torch.Size([101, 16]), torch.Size([101]))

data_length = len(xy)

101개의 데이터를 모두 학습하지 않고 약 8/2정도로 나눠서 훈련용 데이터와 검증용 데이터를 만듭니다. 훈련용 데이터를 만들기 전에 데이터를 랜덤하게 섞어줍니다.

batch_size = .8

train_cnt = int(data_length * batch_size)
valid_cnt = data_length - train_cnt

idx = torch.randperm(data_length)
x = torch.index_select(x_data, dim=0, index=idx).split([train_cnt, valid_cnt], dim=0)
y = torch.index_select(y_data, dim=0, index=idx).split([train_cnt, valid_cnt], dim=0)

학습을 위한 모델을 만들어줍니다. 모델은 16개 데이터를 입력 받아서 최종적으로 7개의 데이터셋을 출력하는 형태입니다.

class ANN(nn.Module):
    
    def __init__(self, D_in, H, D_out):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            nn.Linear(H, D_out),
        )
    
    def forward(self, x):
        return self.layers(x)

D_in, H, D_out = x_data.size(dim=-1), 100, torch.unique(y_data).size(dim=0)
model = ANN(D_in, H, D_out)

loss와 최적화를 위해 함수를 선언합니다. 또 어떻게 loss가 변화하고 그에따라 정확도가 올라가는지를 표시하기 위해서 hist_loss, hist_accr을 선언하고 학습이 완료될 때마다 데이터를 입력해줍니다.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

hist_loss = []
hist_accr = []

epochs = 501

for epoch in range(epochs):
    model.train()
    # loss
    y_pred = model(x[0])
    loss = criterion(y_pred, y[0])
    
    # accuracy
    predict = torch.argmax(y_pred, dim=-1).data == y[0]
    accr = predict.float().mean().item()
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    hist_loss.append(loss.item())
    hist_accr.append(accr)
    
    if epoch%10==0:
        #print(y_pred)
        print('Epoch {:4d}/{} Cost: {:.6f} Accuracy:{}'.format(
            epoch, epochs, loss.item(), accr
        ))

학습이 반복될때마다 loss가 낮아지고 정확도가 올라가는 모습을 볼 수 있습니다. loss도 완만하게 내려가는 것을 보니 learning_rate가 적절히 선언된것 같습니다.

import matplotlib.pyplot as plt

plt.plot(hist_loss)
plt.plot(hist_accr)
plt.legend(['Loss','Accuracy'])
plt.title('Loss/Legend')
plt.xlabel('Epoch')
plt.show()

이제 학습이 완료되었고 테스트용 데이터셋을 활용해서 모델의 정확도를 검증해본결과 Accuracy: 0.95238 데이터를 얻었습니다. 학습을 더 많이 진행한다면 더 높은 정확도를 얻을 수 있을 거라고 생각합니다.

model.eval()

with torch.no_grad():
    predict = model(x[1])
    
    # accuracy
    predict = torch.argmax(predict, dim=-1).data == y[1]
    accr = predict.float().mean().item()
    
    print("Accuracy: %.5f" % accr)

Seq2Seq 문장번역

파이토치 Seq2Seq 예제

import random
import torch
import torch.nn as nn
import torch.optim as optim
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
raw = ['I called Tom for help.	나는 톰에게 도움을 요청했다.',
'I do not like science.	나는 과학이 싫어.',
'I hate myself as well.	나도 내 자신을 싫어해.',
'I knew Tom would lose.	톰이 질 거라는 것을 난 알고 있었어.',
'I know Tom personally.	난 톰을 개인적으로 알고 있어.',
'I like Korean cuisine.	전 한국 요리가 좋아요.',
'I like Korean cuisine.	전 한국 요리를 좋아해요.',
'I like helping others.	나는 남을 돕는 것을 좋아한다.',
'I really like puppies.	저는 강아지가 정말 좋아요.',
'I run faster than Tom.	나는 톰보다 빠르게 달릴 수 있어.',
'I think Tom is lonely.	톰이 외로워하는 것 같아.',
'I think they like you.	그들이 널 좋아하는 것 같아.',
'I want to go to sleep.	나 자러 가고 싶어.',
'I want to go to sleep.	나 자고 싶어.',
'I want to visit Korea.	나는 한국에 들르고 싶다.']

사용한 데이터는 http://www.manythings.org/anki/ 에서 kor-eng.zip 파일을 다운로드 받아 일부 데이터만 사용했습니다. 해당 사이트에 들어가면 한국어 외에도 다양한 형태의 파일을 다운 받을 수 있습니다.

SOS_token = 0 # 문장의 시작 Start of Sentence
EOS_token = 1 #  문장의 끝 End of Sentence
class Vocab:
    def __init__(self):
        self.vocab2index = {"<SOS>":SOS_token, "<EOS>":EOS_token}
        self.index2vocab = {SOS_token:"<SOS>", EOS_token:"<SOS>"}
        self.vocab_count = {}
        self.n_vocab = len(self.vocab2index)
    
    def add_vocab(self, sentence):
        for word in sentence.split(' '):
            if word not in self.vocab2index:
                self.vocab2index[word] = self.n_vocab
                self.vocab_count[word] = 1
                self.index2vocab[self.n_vocab] = word
                self.n_vocab += 1
            else:
                self.vocab_count[word] += 1
# declare simple encoder
class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(Encoder, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = nn.Embedding(input_size, hidden_size) # Embedding(17, 16)
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, x, hidden):
        x = self.embedding(x).view(1, 1, -1)
        x, hidden = self.gru(x, hidden)
        return x, hidden

    
# declare simple decoder
class Decoder(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(Decoder, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size, num_layers=1, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, hidden):
        x = self.embedding(x).view(1, 1, -1)
        x, hidden = self.gru(x, hidden) # lstm을 사용할 경우 해당 위치 수정
        x = self.softmax(self.out(x[0]))
        return x, hidden
# read and preprocess the corpus data
def preprocess(corpus):
    print("reading corpus...")
    pairs = []
    for line in corpus:
        pairs.append([s for s in line.strip().lower().split("\t")])
    print("Read {} sentence pairs".format(len(pairs)))

    pairs = [pair for pair in pairs]
    print("Trimmed to {} sentence pairs".format(len(pairs)))

    source_vocab = Vocab()
    target_vocab = Vocab()

    print("Counting words...")
    for pair in pairs:
        source_vocab.add_vocab(pair[0])
        target_vocab.add_vocab(pair[1])
    print("source vocab size =", source_vocab.n_vocab)
    print("target vocab size =", target_vocab.n_vocab)

    return pairs, source_vocab, target_vocab

# 데이터셋, 입력단어정보, 출력단어정보
pairs, source_vocab, target_vocab = preprocess(raw)

훈련용 입출력 데이터셋을 위와 같이 만든후 이제 인코더, 디코더 모델을 만들어야 합니다. 먼저 만들기 전에 인코더-디코더의 입출력 정보에 대하여 직접 그림으로 그려보시기를 추천합니다. 가장 좋은 것은 노트에 펜으로 그려보시는 것이 좋겠지만 그렇지 않다면 머리속으로 어떤 입력이 들어오고 어떤 출력이 나가는지에 대한 정보를 설계하는 과정이 필요합니다.

이런 과정이 없으면 나중에 인코더와 디코더를 설계할 때에 혼동하기 쉽기 때문에 반드시 모델의 입출력 흐름을 구상해보시기 바랍니다.

본 예제의 인코더-디코더 정보는 다음과 같습니다.
인코더 : input_vector(41) -> Embedding(41,30) -> LSTM(30,30)
디코더 : Embedding(52,30) -> LSTM(30, 52) – hidden_vector(52)

enc_hidden_size = 30
dec_hidden_size = enc_hidden_size
enc = Encoder(source_vocab.n_vocab, enc_hidden_size).to(device)
dec = Decoder(dec_hidden_size, target_vocab.n_vocab).to(device)
def tensorize(vocab, sentence):
    idx = [vocab.vocab2index[word] for word in sentence.lower().split(' ')]
    idx.append(vocab.vocab2index['<EOS>'])
    return torch.Tensor(idx).long().to(device).view(-1,1)
tensorize(source_vocab, 'I called Tom for help.')
output : tensor([[2], [3], [4], [5], [6], [1]])
training_source = [tensorize(source_vocab, pair[0]) for pair in pairs]
training_target = [tensorize(target_vocab, pair[1]) for pair in pairs]

Train

loss_total = 0
number_epoch = 5001

encoder_optimizer = optim.SGD(enc.parameters(), lr=0.01)
decoder_optimizer = optim.SGD(dec.parameters(), lr=0.01)

criterion = nn.NLLLoss()

for epoch in range(number_epoch):
    epoch_loss = 0
    
    for i in range(len(training_source)):
        encoder_optimizer.zero_grad()
        decoder_optimizer.zero_grad()
        
        source_tensor = training_source[i]
        target_tensor = training_target[i]

        encoder_hidden = torch.zeros([1, 1, enc.hidden_size]).to(device)

        source_length = source_tensor.size(0)
        target_length = target_tensor.size(0)
        
        loss = 0

        for enc_input in range(source_length):
            _, encoder_hidden = enc(source_tensor[enc_input], encoder_hidden)

        decoder_input = torch.Tensor([[SOS_token]]).long().to(device)
        decoder_hidden = encoder_hidden # connect encoder output to decoder input

        for di in range(target_length):
            decoder_output, decoder_hidden = dec(decoder_input, decoder_hidden)
            #print(decoder_output, target_tensor[di], criterion(decoder_output, target_tensor[di]))
            loss += criterion(decoder_output, target_tensor[di])
            decoder_input = target_tensor[di]  # teacher forcing
        
        loss.backward()

        encoder_optimizer.step()
        decoder_optimizer.step()
        
        #print(loss.item(),target_length)
        epoch_loss += loss.item()/target_length
        #loss_total += loss_epoch
    if epoch % 100 == 0:
        print('--- epoch {}, total loss {} '.format(epoch,float(epoch_loss/15)))

Evaluate

for pair in pairs:
    print(">", pair[0])
    print("=", pair[1])
    source_tensor = tensorize(source_vocab, pair[0])
    source_length = source_tensor.size()[0]
    encoder_hidden = torch.zeros([1, 1, enc.hidden_size]).to(device)

    for ei in range(source_length):
        _, encoder_hidden = enc(source_tensor[ei], encoder_hidden)
        #print(encoder_hidden.size()) # 1,1,16

    decoder_input = torch.Tensor([[SOS_token]], device=device).long()
    decoder_hidden = encoder_hidden
    decoded_words = []

    for di in range(20):
        decoder_output, decoder_hidden = dec(decoder_input, decoder_hidden)
        #print('decoder_iput',decoder_input, 'decoder_output',decoder_output)
        _, top_index = decoder_output.data.topk(1)
        if top_index.item() == EOS_token:
            decoded_words.append("<EOS>")
            break
        else:
            decoded_words.append(target_vocab.index2vocab[top_index.item()])

        decoder_input = top_index.squeeze().detach()

    predict_words = decoded_words
    predict_sentence = " ".join(predict_words)
    print("<", predict_sentence)
    print("")
> i called tom for help.
= 나는 톰에게 도움을 요청했다.
< 나는 톰에게 도움을 요청했다. <EOS>

> i do not like science.
= 나는 과학이 싫어.
< 나는 과학이 싫어. <EOS>

> i hate myself as well.
= 나도 내 자신을 싫어해.
< 나도 내 자신을 싫어해. <EOS>

> i knew tom would lose.
= 톰이 질 거라는 것을 난 알고 있었어.
< 톰이 질 거라는 것을 난 알고 있었어. <EOS>

> i know tom personally.
= 난 톰을 개인적으로 알고 있어.
< 난 톰을 개인적으로 알고 있어. <EOS>

> i like korean cuisine.
= 전 한국 요리가 좋아요.
< 전 한국 요리를 좋아해요. <EOS>

> i like korean cuisine.
= 전 한국 요리를 좋아해요.
< 전 한국 요리를 좋아해요. <EOS>

> i like helping others.
= 나는 남을 돕는 것을 좋아한다.
< 나는 남을 돕는 것을 좋아한다. <EOS>

> i really like puppies.
= 저는 강아지가 정말 좋아요.
< 저는 강아지가 정말 좋아요. <EOS>

> i run faster than tom.
= 나는 톰보다 빠르게 달릴 수 있어.
< 나는 톰보다 빠르게 달릴 수 있어. <EOS>

> i think tom is lonely.
= 톰이 외로워하는 것 같아.
< 톰이 외로워하는 것 같아. <EOS>

> i think they like you.
= 그들이 널 좋아하는 것 같아.
< 그들이 널 좋아하는 것 같아. <EOS>

> i want to go to sleep.
= 나 자러 가고 싶어.
< 나 자고 싶어. <EOS>

> i want to go to sleep.
= 나 자고 싶어.
< 나 자고 싶어. <EOS>

> i want to visit korea.
= 나는 한국에 들르고 싶다.
< 나는 한국에 들르고 싶다. <EOS>