미리 보기 - 텐서플로를 사용해 순환 신경망을 만들어 영화 리뷰 데이터셋에 적용해서 리뷰를 긍정/부정으로 분류하기

<<<키워드>>>

- 원 핫 인코딩

- 단어 임베딩

IMDB 데이터셋

유명한 영화 데이터베스인 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정 / 부정으로 분류해 놓은 데이터셋이다.

총 50,000개의 샘플로 이루어져 있고 훈련데이터와 테스트 데이터에 각각 25,000개씩 나누어져 있다!

 

데이터로 텍스트 자체를 신경망에 전달?? X 컴퓨터는 숫자 데이터를 다루기 용이하므로 단어를 숫자 데이터로 바꾸는 작업 필요하다.! like 원-핫 인코딩   --> 토큰으로 변환 !!

하나의 샘플은 여러 개의 토큰으로 이루어져 있고 1개의 토큰이 1개의 타임스텝에 해당한다.

 

IMDB 데이터셋을 두 가지 방법으로 순환 신경망에 적용시켜보자!

1. 원-핫 인코딩

2. 단어 임베딩

 

 

keras 데이터셋 패키지의 imdb 모듈을 import해서 자주 사용하는 단어 500개를 불러오자.

( num_words 매개변수를 500으로 지정 )

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,)

 

훈련 세트와 테스트 세트의 크기 25,000씩인것까지 확인!

 

근데 (25000,) 을 보듯이 배열이 1차원이다.! 

텍스트 데이터 특성상 길이가 제각각이므로 고정 2차원 배열에 담는것보다 메모리 효율적이므로 1차원에 저장하는것이다!

즉, 넘파이 배열안에  정수,숫자가 아닌 개별 리뷰가 담겨있다고 볼 수 있다.

 

첫 번째 리뷰의 길이를 출력해보자!!

print(len(train_input[0]))

218

218개의 토큰으로 이루어져 있다.!

 

이제 길이말고 내용을 출력해보고 싶다!

print(train_input[0])

[1, 14, 22, 16, 43, 2, 2, 2, 2, 65, 458, 2, 66, 2, 4, 173,36, 256, 5, 25, 100, 43, 2, 112, 50,
2, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 2, 2, 17, 2, 38, 13
, 447, 4, 192, 50, 16, 6, 147, 2, 19, 14, 22, 4, 2, 2, 469, 4, 22, 71, 87, 12, 16, 43, 2, 38, 
76, 15, 13, 2, 4, 22, 17, 2, 17, 12, 16, 2, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2, 2
, 16, 480, 66, 2, 33, 4, 130, 12, 16, 38, 2, 5, 25, 124, 51, 36, 135, 48, 25, 2, 33, 6, 22, 
12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 2, 15, 256, 4, 2, 7, 2, 5, 2, 36, 
71, 43, 2, 476, 26, 400, 317, 46, 7, 4, 2, 2, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2, 56, 26
, 141, 6, 194, 2, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30,2, 18, 51, 36, 28, 224, 92
, 25, 104, 4, 226, 65, 16, 38, 2, 88, 12, 16, 283, 5, 16, 2, 113, 103, 32, 15, 16, 2, 19, 178,32]

텐서플로에 있는 IMDB 리뷰 데이터는 이미 정수로 변환되어서 개별 토큰으로 출력된다.

참고로 num_words=500으로 지정했기 때문에 어휘 사전에는 500개의 단어만 들어가있다.

따라서 어휘 사전에 없는 단어는 모두 2로 표시 된다!!!

 

이제 타겟 데이터를 출력해보자.!

print(train_target[:20])

[1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 1 0 1]

타겟 데이터 => 0: 부정, 1: 긍정


데이터를 더 살펴보기 전 훈련세트에서 검증세트를 떼어 놓자 !

from sklearn.model_selection import train_test_split
train_input, val_input, train_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state= 42)

20%를 검증세트로 떼어 놓았으니 훈련 세트의 크기는 20,000개로 줄어들것.

 

 

이제 훈련 세트에 대해 몇 가지를 건드려보자.!

각 리뷰의 길이들을 확인해보고 싶다.!! 

 

