Recent Posts
Recent Comments
Archives
반응형
250x250
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Today
Yesterday

Total
05-03 01:47
관리 메뉴

Hey Tech

[NLP] Pretrained 언어모델 기반 한국어 경제 뉴스 기사 감정 분류 본문

AI & 빅데이터/자연어처리(NLP)

[NLP] Pretrained 언어모델 기반 한국어 경제 뉴스 기사 감정 분류

Tony Park 2022. 5. 31. 22:51
728x90
반응형

📚 목차

1. 프로젝트 개요
2. 개발환경
3. Pretrained 언어 모델
4. 경제 뉴스 기사 감정 데이터셋
5. 전체 소스코드
6. 코드 설명

1. 프로젝트 개요

2. 개발환경

3. Pretrained 언어 모델

4. 경제 뉴스 기사 감정 데이터셋

  • 영문 데이터셋인 Finance Phrase Bank를 한국어로 번역하고 육안 검수한 데이터셋(출처: Github @ukaria777)
  • 데이터 개수: 경제 뉴스 기사 내 4,846개 문장(그림 1 참고)
  • 감정 라벨
    • 0: Neutral 59.27%
    • 1: Positive 28.22%
    • 2: Negative 12.51%

그림 1. 데이터셋 샘플

5. 전체 소스코드

Google Colab Pro 환경에서 작성한 소스코드 및 데이터셋은 개인 Github에 업로드해 두었습니다. 참고하시길 바랍니다.

 

GitHub - park-gb/financial-news-sentiment-classifier: 한국 경제 뉴스 감정 분류(Sentiment Classification)

한국 경제 뉴스 감정 분류(Sentiment Classification). Contribute to park-gb/financial-news-sentiment-classifier development by creating an account on GitHub.

github.com

6. 코드 설명

👨‍💻들어가며

본 소스코드를 통해 얻어지는 분석 결과 이미지, 데이터셋, 모델은 구글 드라이브 내 소스파일이 있는 위치에서 각 파일이 해당하는 폴더로 저장하도록 작성되었습니다(아래의 그림 2 참고) 예를 들어, 분석 결과 이미지는 figure, 모델은 model, 폴더에 저장됩니다. 따라서 구글 드라이브 연동을 하신 후 소스파일이 있는 위치에서 각 해당하는 폴더를 생성하시거나, 파일 생성 경로를 수정하신 후 코드를 활용하시길 권장합니다.

그림 2. 파일 구성

1) 구글 드라이브 연동

코랩 소스파일에서 구글 드라이브를 연동(mount)합니다.

import os
from google.colab import drive
drive.mount('/content/drive/')

아래와 같은 출력문이 나왔다면 정상적으로 mount 된 것입니다.

Mounted at /content/drive/

2) 디렉토리 변경

Colab 노트북 초기 시작 시 노트북이 위치한 디렉토리인 Colab Notebook 위치로 디렉토리를 변경합니다. 이는 추후에 학습한 모델을 저장하거나 분석 결과를 파일로 구글 드라이브에 저장하기 위함입니다.

cd /content/drive/MyDrive/Colab Notebooks

3) 패키지 설치

3-1) Hugging Face 패키지 설치

사전 학습 언어 모델과 tokenizer를 편리하게 불러올 수 있는 Hugging Face 활용을 위한 패키지를 설치합니다.

!pip install transformers

3-2) RAdam 패키지 설치

본 프로젝트에서는 Optimizer로써 Rectified Adam을 사용할 예정입니다.

!pip install tensorflow_addons

4) 패키지 import

import os
import pandas as pd
import numpy as np
import re
from tqdm import tqdm
import urllib.request
import seaborn as sns
import matplotlib.pyplot as plt
import tensorflow_addons as tfa
import tensorflow as tf
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from transformers import BertTokenizer, TFBertForSequenceClassification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, \
                            roc_auc_score, confusion_matrix, classification_report, \
                            matthews_corrcoef, cohen_kappa_score, log_loss

5) 언어모델 및 Tokenizer 불러오기

Hugging Face에서 KLUE BERT-base 모델과 tokenizer를 불러옵니다.

MODEL_NAME = "klue/bert-base"
model = TFBertForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=3, from_pt=True)
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)

6) GPU 작동 확인

GPU는 Colab 상단 메뉴바-Runtime-Change Runtime type-Hardware Accelator 순서대로 접근한 후 선택 가능합니다.

device_name = tf.test.gpu_device_name()
if device_name == '/device:GPU:0':
  print("GPU 작동 중")
  mirrored_strategy = tf.distribute.MirroredStrategy()
