seq2seq기반 덧셈 모형 빌드(with Gluon)

필자가 Gluon을 시작한 계기는 바로 Keras로 seq2seq기반의 네트워크를 구축하기가 매우 어렵거나 모호해서 였다. 사실 간단한 영한 기계번역기를 Keras로 만들다가 한계에 부딧혀 포기했다. 그 원인은 아래 도표를 보면 알겠지만 학습과 예측의 네트웍 플로우가 다른데 있는데, 이럴 경우 예측 코드는 Keras에서 작성하기가 어려워진다. 사실 어렵다기 보다는 모호한 코드를 짜게 될 가능성이 많다. 그래서 해당 코드를 작성하고 이틀만 지나도 내가 뭘 했는지 알기 어렵다. 아마도 이런 이유때문에 특히 NLP하는 분들은 PyTorch를 선호하는게 아닐까 한다. 하지만 필자는 올해 10월에 나온 Gluon을 기반으로 구축해 봤다. 왜냐면 아직 이 예제는 Gluon으로 작성된게 없기 때문이다.

해당 예제는 이미 keras example에 아주 잘 작성되어 있다. 간단히 설명하면 encoder의 마지막 시퀀스 출력 벡터를 디코더 길이만큼 반복해 context 벡터를 만들어 입력을 넣어주는 방식이다. 다만 구현이 해당 방식이 아니면 복잡해지는 단점이 있다는 것이다. 그래서 기계번역에서 성능이 좋게 나온다는 오리지널 seq2seq 방식을 Keras 개발자기 직접 블로그에 올리기도 하였다. 하지만 Keras에서 inference loop 쪽 코드를 보면 한계가 여실히 드러나는 것을 알 수 있는데, 필자도 해당 코드 기반으로 영한번역기를 구축하다 결국 디버깅 및 inference code가 복잡하고 어려워지면서 포기했다.

사실 Gluon으로 작업을 하면서도 이 간단한 문제도 예측성능이 나오지 않아 정말 많은 시행착오를 했다(퇴근 후 3일 동안 새벽 3시까지…). 아래 코드를 보면 알겠지만 마지막 레이어에서 fully connected가 3차원일 경우 어떻게 코드를 작성해야 마지막 차원에서 원하는 Softmax를 찾을지.. (사실 이 부분은 tensorflow 코드와 Keras 코드를 알고 있었는데, Gluon에서는 dense layer의 옵션만으로도 적용이 가능했다. 그런데 Keras는 TimeDistribution 레이어로 해결된다. 예제가 많기 때문에 그냥 아무 생각없이 가져다 쓰기 바빴던 내가 참.. 원망 스럽다.) 그리고 encoder,decoder state를 어떻게 전달해야 되는지, 초기 코드의 loss가 80까지 나오는데 (결과적으로 원인은 데이터 생성에서 버그에 있었지만), 원인을 어떻게 찾을지 ….그리고 중간 중간 reshape이나 concat을 하게 되는데 다음 레이어에 맞게 dimension만 맞추려 하다가는 잘못 변환된 데이터를 입력하는 경우가 많고, 이 때문에 학습이 안되는 경우가 많았다. 역시나 이런 부분에 대한 고민과 훈련이 안된 티가 난다. 사실 이런 부분은 Keras를 쓸때는 거의 신경쓰지 않는 문제긴 하지만, 이쪽 영역을 한다면 반드시 익숙해져야 될 부분이다. 머리속에 3차원 4차원의 행렬을 생각하면서 코딩해야 되는 어려움은 해본 분들만 알 수 있을 것이다.

일단 하소연은 이정도로 하겠고… seq2seq에 대한 문제정의를 해보도록 하겠다.

일단 학습 코드인데, 학습시에는 3가지 데이터가 쓰인다. 먼저 “S1+5E”와 같은 수식 텍스트 그리고 decoder에 입력으로 들어갈 정답 “S6E”. 그리고 실제 loss를 계산할 Y인 “6E”.여기서 “S”, “E”는 각각 텍스트의 시작과 마지막을 의미한다.

decoder쪽은 입력으로 t-1의 텍스트가 입려되면, t 시점의 텍스트를 예측하게끔 훈련이 되며, encoder에서는 입력 수식의 데이터의 시계열적인 정보를 2개의 state(여기선 LSTM을 사용했다)로 decoder에 전달하게 된다.

그림에서 마지막 예측 결과를 종합하는 dense layer는 생략되었는데, 이 부분은 코드를 참고하길 바란다.

예측시에는 Encoder에 입력되는 텍스트만 들어가게 되는데, Decoder의 t-1시점의 출력이 t시점의 입력으로 다시 들어가게 되는 부분이 네트워크 구조가 학습시와 달라지는 부분이다. 이 부분 때문에 Keras 구현 난이도가 200% 올라가는 것이다.

그럼 코드를 보면서 주석으로 설명을 올려 보겠다.

In [1]:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from mxnet import nd as  F
import mxnet as mx
from  mxnet import gluon
from mxnet.gluon import nn, rnn
import mxnet.autograd as autograd
from mxnet import nd as F

아래는 학습 데이터 생성 코드이다. 데이터 생성 예제 코드는 인터넷에 많은데… 이 생성 부분은 주로 이곳코드를 참고했다.

In [2]:
def n(digits=3):
    number = ''
    for i in range(np.random.randint(1, digits + 1)):
        number += np.random.choice(list('0123456789'))
    return int(number)


def padding(chars, maxlen):
    return chars + ' ' * (maxlen - len(chars))


N = 50000
N_train = int(N * 0.9)
N_validation = N - N_train

digits = 3  # 최대 자릿수
input_digits = digits * 2 + 3  # 예: 123+456
output_digits = digits + 3 # 500+500 = 1000 이상이면 4자리가 된다

added = set()
questions = []
answers = []
answers_y = []

while len(questions) < N:
    a, b = n(), n()  # 두 개의 수를 적당히 생성한다

    pair = tuple(sorted((a, b)))
    if pair in added:
        continue

    question = 'S{}+{}E'.format(a, b)
    question = padding(question, input_digits)
    answer = 'S' + str(a + b) + 'E'
    answer = padding(answer, output_digits)
    answer_y = str(a + b) + 'E'
    answer_y = padding(answer_y, output_digits)
    

    added.add(pair)
    questions.append(question)
    answers.append(answer)
    answers_y.append(answer_y)

chars = '0123456789+SE '
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))

X = np.zeros((len(questions), input_digits, len(chars)), dtype=np.integer)
Y = np.zeros((len(questions), digits + 3, len(chars)), dtype=np.integer)
Z = np.zeros((len(questions), digits + 3, len(chars)), dtype=np.integer)

for i in range(N):
    for t, char in enumerate(questions[i]):
        X[i, t, char_indices[char]] = 1
    for t, char in enumerate(answers[i]):
        Y[i, t, char_indices[char]] = 1
    for t, char in enumerate(answers_y[i]):
        Z[i, t, char_indices[char]] = 1
    
X_train, X_validation, Y_train, Y_validation, Z_train, Z_validation = \
    train_test_split(X, Y, Z, train_size=N_train)

예시 테스트 데이터를 생성하는 함수

In [4]:
def gen_n_test(N):
    q = []
    y = []
    for i in range(N):
        a, b = n(), n() 
        question = '{}+{}'.format(a, b)
        answer_y = str(a + b)
        q.append(question)
        y.append(answer_y)
    return(q,y)

적중 유무를 색으로 표시하기 위해…

In [6]:
class colors:
    ok = '\033[92m'
    fail = '\033[91m'
    close = '\033[0m'
In [52]:
class calculator(gluon.Block):
    
    def __init__(self, n_hidden,in_seq_len, out_seq_len, vacab_size , **kwargs):
        super(calculator,self).__init__(**kwargs)
        #입력 시퀀스 길이
        self.in_seq_len = in_seq_len
        #출력 시퀀스 길이 
        self.out_seq_len = out_seq_len
        # LSTM의 hidden 개수 
        self.n_hidden = n_hidden
        #고유문자개수 
        self.vacab_size = vacab_size
        
        with self.name_scope():
            self.encoder = rnn.LSTMCell(hidden_size=n_hidden)
            self.decoder = rnn.LSTMCell(hidden_size=n_hidden)
            self.batchnorm = nn.BatchNorm(axis=2)
            #flatten을 false로 할 경우 마지막 차원에 fully connected가 적용된다. 
            self.dense = nn.Dense(self.vacab_size,flatten=False)
            
    def forward(self, inputs, outputs):
        """
        학습 코드 
        """
        #encoder LSTM
        enout, (next_h, next_c) = self.encoder.unroll(inputs=inputs, length=self.in_seq_len, merge_outputs=True)
        
        #decoder LSTM
        for i in range(self.out_seq_len):
            #out_seq_len 길이만큼 LSTMcell을 unroll하면서 출력값을 적재한다. 
            deout, (next_h, next_c) = self.decoder(outputs[:,i,:], [next_h, next_c], )
            if i == 0:
                deouts = deout
            else:
                deouts = F.concat(deouts, deout, dim=1)
        #2dim -> 3dim 으로 reshape 
        deouts = F.reshape(deouts, (-1, self.out_seq_len, self.n_hidden))
        deouts = self.batchnorm(deouts)
        deouts_fc = self.dense(deouts)
        return(deouts_fc)
    
    def calulation(self, input_str, char_indices, indices_char, input_digits=9, lchars=14, ctx=mx.gpu(0)):
        """
        inference 코드 
        """
        #앞뒤에 S,E 코드 추가 
        input_str = 'S' + input_str + 'E'
        #string to one-hot coding 
        X = F.zeros((1, input_digits, lchars), ctx=ctx)
        for t, char in enumerate(input_str):
            X[0, t, char_indices[char]] = 1
        #디코더의 초기 입력값으로 넣을 'S'를 one-hot coding한다. 
        Y_init = F.zeros((1, lchars), ctx=ctx)
        Y_init[0,char_indices['S']] = 1
        #인코더 출력값을 도출한다. 
        enout, (next_h, next_c) = self.encoder.unroll(inputs=X, length=self.in_seq_len, merge_outputs=True)
        deout = Y_init
        #출력 시퀀스 길이만큼 순회 
        for i in range(self.out_seq_len):
            deout, (next_h, next_c) = self.decoder(deout, [next_h, next_c])
            #batchnorm을 적용하기 위해 차원 증가/원복 
            deout = F.expand_dims(deout,axis=1)
            deout = self.batchnorm(deout)
            deout = deout[:,0,:]
            #'S'의 다음 시퀀스 출력값도출 
            deout_sm = self.dense(deout)
            deout = F.one_hot(F.argmax(F.softmax(deout_sm, axis=1), axis=1), depth=self.vacab_size)
            if i == 0:
                ret_seq = indices_char[F.argmax(deout_sm, axis=1).asnumpy()[0].astype('int')]
            else:
                ret_seq += indices_char[F.argmax(deout_sm, axis=1).asnumpy()[0].astype('int')]
            if ret_seq[-1] == ' ' or ret_seq[-1] == 'E':
                break
            #공백 padding 제거 후 리턴
        return(ret_seq.strip('E').strip())

                

