Haru's 개발 블로그

[NLP] 한국어 자연어 처리 관점에서 RNN이란? 본문

머신러닝 & 딥러닝/NLP(자연어 처리)

[NLP] 한국어 자연어 처리 관점에서 RNN이란?

Haru_29 2023. 5. 15. 13:01

RNN(Recurrent Neural Network)이란?

RNN : 피드 포워드 신경망 같이 전부 은닉층에서 활성화 함수를 지난 값은 오직 출력층 방향으로만 향한 신경망 들은 피드 포워드 신경망(Feed Forward Neural Network)이라고 합니다. 그와는 다르게 RNN(Recurrent Neural Network)은 은닉층의 노드에서 활성화 함수를 통해 나온 결과값을 출력층 방향으로도 보내면서, 다시 은닉층 노드의 다음 계산의 입력으로 보내는 특징을 갖고있습니다.

위의 그림에서는 x는 입력층의 입력 벡터, y는 출력층의 출력 벡터입니다. 실제로는 편향 b도 입력으로 존재할 수 있지만 앞으로의 그림에서는 생략합니다. RNN에서 은닉층에서 활성화 함수를 통해 결과를 내보내는 역할을 하는 노드를 셀(cell)이라고 합니다. 이 셀은 이전의 값을 기억하려고 하는 일종의 메모리 역할을 수행하므로 이를 메모리 셀 또는 RNN 셀이라고 표현합니다. 

은닉층의 메모리 셀은 각각의 시점(time step)에서 바로 이전 시점에서의 은닉층의 메모리 셀에서 나온 값을 자신의 입력으로 사용하는 재귀적 활동을 하고 있습니다. 현재 시점을 변수 t로 표현을 한 뒤 t에서의 메모리 셀이 갖고있는 값은 과거의 메모리 셀들의 값에 영향을 받은 것임을 의미합니다. 메모리 셀이 출력층 방향 또는 다음 시점인 t+1의 자신에게 보내는 값을 은닉 상태(hidden state) 라고 합니다. 다시 말해 t 시점의 메모리 셀은 t-1 시점의 메모리 셀이 보낸 은닉 상태값을 t 시점의 은닉 상태 계산을 위한 입력값으로 사용합니다.

RNN을 표현할 때는 일반적으로 위의 그림에서 좌측과 같이 화살표로 사이클을 그려서 재귀 형태로 표현하기도 하지만, 우측과 같이 사이클을 그리는 화살표 대신 여러 시점으로 펼쳐서 표현하기도 합니다. 두 그림은 동일한 그림으로 단지 사이클을 그리는 화살표를 사용하여 표현하였느냐, 시점의 흐름에 따라서 표현하였느냐의 차이일 뿐 둘 다 동일한 RNN을 표현하고 있습니다. 

피드 포워드 신경망에서는 뉴런이라는 단위를 사용했지만, RNN에서는 뉴런이라는 단위보다는 입력층과 출력층에서는 각각 입력 벡터와 출력 벡터, 은닉층에서는 은닉 상태라는 표현을 주로 사용합니다. 위의 그림에서 회색과 초록색으로 표현한 각 네모들은 기본적으로 벡터 단위를 가정하고 있습니다. 피드 포워드 신경망과의 차이를 비교하기 위해서 RNN을 뉴런 단위로 시각화해보겠습니다.

 

위의 그림은 입력 벡터의 차원이 4, 은닉 상태의 크기가 2, 출력층의 출력 벡터의 차원이 2인 RNN이 시점이 2일 때의 모습을 보여줍니다. 다시 말해 뉴런 단위로 해석하면 입력층의 뉴런 수는 4, 은닉층의 뉴런 수는 2, 출력층의 뉴런 수는 2입니다.

1) 일 대 다(one-to-many) 구조의 모델 : 하나의 이미지 입력에 대해서 사진의 제목을 출력하는 이미지 캡셔닝(Image Captioning) 작업에 사용할 수 있습니다. 사진의 제목은 단어들의 나열이므로 시퀀스 출력입니다.

