[Deep Learning] Language Modeling with RNN

LM(Languge Model, 언어 모델)

LM

  • 언어 모델: 문장과 같은 단어 시퀀스에서 각 단어의 확률을 계산하는 모델
  • Word2Vec 또한 여러가지 언어모델 중 하나이다.
  • $l$개의 단어로 구성된 문장은 아래와 같이 나타낼 수 있다.
\[w_1, w_2, w_3, ..., w_l\]
  • CBoW가 target word를 예측할 확률 $P(w_t)$ 는 아래와 같이 나타낼 수 있다.
\[P(w_t \vert w_{t-2},w_{t-1},w_{t+1},w_{t+2})\]
  • Word2Vec이 나오기 전까지 많은 언어 모델은 목표 단어 왼쪽의 단어만을 고려하여 확률을 계산했다.
  • $t$번째로 단어를 예측하기 위해서 0번째부터 $t-1$번째 까지의 모든 단어 정보를 사용한다.
\[P(w_t \vert w_{t-1},w_{t-2}, \cdots ,w_1,w_0)\]
  • $l$개의 단어로 이루어진 문장이 만들어질 확률은 아래와 같이 나타낼 수 있다.
\[P(w_0,w_1, \cdots, w_{l-1}, w_l) = P(w_0)P(w_1 \vert w_0) \cdots P(w_{l-1} \vert w_{l-2}, \cdots, w_1, w_0)P(w_l \vert w_{l-1}, w_{l-2}, \cdots, w_1, w_0)\]
  • 위 언어 모델을 사용하여 ‘I am a student’라는 문장이 만들어질 확률을 구하면 아래와 같이 나타낼 수 있다.
\[P(\text{'I','am','a','student'}) = P(\text{'I'}) \times P(\text{'am'} \vert \text{'I'}) \times P(\text{'a'} \vert \text{'I','am'}) \times P(\text{'student'} \vert \text{'I','am','a'})\]
  • 앞 단어가 등장했을 때 특정 단어가 등장할 확률은 조건부 확률로 구하게 된다.

SLM(Statistical Language Model, 통계적 언어 모델)

  • 신경망 언어 모델이 주목받기 전부터 연구되어 온 전통적인 접근 방식이다.

SLM의 확률 계산

  • SLM에서는 단어의 등장 횟수를 바탕으로 조건부 확률을 계산한다.
\[P(\text{'I','am','a','student'}) = P(\text{'I'}) \times P(\text{'am'} \vert \text{'I'}) \times P(\text{'a'} \vert \text{'I','am'}) \times P(\text{'student'} \vert \text{'I','am','a'})\]
  • 전체 말뭉치의 문장 중 시작할 때 ‘I’로 시작하는 문장의 횟수를 구할 때, 전체 말뭉치의 문장이 1000개이고, 그 중 ‘I’로 시작하는 문장이 100개라면,
\[P(\text{'I'}) = \frac{100}{1000} = \frac{1}{10}\]
  • ‘I’로 시작하는 100개의 문장 중 바로 다음에 ‘am’이 등장하는 문장이 50개 라면,
\[P(\text{'am'} \vert \text{'I'}) = \frac{50}{100} = \frac{1}{2}\]
  • 모든 조건부 확률을 구한 뒤 서로를 곱해주면 문장이 등장할 확률 $P(\text{‘I’,’am’,’a’,’student’})$ 을 구할 수 있다.

SLM의 한계점

  • 횟수 기반으로 확률을 계산하기 때문에 Sparsity(희소성) 문제를 갖고 있다.
  • 학습시킬 말뭉치에 등장하지 않는 표현이라면 절대 만들어 낼 수 없다.
  • 실제로 사용되는 표현임에도 말뭉치에 등장하지 않았다는 이유로 많은 문장이 등장하지 못하게 되는 문제를 희소 문제라고 한다.
  • 통계적 언어 모델의 이런 문제를 개선하기 위해 N-gram이나 Smooting, Back-off와 같은 방법이 고안되었다.(찾아보기)

NLM(Neural Langauge Model, 신경망 언어 모델)

  • NLM에서는 횟수 기반 대신 Word2Vec이나 festText 등의 출력값인 Embedding Vector를 사용한다.
  • 말뭉치에 등장하지 않더라도 의미적, 문법적으로 유사한 단어라면 선택될 수 있다.

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

  • Sequential Data(연속형 데이터)를 처리하기 위해 고안된 신경망 구조이다.
    • Sequential Data: 어떤 순서로 오느냐에 따라 단위의 의미가 달라지는 데이터(대부분의 데이터가 순차 데이터이다. 일반적으로 이미지 데이터는 속하지 않는다.)

RNN의 구조