-> 각 리뷰의 길이를 계산해 넘파이 배열에 담고, 리스트 함축 구문을 이용해 train_input의 원소를 순회하면서 길이를 알아보자.

import numpy as np
lengths = np.array([len(x) for x in train_input])

print(np.mean(lengths), np.median(lengths))
239.00925 178.0

평균 = 239     /   중간값 = 178

 

평균값과 중간값이 차이가 많이 나므로 데이터의 분포가 한쪽에 치우쳐져 있을 것이다.

import matplotlib.pyplot as plt
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()

대부분의 리뷰 길이는 300이 안되는것을 확인할 수 있다.!

 

연습삼아 100개의 단어만 사용해보자!

하지만 100개의 단어도 안되는 리뷰가 굉장히 많으므로 앞서 배운 패딩을 적용시켜야 한다.

 

보통 업계에서 패딩을 나타내는 토큰으로는 0을 사용한다고 한다.

 

케라스의 시퀀스 데이터의 길이는 맞추는 메소드를 이용해보자 (pad_sequences()).

이 메소드를 이용해서 train_input의 길이를 100으로 간편하게 맞춰보자.

from tensorflow.keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100) # 패딩으로 train_input길이를 100으로 맞춤

# 100보다 길면 잘라내고 짧으면 0으로 패딩 ->

100으로 패딩한 train_seq을 출력해보자.

print(train_seq.shape)

(20000, 100)

아무래도 2차원 배열이 데이터를 다루기에 효율적이므로,

패딩을 이용해 길이를 같게 해줌으로써 2차원 배열로 만든것 같다!

샘플 개수 = 25,000개에서 20%떼어냈으므로 20,000개,

토큰 개수 = 100으로 패딩했으므로 100개로  이루어진 2차원배열이다!!

 

train_seq에 있는 첫 번째 샘플을 출력해보자.

 

print(train_seq[0])

[ 10   4  20   9   2 364 352   5  45   6   2   2  33 269   8   2 142   2
   5   2  17  73  17 204   5   2  19  55   2   2  92  66 104  14  20  93
  76   2 151  33   4  58  12 188   2 151  12 215  69 224 142  73 237   6
   2   7   2   2 188   2 103  14  31  10  10 451   7   2   5   2  80  91
   2  30   2  34  14  20 151  50  26 131  49   2  84  46  50  37  80  79
   6   2  46   7  14  20  10  10 470 158]

이 샘플의 앞뒤에 패딩값 0이 없는것으로 보아 100보다는 긴 리뷰라고 볼 수 있다.!

 

원래 샘플의 앞vs뒤 부분중 어디가 잘렸을지 확인해보기 위해 train_input에 있는 원본 샘플의 끝을 먼저 보자.

print(train_input[0][-10:])

[6, 2, 46, 7, 14, 20, 10, 10, 470, 158]

음수 인덱스슬라이싱을 사용해 train_input[0]에 있는 마지막 10개의 토큰을 출력했다

train_seq[0]의 출력값과 정확히 일치하는것 또한 확인할 수 있다.

 

그러면????

샘플 앞부분이 잘렸다는 것!!

 

참고)  pad_sequences 메소드는 기본적으로 maxlen보다 긴 시퀀스의 앞부분을 자른다고 한다!!!

이유-> 일반적으로 텍스트 中 중요한 정보는 뒷부분(어미)에 나올 확률이 높기 때문..

 

하지만!! 

그럼에도 불구하고 앞부분을 자르고 싶다면 

pad_sequences( truncating = 'post') 로 설정해주면 된다. truncating = 'pre'가 default값

 

print(train_seq[5])

[  0   0   0   0   1   2 195  19  49   2   2 190   4   2 352   2 183  10
  10  13  82  79   4   2  36  71 269   8   2  25  19  49   7   4   2   2
   2   2   2  10  10  48  25  40   2  11   2   2  40   2   2   5   4   2
   2  95  14 238  56 129   2  10  10  21   2  94 364 352   2   2  11 190
  24 484   2   7  94 205 405  10  10  87   2  34  49   2   7   2   2   2
   2   2 290   2  46  48  64  18   4   2]