2) 다 대 일(many-to-one) 구조의 모델 : 입력 문서가 긍정적인지 부정적인지를 판별하는 감성 분류(sentiment classification), 또는 메일이 정상 메일인지 스팸 메일인지 판별하는 스팸 메일 분류(spam detection) 등에 사용할 수 있습니다.

3) 다 대 다(many-to-many) 구조의 모델 : 사용자가 문장을 입력하면 대답 문장을 출력하는 챗봇과 입력 문장으로부터 번역된 문장을 출력하는 번역기, 또는 '태깅 작업' 챕터에서 배우는 개체명 인식이나 품사 태깅과 같은 작업이 속합니다.

 

한국어 자연어 처리 관점에서의 RNN

  1. 기본 RNN (Vanilla RNN) : 각 시점의 입력과 이전 시점의 은닉 상태를 이용해 현재 시점의 출력을 예측하는 모델입니다. 하지만 긴 시퀀스를 처리할 때 기울기 소실(Vanishing Gradient)과 폭주(Exploding Gradient) 문제가 발생하여 장기 의존성(Long-term Dependency)을 학습하기 어렵습니다. 예를 들어 가장 중요한 정보가 시점의 앞 쪽에 위치할 수도 있습니다. RNN으로 만든 언어 모델이 다음 단어를 예측하는 과정을 생각해봅시다. 예를 들어 ''이번 여름 휴가에 일본에 있는 료칸에 갈 예정이야. 그런데 글쎄 직장 상사한테 전화가 왔어. 어디냐고 묻더라구 그래서 나는 말했지. 저 여행왔는데요. 여기 ___'' 다음 단어를 예측하기 위해서는 장소 정보가 필요합니다. 그런데 장소 정보에 해당되는 단어인 '일본'는 앞에 위치하고 있고, RNN이 충분한 기억력을 가지고 있지 못한다면 다음 단어를 엉뚱하게 예측합니다.아래에는 바닐라 RNN을 파이썬으로 구현한 예시입니다.
    1. Embedding을 사용하며, 단어 집합(Vocabulary)의 크기가 5,000이고 임베딩 벡터의 차원은 100입니다.
    2. 은닉층에서는 Simple RNN을 사용하며, 은닉 상태의 크기는 128입니다.
    3. 훈련에 사용하는 모든 샘플의 길이는 30으로 가정합니다.
    4. 이진 분류를 수행하는 모델로, 출력층의 뉴런은 1개로 시그모이드 함수를 사용합니다.
    5. 은닉층은 1개입니다.
from keras.models import Sequential
from keras.layers import SimpleRNN, Embedding, Dense

vocab_size = 5000
embedding_dim = 100
hidden_size = 128

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(SimpleRNN(hidden_size))
model.add(Dense(1, activation='sigmoid'))
model.summary()

 

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding (Embedding)       (None, None, 100)         500000    
                                                                 
 simple_rnn (SimpleRNN)      (None, 128)               29312     
                                                                 
 dense (Dense)               (None, 1)                 129       
                                                                 
=================================================================
Total params: 529,441
Trainable params: 529,441
Non-trainable params: 0
_________________________________________________________________

2. LSTM (Long Short-Term Memory) : RNN에서 발생하는 기울기 소실 문제를 해결하기 위해 고안된 모델입니다. 입력, 삭제, 출력 게이트와 셀 상태(cell state)를 이용해 장기 의존성을 학습할 수 있습니다.

위의 그림은 LSTM의 전체적인 내부의 모습을 보여줍니다. LSTM은 은닉층의 메모리 셀에 입력 게이트, 망각 게이트, 출력 게이트를 추가하여 불필요한 기억을 지우고, 기억해야할 것들을 정합니다. 요약하면 LSTM은 은닉 상태(hidden state)를 계산하는 식이 전통적인 RNN보다 조금 더 복잡해졌으며 셀 상태(cell state)라는 값을 추가하였습니다. 위의 그림에서는 t시점의 셀 상태를 표현하고 있습니다.

 

