_2025.03.23_
Chapter 09. RNN
- 57_RNN 감정분석
감성분석
감성분석 Sentiment Analysis
- 입력된 자연어 안의주관적 의견, 감정 등을 찾아내는 문제
- 이중 문장의 긍정/부정 등을 구분하는 경우가 많음
github에 있는 데이터를 사용해서 실습해보자. github의 주소는 아래와 같다.
GitHub - e9t/nsmc: Naver sentiment movie corpus
Naver sentiment movie corpus. Contribute to e9t/nsmc development by creating an account on GitHub.
github.com
git clone을 통해서 데이터를 내려받고, 자료를 보면.. 한글로 이루어진 데이터를 확인할 수 있다.
문장 뒤 숫자로 구분하는데. 1이면 긍정적 문장이고 0이면 부정적 문장이다. 주로 내용은 영화감상에 대한 내용이다.
실습진행
어떤 내용이 있는지 다시 한번 확인해 본다.
path_to_train_file = './Sentiment_Analysis/nsmc/ratings_train.txt'
path_to_test_file = './Sentiment_Analysis/nsmc/ratings_test.txt'
train_text = open(path_to_train_file, 'rb').read().decode(encoding='utf-8')
test_text = open(path_to_test_file, 'rb').read().decode(encoding='utf-8')
print('Length of text (train) : {} characters'.format(len(train_text)))
print('Length of text (Test) : {} characters'.format(len(test_text)))
print(train_text[:300])
Output
Length of text (train) : 7087272 characters
Length of text (Test) : 2368261 characters
id document label
9976970 아 더빙.. 진짜 짜증나네요 목소리 0
3819312 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 1
10265843 너무재밓었다그래서보는것을추천한다 0
9045019 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 0
6483659 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다 1
5403919 막 걸음마 뗀 3세부터 초등학교 1학년생인 8살용영화.ㅋㅋㅋ...별반개도 아까움. 0
7797
데이터를 보면 정리는 안되어 있다. \t (탭), \r\n (줄바꿈) 등이 있다. 단 줄바꿈은 Window에서는 \r\n 이고, Mac에서는 \n 이다
데이터 정리
우선 줄바꿈 (\r\n)을 기준으로 Split 해 본다.
train_text.split('\r\n')
['id\tdocument\tlabel',
'9976970\t아 더빙.. 진짜 짜증나네요 목소리\t0',
'3819312\t흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나\t1',
'10265843\t너무재밓었다그래서보는것을추천한다\t0',
'9045019\t교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정\t0',
'6483659\t사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다\t1',
'5403919\t막 걸음마 뗀 3세부터 초등학교 1학년생인 8살용영화.ㅋㅋㅋ...별반개도 아까움.\t0',
'7797314\t원작의 긴장감을 제대로 살려내지못했다.\t0',
'9443947\t별 반개도 아깝다 욕나온다 이응경 길용우 연기생활이몇년인지..정말 발로해도 그것보단 낫겟다 납치.감금만반복반복..이드라마는 가족도없다 연기못하는사람만모엿네\t0',
'7156791\t액션이 없는데도 재미 있는 몇안되는 영화\t1',
'5912145\t왜케 평점이 낮은건데? 꽤 볼만한데.. 헐리우드식 화려함에만 너무 길들여져 있나?\t1',
'9008700\t걍인피니트가짱이다.진짜짱이다♥\t1',
'10217543\t볼때마다 눈물나서 죽겠다90년대의 향수자극!!허진호는 감성절제멜로의 달인이다~\t1',
'5957425\t울면서 손들고 횡단보도 건널때 뛰쳐나올뻔 이범수 연기 드럽게못해\t0',
'8628627\t담백하고 깔끔해서 좋다. 신문기사로만 보다 보면 자꾸 잊어버린다. 그들도 사람이었다는 것을.\t1',
'9864035\t취향은 존중한다지만 진짜 내생에 극장에서 본 영화중 가장 노잼 노감동임 스토리도 어거지고 감동도 어거지\t0',
'6852435\tㄱ냥 매번 긴장되고 재밋음ㅠㅠ\t1',
'9143163\t참 사람들 웃긴게 바스코가 이기면 락스코라고 까고바비가 이기면 아이돌이라고 깐다.그냥 까고싶어서 안달난것처럼 보인다\t1',
'4891476\t굿바이 레닌 표절인것은 이해하는데 왜 뒤로 갈수록 재미없어지냐\t0',
.
.
.
.
.
각각의 한줄마다, 어떠한 숫자, + \t + 문장 + \t + 숫자(라벨인듯) 구조로 이루어져 있는 것을 알 수 있다. 그래서 다시 \t 기호로 Split 한다.
train_text.split('\r\n')[:3]
-----------------------------------------------------
['id\tdocument\tlabel',
'9976970\t아 더빙.. 진짜 짜증나네요 목소리\t0',
'3819312\t흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나\t1']
train_text.split('\r\n')[3].split('\t')
-----------------------------------------------------
['10265843', '너무재밓었다그래서보는것을추천한다', '0']
이제 데이터가 어떤 구조로 이루어져있고, 또 어떻게 정리할 수 있는지 파악하였다. 그러면, 각 Train, Test 데이터에 같이 포함되어 있는 라벨(Label)데이터만 따로 분류해야 한다.
import numpy as np
# train에서 label 분리
train_Y = []
for row in train_text.split('\r\n')[1:]:
if row.count('\t') > 0:
train_Y.append([int(row.split('\t')[2])])
train_Y = np.array(train_Y) # 리스트를 Numpy 배열로 변환
# Test에서 label 분리리
test_Y = []
for row in test_text.split('\r\n')[1:]:
if row.count('\t') > 0:
test_Y.append([int(row.split('\t')[2])])
test_Y = np.array(test_Y) # 리스트를 Numpy 배열로 변환
여기서 잠시, 몇 가지 개념설명 하나 보고가면..
1. Tokenization : 자연어를 처리 가능한 최소의 단위로 나누는 것으로 여기서는 띄어쓰기
2. clearning : 불필요한 기호를 제거하는 작업
clearning 작업을 진행하기에 앞서서 다시 한번 정규 표현식( Regular Expression )을 인지하고 가야한다.
그래서 아래처럼 clearning 함수를 설정해보면... (형태소 분석, 띄어쓰기 단위)
## cleaning 함수 설정
import re
def clean_str(string):
string = re.sub(r"[^가-힣A-Za-z0-9(),!?\'\`]", " ", string) # [] 외 문자를 " "으로 대체
string = re.sub(r"\'s", " \'s", string) # ' 문자 포함시키기 위해서 역슬래시 사용
string = re.sub(r"\'ve", " \'ve", string)
string = re.sub(r"n\'t", " n\'t", string)
string = re.sub(r"\'re", " \'re", string)
string = re.sub(r"\'d", " \'d", string)
string = re.sub(r"\'ll", " \'ll", string)
# John's 같은 문자를 John 's 처럼 띄어쓰기로 구분
# 형태소 분석에서 띄어쓰기 단위로 분석하기 위함.
string = re.sub(r"\.{2,}", ".", string)
string = re.sub(r"\.", " . ", string)
string = re.sub(r",", " , ", string)
string = re.sub(r"!", " ! ", string)
string = re.sub(r"\(", " \( ", string)
string = re.sub(r"\)", " \) ", string)
string = re.sub(r"\?", " \? ", string)
string = re.sub(r"\s{2,}", " ", string) # 공백문자 2개 이상은 띄어쓰기 1개로 변경
string = re.sub(r"\'{2,}", "\'", string) # ' 작은따옴표 두개 이상은 1개로 변경
string = re.sub(r"\'", "", string) # ' 한개는 삭제
return string.lower()
훈련용 데이터 확보
결국 위 clean 함수를 활용해서 훈련용 데이터를 확보하면.. 아래와 같다. 일단 데이터 구조는 아래처럼 생겼다.
반복문을 이용해서 아래처럼 정리할 수 있다.
train_text_X = []
for row in train_text.split('\r\n')[1:]:
if row.count('\t') > 0:
train_text_X.append(
row.split('\t')[1]
)
train_text_X[1]
train_text_X_clean = []
for sent in train_text_X:
train_text_X_clean.append(clean_str(sent))
sentence = []
for sent in train_text_X_clean:
sentence.append(
sent.split(" ")
)
# 확인작업
for i in range(5):
print(sentence[i])
---------------------------------------------------------------------------------------------------
['아', '더빙', '진짜', '짜증나네요', '목소리']
['흠', '포스터보고', '초딩영화줄', '오버연기조차', '가볍지', '않구나']
['너무재밓었다그래서보는것을추천한다']
['교도소', '이야기구먼', '솔직히', '재미는', '없다', '평점', '조정']
['사이몬페그의', '익살스런', '연기가', '돋보였던', '영화', '!', '스파이더맨에서',
'늙어보이기만', '했던', '커스틴', '던스트가', '너무나도', '이뻐보였다']
하나의 문장이 몇개의 단어조합으로 이루어져있는지 확인해보면...
import matplotlib.pyplot as plt
sentence_len = [len(str) for str in sentence]
# 한 문장에 몇개의 단어가 있는지
sentence_len.sort()
plt.plot(sentence_len)
plt.show()
대부분의 문장이 40개 단어 이하인 것을 알 수 있다.
데이터 크기 맞추기
- 학습을 위해 네트워크에 입력을 넣을 때 입력 데이터 (Input_shape)는 그 크기가 같아야 한다
- 입력 벡터의 크기를 맞춰야하는데,
- 여기서는 긴 문장은 줄이고, 짧은 문장은 공백으로 채우는 방식을 적용한다
- 15만개의 문장 중에서 대부분이 40단어 이하로 되어있음을 인지하자.
25단어 이하로 셋팅 진행, 단 한 단어가 5글자 이상이면 5글자 까지만 저장.
sentences_new = []
for str in sentence:
sentences_new.append([word[:5] for word in sentence][:25])
sentences = sentences_new
for i in range(10):
print(sentences[i])
['아', '더빙', '진짜', '짜증나네요', '목소리']
['흠', '포스터보고', '초딩영화줄', '오버연기조', '가볍지', '않구나']
['너무재밓었']
['교도소', '이야기구먼', '솔직히', '재미는', '없다', '평점', '조정']
['사이몬페그', '익살스런', '연기가', '돋보였던', '영화', '!', '스파이더맨', '늙어보이기', '했던', '커스틴', '던스트가', '너무나도', '이뻐보였다']
['막', '걸음마', '뗀', '3세부터', '초등학교', '1학년생인', '8살용영화', '별반개도', '아까움', '']
['원작의', '긴장감을', '제대로', '살려내지못', '']
['별', '반개도', '아깝다', '욕나온다', '이응경', '길용우', '연기생활이', '정말', '발로해도', '그것보단', '낫겟다', '납치', '감금만반복', '이드라마는', '가족도없다', '연기못하는']
['액션이', '없는데도', '재미', '있는', '몇안되는', '영화']
['왜케', '평점이', '낮은건데', '\\?', '꽤', '볼만한데', '헐리우드식', '화려함에만', '너무', '길들여져', '있나', '\\?', '']
토크나이징과 패딩 (전처리)
keras 의 preprocessing 모듈에 있는 함수 사용 (pad_sequences)
1. Tokenizer
텍스트 데이터를 토큰화(단어를 숫자로 변환) 하는 도구.
주어진 텍스트에서 단어별(혹은 문자별)로 토큰을 생성하고, 각 토큰에 고유한 정수 ID를 할당.
단어 빈도수를 기반으로 제한된 개수의 단어만 사용할 수도 있습니다.
2. pad_sequences
길이가 다른 시퀀스를 고정된 길이로 맞춰주는 역할을 합니다.
신경망 모델에 입력할 때, 동일한 길이의 벡터로 정리해야 하므로 사용됩니다.
기본적으로 앞쪽을 0으로 패딩(Zero Padding)하지만, padding='post'를 사용하면 뒤쪽을 패딩할 수도 있습니다.
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
tokenizer = Tokenizer(num_words=20000) # 20,000 개 단어를 사용하도록 제한
tokenizer.fit_on_texts(sentences)
train_X = tokenizer.texts_to_sequences(sentences)
train_X = pad_sequences(train_X, padding='post')
train_X[:3]
------------------------------------------------------------
array([[ 25, 884, 8, 5795, 1111, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0],
[ 588, 5796, 6697, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0]])
모델구성 및 학습
import tensorflow as tf
model = tf.keras.Sequential([
tf.keras.layers.Embedding(20000, 300, input_length=25),
tf.keras.layers.LSTM(units=50),
tf.keras.layers.Dense(2, activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.summary()
- 임베딩 레이어 (Embedding layer) : 자연어를 수치환된 정보로 바꾸기 위한 레이어
- 자연어는 시간의 흐름에 따라 정보가 연속적으로 이어지는 시퀸스 데이터
- 영어는 문자 단위, 한글은 문자를 넘어 자소 단위로도 쪼개기도 함. 혹은 띄어쓰기나 형태소로 나누기도 함.
- 여러 단어로 묶어서 사용하는 n-gram 방식도 있음
- 여기에 원핫인코딩까지 포함됨.
# GPU 사용
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
history = model.fit(train_X, train_Y, epochs=5, batch_size=128, validation_split=0.2)
-----------------------------------------------------------------------------------------------------------------------------------------
Epoch 1/5
938/938 [==============================] - 13s 8ms/step - loss: 0.4340 - accuracy: 0.7847 - val_loss: 0.3831 - val_accuracy: 0.8209
Epoch 2/5
938/938 [==============================] - 7s 8ms/step - loss: 0.3268 - accuracy: 0.8462 - val_loss: 0.3817 - val_accuracy: 0.8197
Epoch 3/5
938/938 [==============================] - 7s 8ms/step - loss: 0.2755 - accuracy: 0.8670 - val_loss: 0.4283 - val_accuracy: 0.8132
Epoch 4/5
938/938 [==============================] - 8s 8ms/step - loss: 0.2332 - accuracy: 0.8842 - val_loss: 0.4719 - val_accuracy: 0.8125
Epoch 5/5
938/938 [==============================] - 7s 8ms/step - loss: 0.1992 - accuracy: 0.9003 - val_loss: 0.5627 - val_accuracy: 0.8104
예측 진행
test_sentence = '재미있을 줄 알았는데 완전 실망. 대실망. 너무 졸리고 돈이 아까움'
test_sentence = test_sentence.split(' ')
test_sentences = []
now_sentence = []
for word in test_sentence:
now_sentence.append(word)
test_sentences.append(now_sentence[:])
test_X_1 = tokenizer.texts_to_sequences(test_sentences)
test_X_1 = pad_sequences(test_X_1, padding='post', maxlen=25)
prediction = model.predict(test_X_1)
for idx, sentence in enumerate(test_sentences):
print(sentence)
print(prediction[idx])
기존의 train_Y 라벨값이, 0이 부정이고 1이 긍정이였다. 여기서도 [0, 1]은 긍정, [1, 0]은 부정이다. 예측값은 각각 0이될 확률, 1이될 확률 값이다.
결과를 보면, 처음에는 긍정으로 보고 있었지만, 단어가 지속 될 수록 부정이 높아지는 것을 확인할 수 있다.
'Bootcamp_zerobase > YOLO & RNN' 카테고리의 다른 글
RNN #1 (0) | 2025.03.23 |
---|---|
YOLO #2 (0) | 2025.03.21 |
YOLO #1 (0) | 2025.03.16 |