image

  • 3개의 화살표
    1. 입력 벡터가 은닉층에 들어가는 것을 나타내는 화살표
    2. 은닉층으로부터 출력 벡터가 생성되는 것을 나타내는 화살표
    3. 은닉층에서 나와 다시 은닉층으로 입력되는 것을 나타내는 화살표
  • 3번 화살표는 기존 신경망에서는 없었던 과정이다.
  • 이 화살표는 특정 시점에서의 은닉 벡터가 다음 시점의 입력 벡터로 다시 들어가는 과정을 나타낸다.
  • 출력 벡터가 다시 입력되는 특성 때문에 ‘순환 신경망’이라는 이름이 붙었다.
  • 오른쪽 그림 처럼 시점에 따라 펼쳐본다면,
    • $t-1$ 시점에서는 $x_{t-1}$ 와 $h_{t-2}$가 입력되고 $o_{t-1}$ 이 출력된다.
    • $t$ 시점에서는 $x_t$ 와 $h_{t-1}$ 가 입력되고 $o_t$ 이 출력된다.
    • $t+1$ 시점에서는 $x_{t+1}$ 와 $h_t$ 가 입력되고 $o_{t+1}$ 이 출력된다.

image

  • $t$ 시점의 RNN 계층은 그 계층으로의 입력 벡터 $x_t$와 1개 전의 RNN 계층의 출력 백터$h_t-1$을 받아들인다.
  • 입력된 두 벡터를 바탕으로 해당 시점에서의 출력을 아래와 같이 계산한다.
\[h_t = \tanh(h_{t-1}W_h + x_tW_x + b)\]
  • 가중치는 $W_h, W_x$ 2개가 있다.
  • 각각 입력 $x$를 $h$로 변환하기 위한 $W_x$와 RNN의 은닉층의 출력을 다음 h로 변환해주는 $W_h$이다.
  • $b$는 각 편향(bias)을 단순화하여 나타낸 항이다.
  • 이렇게 하면 $t$ 시점에 생성되는 hidden-state 벡터인 $h_t$ 는 해당 시점까지 입력된 벡터 $x_1, x_2, \cdots, x_{t-1}, x_t$ 의 정보를 모두 가지고 있다.
  • Sequential 데이터의 순서 정보를 모두 기억하기 때문에 Sequential 데이터를 다룰 때 RNN을 많이 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# RNN 코드 구현
import numpy as np

class RNN:
    """
    RNN을 구현한 클래스

    Args:
        Wx : time-step 별 입력 벡터에 곱해지는 가중치
        Wh : 이전 time-step 에서 넘어온 Hidden state vector에 곱해지는 가중치
        b : 편향(bias)
    """
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None

    def forward(self, x, h_prev):
        Wx, Wh, b = self.params
        t = np.matmul(h_prev, Wh) + np.matmul(x, Wx) + b
        h_next = np.tanh(t)

        self.cache = (x, h_prev, h_next)
        return h_next

RNN의 형태

image

one-to-many

  • 1개의 벡터를 받아 Sequential한 벡터를 반환한다.
  • 이미지를 입력받아 이를 설명하는 문장을 만들어 내는 Image Captioning에 사용된다.

many-to-one

  • Sequential 벡터를 받아 1개의 벡터를 반환한다.
  • 문장이 긍정인지 부정인지 판단하는 Sentiment Analysis에 사용된다.

many-to-many(1)

  • Sequential 벡터를 모두 입력받은 뒤 Sequential 벡터를 출력한다.
  • Seq2Seq 구조라고도 부른다.
  • 번역할 문장을 입력받아 번역된 문장을 내놓는 Machine translation에 사용된다.

many-to-many(2)

  • Sequential 벡터를 입력받는 즉시 Sequential 벡터를 출력한다.
  • 비디오를 프레임별로 분류(Video Classification per frame)하는 곳에 사용된다.

RNN의 장점과 단점

장점

  • 모델이 간단하고 어떤 길이의 Sequential data라도 처리할 수 있다.

단점

Parallelization(병렬화) 불가능

  • 벡터가 순차적으로 입력된다.
  • 이는 Sequential 데이터 처리를 가능하게 해주는 요인이지만, 이러한 구조는 GPU 연산의 장점인 병렬화를 불가능하게 만든다.

Exploding Gradient(기울기 폭발), Vanishing Gradient(기울기 소실)

  • 치명적인 문제점은 역전파 과정에서 발생한다.
  • 역전파 과정에서 RNN의 활성화 함수인 tanh의 미분값을 전달하게 되는데, tanh를 미분한 함수의 값은 아래와 같다.

image

  • 최댓값이 1이고 (-4, 4) 이외의 범위에서는 거의 0에 가까운 값을 나타낸다.
  • 문제는 역전파 과정에서 이 값을 반복해서 곱해주어야 한다는 점이다.
  • 이 Recurrent가 10회, 100회 반복된다고 보면, 이 값의 10제곱, 100제곱이 식 내부로 들어가게 된다.
  • Vanishing Gradient(기울기 소실)
    • 만약 이 값이 0.9일 때 10제곱이 된다면 0.349가 된다.
    • 이렇게 되면 시퀀스 앞쪽에 있는 hidden-state 벡터에는 역전파 정보가 거의 전달되지 않게 된다.
  • Exploding Gradient(기울기 폭발)
    • 만약 이 값이 1.1일 때 10제곱만해도 2.59배로 커지게 된다.
    • 이렇게 되면 시퀀스 앞쪽에 있는 hidden-state 벡터에는 역전파 정보가 과하게 전달된다.
  • 기울기 정보의 크기가 문제라면 적절하게 조정하여 준다면 문제를 해결할 수 있지 않을까라는 생각을 통해 고안된 것이 LSTM(Long-Short Term Memory, 장단기 기억망)이다.

