본문 바로가기

금융 및 데이터

[Python] 뉴스 감성지수 분류 모델

728x90

https://sieon-dev.tistory.com/4

 

[Python] 포트폴리오 최적화 서비스 제공과 텍스트마이닝을 사용한 금융 데이터 분석

해외의 포트폴리오 최적화 및 백테스트 사이트인 PortfolioVisualizer의 국내화 버전으로 개발하였습니다. 개인의 포트폴리오 최적화와 백테스트 기능을 제공하고 텍스트마이닝을 사용하여 시장, 기

sieon-dev.tistory.com

 

 위 사이트를 개발할 때 사용한 감성지수 분류 모델에 대해 기록을 남겨볼까 합니다.

 

Preview

사실 처음에는 뉴스 데이터를 활용하여 다음날의 주가를 예측하는 모델을 만들고 싶었지만 다양한 방법들을 시도하여도 예측 정확도가 60%대 밖에 나오지 않아 차라리 감성지수를 예측하는 모델을 설계하는 방향으로 바꿨습니다. 

감성지수는 영화 리뷰와 같이 리뷰와 평점이 있는 데이터를 학습시켜 새로운 리뷰가 달렸을 때 해당 리뷰의 감성(감정) 값을 예측하는 모형에 많이 사용됩니다. 이 방법론을 적용하여 뉴스와 뉴스가 나온 다음 날의 주가를 학습시켜 다음날 주가를 예측하는 모델을 만들어보자 했던 것이 처음 시도였고 이게 정확도가 높지가 않아 그럼 뉴스와 뉴스의 감성 분류 값을 학습시키는 모델로 바뀌었던 것이죠.
 
영화 리뷰같은 경우는 사용자가 리뷰를 남김과 동시에 별점을 남기기 때문에 종속변수를 손쉽게 라벨링(긍정이면 1, 부정이면 0)할 수 있지만 뉴스는 그렇지 않아 이 뉴스를 긍정으로 봐야 할지 부정으로 봐야 할지 정하는 것이 정말 애매합니다. 그래서 저는 제가 개발한 방법대로 감성사전을 만들었습니다. 예를 들어 어제 "FOMC 금리 인상 가능성 있어"와 같은 제목의 뉴스가 나왔고 오늘 코스피 종가가 어제보다 올랐으면 "FOMC", "금리", "인상", "가능성" ("있어"는 명사가 아니므로)에 긍정 점수를 부여하고 반대로 떨어졌으면 부정 점수를 부여하는 방식으로 말이죠. 이렇게 만든 감성사전의 모든 단어들의 평균점수보다 새로운 뉴스의 단어들의 평균점수가 높으면 1, 아니면 0으로 라벨링을 한 후 학습을 진행했습니다. 다행히도 이 모델의 감성 분류 예측 정확도를 보니 91%까지 올랐습니다. 

 

1. 데이터 수집

뉴스 데이터는 빅카인즈 에서 경제 섹터의 뉴스를 2017년 1월 1일부터 2021년 3월 30일까지 총 500,194 건을 수집하였습니다. 

 

2. 데이터 전처리

뉴스의 제목만을 사용하기 때문에 날짜와 제목을 제외한 컬럼은 제거 후 야후 파이낸스에서 Kospi 데이터를 다운로드하여 뉴스데이터에 붙였습니다. 뉴스와 뉴스가 나온 다음날 코스피 종가가 전날 대비 상승했으면 1, 아니면 0으로 말이죠. 추가로 불용어를 제거하고 Okt 라이브러리를 사용해서 명사만을 추출한 칼럼을 추가했습니다.

okt = Okt()
n_ = []
title_rename = []
for i in range(len(df)):
  if(i % 10000 ==0):
    print(i,"단계 완료")
  title_rename.append(re.sub("[\(\[].*?[\)\]]", "",df.iloc[i]['제목']))
  n_.append(' '.join(okt.nouns(df.iloc[i]['제목'])))
df['nouns'] = n_
df['제목']=title_rename
df = df[df['nouns']!='']