data iterator 생성

In [53]:
tr_set = gluon.data.ArrayDataset(X_train, Y_train, Z_train)
tr_data_iterator = gluon.data.DataLoader(tr_set, batch_size=256, shuffle=True)

te_set =gluon.data.ArrayDataset(X_validation, Y_validation, Z_validation)
te_data_iterator = gluon.data.DataLoader(te_set, batch_size=256, shuffle=True)
In [61]:
ctx = mx.gpu()

#모형 인스턴스 생성 및 트래이너, loss 정의 
model = calculator(300, 9, 6, 14)
model.collect_params().initialize(mx.init.Xavier(), ctx=ctx)

trainer = gluon.Trainer(model.collect_params(), 'rmsprop') 
loss = gluon.loss.SoftmaxCrossEntropyLoss(axis = 2, sparse_label=False)
In [62]:
print(model)
calculator(
  (encoder): LSTMCell(None -> 1200)
  (decoder): LSTMCell(None -> 1200)
  (batchnorm): BatchNorm(axis=2, eps=1e-05, momentum=0.9, fix_gamma=False, in_channels=None)
  (dense): Dense(None -> 14, linear)
)
In [63]:
def calculate_loss(model, data_iter, loss_obj, ctx=ctx):
    test_loss = []
    for i, (x_data, y_data, z_data) in enumerate(data_iter):
        x_data = x_data.as_in_context(ctx).astype('float32')
        y_data = y_data.as_in_context(ctx).astype('float32')
        z_data = z_data.as_in_context(ctx).astype('float32')
        with autograd.predict_mode():
            z_output = model(x_data, y_data)
            loss_te = loss_obj(z_output, z_data)
        curr_loss = mx.nd.mean(loss_te).asscalar()
        test_loss.append(curr_loss)
    return(np.mean(test_loss))
In [64]:
epochs = 201

### 학습 코드 
tot_test_loss = []
tot_train_loss = []
for e in range(epochs):
    train_loss = []
    for i, (x_data, y_data, z_data) in enumerate(tr_data_iterator):
        x_data = x_data.as_in_context(ctx).astype('float32')
        y_data = y_data.as_in_context(ctx).astype('float32')
        z_data = z_data.as_in_context(ctx).astype('float32')
        with autograd.record():
            z_output = model(x_data, y_data)
            loss_ = loss(z_output, z_data)
        loss_.backward()
        trainer.step(x_data.shape[0])
        curr_loss = mx.nd.mean(loss_).asscalar()
        train_loss.append(curr_loss)
    if e % 10 == 0:
        # 매 10 에폭마다 예제 테스트 결과 출력 
        q, y = gen_n_test(10)
        for i in range(10):
            with autograd.predict_mode():
                p = model.calulation(q[i], char_indices, indices_char).strip()
                iscorr = 1 if p == y[i] else 0
                if iscorr == 1:
                    print(colors.ok + '☑' + colors.close, end=' ')
                else:
                    print(colors.fail + '☒' + colors.close, end=' ')
                print("{} = {}({}) 1/0 {}".format(q[i], p, y[i], str(iscorr) ))
    #caculate test loss
    test_loss = calculate_loss(model, te_data_iterator, loss_obj = loss, ctx=ctx) 

    print("Epoch %s. Train Loss: %s, Test Loss : %s" % (e, np.mean(train_loss), test_loss))    
    tot_test_loss.append(test_loss)
    tot_train_loss.append(np.mean(train_loss))
 93+276 = 1000(369) 1/0 0
 67+7 = 1000(74) 1/0 0
 98+765 = 1000(863) 1/0 0
 33+4 = 432(37) 1/0 0
 2+9 = 100(11) 1/0 0
 5+378 = 1000(383) 1/0 0
 598+2 = 1000(600) 1/0 0
 176+389 = 1326(565) 1/0 0
 5+24 = 550(29) 1/0 0
 2+62 = 228(64) 1/0 0
Epoch 0. Train Loss: 1.18849, Test Loss : 1.12554
Epoch 1. Train Loss: 1.12178, Test Loss : 1.10778
Epoch 2. Train Loss: 1.09253, Test Loss : 1.07927
Epoch 3. Train Loss: 1.0283, Test Loss : 0.991627
Epoch 4. Train Loss: 0.921772, Test Loss : 0.893707
Epoch 5. Train Loss: 0.831767, Test Loss : 0.873785
Epoch 6. Train Loss: 0.758187, Test Loss : 0.747362
Epoch 7. Train Loss: 0.687181, Test Loss : 0.694492
Epoch 8. Train Loss: 0.606952, Test Loss : 0.554216
Epoch 9. Train Loss: 0.497511, Test Loss : 0.481963
 831+966 = 1797(1797) 1/0 1
 682+49 = 731(731) 1/0 1
 98+445 = 545(543) 1/0 0
 9+310 = 319(319) 1/0 1
 31+9 = 119(40) 1/0 0
 17+452 = 460(469) 1/0 0
 1+757 = 758(758) 1/0 1
 1+806 = 806(807) 1/0 0
 242+504 = 753(746) 1/0 0
 6+6 = 122(12) 1/0 0
Epoch 10. Train Loss: 0.37133, Test Loss : 0.342722
Epoch 11. Train Loss: 0.275218, Test Loss : 0.230928
Epoch 12. Train Loss: 0.206047, Test Loss : 0.163056
Epoch 13. Train Loss: 0.155631, Test Loss : 0.125244
Epoch 14. Train Loss: 0.120561, Test Loss : 0.188475
Epoch 15. Train Loss: 0.0960923, Test Loss : 0.0812157
Epoch 16. Train Loss: 0.081216, Test Loss : 0.0795511
Epoch 17. Train Loss: 0.0671884, Test Loss : 0.138703
Epoch 18. Train Loss: 0.0583087, Test Loss : 0.0596575
Epoch 19. Train Loss: 0.0525172, Test Loss : 0.0361313
 0+4 = 5(4) 1/0 0
 225+68 = 294(293) 1/0 0
 3+408 = 411(411) 1/0 1
 76+1 = 78(77) 1/0 0
 4+959 = 963(963) 1/0 1
 31+136 = 167(167) 1/0 1
 5+586 = 591(591) 1/0 1
 58+58 = 115(116) 1/0 0
 35+69 = 104(104) 1/0 1
 90+0 = 99(90) 1/0 0
Epoch 20. Train Loss: 0.0461855, Test Loss : 0.0317076
Epoch 21. Train Loss: 0.0405695, Test Loss : 0.0274729
Epoch 22. Train Loss: 0.0351415, Test Loss : 0.0297918
Epoch 23. Train Loss: 0.0349448, Test Loss : 0.0228843
Epoch 24. Train Loss: 0.0290138, Test Loss : 0.0269108
Epoch 25. Train Loss: 0.030374, Test Loss : 0.0295408
Epoch 26. Train Loss: 0.0242152, Test Loss : 0.0199652
Epoch 27. Train Loss: 0.0238406, Test Loss : 0.0275519
Epoch 28. Train Loss: 0.0234274, Test Loss : 0.0450086
Epoch 29. Train Loss: 0.0208283, Test Loss : 0.0372322
 84+599 = 683(683) 1/0 1
 732+210 = 942(942) 1/0 1
 3+4 = 7(7) 1/0 1
 7+252 = 259(259) 1/0 1
 1+58 = 69(59) 1/0 0
 440+44 = 484(484) 1/0 1
 44+9 = 53(53) 1/0 1
 106+4 = 100(110) 1/0 0
 122+8 = 220(130) 1/0 0
 68+9 = 76(77) 1/0 0
