daniel7481의 개발일지

[AI Tech P Stage] Semantic Text Similarity 대회 본문

Project

[AI Tech P Stage] Semantic Text Similarity 대회

daniel7481 2022. 11. 3. 21:42
반응형

문장 간 유사도 측정(STS)

문장 두 개가 주어지면 문장 간의 유사도를 0~5까지의 실수로 label한 값을 예측하는 대회였다. 평가지표로는 Pearson 상관계수가 주어졌고, label과 2.5 이하면 0, 이상이면 1인 binary 값이 주어졌다.

데이터에 대한 설명은 다음과 같다

  • 총 데이터 개수 : 10,974 문장 쌍
    • Train 데이터 개수: 9,324
    • Test 데이서 개수: 1,100
    • Dev 데이터 개수: 550
  • Label 점수: 0 ~ 5사이의 실수
    • 5점 : 두 문장의 핵심 내용이 동일하며, 부가적인 내용들도 동일함
    • 4점 : 두 문장의 핵심 내용이 동등하며, 부가적인 내용에서는 미미한 차이가 있음
    • 3점 : 두 문장의 핵심 내용은 대략적으로 동등하지만, 부가적인 내용에 무시하기 어려운 차이가 있음
    • 2점 : 두 문장의 핵심 내용은 동등하지 않지만, 몇 가지 부가적인 내용을 공유함
    • 1점 : 두 문장의 핵심 내용은 동등하지 않지만, 비슷한 주제를 다루고 있음
    • 0점 : 두 문장의 핵심 내용이 동등하지 않고, 부가적인 내용에서도 공통점이 없음

피어슨 상관계수

우리의 prediction 값 X와 실제 타겟 데이터 Y에 대하여 피어슨 상관계수를 구하는 task가 주어졌다. 부가 설명으로는 정확한 값을 예측하는 accuracy가 중요한 것이 아니라, 높은 값은 확실히 높게, 낮은 값은 확실히 낮게 예측해야하는 것이 중요한 조건이었다.

2주간의 기간은 절대 길지 않기에 빠르게 도전해야 했다. 먼저 강의들을 전부 수강한 후에 팀원들과 회의를 했다.

분업

우리 팀은 총 3가지 팀으로 나뉘었는데, 모델팀, 데이터팀, EDA팀이었다. 모델, 데이터 팀에 2명씩 붙고 EDA팀을 한 명이 담당했다. 나는 개인적으로 베이스라인 코드를 이해하고, 건드려보고, 바꿔보고 새로운 시도를 해보고 싶어서 모델팀에 자원했다. 

 

타임라인

너무 간단하긴 하지만 전체적인 타임라인을 만들어봤다. 먼저 우리가 생각한 것은 EDA를 완료한 후 데이터 전처리 / Feature를 알아낸 후 데이터 팀에서 이를 토대로 데이터 증강 및 전처리를 하고, 데이터적인 요소가 완료되면 다양한 모델을 돌려보며 실험을 해보는 식으로 진행하였다. 물론 나는 모델을 담당해서 데이터 팀에서 진행한 모든 내용을 알기는 어렵다. 모델 부분에서 바꾼 점을 중점으로 설명하겠다.

 

 

환경 설정 및 서버 git 연결

네이버 부캠 측에서 모두에게 V100을 한 장씩 지급해주었다. 처음에는 이게 무슨 의미인가 싶었지만, 있고 없고는 정말 하늘과 땅 차이다. aistages에서 서버를 할당 받았는데 이를 로컬에서 땡겨오기 위해 vscode의 remote ssh를 사용하였다. 연결하는 부분을 모두 작성하기에는 힘드니 나중에 따로 정리하는 것으로 하겠다. ip와 port를 이용하여 ssh에 연결 한 후, git clone으로 우리 팀의 깃 리포지토리를 clone한 후, 서버 차원에서 conda 가상 환경을 설정 한후 필요한 라이브러리를 pip install -r requirements.txt로 가져왔다. 또한 .gitignore 안에 공개되면 안되는 데이터셋, 학습 결과, config 등 파일들을 넣어주었다.

 

Wandb 연결 및 config 파일 설정

