본문 바로가기
AI/DeepLearning_WIL

[DL/WIL] 3 텍스트 데이터를 위한 인공 신경망

by SolaKim 2024. 11. 23.

입력 데이터의 흐름이 앞으로만 전달되는 신경망을 피드포워드 신경망(feedforward neural network)라고 한다.
Fully connected neural networkConvolution neural network 는 기억 장치가 없어서, 하나의 샘플(또는 하나의 배치)를 사용하여 정방향 계산을 수행하고 나면 그 샘플은 버려지고 다음 샘플을 처리할 때 재사용하지 않는다.

 

신경망이 이전에 처리했던 샘플을 다음 샘플을 처리하는데 재사용하기 위해서는 이렇게 데이터 흐름이 앞으로만 전달되어서는 곤란하다.
다음 샘플을 위해서 이전 데이터가 신경망 층에 순환될 필요가 있다.

 

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

순환 신경망에서는 특별히 층을 cell 이라고 부른다. 한 셀에는 여러 개의 뉴런이 있지만 완전 연결 신경망과 달리 뉴런을 모두 표시하지 않고 하나의 셀로 층을 표현한다. 또 셀의 출력을 은닉 상태(Hidden state) 라고 부른다.

순환 신경망의 기본 구조는 입력에 어떤 가중치를 곱하고 활성화 함수를 통과시켜 다음 층으로 보내는 것이다. 이때 층의 출력(즉 은닉 상태)를 다음 타임 스텝에 재사용한다.

일반적으로 순환 신경망에서는 활성화 함수로 tanh 함수를 많이 사용한다.

tanh 함수는 시그모이드 함수와는 달리 -1 ~ 1 사이의 범위를 가진다.

하지만, 사실 순환층은 기본적으로 마지막 타임스텝의 은닉 상태만 출력으로 내보낸다. 
셀의 입력은 샘플마다 타임스텝(ex 단어 개수)과 단어표현으로 이루어진 2차원 배열이어야 한다. 따라서 첫 번째 셀이 마지막 타임스텝의 은닉 상태만을 출력해서는 안된다. 이런 경우에는 마지막 셀을 제외다른 모든 셀은 모든 타임스텝의 은닉상태를 출력한다. 

그렇기에 마지막 셀의 출력이 1차원이어서 밀집층으로 보낼때 Flatten 클래스로 펼칠 필요가 없다. 

 

 

자 이제, 텍스트 데이터를 입력으로 받는 순환 신경망을 만들어보자.

사실 텍스트 자체를 신경망에 전달하지는 않는다. 컴퓨터에서 처리하는 모든 것은 어떤 숫자 데이터이다. 앞서 합성곱 신경망에서 이미지를 다룰 때는 특별한 변환을 하지 않았다. 이는 이미지가 정수 픽셀값으로 이루어져 있기 때문이다.
텍스트 데이터의 경우 단어를 숫자 데이터로 바꾸는 일반적인 방법은 데이터에 등장하는 단어마다 고유한 정수를 부여하는 것이다.

일반적으로 영어 문장은 모두 소문자로 바꾸고 구둣점을 삭제한 다음 공백을 기준으로 분리한다. 이렇게 분리된 단어를 토큰(Token) 이라고 부른다. 하나의 샘플은 여러 개의 토큰으로 이루어져 있고 1개의 토큰이 하나의 타임스텝에 해당한다.

토큰에 할당하는 정수 중에 몇 개는 특정한 용도로 예약되어 있는 경우가 많다. 예를 들어 0은 패딩, 1은 문장의 시작, 2는 어휘 사전에 없는 토큰을 나타낸다.

순환 신경망을 만들때 토큰을 정수로 변환한 데이터를 input으로 넣게 되면 문제점이 하나 생긴다. 
큰 정수일 수록 큰 활성화 출력을 만든다. 사실 단어에 임의로 고유한 정수를 부여할 뿐이지, 20을 10보다 더 중요시해야 할 이유는 없다.

이 문제를 해결하기 위해서 정수값에 있는 크기 속성을 없애고 각 정수를 고유하게 표현할 수 있도록 원-핫 인코딩(One-hot Encoding)을 도입할 수 있다.

원-핫 인코딩은 어떤 클래스에 해당하는 원소만 1이고 나머지는 모두 0인 벡터이다. 변환된 토큰을 원-핫 인코딩으로 변환하려면 어휘 사전 크기의 벡터가 만들어진다. 

이렇게 원-핫 인코딩을 적용하면 만약 총 500개의 단어만 사용하도록 하는 데이터를 불러오고 100개 단어로 이루어진 (100, ) 크기의 텍스트형 데이터가 들어오게 된다면 (100, 500) 크기로 변환된다.