Epoch 30. Train Loss: 0.0212195, Test Loss : 0.0145791
Epoch 31. Train Loss: 0.0169189, Test Loss : 0.0142156
Epoch 32. Train Loss: 0.0157236, Test Loss : 0.119613
Epoch 33. Train Loss: 0.0146479, Test Loss : 0.0452408
Epoch 34. Train Loss: 0.014292, Test Loss : 0.0181429
Epoch 35. Train Loss: 0.0164574, Test Loss : 0.0203357
Epoch 36. Train Loss: 0.0141047, Test Loss : 0.0113768
Epoch 37. Train Loss: 0.0127254, Test Loss : 0.0210824
Epoch 38. Train Loss: 0.0113013, Test Loss : 0.012517
Epoch 39. Train Loss: 0.0109072, Test Loss : 0.0163044
 314+41 = 355(355) 1/0 1
 4+70 = 74(74) 1/0 1
 406+36 = 442(442) 1/0 1
 712+44 = 756(756) 1/0 1
 43+2 = 45(45) 1/0 1
 5+340 = 345(345) 1/0 1
 456+4 = 450(460) 1/0 0
 339+1 = 340(340) 1/0 1
 1+3 = 4(4) 1/0 1
 891+628 = 1519(1519) 1/0 1
Epoch 40. Train Loss: 0.0107261, Test Loss : 0.0111639
Epoch 41. Train Loss: 0.0108476, Test Loss : 0.00825096
Epoch 42. Train Loss: 0.00988811, Test Loss : 0.0102361
Epoch 43. Train Loss: 0.00900932, Test Loss : 0.0676999
Epoch 44. Train Loss: 0.00982492, Test Loss : 0.0156328
Epoch 45. Train Loss: 0.0102352, Test Loss : 0.0114041
Epoch 46. Train Loss: 0.00736601, Test Loss : 0.0172779
Epoch 47. Train Loss: 0.00944774, Test Loss : 0.012209
Epoch 48. Train Loss: 0.00857665, Test Loss : 0.0202206
Epoch 49. Train Loss: 0.00679102, Test Loss : 0.0154347
 843+215 = 1058(1058) 1/0 1
 4+9 = 12(13) 1/0 0
 457+89 = 546(546) 1/0 1
 12+234 = 246(246) 1/0 1
 2+9 = 11(11) 1/0 1
 86+651 = 737(737) 1/0 1
 500+1 = 501(501) 1/0 1
 1+105 = 106(106) 1/0 1
 90+927 = 1017(1017) 1/0 1
 370+7 = 377(377) 1/0 1
Epoch 50. Train Loss: 0.00842717, Test Loss : 0.00807731
Epoch 51. Train Loss: 0.00688481, Test Loss : 0.00955924
Epoch 52. Train Loss: 0.0059042, Test Loss : 0.0110426
Epoch 53. Train Loss: 0.00722511, Test Loss : 0.0132007
Epoch 54. Train Loss: 0.00804315, Test Loss : 0.00719513
Epoch 55. Train Loss: 0.0077892, Test Loss : 0.00777933
Epoch 56. Train Loss: 0.00528711, Test Loss : 0.00848834
Epoch 57. Train Loss: 0.00471256, Test Loss : 0.007944
Epoch 58. Train Loss: 0.0057872, Test Loss : 0.00857201
Epoch 59. Train Loss: 0.00637925, Test Loss : 0.00862721
 97+0 = 97(97) 1/0 1
 9+65 = 74(74) 1/0 1
 55+30 = 85(85) 1/0 1
 209+9 = 218(218) 1/0 1
 644+617 = 1261(1261) 1/0 1
 59+57 = 116(116) 1/0 1
 53+13 = 66(66) 1/0 1
 4+976 = 980(980) 1/0 1
 9+48 = 57(57) 1/0 1
 558+802 = 1360(1360) 1/0 1
Epoch 60. Train Loss: 0.00502064, Test Loss : 0.00580972
Epoch 61. Train Loss: 0.00607394, Test Loss : 0.012593
Epoch 62. Train Loss: 0.00454351, Test Loss : 0.00767156
Epoch 63. Train Loss: 0.00435373, Test Loss : 0.014334
Epoch 64. Train Loss: 0.00417846, Test Loss : 0.0102815
Epoch 65. Train Loss: 0.00415637, Test Loss : 0.0133149
Epoch 66. Train Loss: 0.00555201, Test Loss : 0.0107528
Epoch 67. Train Loss: 0.00496103, Test Loss : 0.0110011
Epoch 68. Train Loss: 0.00416868, Test Loss : 0.0106568
Epoch 69. Train Loss: 0.00340409, Test Loss : 0.00849371
 910+287 = 1297(1197) 1/0 0
 8+93 = 101(101) 1/0 1
 1+4 = 5(5) 1/0 1
 407+0 = 407(407) 1/0 1
 86+62 = 148(148) 1/0 1
 8+5 = 13(13) 1/0 1
 5+7 = 12(12) 1/0 1
 12+90 = 102(102) 1/0 1
 51+8 = 69(59) 1/0 0
 486+2 = 488(488) 1/0 1
Epoch 70. Train Loss: 0.00397048, Test Loss : 0.00886579
Epoch 71. Train Loss: 0.00355671, Test Loss : 0.0147429
Epoch 72. Train Loss: 0.00346305, Test Loss : 0.00793579
Epoch 73. Train Loss: 0.00381371, Test Loss : 0.00584106
Epoch 74. Train Loss: 0.00278036, Test Loss : 0.0234801
Epoch 75. Train Loss: 0.00336997, Test Loss : 0.0181428
Epoch 76. Train Loss: 0.00283961, Test Loss : 0.00660451
Epoch 77. Train Loss: 0.00572103, Test Loss : 0.005219
Epoch 78. Train Loss: 0.00281588, Test Loss : 0.0140664
Epoch 79. Train Loss: 0.00340437, Test Loss : 0.0066866
 0+1 = 2(1) 1/0 0
 354+4 = 358(358) 1/0 1
 348+3 = 351(351) 1/0 1
 8+69 = 77(77) 1/0 1
 12+53 = 65(65) 1/0 1
 4+375 = 379(379) 1/0 1
 8+655 = 653(663) 1/0 0
 4+70 = 74(74) 1/0 1
 9+71 = 80(80) 1/0 1
 8+5 = 13(13) 1/0 1
Epoch 80. Train Loss: 0.00306732, Test Loss : 0.0088079
Epoch 81. Train Loss: 0.00261282, Test Loss : 0.0104031
Epoch 82. Train Loss: 0.00278683, Test Loss : 0.00651883
Epoch 83. Train Loss: 0.00248736, Test Loss : 0.0101634
Epoch 84. Train Loss: 0.00333953, Test Loss : 0.00823778
Epoch 85. Train Loss: 0.00282169, Test Loss : 0.0104558
Epoch 86. Train Loss: 0.00264165, Test Loss : 0.00954762
Epoch 87. Train Loss: 0.0028707, Test Loss : 0.0245796
Epoch 88. Train Loss: 0.00283279, Test Loss : 0.0141948
Epoch 89. Train Loss: 0.00198803, Test Loss : 0.00606726
 105+0 = 105(105) 1/0 1
 9+1 = 10(10) 1/0 1
 2+8 = 10(10) 1/0 1
 8+49 = 57(57) 1/0 1
 3+97 = 100(100) 1/0 1
 13+892 = 905(905) 1/0 1
 653+491 = 1144(1144) 1/0 1
 6+975 = 981(981) 1/0 1
 20+52 = 72(72) 1/0 1
 1+58 = 69(59) 1/0 0
Epoch 90. Train Loss: 0.00283533, Test Loss : 0.010756
Epoch 91. Train Loss: 0.00268884, Test Loss : 0.00581835
Epoch 92. Train Loss: 0.00323414, Test Loss : 0.0075214
Epoch 93. Train Loss: 0.00276694, Test Loss : 0.00572802
Epoch 94. Train Loss: 0.00156762, Test Loss : 0.00773455
Epoch 95. Train Loss: 0.00151749, Test Loss : 0.00421858
Epoch 96. Train Loss: 0.00132186, Test Loss : 0.00577639
Epoch 97. Train Loss: 0.00245539, Test Loss : 0.00644012
Epoch 98. Train Loss: 0.00250708, Test Loss : 0.00470248
Epoch 99. Train Loss: 0.00177719, Test Loss : 0.00682936
 16+0 = 16(16) 1/0 1
 30+332 = 362(362) 1/0 1
 0+1 = 1(1) 1/0 1
 13+3 = 16(16) 1/0 1
 1+62 = 63(63) 1/0 1
 874+4 = 878(878) 1/0 1
 923+2 = 925(925) 1/0 1
 188+6 = 194(194) 1/0 1
 7+743 = 750(750) 1/0 1
 568+735 = 1303(1303) 1/0 1
Epoch 100. Train Loss: 0.00195897, Test Loss : 0.0095622
Epoch 101. Train Loss: 0.00125458, Test Loss : 0.0054107
Epoch 102. Train Loss: 0.00154039, Test Loss : 0.00911017
Epoch 103. Train Loss: 0.00187413, Test Loss : 0.0180655
Epoch 104. Train Loss: 0.00219021, Test Loss : 0.0061727
Epoch 105. Train Loss: 0.00209904, Test Loss : 0.00908591
Epoch 106. Train Loss: 0.00274257, Test Loss : 0.00850028
Epoch 107. Train Loss: 0.0016415, Test Loss : 0.00635539
Epoch 108. Train Loss: 0.00280382, Test Loss : 0.0102037
Epoch 109. Train Loss: 0.00144518, Test Loss : 0.00437199
 1+334 = 335(335) 1/0 1
 91+9 = 190(100) 1/0 0
 98+4 = 103(102) 1/0 0
 903+6 = 909(909) 1/0 1
 92+55 = 147(147) 1/0 1
 3+114 = 117(117) 1/0 1
 6+8 = 14(14) 1/0 1
 77+7 = 84(84) 1/0 1
 6+7 = 13(13) 1/0 1
 590+7 = 587(597) 1/0 0
