Professional Field

ETL : Airflow / DW(Snowflake) | AI : Python | BI : Tableau / Power BI

Portfolio

[ML/DL] NLP GRU를 이용한 해외영상 댓글 감정분석

K_CY 2024. 7. 18. 22:07

NLP를 이용한 감정분석을 해보기 위해 Kaggle에서 Comment에 관련된 데이터셋을 확보하였다.

https://www.kaggle.com/datasets/nipunarora8/most-liked-comments-on-youtube

 

MOST LIKED COMMENTS ON YOUTUBE

Youtube Comments Dataset

www.kaggle.com

 

좋아요가 많이 달린 영상에 댓글을 가져온 것인데 대부분의 댓글의 어떤 내용이 들어가 있으면 좋아요를 많이 받을 수 있을까라는 주제로 분석을 하려고 한다. 

 

먼저 데이터셋을 불러와서 COMMENT 컬럼 데이터를 VADER의 SentimentIntensityAnalyzer() 라이브러리로 감성분석을 진행해 positive / negative / neutral 를 나누어보았다.

sid = SentimentIntensityAnalyzer()
# 감성 분석을 위한 함수 정의
def analyze_sentiment(text):
    scores = sid.polarity_scores(text)
    if scores['compound'] >= 0.05:
        return 'positive'
    elif scores['compound'] <= -0.05:
        return 'negative'
    else:
        return 'neutral'

# 각 텍스트에 대해 감성 분석 수행
train_X['sentiment'] = train_X['Comment'].apply(analyze_sentiment)

# 결과 출력
print(train_X)

 

문장 텍스트에 특수문자와 숫자들은 의미가 없어 보여 제거를 진행하였다. 

from konlpy.tag import Okt
import re
# Okt 형태소 분석기
okt = Okt()

# 특수 문자 제거 함수
def remove_special_characters(tokens):
    cleaned_tokens = [
        token for token in tokens 
        if not re.fullmatch(r'a{1,5}', token, re.IGNORECASE)
    ]
    cleaned_tokens = [re.sub(r'[^\w\s]|[\d]', '', token) for token in cleaned_tokens]
    return ' '.join(cleaned_tokens)

# 텍스트 전처리
sentences['processed_text'] = sentences['Comment'].apply(remove_special_characters)

 

훈련 데이터와 테스트 데이터를 7:3 비율로 구분해주고, 훈련 데이터에 분포도를 확인해보았다.

train_data, test_data = train_test_split(sentences, test_size=0.2, random_state=97)
print("Train Reviews : ", len(train_data))
print("Test_Reviews : ", len(test_data))
train_data['sentiment'].value_counts().plot(kind='bar')
print(train_data.groupby('sentiment').size().reset_index(name = 'count'))

중립적인 표현이 많으며, 부정보단 긍정이 많은 것을 보인다.

 

이후 sentiment 범주형 변수를 숫자형으로 바꿔주었다.

# sentiment 컬럼의 값을 변경
train_data['sentiment'] = train_data['sentiment'].replace({'positive': 1, 'negative': 0, 'neutral': 2})

 

(예외사항)

특수문자나 숫자를 제거했지만 단어 토크나이징에서 의미없는 단어들이 함께 남아있어, 추가 토큰화를 진행해주었다.

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

# NLTK의 stopwords와 punkt tokenizer를 다운로드합니다.
nltk.download('punkt')
nltk.download('stopwords')

stop_words = set(stopwords.words('english'))

def tokenizing(sentence):
    try:
        tokens = word_tokenize(sentence)
        words = [word for word in tokens if word.lower() not in stop_words]
    except ValueError as e:
        print(e)
        words = []  # 예외가 발생할 경우 빈 리스트를 기본값으로 설정
    return words

# train_data와 test_data에 tokenizing 적용
train_data['tokenized'] = train_data['processed_text'].apply(tokenizing)
print("train finished")
test_data['tokenized'] = test_data['processed_text'].apply(tokenizing)
print("test finished")

최종 tokenized

 

코멘트의 sentiment별 단어 깊이와 빈도를 확인해보자.

negative_words = np.hstack(train_data[train_data.sentiment == 0]['tokenized'].values)
positive_words = np.hstack(train_data[train_data.sentiment == 1]['tokenized'].values)
neutral_words = np.hstack(train_data[train_data.sentiment == 2]['tokenized'].values)