이 6번째 샘플을 보면 앞 부분이 0으로 채워져 있는것으로 보아 리뷰 길이가 100이 안되는것을 확인할 수 있다.

 

또한, 앞 부분이 0으로 채워져 있는것으로 보아 위와 같은 이유(중요한 부분 -> 뒤에 있다.)로 인해

패딩토큰은 시퀀스의 뒷부분이 아닌 앞부분에 추가된다는것을 엿볼 수 있다.

 

하지만!!!!!

또 뒷부분을 0으로 채우고싶다면

pad_sequences( padding = 'post')로 바꿔주면 된다.!

 

이제 검증 세트의 길이도 100으로 맞춰보고 본격적으로 해보자.

val_seq = pad_sequences(val_input, maxlen=100)

훈련 세트, 검증 세트 모두 100으로 패딩완료.

 

순환 신경망 만들기

가장 간단한 클래스인 SimpleRNN클래스를 이용

IMDB 리뷰 긍정/부정 분류 모델이므로 이진분류---> 따라서 활성화 함수는 시그모이드로 선택

 

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'))

<<SimpleRNN의 첫 번째 매개변수 X = 사용할 뉴런의 개수>> -> CNN과 동일

 

input_shape = (100,500)으로 했는데, 첫 번째 차원이 100인 것은 앞에서 샘플의 길이를 100으로 지정했기 때문이다.!

 

두 번째 차원이 500인 것은, 원-핫 인코딩을 위해 배열의 길이를 설정해준것!

(기존의 정수는 크기가 제각각이지만, 그 크기에는 순서와 의미가 없다. 따라서 그것을 제거해주는 원핫인코딩이 필요)

 

새로 등장한 개념인 순환층또한 활성화 함수가 필요하다! SimpleRNN의 기본 활성화 함수는 

하이퍼볼릭 탄젠트 함수인 tanh라는 매우 강력해보이는 함수를 사용한다. tanh => -1~1출력하는 시그모이드

 

 

우리는 원-핫 인코딩을 제공하는 모듈을 사용하도록 하자!

train_oh라는 매개변수 : train_seq을 원핫인코딩으로 변환한 배열

train_oh = keras.utils.to_categorical(train_seq)

print(train_oh.shape)
(20000, 100, 500)

배열 크기 20,000개인 이유 => 정수 하나하나 모두 500차원의 배열로 변경되었음 (20000, 100)크기의 train_seq이 

(20000,100,500으로 변한것)


이렇게 샘플 데이터의 크기가 1차원 정수배열(100,)에서 2차원 배열 (100, 500)로 바꿔야하므 SimpleRNN 클래스의 input_shape매개변수의 값을 (100,500)으로 지정한 것이다.

 

이제 인코딩이 잘 되었는지 확인해보자.

print(train_oh[0][0][:12])


[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]

< train의 첫 번째 샘플의 첫 번째 토큰 中 12개 원소 > -> 인코딩이 잘 되었음을 확인

 

이제 val도 인코딩

val_oh = keras.utils.to_categorical(val_seq)

 

모델 요약 !

model.summary()

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 simple_rnn (SimpleRNN)      (None, 8)                 4072      
                                                                 
 dense (Dense)               (None, 1)                 9         
                                                                 
=================================================================
Total params: 4,081
Trainable params: 4,081
Non-trainable params: 0
_________________________________________________________________

 

rmsprop = keras.optimizers.RMSprop(learning_rate=0.4)

model.compile(optimizer = rmsprop, loss = 'binary_crossentropy',
				metrics=['accuracy'])
                
check_point_cb = keras.callbacks.ModelCheckpoint('best-simplernn-model.h5',
						save_best_only = True)
earlystopping_cb= kerars.callbacks.EarlyStopping(patience=3, restore_best_weights=True)

history=model.fit(train_oh,train_target, epochs=100, batch_size=64,
				validation_data = (val_oh, val_target),
				callbacks = [check_point_cb, earlystopping_cb])
