본문 바로가기
AI/자연어처리

순환신경망을 이용한 IMDB 리뷰 분류해보기(28)

by 바다의 공간 2024. 12. 10.

CNN에서의 대표적인 실습 할 수 있는것이 MNUST같은건데!

RNN으로 가장 대표적인 데이터셋은 IMDB 영화리뷰입니다.

 

https://www.imdb.com/

 

IMDb: Ratings, Reviews, and Where to Watch the Best Movies & TV Shows

IMDb is the world's most popular and authoritative source for movie, TV and celebrity content. Find ratings and reviews for the newest movie and TV shows. Get personalized recommendations, and learn where to watch across hundreds of streaming providers.

www.imdb.com


두가지 방법으로 순환신경망에 입력을 해볼 예정입니다.

1. 원핫인코딩

2. 워드임베딩

 

일단 입력데이터 자체를 그대로 넣을 수 없으니 RNN(순환신경망)에 넣으려면 숫자로 변환해야합니다.

 

원핫인코딩의경우 숫자를 고차원 벡터로 변한한 후 시퀀스 형태로 RNN에 입력합니다. 

"I love movies" → [[1, 0, 0], [0, 1, 0], [0, 0, 1]].

 

워드임베딩의 경우 숫자로 변환하는것은 동일하지만 학습가능한 밀집벡터로 변환합니다.

예시)

"I" → [0.5, 0.1, 0.2]
"love" → [0.6, 0.8, 0.9]
"movies" → [0.1, 0.4, 0.7]

"I love movies" → [[0.5, 0.1, 0.2], [0.6, 0.8, 0.9], [0.1, 0.4, 0.7]] 로 되는거겠죠.

 

워드임베딩같은경우에는 단어간의 의미적 관계(유사성)을 반영합니다.

 

여기서 고차원, 저차원이으로 구분되는거 맞냐 둘다 2차원으로 보이지않나? 라고 생각했지만

-고차원 : 단어 집합 크기에 비례해서 벡터의 차원이 아주 커진다는 뜻

-저차원 : 집합크기가 1만이라도 단어를 50차원이나 100차원처럼 작은 크기의 벡터로 표현합니다.

이라는 뜻입니다. 즉 2차원으로 전처리를 한 이유는 입력데이터가 시퀀스형태로 표현될때는 보통 2차원 배열로 나타나기때문입니다.

 

 

 


  IMDB 리뷰 데이터셋

  • imdb.com 에서 수집한 리뷰를 감상평에 따라 '긍정' 과 '부정' 으로 분류해 놓은 데이터 셋
  • 총 50,000개의 샘플
  • 훈련:테스트 => 25,000개:25,000개
'''
He follows the cat.  He loves the cat
 ↓    ↓      ↓   ↓     ↓   ↓     ↓  ↓
 10   11   12   13    10  14    12  13


각 단어마다 고유한 정수값 부여.  동일 단어에는 동일한 정수 매핑
단어의 의미나 크기와는 관련 없다.

일반적으로, 영어문장의 경우 모두 소문자로 바꾸고 구둣점등을 삭제한 다음 공백을 기준으로 단어 분리
이렇게 분리된 단어를 토큰(token) 이라 부릅니다.

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

※ 간단한 문제라면 영어 말뭉치에서는 '토큰' 과 '단어'는같다고 보아도 무방하나, 한글은 다릅니다
한글은 조사, 어미변형등이 발달되어 있어서 단순히 공백으로 나누는 것만으로는 부족하기에
반드시 형태소 분석을 통해 토큰을 만들어야 합니다  → KoNLPy 사용
'''
None

 

1부터 시작을했고 0은 패딩으로 되는거죠! 그 다음에 OOV도 어떤 정수값에 매칭을 해주어야합니다.

 

'''
토큰에 할당되는 정수중 특별한 용도로 예약되어 있는 경우도 있다
ex)
0 - 패딩
1 - 문장의 시작
2 - 어휘 사전에 없는 토큰 (OOV)

'어휘사전' : 훈련세트에서 '고유한 단어'를 뽑아 만든 목록
'''
None

 

위와같이 토큰에 할당되는 정수중 특별한 용도로 예약되어있는 경우도 있습니다.

패딩은 0 ,문장의 시작은 1, 어휘 사전에 없는 토큰은 2로 할 수 있죠

그래서 어휘사전(훈련세트에서 고유한 단어를 뽑아 만든 목록)을만들게될수도 있습니다.

 

 


  리뷰 분류 실습 시작

▶ 기본 임포트

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

import tensorflow as tf
from tensorflow import keras

tf.keras.utils.set_random_seed(42)   # 랜덤 시드 사용
tf.config.experimental.enable_op_determinism() #텐서플로우의 항상 동일한 실행 결과 보장!

 

  IMDB데이터 보기

