AI/정책 댓글 반응 NLP

[4] 포털 댓글 감성 분석_1. 순환신경망_2. 모델링 및 예측

eraser 2020. 4. 17. 19:49
반응형

 

 앞 단에 이어 모델링을 진행한다. 긍정, 중립, 부정의 3 class로 각각의 텍스트를 분류하는 작업을 수행한다. RNN과 LSTM의 성능 차이를 보기 위해 초기에 RNN, LSTM 모델을 설계했으며, 시행착오를 통해 여러 층으로 구성된 LSTM, GRU 모델을 구성했다. 본격적으로 파라미터, 층을 조정한 것은 여러 층으로 구성된 LSTM, GRU 모델에 한정하였다.

 

 그리고 20200418 현재, 가장 좋은 정확도를 보인 모델은 양방향 LSTM 층을 적용한 모델이다. 테스트 셋에 대한 정확도는 0.9056이다. 이 모델을 통해 포털 댓글에 대한 감성 분석 분류 작업을 수행한다.

 

 


# 공통 사항

  • 임베딩 : Keras의 Embedding 레이어 사용. 100차원 임베딩.
  • 옵티마이저 : adam.
  • loss 측정 : categorical_crossentropy.
  • mask_zero = True : 0으로 패딩된 값이 학습에 영향을 미치지 않도록 한다.
  • 모니터링 : 검증 세트 손실, early stopping의 patience는 3으로 설정.
  • 출력층 : softmax.

 


 

# RNN 

 

 SimpleRNN 모델을 하나의 층으로 쌓았을 때와 두 개의 층으로 쌓았을 때 어떤 변화가 있는지 알아본다. 

 

# 단층 RNN
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(SimpleRNN(128))
model.add(Dense(3, activation = 'softmax'))

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

# early stopping
ck = ModelCheckpoint(filepath=model_path, monitor='val_loss', verbose=1, save_best_only=True)
es = EarlyStopping(monitor='val_loss', patience=3)

# 모델 확인
print(model.summary())

# 훈련
history = model.fit(X_train, y_train,
                    epochs=20,
                    callbacks = [ck, es],
                    validation_split = 0.25)

 

더보기

 다층으로 구성된 RNN 모델은 다음의 부분만 달라진다. 

 

# 다층 RNN
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(SimpleRNN(128, return_sequences=True))
model.add(SimpleRNN(128, return_sequences = False))
model.add(Dense(3, activation = 'softmax'))

 

 

 

 각각의 경우를 비교한 결과는 다음과 같다.

 

  단층 RNN 다층 RNN
테스트셋 정확도 0.8349 0.8256
학습 시간 5' 9'' 7' 58''
에폭(early stopping) 6(0.52534) 4(0.52245)

 

 가장 눈에 띄는 것은 RNN층을 많이 쌓는다고 정확도가 향상되지 않는다는 점이다. 시퀀스 데이터의 경우 복잡하게 층을 구성한다고 해서 학습이 더 잘 이루어지는 것은 아닌 듯하다. 과적합이 더 빨리 일어날 뿐만 아니라, 증가한 시간에 비해 정확도가 유의미하게 향상되지는 않는다.

 

 


 

# LSTM 

 

 RNN 모델의 단점(vanishing gradient 등)을 개선한 LSTM 모델을 사용해 순환신경망을 구현했다. 앞에서와 마찬가지로 단층 LSTM, 다층 LSTM을 각각 구현했다. 

 

# 단층 LSTM
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(LSTM(128))
model.add(Dense(3, activation = 'softmax'))

 

 

더보기

다층 LSTM의 경우 다음의 부분만 달라진다.

 

# 다층 LSTM
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(LSTM(128, return_sequences=True))
model.add(LSTM(128, return_sequences=False))
model.add(Dense(3, activation = 'softmax'))

 

 

 

 결과를 비교하면 다음과 같다.

 

  단층 LSTM 다층 LSTM
테스트셋 정확도 0.8892 0.8868
학습 시간 9' 42'' 17' 31''
에폭(early stopping) 2(0.34758) 2(0.35598)

 

 두 모델 모두 RNN에 비해서  accuracy가 높다. 확실히 단순 RNN 레이어 모델보다는 LSTM 레이어를 활용한 모델을 구성해야 한다. 다만, 학습 시간은 확실히 늘어난다.

 LSTM 모델 역시, 한 층 더 쌓는다고 해서 분류기의 성능이 향상되지는 않았다.

 

 


 