else:
  print("GPU 미작동 중")

7) 경제 뉴스 감정 데이터셋 다운로드

Github @Ukairia777 님 리포에 올라와 있는 경제 뉴스 감정 데이터셋을 다운로드합니다.

DATASET_URL = "https://raw.githubusercontent.com/ukairia777/finance_sentiment_corpus/main/finance_data.csv"
DATASET_NAME = "finance_data.csv"
urllib.request.urlretrieve(DATASET_URL, 
                           filename = DATASET_NAME
                           )

데이터가 잘 받아졌는지 확인해 봅니다.

dataset = pd.read_csv(DATASET_NAME)
dataset.head()

아래의 그림 3과 같이 데이터 프레임이 출력된다면 데이터셋을 정상적으로 다운로드하신 겁니다.

그림 3. 경제 뉴스 감정 데이터셋 일부

8) 데이터 전처리

8-1) 영어 뉴스 기사 칼럼 삭제

한국어 데이터만 사용할 것이기 때문에 영문 데이터를 포함한 칼럼은 삭제합니다.

del dataset['sentence']

8-2) 라벨 데이터 숫자 치환

replace 함수라벨을 리스트에 담아 활용하면 간편하게 라벨을 숫자로 치환할 수 있습니다(그림 4).

dataset['labels'] = dataset['labels'].replace(['neutral', 'positive', 'negative'],[0, 1, 2])
dataset.head()

그림 4. 라벨 데이터 숫자 치환

8-3) 결측치 화인

결측치 존재 여부를 확인합니다.

dataset.info()

아래와 같이 두 칼럼 모두 결측치가 없는 것을 알 수 있습니다(그림 5).

그림 5. 결측치 존재 여부 확인

8-4) 중복 데이터 확인

중복된 뉴스 기사가 있는지 확인합니다.

# 중복 데이터 확인
dataset[dataset['kor_sentence'].duplicated()]

아래 그림 6과 같이 중복 데이터가 많이 존재하는 것을 확인할 수 있습니다.

그림 6. 중복 데이터 조회

8-5) 중복 데이터 제거

중복도니 뉴스 기사를 제거합니다. drop_duplicates 메소드를 활용하면 특정 칼럼에서 중복된 값을 가지는 행을 제거할 수 있습니다.

DATASET_PREP_FILE = './data/dataset_prep.csv'
# 중복 데이터 제거
dataset.drop_duplicates(subset = ['kor_sentence'], inplace = True)
dataset.to_csv(DATASET_PREP_FILE) # 구글 드라이브 내 data 폴더에 저장

9) 라벨별 데이터 분포 확인

라벨별 데이터 분포를 막대 그래프로 시각화합니다.

9-1) 라벨별 데이터 개수 확인

먼저 라벨별 데이터 개수 시각화입니다. 데이터프레임의 특정 칼럼이나 행을 기준을 설정하고 value_counts() 메서드를 활용하면 특정 기준별 데이터 개수를 산출해 줍니다.

LABEL_NUM_FILE = './figure/label_number.png'
dataset['labels'].value_counts().plot(kind = 'bar')
plt.xlabel("Label")
plt.ylabel("Number")
plt.savefig(LABEL_NUM_FILE) # 구글 드라이브 내 figure 폴더에 저장

아래 그림 7과 같이 중립적인 뉴스 기사문이 가장 많고, 긍정과 부정적 뉴스 기사문으로 데이터가 분포하는 것을 알 수 있습니다.

그림 7. 라벨별 데이터 개수 확인

9-2) 라벨별 데이터 비율 확인

라벨별 데이터 분포를 비율로 확인해 보겠습니다. 앞서 살펴본 value_counts() 메서드에서 normalize 옵션True로 설정하면 각 데이터별 비율을 산출합니다.

dataset['labels'].value_counts(normalize = True)

아래의 그림 8과 같이, 중립적인 기사문이 전체 중 59.27%, 긍정적인 기사문이 28.22%, 부정적인 기사문이 12.51%를 차지하는 것을 알 수 있습니다.

그림 8. 라벨별 데이터 비율

10) Train/Test 데이터 분리

먼저, 입력 데이터와 라벨 데이터를 분리합니다.

# 입출력 데이터 분리
X_data = dataset['kor_sentence']
y_data = dataset['labels']

