TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 딥러닝, 신경망>

[딥러닝] RNN을 사용한 자연어 처리: 감성분석

by 기록자_Recordian 2024. 12. 5.
728x90
반응형
이전 내용
 

[딥러닝] RNN을 사용한 자연어 처리

이전 내용 [딥러닝] RNN & CNN(feat. 시카고 교통국 데이터셋) - 3이전 내용  [딥러닝] RNN & CNN(feat. 시카고 교통국 데이터셋) - 2이전 내용 [딥러닝] RNN & CNN(feat. 시카고 교통국 데이터셋) - 1이전 내용

puppy-foot-it.tistory.com


감성 분석

 

이전에 수행했던 IMDb 영화 리뷰 데이터셋을 통해 감성분석을 수행해본다.

 

[머신러닝] 텍스트 분석: 감성 분석

텍스트 분석이란? [머신러닝] 텍스트 분석이전 내용 [머신러닝] 군집화 (Clustering)군집화(Clustering) [군집]군집은 비슷한 샘플을 클러스터 또는 비슷한 샘플의 그룹으로 할당하는 작업으로, 데이

puppy-foot-it.tistory.com

 

 

[딥러닝] 텐서플로 데이터셋 프로젝트

텐서플로 [머신러닝] 텐서플로(TensorFlow)란?텐서플로(TensorFlow)란?  텐서플로(TensorFlow)는 구글에서 개발한 오픈소스 머신러닝 프레임워크이다. 주로 딥러닝 모델을 만들고 학습시키는 데 사용되

puppy-foot-it.tistory.com

탠서플로 데이터셋 라이브러리를 이용하여 IMDb 데이터셋을 로드하고, 훈련 세트의 처음 90%를 훈련에, 나머지 10%를 검증에 사용한다. (필자의 경우, 가상환경에서 시작한 이래 텐서플로 데이터셋을 설치한 적이 없어 먼저 tfds를 설치해줘야 했다.)

# 텐서플로 데이터셋 설치(tfds)
pip install tensorflow-datasets
import tensorflow_datasets as tfds

raw_train_set, raw_valid_set, raw_test_set = tfds.load(
    name="imdb_reviews",
    split=["train[:90%]", "train[90%:]", "test"],
    as_supervised=True
)
tf.random.set_seed(42)
train_set = raw_train_set.shuffle(5000, seed=42).batch(32).prefetch(1)
valid_set = raw_valid_set.batch(32).prefetch(1)
test_set = raw_test_set.batch(32).prefetch(1)

 

몇 개의 리뷰를 확인해본다.

for review, label in raw_train_set.take(4):
    print(review.numpy().decode("utf-8"))
    print("레이블:", label.numpy())

▶ 레이블: 긍정적인 리뷰 - 1, 부정적인 리뷰 - 0

세 번째 리뷰의 경우 부정적인 리뷰임에도 불구하고 긍정적으로 시작된다.

(끝부분의 내용이 "캐나다 시청자들은 이 터무니없는 음모로 즐거운 리퍼 매드니스에 대비하거나 혐오감에 고개를 흔들 수 있습니다." 이다.)


바이트 페어 인코딩(BPE), 구글 SentencePiece

 

이 작업을 위한 모델을 구축하려면 텍스트를 전처리 해야 하는데, 이번에는 문자가 아닌 단어로 자르며, 이를 위해 tf.keras.layers.TextVectorization 층을 사용할 수 있다.

