NeuroWhAI의 잡블로그

[TensorFlow] Seq2Seq로 간단한 번역기 만들기 본문

개발 및 공부/라이브러리&프레임워크

[TensorFlow] Seq2Seq로 간단한 번역기 만들기

NeuroWhAI 2018. 2. 3. 13:20


※ 이 글은 '골빈해커의 3분 딥러닝 텐서플로맛'이라는 책을 보고 실습한걸 기록한 글입니다.


코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#-*- coding: utf-8 -*-
 
import sys
import tensorflow as tf
import numpy as np
 
char_arr = [c for c in 'SEPabcdefghijklmnopqrstuvwxyz단어나무놀이소녀키스사랑']
 
num_dic = {n: i for i, n in enumerate(char_arr)}
dic_len = len(num_dic)
 
seq_data = [['word''단어'], ['wood''나무'], ['game''놀이'],
            ['girl''소녀'], ['kiss''키스'], ['love''사랑']]
 
def make_batch(seq_data):
    input_batch = []
    output_batch = []
    target_batch = []
 
    for seq in seq_data:
        input = [num_dic[n] for n in seq[0]]
        output = [num_dic[n] for n in ('S' + seq[1])]
        target = [num_dic[n] for n in (seq[1+ 'E')]
 
        input_batch.append(np.eye(dic_len)[input])
        output_batch.append(np.eye(dic_len)[output])
        target_batch.append(target)
 
    return input_batch, output_batch, target_batch
 
def translate(sess, model, word):
    seq_data = [word, 'P' * len(word)]
 
    input_batch, output_batch, target_batch = make_batch([seq_data])
 
    prediction = tf.argmax(model, 2)
 
    result = sess.run(prediction, feed_dict={enc_input: input_batch,
        dec_input: output_batch,
        targets: target_batch})
 
    decoded = [char_arr[i] for i in result[0]]
 
    try:
        end = decoded.index('E')
        translated = ''.join(decoded[:end])
 
        return translated
    except:
        return ''.join(decoded)
 
learning_rate = 0.01
n_hidden = 128
total_epoch = 100
 
n_input = n_class = dic_len
 
enc_input = tf.placeholder(tf.float32, [None, None, n_input])
dec_input = tf.placeholder(tf.float32, [None, None, n_input])
targets = tf.placeholder(tf.int64, [None, None])
 
with tf.variable_scope('encode'):
    enc_cell = tf.nn.rnn_cell.BasicRNNCell(n_hidden)
    enc_cell = tf.nn.rnn_cell.DropoutWrapper(enc_cell, output_keep_prob=0.5)
 
    outputs, enc_states = tf.nn.dynamic_rnn(enc_cell, enc_input,
        dtype=tf.float32)
 
with tf.variable_scope('decode'):
    dec_cell = tf.nn.rnn_cell.BasicRNNCell(n_hidden)
    dec_cell = tf.nn.rnn_cell.DropoutWrapper(dec_cell, output_keep_prob=0.5)
 
    outputs, dec_states = tf.nn.dynamic_rnn(dec_cell, dec_input,
        initial_state=enc_states, dtype=tf.float32)
 
model = tf.layers.dense(outputs, n_class, activation=None)
 
cost = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(
    logits=model, labels=targets
))
 
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)
 
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    writer = tf.summary.FileWriter('./logs', sess.graph)
 
    input_batch, output_batch, target_batch = make_batch(seq_data)
 
    for epoch in range(total_epoch):
        _, loss = sess.run([optimizer, cost],
            feed_dict={enc_input: input_batch,
                dec_input: output_batch,
                targets: target_batch})
 
        print('Epoch:''%04d' % (epoch + 1),
            'cost =''{:.6f}'.format(loss))
        sys.stdout.flush()
 
    print('완료!')
 
    print('\n=== 테스트 ===')
    print('word ->', translate(sess, model, 'word'))
    print('wodr ->', translate(sess, model, 'wodr'))
    print('love ->', translate(sess, model, 'love'))
    print('loev ->', translate(sess, model, 'loev'))
    print('abcd ->', translate(sess, model, 'abcd'))
 
cs

결과:
Epoch: 0001 cost = 3.775746
Epoch: 0002 cost = 2.714902
Epoch: 0003 cost = 1.914051
Epoch: 0004 cost = 1.225858
Epoch: 0005 cost = 0.644970
Epoch: 0006 cost = 0.294245
Epoch: 0007 cost = 0.302635
Epoch: 0008 cost = 0.305286
Epoch: 0009 cost = 0.111487
Epoch: 0010 cost = 0.159784
(중략)
Epoch: 0091 cost = 0.001104
Epoch: 0092 cost = 0.000147
Epoch: 0093 cost = 0.000220
Epoch: 0094 cost = 0.000447
Epoch: 0095 cost = 0.000271
Epoch: 0096 cost = 0.000894
Epoch: 0097 cost = 0.000134
Epoch: 0098 cost = 0.000389
Epoch: 0099 cost = 0.000438
Epoch: 0100 cost = 0.000191
완료!