Train과 Test 데이터는 8:2로 분리하겠습니다. 여기서 strtify 옵션(계층적 데이터 추출 옵션)에 라벨 데이터(y_data)를 입력하면 데이터 분리 이전의 라벨별 비율을 고려하여 데이터를 분리합니다. 이는 본 프로젝트와 마찬가지로 Classification 문제에서 권장되는 옵션입니다.

TEST_SIZE = 0.2 # Train: Test = 8 :2 분리
RANDOM_STATE = 42
X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, 
                                                    test_size = TEST_SIZE, 
                                                    random_state = RANDOM_STATE, 
                                                    stratify = y_data)

실제로 데이터 비율을 유지하는지 확인해 봅니다.

print(f"훈련 입력 데이터 개수: {len(X_train)}")
print(f"테스트 입력 데이터 개수: {len(X_test)}")

데이터 개수로 확인하면 다음과 같습니다.

훈련 입력 데이터 개수: 3861
테스트 입력 데이터 개수: 966

비율로 확인해 보겠습니다.

# 훈련 데이터 라벨별 비율
y_train.value_counts(normalize = True)

먼저 훈련 데이터의 경우 아래의 그림 9와 같이 데이터 분포가 정해졌습니다.

그림 9. 훈련 데이터의 라벨별 데이터 비율

이어서 테스트 데이터의 경우도 확인해 봅니다.

# 테스트 데이터 라벨별 비율
y_test.value_counts(normalize = True)

아래의 그림 10과 같이 훈련 데이터와 테스트 데이터가 라벨별 데이터 비율이 거의 일치하는 것을 확인할 수 있습니다.

그림 10. 테스트 데이터의 라벨별 데이터 비율

11) BERT 입력용 데이터 포맷으로 변경

입력 데이터를 BERT 입력용 포맷으로 변경해야 합니다. 먼저, 입력 데이터의 총길이를 64로 제한하겠습니다.

# 입력 데이터(문장) 길이 제한
MAX_SEQ_LEN = 64

BERT의 입력은 token, segment, position으로 구성됩니다. 여기서 token은 텍스트 데이터를 tokenize하고 인덱스 번호를 부여한 것을 말합니다. segment는 문장의 앞뒤 관계를 구분하기 위한 용도입니다. 본 프로젝트에서 사용하는 입력 데이터가 모두 최대 한 문장이므로 segment는 사실상 모두 0이라고 볼 수 있습니다. position은 이름 그대로 단어의 위치를 의미합니다. 이러한 BERT 입력의 구성 요소를 고려하기 위한 함수를 아래와 같이 작성하였습니다.

def convert_data(X_data, y_data):
    # BERT 입력으로 들어가는 token, mask, segment, target 저장용 리스트
    tokens, masks, segments, targets = [], [], [], []
    
    for X, y in tqdm(zip(X_data, y_data)):
        # token: 입력 문장 토큰화
        token = tokenizer.encode(X, truncation = True, padding = 'max_length', max_length = MAX_SEQ_LEN)
        
        # Mask: 토큰화한 문장 내 패딩이 아닌 경우 1, 패딩인 경우 0으로 초기화
        num_zeros = token.count(0)
        mask = [1] * (MAX_SEQ_LEN - num_zeros) + [0] * num_zeros
        
        # segment: 문장 전후관계 구분: 오직 한 문장이므로 모두 0으로 초기화
        segment = [0]*MAX_SEQ_LEN

        tokens.append(token)
        masks.append(mask)
        segments.append(segment)
        targets.append(y)

    # numpy array로 저장
    tokens = np.array(tokens)
    masks = np.array(masks)
    segments = np.array(segments)
    targets = np.array(targets)

    return [tokens, masks, segments], targets

위의 함수를 활용하여 훈련 데이터와 테스트 데이터를 BERT 입력 포맷에 맞게 변환해 줍니다.

# train 데이터를 Bert의 Input 타입에 맞게 변환
train_x, train_y = convert_data(X_train, y_train)
# test 데이터를 Bert의 Input 타입에 맞게 변환
test_x, test_y = convert_data(X_test, y_test)

12) BERT 기반 감정 분류 모델링

이제 BERT를 활용하여 감정 분석 모델을 만들어 봅니다.

12-1) BERT 입력 정의

입력을 먼저 정의합니다.

# token, mask, segment 입력 정의
token_inputs = tf.keras.layers.Input((MAX_SEQ_LEN,), dtype = tf.int32, name = 'input_word_ids')
mask_inputs = tf.keras.layers.Input((MAX_SEQ_LEN,), dtype = tf.int32, name = 'input_masks')
segment_inputs = tf.keras.layers.Input((MAX_SEQ_LEN,), dtype = tf.int32, name = 'input_segment')
bert_outputs = model([token_inputs, mask_inputs, segment_inputs])