LST & GRU

LSTM(Long Term Short Memory, 장단기기억망)

  • RNN에 기울기 정보 크기를 조절하기 위한 Gate를 추가한 모델을 LSTM이라고 한다.
  • 요즘은 RNN이라고 하면 당연히 LSTM이나 GRU를 지칭한다.

LSTM의 구조

image

  • LSTM은 기울기 소실 문제를 해결하기 위해 3가지 Gate를 추가했다.
    1. forget gate ($f_t$): 과거 정보를 얼마나 유지할 것인가?
    2. input gate ($i_t$): 새로 입력된 정보는 얼마만큼 활용할 것인가?
    3. output gate ($o_t$): 두 정보를 계산하여 나온 출력 정보를 얼마만큼 넘겨줄 것인가?
  • hidden-state 말고도 활성화 함수를 직접 거치지 않는 상태인 cell-state가 추가됐다.
  • cell-state는 역전파 과정에서 활성화 함수를 거치지 않아 정보 손실이 없기 때문에 뒷쪽 시퀀스의 정보에 비중을 결정할 수 있으면서 동시에 앞쪽 시퀀스의 정보를 완전히 잃지 않을 수 있다.

LSTM의 역전파

image

LSTM의 사용

  • 언어 모델 뿐만 아니라 신경망을 활용한 시계열 알고리즘에는 대부분 LSTM을 사용하고 있다.

GRU(Gated Recurrent Unit)

image

GRU의 특징

  • LSTM에서 있었던 cell-state가 사라졌다.
    • cell-state 벡터 $c_t$ ​와 hidden-state 벡터 $h_t$​가 하나의 벡터 $h_t$​로 통일되었다.
  • 하나의 Gate $z_t$가 forget, input gate를 모두 제어한다.
    • $z_t$가 1이면 forget 게이트가 열리고, input 게이트가 닫히게 되는 것과 같은 효과를 나타낸다.
    • 반대로 $z_t$가 0이면 input 게이트만 열리는 것과 같은 효과를 나타낸다.
  • GRU 셀에서는 output 게이트가 없어졌다.
    • 대신 전체 상태 벡터 $h_t$ 가 각 time-step에서 출력되며, 이전 상태의 $h_{t-1}$ 의 어느 부분이 출력될 지 새롭게 제어하는 Gate인 $r_t$ 가 추가되었다.

LSTM 코드 실습

Keras 이용 RNN/LSTM 텍스트 감정 분류

  • IMDB 영화 리뷰 데이터
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
110
111
112
113
114
115
116
117
118
119
120
121
from __future__ import print_function

from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding
from tensorflow.keras.layers import LSTM
from tensorflow.keras.datasets import imdb

# 파라미터 설정
max_features = 20000
maxlen = 80
batch_size = 32

# 데이터 import
print('Loading data...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')
'''
Loading data...
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 0s 0us/step
<string>:6: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
/usr/local/lib/python3.7/dist-packages/tensorflow/python/keras/datasets/imdb.py:155: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
  x_train, y_train = np.array(xs[:idx]), np.array(labels[:idx])
25000 train sequences
25000 test sequences
/usr/local/lib/python3.7/dist-packages/tensorflow/python/keras/datasets/imdb.py:156: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
  x_test, y_test = np.array(xs[idx:]), np.array(labels[idx:])
'''


# Sequence Padding
print('Pad Sequences (samples x maxlen)')
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape: ', x_train.shape)
print('x_test shape: ', x_test.shape)
'''
Pad Sequences (samples x time)
x_train shape:  (25000, 80)
x_test shape:  (25000, 80)
'''


x_train[0]
'''
array([   15,   256,     4,     2,     7,  3766,     5,   723,    36,
          71,    43,   530,   476,    26,   400,   317,    46,     7,
           4, 12118,  1029,    13,   104,    88,     4,   381,    15,
         297,    98,    32,  2071,    56,    26,   141,     6,   194,
        7486,    18,     4,   226,    22,    21,   134,   476,    26,
         480,     5,   144,    30,  5535,    18,    51,    36,    28,
         224,    92,    25,   104,     4,   226,    65,    16,    38,
        1334,    88,    12,    16,   283,     5,    16,  4472,   113,
         103,    32,    15,    16,  5345,    19,   178,    32],
      dtype=int32)
'''


import tensorflow as tf