Epoch 110. Train Loss: 0.00118595, Test Loss : 0.00797355
Epoch 111. Train Loss: 0.00320637, Test Loss : 0.00650378
Epoch 112. Train Loss: 0.00130261, Test Loss : 0.00348112
Epoch 113. Train Loss: 0.00146112, Test Loss : 0.00779839
Epoch 114. Train Loss: 0.00168527, Test Loss : 0.00756565
Epoch 115. Train Loss: 0.000930022, Test Loss : 0.00544935
Epoch 116. Train Loss: 0.00145466, Test Loss : 0.0160591
Epoch 117. Train Loss: 0.00112997, Test Loss : 0.00521217
Epoch 118. Train Loss: 0.00127665, Test Loss : 0.00597252
Epoch 119. Train Loss: 0.00143328, Test Loss : 0.00937187
 92+81 = 173(173) 1/0 1
 6+40 = 46(46) 1/0 1
 73+31 = 104(104) 1/0 1
 948+2 = 950(950) 1/0 1
 343+3 = 346(346) 1/0 1
 305+98 = 403(403) 1/0 1
 5+505 = 510(510) 1/0 1
 7+8 = 15(15) 1/0 1
 31+406 = 437(437) 1/0 1
 38+3 = 51(41) 1/0 0
Epoch 120. Train Loss: 0.00141822, Test Loss : 0.0113712
Epoch 121. Train Loss: 0.00146098, Test Loss : 0.0196588
Epoch 122. Train Loss: 0.0016774, Test Loss : 0.00355691
Epoch 123. Train Loss: 0.000572061, Test Loss : 0.0038612
Epoch 124. Train Loss: 0.00025224, Test Loss : 0.0482143
Epoch 125. Train Loss: 0.000908123, Test Loss : 0.00792559
Epoch 126. Train Loss: 0.00132589, Test Loss : 0.00652228
Epoch 127. Train Loss: 0.00176812, Test Loss : 0.00711878
Epoch 128. Train Loss: 0.00153973, Test Loss : 0.00764472
Epoch 129. Train Loss: 0.0014569, Test Loss : 0.00672641
 46+590 = 636(636) 1/0 1
 379+3 = 382(382) 1/0 1
 4+1 = 6(5) 1/0 0
 8+632 = 640(640) 1/0 1
 333+91 = 424(424) 1/0 1
 838+851 = 1689(1689) 1/0 1
 6+7 = 13(13) 1/0 1
 82+5 = 86(87) 1/0 0
 954+51 = 1005(1005) 1/0 1
 7+9 = 16(16) 1/0 1
Epoch 130. Train Loss: 0.00228727, Test Loss : 0.00801615
Epoch 131. Train Loss: 0.00211349, Test Loss : 0.00721177
Epoch 132. Train Loss: 0.00226279, Test Loss : 0.00972691
Epoch 133. Train Loss: 0.00144402, Test Loss : 0.0070334
Epoch 134. Train Loss: 0.00134113, Test Loss : 0.0151508
Epoch 135. Train Loss: 0.00130168, Test Loss : 0.00492381
Epoch 136. Train Loss: 0.00134316, Test Loss : 0.0052638
Epoch 137. Train Loss: 0.00194314, Test Loss : 0.0225744
Epoch 138. Train Loss: 0.000960593, Test Loss : 0.00596013
Epoch 139. Train Loss: 0.000926426, Test Loss : 0.00496691
 24+705 = 729(729) 1/0 1
 88+7 = 95(95) 1/0 1
 0+2 = 23(2) 1/0 0
 583+89 = 672(672) 1/0 1
 35+368 = 403(403) 1/0 1
 386+89 = 475(475) 1/0 1
 4+4 = 8(8) 1/0 1
 82+4 = 86(86) 1/0 1
 559+166 = 725(725) 1/0 1
 682+824 = 1506(1506) 1/0 1
Epoch 140. Train Loss: 0.00123914, Test Loss : 0.00622027
Epoch 141. Train Loss: 0.00114216, Test Loss : 0.00766453
Epoch 142. Train Loss: 0.00150233, Test Loss : 0.0054836
Epoch 143. Train Loss: 0.00154832, Test Loss : 0.00927247
Epoch 144. Train Loss: 0.00128405, Test Loss : 0.00900654
Epoch 145. Train Loss: 0.00121568, Test Loss : 0.011445
Epoch 146. Train Loss: 0.00107785, Test Loss : 0.00929671
Epoch 147. Train Loss: 0.00102916, Test Loss : 0.00781922
Epoch 148. Train Loss: 0.000316407, Test Loss : 0.00399398
Epoch 149. Train Loss: 0.000612223, Test Loss : 0.00497149
 4+0 = 5(4) 1/0 0
 2+871 = 873(873) 1/0 1
 37+93 = 130(130) 1/0 1
 89+817 = 906(906) 1/0 1
 727+984 = 1711(1711) 1/0 1
 57+0 = 57(57) 1/0 1
 243+8 = 251(251) 1/0 1
 1+818 = 819(819) 1/0 1
 26+7 = 33(33) 1/0 1
 824+50 = 874(874) 1/0 1
Epoch 150. Train Loss: 0.000186055, Test Loss : 0.00501143
Epoch 151. Train Loss: 4.04054e-05, Test Loss : 0.00401301
Epoch 152. Train Loss: 9.7102e-06, Test Loss : 0.00419563
Epoch 153. Train Loss: 7.57328e-06, Test Loss : 0.00374868
Epoch 154. Train Loss: 6.58183e-06, Test Loss : 0.00372357
Epoch 155. Train Loss: 5.98684e-06, Test Loss : 0.00416506
Epoch 156. Train Loss: 5.53783e-06, Test Loss : 0.00376316
Epoch 157. Train Loss: 5.23978e-06, Test Loss : 0.00417315
Epoch 158. Train Loss: 4.92797e-06, Test Loss : 0.0037615
Epoch 159. Train Loss: 4.74416e-06, Test Loss : 0.00407676
 83+137 = 220(220) 1/0 1
 813+51 = 864(864) 1/0 1
 1+2 = 33(3) 1/0 0
 471+6 = 477(477) 1/0 1
 12+6 = 18(18) 1/0 1
 8+2 = 10(10) 1/0 1
 64+1 = 65(65) 1/0 1
 731+700 = 1431(1431) 1/0 1
 344+69 = 413(413) 1/0 1
 3+7 = 10(10) 1/0 1
Epoch 160. Train Loss: 4.45369e-06, Test Loss : 0.00373083
Epoch 161. Train Loss: 4.31551e-06, Test Loss : 0.00375279
Epoch 162. Train Loss: 4.16575e-06, Test Loss : 0.00369303
Epoch 163. Train Loss: 4.1166e-06, Test Loss : 0.00398206
Epoch 164. Train Loss: 3.81897e-06, Test Loss : 0.00369495
Epoch 165. Train Loss: 3.82295e-06, Test Loss : 0.0036701
Epoch 166. Train Loss: 3.68564e-06, Test Loss : 0.00370591
Epoch 167. Train Loss: 3.57566e-06, Test Loss : 0.00379358
Epoch 168. Train Loss: 3.43527e-06, Test Loss : 0.0037078
Epoch 169. Train Loss: 3.40323e-06, Test Loss : 0.00382377
 1+89 = 90(90) 1/0 1
 5+28 = 33(33) 1/0 1
 4+1 = 6(5) 1/0 0
 613+89 = 702(702) 1/0 1
 801+874 = 1675(1675) 1/0 1
 13+597 = 610(610) 1/0 1
 315+4 = 319(319) 1/0 1
 21+31 = 52(52) 1/0 1
 603+0 = 603(603) 1/0 1
 9+8 = 17(17) 1/0 1
Epoch 170. Train Loss: 3.37041e-06, Test Loss : 0.00369082
Epoch 171. Train Loss: 3.15295e-06, Test Loss : 0.00370142
Epoch 172. Train Loss: 3.28802e-06, Test Loss : 0.00372233
Epoch 173. Train Loss: 3.15507e-06, Test Loss : 0.00370121
Epoch 174. Train Loss: 3.02016e-06, Test Loss : 0.0039215
Epoch 175. Train Loss: 2.98917e-06, Test Loss : 0.00372771
Epoch 176. Train Loss: 2.91798e-06, Test Loss : 0.0036647
Epoch 177. Train Loss: 2.87783e-06, Test Loss : 0.00387829
Epoch 178. Train Loss: 2.82765e-06, Test Loss : 0.0036976
Epoch 179. Train Loss: 2.80179e-06, Test Loss : 0.00367445
 49+80 = 129(129) 1/0 1
 17+87 = 104(104) 1/0 1
 3+5 = 8(8) 1/0 1
 16+176 = 192(192) 1/0 1
 8+30 = 38(38) 1/0 1
 426+9 = 435(435) 1/0 1
 22+157 = 179(179) 1/0 1
 24+1 = 25(25) 1/0 1
 2+1 = 3(3) 1/0 1
 47+6 = 53(53) 1/0 1