한국어 자연어 처리에서 LSTM은 주로 시퀀스 데이터를 처리하는데 사용됩니다. 예를 들어, 문장에서 단어들의 시퀀스를 입력으로 받아 각 단어의 품사나 의미를 판단하는 작업이나, 문서에서 문장들의 시퀀스를 입력으로 받아 주제 분류를 하는 작업 등이 있습니다.

LSTM은 한국어의 특성을 고려해 모델을 설계할 수 있습니다. 한국어는 교착어이기 때문에 각 형태소에 대한 정보를 따로 처리하는 것이 중요합니다. 이를 위해 LSTM에 형태소 분석 결과를 입력으로 넣어 각 형태소에 대한 정보를 학습하거나, 또는 자질(feature) 추출을 위한 다양한 방법을 적용할 수 있습니다. 또한, LSTM 모델을 양방향(Bidirectional)으로 구성해 입력 시퀀스를 앞에서부터 처리하는 Forward LSTM과 뒤에서부터 처리하는 Backward LSTM을 결합해 양방향으로 정보를 학습할 수도 있습니다.

아래의 예시는 위쪽 바닐라 RNN코드에서 모델을 LSTM으로 변경한 코드입니다.

from keras.models import Sequential
from keras.layers import LSTM, Embedding, Dense

vocab_size = 5000
embedding_dim = 100
hidden_size = 128

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(LSTM(hidden_size))
model.add(Dense(1, activation='sigmoid'))
model.summary()
Model: "sequential_3"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_3 (Embedding)     (None, None, 100)         500000    
                                                                 
 lstm (LSTM)                 (None, 128)               117248    
                                                                 
 dense_1 (Dense)             (None, 1)                 129       
                                                                 
=================================================================
Total params: 617,377
Trainable params: 617,377
Non-trainable params: 0
_________________________________________________________________

 

 

3. GRU (Gated Recurrent Unit) : LSTM보다 간단한 구조를 가지면서도 유사한 성능을 보이는 모델입니다. LSTM에서 사용하는 입력, 삭제, 출력 게이트를 하나의 게이트로 통합한 것이 특징입니다.

GRU와 LSTM 중 어떤 것이 모델의 성능면에서 더 낫다라고 단정지어 말할 수 없으며, 기존에 LSTM을 사용하면서 최적의 하이퍼파라미터를 찾아낸 상황이라면 굳이 GRU로 바꿔서 사용할 필요는 없습니다. 

경험적으로 데이터 양이 적을 때는 매개 변수의 양이 적은 GRU가 조금 더 낫고, 데이터 양이 더 많으면 LSTM이 더 낫다고도 합니다. GRU보다 LSTM에 대한 연구나 사용량이 더 많은데, 이는 LSTM이 더 먼저 나온 구조이기 때문입니다. 

아래는 모델을 GRU로 변경하여 코드를 작성하였습니다.

from keras.models import Sequential
from keras.layers import GRU, Embedding, Dense

vocab_size = 5000
embedding_dim = 100
hidden_size = 128

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(GRU(hidden_size))
model.add(Dense(1, activation='sigmoid'))
model.summary()
Model: "sequential_4"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_4 (Embedding)     (None, None, 100)         500000    
                                                                 
 gru (GRU)                   (None, 128)               88320     
                                                                 
 dense_2 (Dense)             (None, 1)                 129       
                                                                 
=================================================================
Total params: 588,449
Trainable params: 588,449
Non-trainable params: 0
_________________________________________________________________

4. Bi-directional RNN : 입력 시퀀스를 앞에서부터 처리하는 Forward RNN과 뒤에서부터 처리하는 Backward RNN을 결합하여 양방향으로 정보를 학습하는 모델입니다. 양방향 순환 신경망은 시점 t에서의 출력값을 예측할 때 이전 시점의 입력뿐만 아니라, 이후 시점의 입력 또한 예측에 기여할 수 있다는 아이디어에 기반합니다. 빈칸 채우기 문제에 비유하여 보겠습니다.

운동을 열심히 하는 것은 [        ]을 늘리는데 효과적이다.