# model을 정의합니다.
# dropout, recurrent_dropout 차이 찾아보기
"""
keras의 기본 Embedding 벡터 사용
LSTM 층에 dropout/recurrent_dropout 적용
"""
model = tf.keras.models.Sequential([
  tf.keras.layers.Embedding(max_features, 128),
  tf.keras.layers.LSTM(128, dropout=0.2, recurrent_dropout=0.2),
  tf.keras.layers.Dense(1, activation='sigmoid')
])

model.compile(loss='binary_crossentropy',
              optimizer='adam', 
              metrics=['accuracy'])

model.summary()
'''
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 128)         2560000   
_________________________________________________________________
lstm (LSTM)                  (None, 128)               131584    
_________________________________________________________________
dense (Dense)                (None, 1)                 129       
=================================================================
Total params: 2,691,713
Trainable params: 2,691,713
Non-trainable params: 0
_________________________________________________________________
'''


unicorns = model.fit(x_train, y_train,
          batch_size=batch_size, 
          epochs=3, 
          validation_data=(x_test,y_test))
'''
Epoch 1/3
782/782 [==============================] - 155s 195ms/step - loss: 0.4287 - accuracy: 0.7973 - val_loss: 0.3605 - val_accuracy: 0.8402
Epoch 2/3
782/782 [==============================] - 152s 194ms/step - loss: 0.2566 - accuracy: 0.8985 - val_loss: 0.4096 - val_accuracy: 0.8314
Epoch 3/3
782/782 [==============================] - 152s 195ms/step - loss: 0.1684 - accuracy: 0.9369 - val_loss: 0.4209 - val_accuracy: 0.8296
'''


import matplotlib.pyplot as plt

# Plot training & validation loss values
plt.plot(unicorns.history['loss'])
plt.plot(unicorns.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show();

image

Keras 이용 LSTM 텍스트 생성기

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# 니체 글 학습하여 비슷한 글 생성 튜토리얼 코드로 생성 실습
from __future__ import print_function
from keras.callbacks import LambdaCallback
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random
import sys
import io

path = get_file(
    'nietzsche.txt',
    origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
with io.open(path, encoding='utf-8') as f:
    text = f.read().lower()
print('corpus length:', len(text))

chars = sorted(list(set(text)))
print('total chars:', len(chars))
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
'''
Downloading data from https://s3.amazonaws.com/text-datasets/nietzsche.txt
606208/600901 [==============================] - 0s 1us/step
614400/600901 [==============================] - 0s 1us/step
corpus length: 600893
total chars: 57
'''


# max length를 이용하여 문자열의 크기 정렬
maxlen = 40
step = 3

sentences = []
next_chars = []

for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[i + maxlen])
print('nb sequences:', len(sentences))

print('Vectorization...')

x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)

for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1
'''
nb sequences: 200285
Vectorization...
'''


# LSTM 모델 제작
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

optimizer = RMSprop(learning_rate=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)
'''
Build model...
'''


# sample 문장 생성 함수
def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)


# Epoch가 끝날 때마다 sample 문장 생성 함수
def on_epoch_end(epoch, _):
    print()
    print('----- Generating text after Epoch: %d' % epoch)

    start_index = random.randint(0, len(text) - maxlen - 1)

    # temperature를 조정하여 단어 선택 시 다양성을 부여합니다.
    """
    https://3months.tistory.com/491, https://stackoverflow.com/questions/58764619/why-should-we-use-temperature-in-softmax
    위 링크 참조하여 temperature(diversity) 값이 커질수록
    단어 선택이 어떻게 변할 지 찾아보기
    """ 
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print('----- diversity:', diversity)

        generated = ''
        sentence = text[start_index: start_index + maxlen]
        generated += sentence
        print('----- Generating with seed: "' + sentence + '"')
        sys.stdout.write(generated)

        for i in range(400):
            x_pred = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.

            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]

            generated += next_char
            sentence = sentence[1:] + next_char

            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)


model.fit(x, y,
          batch_size=128,
          epochs=60,
          callbacks=[print_callback])