Epoch 180. Train Loss: 2.76078e-06, Test Loss : 0.0037565
Epoch 181. Train Loss: 2.66419e-06, Test Loss : 0.00397936
Epoch 182. Train Loss: 2.6687e-06, Test Loss : 0.00375932
Epoch 183. Train Loss: 2.52133e-06, Test Loss : 0.00363644
Epoch 184. Train Loss: 2.67349e-06, Test Loss : 0.00365268
Epoch 185. Train Loss: 2.5557e-06, Test Loss : 0.00386944
Epoch 186. Train Loss: 2.49811e-06, Test Loss : 0.00370046
Epoch 187. Train Loss: 2.49869e-06, Test Loss : 0.00361573
Epoch 188. Train Loss: 2.47827e-06, Test Loss : 0.00367216
Epoch 189. Train Loss: 2.40782e-06, Test Loss : 0.00364053
 0+120 = 120(120) 1/0 1
 45+489 = 534(534) 1/0 1
 8+762 = 760(770) 1/0 0
 602+607 = 1209(1209) 1/0 1
 2+3 = 5(5) 1/0 1
 5+3 = 8(8) 1/0 1
 998+519 = 1517(1517) 1/0 1
 403+794 = 1197(1197) 1/0 1
 13+7 = 20(20) 1/0 1
 5+56 = 61(61) 1/0 1
Epoch 190. Train Loss: 2.4565e-06, Test Loss : 0.00369676
Epoch 191. Train Loss: 2.39905e-06, Test Loss : 0.00382188
Epoch 192. Train Loss: 2.30808e-06, Test Loss : 0.00378665
Epoch 193. Train Loss: 2.27133e-06, Test Loss : 0.00365563
Epoch 194. Train Loss: 2.22566e-06, Test Loss : 0.00364971
Epoch 195. Train Loss: 2.20956e-06, Test Loss : 0.00361473
Epoch 196. Train Loss: 2.24031e-06, Test Loss : 0.00367714
Epoch 197. Train Loss: 2.20251e-06, Test Loss : 0.00365003
Epoch 198. Train Loss: 2.07808e-06, Test Loss : 0.00390731
Epoch 199. Train Loss: 2.13289e-06, Test Loss : 0.00402877
 8+6 = 14(14) 1/0 1
 3+31 = 34(34) 1/0 1
 1+282 = 283(283) 1/0 1
 4+7 = 11(11) 1/0 1
 453+628 = 1081(1081) 1/0 1
 6+56 = 62(62) 1/0 1
 16+70 = 86(86) 1/0 1
 596+903 = 1499(1499) 1/0 1
 87+956 = 1043(1043) 1/0 1
 0+4 = 4(4) 1/0 1
Epoch 200. Train Loss: 2.07594e-06, Test Loss : 0.00362931

매 10에폭마다 랜덤 예제의 값을 예측하게 했는데, 200에폭에 근접하면서 랜덤 테스트셋을 대부분 적중하는 모습을 볼 수 있어 제대로 학습이 진행되고 있음을 확인할 수 있을 것이다.

이 모형으로 덧셈을 할 사람이 누가 있을까? 사실 이 코드는 실제 활용을 위한 코드는 아니고 이 seq2seq 코드로 직접 기계번역 모형을 빌드해보기 위함이다. 성공적으로 토이 모형이 만들어 졌으니 아마도 다음 포스트는 기계번역 초기 모형이 되지 않을까 하는 생각을 해본다. 그 포스트에서는 Attention 등과 같은 기술도 구현해 볼 수 있지 않을까 생각해본다.

전이학습(transfer learning)으로 모형 재사용하기 (Gluon 기반)

전이학습(traansfer learning)은 꽤 오래된 주제지만 실무에서 활용한만한 가치가 높은 기법중에 하나이다. 특히나 이미지 관련 분류 모형에서는 이 기법은 거의 필수라고 생각하고 일반적인 기술일 것이다. 이 기법은 기존 분류에 대한 성능이 좋은 네트워크와 가중치를 일부 혹은 전부 가져와 상위 레이어를 튜닝하는 방식으로 자신이 원하는 문제를 풀게 모형을 재활용 혹은 전용하는 방법이다.
특히나 학습셋이 부족한 경우 그리고 문제영역 자체가 세부 영역으로 좁혀진 경우 매우 활용 가치가 높다.

필자가 최근에 관심있어하는 Gluon을 기반으로 작업을 진행했다. 프레임웍에 대해서 개인적으로 테스트 하는 과정의 일환이고, 익숙해지기 위한 과정인데, 혹여나 비슷한 시행착오를 하는 독자를 위해 최대한 상세하게 주석과 레퍼런스를 달아두었다. Gluon이 고작 0.1 버전을 유지하고 있지만, 필자가 익숙해지고자 하는 이유는 PyTorch보다 API구조가 더 잘 되어 있다는 것과, PyTorch의 유연함과 더불어 Keras의 간결합을 가지고 있어서이다. 매우 개인적인 이유이지만 1년전 MXNet을 실무에서 활용하려다 못한 약간은 한풀이 느낌도 있긴 하다.

필자가 API튜토리얼 문서Github을 모두 참고하면서 기존 이미지 분류 모형을 기반으로 상위에 레이어를 추가해 전이학습을 수행하는 과정을 진행해 나가고자 한다. 아마도 Gluon기반으로 비슷한 작업을 해보신 분들은 많은 시행착오를 했을 부분이라 생각한다. 필자도 많은 시행착오를 했지만 그 시행착오 덕분에 내부 구조나 API를 더 충실히 확인할 수 있었다.

데이터는 kaggle에서 획득한 train.zip을 활용했다. 이 노트북이 존재하는 경로에 그대로 압축을 풀어서 작업을 수행했다.

아래와 같은 과정으로 진행된다.

  1. jpg를 Image RecordIO dataset 파일로 전환 이 문서를 주로 참고했다.
  2. 모형 아키텍처 정의 (vgg16 기반)
  3. 학습/평가
  4. 예제 적용

아마도 이 주제에 대해서 모형을 구축해본 분들이 있을지 모르겠는데, 해당 모형을 전이학습이 아닌 모형으로 전이학습이 된 모형정도의 성능을 나오게 하는건 매우 어렵다. 필자의 경험으로 fully connected 와 convolution 몇개로 모형을 구축했을때 80%의 분류 정확도에서 더 올리는건 어려웠던 경험이 있다.

학습 데이터 생성

  • 먼저 kaggle에서 학습셋을 다운 받아 로컬에 압축을 풀어준다.
In [5]:
import pandas as pd
import numpy as np
from sklearn.utils import shuffle
import mxnet as mx
from mxnet import gluon
from mxnet.gluon import nn
from mxnet.image import color_normalize
import mxnet.autograd as autograd
from mxnet.gluon.model_zoo import vision as models
In [2]:
#더미 파일 리스트를 구축한다. , im2rec.py는 https://github.com/apache/incubator-mxnet/blob/master/tools/im2rec.py 
%run im2rec.py --list list catsdogs_full train
In [3]:
#아래 보는것과 같이 텝으로 구분되어서 파일 인덱스, 레이블(클래스), 사진파일명 정보가 구성된다. 
!head catsdogs_full.lst
3197	0.000000	cat.1625.jpg
15084	0.000000	dog.12322.jpg
1479	0.000000	cat.11328.jpg
5262	0.000000	cat.3484.jpg
20714	0.000000	dog.6140.jpg
9960	0.000000	cat.7712.jpg
945	0.000000	cat.10848.jpg
15585	0.000000	dog.1524.jpg
12376	0.000000	cat.9888.jpg
4367	0.000000	cat.2679.jpg
In [6]:
#pandas 테이블로 파일을 읽어들여 레이블을 마킹하고 랜덤 셔플한 뒤 학습과 테스트셋으로 만든다. 
catdoglist = pd.read_csv('catsdogs_full.lst',sep='\t', names=['idx', 'class', 'fn'])

catdoglist['class'] = [1  if i else 0 for i in catdoglist['fn'].str.contains('dog')]

catdoglist = shuffle(catdoglist)

train = catdoglist.iloc[:6500, ]

train.shape

test = catdoglist.iloc[6500:, ]

train.to_csv('catsdogs_train.lst',doublequote=False,sep='\t', header=False, index=False)
test.to_csv('catsdogs_test.lst',doublequote=False,sep='\t', header=False, index=False)
In [165]:
#실제 Image RecordIO dataset을 만든다. 
!python im2rec.py --num-thread 10 catsdogs_t train
/home/gogamza/python_3.6/lib/python3.6/distutils/__init__.py:4: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
Creating .rec file from /home/gogamza/work/gluons/catsdogs_train.lst in /home/gogamza/work/gluons
time: 0.03124403953552246  count: 0
time: 1.179645299911499  count: 1000
time: 0.976043701171875  count: 2000
time: 1.1002657413482666  count: 3000
time: 1.051544189453125  count: 4000
time: 1.0289239883422852  count: 5000
time: 0.5287754535675049  count: 6000
Creating .rec file from /home/gogamza/work/gluons/catsdogs_test.lst in /home/gogamza/work/gluons
time: 0.18526077270507812  count: 0
time: 0.9183506965637207  count: 1000
time: 0.8911347389221191  count: 2000
time: 1.0432755947113037  count: 3000
time: 0.8570001125335693  count: 4000
time: 1.0042638778686523  count: 5000
time: 0.9739773273468018  count: 6000
time: 0.8833253383636475  count: 7000
time: 0.9417126178741455  count: 8000
time: 1.2968952655792236  count: 9000
time: 1.1031651496887207  count: 10000
time: 1.019164800643921  count: 11000
time: 0.9050683975219727  count: 12000
time: 0.9271485805511475  count: 13000
time: 0.8212544918060303  count: 14000
time: 0.8985750675201416  count: 15000
time: 1.2766637802124023  count: 16000
time: 0.721259355545044  count: 17000
time: 0.48262739181518555  count: 18000
[17:43:09] src/engine/engine.cc:55: MXNet start using engine: NaiveEngine
[17:43:09] src/engine/naive_engine.cc:55: Engine shutdown
In [7]:
#학습은 GPU에서 
ctx = mx.gpu(0)