negative_word_count = Counter(negative_words)
print(negative_word_count.most_common(20))
positive_word_count = Counter(positive_words)
print(positive_word_count.most_common(20))
neutral_word_count = Counter(neutral_words)
print(neutral_word_count.most_common(20))

negative
positive
neutral

이후 토큰화 된 리뷰(Comment)컬럼의 토근 개수로 평균 길이를 확인해보았다.

길이를 구하는 이유는 데이터의 구성을 파악하기 위함이다. 평균 길이가 길다면 섬세하거나 설명하는 글이 많다는 뜻이고, 짧다면 간단한 의견을 남긴다는 뜻이다.

fig,(ax1,ax2, ax3) = plt.subplots(1,3,figsize=(10,5))
text_len = train_data[train_data['sentiment']==1]['tokenized'].map(lambda x: len(x))
ax1.hist(text_len, color='red')
ax1.set_title('Positive Reviews')
ax1.set_xlabel('length of samples')
ax1.set_ylabel('number of samples')
print('긍정 리뷰의 평균 길이 :', np.mean(text_len))

text_len = train_data[train_data['sentiment']==0]['tokenized'].map(lambda x: len(x))
ax2.hist(text_len, color='blue')
ax2.set_title('Negative Reviews')
fig.suptitle('Words in texts')
ax2.set_xlabel('length of samples')
ax2.set_ylabel('number of samples')
print('부정 리뷰의 평균 길이 :', np.mean(text_len))


text_len = train_data[train_data['sentiment']==2]['tokenized'].map(lambda x: len(x))
ax3.hist(text_len, color='green')
ax3.set_title('Neutral Reviews')
fig.suptitle('Words in texts')
ax3.set_xlabel('length of samples')
ax3.set_ylabel('number of samples')
print('중립 리뷰의 평균 길이 :', np.mean(text_len))
plt.show()

분석결과, 짧은 댓글들이 압도적으로 많다.

 

 

 

이제 텍스트 데이터를 수치형 데이터로 전환하기 위해 정수인코딩은 각 단어를 고유한 정수로 매핑하고, 고유한 정수 인덱스를 부여한다. 장점은 구현이 쉽고 메모리 사용량이 적다.

 

임베딩은 대표적으로 TF-IDF, Word2Vec, GloVe, BERT 가 존재한다. 단어를 고차원 공간의 벡터로 변환시키는 방법이다.

장점은 단어간의 의미적 유사성을 반영할 수 있다.

 

단점은 서로의 장점을 가지고 있지 않다.

 

나는 감정분석으로 댓글 토큰간의 유사성을 보지 않을 것이기 떄문에 정수인코딩을 진행하겠다. 

#정수인코딩
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
print(len(tokenizer.word_index))
threshold=2
total_cnt = len(tokenizer.word_index)
rare_cnt = 0
total_freq = 0
rare_freq = 0

for key, value in tokenizer.word_counts.items():
    total_freq = total_freq + value
    
    if value < threshold:
        rare_cnt += 1
        rare_freq = rare_freq + value

print("Size of Vocabulary :", total_cnt)
print(f'등장빈도가 {threshold-1}번 이하인 희귀 단어의 수 : {rare_cnt}')
print("단어 집합에서 희귀 단어의 비율 : ", (rare_cnt / total_cnt)*100)
print('전체 등장 빈도에서 희귀 단어 등장 빈도 비율 :', (rare_freq/total_freq)*100)
Size of Vocabulary : 19841
등장빈도가 1번 이하인 희귀 단어의 수 : 10031
단어 집합에서 희귀 단어의 비율 :  50.556927574215
전체 등장 빈도에서 희귀 단어 등장 빈도 비율 : 2.8475155561611483

 

짧은 댓글들을 분석한 결과, 단어 중 등장빈도가 threshold 값인 2회 미만. 즉, 1회인 단어들은 단어 집합에서 56% 존재

하지만, 실제로 훈련 데이터에서 등장 빈도로 차지하는 비중은 2.8%로 중요하지 않다고 판단하여 제거

등장 빈도수가 1인 단어들의 수를 제외한 단어의 개수를 단어 집합의 최대 크기로 제한

vocab_size = total_cnt - rare_cnt + 2
print('단어 집합의 크기 : ', vocab_size)

# 단어 지비합의 크기 제한
단어 집합의 크기 :  9812

 