가장 먼저 wandb 연결과 config 파일 설정을 해주어서 학습을 효율적이게 만들어야 할 필요가 있었다. Wandb와 sweep은 따로 구현을 해놓았고, 팀원 분 중 한 분이 pyYAML을 사용하여 config 파일을 정리하는 코드를 구현하였다.

parser = argparse.ArgumentParser()
parser.add_argument('--config',type=str,default='xlm_roberta_with_aug')
args, _ = parser.parse_known_args()
cfg = OmegaConf.load(f'./config/{args.config}.yaml')

먼저 argparse를 사용하여 config라는 argument를 받아오게끔 하였다. 이 argument 같은 터미널에서 파이썬 파일 실행 시 뒤에 --config=myconfig 이런식으로 실행하면 입력이 된다. 밑에 OmegaConf를 사용하여 pyYAML 파일을 가져오면 거기서 config을 읽어온다.

.yaml 파일의 예시이다. 만약 max_epoch을 가져오고 싶다면 config.train.max_epoch으로 가져오면 된다.

.sh 파일을 만들어서 다양한 yaml파일을 실행할 수 있다. CONFIGS에 yaml 파일 이름을 넣어주면 되는데, 여기서 Linux는 ,를 입력으로 받지 않으니 yaml 파일 이름 중간에 ,(쉼표)를 사용하지 않는 것으로 하자.

위 방법을 사용하면 귀찮게 argument를 다 입력해줄 필요 없이 많은 yaml 파일을 만들어서 순차적으로 돌려줄 수 있다.

 

데이터 증강

외부 데이터를 사용하는 것은 금지이고, 데이터 증강을 해도 랜덤하게 근거 없이 증강하는 것이 아니라 실행시키면 얻을 수 있는 근거 있는 방법을 사용해야 한다는 룰이 있었다. 이를 토대로 생각해보았을 때 데이터 팀에서 생각한 방법은 문장 역번역, sentence1과 sentence2의 위치를 바꿔주고 라벨을 유지한 채로 데이터 추가가 있었다. 사실 sentence1과 sentence2의 위치가 바껴도 라벨 값은 유지되어야 하는 것이 맞다, 데이터가 10000개 내외로 많은 편이 아니었기에 최대한 많은 데이터를 확보해야 하는 것이 필요했다. s1 + [SEP]  + s2와 s2 + [SEP] + s1은 임베딩을 지나게 되면 아예 다른 값이 나올 수도 있다. 유의미한 증강이 가능할 수도 있겠다는 생각이 들었다. 또 데이터 분포를 살펴보다보면 라벨이 0인 데이터가 굉장히 많고, 5인 데이터가 굉장히 적은 것을 알 수 있다. 이는 라벨간의 불균형을 의미하기도 했다.

이를 어떻게 타개해야 할까 고민하다가, 두 가지 방법을 떠올리게 되었다. 5점 데이터를 증강하거나(s1, s2 똑같은 문장을 넣은 후 레이블에 5점을 넣어준다. 실제로 5점인 데이터를 살펴보니 전부 똑같은 문장들이었다. 물론 s1은 5점이 아닌 문장들에서 random sampling해서 가져왔다), 0점 데이터를 자르는 방법을 생각하게 되었다. 데이터가 줄어들면 좋지 않지만 시도해볼만 했던 것 같다. 결론을 얘기하자면 s1, s2 위치를 바꿔준 것과 5점 라벨을 추가해주는 것이 효과적이었다. 심지어 5점 라벨을 나머지 데이터의 50% 증강해도 유의미한 성능 개선이 되었다. 

0점 데이터를 자르는 것은 역시 데이터를 줄여서 오히려 성능이 낮아지는 결과를 낳은 것 같다. 이러한 성능적인 부분은 블랙박스적인 요소가 많기 때문에 확실한 결론이 없다면 추측할 수 밖에 없는 것 같다. 또한 역번역은 역번역 모델의 성능 부진으로 실패하였다.

명확히 다른 의미의 단어를 역번역하는 모델의 성능 때문에 역번역 데이터 증강은 무리라고 판단