from tensorflow.keras.datasets import imdb

 

(train_input, traia_target),(test_input, test_target) = IMDB.load_data(num_words=10000)

 

여기서 데이터 로드할때 num_words라는 파라미터가 있습니다.

전체 데이터 셋에서 가장 자주 등장하는 단어 num_words만큼만 사용 지정합니다.

만약 지정하지않으면 88584만큼(원본데이터) 단어가 로딩되는데...인덱스 크기가 엄청 커지는거죠!


  Data 확인

train_input.shape, test_input.shape  # 훈련용:테스트용 = 25000:25000
train_input.dtype  # 데이터 원소가 python object

 

print(train_input[0])  # 첫번째 샘플 <- list 다!  <- 문장의 각 토큰이 정수인덱싱화 한 결과

결과 값은 [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 45......]로 됩니다.

이미 토큰화+정수인덱싱까지 된 결과값이 나오게 됩니다.

 

 

▶ 각 1,2번째 리뷰의 단어 개수 살펴보기

len(train_input[0])  # 첫번째 리뷰의 단어개수
218

len(train_input[1])  # 두번째 리뷰의 단어개수
189

리뷰는 당연히 문장의 길이는 당연히 제각각 다를겁니다.

하나의 리뷰는 하나의 샘플이 됩니다 그래서 서로 다른 길이의 샘플을 신경망에 어떻게 전달해야하는지를 생각해야합니다.

 

시퀀스의 길이가 입력이 쉐이프가 된다고 했는데 이거 문장마다 다릅니다..

 

근데 첫번째 샘플의 결과값을 봤는데 2가 있었습니다. 

2가 있었다는건 위에서 보면 OOV로 어휘에 없는 토큰을 뜻합니다.

그래서 단어사전(어휘사전)의 첫번째는 3부터 시작하게 됩니다.

1은 문장의 시작이고요!

 

단어사전에 없는 단어는 모두 2로 인코딩되어있다는걸 알 수 있습니다.

 

 

  Target 확인

print(train_target[:20])

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

 

리뷰가 긍정이면 1

리뷰가 부정이면 0 인지를 판단하는 이진분류 문제가 됩니다.

 

여기서 target을 했는데 데이터가 0과1로 이진분류가 되는 이유는 imdb.load_data()는 이미 전처리된 상태로 데이터를 불러오기 때문에 레이블과 데이터가 딱 맞게 나올 수 있습니다.

 


  데이터 확인하기

아래와 같은 순서로 확인이 데이터 확인은 가능합니다.

 

1. 단어 인덱스 가져오기, 단어이인덱스를(단어->인덱스)에서 (인덱스->단어)로 변한합니다.

인덱스가 3부터 시작하므로 인덱스 오프셋을 적용합니다.(지금 다루는 데이터는 규칙을 이미 정해놔씩때문에!)

2. 리뷰를 실제 단어로 변환해서 출력하기.


▶ imdb.get_word_index()를 가지고 단어타입과 개수를 뽑아보았습니다.

 

# 단어인덱스 가져오기
word_index = imdb.get_word_index()
print(type(word_index), len(word_index))


<class 'dict'> 88584

 

총 88584만큼의 word가 들어있다는것을 확인할 수 있습니다.

print(word_index)
{'fawn': 34701, 'tsukino': 52006, 'nunnery': 52007, 'sonja': 16816, 'vani': 6395......}

이런식으로 단어사진을 확인할 수 있습니다.

 

▶ 또한 특정 단어를 인덱스로 가져오기

#특정단어 -> 인덱스

word_index['spiders']

 

16115라는 숫자를 가지고 있다는것을 알 수 있습니다.

{'spiders':16115}

 

▶ 지금 단어를 키값으로 했는데 인덱스->단어로 변환하려면

# 단어 인덱스를 {단어:인덱스} 에서 {인덱스:단어} 로 변환
# 딕셔너리 컴프리헨션!

reversed_word_index = {value: key for key, value in word_index.items()}
print(reversed_word_index)

{34701: 'fawn', 52006: 'tsukino', 52007: 'nunnery', 16816: 'sonja', 63951: 'vani', 14...}

 

▶ 여기서 인덱스 0번값을 가져오면 당연히 에러가 뜹니다 이유는?

보통 0번은 패딩으로 처리되기때문에 Key Error가 발생합니다.

 

 

 

인덱스 [1]을 하게되면 the가 나옵니다.

▶ 이유는 뭐냐면 가장 많은 값이 1로 나오기 때문에 the가 나오게 됩니다 이건 납득이 바로가능하더라구요.

 

 

