Group Study (2023-2024)/Machine Learning 입문

[ML입문] week6 - 순환 신경망

임지 Imjee 2023. 12. 11. 23:43

1. 순환 신경망

사진 출처 : https://www.goldenplanet.co.kr/our_contents/blog?number=857&pn=

1) 순환 신경망의 정의와 작동원리

이전의 합성곱 신경망과 다르게 뉴런에 자신을 참조하는 화살표, 즉 순환고리가 있다.

A,B,C 데이터가 있다고 가정하고, 순환 신경망의 작동 원리를 살펴보자.

  1. A가 먼저 뉴런에 입력된다. 이에 출력값인 Oa가 생성되고, 이 Oa가 B를 처리할 때 재사용된다.
  2. 순환 신경망은 앞의 샘플의 출력을 다시 사용해서 B를 계산할 때 다시 사용한다.
  3. 그래서 Ob는 어느 정도 Oa에 대한 정보 일를 담고 있을 가능성이 있다.
  4. 다음으로 C를 처리할 때에는 Ob를 다시 재사용해서 Oc를 계산한다.

2) 타임스텝과 순환 신경망의 활성화 함수

순환 신경망에서 하나의 샘플을 처리하는 하나의 단계를 타임스텝이라고 한다.

또한, 셀(순환층)에서 출력한 값을 은닉상태라고 한다.

각 뉴런에는 활성화함수가 적용되는데, 순환 신경망의 경우에는 tanh함수가 활성화함수로서 사용된다.

이 tanh함수를 '시그모이드 함수'라고 칭할 때도 있는데, 이는 로지스틱 함수의 시그모이드 함수를 뜻하는 것이 아니다.
단지, tanh함수의 모양이 S자 모양이기에, 시그모이드 함수라고도 부르는 것이다.

사진 출처 : https://reniew.github.io/12/

위 사진은 Tanh 함수로, S자 모앙이나 y값 범위가 -1이상 1이하이다.

사진 출처 : https://reniew.github.io/12/

위 사진은 로지스틱 함수로서의 시그모이드 함수로, 똑같이 S자 모양이나 y값 범위가 0이상 1이하라는 점에서 tanh함수와 차이가 있다.

3) 순환 신경망과 가중치

순환 신경망의 경우에도 합성곱신경망처럼 입력값에 가중치를 곱하나, 이전 샘플의 은닉상태, 즉 Wh도 함께 곱해진다.
순환 신경망은 이렇게 새로운 은닉상태를 만든다.

이와 같은 과정을, '타입스텝으로 펼친다'라고도 표현한다.

이 때 중요한 것은,가중치 Wx가 샘플마다 동일하게 사용된다는 점이다.

순환 신경망은 타임스텝에 걸쳐 가중치를 공유한다.
사진 출처 : https://doongjun.tistory.com/m/5

또한 위 그림처럼, 입력층과 순환층이 있을 때 각각의 뉴런들은 완전 연결된다.
즉, 완전 연결 신경망에서 순환 고리가 추가되었다고 이해하면 좋다.

 

4) 순환 신경망의 입력

예를 들어, 'I am a boy'라는 글자가 있다고 해보자.
이전에 이미지 데이터를 처리할 때 데이터를 정수나 실수값으로 변환해서 처리했듯, 텍스트 데이터의 경우도 여러 개의 정수값, 실수값으로도 표현 가능하다.
여기서는 단어 한 개당 3개의 원소를 가진 벡터로 표현해보려 한다.
제시문이 'I am a boy'이므로, (1, 4, 3)로 표현해볼 수 있겠다.
이는 샘플 1개, 타임스텝 4번, 차원 3개로 이해할 수 있다.
이 샘플이 순환층을 통과하게 되면, 순환층의 뉴런의 개수만큼 출력이 만들어진다.

즉, 셀에 있는 뉴런의 개수만큼 은닉상태가 만들어진다.

 

5) 다층 순환 신경망

순환층 셀에서 은닉상태가 출력될 때 마지막 타임스텝의 은닉상태를 출력하는 게 일반적이다.
만약, 순환신경망을 2개 이상 쌓는다면?
이 경우에도, 마지막 셀의 타임스텝의 은닉상태만 출력해야한다.