=== 테스트 ===
word -> 단어
wodr -> 단어
love -> 사랑
loev -> 사랑
abcd -> 소녀

사실 검색해보니까 seq2seq 예제는 대부분 전용 API를 이용해서 구현하더라고요.
근데 책에서 이렇게 하니까 그냥 따라했습니다.

코드 설명은 특이사항 위주로 적어보겠습니다.


make_batch 함수를 보시면 디코더의 입력이 될 output_batch와 디코더의 목표 출력이 될 target_batch를 만들때
'S'와 'E'를 각각 앞, 뒤에 이어붙히고 있는데 이걸 심볼이라고 부른다고 합니다.
S는 디코더에게 번역을 시작하라는걸 알려주는 입력이고 E는 디코더가 번역이 끝났음을 알려주는 용도로 쓰이는 특수한 문자들이죠.
위 사진에서 '< go >'가 'S'와 같고 '<eos>'가 'E'와 같은 역할을 합니다.

이번에도 sparse_softmax_cross_entropy_with_logits를 사용할 것이라서
target_batch는 원 핫 인코딩하지 않습니다.

translate는 나중에 보겠습니다.

인코더의 입력이 될 enc_input은 [None, None, n_input] Shape인데
[batch size, step size, n_input]으로 보시면 됩니다.
batch size는 원래 이때까지 None으로 뒀었지만
step size까지 None으로 둔 이유는 책의 저자께서 길이가 다른 단어들도 사용해보라는 의미라고 하셨습니다.
단어 길이가 달라지면 step size도 달라지니까요.
dec_input와 targets의 Shape도 같은 이유에서 저렇습니다.

인코더를 만드는건 특이사항이 없지만 디코더엔 주목할 부분이 있습니다.
인코더 RNN의 최종 state인 enc_states를 디코더 RNN의 처음 state로 설정해주고 있다는 점입니다.
initial_state=enc_states
이 부분이죠.


위 그림에서 빨간색으로 표시한 부분에 해당합니다.
인코더의 최종 state 출력을 디코더의 첫 state 입력으로 넣어주고 있죠.

다음으론 디코더의 출력에 Dense 계층을 연결해줬는데
(batch size, step size, n_hidden)의 3차원 텐서인 디코더 출력 outputs를 어떻게 바로 입력으로 넣어줄 수 있었는지 이해가 안됬었습니다.
그래서 따로 글을 써놨습니다.
아무튼 이렇게 Dense 계층을 거치고나면 model은 (batch size, step size, n_class) Shape의 텐서가 됩니다.

남은건 손실함수를 만들고 트레이닝을 시키면 끝이지만
결과 확인을 위해서 translate 함수가 쓰이는데 아까 위에서 그냥 넘어갔었으니 이제 적어보겠습니다.
translate는 영어 단어 word를 받아서 번역된 문자열로 반환해주도록 작성한 함수입니다.
(sess, model 파라미터는 그냥 계산에 필요해서 받고 있습니다)
원래 seq2seq 예제들을 보면 훈련용과 테스트용을 따로 만들고
테스트시에는 디코더의 출력을 다시 디코더의 입력으로 사용하도록 합니다.
하지만 여기선 그렇게 따로 만들지 않아서 테스트시에는 'P' 심볼로 채워진 의미없는 문자열을 디코더의 입력으로 줍니다.
seq_data = [word, 'P' * len(word)]
위 코드가 해당 작업에 쓰일 입력을 만듭니다.
(근데 위 코드로는 word보다 번역 이후의 단어길이가 더 길 경우 잘리는게 아닐까 싶은데... 뭐 아무튼)
위 seq_data로 make_batch를 호출해서 실제 입력에 사용할 데이터를 만듭니다.
그런다음 아래 코드를 사용하는데
prediction = tf.argmax(model, 2)
model의 3번째 축에서 argmax(리스트의 요소 중 가장 큰 값의 인덱스를 취함)연산을 수행합니다.
이게 무슨 뜻이냐면
model의 Shape은 (batch size, step size, n_class)라고 했었습니다.
3번째 축은 n_class가 되는데 즉, 원 핫 인코딩 되어있는 데이터 리스트들에 대해 argmax를 수행한다는 것이고
이건 곧 원 핫 인코딩하기 전의 값인 문자에 대응하는 번호로 만든다는 소리가 됩니다.
고로 결과 Shape은 (batch size, step size)가 됩니다.
세션에서 prediction을 연산하고 result에 담았는데
batch size가 1이므로 result[0]으로 바로 step size의 리스트에 접근하고 있습니다.
문자 번호가 저장되어 있으므로 이걸 문자로 바꿔주고 종료문자('E') 전까지만 join해주면
번역된 단어가 나오게 됩니다.
(try-except가 쓰인 이유는 책에서 'E'가 없을경우 예외가 발생할 수 있다고 말해서 입니다)


이걸로 설명을 빙자한 혼자 이해하기는 끝!
갈수록 이해하기가 힘들어지네요.
사실 그럴 수 밖에 없는게 이런 비교적 최신 신경망?들은 제가 이론적으로 공부할 기회가 별로 없어서... ㅠㅠ




Comments