그리고 아까 단어 인덱스가 88584까지 있어서 다시 확인해보니 88584는 있고 

88585는 Key Error가 뜹니다 이유는 당연히 갖고있는 사전의 개수보다 더 뒤데있는 숫자를 인덱스로찍으니 

없는값이니 eroor가 나옵니다.


▶  인덱스가 3부터 시작하므로 인덱스 오프셋을 적용합니다.

IMDB 데이터셋에서 인덱스 오프셋이 3인 이유는 특별토큰예약때문인데요

0:패딩토큰, 1:시작토큰, 2:알수없는단어(유사한거:OOV)로 돼서 시작은 3부터 시작합니다. <-헷갈리면안돼요~

 

 

여기서보면 1,13,22 뭐 있는데분명 3번부터 나오지않냐 할수있어요

이건 방금 설명적었듯이 오프셋이 0,1,2,는 각의 역할이 있으니 대입하면됩니다.

앞에 1은 시작토큰입니다.

 

여기서 시작토큰과 실제 첫 번째 단어(3)이 이해가 잘 안갔는데

토큰1은 진짜절대적으로 모델이 문장의 시작을 알게하는 메타정보고 토큰3은 문장의 실제 첫 번째단어라고 합니다.

그니까 여기서 1은 모델에게 여기서부터 문장 시작이야~ 라고 알려주는 부분인거죠!

예를들어서 
I love your baby라는 문장이 있다고 하면 IMDB같은 데이터셋의 인덱스 토큰이라면!

[1, 4, 7, 10, 13]
  • 1 = <START>
  • 4 = "I"
  • 7 = "love"
  • 10 = "your"
  • 13 = "baby"
이렇게 됩니다

이런식으로 됩니다. 

 

그러면 print를 해서 1을 해보면 어떤게 나올지 궁금해서 해보니 'the'가 나옵니다.

▶ 왜그런지해서 보니 reversed_word_index는 현재 특별토큰을 지정하지않아서그렇습니다.

 


 

인덱스가 3부터 시작하므로 인덱스 오프셋을 적용할겁니다.

 

IMDB 데이터셋에서 인덱스 오프셋이 3인 이유는 다음과 같은 특별한 토큰들을 예약해 두기 위해서입니다:

0: 패딩 토큰 (padding token)

1: 시작 토큰 (start token)

2: 알 수 없는 단어 (unknown token)

# 정수 인코딩된 문장을 단어로 구성된 문장으로 디코딩
def decode_review(encoded_review):
    return ' '.join([reversed_word_index.get(i - 3, '?') for i in encoded_review])

 

여기서 decoding은 정수로 인코딩 된 데이터(리뷰)를 원래 다시 단어의 상태로 복원하는 과정을 말합니다.

 

# i번째 리뷰를 단어로 변환해서 출력
i = 1
decoded_review = decode_review(train_input[i])
print(train_input[i])  # 토큰의 정수 인덱스 값
print(decoded_review)
print(train_target[i])

[1, 194, 1153, 194, 8255, 78, 228, 5, 6, 1463, 4369, 5012, 134, 26, 4, 715, 8, 118, 1634, 14, 394, 20, 13, 119, 954, 189, 102, 5, 207, 110, 3103, 21, 14, 69, 188, 8, 30, 23, 7, 4, 249, 126, 93, 4, 114, 9, 2300, 1523, 5, 647, 4, 116, 9, 35, 8163, 4, 229, 9, 340, 1322, 4, 118, 9, 4, 130, 4901, 19, 4, 1002, 5, 89, 29, 952, 46, 37, 4, 455, 9, 45, 43, 38, 1543, 1905, 398, 4, 1649, 26, 6853, 5, 163, 11, 3215, 2, 4, 1153, 9, 194, 775, 7, 8255, 2, 349, 2637, 148, 605, 2, 8003, 15, 123, 125, 68, 2, 6853, 15, 349, 165, 4362, 98, 5, 4, 228, 9, 43, 2, 1157, 15, 299, 120, 5, 120, 174, 11, 220, 175, 136, 50, 9, 4373, 228, 8255, 5, 2, 656, 245, 2350, 5, 4, 9837, 131, 152, 491, 18, 2, 32, 7464, 1212, 14, 9, 6, 371, 78, 22, 625, 64, 1382, 9, 8, 168, 145, 23, 4, 1690, 15, 16, 4, 1355, 5, 28, 6, 52, 154, 462, 33, 89, 78, 285, 16, 145, 95]
? big hair big boobs bad music and a giant safety pin these are the words to best describe this terrible movie i love cheesy horror movies and i've seen hundreds but this had got to be on of the worst ever made the plot is paper thin and ridiculous the acting is an abomination the script is completely laughable the best is the end showdown with the cop and how he worked out who the killer is it's just so damn terribly written the clothes are sickening and funny in equal ? the hair is big lots of boobs ? men wear those cut ? shirts that show off their ? sickening that men actually wore them and the music is just ? trash that plays over and over again in almost every scene there is trashy music boobs and ? taking away bodies and the gym still doesn't close for ? all joking aside this is a truly bad film whose only charm is to look back on the disaster that was the 80's and have a good old laugh at how bad everything was back then
0

 