따라서 이전에 있는 셀은 마지막 은닉상태 뿐만 아니라 모든 타임스텝의 은닉상태를 출력해야한다.
이때 다음 셀에 전달하는 텐서는 (샘플, 타임스텝, 이전층 셀에 있는 뉴런 개수)로 구성된다.

 

2. 순환 신경망으로 IMDB 데이터셋 리뷰 분류하기

 

1) IMDB 리뷰 데이터셋과 용어 정리

IMDB : 인터넷 무비 데이터 베이스, 즉 영화 평점을 담은 데이터 베이스이다.
보통 텍스트 데이터로 예제를 만들 때 많이 사용한다.
리뷰 텍스트를 보고 좋게 평가하는지 나쁘게 평가하는지 (감성분석, 감정분석)을 진행한다.

  • NLP (Natural Language Processing)
  • 말뭉치 (NLP의 데이터셋)
  • 토큰 (공백을 기준으로 잘라서 사용, 하나의 단어를 토큰이라고 함)
  • 어휘 사전 (토큰의 집합, 겹치는 것 X)

he follows the cat과 he loves the cat 이라는 예문 2개가 있을 때, He는 이미 어휘 목록에 있으니 다시 추가할 필요가 없다.그래서 어휘 목록에는 [ he follows the cat loves ]가 들어가게 된다.

 

2) 케라스로 IMDB 데이터 불러오기

단어를 그대로 신경망에 주입하는 건 어려운 일이다. 따라서 모든 데이터가 숫자로 표현되어야 한다.
가장 간단한 건 고유의 단어에 임의의 번호를 붙이는 방법이다.
이를 하나하나 임의로 설정하기 보다는, 케라스는 이미 영어 단어에 숫자를 부여하고 있기에 케라스를 활용해보려한다.

from tensorflow.keras.datasets import imdb

(train_input, train_target), (test_input, test_target) = imdb.load_data(num_words=500)

print(train_input.shape, test_input.shape)
# (25000,) (25000,)

print(train_input[0])
#[1, 14, 22, 16, 43, 2, ...]

print(train_target[:20])
#[1 0 0 1 0 0 1 0 1 0 1 ... ]

print(train_input[0]) : 숫자 1은 특별히 예약된 정수로, 문장의 시작부분을 나타낼 때 보통 1을 지정한다.
숫자 2 또한 예약된 정수로서, 포함되지 않은 단어를 나타낸다. 즉, 빈 숫자를 2로 채운다.
위 경우 num_words = 500으로 했기 때문에, 500개의 단어에 포함되지 않은 단어들, 즉 5번째 단어부터 포함되지 않은 단어들이 있음을 확인할 수 있다.
따라서 num_words의 값이 클 수록 2가 나오는 경우는 적어질 것이다.

print(train_target[:20]) : 넘파이 배열로 파이썬 리스트를 담는다. 각각 리뷰마다 길이가 모두 다르기 때문에 2차원 배열 넘파이 배열로는 이를 표현할 수 없다.
긍정적이냐 부정적이냐를 기준으로, (이진분류) 긍정적일때는 1, 부정적일때에는 0으로 타깃 데이터가 준비된다.

 

3) 훈련 세트 준비

train_input, val_input, train_target, val_target = train_test_split(train_input, train_target, test_size = 0.2, random_state=42)
# 20퍼센트를 검증세트로 분류

lengths = np.array([len(x) for x in train_input])
# 샘플들을 모두 순회하며 파이썬 리스트의 길이를 계산한다.

print(np.mean(lengths), np.median(lengths))
#239.00925 178.0
# 단어 개수 평균이 239, 중간값은 178

#히스토그램으로 길이와 빈도를 출력
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()

 

4) 시퀀스 패딩

예를 들어 문장의 길이를, 7 토큰을 사용한다고 해보자. 이 경우 'I love you'의 경우 3개의 토큰만을 사용한다. 나머지 4개의 토큰은 비워져 있을 수 밖에 없다.

합성곱신경망의 경우처럼 0으로 비워진 자리를 채우는 패딩으로써 사용할 수 있다. (0은 패딩으로 사용되는 예약어)


따라서 이번에는 만약 길이가 더 길다면 잘라내고, 길이가 더 짧다면 빈 공간을 0으로 채워주는 작업을 하려 한다.