모형 생성

In [148]:
class cats_and_dogs(gluon.HybridBlock):
    def __init__(self, num_class, num_hidden , **kwargs):
        super(cats_and_dogs, self).__init__(**kwargs)
        
        with self.name_scope():
            #model zoo에서 vgg16 모형을 가져온다. 
            #이렇게 vgg를 클래스 내부에서 로딩하면 인스턴스마다 context, prefix 가 통일되게 생성되어 모형 저장/로딩시 용이한 측면이 있다.  
            self.vgg_net =  models.vgg16(pretrained=True, root=".models").features
            self.classifier = nn.HybridSequential()
            with self.classifier.name_scope():
                self.classifier.add(nn.Dense(num_hidden, activation="relu"))
                self.classifier.add(nn.Dense(int(num_hidden/2), activation="relu"))
                self.classifier.add(nn.Dense(units=num_class))            
    
    def hybrid_forward(self, F, inputs):
        o_vgg = self.vgg_net(inputs)
        pred = self.classifier(o_vgg)
        return pred
        
        
        
In [149]:
model = cats_and_dogs(num_class=2, num_hidden = 500)
In [137]:
#Symbolic 텐서로 컴파일한다. 
#확실히 GPU메모리 사용량은 줄어드나, 학습 속도가 2배 넘게 빨리지지는 않는거 같다. 
model.hybridize()
In [150]:
print(model)
cats_and_dogs(
  (vgg_net): HybridSequential(
    (0): Conv2D(3 -> 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): Activation(relu)
    (2): Conv2D(64 -> 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): Activation(relu)
    (4): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
    (5): Conv2D(64 -> 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): Activation(relu)
    (7): Conv2D(128 -> 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): Activation(relu)
    (9): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
    (10): Conv2D(128 -> 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): Activation(relu)
    (12): Conv2D(256 -> 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): Activation(relu)
    (14): Conv2D(256 -> 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): Activation(relu)
    (16): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
    (17): Conv2D(256 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): Activation(relu)
    (19): Conv2D(512 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): Activation(relu)
    (21): Conv2D(512 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): Activation(relu)
    (23): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
    (24): Conv2D(512 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): Activation(relu)
    (26): Conv2D(512 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): Activation(relu)
    (28): Conv2D(512 -> 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): Activation(relu)
    (30): MaxPool2D(size=(2, 2), stride=(2, 2), padding=(0, 0), ceil_mode=False)
    (31): Dense(25088 -> 4096, Activation(relu))
    (32): Dropout(p = 0.5)
    (33): Dense(4096 -> 4096, Activation(relu))
    (34): Dropout(p = 0.5)
  )
  (classifier): HybridSequential(
    (0): Dense(None -> 500, Activation(relu))
    (1): Dense(None -> 250, Activation(relu))
    (2): Dense(None -> 2, linear)
  )
)

학습

In [151]:
#생성한 rec 파일로 이터레이터를 생성한다. 
#Tensorflow와 케라스에서와는 다르게 채널이 먼저 나온다.
#이미지 학습을 하는데 ImageRecordIter는 거의 필수라고 생각하는데, 이는 다양한 data augmentation의 옵션을 제공하기 때문이다.
batch_size = 50  # GPU메모리 크기에 따라 정해준다. 
valid_batch_size = 30 #validation은 적어도 큰 문제가 없다. 
train_iter = mx.io.ImageRecordIter(path_imgrec='catsdogs_train.rec',
                                   min_img_size=256,
                                   data_shape=(3, 224, 224),
                                   rand_crop=True,
                                   shuffle=True,
                                   batch_size=batch_size,
                                   max_random_scale=1.5,
                                   min_random_scale=0.75,
                                   rand_mirror=True)
val_iter = mx.io.ImageRecordIter(path_imgrec="catsdogs_test.rec",
                                 min_img_size=256,
                                 data_shape=(3, 224, 224),
                                 batch_size=valid_batch_size)
In [152]:
def evaluate(net, data_iter, ctx):
    data_iter.reset()
    acc = mx.metric.Accuracy()
    for batch in data_iter:
        mx.nd.waitall()
        #reference 
        #https://mxnet.incubator.apache.org/api/python/gluon/model_zoo.html
        data = color_normalize(batch.data[0]/255,
                               mean=mx.nd.array([0.485, 0.456, 0.406]).reshape((1,3,1,1)),
                               std=mx.nd.array([0.229, 0.224, 0.225]).reshape((1,3,1,1)))
        
        data = data.as_in_context(ctx)
        label = batch.label[0].as_in_context(ctx)
        with autograd.predict_mode():
            output = net(data)
        predictions = mx.nd.argmax(output, axis=1)
        acc.update(preds=predictions, labels=label)
        mx.ndarray.waitall()
    return acc.get()[1]
In [155]:
import mxnet.autograd as autograd
from mxnet import gluon
import logging
import os
import time
import numpy as np
logging.basicConfig(level=logging.INFO)

def train(net, train_iter, val_iter, epochs, ctx, need_init=False):
    if need_init:
        #vgg16 모형은 학습을 fix 한다. 
        # https://github.com/apache/incubator-mxnet/issues/1340
        net.vgg_net.collect_params().setattr('grad_req', 'null')
        net.classifier.collect_params().initialize(mx.init.Xavier(), ctx=ctx)
    #변수들이 모두 같은 ctx에 있는지 다시 확인 
    net.collect_params().reset_ctx(ctx)

    trainer = gluon.trainer.Trainer(net.collect_params(), 'rmsprop', optimizer_params={'learning_rate':0.001})
    loss = gluon.loss.SoftmaxCrossEntropyLoss(sparse_label=False)


    val_accs = evaluate(net, val_iter, ctx)
    logging.info('[Initial] validation accuracy : {}'.format(val_accs))
    tr_loss = []
    val_accs = []
    for epoch in range(epochs):
        tic = time.time()
        train_iter.reset()
        btic = time.time()
        loss_seq = []
        for i, batch in enumerate(train_iter):
            #속도를 좀 감소시키더라도 GPU 메모리에 대한 관리를 더 한다.  
            mx.nd.waitall()
            data = color_normalize(batch.data[0]/255,
                                   mean=mx.nd.array([0.485, 0.456, 0.406]).reshape((1,3,1,1)),
                                   std=mx.nd.array([0.229, 0.224, 0.225]).reshape((1,3,1,1)))
            data = data.as_in_context(ctx)
            label = mx.nd.one_hot(batch.label[0].as_in_context(ctx),2)
            with autograd.record():
                z = net(data)
                L = loss(z, label)
                L.backward()
            trainer.step(data.shape[0])
            btic = time.time()
            curr_loss = mx.nd.mean(L).asscalar()
            loss_seq.append(curr_loss)
            mx.ndarray.waitall()
        logging.info('[Epoch {}] training loss : {}'.format(epoch, np.mean(loss_seq)))
        logging.info('[Epoch %d] time cost: %f'%(epoch, time.time()-tic))
        val_acc = evaluate(net, val_iter, ctx)
        logging.info('[Epoch %d] validation accuracy : %s'%(epoch, val_acc))
        val_accs.append(val_acc)
        tr_loss.append(np.mean(loss_seq))
    return(val_accs, tr_loss)
In [156]:
accs, losses = train(model,train_iter, val_iter, 10, ctx=ctx, need_init=True)
INFO:root:[Initial] validation accuracy : 0.5424635332252836
INFO:root:[Epoch 0] training loss : 0.1768229603767395
INFO:root:[Epoch 0] time cost: 33.588889
INFO:root:[Epoch 0] validation accuracy : 0.977525661804
INFO:root:[Epoch 1] training loss : 0.14089804887771606
INFO:root:[Epoch 1] time cost: 34.197274
INFO:root:[Epoch 1] validation accuracy : 0.979653679654
INFO:root:[Epoch 2] training loss : 0.12714947760105133
INFO:root:[Epoch 2] time cost: 34.241362
INFO:root:[Epoch 2] validation accuracy : 0.979038357645
INFO:root:[Epoch 3] training loss : 0.12572327256202698
INFO:root:[Epoch 3] time cost: 34.283421
INFO:root:[Epoch 3] validation accuracy : 0.972987574284
INFO:root:[Epoch 4] training loss : 0.12414756417274475
INFO:root:[Epoch 4] time cost: 34.316082
INFO:root:[Epoch 4] validation accuracy : 0.980194805195
INFO:root:[Epoch 5] training loss : 0.11750277131795883
INFO:root:[Epoch 5] time cost: 34.442768
INFO:root:[Epoch 5] validation accuracy : 0.980064829822
INFO:root:[Epoch 6] training loss : 0.12463101744651794
INFO:root:[Epoch 6] time cost: 34.416495
INFO:root:[Epoch 6] validation accuracy : 0.979200432199
INFO:root:[Epoch 7] training loss : 0.1226000264286995
INFO:root:[Epoch 7] time cost: 34.490258
INFO:root:[Epoch 7] validation accuracy : 0.979761904762
INFO:root:[Epoch 8] training loss : 0.11914137005805969
INFO:root:[Epoch 8] time cost: 34.639952
INFO:root:[Epoch 8] validation accuracy : 0.98038897893
INFO:root:[Epoch 9] training loss : 0.11623886972665787
INFO:root:[Epoch 9] time cost: 34.496284
INFO:root:[Epoch 9] validation accuracy : 0.98076715289