12-2) BERT 출력 정의

BERT 출력을 정의합니다.

bert_output = bert_outputs[0]

12-3) 감정 분류 모델 컴파일

드롭아웃을 설정하고 감정 분류 모델의 output layer를 설정합니다. 3가지 감정 중 1가지 감정으로 텍스트를 분류하기 때문에 다중 클래스 분류 문제입니다. 이에, output layer의 활성화 함수는 softmax를 사용하였습니다.

DROPOUT_RATE = 0.5
NUM_CLASS = 3
dropout = tf.keras.layers.Dropout(DROPOUT_RATE)(bert_output)
# Multi-class classification 문제이므로 activation function은 softmax로 설정
sentiment_layer = tf.keras.layers.Dense(NUM_CLASS, activation='softmax', kernel_initializer = tf.keras.initializers.TruncatedNormal(stddev=0.02))(dropout)
sentiment_model = tf.keras.Model([token_inputs, mask_inputs, segment_inputs], sentiment_layer)

옵티마이저는 Rectified Adam을 사용하였습니다.

# 옵티마이저 Rectified Adam 하이퍼파리미터 조정
OPTIMIZER_NAME = 'RAdam'
LEARNING_RATE = 5e-5
TOTAL_STEPS = 10000
MIN_LR = 1e-5
WARMUP_PROPORTION = 0.1
EPSILON = 1e-8
CLIPNORM = 1.0
optimizer = tfa.optimizers.RectifiedAdam(learning_rate = LEARNING_RATE,
                                          total_steps = TOTAL_STEPS, 
                                          warmup_proportion = WARMUP_PROPORTION, 
                                          min_lr = MIN_LR, 
                                          epsilon = EPSILON,
                                          clipnorm = CLIPNORM)

모델을 컴파일합니다. 본 프로젝트에서는 라벨이 0, 1, 2 중 하나를 예측하는 모델을 사용하기 때문에 손실함수로는 Sparse Categorical Crossentropy를 사용하였습니다.

# 감정분류 모델 컴파일
sentiment_model.compile(optimizer = optimizer, 
                        loss = tf.keras.losses.SparseCategoricalCrossentropy(), 
                        metrics = ['accuracy'])

Early Stopping을 추가하였습니다. patience 옵션을 5로 설정함으로써 모델 학습 시 성능이 개선되지 않는 횟수가 5회를 초과하면 학습을 멈추도록 구현하였습니다.

MIN_DELTA = 1e-3
PATIENCE = 5

early_stopping = EarlyStopping(
    monitor = "val_accuracy", 
    min_delta = MIN_DELTA,
    patience = PATIENCE)

모델 학습 증 최고 성능이 나왔을 때만 모델이 저장되도록 checkpoint에 save_best_only 옵션을 True로 설정하였습니다. 테스트 데이터의 손실값이 최소가 되도록 만드는 것이 목표이기 때문에 monitor 변수는 val_loss를, mode는 최솟값을 의미하는 min을 설정하였습니다.

# 최고 성능의 모델 파일을 저장할 이름과 경로 설정
BEST_MODEL_NAME = './model/best_model.h5'
model_checkpoint = ModelCheckpoint(
    filepath = BEST_MODEL_NAME,
    monitor = "val_loss",
    mode = "min",
    save_best_only = True, # 성능 향상 시에만 모델 저장
    verbose = 1
)

Early Stop과 Model Checkpoint를 콜백 함수로 설정합니다.

callbacks = [early_stopping, model_checkpoint]

12-4) 감정 분류 모델 학습

100번의 Epoch 동안 32개의 배치 크기에서 모델을 학습시킵니다.

EPOCHS = 100
BATCH_SZIE = 32

sentiment_model.fit(train_x, train_y, 
                    epochs = EPOCHS, 
                    shuffle = True, 
                    batch_size = BATCH_SZIE, 
                    validation_data = (test_x, test_y),
                    callbacks = callbacks
                    )

저는 17번의 Epoch 도중 학습이 조기종료되었습니다(그림 11).

그림 11. 감정 분류 모델 학습 중 성능 모니터링

13) 감정 분류 모델 성능 평가

학습 중 최고 성능을 보인 모델을 불러옵니다.

# 최고 성능의 모델 불러오기
sentiment_model_best = tf.keras.models.load_model(BEST_MODEL_NAME,
                                                  custom_objects={'TFBertForSequenceClassification': TFBertForSequenceClassification})