그래서 2번째 print문을 보면 big, hair등으로 다시 디코딩되는것을 볼 수 있습니다.

?의 의미는 어휘사전에 없는 단어를 나타낸 것입니다.

 

이번에는 500개의 단어만 학습으로 사용하도록 하겠습니다 너무 많으면 안돌아가니까..

# 이번예제에선 500개의 단어만 사용 (학습 용이)
(train_input, train_target), (test_input, test_target) =\
                    imdb.load_data(num_words=500)

  Train Validation 데이터세트 분리

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)
        
train_input.shape, val_input.shape
# ((20000,), (5000,))

 

  Train 세트에 대한 조사!

-적절한 길이의 padding을 위해 하는것입니다.

- 긴 리뷰는 잘라내기로 길이를 줄이고, 짧은 리뷰는 패딩으로 길이를 늘리는것!

lengths = np.array([len(x) for x in train_input])  # 문장의 길이(들)

 

각 리뷰의 길이를 계산해서 np로 저장하는 역할을 하게됩니다.

왜 np를 사용하냐면 넘파이 배열은 길이의 최소값, 최대값, 평균, 중앙값 등을 빠르게 계산할 수 있기에 Numpy로 변환하게 됩니다. 결국 리뷰의 분포를 확인해서 패딩길이(maxlen)을 설정하는데 사용됩니다.

np.min(lengths), np.max(lengths), np.mean(lengths), np.median(lengths)

(11, 1854, 239.00925, 178.0)

최소는 11, 최대는 1854, 평균은 23 중앙값은 178이되는것을 확인할 수 있습니다. 

 

이 부분을 히스토그램으로 작성해보면

plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()


  Padding pad_sequences()

tk.keras.utils.pad_sequences()

https://www.tensorflow.org/api_docs/python/tf/keras/utils/pad_sequences

 

tf.keras.utils.pad_sequences  |  TensorFlow v2.16.1

Pads sequences to the same length.

www.tensorflow.org

tf.keras.utils.pad_sequences(
    sequences,
    maxlen=None,
    dtype='int32',
    padding='pre',
    truncating='pre',
    value=0.0
)

 

위 코드는 각각의 파라미터들을 나열한것이다 .이런 파라미터들을 좀 다뤄보려고한다.

 

여기서 Padding을 하는 이유는????

빈 공간채우기 위한 이유가 가장 핵심적이고 RNN이나 신경망 모델은 고정된 크기의 입력만 처리할 수 있기때문에

짧은 리뷰에 대해서는 빈 공간을 채워서 길이를 맞춰주어야합니다.

 

당연히 이렇게 하는 이유는 통일해서 한번에 연산작업을 하기 위한것이겠죠! 이것이 장점이 되겠구요!

 

그리고...

패딩이 앞쪽에 보통 들어가는 이유는 일반적으로 리뷰의 끝 부분의 정보가 더 중요하다고 여겨지기때문에 

pre가 기본이 되지만 뒤로 배치해야된다면 위치를 조정할 수 있습니다.

 

 

from keras.preprocessing.sequence import pad_sequences

# padding 전
len(train_input[0]), len(train_input[5])
# max_len=100 으로 할거다.
# (259, 96)   <-  100보다 큰 문장,  100보다 작은 문장

 

패딩작업 전 은  259, 96고, 작업 전 첫번재 리뷰와 5번째 리뷰의 길이를 확인한거죠

패딩작업을 거치게 되면 (100으로 할것입니다.)

그렇게되면 패딩 후 shape를 보면 100이되겠져?

 

max_len = 100

train_seq = pad_sequences(
    sequences=train_input,
    maxlen = max_len,
)

train_seq.shape

 

이렇게 출력해보면 (20000, 100)으로됩니다.

train_input 은 파이썬 list의 1차원 배열이었지만 train_seq는 (20000,100) 크기의 2차원 array가 되었습니다.

 

그래서 padding 후는 아래처럼!

 

# padding 후
len(train_seq[0]), len(train_seq[5])

(100,100)

 

이렇게 변경되는것을 확인할 수 있습니다.

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]

 

출력을 해보면 이런식인데!  100개가 나오겠죠?

근데 아까 첫번째 문장의길이는 259였고 지금은 100이잖아요?