1) 근육
2) 지방
3) 스트레스

'운동을 열심히 하는 것은 [ ]을 늘리는데 효과적이다.' 라는 문장에서 문맥 상으로 정답은 '근육'입니다. 위의 빈 칸 채우기 문제를 풀 때 이전에 나온 단어들만으로 빈 칸을 채우려고 시도해보면 정보가 부족합니다. '운동을 열심히 하는 것은' 까지만 주고 뒤의 단어들은 가린 채 빈 칸의 정답이 될 수 있는 세 개의 선택지 중 고르는 것은 뒤의 단어들까지 알고있는 상태보다 명백히 정답을 결정하기가 어렵습니다.

RNN이 풀고자 하는 문제 중에서는 과거 시점의 입력 뿐만 아니라 미래 시점의 입력에 힌트가 있는 경우도 많습니다. 그래서 이전과 이후의 시점 모두를 고려해서 현재 시점의 예측을 더욱 정확하게 할 수 있도록 고안된 것이 양방향 RNN입니다.

양방향 RNN은 하나의 출력값을 예측하기 위해 기본적으로 두 개의 메모리 셀을 사용합니다. 첫번째 메모리 셀은 앞 시점의 은닉 상태(Forward States) 를 전달받아 현재의 은닉 상태를 계산합니다. 위의 그림에서는 주황색 메모리 셀에 해당됩니다. 두번째 메모리 셀은 앞에서 배운 것과는 다릅니다. 앞 시점의 은닉 상태가 아니라 뒤 시점의 은닉 상태(Backward States) 를 전달 받아 현재의 은닉 상태를 계산합니다. 입력 시퀀스를 반대 방향으로 읽는 것입니다. 위의 그림에서는 초록색 메모리 셀에 해당됩니다. 그리고 이 두 개의 값 모두가 현재 시점의 출력층에서 출력값을 예측하기 위해 사용됩니다.

아래의 예시는 GRU 레이어를 Bidirectional 래퍼로 감싸서 양방향 RNN 모델을 구성하였습니다.

from keras.models import Sequential
from keras.layers import Bidirectional, GRU, Embedding, Dense

vocab_size = 5000
embedding_dim = 100
hidden_size = 128

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(Bidirectional(GRU(hidden_size)))
model.add(Dense(1, activation='sigmoid'))
model.summary()
Model: "sequential_8"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_7 (Embedding)     (None, None, 100)         500000    
                                                                 
 bidirectional_1 (Bidirectio  (None, 256)              176640    
 nal)                                                            
                                                                 
 dense_4 (Dense)             (None, 1)                 257       
                                                                 
=================================================================
Total params: 676,897
Trainable params: 676,897
Non-trainable params: 0
_________________________________________________________________

 

5. Attention Mechanism : 딥러닝 모델에서 입력 시퀀스의 각 위치에 대한 가중치를 동적으로 조정하여 출력값을 계산하는 방법입니다. 특히, 이를 이용하여 입력 시퀀스에서 중요한 정보에 더 집중하고, 덜 중요한 정보는 덜 고려하는 것이 가능합니다.

한국어 자연어 처리에서는 Attention Mechanism이 다양한 곳에서 활용됩니다. 예를 들어, 번역, 감성 분석, 기계 독해 등에서 사용됩니다. 번역에서는 입력 문장과 출력 문장 간의 상관 관계를 파악하여 번역의 품질을 높이는 데 활용됩니다. 감성 분석에서는 입력 문장에서 중요한 단어나 구문을 찾아서 해당 문장의 감성을 분석하는 데 활용됩니다. 기계 독해에서는 주어진 문장에서 질문과 관련 있는 정보를 찾아내는 데 활용됩니다.

Attention Mechanism은 여러 가지 방식으로 구현할 수 있으며, 대표적인 방식으로는 Bahdanau Attention, Luong Attention 등이 있습니다. 이들 방식은 각각 입력 시퀀스와 출력 시퀀스 간의 상관 관계를 계산하는 방법이 다르며, 적용할 모델의 구조와 데이터 특성에 따라 적절한 방식을 선택하여 사용할 수 있습니다.