for 루프를 돌릴 수도 있겠지만.. 케라스에 이미 이 기능이 구현되어있다.

pad_sequences라는 함수를 사용하면 빈 자리를 채울 수 있다. maxlen = 100으로 잡아보았다.

from tensorflow.keras.preprocessing.sequence import pad_sequences

train_seq = pad_sequences(train_input, maxlen=100)

print(train_seq.shape)
#(20000, 100)

print(train_seq[0])
# [ 10 4 ... 158]

print(train_input[0][-10:])
#[6, 2, 46 ... 158]

print(train_seq[5])
[ 0 0 0.... 2]

val_seq = pad_sequences(val_input, maxlen=100)
# 검증세트도 똑같이 진행한다.

print(train_input[0][-10:]) : 마지막 10개를 출력해보면 train_seq의 뒷부분과 같다. 이 문장은 100개 토큰보다 긴 문장일 것이다. (0이 없으므로)

일반적으로 pad_sequencesmaxlen의 앞부분을 자른다.

문장의 뒷부분이 더 의미있을 수 있다고 가정했기 때문이다.

print(train_seq[5]) : 앞부분이 0으로 패딩되었음을 확인할 수 있다. 토큰의 개수가 100개보다 모자라서 앞에 0으로 패딩 된 것이다.

 

5) 순환 신경망 모델 만들기

from tensorflow import keras

model = keras.Sequential()

model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))

model.add(keras.layers.Dense(1, activation='sigmoid')
# 이진 분류이므로 Dense 층을 두어야 한다.
# 순환층이 1개 있는 간단한 모델

model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500))) : 순환층을 만들 때, SimpleRNN 클래스를 사용한다.
Dense층을 만들때와 유사하게, 첫 번째 매개변수 : 뉴런의 개수 지정, 2번째 매개변수 : input_shape의 크기 지정.
input_shape = (100,500)) : (100개의 토큰이 들어오고, 두 번째 차원은 500인데, 500은 추후에 설명하려함.)

 

6) 임베딩

만약 원핫 인코딩으로 입력 데이터를 준비한다면, 20000개의 샘플, 100개의 토큰, 토큰마다의 500개의 원핫인코딩 배열이 생성 될 것이다. 즉, 입력 데이터 크기가 너무 거대하다.
이런 경우 모델을 돌릴 때마다 차원의 개수가 급격히 늘어나게 된다.
또한 원핫 인코딩의 단점은 각각의 토큰의 서로의 관계, 유사도를 무시한다는 점이다.
이에 대비해 텍스트 처리에서 높은 성능을 보여주는게 임베딩 방법이다.

 임베딩은 토큰을 지정된 갯수의 실수 벡터로 변환하는 방법이다. ex) cat이면 0.2, 0.1, 1.3 ... 실수값으로 채우는 것.

이 임베딩 방법도 케라스에서 클래스를 제공한다.

model2 = keras.Sequential()

model2.add(keras.layers.Embedding(500, 16, input_length=100))
# 500개의 어휘 사전, 출력 차원을 16으로 둠. (16개의 벡터로 토큰 하나를 출력)
# 이를 통해 SimpleRNN에서 처리하는 벡터의 개수를 훨씬 줄일 수 있다.
model2.add(keras.layers.SimpleRNN(8))
model2.add(keras.layers.Dense(1, activation='sigmoid'))

model2.summary()

 

 
embedding의 Param은 8000개인데, 샘플 개수가 500개이기 때문에, 500x16개의 가중치 = 8000

SimpleRNN의 가중치를 계산해보면 16x(이전층의 출력값)x8(뉴런개수) + 8x8(순환) + 8(절편) = 200 이라는 값을 얻었음을 확인할 수 있다.

 

3. LSTM

1) LSTM 셀

Long Short Term Memory의 약자로, 작동 원리를 설명해보자면 아래와 같다.

사진 출처 : https://velog.io/@2a_02/%ED%98%BC%EA%B3%B5%EB%A8%B8%EC%8B%A0%EB%94%A5%EB%9F%AC%EB%8B%9D-09-3-LSTM%EA%B3%BC-GRU-%EC%85%80