# GRU

 

 LSTM 모델의 게이트, 출력값 계산 함수 등을 변형한 GRU 모델을 사용해 순환신경망을 구현했다.

 

# 단층 GRU
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(GRU(128))
model.add(Dense(3, activation = 'softmax'))

 

 

더보기

다층 GRU 모델을 구성할 경우, 다음의 부분만 달라진다.

 

# 다층 GRU
model = Sequential()
model.add(Embedding(vocab_size, 100, mask_zero=True))
model.add(GRU(128, return_sequences=True))
model.add(GRU(128, return_sequences=False))
model.add(Dense(3, activation = 'softmax'))

 

 

 

 결과를 비교하면 다음과 같다.

 

  단층 GRU 다층 GRU
테스트셋 정확도 0.8910 0.8880
학습 시간 7' 46'' 14' 20''
에폭(early stopping) 2(0.34948) 2(0.35047)

 

 둘 모두 LSTM에 비해 '전반적으로' 성능이 좋다. 테스트셋 정확도가 향상되었을 뿐더러, 학습 시간도 줄어들었다. RNN, LSTM 모델과 마찬가지로, 층을 더 복잡하게 구성했을 때 성능이 좋아지지는 않았다.

 

 


 

 결론적으로, 순환신경망 모델의 경우 RNN보다는 LSTM, GRU 모델을 사용했을 때 분류기 성능이 가장 좋았다. 또한, 지금의 세팅(이 때, 세팅이라 함은 라벨링부터 모델 변수 설정에 이르기까지 모든 것을 포함하는 의미에서의 '세팅'이다.)에서는 dropout, 활성화 층 등 다른 조정 없이는 층을 더 복잡하게 구성한다고 해서 분류기의 성능이 향상되지는 않는 것으로 보인다. 오히려 학습 소요 시간 등 다른 비용을 생각해 본다면, 단층으로 구성하는 게 더 낫다.

 모든 경우에서 학습을 진행할수록 과적합이 발생한다. loss, accuracy로 그래프를 그려보면, RNN 모델의 경우 에폭이 3, 4 정도일 때 갑자기 테스트 셋에서 loss가 폭발적으로 상승하고 accuracy가 낮아지는 양상이 나타난다. (참고) LSTM, GRU 모델의 경우 테스트 셋에서의 loss, accuracy 모두 큰 변동 없이 유지되는 양상을 보였다. 지금까지의 결과로 판단하면, 에폭 수는 4~5 정도가 적절해 보인다.

 

 


 

# 최종 모델_tune parameters

 

 위의 결과를 바탕으로 모델을 미세 조정하여 테스트 셋에서 가장 좋은 정확도를 보인 모델을 선정하였다. 두 모델 모두 변수가 다르게 적용되었고, 어휘 집합의 수는 threshold를 2로 설정했을 때의 23832개이다. 소수점 셋째 자리 이하 차이는 실제 포털 댓글 데이터에 적용했을 때 큰 차이가 없을 것이라 생각해 둘 모두 기록했다.

 

1)  양방향  LSTM 모델 : 정확도 0.9056 (최고 정확도)

 

 뒤 쪽의 시퀀스 데이터가 앞 쪽에도 영향을 줄 수 있는 시퀀스 데이터의 경우, 양방향 LSTM 모델이 종종 적용된다. 이에 양방향 LSTM 레이어를 사용하고, 앞 쪽에서 과적합이 일어났음을 생각해 순환 dropout을 적용했다.

 

# 모델 생성
model = Sequential()
model.add(Embedding(vocab_size, 100)) # vocab_size : 23832
model.add(Bidirectional(LSTM(128, recurrent_dropout=0.2))
model.add(Dense(3, activation='softmax'))

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

# 모델 저장
model_dir = f'{my_path}/model'
if not os.path.exists(model_dir):
    os.mkdir(model_dir)
model_path = model_dir + "/LSTM_bidirectional.model"

ck = ModelCheckpoint(filepath=model_path, monitor='val_loss', verbose=1, save_best_only=True)
es = EarlyStopping(monitor='val_loss', patience=3)

# 모델 훈련
history = model.fit(X_train, y_train,
                    epochs=20,
                    callbacks = [ck, es],
                    validation_split = 0.2)

 

 

 

 

2)  GRU + relu 활성화 : 정확도 0.9021

 

 앞의 모델보다 테스트 셋 정확도는 낮지만, 훈련 시간이 훨씬 짧다는 점을 고려했을 때 선택해봄직하다. 당장 지금 프로젝트에서 최종 결과는 앞의 모델으로 예측한 데이터를 분석하겠지만, 실제로 현장에서 모델을 사용한다면 이 모델의 성능을 향상시켜 보는 방향으로 고민했을 것이다.

 