'''
Epoch 1/60
1565/1565 [==============================] - 125s 79ms/step - loss: 2.2720

----- Generating text after Epoch: 0
----- diversity: 0.2
----- Generating with seed: "th which is
occasionally mingled a sligh"
th which is
occasionally mingled a slight the such a server the such as in the prese the great and and great and sense of the prese the stand the world and the prese the stronger and the world and many one a present of the such a soul and a soul the stand and still the master of the such as in the present and the world and and the such as a sure and in the self the will the prese of the such as in the present of the prese of the such co
----- diversity: 0.5
----- Generating with seed: "th which is
occasionally mingled a sligh"
th which is
occasionally mingled a slight they an the this more they are in the sure and in the pals in the world responss of in the such philosopher hell the world in their self, as a serstaral and world the will not the fame be sure sense of and still and must the a men whon the will not will with the will as it is relight of a price and mankind prepsing good and compreces and formen which it is the wished many some oncempreded the co
----- diversity: 1.0
----- Generating with seed: "th which is
occasionally mingled a sligh"
th which is
occasionally mingled a slight
soees and it lome
legoudness alak in alsouths. whas not it is ceuld in the bich the themselves or less man" thevesang
even the dotentuly man--ruatical taoke be the onled oven extray, are rowal explarations theum with re
or iglet beyivead of the lowe abong still
purtenciunan  would to us. knought. and hordcintt accsvider, his severfory, for doren eptoss--we a is
adpocition im wrome as the world p
'
'
'
----- diversity: 1.0
----- Generating with seed: "clusions
closely resembling the judaic o"
clusions
closely resembling the judaic only "si
flread, and and (and attensbility, something of the powerful gabje noss therepates and would at the after the best, and all includ in the
man of was into their tarst observation, only something
dipsted to the different
respect estimateed and age of cestrobliness to men and importentars who wangonres--in men, the respull to habits only rewats and sleep, and light will known with evolvations
----- diversity: 1.2
----- Generating with seed: "clusions
closely resembling the judaic o"
clusions
closely resembling the judaic octation, character,
kind olding ding aliginacily bstws tended the "free fviluted.
d us vi." freed!

yëuge classity. there as i one. from prescribelly. do a meaning is above--succession of atton-sigbny, mean just laws and expierations, explanationtion, all
ditewationed rilu-with
among it knowled, a psythroc-in--therefren, flaws the
germany opinion,
imseed in nature. , for
us for whom feltration whi
Epoch 57/60
 501/1565 [========>.....................] - ETA: 1:35 - loss: 1.2432
'''

RNN 구조에 Attention 적용

기존 RNN(LSTM, GRU) 기반 번역 모델의 단점

  • RNN이 가진 가장 큰 단점 중 하나는 기울기 소실로부터 나타나는 장기 의존성(Long-term dependency)문제이다.
  • 장기 의존성 문제란 문장이 길어질 경우 앞 단어의 정보를 일어버리게 되는 현상이다.
  • 장기 의존성 문제를 해결하기 위해 나온 것이 셀 구조를 개선한 LSTM과 GRU이다.
  • 기계 번역에서 기존의 RNN 기반의 모델(LSTM, GRU)이 단어를 처리하는 방법은 아래와 같다.

seq2seq_6

Attention

  • 위 문제는 고정 길이의 hidden-state 벡터에 모든 단어의 의미를 담아야 한다는 점이다.
  • 아무리 LSTM, GRU가 장기 의존성 문제를 개선하였더라도 문장이 매우 길어지면 모든 단어 정보를 고정 길이의 hidden-state에 담기 어렵다.
  • 이런 문제를 해결하기 위해 고안된 방법이 Attention이다.

seq2seq_7

  • Attention은 각 인코더의 Time-step 마다 생성되는 hidden-state 벡터를 간직한다.
  • 입력 단어가 N개 라면 N개의 hidden-state 벡터를 모두 간직한다.
  • 모든 단어가 입력되면 생성된 hidden-state 벡터를 모두 디코더에 넘겨준다.

검색 시스템의 아이디어

image

  • 검색 시스템의 3단계
    1. 찾고자 하는 정보에 대한 Query 입력한다.
    2. 검색 엔진은 검색어와 가장 비슷한 key를 찾는다.
    3. 해당 key와 연결된 Value를 유사도 순서대로 보여준다.

디코더에서 Attention의 동작

  • 디코더의 각 time-step 마다의 hidden-state 벡터는 쿼리로 작용한다.
  • 인코더에서 넘어온 N개의 hidden-state 벡터를 key로 여기고 이들과의 연관성을 계산한다.
  • 이 때 계산은 내적(dot-product)을 사용하고 내적의 결과를 Attention 가중치로 사용한다.
  • 아래 그림은 디코더 첫 단어에 대한 어텐션 가중치가 구해지는 과정이다.

image

  1. 쿼리(보라색)로 디코더의 hidden-state 벡터, 키(주황색)로 인코더에서 넘어온 각각의 hidden-state 벡터를 준비한다.
  2. 각각의 벡터를 내적한 값을 구한다.
  3. 이 값에 softmax 함수를 취해준다.
  4. 소프트맥스를 취하여 나온 값에 Value(주황색)에 해당하는 인코더에서 넘어온 hidden-state 벡터를 곱해준다.
  5. 이 벡터를 모두 더하여 Context 벡터(파란색)를 만들어준다. 이 벡터의 성분 중에는 쿼리-키 연관성이 높은 벨류 벡터의 성분이 더 많이 들어있다.
  6. 최종적으로 5에서 생성된 Context 벡터와 디코더의 hidden-state 벡터를 사용하여 출력 단어를 결정하게 된다.
  • 디코더는 인코더에서 넘어온 모든 hidden state 벡터에 대해 위와 같은 계산을 실시한다.
  • 그렇기 때문에 Time-step마다 출력할 단어가 어떤 인코더의 어떤 단어 정보와 연관되어 있는지, 즉 어떤 단어에 집중할 지 알 수 있다.
  • Attention을 활용하면 디코더가 인코더에 입력되는 모든 단어의 정보를 활용할 수 있기 때문에 장기 의존성 문제를 해결할 수 있다.