물론 모델마다 데이터셋에 따라 성능의 차이가 존재할 수도 있다는 생각에 모델들을 여러 데이터셋에도 돌려보았다.

실험과 도전

다양한 모델 사용

이번에 모델을 담당하면서 정말 많은 실험과 도전을 해보았다. 그 과정에서 좋은 결과를 낸 시도도 있었고, 실패도 있었다. 모두 적어보려고 한다. 먼저 PLM들을 실험해봐야 했다. 우리 팀이 시도해본 모델들은 다음과 같다. 

  • klue/roberta-(base, small, large)
  • snunlp/KR-ELECTRA-discriminator
  • beomi/KcELECTRA-base
  • monologg/koelectra-base-v3-discriminator
  • snunlp/KR-SBERT-V40K-klueNLI-augSTS

실험 결과 의외로 snunlp/KR-ELECTRA-discriminator가 가장 좋은 성능을 내었다. 그 중 이상한 점은 klue/roberta-large가 전혀 학습이 되지 않는다는 것이었다. Training loss가 전혀 수렴하지 못하는 모습에 찾아보니 큰 모델들은 fine-tuning하는데 시간이 조금 든다고 한다. 그럼에도 아예 학습이 되지 않자, 다양한 방법을 실험해보았지만 여의치 않았다. 그러다 곰곰히 생각해보았는데 수렴을 하지 못한다는 것은 parameter update 폭이 너무 큰게 아닌가하는 생각이 들었고, 원래 1e-5였던 learning rate를 1e-6으로 내렸다. 학습률을 낮춰주니 학습이 정상적으로 되었다. 또한 배치 사이즈는 8을 썼는데, 배치 사이즈는 웬만하면 높여주는 것이 학습에 좋은 것 같다. 만약 모델이 수렴하지 않으면 random seed와 lr을 조금씩 조정해 보는 것을 추천한다.

이제 다른 방법론을 찾아보면서 모델을 실험하는 시간이 필요했다. 각자 모델을 하나 맡은 다음에 가장 기본적인 hyperparameter 세트로 학습을 해보고, 학습이 정상적으로 되면 hyperparameter searching을 해주었다. 총 265번의 run이 있을만큼 다들 열정적으로 참여해주었다. 의미있는 결과는 snunlp/KR-ELECTRA-discriminator에서 볼 수 있었고, val_pearson이 0.92를 대부분 통과했고, 0.93을 통과하는 경우도 있었다. 제출 후에는 최대 91.XX 정도의 성능을 낼 수 있었다. 더 좋은 모델이 필요했다.

Siamse network

다른 방법론을 찾고 있던 와중 siamese network에 대하여 멘토님이 설명해주셨다. 이러한 문장간의 relation을 추출하는 task에서 효과적인 방법이라고 한다.

BERT모델을 encoding에 사용하여 각 문장에 대한 sentence embedding을 구한 후, 그 값을 mean_pooling에 넘겨 준 후 cosine similarity로 두 문장의 유사도를 구하는 방법이다. Sbert 학습을 진행하려면 문장을 tokenizing을 한 후 모델에 feeding해주는 방식이 아닌, 문장 그대로를 Sbert에 넣어준 후, 나온 embedding 값을 모델에 전달해야 했다. 다음과 같은 시도를 해보았다.

1. Sbert를 Dataloader 단에 넣어서 아예 embedding된 값을 넣어준다 - dataloader에 모델을 넣으면 모델 학습 자체를 할 수 없다. 실패

2. Dataloader에서는 문장을 tokenizing을 한 값이 아닌 문장 그대로를 전달해주었다. - 내부적으로 now()함수에 str을 지원하지 않는 error가 발생하였다. 실패

3. Sbert를 모델단에 붙여주고, dataloader에서 문장을 tokenizing 한 값을 s1, s2 따로 넣어준 후 list로 묶어서 전달해주었다. - 문장을 [SEP] 토큰으로 이어주지 않고 두 문장을 따로 저장하면 transformer.Encodings에는 dim이라는 attribute가 없다는 에러가 발생한다. 실패