OOV 기법을 통해서 모델이 학습하지 않은 단어가 입력으로 들어왔을 때 아래와 같은 방법을 사용할 수 있다.

 

  • UNK 토큰 사용: 학습 데이터에 없는 모든 단어를 특별한 토큰, 보통 <UNK>로 대체합니다. 이 방법은 간단하지만, 모든 OOV 단어를 동일하게 처리하므로 정보 손실이 큽니다.
  • 서브워드 분리: 단어를 더 작은 단위로 분리하여 처리합니다. Byte Pair Encoding(BPE) 또는 WordPiece 알고리즘을 사용해 단어를 서브워드로 나누어 학습합니다. 이렇게 하면 새로운 단어라도 서브워드 조합을 통해 이해할 수 있게 됩니다.
  • 임베딩 확장: 사전 학습된 임베딩(예: Word2Vec, GloVe)을 사용할 때, OOV 단어를 이웃 단어들의 임베딩을 평균하거나 다른 방법으로 생성된 벡터로 대체합니다.
  • Character-level 모델링: 단어를 문자 단위로 모델링합니다. RNN이나 CNN 같은 모델을 사용하여 단어의 철자 패턴을 학습합니다. 이 방법은 새로운 단어에 대해 더 유연하게 대응할 수 있습니다.
  • Contextual Embeddings: BERT, GPT-3 같은 모델은 문맥을 고려하여 단어 임베딩을 생성합니다. 이런 모델들은 OOV 문제를 덜 겪습니다. 모델이 단어의 문맥을 통해 새로운 단어의 의미를 추론할 수 있기 때문입니다.

 

나는 1번 UNK 토큰을 사용해서 OOV 문제를 해결하려고 한다. (UNK = OOV 명칭만 변경)

tokenizer = Tokenizer(vocab_size, oov_token='OOV')
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)

 

이후 패딩 작업을 진행해야한다.

패딩작업은 모델에 들어가는 문장의 길이가 일치해야 학습을 시킬 수 있기 때문에 패딩을 통해 모든 시퀀스의 길이를 동일하게 맞춰줘야 한다.

 

먼저, 리뷰의 최대 길이를 확인해준다.

