소소하지만 소소하지 않은 개발 공부/머신 러닝 교과서

[머신러닝교과서] RNN을 사용한 영화 리뷰 감성 분석(1)

still..epochs 2023. 1. 31. 21:41

* 본 포스팅은 머신러닝교과서를 참조하여 작성되었습니다.

* https://github.com/rickiepark/python-machine-learning-book-3rd-edition

 

GitHub - rickiepark/python-machine-learning-book-3rd-edition: <머신 러닝 교과서 3판>의 코드 저장소

<머신 러닝 교과서 3판>의 코드 저장소. Contribute to rickiepark/python-machine-learning-book-3rd-edition development by creating an account on GitHub.

github.com

 

감성 분석을 위해서 다대일(many-to-one) 구조의 다층 RNN을 구현할 수 있다. 머신러닝교과서에서 제공하는 movie_data 셋을 활용하여 데이터 전처리를 통해 RNN을 구현해 보자!

 

영화 리뷰 데이터 준비

import tensorflow as tf
import tensorflow_datasets as tfds
import numpy as np
import pandas as pd
import os
import gzip
import shutil

df = pd.read_csv('movie_data.csv', encoding='utf-8')

df.tail()

* https://github.com/rickiepark/python-machine-learning-book-3rd-edition/blob/master/ch08/movie_data.csv.gz

 

GitHub - rickiepark/python-machine-learning-book-3rd-edition: <머신 러닝 교과서 3판>의 코드 저장소

<머신 러닝 교과서 3판>의 코드 저장소. Contribute to rickiepark/python-machine-learning-book-3rd-edition development by creating an account on GitHub.

github.com

영화 리뷰 데이터셋은 해당 링크에서 다운로드 할 수 있다.

 

 

movie_data 셋을 다운받아 확인해 보니 데이터프레임 df 에는 'review'와 'sentiment' 두 개의 컬럼이 있다. 'review'는 영화 리뷰 텍스트(입력 특성)를 담고 있고 'sentiment'는 예측하려는 타깃 레이블이다.

 

영화 리뷰 텍스트는 단어의 시퀀스 이다. RNN모델을 만들어 각 시퀀스를 긍정적(1) 또는 부정적(0)인 리뷰로 분류해보자.

 

RNN 모델에 데이터를 주입하기 전에 몇 가지 전처리 단계를 적용해야 한다.

  1. 텐서플로 데이터셋 객체를 만들고 훈련, 테스트, 검증 데이터셋으로 나눈다.
  2. 훈련 데이터셋에 있는 고유한 단어를 찾는다.
  3. 고유한 단어를 고유한 정수로 매핑하고 리뷰 텍스트를 정수(고유 단어의 인덱스) 배열로 인코딩한다.
  4. 모델에 입력하기 위해 데이터셋을 미니 배치로 나눈다.

 

 

1. 단계

첫 번째 단계이다. 데이터 프레임에서 텐서플로 데이터셋을 만든다.

## 1단계 : 데이터셋 만들기
target = df.pop('sentiment')
ds_raw = tf.data.Dataset.from_tensor_slices((df.values, target.values))

## 확인:
for ex in ds_raw.take(3):
    tf.print(ex[0].numpy()[0][:50], ex[1])

이제 훈련, 테스트, 검증 데이터셋으로 나눌 수 있다. 전체 데이터셋은 5만 개의 샘플을 담고 있다. 처음 2만 5000개의 샘플은 평가를 위해 떼어 놓는다. 그 다음 2만 개의 샘플은 훈련에 사용하고 5,000개의 샘플은 검증에 사용한다.

# 일정한 결과값을 얻기 위한 seed값 설정
tf.random.set_seed(1)

ds_raw = ds_raw.shuffle(50000, reshuffle_each_iteration=False)
ds_raw_test = ds_raw.take(25000)
ds_raw_train_valid = ds_raw.skip(25000)
ds_raw_train = ds_raw_train_valid.take(20000)
ds_raw_valid = ds_raw_train_valid.skip(20000)

 

2단계

신경망의 입력으로 데이터를 준비하기 위해 단계 2~3에서 언급한 것처럼 숫자 값으로 인코딩 해야 한다. 이렇게 하기 위해 먼저 훈련 데이터셋에서 고유한 단어(토큰)을 찾는다. 데이터셋을 사용하여 고유한 토큰을 찾을 수 있지만 파이썬 표준 라이브러리에 있는 collections 패키지의 Counter 클래스를 사용하는 것이 효율적이다.

 

다음 코드에서 Counter 객체(token_counts)를 만들어 고유한 단어의 빈도를 수집한다. 텍스트를 단어(또는 토큰)로 나누려면 tensorflow_datasets 패키지가 제공하는 Tokenizer 클래스를 사용할 수 있다.

## 2단계: 고유 토큰(단어) 찾기
from collections import Counter
tokenizer = tfds.deprecated.text.Tokenizer()
token_counts = Counter()
for example in ds_raw_train:
    tokens = tokenizer.tokenize(example[0].numpy()[0])
    token_counts.update(tokens)

print('어휘 사전 크기:', len(token_counts))

>> 어휘 사전 크기: 87007

 

 

3단계

그 다음 각각의 고유 단어를 고유 정수로 매핑한다. 파이썬 딕셔너리를 사용하여 수동으로 처리할 수 있다. 키는 고유 토큰(단어)이고 키에 매핑된 값은 고유한 정수이다. 하지만 tensorflow_datasets 패키지는 이런 매핑과 전체 데이터셋을 인코딩할 수 있는 TokenTextEncoder 클래스를 제공한다. 먼저 고유한 토큰을 전달하여 TokenTextEncoder 클래스로 encoder 객체를 만든다(token_counts는 토큰과 횟수를 포함하고 있지만 여기서는 횟수가 필요하지 않으므로..무시). encoder.encode() 메서드를 호출하여 입력 텍스트를 정수 리스트로 변환한다.