attn_visualization

  • 위 그림은 문장을 번역(Je suis etudiant -> I am a student) 했을 때, 각 단어마다의 Attention 스코어를 시각화 한 그림이다.
  • 왼쪽 단어가 생성될 때 오른쪽 단어와 연관되어 있음을 확인할 수 있다.

LSTM with Attention 코드 실습

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
import tensorflow as tf

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from sklearn.model_selection import train_test_split

import unicodedata
import re
import numpy as np
import os
import io
import time


path_to_zip = tf.keras.utils.get_file(
    'spa-eng.zip', origin='http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip',
    extract=True)

path_to_file = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"


# 유니코드 파일을 아스키코드로 변환하는 함수
def unicode_to_ascii(s):
  return ''.join(c for c in unicodedata.normalize('NFD', s)
                 if unicodedata.category(c) != 'Mn')


def preprocess_sentence(w):
  w = unicode_to_ascii(w.lower().strip())

  # creating a space between a word and the punctuation following it
  # eg: "he is a boy." => "he is a boy ."
  # Reference:- https://stackoverflow.com/questions/3645931/python-padding-punctuation-with-white-spaces-keeping-punctuation
  w = re.sub(r"([?.!,¿])", r"  ", w)
  w = re.sub(r'[" "]+', " ", w)

  # replacing everything with space except (a-z, A-Z, ".", "?", "!", ",")
  w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)

  w = w.strip()

  # adding a start and an end token to the sentence
  # so that the model know when to start and stop predicting.
  w = '<start> ' + w + ' <end>'
  return w
  

en_sentence = u"May I borrow this book?"
sp_sentence = u"¿Puedo tomar prestado este libro?"
print(preprocess_sentence(en_sentence))
print(preprocess_sentence(sp_sentence).encode('utf-8'))
'''
<start> may i borrow this book ? <end>
b'<start> \xc2\xbf puedo tomar prestado este libro ? <end>'
'''


# 1. Remove the accents
# 2. Clean the sentences
# 3. Return word pairs in the format: [ENGLISH, SPANISH]
def create_dataset(path, num_examples):
  lines = io.open(path, encoding='UTF-8').read().strip().split('\n')

  word_pairs = [[preprocess_sentence(w) for w in line.split('\t')]
                for line in lines[:num_examples]]

  return zip(*word_pairs)


en, sp = create_dataset(path_to_file, None)
print(en[-1])
print(sp[-1])
'''
<start> if you want to sound like a native speaker , you must be willing to practice saying the same sentence over and over in the same way that banjo players practice the same phrase over and over until they can play it correctly and at the desired tempo . <end>
<start> si quieres sonar como un hablante nativo , debes estar dispuesto a practicar diciendo la misma frase una y otra vez de la misma manera en que un musico de banjo practica el mismo fraseo una y otra vez hasta que lo puedan tocar correctamente y en el tiempo esperado . <end>
'''


def tokenize(lang):
  lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
  lang_tokenizer.fit_on_texts(lang)

  tensor = lang_tokenizer.texts_to_sequences(lang)

  tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor,
                                                         padding='post')

  return tensor, lang_tokenizer


def load_dataset(path, num_examples=None):
  # creating cleaned input, output pairs
  targ_lang, inp_lang = create_dataset(path, num_examples)

  input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
  target_tensor, targ_lang_tokenizer = tokenize(targ_lang)

  return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer


# Try experimenting with the size of that dataset
num_examples = 30000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)

# Calculate max_length of the target tensors
max_length_targ, max_length_inp = target_tensor.shape[1], input_tensor.shape[1]


# Creating training and validation sets using an 80-20 split
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)

# Show length
print(len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val))
'''
24000 24000 6000 6000
'''


# 구조와 관련된 파라미터 설정
BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 64
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256
units = 1024
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1

dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)


example_input_batch, example_target_batch = next(iter(dataset))
example_input_batch.shape, example_target_batch.shape
'''
(TensorShape([64, 16]), TensorShape([64, 11]))
'''


# 인코더 구현
class Encoder(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
    super(Encoder, self).__init__()
    self.batch_sz = batch_sz
    self.enc_units = enc_units
    self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
    self.gru = tf.keras.layers.GRU(self.enc_units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_initializer='glorot_uniform')

  def call(self, x, hidden):
    x = self.embedding(x)
    output, state = self.gru(x, initial_state=hidden)
    return output, state

  def initialize_hidden_state(self):
    return tf.zeros((self.batch_sz, self.enc_units))


encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)

# sample input
sample_hidden = encoder.initialize_hidden_state()
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
print('Encoder output shape: (batch size, sequence length, units)', sample_output.shape)
print('Encoder Hidden state shape: (batch size, units)', sample_hidden.shape)
'''
Encoder output shape: (batch size, sequence length, units) (64, 16, 1024)
Encoder Hidden state shape: (batch size, units) (64, 1024)
'''