이 층은 단어 경계를 식별하기 위해 공백을 사용하므로 일부 언어에서는 제대로 작동하지 않을 수 있다.

  • 중국어: 단어 사이에 공백 사용 하지 않음.
  • 베트남어: 단어 내에서도 공백 사용
  • 독일어: 공백 없이 여러 단어를 함께 붙이는 경우 있음.
  • 해시태그처럼 공백을 사용하지 않는 경우 있음. (예. #ILoveNewYork)

이러한 문제를 해결하기 위해 2016년 에든버러 대학교에서 부분 단어 수준에서 텍스트를 토큰화하거나 복원하는 몇 가지 방법을 탐구했고, 바이프 페어 인코딩(BPE)이라는 기술을 평가했다. BPE는 전체 훈련 세트를 개별 문자(공백 포함)로 분할한 다음 어휘 사전이 원하는 크기에 도달할 때까지 가장 빈번하게 등장하는 인접 쌍을 반복으로 병합한다.

또한, 2018년 구글의 논문에서는 부분 단어 토큰화를 개선하여 많은 언어에서 토큰화 전에 전처리의 필요성을 제거하였다. 그리고 이 논문에서는 훈련 중에 토큰화에 약간의 무작위성을 도입하여 정확도와 견고성을 향상시키는 부분 단어 규제라는 새로운 규제 기법을 제안했다.

 

◆ 바이트 페어 인코딩 (Byte Pair Encoding, BPE)
바이트 페어 인코딩(BPE)은 언어 모델링에서 사용하는 서브워드 토크나이제이션(subword tokenization) 기법 중 하나이다. BPE는 희귀 단어를 더 작은 단어 부분(subwords)으로 분할하여, 어휘의 크기를 줄이고 희소성을 줄이는 데 효과적이다.

- BPE 알고리즘의 주요 과정:

  • 초기화: 모든 단어를 문자 단위로 분리하여 시작한다. 예를 들어, "word"는 [w, o, r, d]로 시작한다.
  • 병합 반복: 가장 자주 나타나는 문자 쌍을 찾아 하나의 새로운 토큰으로 병합한다. 이 과정은 사전 정의된 횟수(예: 10,000번)만큼 반복된다. 예를 들어, [w, o, r, d]가 [wo, r, d]로 병합될 수 있다.
  • 어휘 생성: 최종적으로 생긴 서브워드 어휘를 사용하여 텍스트를 토큰화한다.

- 장점:

  • 어휘 크기를 제어할 수 있어 메모리 사용을 최적화할 수 있다.
  • 희귀 단어에 대해 효과적으로 작동한다.
  • 새로운 단어에 대해서도 서브워드를 통해 다룰 수 있다.

구글 SentencePiece
구글 SentencePiece는 BPE와 유사한 원리를 기반으로 하는 토크나이제이션 기법이다. SentencePiece는 언어 모델을 학습할 때 유니코드 문자열을 입력으로 받아 서브워드 단위로 분할한다.

- SentencePiece의 특징:

  • 언어 중립: SentencePiece는 입력 텍스트 언어에 종속되지 않으며 어떤 언어의 텍스트에서도 사용할 수 있다.
  • 공백 처리: SentencePiece는 공백을 별도의 토큰으로 간주하여 공백의 유무도 중요하게 다룬다.
  • BPE와 Unigram 언어 모델: SentencePiece는 BPE와 유니그램(Unigram) 언어 모델 두 가지 방법을 지원한다. 유니그램 모델은 서브워드의 확률을 기반으로 최적의 서브워드 분할을 찾는다.
  • 편리한 사용: SentencePiece는 Python 라이브러리로 제공되어 사용하기 매우 편리하다.

그 외에도 텐서플로 텍스트 라이브러리는 Word Piece를 포함한 다양한 토큰화 전략을 구현하고 있다.

허깅 페이스의 Tokenizers 라이브러리도 매우 빠르고 다양한 토크나이저를 제공한다.

 

영어로 된 IMDb 작업의 경우 토큰 경계를 위해 공백을 사용하는 것으로 충분하기 때문에 TextVectorization 층을 생성하고 이를 훈련 세트에 적용해본다. 이 작업에서는 매우 드문 단어가 중요하지 않을 가능성이 높고 어휘 사전의 크기를 제한하면 모델이 학습해야 하는 파라미터 수가 줄어들기 때문에 어휘 사전을 1,000개 토큰으로 제한한다.

Layers = tf.keras.layers
vocab_size = 1000
text_vec_layer = Layers.TextVectorization(max_tokens=vocab_size)
text_vec_layer.adapt(train_set.map(lambda reviews, labels: reviews))

 

이제 모델을 만들고 훈련한다.

embed_size = 128
tf.random.set_seed(42)
model = tf.keras.Sequential([
    text_vec_layer,
    Layers.Embedding(vocab_size, embed_size),
    Layers.GRU(128),
    Layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='nadam',
             metrics=['accuracy'])
history = model.fit(train_set, validation_data=valid_set, epochs=2)

[코드 설명]

1. 설정

embed_size = 128
tf.random.set_seed(42)
  • embed_size = 128: 임베딩 벡터의 크기를 128로 설정한다.
  • tf.random.set_seed(42): 랜덤 시드를 설정하여 향후 결과의 재현성을 보장한다.

2. 모델 정의

model = tf.keras.Sequential([
    text_vec_layer,
    Layers.Embedding(vocab_size, embed_size),
    Layers.GRU(128),
    Layers.Dense(1, activation='sigmoid')
])
  • 모델 구성:
    • tf.keras.Sequential: 모델을 순차적으로 정의하는 클래스.
    • text_vec_layer: 텍스트 데이터를 수치 벡터로 변환하는 레이어. 이 레이어는 텍스트 전처리 과정으로서, 보통 텍스트 벡터화(Text Vectorization) 레이어를 의미한다.
    • Layers.Embedding(vocab_size, embed_size): 임베딩 레이어. 단어를 고정된 크기인 embed_size의 벡터로 매핑한다. vocab_size는 어휘의 크기이다.
    • Layers.GRU(128): GRU(Gated Recurrent Unit) 레이어. 128 유닛을 사용하여 순환 신경망을 구성한다.
    • Layers.Dense(1, activation='sigmoid'): 출력 레이어. 출력 뉴런 하나를 가지며, 활성화 함수로 시그모이드를 사용하여 이진 분류를 수행한다.

3. 모델 컴파일

model.compile(loss='binary_crossentropy', optimizer='nadam', metrics=['accuracy'])
  • 손실 함수:
    • loss='binary_crossentropy': 이진 분류 문제에 적합한 손실 함수로, 예측 값과 실제 값 간의 차이를 계산.
  • 옵티마이저:
    • optimizer='nadam': 나담(Nadam) 옵티마이저를 사용하여 모델을 최적화. 나담은 RMSProp과 Nesterov accelerated gradient를 결합한 옵티마이저이다.
  • 평가지표:
    • metrics=['accuracy']: 모델의 성능을 평가하기 위해 정확도를 사용한다.

4. 모델 학습

history = model.fit(train_set, validation_data=valid_set, epochs=2)
  • 훈련:
    • train_set: 모델을 학습시킬 훈련 데이터셋.
    • validation_data=valid_set: 모델 성능을 검증할 검증 데이터셋.
    • epochs=2: 전체 훈련 데이터를 두 번 반복하여 학습.

이 코드는 텍스트 분류를 위한 신경망 모델을 정의하고, 이를 학습시키는 과정이다. 주어진 텍스트 데이터를 벡터화하고, 임베딩 레이어를 통해 고정 크기의 벡터로 변환한 후 GRU 레이어를 사용하여 순환 신경망을 통해 텍스트를 처리한다. 마지막으로 출력 레이어에서 시그모이드 활성화 함수를 사용하여 이진 분류를 수행한다. 모델은 binary_crossentropy 손실 함수와 nadam 옵티마이저를 사용해, 두 번의 에폭 동안 학습된다.

 

그러나 이 코드를 실행하면 정확도는 50%에 가까워 랜덤한 선택보다 나을 것이 없다.

그 이유는 TextVectorization 층이 리뷰를 토큰 ID의 시퀀스로 변환할 때 리뷰의 길이가 서로 다르기 때문에 짧은 시퀀스를 패딩 토큰(ID 0)으로 패딩하여 배치에서 가장 긴 시퀀스만큼 길게 만든다. 결과적으로 대부분의 시퀀스는 많은 패딩 토큰으로 끝나는데, 종종 수십 개 또는 수백 개의 토큰이 사용된다.

SimpleRNN 층보다 훨씬 나은 GRU 층을 사용하고 있지만 단기 기억은 여전히 좋지 않기 때문에 많은 패딩 토큰을 거치면 결국 리뷰 내용을 잊게 된다.

해결책은 모델에 동일한 길의 문장으로 구성된 배치를 주입하는 것, RNN이 패딩 토큰을 무시하도록 하는 것 두 가지이다.


마스킹

 

모델이 패딩 토큰을 무시하게 만드는 것은 케라스에서 간단한 데, Embedding 층을 만들 때 mask_zero=True 매개변수를 추가하면 된다. 이렇게 하면 이어지는 모든 층에서 ID가 0인 패딩 토큰을 무시한다.

embed_size = 128
tf.random.set_seed(42)
model = tf.keras.Sequential([
    text_vec_layer,
    Layers.Embedding(vocab_size, embed_size, mask_zero=True),
    Layers.GRU(128),
    Layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='nadam',
             metrics=['accuracy'])
history = model.fit(train_set, validation_data=valid_set, epochs=2)

mask_zero=True 매개변수만 추가했을 뿐인데, 정확도가 크게 상승했다.

 

많은 케라스 층이 마스킹을 지원한다.

  • SimpleRNN, GRU, LSTM, Bidirectional, Dense, TimeDistributed, Add 등 (모두 tf.keras.layers 패키지에 있음)
  • Conv1D 등 합성곱 층은 마스킹을 지원하지 않음.

마스킹을 지원하는 사용자 정의 층을 구현하려면 call() 메서드에 mask 매개변수를 추가하고 이 메서드에서 마스크를 사용해야 한다. 또한 마스크를 다음 층으로 전파해야 하는 경우 생성자에서 self.supports_masking=True로 설정해야 한다. 마스크가 전파되기 전에 마스크를 업데이트해야 하는 경우 compute_mask() 메서드를 구현해야 한다.

모델이 Embedding 층으로 시작하지 않는다면 대신 tf.keras.layers.Masking 층을 사용할 수 있다. 기본적으로 이 층은 마스크를 tf.math.reduce_any(tf.math.not_equal(X, 0),axis=1)로 설정하므로 마지막 차원이 모두 0인 타임 스텝은 후속 층에서 마스킹된다.

 

Conv1D 층과 순환 층을 혼합하여 사용하는 경우와 같이 복잡한 모델에서는 함수형 API 또는 서브클래싱 API를 사용하여 마스크를 명시적으로 계산하고 적절한 층에 전달해야 한다.

다음 모델은 함수형 API를 사용하여 구축하고 마스킹을 수동으로 처리한다는 점을 제외하면 이전 모델과 동일하다. 또한 이전 모델이 약간 과대적합 되었으므로 약간의 드롭아웃을 추가한다.

tf.random.set_seed(42)
inputs = Layers.Input(shape=[], dtype=tf.string)
token_ids = text_vec_layer(inputs)
mask = tf.math.not_equal(token_ids, 0)
Z = Layers.Embedding(vocab_size, embed_size)(token_ids)
Z = Layers.GRU(128, dropout=0.2)(Z, mask=mask)
outputs = Layers.Dense(1, activation='sigmoid')(Z)
model = tf.keras.Model(inputs=[inputs], outputs=[outputs])

 

모델을 컴파일하고 훈련해본다.

model.compile(loss="binary_crossentropy", optimizer="nadam",
              metrics=["accuracy"])
history = model.fit(train_set, validation_data=valid_set, epochs=2)

 


사전 훈련된 임베딩과 언어 모델 재사용하기

 

모든 긍정 단어와 모든 부정 단어가 군집을 형성하고 있다면 이는 감성 분석에 도움이 된다. 따라서 단어 임베딩을 훈련하는 대신 구글의 Word2Vec 임베딩, 스탠퍼드의 GloVe 임베딩 또는 메타의 FastText 임베딩과 같이 사전 훈련된 임베딩을 다운로드하여 사용할 수 있다.

그 중 구글 연구 팀이 소개한 모델 아키텍처인 범용 문장 인코더(Universal Setence Encoder)를 기반으로 분류기를 만들어본다. 이 모델은 텐서플로 허브를 통해 사용할 수 있다.

import os
import tensorflow_hub as hub

Layers = tf.keras.layers

# TFHub 캐시 디렉토리 설정
os.environ['TFHUB_CACHE_DIR'] = 'my_tfhub_cache'

# 모델 정의
model = tf.keras.Sequential([
    hub.KerasLayer('https://tfhub.dev/google/universal-sentence-encoder/4',
                   trainable=True, dtype=tf.string, input_shape=[], name='USE_layer'),
    Layers.Dense(64, activation='relu'),
    Layers.Dense(1, activation='sigmoid')
])

# 모델 컴파일
model.compile(loss='binary_crossentropy', optimizer='nadam',
              metrics=['accuracy'])

# 모델 학습
model.fit(train_set, validation_data=valid_set, epochs=10)

[일부 코드 설명]

- TFHub 캐시 디렉토리 설정: 모델 다운로드 시 사용할 캐시 디렉토리를 설정.

- KerasLayer를 통해 Universal Sentence Encoder를 사용. ( 텐서플로 허브 모듈 URL의 마지막 부분은 모델 버전 4를 나타냄.)

- trainable=True 로 지정하면 사전 훈련된 범용 문장 인코더가 훈련 중에 미세 튜닝되고 일부 가중치가 역전파를 통해 수정된다.

- 추출된 특성들을 다룰 Dense 층을 추가:

  • 첫 번째 Dense 층: 유닛 64개, ReLU 활성화 함수.
  • 두 번째 Dense 층: 유닛 1개, 시그모이드 활성화 함수를 사용한 출력층.

이 모델은 크기가 1GB 정도로 상당히 크기 때문에 다운로드하는 데 시간이 걸린다.

훈련 후 이 모델은 90%에 가까운 검증 정확도에 도달한다. 


다음 내용

 

[딥러닝] RNN을 사용한 자연어 처리: 신경망 기계 번역

이전 내용 [딥러닝] RNN을 사용한 자연어 처리: 감성분석이전 내용 [딥러닝] RNN을 사용한 자연어 처리이전 내용 [딥러닝] RNN & CNN(feat. 시카고 교통국 데이터셋) - 3이전 내용  [딥러닝] RNN & CNN(fea

puppy-foot-it.tistory.com


[출처]

핸즈 온 머신러닝

728x90
반응형