## 3단계: 고유 토큰을 정수로 인코딩하기
encoder = tfds.deprecated.text.TokenTextEncoder(token_counts)
example_str = 'This is an example!'
print(encoder.encode(example_str))

>> [232, 9, 270, 1123]

검증 데이터와 테스트 데이터에 있는 토큰이 훈련 데이터에 없다면 매핑되지 않을 수 있다.q개(TokenTextEncoder에 전달한 token_counts의 크기, 여기서는 8만 7007개)의 토큰이 있고 이전에 본 적이 없으며 token_counts에 포함되지 않은 모든 토큰은 정수 q+1에 할당된다. 다른 말로 하면 인덱스 q+1이 알려지지 않은 단어를 위해 예약된다. 예약된 또 다른 값은 정수 0이다. 시퀸스 길이를 조절하기 위한 용도로 사용된다.

 

데이터셋 객체의 map() 메서드를 사용하여 다른 변환을 적용하듯이 데이터셋에 있는 각 텍스트를 변환할 수 있다. 하지만 여기에는 문제가 있다. 텍스트 데이터가 텐서 객체에 들어가 있어 즉시 실행 모드에서 텐서의 numpy()메서드로 참조할 수 있다. 하지만 map() 메서드로 변환하는 동안에는 즉시 실행이 비활성화된다. 이 문제를 해결하기 위해 두 개의 함수를 정의한다.

## 3-A단계: 변환을 위한 함수 정의
def encode(text_tensor, label):
    text = text_tensor.numpy()[0]
    encoded_text = encoder.encode(text)
    return encoded_text, label

## 3-B단계: 함수를 TF 연산으로 변환하기
def encode_map_fn(text, label):
    return tf.py_function(encode, inp=[text, label], Tout=(tf.int64, tf.int64))

ds_train = ds_raw_train.map(encode_map_fn)
ds_valid = ds_raw_valid.map(encode_map_fn)
ds_test = ds_raw_test.map(encode_map_fn)

#샘플의 크기 확인하기:
tf.random.set_seed(1)
for example in ds_train.shuffle(1000).take(5):
    print('시퀀스 길이', example[0].shape)

 

지금까지 단어 시쿠니슬르 정수 시퀀스로 변환했다. 하지만 여전히 해결할 문제가 있는데 시퀀스 길이가 다르다는 것이다.

 

일반적으로 RNN은 다른 길이의 시퀀스를 다룰 수 있지만 미니 배치에 있는 시퀀스는 효율적으로 텐서에 저장하기 위해 동일한 길이가 되어야 한다.

 

크기가 다른 원소를 가진 데이터셋을 미니 배치로 나누기 위해 텐서플로는 (batch() 대신) padded_batch() 메서드를 제공한다. 이 메서든느 하나의 배치에 포함되는 모든 원소를 자동으로 0으로 패딩하여 배치에 있는 모든 시퀀스가 동일한 길이가 되도록 만든다.

 

실제 예시로 확인해 보자! 훈련 데이터셋에서 크기가 8인 작은 데이터셋 ds_train을 만든다. 그리고 batch_size=4로 padded_batch() 메서드를 적용해 보자. 미니 배치에 들어가기 전에 개별 원소의 길이와 만들어진 미니 배치의 차원을 출력한다.

## 일부 데이터 추출하기
ds_subset = ds_train.take(8)
for example in ds_subset:
    print('개별 샘플 크기:', example[0].shape)
    
>>  개별 샘플 크기: (119,)
	개별 샘플 크기: (688,)
	개별 샘플 크기: (308,)
	개별 샘플 크기: (204,)
	개별 샘플 크기: (326,)
	개별 샘플 크기: (240,)
	개별 샘플 크기: (127,)
	개별 샘플 크기: (453,)



## 배치 데이터 만들기
ds_batched = ds_subset.padded_batch(
             4, padded_shapes=([-1], []))
for batch in ds_batched:
    print('배치 차원:', batch[0].shape)

>>  배치 차원: (4, 688)
	배치 차원: (4, 453)

출력된 텐서 크기에서 알 수 있듯 첫 번째 배치의 열 개수(즉, .shape[1])는 688이다. 처음 네 개의 샘플이 하나의 배치가 되었고 이 샘플 중에 가장 큰 크기를 사용했다. 다시 말하면 이 배치에 있는 세 개의 다른 샘플을 이 크기에 맞도록 필요한 만큼 패딩을 추가한다. 비슷하게 두 번째 배치의 열 크기는 다음 네 개의 샘플 중에 가장 큰 크기인 453이다. 여기서도 최대 길이보다 작은 다른 샘플에 패딩을 추가했다.

 

세 개의 데이터셋을 모두 배치 크기 32의 미니 배치로 나눈다.

train_data = ds_train.padded_batch(32, padded_shapes=([-1], []))
valid_data = ds_valid.padded_batch(32, padded_shapes=([-1], []))
test_data = ds_test.padded_batch(32, padded_shapes=([-1], []))

이제 데이터가 이어지는 절에서 구현할 RNN 모델에 적합한 포맷이 되었다. 그전에 먼저 다음 절에서 특성 임베딩(embedding)에 대해 알아보자! 필수적인 것은 아니지만 단어 벡터의 차원을 줄여 주기 때문에 매우 권장되는 전처리 단계이다!