본문 바로가기
Natural Language Processing

[Goorm] 딥러닝을 이용한 자연어 처리 7 ( Seq2Seq 와 인코더/디코더)

by 자몽먹은토끼 2024. 7. 9.
728x90
반응형

해보기 실습

  •  1 ~ 10 사이의 숫자 5개를 입력하면 거꾸로 출력되는 모델 만들기
  • 검증은 계속 반복하며 Quit를 입력하면 종료
  • Ex)  8 4 2 3 7 > 7 3 2 4 8

 

RNN에서 Seq2Seq로 - Step 1 (RNN을 사용한 모델 설계)

 

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense, TimeDistributed, Bidirectional

x= np.random.randint(0, 10, size= (1000,5))
y= np.array([i[::-1] for i in x])

model= Sequential()
model.add(Embedding(10, 100, input_length=5))
model.add(Bidirectional(LSTM(64, return_sequences= True)))  # 시퀀스 전체를 예측
model.add(Dense(10, activation='softmax'))

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

model.fit(x, y, epochs=10, verbose=1, validation_split=0.2)

> loss : 1.040, acc: 0.553

pred_x= np.random.randint(1, 10, size= (10,5))
pred_y= model.predict(pred_x)
pred_y= np.argmax(pred_y, axis= 2)

count= 0
real_y= np.array([i[::-1] for i in pred_x])
for i in range(10):
    print(pred_x[i])
    print('예측', pred_y[i])
    print('정답', real_y[i])
    if (real_y[i] == pred_y[i]).all():
        count+= 1
        print('정답')
    else:
        print('오답')
print(count/10 *100 ,'% 의 정답률')
1/1 [==============================] - 1s 1s/step
[3 2 6 8 6]
예측 [6 6 6 3 3]
정답 [6 8 6 2 3]
오답
[5 3 7 3 9]
예측 [3 3 3 3 3]
정답 [9 3 7 3 5]
오답
[9 8 4 6 9]
예측 [9 9 9 9 9]
정답 [9 6 4 8 9]
오답
[5 6 6 1 8]
예측 [8 6 6 6 6]
정답 [8 1 6 6 5]
오답
[4 2 8 2 4]
예측 [4 4 4 4 4]
정답 [4 2 8 2 4]
오답
[6 2 1 2 5]
예측 [5 2 2 2 6]
정답 [5 2 1 2 6]
오답
[2 1 6 2 9]
예측 [9 9 2 2 2]
정답 [9 2 6 1 2]
오답
[6 5 4 3 2]
예측 [2 2 6 6 6]
정답 [2 3 4 5 6]
오답
[4 9 4 6 9]
예측 [9 9 4 4 4]
정답 [9 6 4 9 4]
오답
[1 1 9 1 4]
예측 [1 1 1 1 1]
정답 [4 1 9 1 1]
오답
0.0 % 의 정답률

> 일부만 맞추고 5개의 숫자를 모두 맞춘 경우는 드물었다.

> 알게 된 점 ❗
위 코드는 토크나이저를 사용하여 토큰화 한게 아니기 때문에, 리스트형식이 아닌 np.array형식으로 맞춰준 데이터를 모델 레이어에 입력해야 한다.

 

 

 

 

 

RNN에서 Seq2Seq로 - Step 2 (RNN → Seq2Seq)
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense, TimeDistributed, Dropout, RepeatVector
from tensorflow.keras.callbacks import EarlyStopping

x= np.random.randint(0, 10, size= (20000,5))
y= np.array([i[::-1] for i in x])

# 데이터 형태 변환 - LSTM을 사용하기 위해 3D텐서로 변환
x = x.reshape(x.shape[0], x.shape[1], 1)
y = y.reshape(y.shape[0], y.shape[1], 1)

model= Sequential()
model.add(LSTM(128, input_shape= (5,1)))
model.add(Dropout(0.2))
model.add(RepeatVector(5))
model.add(LSTM(128, return_sequences= True))
model.add(Dropout(0.2))
model.add(TimeDistributed(Dense(10, activation='softmax')))

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

