DACON 발화자의 감정인식

DACON 대회: 월간 데이콘 발화자의 감정인식 AI 경진대회

대회 제출 코드: dacon.io/codeshare


모델 선택

모델 선택에서 가장 중요한 포인트는 '사전 학습'이었습니다. 대회 규칙 상 외부 데이터를 학습할 수 없기 때문에 '사전 학습된 모델을 얼마나 잘 활용하는가'에 따라 결과가 달라질 거라고 생각했습니다. 그래서 huggingface에서 찾을 수 있는 bert, roberta, t5 등등을 기반으로 감정 데이터셋을 사전 학습한 여러 모델을 테스트해보았습니다.(i.e. sibert) 그중, EmoBERTa(tae898)가 압도적으로 좋은 성능을 보였습니다. 또 유일하게 EmoBERTa가 현재 데이터셋과 동일한 레이블을 가지고 있었고, 이 때문에 가장 좋은 성능을 보인 것이라고 생각합니다. 


레이블 인코딩

EmoBERTa 모델의 장점을 최대로 활용하기 위해 우선 레이블을 확인해 보았습니다. tae898/erc/utils.py에서 학습된 레이블을 확인할 수 있었고, 이와 동일하게 학습될 수 있도록 Label-Encoder를 생성해 주었습니다. 비교를 위해 sklearn에서 제공하는 LabelEncoder를 활용해 랜덤하게 레이블을 지정해 주고, 직접 만든 Encoder와 비교해 주었습니다. 당연한 결과지만, EmoBERTa에 맞게 직접 만든 Label-Encoder가 확연히 더 좋은 성능을 보였습니다. 


발화 문장 전처리

데이터를 살펴봤을 때 대부분 20 단어 이내의 문장이었고, 2 ~ 5개의 단어로 구성된 문장도 다수 포함되어 있었습니다. 

발화 문장의 경우, 특수 문자가 많이 포함되어 있는 구어체 문장으로 이루어져 있었습니다. didn't와 같은 축약형에서 사용되는 어퍼스트로피(')도 두 종류가 섞여서 사용되고 있는 등 불균일한 모습을 보였습니다. 그뿐 아니라 Aaaaaaawwwww나 Oh-oh-oh-oh-oh와 같이 동일한 패턴의 문자가 반복적으로 사용되는 단어도 포함되어 있었습니다. 