print('리뷰의 최대 길이 :',max(len(review) for review in X_train))
print('리뷰의 평균 길이 :',sum(map(len, X_train))/len(X_train))
plt.hist([len(review) for review in X_train], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
리뷰의 최대 길이 : 1602
리뷰의 평균 길이 : 22.815544041450778

 

def below_threshold_len(max_len, nested_list):
    count = 0
    for sentence in nested_list:
        if len(sentence) <= max_len:
            count += 1
    print(f'길이가 {max_len}이하인 샘플의 비율 {(count)/len(nested_list)*100}')
    
max_len = 1472
below_threshold_len(max_len, X_train)
길이가 1472이하인 샘플의 비율 99.99352331606217

 

최대 길이가 1602인데, 1472로 패딩할 경우 99.99%의 길이를 갖기 때문에 훈련용 리뷰를 길이 1472이하로 패딩한다.

 

GRU를 통해 감성분석을 시도하고자 한다. GRU란 딥러닝 RNN에 한 종류로, LSTM과 유사하게 장기 의존성 문제를 해결할 수 있는 기법이다. 

#GRU 사용법

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import GRU, Dense

# 모델 정의
model = Sequential()
model.add(GRU(128, input_shape=(timesteps, input_dim)))
model.add(Dense(1, activation='sigmoid'))

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

# 모델 요약 출력
model.summary()

 

나는 하이퍼파라미터 중 임베딩 벡터 차원을 100으로 설정하고, 은닉층은 128개로 설정을 하겠다.

이진 분류 문제의 경우, 출력층에 로지스틱 회귀를 사용해야 하므로 활성화 함수로는 시그모이드 함수를 사용하고, 손실 함수로 크로스 엔트로피 함수를 사용해야한다. 또한, 로스도 binary_크로스엔트로피를 사용해야한다.

 

하지만 나는 긍정 및 부정 외의  중립이라는 분류 문제가 있기 떄문에 활성화 함수로 시그모이드 대신 소프트맥스를 사용하고 로스도 categorical_crossentropy를 사용했다. 또한, 배치크기는 64로 설정하고, 에포크는 5까지 돌려보도록 하겠다. (사양이슈 에포크는 밑에서 언급하겠지만 데이터 손실이 일어나면 자동으로 정지하도록 설정하였다.)

 

추가 출력층 설정을 해야 한다. 이진분류에 경우는 출력층이 0과 1사이의 값을 출력하여 특정 클래스를 분류하는데 반면,

다중분류에 사용하는 소프트맥스는 확률의 합이 1이 되도록 하기때문에 {0:부정, 1:긍정, 2:중립}으로 되어 있는 Y_train 값을 원핫인코딩 시켜주어야 한다.

Y_train = to_categorical(Y_train, 3)
Y_test = to_categorical(Y_test, 3)

 

[python] 이진 분류에서는 출력층에 하나의 뉴런을 사용하고 활성화 함수로 시그모이드를 사용하여 0 또는 1 중 하나의 클래스를 예측합니다. 그러나 다중 클래스 분류에서는 클래스의 수만큼 출력 뉴런이 필요하며, 각각의 뉴런은 특정 클래스의 확률을 나타냅니다. 다중 클래스 분류에서 출력층은 다음과 같은 이유로 여러 개의 뉴런을 가집니다:

  1. 각 클래스별 확률 출력: 다중 클래스 분류 문제에서 출력층의 뉴런 수는 예측하려는 클래스의 수와 동일해야 합니다. 각 뉴런은 특정 클래스에 대한 확률을 출력합니다.
  2. 소프트맥스 활성화 함수 사용: 소프트맥스 함수는 출력값을 확률로 변환하고, 이 확률의 합이 1이 되도록 합니다.

from tensorflow.keras.layers import Embedding, Dense, GRU
from tensorflow.keras.models import Sequential
from tensorflow.keras.models import load_model
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

# 하이퍼파라미터
embedding_dim = 100
hidden_units = 128

# 모델 정의
model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(GRU(hidden_units))
model.add(Dense(3, activation='softmax')) #분류 3가지

# 콜백
es = EarlyStopping(monitor='var_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

# 모델 컴파일
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
# 모델 훈련
history = model.fit(X_train, Y_train, epochs=15, callbacks=[es, mc], batch_size=64, validation_split=0.2)

** EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)는 검증 데이터 손실(val_loss)이 증가하면, 과적합 징후므로 검증 데이터 손실이 4회 증가하면 정해진 에포크가 도달하지 못하였더라도 학습을 조기 종료(Early Stopping)한다는 의미입니다.

** ModelCheckpoint를 사용하여 검증 데이터의 정확도(val_acc)가 이전보다 좋아질 경우에만 모델을 저장합니다. validation_split=0.2을 사용하여 훈련 데이터의 20%를 검증 데이터로 분리해서 사용하고, 검증 데이터를 통해서 훈련이 적절히 되고 있는지 확인합니다. 검증 데이터는 기계가 훈련 데이터에 과적합되고 있지는 않은지 확인하기 위한 용도로 사용됩니다.

 

모델을  best_model.h5 에 저장하였다.(저장된 모델 불러오기)

from keras.models import load_model

# 저장된 모델 불러오기
best_model = load_model('best_model.h5')

# 불러온 모델로 예측 수행
predictions = best_model.predict(X_test)

 

 

모델 결과, 에포크를 15번 돌렸고 시간은 3시간이 걸렸다. 

loss와 모델의 정확도는 에포크를 돌릴수록 더 좋아졌다.

Epoch 1/15
193/193 [==============================] - ETA: 0s - loss: 0.7727 - acc: 0.6638WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 1: val_acc improved from -inf to 0.76878, saving model to best_model.h5
193/193 [==============================] - 389s 2s/step - loss: 0.7727 - acc: 0.6638 - val_loss: 0.5994 - val_acc: 0.7688
Epoch 2/15
193/193 [==============================] - ETA: 0s - loss: 0.4890 - acc: 0.8198WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 2: val_acc improved from 0.76878 to 0.82545, saving model to best_model.h5
193/193 [==============================] - 399s 2s/step - loss: 0.4890 - acc: 0.8198 - val_loss: 0.4910 - val_acc: 0.8255
Epoch 3/15
193/193 [==============================] - ETA: 0s - loss: 0.3658 - acc: 0.8761WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 3: val_acc improved from 0.82545 to 0.84326, saving model to best_model.h5
193/193 [==============================] - 419s 2s/step - loss: 0.3658 - acc: 0.8761 - val_loss: 0.4559 - val_acc: 0.8433
Epoch 4/15
193/193 [==============================] - ETA: 0s - loss: 0.3014 - acc: 0.9020WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 4: val_acc improved from 0.84326 to 0.85395, saving model to best_model.h5
193/193 [==============================] - 418s 2s/step - loss: 0.3014 - acc: 0.9020 - val_loss: 0.4545 - val_acc: 0.8540
Epoch 5/15
193/193 [==============================] - ETA: 0s - loss: 0.2608 - acc: 0.9186WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 5: val_acc did not improve from 0.85395
193/193 [==============================] - 408s 2s/step - loss: 0.2608 - acc: 0.9186 - val_loss: 0.4805 - val_acc: 0.8475
Epoch 6/15
193/193 [==============================] - ETA: 0s - loss: 0.2333 - acc: 0.9288WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 6: val_acc did not improve from 0.85395
193/193 [==============================] - 430s 2s/step - loss: 0.2333 - acc: 0.9288 - val_loss: 0.4689 - val_acc: 0.8491
Epoch 7/15
193/193 [==============================] - ETA: 0s - loss: 0.2095 - acc: 0.9356WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 7: val_acc did not improve from 0.85395
193/193 [==============================] - 423s 2s/step - loss: 0.2095 - acc: 0.9356 - val_loss: 0.4944 - val_acc: 0.8436
Epoch 8/15
193/193 [==============================] - ETA: 0s - loss: 0.1863 - acc: 0.9442WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 8: val_acc did not improve from 0.85395
193/193 [==============================] - 402s 2s/step - loss: 0.1863 - acc: 0.9442 - val_loss: 0.5204 - val_acc: 0.8491
Epoch 9/15
193/193 [==============================] - ETA: 0s - loss: 0.1713 - acc: 0.9493WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 9: val_acc did not improve from 0.85395
193/193 [==============================] - 404s 2s/step - loss: 0.1713 - acc: 0.9493 - val_loss: 0.5272 - val_acc: 0.8394
Epoch 10/15
193/193 [==============================] - ETA: 0s - loss: 0.1552 - acc: 0.9540WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 10: val_acc did not improve from 0.85395
193/193 [==============================] - 393s 2s/step - loss: 0.1552 - acc: 0.9540 - val_loss: 0.5480 - val_acc: 0.8423
Epoch 11/15
193/193 [==============================] - ETA: 0s - loss: 0.1401 - acc: 0.9579WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 11: val_acc did not improve from 0.85395
193/193 [==============================] - 387s 2s/step - loss: 0.1401 - acc: 0.9579 - val_loss: 0.5883 - val_acc: 0.8287
Epoch 12/15
193/193 [==============================] - ETA: 0s - loss: 0.1310 - acc: 0.9615WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 12: val_acc did not improve from 0.85395
193/193 [==============================] - 383s 2s/step - loss: 0.1310 - acc: 0.9615 - val_loss: 0.6112 - val_acc: 0.8361
Epoch 13/15
193/193 [==============================] - ETA: 0s - loss: 0.1151 - acc: 0.9668WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 13: val_acc did not improve from 0.85395
193/193 [==============================] - 384s 2s/step - loss: 0.1151 - acc: 0.9668 - val_loss: 0.6221 - val_acc: 0.8238
Epoch 14/15
193/193 [==============================] - ETA: 0s - loss: 0.1061 - acc: 0.9687WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 14: val_acc did not improve from 0.85395
193/193 [==============================] - 394s 2s/step - loss: 0.1061 - acc: 0.9687 - val_loss: 0.6777 - val_acc: 0.8235
Epoch 15/15
193/193 [==============================] - ETA: 0s - loss: 0.0947 - acc: 0.9723WARNING:tensorflow:Early stopping conditioned on metric `var_loss` which is not available. Available metrics are: loss,acc,val_loss,val_acc

Epoch 15: val_acc did not improve from 0.85395
193/193 [==============================] - 396s 2s/step - loss: 0.0947 - acc: 0.9723 - val_loss: 0.6968 - val_acc: 0.8232

 

아까 데이터 셋을 train_test로 나눈 test set 으로 모델을 테스트 진행한다.

loaded_model = load_model('best_model.h5')
print('테스트 정확도 : %.4f' % (loaded_model.evaluate(X_test, Y_test)[1]))
121/121 [==============================] - 27s 221ms/step - loss: 0.4039 - acc: 0.8681
테스트 정확도 : 0.8681

 

테스트 정확도는 87% 이다. 파라미터를 다르게 조정해서 테스트했다면 더 좋은 성능이 나왔으리라 생각한다.

def sentiment_predict(new_sentence: str):
    # 특수 문자를 제거하여 한글만 남깁니다.
    new_sentence = re.sub(r'[^ㄱ-ㅎㅏ-ㅣ가-힣]', '', new_sentence)
    # 토크나이징 함수를 호출합니다.
    new_sentence = tokenizing(new_sentence)
    # 문장을 인코딩합니다.
    encoded = tokenizer.texts_to_sequences([new_sentence])
    # 패딩을 적용합니다.
    pad_new = pad_sequences(encoded, maxlen=max_len)
    # 모델을 사용하여 예측합니다.
    predictions = loaded_model.predict(pad_new)
    # 소프트맥스 출력이므로, 세 클래스에 대한 확률이 배열로 반환됩니다.
    score = predictions[0]

    # 각 클래스의 확률을 가져옵니다.
    negative_prob = score[0]
    neutral_prob = score[1]
    positive_prob = score[2]

    # 가장 높은 확률을 가진 클래스를 찾습니다.
    if max(score) == positive_prob:
        print("{:.2f}% 확률로 긍정 리뷰입니다.".format(positive_prob * 100))
    elif max(score) == neutral_prob:
        print("{:.2f}% 확률로 중립 리뷰입니다.".format(neutral_prob * 100))
    else:
        print("{:.2f}% 확률로 부정 리뷰입니다.".format(negative_prob * 100))

정수인코딩을 진행해서인지 문장의 흐름을 읽지 못하는 것 같다. 정수인코딩 이후 임베딩까지 진행을 했다면 문장에 대해 훈련이 되었을 것 같다. 단어를 넣었을 때는 긍정/부정/중립이 분리가 되는 것을 보인다. 또한, 검증 데이터 셋이 로스가 올라가는 현상이 보인다. 과적합이 된 것 같은데 원인은 아마 데이터셋이 적거나 토큰화를 진행할 때 의미없는 단어들을 많이 제외했어야 했을 것 같다. 

 

포트폴리오를 작성하게 된 이유는 NLP 중 감성분석을 직접적으로 경험해보고 싶어 코딩을 해보았고, 다음번에는 임베딩기법을 사용해서 한국어 리뷰를 분석해보는 시간을 갖으려고 한다.

  • 임베딩 (Embedding): 단어를 고차원 벡터 공간으로 매핑하여 의미적 유사성을 반영하는 방법. 주로 신경망 기반의 학습된 모델을 사용.
    • 예: Word2Vec, GloVe, FastText

임베딩은 단어의 의미와 문맥을 이해해야 하는 더 복잡한 NLP 작업에 적합하다.

 

 출처:

https://roothyo.tistory.com/78

 

유튜브 댓글 감성 분석하기 (Sentiment Analysis) - (1)

1. 개발환경 세팅하기 저는 로컬에서 miniconda로 가상환경을 생성하고, 프로젝트를 진행하였습니다. 형태소 분석기로는 이전 프로젝트를 진행했을 때, 비교적 효과가 좋았던 kiwipiepy(https://github.com

roothyo.tistory.com

https://wikidocs.net/59427

 

05-02 소프트맥스 회귀(Softmax Regression) 이해하기

앞서 로지스틱 회귀를 통해 2개의 선택지 중에서 1개를 고르는 이진 분류(Binary Classification)를 풀어봤습니다. 이번 챕터에서는 소프트맥스 회귀를 통해 3개 이…

wikidocs.net

https://jominseoo.tistory.com/46

 

[파이썬 머신러닝 완벽 가이드] Chapter 8 | 텍스트 분석 (비지도학습 기반 감성 분석)

1. 비지도학습 기반 감성분석 소개 감성 어휘 사전 'Lexicon' 기반 감성 사전은 긍정, 부정 감성의 정도를 나타내는 감성 지수를 가지고 있음 NLP의 WordNet 은 시맨틱 (문맥상 의미) 분석을 제공하는

jominseoo.tistory.com

https://da-journal.tistory.com/entry/%ED%85%8D%EC%8A%A4%ED%8A%B8-%EA%B0%90%EC%84%B1-%EB%B6%84%EC%84%9D-Sentiment-Analysis

 

[텍스트] 감성 분석 (Sentiment Analysis)

감성 분석 (Sentiment Analysis) 문서의 주관적인 감성/의견/감정/기분 등을 파악하기 위한 방법. 소셜 미디어, 여론 조사, 온라인 리뷰, 피드백 등 다양한 분야에서 활용. 지도 학습 기반의 분석 지도

da-journal.tistory.com