early_stopping= EarlyStopping(monitor='val_loss', patience=5, restore_best_weights= True) # 학습 마지막 가중치가 아닌 가장 좋은 가중치를 저장

model.fit(x, y, epochs=10, validation_split= 0.2, callbacks= [early_stopping], verbose=1)

> loss : 0.079, acc : 0.993

 

  • 데이터 형태 변환 : LSTM의 입력으로 3차원의 텐서 사용 = ( batch_size, sequence_length, vector_dim)→ 위 코드는 Embedding레이어가 없으므로 LSTM의 입력 shape으로 미리 맞춰주어야 한다.
  • LSTM( )
    • input_shape= (5,1)은 (x.shape[1], 1)과 동일
    • 128차원으로 출력
  • RepeatVector ( ) : 입력 데이터를 여러 번 반복하여 출력 시퀀스를 만든다. 아래에서 이야기 할 디코더의 입력 형태에 맞추어 인코더 출력을 복제한다. 
    • n : 입력벡터를 n번 반복하여 새로운 시퀀스 생성

> 위 코드 상으로 이 레이어의 출력은 (5,128) 이 되고, 다시 LSTM의 입력으로 들어간다.

 

  • TimeDistributed( ) : 각 타임스텝에 대해 주어진 레이어를 독립적으로 적용한다. (주로 Dense 레이어)
    디코더의 출력 시퀀스를 각 타임스텝마다 Dense레이어에 전달하여 개별 예측값을 생성한다.

> TimeDistributed의 입력으로 (5,128)이 들어가고, 출력으로는 (5, 10)으로 5는 시퀀스의 길이, 10은 예측 클래스의 개수를 의미한다. 

의도치 않게 encoder와 decoder 사용으로 인해 seq2seq 형태를 갖는 모델이 됨

> 인코더 : 입력 시퀀스를 고정길이의 벡터로 변환

> RepeatVector : 고정길이의 백터를 다코더의 타임스텝 수만큼 반복

> 디코더 : 반복되는 고정 길이의 벡터를 입력으로 받아 시퀀스를 생성

> TimeDistributed : 디코더의 각 타임스텝 출력을 Dense레이어에 전달하여 예측값을 생성

 

pred_x= np.random.randint(1, 10, size= (10,5))
pred_y= model.predict(pred_x)
pred_y= np.argmax(pred_y, axis= 2)

count= 0
real_y= np.array([i[::-1] for i in pred_x])
for i in range(10):
    print(pred_x[i])
    print('예측', pred_y[i])
    print('정답', real_y[i])
    if (real_y[i] == pred_y[i]).all():
        count+= 1
        print('정답')
    else:
        print('오답')
print(count/10 *100 ,'% 의 정답률')
1/1 [==============================] - 0s 102ms/step
[5 4 5 3 3]
예측 [3 3 5 4 5]
정답 [3 3 5 4 5]
정답
[3 1 4 9 6]
예측 [6 9 4 1 3]
정답 [6 9 4 1 3]
정답
[4 9 2 7 6]
예측 [6 7 2 9 4]
정답 [6 7 2 9 4]
정답
[3 7 3 5 8]
예측 [8 5 3 7 3]
정답 [8 5 3 7 3]
정답
[2 8 5 7 1]
예측 [1 7 5 8 2]
정답 [1 7 5 8 2]
정답
[9 7 1 2 6]
예측 [6 2 1 7 9]
정답 [6 2 1 7 9]
정답
[8 6 7 4 3]
예측 [3 4 7 6 8]
정답 [3 4 7 6 8]
정답
[3 8 1 2 3]
예측 [3 2 1 8 3]
정답 [3 2 1 8 3]
정답
[8 3 2 4 9]
예측 [9 4 2 3 8]
정답 [9 4 2 3 8]
정답
[5 7 8 2 7]
예측 [7 2 8 7 5]
정답 [7 2 8 7 5]
정답
100.0 % 의 정답률

 

 