테스트용 입력 데이터를 기반으로 최고 성능의 모델로 라벨을 예측합니다.

# 모델이 예측한 라벨 도출
predicted_value = sentiment_model_best.predict(test_x)
predicted_label = np.argmax(predicted_value, axis = 1)

Multi-class 평가 지표 중 하나인 분류 리포트를 출력합니다.

# Classification Report 저장
CL_REPORT_FILE = "./metric/cl_report.csv"

cl_report = classification_report(test_y, predicted_label, output_dict = True)
cl_report_df = pd.DataFrame(cl_report).transpose()
cl_report_df = cl_report_df.round(3)
cl_report_df.to_csv(CL_REPORT_FILE)
print(cl_report_df)


분류 정확도는 85.2%, F1 점수는 0.851로 나왔습니다(그림 12).

그림 12. 분류 리포트 결과

Confusion Matrix(혼동 행렬)로 모델의 예측과 정답 간의 차이를 시각화하면 아래 그림 13과 같습니다. 데이터를 분류하기 전과 유사한 비율을 갖는 라벨 예측 결과가 나타났습니다.

# 이미지 파일 저장정보
FIGURE_FILE = "./figure/cf_matrix.png"

cf_matrix = confusion_matrix(test_y, predicted_label)
fig, ax = plt.subplots(figsize = (8,6))
sns.heatmap(cf_matrix, annot = True, fmt = 'd')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.savefig(FIGURE_FILE)
plt.show()

그림 13. Confusion MAtrix

Multi-class Classification 시 성능 평가를 위하여 주로 활용되는 평가지표를 활용하였습니다. Accuracy, Precion, Recall, F1과 함께, Cohen's Kappa Coeffient, Matthews_corrcoef, Log Loss 등 다중 분류 문제에서 주로 활용되는 평가 지표를 활용하였습니다.

accuracy_score_v = round(accuracy_score(y_test, predicted_label), 3) # Accuracy
precision_score_v = round(precision_score(y_test, predicted_label, average="weighted"), 3) # Precision
recall_score_v = round(recall_score(y_test, predicted_label, average="weighted"), 3) # Recall
f1_score_v = round(f1_score(y_test, predicted_label, average="weighted"), 3) # F1 Score
roc_auc_score_v = round(roc_auc_score(y_test, predicted_value, average="weighted", multi_class="ovr"), 3) # ROC AUC Score
cohen_kappa_score_v = round(cohen_kappa_score(y_test, predicted_label), 3) # Cohen's Kappa Score
matthews_corrcoef_v = round(matthews_corrcoef(y_test, predicted_label), 3) # Matthew's Correlation Coefficient
log_loss_v = round(log_loss(y_test, predicted_value), 3) # Log Loss

평가지표별로 Key-Value 형태의 Dictionary에 저장하고, 종합한 평가지표는 데이터프레임으로 저장합니다.

# 평가지표 결과 저장
METRIC_FILE = './metric/metric.csv'

metric_total = pd.DataFrame({
    'PLM': MODEL_NAME,
    'Optimizer': OPTIMIZER_NAME,
    'Accuracy': accuracy_score_v,
    'Precision': precision_score_v,
    'Recall': recall_score_v,
    'F1_score': f1_score_v,
    'ROC_AUC_score': roc_auc_score_v,
    'Cohen_kappa_coef': cohen_kappa_score_v,
    'Matthews_corrcoef': matthews_corrcoef_v,
    'Log_loss': log_loss_v},
    index = ['-']
    )

종합적인 평가 지표는 엑셀로 저장합니다.

metric_total.to_csv(METRIC_FILE)

평가 결과는 아래와 같습니다.

Metric Score
Accuracy 0.852
Precision 0.851
Recall 0.852
F1 Score 0.851
ROC AUC Score 0.920
Cohen's Kappa Coefficient 0.728
Matthews Correlation Coefficient 0.729
Log Loss 0.555

Last Updated @2022-05-31


오늘은 사전 학습 언어 모델인 KLUE BERT-base과 오픈소스 데이터를 활용하여 한국 경제 뉴스 기사의 감정 분류 프로젝트에 대해 알아봤습니다. 포스팅 내용에 오류가 있거나 보완할 점이 있다면 아래에 댓글 남겨주시면 감사드리겠습니다. 그럼 오늘도 즐겁고 행복한 하루 보내시길 바랍니다. 고맙습니다 :)

728x90
반응형
Comments