아래의 예시는 Attention Mechanism을 구현하기 위해서 아래 사항이 변경이 되었습니다.

  • 모델을 Model 클래스로 변경하고, 입력과 출력을 Input과 Output 클래스로 정의
  • 양방향 GRU 레이어를 추가 (Bidirectional 클래스 사용)
  • TimeDistributed 레이어를 추가하여 각 시점의 어텐션 가중치 계산
  • Dot 레이어를 추가하여 가중평균된 문맥 벡터 계산
from keras.models import Model
from keras.layers import Input, Embedding, Dense, GRU, Bidirectional, Concatenate, TimeDistributed, Dot, Activation, Lambda

vocab_size = 5000
embedding_dim = 100
hidden_size = 128

# 입력 시퀀스
input_sequence = Input(shape=(None,))

# 입력 시퀀스를 임베딩
embedding_layer = Embedding(vocab_size, embedding_dim)(input_sequence)

# 양방향 GRU
encoder = Bidirectional(GRU(hidden_size, return_sequences=True))(embedding_layer)

# Attention Mechanism 계산
attention = TimeDistributed(Dense(1, activation='tanh'))(encoder)
attention = Activation('softmax')(attention)
context = Dot(axes=1)([attention, encoder])

# 출력층
output = Dense(1, activation='sigmoid')(context)

# 모델 생성
model = Model(inputs=[input_sequence], outputs=[output])
model.summary()

 

Model: "model"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_1 (InputLayer)           [(None, None)]       0           []                               
                                                                                                  
 embedding_8 (Embedding)        (None, None, 100)    500000      ['input_1[0][0]']                
                                                                                                  
 bidirectional_2 (Bidirectional  (None, None, 256)   176640      ['embedding_8[0][0]']            
 )                                                                                                
                                                                                                  
 time_distributed (TimeDistribu  (None, None, 1)     257         ['bidirectional_2[0][0]']        
 ted)                                                                                             
                                                                                                  
 activation (Activation)        (None, None, 1)      0           ['time_distributed[0][0]']       
                                                                                                  
 dot (Dot)                      (None, 1, 256)       0           ['activation[0][0]',             
                                                                  'bidirectional_2[0][0]']        
                                                                                                  
 dense_6 (Dense)                (None, 1, 1)         257         ['dot[0][0]']                    
                                                                                                  
==================================================================================================
Total params: 677,154
Trainable params: 677,154
Non-trainable params: 0
__________________________________________________________________________________________________

 

6. Transformer : Transformer는 주로 자연어 처리 태스크에서 좋은 성능을 보이며, 기존의 RNN 계열 모델의 단점인 계산 병목과 학습 시간 증가 문제를 해결하기 위해 제안된 모델입니다.

Transformer는 Self-Attention 메커니즘을 사용하여 입력 시퀀스의 전체 정보를 동시에 고려하면서 출력을 계산하는 모델입니다. 이를 위해 입력 시퀀스의 모든 단어들이 서로 다른 가중치로 연관성을 학습하며, 이를 통해 모델은 단어들 간의 관계를 파악할 수 있습니다.

Transformer 모델은 크게 인코더와 디코더로 구성됩니다. 인코더는 입력 시퀀스를 받아 Self-Attention을 통해 입력 시퀀스를 인코딩한 후 디코더에 전달합니다. 디코더는 인코더가 출력한 정보와 이전 시점에서의 출력을 입력으로 받아 다음 출력을 예측합니다.

Transformer는 이러한 구조로 인해 RNN 계열 모델들과 달리 입력 시퀀스의 길이와는 무관하게 고정된 시간복잡도를 가지며, 병렬적으로 처리가 가능해 학습 시간이 더욱 단축됩니다. 이로 인해 자연어 처리 분야에서는 기존의 RNN 계열 모델들 대신 Transformer 모델을 사용하는 추세입니다.