> 이 실습의 경우, 5개의 숫자를 모두 입력 받아야만 출력을 내놓을 수 있다.

 

 

일반 RNN모델의 경우, 하나의 입력이 들어올 때 마다 그때그때 출력을 처리했지만

Seq2Seq모델의 경우, 하나의 시퀀스 입력이 끝나야지만 출력으로 시퀀스가 처리된다.

 

이론적으로 생각했을 때에도, 우리가 한 실습의 경우에는 RNN모델보다 Seq2Seq모델이 더 적합하지 않겠는가?

 

(사실 RNN모델 사용시에도 epoch를 100으로 설정한다면 98%에 해당하는 높은 정확도를 확인할 수 있지만,

 Seq2Seq 모델 사용시에는 epoch 5-6 부터 99%가 넘는 정확도를 가진다는 것을 확인할 수 있었다.

그러니 두가지 모두 동일하게 epoch 10설정시에 정확도 측면에서 차이가 많이 나는 거겠지)

 

 

 

 

 

 

 


RNN에서 Seq2Seq로 - Step 3 (Seq2Seq → 인코더/디코더 구분 구현)

 

> 간단히 생각하면 seq2seq모델을 두 번에 나누어 처리하는 모델(인코더와 디코더)로 구현

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense, TimeDistributed, Dropout, RepeatVector
from tensorflow.keras.callbacks import EarlyStopping

x= np.random.randint(0, 10, size= (20000,5))
y= np.array([i[::-1] for i in x])

# 데이터 형태 변환 - LSTM을 사용하기 위해 3D텐서로 변환
x = x.reshape(x.shape[0], x.shape[1], 1)
y = y.reshape(y.shape[0], y.shape[1], 1)

> 여기까지 동일

 

from tensorflow.keras.layers import Input