그럼 어디부분이 잘렸던건데 어딜까요? truncating='pre'니까 앞부분이 잘린것으로 확인할 수 있겠죠?

 

# 어디가 잘렸나?  앞부분!  truncating='pre',
print(train_input[0][:100])
print(train_input[0][-100:])

결과값 ↓
[1, 73, 89, 81, 25, 60, 2, 6, 20, 141, 17, 14, 31, 127, 12, 60, 28, 2, 2, 66, 45, 6, 20, 15, 497, 8, 79, 17, 491, 8, 112, 6, 2, 20, 17, 2, 2, 4, 436, 20, 9, 2, 6, 2, 7, 493, 2, 6, 185, 250, 24, 55, 2, 5, 23, 350, 7, 15, 82, 24, 15, 2, 66, 10, 10, 45, 2, 15, 4, 20, 2, 8, 30, 17, 2, 5, 2, 17, 2, 190, 4, 20, 9, 43, 32, 99, 2, 18, 15, 8, 157, 46, 17, 2, 4, 2, 5, 2, 9, 32]
[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]

 

이렇게 보면 알 수 있듯

pad_sequences()함수는 기본적으로 maxlen보다 긴 시퀀스는 앞부분을 자릅니다.

한국말도 끝까지 들어봐야 안다 ! 라는말을 인용하면 왜 앞을 자르는지 이해가 가실거같아요!

 

또한 만약 앞이아니라 이건 특수해서 뒤쪽을 잘라야한다면 'pre' -> 'post'로 바꾸면 됩니다.

print(train_seq[5])

 

아까 5번쨰문장은 96이였는데 패딩을 해서 100으로 했죠?

그리고 turncating='pre'라서 앞쪽에 0이 4개로 채워진것을 확인할 수 있습니다.


# val 데이터 세트도 패딩
val_seq = pad_sequences(val_input, maxlen=max_len)

 

물론 val데이터세트도 패딩해야합니다.


  순환 신경망 만들기 SimpleRNN

- keras.layers.SimpleRNN 사용합니다.

https://www.tensorflow.org/api_docs/python/tf/keras/layers/SimpleRNN

 

tf.keras.layers.SimpleRNN  |  TensorFlow v2.16.1

Fully-connected RNN where the output is to be fed back as the new input.

www.tensorflow.org

tf.keras.layers.SimpleRNN(
    units,
    activation='tanh',
    use_bias=True,
    kernel_initializer='glorot_uniform',
    recurrent_initializer='orthogonal',
    bias_initializer='zeros',
    kernel_regularizer=None,
    recurrent_regularizer=None,
    bias_regularizer=None,
    activity_regularizer=None,
    kernel_constraint=None,
    recurrent_constraint=None,
    bias_constraint=None,
    dropout=0.0,
    recurrent_dropout=0.0,
    return_sequences=False,
    return_state=False,
    go_backwards=False,
    stateful=False,
    unroll=False,
    seed=None,
    **kwargs
)

 

헉 파라미터가 굉장히 많네요.

 

일단 RNN 모델 구조는

1. 입력층(100,500) 크기의 입력데이터(리뷰길이100, 원핫인코딩 벡터크기 500)

2. 순환층 SimpleRNN으로 뉴런 8개

3. 출력층 Dense 층으로 뉴런1개 활성화함수는 sigmoid가 됩니다.

 

여기서

model = keras.Sequential()

model.add(keras.layers.Input(shape=(100, 500)))  # 입력차원 (100, 500)  <- (샘플의 시퀀스 길이 100, 뒤의 500은?)
model.add(keras.layers.SimpleRNN(units=8))  # 순환층 뉴런 개수

# IMDB 리뷰 문제는 이진 분류이므로 마지막 출력층은 1개의 뉴런을 가지고 sigmoid 활성화 함수 사용해야 함.
model.add(keras.layers.Dense(1, activation='sigmoid'))

 

각 층의 의미 를 좀 더 디테일하게 보면


▶Input 층:
입력 데이터의 크기: (100, 500)
100: 리뷰의 단어 수 (패딩된 길이).
500: One-Hot 인코딩으로 표현된 단어 벡터 크기.
이 층은 데이터를 RNN 층에 전달하기 위한 구조를 정의합니다.

 

▶ SimpleRNN 층:
units=8: RNN에서 사용하는 뉴런(또는 은닉 상태)의 개수.
뉴런은 데이터를 처리하고, 다음 타임스텝으로 정보를 전달하는 역할을 합니다.
여기서는 8개의 뉴런으로 입력 데이터를 학습하며 리뷰의 맥락을 추출합니다.

 