class BahdanauAttention(tf.keras.layers.Layer):
  def __init__(self, units):
    super(BahdanauAttention, self).__init__()
    self.W1 = tf.keras.layers.Dense(units)
    self.W2 = tf.keras.layers.Dense(units)
    self.V = tf.keras.layers.Dense(1)

  def call(self, query, values):
    # query hidden state shape == (batch_size, hidden size)
    # query_with_time_axis shape == (batch_size, 1, hidden size)
    # values shape == (batch_size, max_len, hidden size)
    # we are doing this to broadcast addition along the time axis to calculate the score
    query_with_time_axis = tf.expand_dims(query, 1)

    # score shape == (batch_size, max_length, 1)
    # we get 1 at the last axis because we are applying score to self.V
    # the shape of the tensor before applying self.V is (batch_size, max_length, units)
    score = self.V(tf.nn.tanh(
        self.W1(query_with_time_axis) + self.W2(values)))

    # attention_weights shape == (batch_size, max_length, 1)
    attention_weights = tf.nn.softmax(score, axis=1)

    # context_vector shape after sum == (batch_size, hidden_size)
    context_vector = attention_weights * values
    context_vector = tf.reduce_sum(context_vector, axis=1)

    return context_vector, attention_weights


attention_layer = BahdanauAttention(10)
attention_result, attention_weights = attention_layer(sample_hidden, sample_output)

print("Attention result shape: (batch size, units)", attention_result.shape)
print("Attention weights shape: (batch_size, sequence_length, 1)", attention_weights.shape)
'''
Attention result shape: (batch size, units) (64, 1024)
Attention weights shape: (batch_size, sequence_length, 1) (64, 16, 1)
'''


# 디코더 구현
class Decoder(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
    super(Decoder, self).__init__()
    self.batch_sz = batch_sz
    self.dec_units = dec_units
    self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
    self.gru = tf.keras.layers.GRU(self.dec_units,
                                   return_sequences=True,
                                   return_state=True,
                                   recurrent_initializer='glorot_uniform')
    self.fc = tf.keras.layers.Dense(vocab_size)

    # used for attention
    self.attention = BahdanauAttention(self.dec_units)

  def call(self, x, hidden, enc_output):
    # enc_output shape == (batch_size, max_length, hidden_size)
    context_vector, attention_weights = self.attention(hidden, enc_output)

    # x shape after passing through embedding == (batch_size, 1, embedding_dim)
    x = self.embedding(x)

    # x shape after concatenation == (batch_size, 1, embedding_dim + hidden_size)
    x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

    # passing the concatenated vector to the GRU
    output, state = self.gru(x)

    # output shape == (batch_size * 1, hidden_size)
    output = tf.reshape(output, (-1, output.shape[2]))

    # output shape == (batch_size, vocab)
    x = self.fc(output)

    return x, state, attention_weights


decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)

sample_decoder_output, _, _ = decoder(tf.random.uniform((BATCH_SIZE, 1)),
                                      sample_hidden, sample_output)

print('Decoder output shape: (batch_size, vocab size)', sample_decoder_output.shape)
'''
Decoder output shape: (batch_size, vocab size) (64, 4935)
'''


optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True,
                                                            reduction='none')


def loss_function(real, pred):
  mask = tf.math.logical_not(tf.math.equal(real, 0))
  loss_ = loss_object(real, pred)

  mask = tf.cast(mask, dtype=loss_.dtype)
  loss_ *= mask

  return tf.reduce_mean(loss_)


@tf.function
def train_step(inp, targ, enc_hidden):
  loss = 0

  with tf.GradientTape() as tape:
    enc_output, enc_hidden = encoder(inp, enc_hidden)

    dec_hidden = enc_hidden

    dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)

    # Teacher forcing - feeding the target as the next input
    for t in range(1, targ.shape[1]):
      # passing enc_output to the decoder
      predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)

      loss += loss_function(targ[:, t], predictions)

      # using teacher forcing
      dec_input = tf.expand_dims(targ[:, t], 1)

  batch_loss = (loss / int(targ.shape[1]))

  variables = encoder.trainable_variables + decoder.trainable_variables

  gradients = tape.gradient(loss, variables)

  optimizer.apply_gradients(zip(gradients, variables))

  return batch_loss


EPOCHS = 10

for epoch in range(EPOCHS):
  start = time.time()

  enc_hidden = encoder.initialize_hidden_state()
  total_loss = 0

  for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
    batch_loss = train_step(inp, targ, enc_hidden)
    total_loss += batch_loss

    if batch % 100 == 0:
      print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
                                                   batch,
                                                   batch_loss.numpy()))
      
  print('Epoch {} Loss {:.4f}'.format(epoch + 1,
                                      total_loss / steps_per_epoch))
  print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