Epoch 1/100
313/313 [==============================] - 7s 19ms/step - loss: 0.7534 - accuracy: 0.4992 - val_loss: 0.7433 - val_accuracy: 0.5002
Epoch 2/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7456 - accuracy: 0.4998 - val_loss: 0.7360 - val_accuracy: 0.4996
Epoch 3/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7392 - accuracy: 0.4992 - val_loss: 0.7300 - val_accuracy: 0.5032
Epoch 4/100
313/313 [==============================] - 6s 18ms/step - loss: 0.7332 - accuracy: 0.4992 - val_loss: 0.7240 - val_accuracy: 0.5048
Epoch 5/100
...............................................................................................
313/313 [==============================] - 6s 18ms/step - loss: 0.6931 - accuracy: 0.5112 - val_loss: 0.6934 - val_accuracy: 0.5028
Epoch 33/100
313/313 [==============================] - 6s 18ms/step - loss: 0.6931 - accuracy: 0.5127 - val_loss: 0.6933 - val_accuracy: 0.5028

조기 종료(33회) 시의 에포크를 확인해보자! 정확도도 약 80%로 뛰어난 편이라고 할 수 있다.

 

이제 훈련 손실과 검증 손실을 이용해 그래프를 그려보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epochs')
plt.ylabel('loss')
plt.legend(['train','val'])
plt.show()

단어 임베딩

순환 신경망에서 텍스트 처리 방법의 일종으로,

각 단어를 고정된 크기의 실수 벡터로 만들어 주는 것.!

 

보통 NLP에서는 원-핫 인코딩 보다 단어 임베딩이 더 좋다고한다..!

(원핫 인코딩된 숫자보다 의미 있는 값으로 채워줄 수 있다고해서)

 

또한 단어 임베딩의 장점 : 입력으로 정수를 받을 수 있다..!

<<즉, 원-핫 인코딩으로 변경된 train_oh 배열이 아니 train_seq를 사용할 수 있다. -> 메모리 효율적>>

 

이 또한 케라스 라이브러리에 있는 모듈을 사용해보자.

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

Embedding에 있는 500은 어휘 사전의 크기이다. (앞서서 설정한 500)

              ""               16은  임베딩 벡터의 크기이다.

 

모델 요약!

model2.summary()

Model2: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_1 (Embedding)     (None, 100, 16)           8000      
                                                                 
 simple_rnn_2 (SimpleRNN)    (None, 8)                 200       
                                                                 
 dense_2 (Dense)             (None, 1)                 9         
                                                                 
=================================================================
Total params: 8,209
Trainable params: 8,209
Non-trainable params: 0
_________________________________________________________________

 

 

키워드 :  순차 데이터

               피드포워드 신경망

               타임스텝

               은닉 상태

               

 

댓글 등 텍스트를 인식하여 어떠한 평가, 의견등을 분석해 볼 수 있을까??

ex) 상품 판매 홈페이지에서 댓글을 통해 상품에 대한 긍정 / 부정적인 의견 구분하기

 

순차 데이터

순차 데이터는 텍스트, 리스트와 같이 순서에 의미가 있는 데이터를 말한다. ( 시계열 데이터 )

우리가 여태까지 사용했던 feature 데이터 등은 순서가 중요하지 않았다.

( 사용자 A : "별로지만 추천해요." ==> 긍정 부정 판단시 애매함, 순서 고려해야함 !! )

 

앞서 우리가 머신러닝 말고도 딥러닝의 완전 연결 신경망이나 CNN에서는 하나의 샘플에 대해 정방향 계산을 수행하고 

나면, 그 샘플은 버려지고 다시 사용하지 않았다.!!

이렇게 입력데이터의 흐름이 앞으로만 전달되는 신경망을 피드포워드 신경망이라고 한다.!!

신경망이 이전에 처리했던 샘플을 처리하는데 재사용하기 위해 이렇게 데이터의 흐름 앞으로 전해져선 안된다고 한다.

다음 샘플을 위해선 이전 데이터가 신경망 층에 순환될 필요가 있다. 이러한 신경망을 순환 신경망이라고 한다.

 

순환 신경망

순환 신경망은 일반적인 완전 연결 신경망과 거의 비슷하다.