아래의 코드는 Transformer로 코드를 구현하였습니다.

  • MultiHeadAttention는 Self-Attention 메커니즘을 구현하는 데 사용
  • LayerNormalization는 각 레이어의 입력값을 정규화하는 데 사용
  • concatenate 함수는 입력값을 연결하는 데 사용됨
  • num_heads, hidden_size, dropout_rate 등의 하이퍼파라미터는 사용자가 임의로 지정 가능
from keras.models import Model
from keras.layers import Input, Embedding, Dense, Dropout, LayerNormalization
from keras.layers import MultiHeadAttention, TimeDistributed, Concatenate, Flatten

vocab_size = 5000
embedding_dim = 100
hidden_size = 128
num_heads = 8
dropout_rate = 0.1
max_sequence_length = 100

# 입력 시퀀스
inputs = Input(shape=(max_sequence_length,))

# 입력 시퀀스의 각 단어를 임베딩 벡터로 변환
embed = Embedding(vocab_size, embedding_dim)(inputs)

# Multi-Head Attention 레이어
attention = MultiHeadAttention(num_heads=num_heads, key_dim=hidden_size)(embed, embed)

# Dropout 레이어
dropout1 = Dropout(dropout_rate)(attention)

# Layer Normalization 레이어
normalize1 = LayerNormalization(epsilon=1e-6)(dropout1)

# Concatenate 레이어
concatenate1 = Concatenate()([embed, normalize1])

# Feed-Forward 레이어
feed_forward = TimeDistributed(Dense(hidden_size*4, activation='relu'))(concatenate1)
feed_forward = TimeDistributed(Dropout(dropout_rate))(feed_forward)
feed_forward = TimeDistributed(Dense(hidden_size))(feed_forward)

# Dropout 레이어
dropout2 = Dropout(dropout_rate)(feed_forward)

# Layer Normalization 레이어
normalize2 = LayerNormalization(epsilon=1e-6)(dropout2)

# Dense 레이어
outputs = TimeDistributed(Dense(1, activation='sigmoid'))(normalize2)

# 모델 생성
model = Model(inputs=inputs, outputs=outputs)

# 모델 요약
model.summary()

 

Model: "model_1"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_3 (InputLayer)           [(None, 100)]        0           []                               
                                                                                                  
 embedding_10 (Embedding)       (None, 100, 100)     500000      ['input_3[0][0]']                
                                                                                                  
 multi_head_attention_1 (MultiH  (None, 100, 100)    412772      ['embedding_10[0][0]',           
 eadAttention)                                                    'embedding_10[0][0]']           
                                                                                                  
 dropout (Dropout)              (None, 100, 100)     0           ['multi_head_attention_1[0][0]'] 
                                                                                                  
 layer_normalization (LayerNorm  (None, 100, 100)    200         ['dropout[0][0]']                
 alization)                                                                                       
                                                                                                  
 concatenate (Concatenate)      (None, 100, 200)     0           ['embedding_10[0][0]',           
                                                                  'layer_normalization[0][0]']    
                                                                                                  
 time_distributed_1 (TimeDistri  (None, 100, 512)    102912      ['concatenate[0][0]']            
 buted)                                                                                           
                                                                                                  
 time_distributed_2 (TimeDistri  (None, 100, 512)    0           ['time_distributed_1[0][0]']     
 buted)                                                                                           
                                                                                                  
 time_distributed_3 (TimeDistri  (None, 100, 128)    65664       ['time_distributed_2[0][0]']     
 buted)                                                                                           
                                                                                                  
 dropout_2 (Dropout)            (None, 100, 128)     0           ['time_distributed_3[0][0]']     
                                                                                                  
 layer_normalization_1 (LayerNo  (None, 100, 128)    256         ['dropout_2[0][0]']              
 rmalization)                                                                                     
                                                                                                  
 time_distributed_4 (TimeDistri  (None, 100, 1)      129         ['layer_normalization_1[0][0]']  
 buted)                                                                                           
                                                                                                  
==================================================================================================
Total params: 1,081,933
Trainable params: 1,081,933
Non-trainable params: 0
__________________________________________________________________________________________________

출처 : https://wikidocs.net/45101

Comments