4. 데이터로더 단에 sbert를 단 후 embedding한 값을 넘겨주되, 모델 단에는 roberta-large모델을 하나 더 붙여서 embedding된 값을 학습하는 방식으로 학습 - 이 경우에도 Sbert는 학습이 되지 않고, roberta-large의 embedding layer를 뗴어낸 후 학습을 해야 한다. embedding layer를 뗴어낸만큼 성능이 안 나올 가능성이 높고, sbert의 결과로 나오는 텐서의 차원과 roberta-large input_size랑 맞지 않는다는 에러가 발생한다. 실패

결국에는 구현해볼 수는 없었다. Siamese Net은 contrastive learning의 일종으로, 나중에 더 알아보고 나중의 task에는 적용해보고 싶은 욕심이 있다.

앙상블

모든 모델에 대한 성능을 구현한 후, 우리 팀은 방향을 잃었다. 더 이상 성능을 개선할만한 부분이 앙삼블과 k-fold validation 등 밖에 없었다. 앙상블에 대하여 자료조사를 하고 있던 중, voting를 알아보았다. 하드 보팅 소프트 보팅이 있는데, 하드 보팅은 다수결로 가장 많이 나온 category를 선택하는 것이고, 소프트 보팅은 각 모델에서 나온 결과를 평균내어 결과를 내는 것이었다. 우리는 회귀 문제를 푸는만큼 소프트 보팅 방법이 맞는 것 같았고, 단순히 평균을 내는 것보다는 각 모델의 성능을 가중치로 가중치 평균을 구하는 것이 좋지 않을까라는 생각이 들었다. 시간이 많지 않은 관계로 모델을 불러와서 inference하는 것이 아닌, 각 모델의 inference한 결과인 output.csv를 가져와서 모델의 성능을 tensor로 만든 후 softmax 층을 통과한 후, 각 가중치를 inference한 logit 값과 곱해주고 전부 더해주었다. 

결과는 0.91초반에서 0.9255까지 성능이 올랐다. 정말 흥분의 도가니였다, 대회를 처음 진행해보는 입장에서 굉장히 재미있었다. 대회가 끝나기 마지막 날에는 전처리, 모델 등 다른 선택은 시간이 안될 가능성이 높았고, 앙상블에 집중을 할 수 밖에 없었다. 앙상블을 찾던 와중 왜 앙상블이 잘될까에 대해서 적은 블로그 내용이 있었다.

https://velog.io/@jaylnne/%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EC%95%99%EC%83%81%EB%B8%94-Ensemble-%EC%9D%80-%ED%95%AD%EC%83%81-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EC%84%B1%EB%8A%A5%EC%9D%84-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%AC%EA%B9%8C

 

[머신러닝] 앙상블 (Ensemble) 은 항상 모델의 성능을 향상시킬까?

Hi! I'm Jaylnne ✋ 저는 최근에 검색어(=쿼리 키워드) 분류 모델을 개발하고 있습니다. 저와 연구소 팀원 한 분(총 2명)이서 데이터 수집부터 개발까지 1~2개월간 진행한 프로젝트였습니다. 상당히

velog.io

처음 생각해봐야 할것은 앙상블은 출력 분포의 편향과 분산을 줄여준다는 것이었다, 결국 model을 smoothing 해준다는 의의가 있는 것 같았다. 중요한 내용은 앙상블 하는 모델은 약한 상관관계를 가지고 있어야 효과가 있다는 점이었다. 모델 간에 상관관계가 낮다는 뜻이 내가 이해하기에는 다른 모델로 학습을 했으면 상관관계가 낮다는 가능성이 있을 것 같았다. 똑같은 모델 중에서 하이퍼파라미터를 바꿔서 앙상블을 하는 것보다 성능이 조금 낮더라도 다양한 모델을 앙상블 해보는 것이 더 효과가 좋을 것 같다는 생각이 들었다. 가장 먼저 snunlp KrELECTRA를 klue/roberta-large와 앙상블을 해주었다. klu/roberta-large가 성능이 좋지 않았지만, 모델의 크기가 큰만큼 private 단계에서는 더 general할 것이라는 믿음이었다. 또 KrELECTRA와 roberta-large를 각 3개씩 가져와서 앙상블을 해주었지만 효과가 별로 없었고, 위에 실행했던 모델 중에 결과가 괜찮았던 애들을 앙상블을 해주었고, 앙상블을 해준 output을 또 앞에서 앙상블 했던 output과 가중합 평균을 내어 앙상블을 해주었다. 