따라서 시도해본 전처리는 아래와 같습니다. 

  • 유사한 특수문자 통일 (i.e. ")
  • 소문자로 통일
  • TweetTokenizer 활용
  • 불용어(stopwords) 제거
  • 반복되는 표현 제거 (i.e. Oh-oh-oh-oh-oh  →  Oh)
  • 축약 표현 복원 (i.e. didn't  →  did not)
  • 의미 없는 특수 문자 제거 (i.e. ' : Ok'  →  'Ok')
  • 표제어 추출(lemmatization)

우선, 표제어 추출이나 불용어 제거와 같이 정보의 손실이 많은 경우는 성능이 크게 떨어졌습니다. 

"""
       원문: I didn't break the cup!!!
축약어 복원: I did not break the cup!!!
불용어 제거: I break cup !!!
"""

>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("roberta-base")
>>> tokenizer.tokenize("I didn't break the cup!!!")
['I', 'Ġdidn', "'t", 'Ġbreak', 'Ġthe', 'Ġcup', '!!!']
>>> tokenizer.tokenize("I break cup !!!")
['I', 'Ġbreak', 'Ġcup', 'Ġ', '!!!']

보는 것처럼 축약어 복원 + 불용어 제거가 만나며 문장의 의미 자체가 바뀌어 버리는 상황도 발생했습니다. 

"""
       원문: I did not break the cup!!!
표제어 추출: I do not break the cup!!!
"""

>>> tokenizer.tokenize("I do not break the cup!!!")
['I', 'Ġdo', 'Ġnot', 'Ġbreak', 'Ġthe', 'Ġcup', '!!!']

표제어 추출도 마찬가지입니다. "제가 컵 안 깼어요!!!" "저는 컵 안 깹니다!!!"는 분명 다른 상황에서 사용될 수 있는 표현이라고 생각합니다. 실제 이러한 전처리를 거쳐 학습된 모델은 좋지 않은 성능을 보였습니다. 

그 외 전처리도 유의미한 차이를 보이지는 못했습니다. 다만, 현재 데이터셋에 대하여 TweetTokenizer는 약간의 성능 향상을 보였습니다. 결론적으로 허무하게도 원본 데이터를 최대한 유지하는 것이 중요했습니다. 


모델 구조

모델 구조도 변형해가며 시도해 보았습니다. 우선 연속적인 대화 데이터이기 때문에 RNN 계열의 모델을 결합해 사용하면 좋지 않을까 하여 아래와 같은 상황을 테스트해 보았습니다. 

  • 모델 전체 학습
  • Classifier 층(Linear ~ Softmax)만 학습
  • Classifier 층 대신 GRU 결합 후 학습

모델 전체 학습은 Github에 공개된 코드처럼 일반적인 학습 방식을 말합니다. 

그리고 해당 모델은 분류 모델이기 때문에 RoBERTa + Classifier의 형태를 가지고 있습니다. 따라서 RoBERTa 부분은 학습되지 않도록 하고, 마지막 Classifier 층의 가중치만 추가로 학습시켜 보았습니다. 학습된 모델과 해결하고자 하는 문제가 동일하기 때문에 이 방식도 효과가 있을 것이라고 판단했습니다. 

for name, param in emoberta.named_parameters():
    if not str(name).startswith("classifier"):
        param.requires_grad = False

 

마지막으로 문맥을 파악하기 위해 Classifier 대신 GRU를 사용하였습니다. 이미 유사한 구조를 사용해보았던 "Sentiment Analysis With Ensemble Hybrid Deep Learning Model"에서 제시한 값들과 optimizer를 참고해 학습하였습니다. 대신 이 경우, 데이터를 랜덤하게 섞지 않고 발화 순서를 유지하며 입력될 수 있도록 처리하였습니다. 

결론적으로 현재 데이터에 대해서는 전체를 학습시키는 방법이 가장 좋은 성능을 보였고, Classifier만 학습시킨 모델도 준수한 성능을 보였습니다. 


기타 요소

성능에 가장 큰 영향을 미치는 요소는 Batch size였습니다. Batch size를 8, 16, 32로 테스트 해보았고, 메모리 문제로 인해 Gradient Accumulation을 적용해 8*8, 16*8, 16*16과 같이 키워가며 테스트 해보았습니다. Batch size는 32 정도가 최적의 결과를 보였고, Gradient Accumulation을 적용한 경우 16*16과 32*8에서 가장 좋은 성능을 보였습니다. (제가 실행한 환경은 단일 Batch size로 32가 한계였습니다... 이후 64 이상으로도 실행해 보았지만 성능 향상은 없었습니다.)

Learning rate는 3e-5, 1e-5, 1e-6, 1e-7처럼 점점 감소시키며 학습되는 과정을 확인해 보았고, 특이하게도 2e-7 정도의 아주 작은 값으로 학습할 때 학습이 잘 되었습니다. 많은 경우 1e-5 ~ 3e-5 정도에서 최적의 성능을 보인다고 합니다.

Optimizer는 transformers에서 제공하는 AdamW를 사용했고, 그 외 요소는 유의미한 영향을 확인하지 못했습니다. 


Test Score와 Evaluation Score 차이

실제 대회 측에서 계산하는 test set의 f1-score와 자체적으로 training set을 분리해 측정한 eval set의 f1-score가 많게는 0.1점까지도 차이가 났습니다. 이 때문에 Label Smoothing이나 Weight Decay와 같은 값들을 적용해 보았지만 test set에 대한 성능이 개선되지 않았습니다. 

test set을 살펴보면 "Quoi?"나 "oui"와 같은 프랑스어 표현이 포함되어 있었습니다. 사용한 모델은 영어 데이터만 학습하였기 때문에 프랑스어를 잘 예측하지 못한 것이 아닌가 추측해 보았습니다. (test set의 정답 레이블이 없어 정확한 원인은 파악하지 못했습니다.) 하지만 test set의 특징을 보고 직접적인 전처리를 하면 Data Leakage 문제가 있기 때문에 별다른 처리는 하지 못하였습니다. 


프로젝트에 대한 생각

이번 학기에 전공으로 '자연어 처리' 수업을 들으며 NLP를 처음 접했고, transformer 계열의 모델을 처음 다루다 보니 하루 종일 삽질만 하기도 했습니다. 처음에는 생각 없이 이것저것 만져보다가 결국 뭐가 뭔지 기억이 안 나서 다시 처음부터 새로 시작하기도 했습니다. 다행히 뒤로는 정신 차리고 변경한 내용들을 하나하나 기록해가며 어떤 요소가 얼마나 영향을 주었는지 비교해 나갔습니다. 앞으로는 base 모델부터 차근차근 기록해가며 값을 조정해야겠구나라는 것을 뼈저리게 느꼈습니다... 또 이번에 너무 Private score(DACON 대회 중 공개되는 점수)에 집착하다보니 큰 그림을 그리지 못한 것 같다는 아쉬움도 있습니다. 그래도 모델을 학습하며 발생한 문제들을 해결하기 위해 Label Smootiong이나 Gradient Accumulation와 같은 새로운 개념들도 알게 되어서 배운 게 많은 프로젝트가 된 것 같습니다. 

무엇보다도 대회가 종료되고 다른 분들의 코드를 보며 배운 게 가장 큽니다. 제가 생각하지 못한 아이디어를 적용하거나 제가 몰랐던 테크닉을 사용해 데이터나 모델을 처리한 것을 보고 새롭게 알게 된 것들이 많았습니다. 궁금하신 분들은 'DACON 코드 공유'를 확인해 보시길 바랍니다.