keras.utils.to_categorical(train_data) 와 같은 코드를 통해 원-핫 인코딩을 적용할 수 있다.

하지만 원-핫 인코딩의 단점입력 데이터가 엄청 커진다는 것이다. 토큰 1개를 500차원으로 늘렸기 때문에 대략 500배가 커진다고 볼 수 있다. 이는 썩 좋은 방법으로 보이진 않는다.

 

그래서 실제 순환 신경망에서는 텍스트형 데이터를 처리할 때 주로 단어 임베딩(Word Embedding)을 이용한다.

단어 임베딩은 정수로 변환된 토큰을 비교적 작은 크기의 실수 밀집 벡터로 변환한다. 이런 밀집 벡터는 단어 사이의 관계를 표현할 수 있기 때문에 자연어 처리에서 좋은 성능을 발휘한다.

케라스에서는 keras.layers 패키지 아래 Embedding 클래스로 임베딩 기능을 제공한다. 이 클래스를 다른 층처럼 모델에 추가하면 처음에는 모든 벡터가 랜덤하게 초기화되지만 훈련을 통해 데이터에서 좋은 단어 임베딩을 학습한다.

model = keras.Sequential()
model.add(keras.layers.Embedding(500, 16, input_length=100))
model.add(keras.layers.SimpleRNN(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))

Embedding 클래스의 첫번째 매개변수 500은 어휘 사전의 크기이다. 두번째 매개변수 16은 임베딩 벡터의 크기이다. 세번째 매개변수 100은 입력 시퀀스의 길이이다. 

 

예시 문제를 한번 보자.

Q: 어떤 순환층에 (100, 10) 크기의 입력이 주입되고 이 순환층의 뉴런 개수가 16개라면 이 층에 필요한 모델의 파라미터 개수는 몇개일까?

A: 입력 시퀀스에 있는 토큰 벡터의 크기가 10이고 순환층의 뉴런 개수가 16이므로 Wx의 크기는 10 x 16 = 160 개이다. 순환층의 은닉 상태에 곱해지는 wh의 크기는 16 x 16 = 256 개이다. 마지막으로 뉴런마다 1개씩 총 16개의 절편이 있기 때문에 160 + 256 + 16 = 432개이다.

 

 

고급 순환층 : LSTM 과 GPU 셀 

더 나아가 고급 순환층인 LSTMGRU 에 대해서 알아보도록 하자.

위의 두 개의 층들은 SimpleRNN 보다 계산이 훨씬 복잡하다. 하지만 성능이 뛰어나기 때문에 순환 신경망에 많이 채택되고 있다.

일반적으로 기본 순환층은 긴 시퀀스를 학습하기 어렵다. 이는 시퀀스가 길수록 순환되는 은닉 상태에 담긴 정보가 점차 희석되기 때문이다. 따라서 멀리 떨어져 있는 단어 정보를 인식하는 데 어려울 수 있다. 이를 위해 LSTM 과 GRU 셀이 발명 되었다.

 

LSTM (Long Short-Term Memory) 는 단기 기억을 오래 기억하기 위해 고안되었다. 

LSTM 에는 입력과 가중치를 곱하고 절편을 더해 활성화 함수를 통과시키는 구조를 여러개 가지고 있다. 이런 계산 결과는 다음 타임스텝에 재사용된다.

먼저 LSTM 이 은닉 상태를 만드는 방법을 알아보자.
은닉 상태는 입력과 이전 타임스텝의 은닉 상태를 가중치에 곱한 후 활성화 함수를 통과시켜 다음 은닉 상태를 만든다. 
이때 기본 순환층과 달리 시그모이드 활성화 함수를 사용한다. 또 tanh 활성화 함수를 통과한 어떤 값과 곱해져서 은닉 상태를 만든다.

Wo는 은닉 상태를 계산할 때 사용하는 가중치 Wx와 Wh를 통틀어 표현한 것이다. 

LSTM 에는 순환되는 상태가 2개이다. 은닉 상태말고 셀 상태(cell state) 라고 부르는 값이 또 있다. 은닉 상태와 달리 셀 상태는 다음 층으로 전달되지 않고 LSTM 셀에서만 순환되는 값이다. 

셀 상태는 위 그림의 초록색 c 부분이다. 또한 LSTM 에는 총 4개의 셀이 존재한다.
먼저 입력은닉 상태를 각기 다른 가중치에 곱한 다음, 하나는 시그모이드 함수를 통과시키고 다른 하나는 tanh 함수를 통과 시킨다.
그다음 두 결과를 곱한 후 이전 셀 상태와 더한다. 이 결과가 최종적인 다음 셀 상태가 되는 것이다.