▶ Dense 층 (출력층):
뉴런 1개: 긍정(1) 또는 부정(0)을 예측하기 위한 출력값 생성.
활성화 함수 sigmoid: 출력값을 0~1 사이로 변환하여 이진 분류에 적합하게 만듭니다.


 이 데이터셋은 두가지 방법으로 변형하여서 순환 신경망에 주입해보도록하겠습니다!

위에서 말했듯

1. 원핫인코딩

2. 단어임베딩

 

  One-Hot-Encoding

여기서 단어 토큰(정수값)은 산술 연살과는 상관이 없는 데이터고 

이건 0,1로 나누는 분류형입니다. 그래서 고유하게 표현하는 방법으로 one-hot-encoding사용합니다

# 첫 문장 seq
train_seq[0]

첫번째 문장 인코딩된부분

 

하나의 토큰을 0과 1의 배열로 표현하고 이 배열에는 한개만 1이고 나머지는 0으로 표현됩니다.

 

▶ 변경하는 방법은 to_categorical()을 사용합니다.

tf.keras.utils.to_categorical(정수배열)

https://www.tensorflow.org/api_docs/python/tf/keras/utils/to_categorical

 

tf.keras.utils.to_categorical  |  TensorFlow v2.16.1

Converts a class vector (integers) to binary class matrix.

www.tensorflow.org

tf.keras.utils.to_categorical(
    y,
    num_classes=None,
    dtype='float32'
)

원핫인코딩 전에 확인해보자면

train_seq.shape는 (20000, 100)입니다.

 

train_oh = keras.utils.to_categorical(train_seq)
train_oh.shape  # 토큰 하나가 500개로 표현된다!

(20000, 100, 500)

 

인코딩 한 후 shape를 보면

 

# 첫번째 토큰 '10' 이 one-hot encoding 으로 변환된 모습
train_oh[0][0]   # 딱 한개만 '1'

 

이렇게되면 너무 커지니까 워드임베딩을 진행하려고합니다.

워드 임베딩은 텍스트 데이터를 고정된 크기의 실수 벡터로 변환합니다.

np.sum(train_oh[0][0])

 

첫번재 리뷰데이터의 첫번째 단어를 원핫인코딩한 벡터에서 1의 개수를 더하면 당연히 1이 나오겠죠~

 

 

val_oh = keras.utils.to_categorical(val_seq)
val_oh.shape

 

원핫인코딩한 후 shape인데 (5000, 100, 500)이 나옵니다

샘플 수, 리뷰길이, 어휘사전 크기!

 

모델을 좀 보자면

"""
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ simple_rnn (SimpleRNN)               │ (None, 8)                   │           4,072 │

     # SimpleRNN 에 전달할 샘플의 크기는 (100, 500) 이지만
     # 이 순환층은 마지막 타임스텝의 은닉상태만 출력합니다
     # 이 때문에 출력크기가 순환층의 뉴런 개수와 동일한 8임을 확인할수 있다

    #  parameter 개수
    #  입력토큰은 500차원,  이 배열이 순환층의 뉴런 8개의 fully-connected
    #      500 x 8 = 4000 개의 입력 weight

     #   순환층의 은닉상태는 다시 다음 타임스텝에 사용되기 위해 또다른 weight 와 곱해진다.
     #   이 은닉상태도 순환층의 뉴런과 완전히 연결되기 때문에
     #       8(은닉상태 크기) * 8(뉴런의 개수) = 64 개의 weight

     #  그리고 뉴런마다 bias 있다. +8개의 weight

     #   4000 + 64 + 8 => 4072 개 parameter
     
           ★ SimpleRNN 파라미터 개수
            => units * (units + inputs +1)
            => ex) 8*(8 + 500 + 2) =? 4072


├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense (Dense)                        │ (None, 1)                   │               9 │

  # parameter
  #  (입력 8 + bias 1) x 출력 1 => 9개의 weight

└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
 Total params: 4,081 (15.94 KB)
 Trainable params: 4,081 (15.94 KB)
 Non-trainable params: 0 (0.00 B)
"""
None

 

으로됩니다.


  순환 신경망 학습

rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)  # learning_rate : 0.0001
model.compile(optimizer=rmsprop, loss='binary_crossentropy', metrics=['accurary'])

# callback
checkpoint_cb = keras.callbacks.ModelCheckpoint(
    'best-simplernn-model.keras',
    save_best_only=True,
)

early_stopping_cb = keras.callbacks.EarlyStopping(
    patience=3,
    restore_best_weights=True,
)

 

이렇게하니까 코랩에서 꺼졌습니다..... 메모리 부족으로 추정되는 훈련이 안되는 증상 발생이됩니다.

history1 = model.fit(train_oh, train_target,
                     epochs=100, batch_size=32,
                     validation_data=(val_oh, val_target),
                     callbacks=[
                       checkpoint_cb,
                       early_stopping_cb,
                     ])

 