LSTM 모델은 셀 안에 셀이 더 있다고 생각하면 된다.
즉, 입력과 가중치를 곱하고 절편을 더해 활성화 함수를 통과시키는 구조를 여러 개 가지고 있다.
새로운 용어로, 셀 상태가 등장한다. 이는 출력되지 않는, 즉 LSTM 셀 내에서 순환만 하는 값이다.
곱해지는 부분을 게이트라고 표현한다.

1. Wf가 있는 이 셀을 통과해서 셀 상태를 만드는데 기여하는 곱셈 부분을 삭제 게이트
2. 두 셀의 곱해지는 곱셈을 입력게이트
3. 맨 오른쪽의 tanh를 통과시키고 곱해지는 부분을 출력 게이트라고 한다.

2가지로 요악해보자면,

순환 셀이 셀 안에 4개가 더 들어있다.
은닉상태 뿐만 아니라, 셀 상태도 있는데, 셀 자체 내에서 순환되는 값이다. (출력값 x) 

 

2) LSTM 신경망 

model = keras.Sequential()

model.add(keras.layers.Embedding(500, 16, input_length=100))
model.add(keras.layers.LSTM(8))
#SimpleRNN을 LSTM으로 변경
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()

LSTM층의 Param 계산 :
16(입력) x 8 (뉴런개수) + 8(LSTM셀의 순환되는 셀)x8(은닉상태의 셀 순환) + 8 (절편)
이렇게 얻은 값 200에 x4(셀 내부 4개의 셀) 를 통해 800이라는 값을 얻을 수 있다.
 

3) 드롭아웃 적용하기

순환 신경망의 경우, 드롭아웃층을 따로 추가하지 않고 매개변수로 지정할 수 있다.

model2 = keras.Sequential()

model.add(keras.layers.Embedding(500, 16, input_length=100))
model2.add(keras.layers.LSTM(8, dropout = 0.3))
# dropout : 은닉상태의 드롭아웃 비율 지정
# 순환되는 셀 상태의 드롭아웃 비율은 따로 지정해주어야함.
model2.add(keras.layers.Dense(1, activation='sigmoid'))

 

4) 2개의 층을 연결하기

마지막 셀이 아닌 이상, 그 앞에 있는 순환층들은 모든 타임스텝의 은닉상태를 모두 출력해야한다.

이를 return_sequenes=True로 두어 매 타임스텝마다 은닉상태까지 출력한다.

modle3 = keras.Sequential()

model3.add(keras.layers.Embedding(500, 16, input_length=100))
model3.add(keras.layers.LSTM(8, dropout=0.3, return_sequences=True))
#2개의 층을 만듦.
model3.add(keras.layers.LSTM(8, dropout = 0.3))
model3.add(keras.layers.Dense(1, activation='sigmoid'))

model3.summary()

 
 

4. GRU 셀

1) GRU 셀

GRU셀은 LSTM셀의 간소화 버전이다.
은닉상태와 입력을 곱해 새로운 은닉상태를 만드는 것 까지는 동일하나, 여기는 셀 상태가 없다.

사진 출처 : https://velog.io/@2a_02/%ED%98%BC%EA%B3%B5%EB%A8%B8%EC%8B%A0%EB%94%A5%EB%9F%AC%EB%8B%9D-09-3-LSTM%EA%B3%BC-GRU-%EC%85%80

GRU셀은 STLM과 달리, 셀 내부에 작은 셀 3개가 더 존재한다.
여기서 2개는 시그모이드함수, 1개는 tanh 함수를 사용한다.
단, 이전처럼 입력값과 은닉상태(h)를 곱해서 새로운 은닉상태(hf)는 과정은 동일하다.
 

2) GRU 신경망

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

model4.summary()

 
GRU층의 Param 개수 계산 : 16(입력)x8(뉴런개수) + 8x8(은닉상태에 곱해지는 가중치) + 8(절편) = 200, 여기에 8을 더한다 (8개의 뉴런마다 8개의 가중치가 더 필요)
이렇게 얻은 값인 208에 x3(내부셀 3개)을 하면 624라는 값을 얻을 수 있다.
 
---
참고 자료
https://www.inflearn.com/course/%ED%98%BC%EC%9E%90%EA%B3%B5%EB%B6%80-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EB%94%A5%EB%9F%AC%EB%8B%9D/dashboard