LSTM 은 마치 작은 셀을 여러 개 포함하고 있는 큰 셀과 같다. 중요한 것은 입력과 은닉 상태에 곱해지는 가중치들이 다르다는 점이다. 

위의 그림처럼 세 군데의 곱셈을 왼쪽부터 삭제 게이트, 입력 게이트, 출력 게이트라고 부른다.

삭제 게이트는 셀 상태에 있는 정보를 제거하고 입력 게이트는 새로운 정보를 셀 상태에 추가한다. 마지막으로 출력 게이트를 통해서 이 셀 상태가 다음 은닉 상태로 출력된다.

LSTM 은 다음과 같이 keras로 사용할 수 있다.

from tensorflow import keras

model = keras.Sequential()
model.add(keras.layers.Embedding(500, 16, input_length = 100))
model.add(keras.layers.LSTM(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))

 

순환층에 드롭아웃 적용하기

Fully connected Neural Network 와 Convolution Neural Network 에서는 Dropout 클래스를 사용해 드롭아웃을 적용했었다. 이를 통해 모델이 훈련 세트에 과대적합이 되는것을 막을 수 있었다. Recurrent Neural Network 에서도 자체적으로 드롭아웃 기능을 제공한다. 두 가지가 있는데,  dropout 매개변수와 recurrent_dropout이 있다.

dropout 매개변수는 셀의 입력에 드롭아웃을 적용하고,
recurrent_dropout 매개변수는 순환되는 은닉 상태에 드롭아웃을 적용한다. 
하지만 기술적 문제로 인해 recurrent_dropout 를 사용하면 GPU 를 사용하지 못하여 모델의 훈련속도가 매우 느려질 수 있다.

 

순환층을 여러 개 연결하기

순환층을 연결할 때는 주의할 점이 있다. 순환층의 은닉 상태는 샘플의 마지막 타임스텝에 대한 은닉 상태만 다음 층으로 전달한다. 
하지만 순환층을 쌓게 되면 모든 순환층의 순차 데이터가 필요하다. 따라서 앞쪽의 순환층이 모든 타임스텝에 대한 은닉 상태를 출력해야 한다. 오직 마지막 순환층만 마지막 타임스택의 은닉상태를 출력해야한다.

이는 다음과 같이 코드를 작성하면 된다.

from tensorflow import keras

model = keras.Sequential()
model.add(keras.layers.Embedding(500, 16, input_length = 100))
model.add(keras.layers.LSTM(8, dropout=0.3, return_sequences=True))
model.add(keras.layers.LSTM(8, dropout=0.3)
model.add(keras.layers.Dense(1, activation='sigmoid'))

위의 코드와 같이 케라스의 순환층에서 모든 타임스텝의 은닉 상태를 출력하려면 마지막을 제외한 다른 모든 순환층에서 return_sequences 매개변수를 True 로 지정하면 된다.

 

GRU (Gated Recurrent Unit) 구조

GRU 는 뉴욕 대학교 조경현 교수가 발명한 셀이다. 이 셀은 LSTM 을 간소화한 버전으로 생각할 수 있다. 이 셀은 LSTM 처럼 셀 상태를 계산하지 않고 은닉 상태 하나만 포함하고 있다. 먼저 GRU 셀의 그림은 다음과 같다.

GRU 셀에는 은닉 상태와 입력에 가중치를 곱하고 절편을 더하는 작은 셀이 3개 들어있다. 2개는 시그모이드 활성화 함수를 사용하고 하나는 tanh 활성화 함수를 사용한다. 위 그림에서는 은닉 상태와 입력에 곱해지는 가중치를 합쳐서 나타나 있다.

맨 왼쪽에서 Wz 를 사용하는 셀의 출력이 은닉 상태에 바로 곱해져 삭제 게이트 역할을 수행한다. 
이와 똑같은 출력을 1에서 뺀 다음에 가장 오른쪽 Wg 를 사용하는 셀의 출력에 곱한다. 이는 입력되

는 정보를 제어하는 역할을 수행한다.
가운데 Wr 을 사용하는 셀에서 출력된 값은 Wg 셀이 사용할 은닉 상태의 정보를 제어한다.

GRU 셀은 LSTM 보다 가중치가 적기 때문에 계산량이 적지만 LSTM 못지않은 좋은 성능을 내는것으로 유명하다.

from tensorflow import keras

model = keras.Sequential()
model.add(keras.layers.Embedding(500, 16, input_length = 100))
model.add(keras.layers.GRU(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))