0.91XX - 0.9225 - 0.9269 - 0.9290까지 앙상블이 마지막에 정말 많은 성능 향상을 보여주었다. 다들 모델 성능을 올리려면 앙상블 하면 되지~라고 했던 이유를 알게 되었다. 아쉬웠던 점은 막바지에는 생각 없이 앙상블을 해주었다는 점이다. 앙상블을 무조건 많이 한다고 성능이 좋아지지는 않기에, 다음에 기회가 된다면 논리를 가지고 앙상블을 해주고 싶고, bagging boosting도 구현해보고 싶다.

 

더 도전하고 싶은 것들

역번역 문장을 넣기 전에 cosine similarity > 0.7(threshold)만 넣어줌
padding='max_padding'이 아니라 128로 해주면 roberta-large batch 32까지 가능
huberloss -> L1 + L2 Loss
bert 통과 시킨 last hidden state을 LSTM or GRU에 넣어줘서 한 번 더 학습
smart 구현
TAPT (task adaptive pre training) 구현해보기
Classification task로 바꿔서 마지막에 1.0, 0.0으로 바꿔준 후 loss 연결해도  된다
모델 A, B, C가 왜 잘되는지 ,안되는지 검증하고 설명할 수 있어야 한다.
코드 작성 시 docstring 같이 적어주기
학습된 모델 재학습 구현해보기
문장의 위치를 바꿔서 inference해주고 그 값의 평균을 냄
특정 task를 잘 푸는 모델을 학습시키고 초반 분류기를 통해 먼저 task를 나눈 후 성능이 좋은 모델에 forward

 

결과

대회 시작부터 지금까지 제일 많이 들어온 말이 순위는 중요하지 않다는 점이었다. 당연히 맞는 말이다. 어느 누구도 알아주지 않는 등수이고, 어떤 점을 배웠는지가 훨씬 중요하다는 점 또한 알고 있다. 나는 개인적으로 이번 2주 동안 정말 정말 많은 내용을 배웠고, 베이스라인 코드를 수정하는 두려움을 이겨내는 시간이었던 것 같다. 대회를 처음 진행하면서 기능을 구현하는 것이 두려웠지만, document를 밥먹듯이 읽고, 블로그, 논문을 참고하면서 하나 하나씩 결과를 찍어내가며 구현하는 경험이 너무나 값졌던 것 같다. 솔직한 마음으로는 과정이 너무나 아름다웠고, 결과 또한 아름다우면 나한테도 너무 좋고 우리 팀의 기세에도 도움이 될 것 같았다. 먼저 대회 기간 동안 우리 팀은 8~10위에 머물러 있었다. 모델 성능 개선이 쉽지 않았고, 0.92를 통과한 팀들은 따라잡기 어려웠다. 그러나 대회 막바지쯤 내가 구현한 앙상블로 4등까지 순위를 올려보았고, 노력을 통해 결과를 보게 되니 정말 너무 재미있고 흥분되었다. 사실 public보다 더 중요한 것이 private이기 때문에, 대회가 끝날 때까지 팀원들과 기다리다가 결과를 봤는데, 오히려 등수가 하나 올라가서 최종 3위를 달성하게 되었다~~~~~ 노력한 만큼 결과가 나왔다는 점에서 너무나 기뻤고, 앞으로 더 나아갈 수 있는 힘을 얻은 것 같다. 대회에 참가해보니 강의를 들을 때는 보이지 않던 에러가 발생하고, 환경설정 맞춰주는 것과 사소한 문제들을 직면하고 해결할 수 있는 진귀한 시간이었던 것 같다. 기술적인 부분들은 다른 포스트에서 작성하도록 하겠다. 방금 대회가 끝나서 너무 두서없이 적은 것이 아닌가 싶지만 이번 과정을 200% 즐겼고, 다가오는 다음 P stage도 너무 기대된다.

반응형