#2차 불용어 제거
df['제목'] = df['제목'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
df['제목'].replace('', np.nan, inplace=True)
df = df.dropna(how='any')

라벨의 비율 1: 269861 0: 228765

Train : Test의 비율 400000 : 98626 로 분할 후 빈 행 제거

 

 

3. 감성사전 구축

한 글자인 단어는 제거하고 두 글자 이상인 단어들의 점수를 초기화해주었습니다.

총 47229개의 단어가 등장했습니다.

up = 269861
down = 228765
up_ratio = up/(up+down)
down_ratio = down/(up+down)

import collections
for i,w in enumerate(df['nouns']):
    w = w.split(' ')
    if (df.iloc[i]['updown']==1):
        for j in range(len(w)):
            noun = w[j]
            if len(noun)<=1:
              continue
            vocab[noun] = vocab[noun] + down_ratio
    else:
        for j in range(len(w)):
            noun = w[j]
            if len(noun)<=1:
              continue
            vocab[noun] = vocab[noun] - up_ratio

이 처럼 상승비율과 하락비율을 정의해준 다음 라벨 값이 1이면 하락 비율을 각 단어에 더해주고 라벨값이 0이면 상승 비율을 차감해주었습니다. 라벨값이 1이면 상승인데 왜 상승 비율을 더해주지 않았냐면 만약 라벨값이 1인 데이터가 아닌 데이터보다 훨씬 많다면 해당 단어들의 점수가 너무 커져서 점수가 고르지 못한 감성사전이 만들어지기 때문에 정규화를 해준 셈이죠.

 

결과는 이렇습니다.

감성사전

이제 만들어진 감성사전을 활용하여 뉴스의 평균 감성 점수를 산출한 뒤 칼럼에 추가했습니다.

total = []
for i,w  in enumerate(df_train['nouns']):
    sent_score = 0
    w= w.split(' ')
    for j in w:
        if(len(j)<=1):
          continue
        elif(j not in sent_dictionary):
          continue
        else:
          sent_score = sent_score + sent_dictionary[j]
    total.append(sent_score/len(w))
df_train['sent_score'] = total

Train Set

*감성사전을 만들 땐 전체 데이터를 사용하여 만들었기 때문에 어떻게 보면 Train set에 Test set의 정보까지도 들어가 있는 문제가 있습니다. 처음 모델을 만들 땐 Train set만을 사용해서 감성사전을 만들었지만 Test set에 있는 단어들이 들어가지 않는다는 문제점(예를 들어 코로나)이 있어 최종 웹에 Deploy 하기 위한 모델은 전체 데이터를 사용해서 감성사전을 구축했습니다. 

 

이후 sent_score가 감성사전의 평균 점수보다 높으면 1, 낮으면 0으로 새롭게 라벨링을 해주었습니다.

sum = 0
for i in range(len(vocab)):
  sum = sum + list(vocab.values())[i]
sent_mean = sum/len(vocab)

a_ = []
for i in range(len(df_train)):
  if(df_train.iloc[i]['sent_score']>sent_mean):
    a_.append(1)
  else:
    a_.append(0)
df_train['sent_label'] = a_

 

위 과정을 Test set에도 적용하여 데이터 전처리를 완료하였습니다.

 

4. 모델링

모델링하기에 앞서 Colab에서 형태소 분류기인 Mecab 설치를 해줘야 합니다. 앞서 감성사전을 구축할 땐 Okt 라이브러리를 사용했지만 Mecab이 성능이 훨씬 빠르기 때문에 Mecab으로 넘어왔습니다.

#Mecab 설치
!git clone https://github.com/SOMJANG/Mecab-ko-for-Google-Colab.git
%cd Mecab-ko-for-Google-Colab
!bash install_mecab-ko_on_colab190912.sh
!pip install Mecab

모델링에는 Bi-LSTM 모델을 사용하였습니다. Bert도 사용해봤지만 성능면에서는 Bi-LSTM이 미세한 차이로 좋게 나왔습니다. 

 

우선 validation set과 train set을 나눠준 뒤 형태소를 분류하고 X와 Y 값을 지정해줍니다. 

train_data, validation_data = train_test_split(df_train, test_size = 0.2, random_state = 42)
train_data.dropna(how='any',inplace= True)
validation_data.dropna(how='any', inplace=True)
mecab = Mecab() 
train_data['tokenized'] = train_data['제목'].apply(mecab.morphs)
validation_data['tokenized'] = validation_data['제목'].apply(mecab.morphs)
X_train = train_data['tokenized'].values
Y_train = train_data['sent_label'].values
X_vali= validation_data['tokenized'].values
Y_vali = validation_data['sent_label'].values

이후 keras의 Tokenizer를 사용해 텍스트를 시퀀스 벡터로 변환합니다. (모델의 Input으로는 텍스트가 들어갈 수 없기 때문)

#vocab_size는 단어 집합의 크기
tokenizer = Tokenizer(vocab_size, oov_token = 'OOV') 
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_vali = tokenizer.texts_to_sequences(X_vali)

X_train의 형태

일정한 Input 형태를 만들기 위해 적절한 길이를 지정하고 padding 과정으로 마무리합니다.

def below_threshold_len(max_len, nested_list):
  cnt = 0
  for s in nested_list:
    if(len(s) <= max_len):
        cnt = cnt + 1
  print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (cnt / len(nested_list))*100))

max_len = 30
below_threshold_len(max_len, X_train)
X_train = pad_sequences(X_train, maxlen = max_len)
X_vali = pad_sequences(X_vali, maxlen = max_len)

 

이제 모델을 구현하겠습니다.

model = Sequential()
model.add(Embedding(vocab_size, 100))
model.add(Bidirectional(LSTM(100)))
model.add(Dense(1, activation='sigmoid'))

es = EarlyStopping(monitor='val_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='binary_crossentropy', metrics=['acc'])
history = model.fit(X_train, Y_train, epochs=15, callbacks=[es, mc], batch_size=256, validation_split=0.2)

모델 실행결과

 

Validation Set의 예측 정확도는 0.92가 나왔습니다. 이제 Test Set 도 같은 과정을 거쳐 전처리를 한 후 성능을 확인해보겠습니다.

Test Set의 정확도 역시 0.91로 높은 성능을 보이고 있습니다. 

 

 

이후 이 모델을 웹 서버에 Deploy 하여 네이버 News API를 통해 수집한 뉴스들을 분석하는 데 사용하고 있습니다. 많은 분께 도움이 되는 포스트였으면 좋겠습니다. 추후 이 모델을 파이썬 Django에서 어떻게 사용하는지도 포스팅하겠습니다.