분류 예제

In [157]:
from skimage.color import rgba2rgb
from skimage import io
from matplotlib import pyplot as plt
%matplotlib inline

#cpu에서 동작 시킨다.  
model.collect_params().reset_ctx(mx.cpu(0))

#reference : http://gluon.mxnet.io/chapter08_computer-vision/fine-tuning.html
def classify_dogcat(net, url):
    I = io.imread(url)
    if I.shape[2] == 4:
        I = rgba2rgb(I)
    image = mx.nd.array(I).astype(np.uint8)
    plt.subplot(1, 2, 1)
    plt.imshow(image.asnumpy())
    image = mx.image.resize_short(image, 256)
    image, _ = mx.image.center_crop(image, (224, 224))
    plt.subplot(1, 2, 2)
    plt.imshow(image.asnumpy())
    image = mx.image.color_normalize(image.astype(np.float32)/255,
                                     mean=mx.nd.array([0.485, 0.456, 0.406]),
                                     std=mx.nd.array([0.229, 0.224, 0.225]))
    image = mx.nd.transpose(image.astype('float32'), (2,1,0))
    image = mx.nd.expand_dims(image, axis=0)
    out = mx.nd.SoftmaxActivation(net(image))
    print('Probabilities are: '+str(out[0].asnumpy()))
    result = np.argmax(out.asnumpy())
    outstring = ['cat!', 'dog!']
    print(outstring[result])
In [169]:
%timeit
classify_dogcat(net=model, url="cats.jpg")
Probabilities are: [  9.99972343e-01   2.76759565e-05]
cat!
In [158]:
%timeit
classify_dogcat(net=model, url="Cute-dog-listening-to-music-1_1.jpg")
Probabilities are: [  3.01063788e-04   9.99698997e-01]
dog!

모형 저장/로딩

In [ ]:
model.save_params("cats_and_dogs_model.mdl")
In [160]:
new_model = cats_and_dogs(num_class=2, num_hidden = 500)
In [163]:
new_model.load_params('cats_and_dogs_model.mdl', ctx=mx.cpu(0))
In [164]:
%timeit
classify_dogcat(net=new_model, url="Cute-dog-listening-to-music-1_1.jpg")
Probabilities are: [  3.01063788e-04   9.99698997e-01]
dog!

모형을 로딩해서 다시 예측을 해도 같은 결과를 보여준다..(너무 당연하지만, 혹시나 하는…)

정리

일단 10번의 에폭만으로도 98% 이상의 테스트셋 정확도를 가져오는 것을 확인할 수 있었다. 일반적으로 이 문제에서 전이학습으로 달성 가능한 성능에는 근접해서, 이런 저런 개인적 실험은 성공한 것으로 판단한다.

사실 관련 토픽에 대한 매우 훌륭한 튜토리얼이 존재하는데 다소 어렵기도 하고, 복잡한 단점이 있으며 실제 가장 많이 활용되는 방법이라고 보기 어려운 전이학습 방식이다. 실무에서는 하위 레이어에 대한 학습을 고정하고 상위 N레이어를 교체하는 방식으로 활용하는데, 이를 위해 Gluon의 특정 레이어의 학습을 고정하는 방법을 확인할 필요가 있었고, 모형 저장/로딩 방식을 전이학습에서 어떻게 가져가야 될지에 대한 실제 활용단의 고민이 필요했다.

개인적으로 Gluon의 클래스 구조를 확인해본 흥미로운 경험이었는데, 구조를 확인하면서 다양한 옵션을 설정할 수 있고, 확인해볼 수 있어서 개인 연구용으로 활용 가능한 프레임웍이라는 생각이 들었다.

이 실습을 하면서 GPU 메모리 오버플로가 일어나면서 프로세스가 죽는 경우가 있었는데, export MXNET_ENGINE_TYPE=NaiveEngine와 같은 환경변수 설정과 배치 사이에 mx.ndarray.waitall()와 같은 트릭이 이들을 피하는데 많은 도움이 되었다.

Gluon을 이용한 Grad CAM

먼저 이 글을 독자분들이 읽기 전에 배경 지식을 위해 아래 글을 먼저 읽어보길 추천한다.

이전 글의 코드를 보면 알다시피 keras의 레이어 API구조로 직관적으로 gradient나 layer 출력을 직관적으로 뽑아보는게 다소 번거롭고 backend API의 도움으로 부분적으로나마 가능하다는 생각을 독자분들도 했을거라 생각한다. 아마도 이런 부분때문에 PyTorch와 같은 imperative 방식의 딥러닝 프레임웍이 인기가 있다고 생각한다. 왜냐면 흡사 numpy를 쓰듯이 프레임웍을 사용하면 되기 때문이다. 결정적으로 seq2seq 방식의 기계번역 모형을 keras로 만들면서 이 부분에 대한 절실함을 느꼈고(비주얼베이직 코드에 어셈블리어 코드를 심는 느낌…), 앞으로 뭔가 고도화되고 복잡하고 다이내믹한 모형을 빌드한다면 반드시 뭔가 대안이 필요할 것이라는 생각을 했다.

필자의 이번에 소개할 것은 Gluon이다. 사실 이 프레임웍을 사용하게 된 계기는 튜토리얼을 보고 흡사 Keras + PyTorch와 같은 느낌을 받았기 때문이다. 게다가 아직 버전이 낮음에도 불구하고 API가 생각보다 잘 정리되어 있다는 것과 HybridBlock으로 성능과 효율사이에서 사용자가 유연하게 선택할 수 있게 하는 기능도 보유하고 있기 때문이다.

일단 뭔가를 배우는 가장 빠른 방식은 해당 도구로 원하는 것을 직접 만들어 보는 것일 것인데, Gluon으로 얼마전에 만들어본 Grad CAM을 구현해 보도록 하겠다. 이전 글을 본 독자하면 앞의 코드는 건너뛰고 “학습, 테스트를 위한 DataLoader 정의” 부터 보길 바란다.

이 글의 목적은 필자의 Gluon 실습이며 동시에 keras와 유사한 결과를 보여준다면 어느정도 믿을만한 프레임웍이란 생각도 해볼 수 있는 근거마련을 하기 위함이다.

In [1]:
import pandas as pd
import numpy as np
from konlpy.tag import Mecab
from collections import Counter
import operator
import matplotlib.pyplot as plt
import numpy as np
import mxnet as mx
from mxnet import gluon, autograd
from mxnet.gluon import nn, rnn

%matplotlib inline

mecab = Mecab()

학습셋 전처리

In [2]:
tbl = pd.read_csv("nsmc/ratings_train.txt",sep='\t')

keywords = [mecab.morphs(str(i).strip()) for i in tbl['document']]

np.median([len(k) for k in keywords]), len(keywords), tbl.shape

keyword_cnt = Counter([i for item in keywords for i in item])
keyword_clip = sorted(keyword_cnt.items(), key=operator.itemgetter(1))[-5000:]
keyword_clip_dict = dict(keyword_clip)
keyword_dict = dict(zip(keyword_clip_dict.keys(), range(len(keyword_clip_dict))))
#공백과 미학습 단어 처리를 위한 사전 정보 추가  
keyword_dict['_PAD_'] = len(keyword_dict)
keyword_dict['_UNK_'] = len(keyword_dict) 
#키워드를 역추적하기 위한 사전 생성 
keyword_rev_dict = dict([(v,k) for k, v in keyword_dict.items()])
#리뷰 시퀀스 단어수의 중앙값 +5를 max 리뷰 시퀀스로 정함... 
max_seq =np.median([len(k) for k in keywords]) + 5
def encoding_and_padding(corp_list, dic, max_seq=50):
    from keras.preprocessing.sequence import pad_sequences
    coding_seq = [ [dic.get(j, dic['_UNK_']) for j in i]  for i in corp_list ]
    #일반적으로 리뷰는 마지막 부분에 많은 정보를 포함할 가능성이 많아 패딩은 앞에 준다. 
    return(pad_sequences(coding_seq, maxlen=max_seq, padding='pre', truncating='pre',value=dic['_PAD_']))

max_seq

train_x = encoding_and_padding(keywords, keyword_dict, max_seq=int(max_seq))
train_y = tbl['label']
train_x.shape, train_y.shape

학습, 테스트를 위한 DataLoader 정의

In [8]:
tr_idx = np.random.choice(train_x.shape[0], int(train_x.shape[0] * 0.9) )
tr_idx_set = set(tr_idx)
te_idx = np.array([i for i in range(train_x.shape[0]) if i not in tr_idx_set])
len(te_idx), len(tr_idx)