완전 연결 신경망에서 이전 데이터의 처리 흐름을 순환하는 고리 하나만 추가하면 된다!!!

( 아래 그림에서 은닉층에 있는 붉은 고리처럼 )

그림을 보면 뉴런의 출력이 다시 자기 자신으로 되돌아온다. (재사용)

 

예를 들어 A,B,C 3개의 샘플을 처리하는 순환 신경망의 뉴런을 가정해보자. 첫 번째 A를 처리하고 난 뒤는 어떻게 될까?

첫 번째 샘플인 A를 처리하고, 다시 뉴런으로 들어간다. 그다음 B를 처리할 때 앞에서 A를 사용해 만든 출력 OA를 함께 사용한다.

따라서 OB에는 A에 대한 정보도 어느정도 포함되어 있을 것이다. 그다음 C도 마찬가지로 Ob를 함꼐 사용한다. (누적되겠지?)

이렇게 샘플을 처리하는 한 단계를 타임스텝이라고 말한다.

※OC에는 A보다 B에 대한 정보가 더 많다!!※--> 최신화 가중치?

 

순환 신경망에서는 층을 이라고 말한다.

하나의 셀에는 여러 개의 뉴런이 있지만 완전 연결 신경망과 달리 뉴런을 모두 표시하지 않고 하나의 셀로 층을 표현한다. 

또 셀의 출력을 은닉 상태라고 말한다.!!

 

--> summary : 기존의 연결 신경망 : 입력에 어떤 가중치를 곱하고 활성화 함수를 통과시                             켜 다음층으로 보내는것. 

                            + 층의 출력을 다음 스텝에서 재사용하면 순환 신경망

 

일반적으로 은닉층의 활성화 함수는 하이퍼볼릭 탄젠트 함수를 사용한다. tanh 

모양은 시그모이드함수와 비슷하게 생겼다.

차이점은 시그모이드와 달리 -1 ~ 1의 범위를 가진다.

 

아까 순환신경망은 재사용 빼고 다 같다고 했는데, 그렇다면 가중치는 어떻게 될까??

 

순환 신경망에서의 뉴런은 가중치가 하나 더 있다. 

이전 타임스텝의 은닉 상태에 곱해지는 가중치

Wx : 입력에 곱해지는 가중치

Wh : 이전 타임스텝의 은닉 상태에 곱해지는 가중치

셀의 은닉 상태 ( = 출력 )가 다음 타임스텝에서 재사용되기 때문에 타임스텝으로 셀을 나눠서 다시 그림을 보자.

 

셀의 가중치와 입출력

순환 신경망의 셀에서 필요한 가중치의 크기를 계산 해보자.

입력층 4개, 순환층 3개

Wx :  4 x 3 = 12

Wh :  3 x 3 = 9개

Wh의 크기는 n^2이다.

 

이제 모델 파라미터 개수 = 가중치 + 절편

12 + 9 + 3 이므로 24가지의 파라미터가 있음을 확인할 수 있다.!!

 

 

순환층의 입력 / 출력

CNN을 생각해 보면 합성곱 층의 입력은 전형적으로 하나의 샘플이 3개의 차원을 가집니다.

너비, 높이, 채널이다. 입력이 합성곱 층과 풀링 층을 통과하면 너비, 높이, 채널의 크기가 달라지지만 차원의 개수는 그대로 유지된다 !!!

 

순환층은 일반적으로 샘플마다 2개의 차원을 가진다. 

보통 하나의 샘플을 하나의 시퀀스라고 말한다고 한다고 한다.

시퀀스 안에는 여러 개의 아이템이 들어 있는데, 여기에서 시퀀스의 길이가 타임스텝의 길이가 된다!!!

 

ex) " I am a boy. "

이 샘플은 4개의 단어로 이루어져 있는데, 각 단어를 3개의 어떤 숫자로 표현한다고 가정해보자.

이런 입력이 순환층을 통과하면 두 번째, 세 번째 차원이 사라지고 순환층의 뉴런 개수만큼 출력된다.

이를 차근차근 설명해 보겠다. 

 

 

 

 

 

+ Recent posts