그래서 따로 훈련된 모델을 가지고 가져오려고합니다.

 

변수는 base_path로 받겠습니다.

 

평가를 하자면

# loss, accuracy = model.evaluate(val_oh, val_target, batch_size=64)

# print('Val Loss', loss)
# print('Val Accuracy', accuracy)

즉 원핫인코딩의 문제점은 입력데이터 용량이 커집니다 거의 뭐 8G정도까지 올라가더라구요.

그래서 워드임베딩을 사용합니다.

  Word Embedding(단어임베딩) 사용

순환신경망에서 텍스트를 처리할때 자주 사용하는방법이 워드임베딩인데

단어를 고정된 크기(개수)의 실수 벡터로 변경해줍니다.

 

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding?hl=en

 

tf.keras.layers.Embedding  |  TensorFlow v2.16.1

Turns positive integers (indexes) into dense vectors of fixed size.

www.tensorflow.org

 

tf.keras.layers.Embedding

tf.keras.layers.Embedding(
    input_dim,   # 어휘 사전의 크기
    output_dim,  # 임베딩 벡터의 크기
    embeddings_initializer='uniform',
    embeddings_regularizer=None,
    activity_regularizer=None,
    embeddings_constraint=None,
    mask_zero=False,
    input_length=None,
    **kwargs
)

 

이 레이어도 모델에 추가되어 학습되는 레이어입니다(즉 파라미터가 있다는거죠)

처응메는 모든 벡터가 랜덤으로 초기화되지만 훈련을 통해 데이터에서 점점 좋은 단어 임베딩으로 학습되어집니다.

 

단어임베딩의 장점은 그냥 정수 데이터를 받는다는것이고 원핫인코딩으로 변경된 train_oh 배열이 아니라

train_seq를 바로 사용할 수 있습니다. 따라서 메모리를 훨씬 효율적으로 사용할수있다는거죠.

 

앞서 작성했던 원핫인코딩은 샘플차원하나를 500차원으로 늘렸기때문에 (100, ) -> (100, 500)으로 커진거죠

 

단어 임베딩도 (100, ) -> (100, 20)과 같이  2차원 배열로 늘립니다.

그러나 원핫인코딩과 달리 훨씬 작은 크기로도 단어를 잘 표현할 수 있어요.


▶모델 생성

# Embedding 레이어를 SimpleRNN 앞에 추가하여 model 생성

model2 = keras.Sequential()

model2.add(keras.layers.Input(shape=(100,)))  # 입력 시퀀스의 길이

model2.add(keras.layers.Embedding(
    input_dim = 500, # 어휘 사전의 크기,
                    # imdb.load_data(num_words=500)
    output_dim = 16,  # 임베딩 벡터의 크기,  One-hot 보다는 훨~씬 작은 크기의 벡터.
))

model2.add(keras.layers.SimpleRNN(8))
model2.add(keras.layers.Dense(1, activation='sigmoid'))

model2.summary()
"""
Model: "sequential_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ embedding (Embedding)                │ (None, 100, 16)             │           8,000 │
    # parameter 개수
    # Embedding 클래스는 500가지의 각 토큰을  크기가16개인 벡터로 변경하기 때문에
    #  500 x 16 = 8000 개의 모델 파라미터 가짐

├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ simple_rnn_1 (SimpleRNN)             │ (None, 8)                   │             200 │

    # parameter 개수
    #  입력 16 x 8 => 128개
    #  hidden  8 x 8 => 64개
    #  bias  8개

├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_1 (Dense)                      │ (None, 1)                   │               9 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
 Total params: 8,209 (32.07 KB)
 Trainable params: 8,209 (32.07 KB)
 Non-trainable params: 0 (0.00 B)
"""
None

 

모델 구조는 이렇게 되어있고 

워드 임베딩을 사용한 모델의 학습 과정을 정의하고 실행해보겠습니다!

rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)  # learning_rate : 0.0001
model2.compile(optimizer=rmsprop, loss='binary_crossentropy', metrics=['accuracy'])

# callback
checkpoint_cb = keras.callbacks.ModelCheckpoint(
    'best-embedding-model.keras',
    save_best_only=True,
)

early_stopping_cb = keras.callbacks.EarlyStopping(
    patience=3,
    restore_best_weights=True,
)

history2 = model2.fit(
    train_seq, train_target,   # 정수 인코딩된 시퀀스 그대로 입력 가능!
    epochs=100, batch_size=32,
    validation_data=(val_seq, val_target),   # 정수 인코딩된 시퀀스 그대로 입력 가능!
    callbacks=[checkpoint_cb, early_stopping_cb],
)

 

 