# 모델 생성
model = Sequential()
model.add(Embedding(vocab_size, 100)) # vocab_size = 23832
model.add(GRU(128))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(3, activation = 'softmax'))

 

 


 

# 예측

 

 순환신경망 기반 최종 모델은 양방향 LSTM  모델로 선정했다. 저장한 모델과 토크나이저, 불용어 리스트를 통해 각각의 테스트 문장을 만들고 예측을 진행했다. 댓글 데이터임을 고려해 일부러 띄어쓰기를 하지 않은 문장도 테스트했다.

 

mecab = Mecab()
MAX_LEN = 70


def test_sentence(sentence, tokenizer, model_path):
    # tokenizing and padding
    sent = [mecab.morphs(sentence)]
    X = [word for word in sent if not word in stopwords]
    X_input = tokenizer.texts_to_sequences(X)
    X_input = pad_sequences(X_input, maxlen=MAX_LEN)    
    # load best model
    model = load_model(model_path)
    # predict    
    pred = np.argmax(model.predict(X_input))    
    # result
    if pred == 1:
        result = "긍정"
    elif pred == 0:
        result = "중립"
    else:
        result = "부정"
    
    return result

 

 결과는 다음과 같다.

 

문장 결과
주52시간으로 삶이 쾌적해졌어요. 긍정
워라밸이좋아졌 네 요. 긍정
이건 재앙이야ㅡㅡ 부정
돈을 더 못 벌자나 긍정
주52시간 조금만 더 보완하면 되겠네요. 긍정
결국 다 망하자는 거죠. 부정
아이들이랑놀아줄수있는데왜그러는지모르겠네요. 중립
사무직은 좋지만 생산직은? 긍정
배부른 소리하고있네. 중립
적게 일하고 많이 법시다. 중립
이 밤을 우린 어떻게 할까요.... 중립
배고파 중립
배불러 중립

 

 감성어 사전을 기반으로 해서인지, 확실히 사전에 들어 있는 말 위주로 예측이 이루어지는 느낌이다. 특히 "아이들이랑놀아줄수있는데왜그러는지모르겠네요."와 같은 문장의 경우,  '놀다' 혹은 '놀아 주다'가 사전에 수록되어 있지 않아 중립으로 예측되는 결과를 볼 수 있다. 정책의 효과를 판단할 수 있는 문장임에도 OOV 문제로 인해 제대로 확인할 수 없다는 문제를 확인할 수 있는 부분이다.

 한편, "돈을 더 못 벌자나"와 같은 문장은 인간의 판단과 다르게 예측되는 것을 확인할 수 있다.

 


 

 이를 바탕으로 포털 댓글 데이터셋 170만 건에 대해 예측을 진행했다. 총 raw data는 180만 건 정도이지만, threshold를 2로 하여 어휘집합의 수를 제한했기 때문에, 희귀 단어로만 구성된 문장을 제외했기 때문에 실제 예측에 사용된 데이터 건수는 그보다 적었다.

 

# load data for predict
X_pred_raw = prepare_pred(PRED_PATH, mecab=True, filter=True)

# get morphs
stopwords_path = os.path.join(cur_path, stop_name)
X_pred = get_wordlists(X_pred_raw['document'], filter=True, file_path=stopwords_path) # dataframe : need column indexing.

# tokenize data
X_pred, drop_pred = tokenize(X_pred, fit_tok)

# check empty data
empty_pred = X_pred_raw[X_pred_raw.index.isin(drop_pred)]

# delete data
X_pred = np.delete(X_pred, drop_pred, axis=0)

# pad data
X_pred = pad_sentences(X_pred, max_len)

# predict with data
%%time
prediction = pred_data(BEST_MODEL, X_pred)

 

 

 

반응형