# 인코더 정의 (LSTM)
encoder_inputs= Input(shape=(5,1))
encoder= LSTM(128, return_state= True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
encoder_states= [state_h, state_c]

# 디코더 정의 (LSTM, Dense)
decoder_inputs= Input(shape= (5,1))
decoder_lstm= LSTM(128, return_sequences= True, return_state= True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state= encoder_states) # 인코더 출력값 전달

decoder_dense= Dense(10, activation= 'softmax')
decoder_outputs= decoder_dense(decoder_outputs)
  • 인코더 (및 디코더)
    • Input() : 사용하는 모델의 입력텐서
    • LSTM()
      • return_sequences : 모든 출력층을 반환할 것인지(20000, 128), 마지막 층만 반환할 것인지(1, 128)
      • return_state : 출력과 더불어 마지막 상태(셀상태) 까지 반환할 것인지
                              (hidden_state, last_hidden_state, last_cell_state), 아닌지 (hidden_state)
      • intial_state : 인코더의 출력값을 디코더의 입력과 함께 전달

>> 출력층을 반환한다는 것은 hidden_state를 말하는 것이고,
>> 만약 마지막층만 반환하되(return_sequences= False) 마지막 상태까지 반환한다면(return_state= True), 반환받는 세개의 값 중 앞에 두개의 값은 동일하게 된다.

 

(자세한 이론은 아래에)

 

 

from tensorflow.keras.models import Model

model= Model([encoder_inputs, decoder_inputs], decoder_outputs)

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

# 입력데이터와 타겟데이터를 동일하게 맞춤
decoder_input_data= np.zeros_like(x)

early_stopping= EarlyStopping(monitor='val_loss', patience=5, restore_best_weights= True)
model.fit([x, decoder_input_data], y, epochs=10, validation_split= 0.2, callbacks=[early_stopping])

> loss: 0.003, acc : 1.0

 

  • decoder_input_data는 디코더의 입력으로 들어가는 데이터를 초기화한 값이다.
  • model.fit에서 입력으로 [x, decoder_input_data]를, 출력으로는 y를 명시하면서 학습을 수행한다.
  • 따라서 fit의 실행에 따라 Model함수가 호출되고, Model함수의 인자로 지정된 것처럼
    fit의 x가 Model 함수내의 encoder_inputs으로 들어가게 되고
    fit의 decoder_input_data는 Model함수내의 decoder_inputs으로 들어가게 된다.
  • 그리고 decoder_outputs이 출력으로 사용되면서, 앞서 정의했던 인코더와 디코더가 순차적으로 이루어지게 된다.
  • 최종적으로 실제 출력 값인 y와 decoder_outputs이 비교되어 손실(loss)을 계산한다.

 

 

pred_x= np.random.randint(1, 10, size= (10,5)).reshape((10,5,1))
decoder_input_data_pred= np.zeros_like(pred_x)
pred_y= model.predict([pred_x, decoder_input_data_pred])
pred_y= np.argmax(pred_y, axis= 2)

count= 0
real_y= np.array([i[::-1] for i in pred_x])
for i in range(10):
    print(pred_x[i].reshape((5)))
    print('예측', pred_y[i])
    print('정답', real_y[i].reshape((5)))
    if (real_y[i].reshape((5)) == pred_y[i]).all():
        count+= 1
        print('정답')
    else:
        print('오답')
print(count/10 *100 ,'% 의 정답률')
1/1 [==============================] - 0s 98ms/step
[3 6 8 7 3]
예측 [3 7 8 6 3]
정답 [3 7 8 6 3]
정답
[5 5 9 3 7]
예측 [7 3 9 5 5]
정답 [7 3 9 5 5]
정답
[5 2 4 4 1]
예측 [1 4 4 2 5]
정답 [1 4 4 2 5]
정답
[1 9 1 6 9]
예측 [9 6 1 9 1]
정답 [9 6 1 9 1]
정답
[4 2 5 8 5]
예측 [5 8 5 2 4]
정답 [5 8 5 2 4]
정답
[6 2 3 9 6]
예측 [6 9 3 2 6]
정답 [6 9 3 2 6]
정답
[7 4 4 3 1]
예측 [1 3 4 4 7]
정답 [1 3 4 4 7]
정답
[3 6 9 2 2]
예측 [2 2 9 6 3]
정답 [2 2 9 6 3]
정답
[7 9 2 9 9]
예측 [9 9 2 9 7]
정답 [9 9 2 9 7]
정답
[6 5 7 2 3]
예측 [3 2 7 5 6]
정답 [3 2 7 5 6]
정답
100.0 % 의 정답률

>> 10번의 학습 epoch에서 2번째 epoch만에 val_acc가 0.99 까지 올라갔고, 5번째 epoch에서는 val_acc가 1.0을 찍었다.

>> 앞선 코드들에 비해 엄청나게 빠르고 높은 정확도를 얻을 수 있었다.

 

 

Encoder (인코더)

입력 시퀀스를 고정된 길이의 벡터(컨텍스트 벡터)로 변환하고, 이 벡터의 은닉상태와 셀상태를 디코더로 전달

Hidden State (은닉상태)

  • 각 타임스텝의 출력
  • 단기 정보를 기억한다
  • 이전 스텝의 정보를 포함하지 않는다

Cell State (셀 상태)

  • 장기 정보를 기억한다
  • 이전 타임 스텝의 정보를 포함한다

Decoder (디코더)

인코더의 상태(은닉/셀)를 초기상태로 사용하여 새로운 시퀀스를 생성하고, 각 타임스텝마다 클래스 확률을 예측

 

 

 

 

그러나, 길이가 길어짐에 따라 정확도의 차이가 난다는 것을 확인할 수 있다.

가장 정확도가 높았던 마지막 모델에서 5길이의 데이터를 50으로 늘려서 모든 조건 동일하게 학습시켜 보았다.

 

> loss : 1.329, acc : 0.505

결과는 거의 절반 정도 가까이 정확도가 떨어진 것을 볼 수 있었다. 길이가 길어짐에 따라 정확도가 감소한다는 것을 알 수 있었다.

728x90
반응형