에포크는 19바퀴정도 돈것같고 근데 출력결과는 원핫인코딩이랑 비슷해보여요

 

무엇보다 학습속도가 향상되었고요 파라미터개수는 두배정도가 늘었음에도 학습속도가 향상된건 

RNN순환층의 WEIGHT가 학습속도에 크게 영향을 준다는것을 알 수 있습니다.

 

순환층의 weight의 개수는 훨씬 작고 훈련용 데이터세트의 크기도 훨씬 줄어듭니다.

 

그래프로 그려보면 

history2.history

 

사실 num_word= 500보다 크게 해보고 10000개를 넘어야...좀 더 좋은 성능이 나오겠지만 

사실 일반 cpu로는 는 조금 힘들거같고 학습시간도 매우 길어집니다.

 


  평가

loss, accuracy = model2.evaluate(val_seq, val_target, batch_size=64)
print('Val loss:', loss)
print('Val Accuracy:', accuracy)

79/79 ━━━━━━━━━━━━━━━━━━━━ 1s 8ms/step - accuracy: 0.5702 - loss: 0.6833
Val loss: 0.6824707388877869
Val Accuracy: 0.5759999752044678

 

# 테스트
test_input.shape, test_target.shape

(25000,), (25000,))

test_seq = pad_sequences(test_input, maxlen=max_len)

test_seq.shape
(25000, 100)
loss, accuracy = model2.evaluate(test_seq, test_target, batch_size=64)
print('Val loss:', loss)
print('Val Accuracy:', accuracy)
391/391 ━━━━━━━━━━━━━━━━━━━━ 3s 7ms/step - accuracy: 0.5651 - loss: 0.6813
Val loss: 0.6822234988212585
Val Accuracy: 0.5668399930000305

 


  예측하기

sample_review = 'The best documentary I have watched in a very long time. This is definitely a must see for everyone. This family and their love and support for each other is truly amazing.'
sample_review

import nltk
from nltk.tokenize import word_tokenize
nltk.download('punkt_tab')

[nltk_data] Downloading package punkt_tab to /root/nltk_data...
[nltk_data]   Unzipping tokenizers/punkt_tab.zip.
True

이렇게 임포트 시켜준 후 

token이라는 변수에 소문자 변환 뒤 토큰화를 해보려고합니다.ㅏ

# 소문자 변환뒤 토큰화
tokens = word_tokenize(sample_review.lower())
tokens

그러면 각 ['love', 'tail'] 등으로 변환이 된 것을 확인할 수 있습니다.

 

# 리뷰를 인덱스로 변환
# word_index 사용

sample_review_index = [word_index.get(word, 0) for word in tokens]

sample_review_index = [(idx if idx <= max_len else 0) for idx in sample_review_index]

print(tokens)
print(sample_review_index)

word_index를 통해서 인덱싱을 해보았습니다. 

 

텍스트 데이터를 모델이 이해할 수 있도록 숫자형태로 변환했죠!

사전에 없는 단어느 0으로 문장이 시작되는곳은 1로 됩니다.

 

이제 이 정수리스트는 패딩과정을 거쳐서 길이를 고정합니다.그래야 고정된 길이의 숫자 배열로 만들어야 신경망에

입력할 수 있습니다.

이렇게되면 모델 입력 형태를 준비하게되는겁니다.

# 패딩
sample_review_index_padded = pad_sequences([sample_review_index], maxlen=max_len)

sample_review_index_padded

 

이렇게 패딩과정까지 거쳤습니다.쉐이프를 찍어보면 (1,100)의 2차원형태로 보입니다.

(1= 샘플 리뷰의 개수)(100 = 패딩된 리뷰의 길이(단어수))->MAX_LEN=100으로 맞춘 시퀀스입니다.

 

 

예측을 이제 해보자면! model2.predict()는 데이터를 모ㅔㄷㄹ에 입력하여 긍정(1)일 확률을 에측하는겁니다.

# 예측
prediction = model2.predict(sample_review_index_padded)

prediction[0]

를 하니까 array([0.6984383], dtype=float32)가 되고 

1일 확률이 69%라는것을 확인할 수 있습니다. 이부분을 함수로 만들어보면

# 함수로 만들자

def predict_review(review):
  sample_review_index = [word_index.get(word, 0) for word in word_tokenize(review.lower())]
  sample_review_index = [(idx if idx <= max_len else 0) for idx in sample_review_index]

  sample_review_index_padded = pad_sequences([sample_review_index], maxlen=max_len)

  prediction = model2.predict(sample_review_index_padded)
  print('Prediction:', prediction[0][0])

 

predict_review로 받을 수 있고

다른 문장들을 예측해보면서 결과를 확인할 수 있습니다.