'''
Epoch 1 Batch 0 Loss 4.6026
Epoch 1 Batch 100 Loss 2.1524
Epoch 1 Batch 200 Loss 1.8952
Epoch 1 Batch 300 Loss 1.6539
Epoch 1 Loss 2.0383
Time taken for 1 epoch 1016.7628138065338 sec

Epoch 2 Batch 0 Loss 1.5593
Epoch 2 Batch 100 Loss 1.4487
Epoch 2 Batch 200 Loss 1.2338
Epoch 2 Batch 300 Loss 1.3027
Epoch 2 Loss 1.3807
Time taken for 1 epoch 992.9955353736877 sec

Epoch 3 Batch 0 Loss 1.0330
Epoch 3 Batch 100 Loss 1.0200
Epoch 3 Batch 200 Loss 0.8721
Epoch 3 Batch 300 Loss 0.9157
Epoch 3 Loss 0.9653
Time taken for 1 epoch 989.1491215229034 sec

Epoch 4 Batch 0 Loss 0.6319
Epoch 4 Batch 100 Loss 0.6260
Epoch 4 Batch 200 Loss 0.5699
Epoch 4 Batch 300 Loss 0.7347
Epoch 4 Loss 0.6478
Time taken for 1 epoch 986.2824234962463 sec

Epoch 5 Batch 0 Loss 0.4384
Epoch 5 Batch 100 Loss 0.3671
Epoch 5 Batch 200 Loss 0.4387
Epoch 5 Batch 300 Loss 0.4493
Epoch 5 Loss 0.4406
Time taken for 1 epoch 998.4929230213165 sec

Epoch 6 Batch 0 Loss 0.2667
Epoch 6 Batch 100 Loss 0.2406
Epoch 6 Batch 200 Loss 0.2759
Epoch 6 Batch 300 Loss 0.3238
Epoch 6 Loss 0.3082
Time taken for 1 epoch 976.1749527454376 sec

Epoch 7 Batch 0 Loss 0.2104
Epoch 7 Batch 100 Loss 0.2372
Epoch 7 Batch 200 Loss 0.2571
Epoch 7 Batch 300 Loss 0.2070
Epoch 7 Loss 0.2207
Time taken for 1 epoch 1002.5251026153564 sec

Epoch 8 Batch 0 Loss 0.1459
Epoch 8 Batch 100 Loss 0.1691
Epoch 8 Batch 200 Loss 0.2185
Epoch 8 Batch 300 Loss 0.1537
Epoch 8 Loss 0.1652
Time taken for 1 epoch 1005.3521428108215 sec

Epoch 9 Batch 0 Loss 0.1419
Epoch 9 Batch 100 Loss 0.1146
Epoch 9 Batch 200 Loss 0.0952
Epoch 9 Batch 300 Loss 0.1238
Epoch 9 Loss 0.1262
Time taken for 1 epoch 985.0161681175232 sec

Epoch 10 Batch 0 Loss 0.0667
Epoch 10 Batch 100 Loss 0.0909
Epoch 10 Batch 200 Loss 0.1094
Epoch 10 Batch 300 Loss 0.1214
Epoch 10 Loss 0.1024
Time taken for 1 epoch 987.7415053844452 sec
'''


def evaluate(sentence):
  attention_plot = np.zeros((max_length_targ, max_length_inp))

  sentence = preprocess_sentence(sentence)

  inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
  inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs],
                                                         maxlen=max_length_inp,
                                                         padding='post')
  inputs = tf.convert_to_tensor(inputs)

  result = ''

  hidden = [tf.zeros((1, units))]
  enc_out, enc_hidden = encoder(inputs, hidden)

  dec_hidden = enc_hidden
  dec_input = tf.expand_dims([targ_lang.word_index['<start>']], 0)

  for t in range(max_length_targ):
    predictions, dec_hidden, attention_weights = decoder(dec_input,
                                                         dec_hidden,
                                                         enc_out)

    # storing the attention weights to plot later on
    attention_weights = tf.reshape(attention_weights, (-1, ))
    attention_plot[t] = attention_weights.numpy()

    predicted_id = tf.argmax(predictions[0]).numpy()

    result += targ_lang.index_word[predicted_id] + ' '

    if targ_lang.index_word[predicted_id] == '<end>':
      return result, sentence, attention_plot

    # the predicted ID is fed back into the model
    dec_input = tf.expand_dims([predicted_id], 0)

  return result, sentence, attention_plot


# function for plotting the attention weights
def plot_attention(attention, sentence, predicted_sentence):
  fig = plt.figure(figsize=(10,10))
  ax = fig.add_subplot(1, 1, 1)
  ax.matshow(attention, cmap='viridis')

  fontdict = {'fontsize': 14}

  ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
  ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)

  ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
  ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

  plt.show()


def translate(sentence):
  result, sentence, attention_plot = evaluate(sentence)

  print('Input: %s' % (sentence))
  print('Predicted translation: {}'.format(result))

  attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
  plot_attention(attention_plot, sentence.split(' '), result.split(' '))


translate(u'hace mucho frio aqui.')
translate(u'esta es mi vida.')
'''
Input: <start> hace mucho frio aqui . <end>
Predicted translation: it s very cold here . <end> 

Input: <start> esta es mi vida . <end>
Predicted translation: this is my life . <end> 
'''

image

image

0%