tr_set = gluon.data.ArrayDataset(train_x[tr_idx], train_y[tr_idx].as_matrix())
tr_data_iterator = gluon.data.DataLoader(tr_set, batch_size=100, shuffle=True)

te_set =gluon.data.ArrayDataset(train_x[te_idx], train_y[te_idx].as_matrix())
te_data_iterator = gluon.data.DataLoader(te_set, batch_size=100, shuffle=True)

type(train_y), type(train_x)
Out[8]:
(pandas.core.series.Series, numpy.ndarray)

네이버 무비 리뷰 분류 모형 with Gluon(MXNet)

In [33]:
class SentClassificationModel(gluon.Block):
    """무비 리뷰 분류 모형
    keras 모형과 같은 아키텍처로 구성했다. 
    """

    def __init__(self, vocab_size, num_embed, **kwargs):
        super(SentClassificationModel, self).__init__(**kwargs)
        with self.name_scope():
            self.embed = nn.Embedding(input_dim=vocab_size, output_dim=num_embed,
                                        weight_initializer = mx.init.Xavier())
            self.conv1 = nn.Conv1D(channels=32, kernel_size=1, padding=1)
            self.conv2 = nn.Conv1D(channels=16, kernel_size=2, padding=1)
            self.conv3 = nn.Conv1D(channels=8, kernel_size=3, padding=2)
            
            self.pool1 = nn.AvgPool1D()
            self.pool2 = nn.AvgPool1D()
            self.pool3 = nn.AvgPool1D()

            self.lstm = rnn.GRU(layout='NTC', hidden_size=10, dropout=0.2, bidirectional=True)
            self.dense = nn.Dense(units=1, activation='sigmoid')
            
            self.conv1_act = None
            
    def forward(self, inputs, get_act=False):
        em_out = self.embed(inputs)
        em_swaped = mx.nd.swapaxes(em_out, 1,2)
        conv1_ = self.conv1(em_swaped)
        if get_act:
            self.conv1_act = conv1_
            
        conv1_out = self.pool1(conv1_)
        conv2_ =self.conv2(em_swaped)
        conv2_out = self.pool2(conv2_)
        conv3_ =self.conv3(em_swaped)
        conv3_out = self.pool3(conv3_)
        cated_layer = mx.nd.concat(conv1_out, conv2_out, conv3_out , dim=1)
        cated_layer = mx.nd.swapaxes(cated_layer, 1,2)
        bigru_out  = self.lstm(cated_layer)
        outs = self.dense(bigru_out)
        return outs
In [54]:
ctx = mx.gpu()

#모형 인스턴스 생성 및 트래이너, loss 정의 
model = SentClassificationModel(vocab_size = len(keyword_dict), num_embed=50)
model.collect_params().initialize(mx.init.Xavier(), ctx=ctx)
#keras와 같은 값으로 learning_rate를 0.001로 했을 경우 모형이 오버피팅 되는 경향이 있어 줄였다. 
trainer = gluon.Trainer(model.collect_params(), 'rmsprop', optimizer_params={'learning_rate':0.0002})
loss = gluon.loss.SigmoidBCELoss(from_sigmoid=True)
In [55]:
#구조 출력 
print(model)
SentClassificationModel(
  (embed): Embedding(5002 -> 50, float32)
  (conv1): Conv1D(None -> 32, kernel_size=(1,), stride=(1,), padding=(1,))
  (conv2): Conv1D(None -> 16, kernel_size=(2,), stride=(1,), padding=(1,))
  (conv3): Conv1D(None -> 8, kernel_size=(3,), stride=(1,), padding=(2,))
  (pool1): AvgPool1D(size=(2,), stride=(2,), padding=(0,), ceil_mode=False)
  (pool2): AvgPool1D(size=(2,), stride=(2,), padding=(0,), ceil_mode=False)
  (pool3): AvgPool1D(size=(2,), stride=(2,), padding=(0,), ceil_mode=False)
  (lstm): GRU(None -> 30, NTC, dropout=0.2, bidirectional)
  (dense): Dense(None -> 1, Activation(sigmoid))
)

학습

In [56]:
def calculate_loss(model, data_iter, loss_obj, ctx=ctx):
    test_loss = []
    for i, (te_data, te_label) in enumerate(data_iter):
        te_data = te_data.as_in_context(ctx)
        te_label = te_label.as_in_context(ctx)
        with autograd.predict_mode():
            te_output = model(te_data)
            loss_te = loss_obj(te_output, te_label.astype('float32'))
        curr_loss = mx.nd.mean(loss_te).asscalar()
        test_loss.append(curr_loss)
    return(np.mean(test_loss))
In [57]:
epochs = 10


tot_test_loss = []
tot_train_loss = []
for e in range(epochs):
    train_loss = []
    #batch training 
    for i, (data, label) in enumerate(tr_data_iterator):
        data = data.as_in_context(ctx)
        label = label.as_in_context(ctx)
        with autograd.record():
            output = model(data)
            loss_ = loss(output, label.astype('float32'))
            loss_.backward()
        trainer.step(data.shape[0])
        curr_loss = mx.nd.mean(loss_).asscalar()
        train_loss.append(curr_loss)

    #caculate test loss
    test_loss = calculate_loss(model, te_data_iterator, loss_obj = loss, ctx=ctx) 

    print("Epoch %s. Train Loss: %s, Test Loss : %s" % (e, np.mean(train_loss), test_loss))    
    tot_test_loss.append(test_loss)
    tot_train_loss.append(np.mean(train_loss))
Epoch 0. Train Loss: 0.467143, Test Loss : 0.38506
Epoch 1. Train Loss: 0.366111, Test Loss : 0.375705
Epoch 2. Train Loss: 0.354542, Test Loss : 0.372504
Epoch 3. Train Loss: 0.348722, Test Loss : 0.370496
Epoch 4. Train Loss: 0.344557, Test Loss : 0.368375
Epoch 5. Train Loss: 0.340094, Test Loss : 0.366121
Epoch 6. Train Loss: 0.335008, Test Loss : 0.363967
Epoch 7. Train Loss: 0.328834, Test Loss : 0.361943
Epoch 8. Train Loss: 0.322123, Test Loss : 0.361348
Epoch 9. Train Loss: 0.31443, Test Loss : 0.356754
In [59]:
plt.plot(tot_train_loss)
plt.plot(tot_test_loss)
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'valid'], loc='upper left')
plt.show()

GRAD CAM 구현 및 예제 확인


In [60]:
def grad_cam_conv1D(model, x, y, loss, ctx):
    with autograd.record():
        output = model.forward(mx.nd.array([x,],ctx=ctx),get_act=True)
        loss_ = loss(output, mx.nd.array([[y,]],ctx=ctx).astype('float32'))
    loss_.backward()
    acts = model.conv1_act
    pooled_grad = mx.nd.mean(model.conv1.weight.grad(), axis=(1,2))
    for i in range(acts.shape[1]):
        acts[:,i,:] *= pooled_grad[i]
    heat = mx.nd.mean(acts, axis=1)
    return(heat.asnumpy()[0][1:-1])
In [61]:
tbl_test = pd.read_csv("nsmc/ratings_test.txt",sep='\t')
parsed_text = [mecab.morphs(str(i).strip()) for i in tbl_test['document']]
test_x = encoding_and_padding(parsed_text, keyword_dict, max_seq=int(max_seq))
test_y = tbl_test['label']
In [62]:
test_x.shape, test_y.shape
Out[62]:
((50000, 19), (50000,))
In [63]:
prob = model(mx.nd.array(test_x, ctx=ctx))
In [64]:
prob_np = prob.asnumpy()
In [65]:
%reload_ext rpy2.ipython
In [66]:
%%R -i test_y -i prob_np
require(pROC)
plot(roc(test_y, prob_np), print.auc=TRUE)
In [67]:
idx = 90
In [68]:
prob_np[idx], tbl_test.iloc[idx], test_y[idx]
Out[68]:
(array([ 0.0202708], dtype=float32),
 id                                                   9912932
 document    로코 굉장히 즐겨보는데, 이 영화는 좀 별로였다. 뭔가 사랑도 개그도 억지스런 느낌..
 label                                                      0
 Name: 90, dtype: object,
 0)
In [69]:
heat = grad_cam_conv1D(model, test_x[idx], y=0, loss=loss, ctx=ctx)
In [70]:
hm_tbl = pd.DataFrame({'heat':heat, 'kw':[keyword_rev_dict[i] for i in test_x[idx] ]})
In [71]:
%%R -i hm_tbl
library(ggplot2)

ggplot(hm_tbl, aes(x=kw, y=heat)) + geom_bar(stat='identity')

결과가 “별로, 억지, 개그”와 같은 단어가 부정적인 리뷰에 기여하는것으로 도출되었으며, 예상했던 대로 keras의 결과와 유사하게 도출되었다.
코드를 보면 알겠지만, forword()함수에 인자를 줘 중간 convolution 결과를 맴버변수에 적재해 놓은 부분을 볼 수 있을 것이다.다소 직관적인 코드로 keras가 별도 백엔드 API를 이해해야 되는 것에 비하면 간편한 부분이라 생각한다.
PyTorch로 그냥 갈까 하다가도 최근 MXNet 행보를 보거나 Gluon의 개발 속도를 보자면 좀더 전망이 있어 보이는 프레임웍이 아닐까 한다.