Giter VIP home page Giter VIP logo

mrc-level2-nlp-02's Introduction

Pstage_02_MRC

Solution for MRC Competitions in 2nd BoostCamp AI Tech 2기 by 메타몽팀 (2조)

Content

Competition Abstract

  • 주어지는 지문이 따로 존재하지 않을 때 사전에 구축되어 있는 대용량의 corpus에서 질문에 대답할 수 있는 문서를 찾고, 다양한 종류의 질문에 대답하는 인공지능 모델 개발
  • 데이터셋 통계:
    • Corpus : Wikipedia 약 5,7000개 문서
    • train_data : 3,952개 (Context, Question, Answer)
    • validation_data : 240개 (Context, Question, Answer)
    • test_data : 600개 (Question)

Result

EM F1 RANK
Public 74.580 83.100 3
Private 70.280 79.530 5

Hardware

  • Intel(R) Xeon(R) Gold 5120 CPU @ 2.20GHz
  • NVIDIA Tesla V100-SXM2-32GB

Operating System

  • Ubuntu 18.04.5 LTS

Archive Contents

  • mrc-level2-nlp-02 : 구현 코드와 모델 checkpoint 및 모델 결과를 포함하는 디렉토리
mrc-level2-nlp-02/
├── utils
│   ├── crawling_papago_rtt.ipynb
│   ├── nbest_ensemble.ipynb
│   ├── Question type Tagging.ipynb
│   ├── question_generation.ipynb
│   └── use_ner.ipynb
├── data
│   ├── papago_ner.csv
│   ├── question_generation.csv
│   ├── question_tag_rtt_papago_ner.csv
│   ├── question_tag_testset.csv
│   ├── question_tag_trainset.csv
│   ├── question_tag_validset.csv
│   ├── trainset_rtt_papago.csv
│   └── trainset_rtt_pororo.csv
├── arguments.py
├── custom_tokenizer.py
├── inference.py
├── inference_k_fold.py
├── preprocessor.py
├── rt_bm25.py
├── train.py
├── train_k_fold.py
├── trainer_qa.py
└── utils_qa.py
  • utils/ : 해당 디렉토리 내 ipynb 파일 실행 시 data 디렉토리에 csv 파일 생성
  • data/ : train/inference 시 활용하는 데이터 파일
  • inference.py : retriever-reader inference 후 predictions.json 및 nbest_predictions.json 생성
  • inference_k_fold.py : k fold를 사용하여 inference하는 파일
  • preprocessor.py : 데이터 전처리용 코드
  • rt_bm25.py : bm25를 사용한 retriever
  • train.py : reader 모델 학습을 위한 파일
  • train_k_fold.py : reader 모델 학습시 k fold 적용 파일
  • trainer_qa.py : Question Answering Trainer를 정의하는 파일
  • utils_qa.py : Question Answering 후처리(post processing) 코드

Getting Started

Dependencies

  • torch==1.6.0
  • transformers==4.11.0
  • datasets==1.4.0

Install Requirements

sh requirement_install.sh

Arguments

Model Arguments

argument description default
model_name_or_path 사용할 모델 선택 klue/roberta-large
rt_model_name 사용할 모델 선택 klue/bert-base
config_name Pretrained된 model config 경로 klue/roberta-large
tokenizer_name customized tokenizer 경로 선택 None
customized_tokenizer_flag customized roberta tokenizer 로드하기 False
k_fold K-fold validation의 k 선택 5

DataTrainingArguments

argument description default
dataset_name 사용할 데이터셋 이름 지정 /opt/ml/data/train_dataset
overwrite_cache 캐시된 training과 evaluation set을 overwrite하기 False
preprocessing_num_workers 전처리동안 사용할 prcoess 수 지정 2
max_seq_length Sequence 길이 지정 384
pad_to_max_length max_seq_length에 모든 샘플 패딩할지 결정 True
doc_stride 얼마나 stride할지 결정 128
max_answer_length answer text 생성 최대 길이 설정 30
eval_retrieval 원하는 retrieval 선택 sparse
num_clusters faiss 사용 시, cluster 갯수 지정 64
top_k_retrieval retrieve 시, 유사도 top k만큼의 passage 정의 50
score_ratio score ratio 정의 0
train_retrieval sparse/dense embedding을 train에 사용 유무 결정 False
data_selected context or answers or question 중, 추가할 Unknown token 설정 ""
rtt_dataset_name RTT data path 설정 None
preprocessing_pattern 원하는 전처리 선택 None
add_special_tokens_flag special token 추가 False
add_special_tokens_query_flag Question type에 관한 speical token 추가 False
retrieve_pickle pickle file 넣기 ''
another_scheduler_flag 다른 scheduler 사용 False
num_cycles cosine schedule with warmup cycle 설정 1

LoggingArguments

argument description default
wandb_name wandb에 기록될 모델의 이름 model/roberta
dotenv_path wandb key값을 등록하는 파일의 경로 ./wandb.env
project_name wandb에 기록될 project name False

Running Command

Train

$ python train.py --output_dir ./models --do_train --preprocessing_pattern 0 --add_special_tokens_query_flag True

Reader evaluation

$ python train.py --output_dir ./outputs --do_eval --model_name_or_path ./models --preprocessing_pattern 0 --add_special_tokens_query_flag True

ODQA evaluation

$ python inference.py --output_dir ./outputs --do_eval --model_name_or_path ./models --preprocessing_pattern 0 --add_special_tokens_query_flag True --top_k_retrieval 100 --score_ratio 0.85

Inference prediction

$ python inference.py --output_dir ./outputs --do_predict --model_name_or_path ./models --preprocessing_pattern 0 --add_special_tokens_query_flag True  --dataset_name ../data/test_dataset/ --top_k_retrieval 100 --score_ratio 0.85

Soft-voting Ensemble

단일 모델의 결과 nbest_predictions.json 파일들에서 probability 기반 soft-voting 하여 최종 ensemble 결과 json을 생성합니다.

Hard-voting Ensemble

단일 모델의 결과 predictions.json 파일들에서 빈도 기반 hard-voting 하여 최종 ensemble 결과 json을 생성합니다.

utils/nbest_ensemble.ipynb

Reference

  1. Dense Passage Retrieval for Open-Domain Question Answering

    https://arxiv.org/abs/2004.04906

  2. Passage Re-Ranking With BERT

    https://arxiv.org/pdf/1901.04085.pdf

  3. Latent Retrieval for Weekly Supervised Open Domain Question Answering

    https://arxiv.org/pdf/1906.00300.pdf

  4. Cheap and Good? : Simple and Effective Data Augmentation for Low Source Machine Reading

    https://arxiv.org/abs/2106.04134

  5. How NLP Can Improve Question Answering

    https://core.ac.uk/download/pdf/31832115.pdf

mrc-level2-nlp-02's People

Contributors

abbymark avatar changyong93 avatar gistarrr avatar j961224 avatar presto105 avatar yebin46 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

mrc-level2-nlp-02's Issues

Question type을 쓰는 것이 옳은 것인가?

논문에서 Question Answering task의 성능을 증진시키는 방법으로 Question category화(Question type 사용)를 설명하여 적용했습니다! (노션에 MRC대회 -> 데이터팀의 칸에 이 논문 외에도 Question category화에 대해서 1개 더 올려두었습니다.)

하지만! 어제 멘토링에서 과연, Reader 모델이 Who, Why, Where, When, What, how로 Quetion type을 넣는것이 효과적인가?, Question type 없이도 구별을 못하는가? 라고 말씀하셨습니다.

그리하여, 전처리 없을 경우, 전처리 1적용 경우, 전처리 2적용 경우, 전처리 3적용 경우의 Question type 없을 때와 있을 때 validation을 다 확인했습니다. -> 모델이 predict 했을 시, 원래 validation 정답과 틀린 것 위주로 봤습니다!

솔직히, 이미 진행한거라 의미가 없으면 슬플 것 같다라는 생각으로 조마조마했습니다...

미리 결론은!! Question type을 안 넣을 경우, Category에 맞지 않는 답을 도출함을 확인했습니다!! (다행..)

우선, 확인한 것을 모든 전처리 기법을 설명드리기에는 많으니 전처리 2의 경우에서 말씀 드리겠습니다!

1. Question type을 적용 유무에 따른 answer 도출 결과 비교!!

- mrc-1-001023 -

Question:  신란의 동반자가 죽었다고 전해지는 지역은?

original answer:  도고쿠

Question type 적용 안 한 predict answer:  「젠호인」(善法院)

Question type 적용한 predict answer:  도고쿠

---

- mrc-0-001870 -

Question:  스뮈츠에게 학비를 지원해 준 사람은?

original answer:  마리즈 교수

Question type 적용 안 한 predict answer:  콜럼비아 신학 대학원

Question type 적용한 predict answer:  마리즈 교수

---

- mrc-0-001073 -

Question:  일본의 대학 입시는 며칠간 진행되는가?

original answer:  이틀

Question type 적용 안 한 predict answer:  1월 13일 이후 첫 번째 토요일과 일요일 이틀

Question type 적용한 predict answer:  이틀

---

- mrc-0-000939 -

Question:  김준연은 김구가 누구와 몰래 교류하고 있다고 주장했나요?

original answer:  공산당

Question type 적용 안 한 predict answer:  우익진영

Question type 적용한 predict answer:  공산당

---

- mrc-0-003677 -

Question:  러셀의 여자 친구의 종교는?

original answer:  퀘이커 교

Question type 적용 안 한 predict answer:  퀘이커 교도

Question type 적용한 predict answer: 퀘이커

---

- mrc-0-004454 -

Question:  Mr.9은 라분을 무엇으로 사용하려고 했는가?

original answer:  식재료

Question type 적용 안 한 predict answer:  금 사냥꾼

Question type 적용한 predict answer: 식재료

---

- mrc-0-001646 -

Question:  1881년 리비에르의 목적지는?

original answer:  하노이

Question type 적용 안 한 predict answer:  쥘

Question type 적용한 predict answer: 하노이

---

- mrc-0-001552 -

Question:  화순 춘산영당에 소장된 최익현의 초상화는 무엇에다 그린 것인가?

original answer:  비단

predict answer:  1911년 4월

no tag answer:  비단

---

- mrc-0-004837 -

Question:  조르댕의 딸은 누구를 좋아하는가?

original answer:  클레몽트

predict answer:  바다

no tag answer:  바다

위의 결과를 표로 정리해서 말씀드리겠습니다.

id 원래 정답의 answer type type 적용 안 할시 나오는 type type 적용 시 나오는 type
mrc-1-001023 where who where
mrc-0-001646 where who where
mrc-0-001870 who where who
mrc-0-003677 what who what
mrc-0-004454 what who what
mrc-0-004837 who what what
mrc-0-001552 what what when
  • Question type 적용 안 할 시, answer가 다른 type을 도출하는 경우: 6개

  • Question type 적용 할 시, answer가 다른 type을 도출하는 경우: 2개

2. Question type 적용 전, 후 결과 비교

모든 결과는 epoch 1을 기준으로 하고 있습니다.

전처리 방법 type 적용 안 한 경우 type 적용한 경우
X 56.66/65.547 60.416/68.81
전처리 1 59.58/67.32 58.75/66.92
전처리 2 57.08/65.13 61.25/68.51
전처리 3 54.166/62.253 55.0/62.74

전처리 1번을 제외하고는 다 오른 것을 확인했습니다.

3. Question type 보완할 점

제가 train의 validation question type을 확인해보니 5~6개가 type이 잘못된 것을 확인했습니다!

그래서 train question type도 다시 창용님께서 보내주신 영어로 번역된 question을 보면서 확인해 볼 예정입니다!

4. [CITE] Question type 적용 고민

mrc-0-001650
Question:  한국을 '형제의 나라' 라고 칭한 나라는 어디인가요?
original answer:  멕시코
tag predict answer:  일본
no tag predict answer: 일본

mrc-0-004527
Question:  《악마는 프라다를 입는다》은 어느 국가의 영화인가요?
original answer:  미국
tag predict answer:  영국
no tag answer:  중국

이렇게 영화나, 책, 등을 인용하는 질문은 [CITE]라는 Question type을 붙인다면, "형제의 나라"나 "악마는 프라다를 입는다"라는 경우가 들어갈 경우에 answer를 좀 더 잘 찾지 않을까라고 생각이 들었습니다.

5. 오직 retriever 없이, reader 모델로만의 eval answer 도출 결과 비교(Question type 유무)

5번과 6번의 결과는 그냥, type을 추가하고 Question에 따른 맞는 answer type을 도출하는지 확인 용도입니다.

mrc-1-000753
Question:  브루투스가 세운도시의 현재 이름은?
original answer:  런던
tag predict answer:  런던
no tag predict answer: "트로이아 노바"

mrc-0-003677
Question:  러셀의 여자 친구의 종교는?
original answer:  퀘이커 교
tag predict answer:  퀘이커
no tag predict answer: 퀘이커  교도

위의 결과를 표로 정리해서 말씀드리겠습니다.

id 원래 정답의 answer type type 적용 안 할시 나오는 type type 적용 시 나오는 type
mrc-1-000753 where what where
mrc-0-003677 what who what
  • Question type 적용 안 할 시, answer가 다른 type을 도출하는 경우: 2개

  • Question type 적용 할 시, answer가 다른 type을 도출하는 경우: 0개

6. 오직 retriever 없이, reader 모델로만의 eval 성능

Date나 Where 같은 경우도 좀 더 깔끔한 Date와 Where에 관한 answer text를 도출하는 것을 확인

전처리 방법 type 적용 안 한 경우 type 적용한 경우
X 64.583/73.923 69.166/77.0813

번외1. validation data에 답이 이상한 data

mrc-0-004307
Question:  콘스탄스를 죽인 것은 누구인가?
original answer:  350년
predict answer:  마그넨티우스

-> 이 경우 '사람' 관련 해서 답이 나와야 하는데, 답이 '350년'이라는 년도로 나와 있습니다.

번외2. answer text의 끝에 조사만 남는 경우

아래의 결과는 전처리 2 + Question tag + epoch 1을 했을 시, predict answer 결과입니다.

mrc-0-001704
Question:  일본 프로 야구에서 처음으로 가쿠사다마를 사용한 선수는?
original answer:  가리타 히사노리
predict answer:  가리타 히사노리에

mrc-0-004197
Question:  어느정도 규모 이상의 대회에서 깃털 셔틀콕만을 사용하는 이유는?
original answer:  깃털 셔틀콕의 타구감을 선호하고, 또한 플라스틱보다 깃털 셔틀콕이 정교한 컨트롤을 하기에 보다 더 적합하기 때문이다
predict answer:  깃털 셔틀콕의 타구감을 선호하고, 또한 플라스틱보다 깃털 셔틀콕이 정교한 컨트롤을 하기에 보다 더 적합하기 때문

mrc-1-000899
Question:  길버트 J. 체크 중령이 7월 23일 저녁에 공격을 막기 위해 준비한 곳은 어디 주변이었나?
original answer:  보은군 남쪽 상용리(현 영동군 용산면 상용리) 마을
predict answer:  보은군 남쪽 상용리(현 영동군 용산면 상용리) 마을 근처

mrc-1-001567
Question:  아카키우스 분열은 얼마나 유지됐는가?
original answer:  35년
predict answer:  35년간

mrc-0-003752
Question:  제6회 전조선축구대회가 진행된 기간은?
original answer:  사흘
predict answer:  사흘간

위의 결과와 같이, 끝에 조사(에, 간, 근처 등)이 존재함을 알 수 있습니다.

-> 이 조사들을 제거하는 방식 또한 생각하려 합니다!

따라서 Question type을 붙이는데 좀 더 Question에 맞는 answer type이 나옴을 알 수 있습니다.

Question type 더 정확히 붙이는 중... -> (10.31일 14:31) inference에서 Question type에 맞는 answer type 생성 정확도 늘림 -> 전 버전에 비해 성능에 더 떨어져서 사용 못 할 것 같습니다..

answer text 끝에 조사 제거하는 방식 생각중 -> 성공!

Sparse Retrieval를 응용한 BM25를 적용해보자!

우선, 이 글을 쓰는 이유는 모든 파트가 할 것이 많지만 그 중, Retriever를 조금 건드려 봤습니다.
그래서 아직 역할이 안 정해졌지만 Retriever하시는 분들에게 조금이라도 편하고 빠르게 진행할 수 있도록 시도해봤습니다.
(아직 코드에 허점이 있을 것 같지만 성과를 보였기에 말씀드리겠습니다.)

흐름은 rank_bm25의 github 소개와 사용법 설명 -> retrieval.py 적용 부분 -> inference.py & train.py 적용 부분 순으로 설명 드리겠습니다.

다들 저보다 아시겠지만 BM25는 TF-IDF 개념 + 문서의 길이까지 고려(길이가 작은 문서에 더 가중치)한 방법입니다!

1. rank_bm25의 github 소개와 적용법

github 링크

github를 보시면, BM25의 다양한 방법(BM25Okapi, BM25L, BM25Plus 등)과 score 계산법이 소개되어있습니다!
제가 아직 완벽히 분석하지 못 해서, 각 방법에 대한 score 계산법에 대해서는 말씀 못 드리는 점 죄송합니다.

github의 rank_bm25.py를 보면 BM25 class를 크게 두고 다양한 방법들이 BM25 class를 상속하여 사용되는 것으로 보아 다양한 BM25 class들을 쉽게 불러와서 적용할 수 있습니다!

1-1. 설치법

pip install rank_bm25

1-2. BM25 class의 instant 생성 (BM25관련 passage embedding 생성)

from rank_bm25 import BM25Plus

corpus = [
    "Hello there good man!",
    "It is quite windy in London",
    "How is the weather today?"
]

# 원래 Sparse retriever 할 시, TfidfVectorizer에서 tokenize를 넣어 자동으로 적용됐지만 BM25는 tokenize를 따로 해줘야 합니다!
tokenized_corpus = [doc.split(" ") for doc in corpus] 

bm25 = BM25Plus(tokenized_corpus) # <rank_bm25.BM25Plus at 0x7fc5a7a3cd00>

1-3. doc score 및 indices 구하기

queries = ["windy London", "cold weather","what is weather"]
tokenized_queries= [i.split(" ") for i in queries]

doc_scores = []
doc_indices = []
for idx,i in enumerate(tokenized_queries):
    print("질문: "+ queries[idx])
   
   # get_scores 함수를 통해 passage embedding과 query vector간의 계산 완료!
    scores = bm25.get_scores(i)
    print("Scores: ",scores)

    sorted_score = np.sort(scores)[::-1]
    sorted_indices = np.argsort(scores)[::-1]
    print("오름차순 index: ",sorted_indices)

질문: windy London
Scores:  [2.77258872 5.3162481  2.77258872]
오름차순 index:  [1 2 0]
질문: cold weather
Scores:  [1.38629436 1.38629436 2.77258872]
오름차순 index:  [2 1 0]
질문: what is weather
Scores:  [2.07944154 2.71535639 4.15888308]
오름차순 index:  [2 1 0]

2. retrieval.py 적용 부분

아직 faiss 부분에는 적용하지 않았습니다!

2-1. init 함수

# 기존 init 함수에서 추가한 부분!
self.BM25 = None # BM25 class의 instant를 저장
self.tokenizer = tokenize_fn #BM25 instant 생성 전에, 사용할 tokenizer 저장 

2-2. get_sparse_BM25 함수 (baseline get_sparse_embedding 함수였던 것)

# Pickle을 저장합니다.
pickle_name = f"BM25_embedding.bin"
# BM class instant(BM passage embedding)을 저장하거나 저장된 경로 설정
bm_emd_path = os.path.join(self.data_path, pickle_name)
        
# BM25 존재하면 가져오기
if os.path.isfile(bm_emd_path):
    with open(bm_emd_path, "rb") as file:
         self.BM25 = pickle.load(file)            
    print("BM25_class_instant pickle load.")
        
# https://github.com/dorianbrown/rank_bm25 -> initalizing 부분
# BM25 존재 하지 않으면, tokenizer 한 후, BM25Plus로 passage embedding?
else:
    print("Build BM25_class_instant")
    # BM25는 어떤 text 전처리 X ->  tokenize 따로 한 후에, BM25 클래스의 인스턴스를 생성
    tokenized_contexts= [self.tokenizer(i) for i in self.contexts]
    self.BM25 = BM25Plus(tokenized_contexts)           
    with open(bm_emd_path, "wb") as file:
         pickle.dump(self.BM25, file)
    print("BM25_class_instant pickle saved.")

2-3. get_relevant_doc_BM25 함수 (baseline get_relevant_doc 함수였던 것)

tokenized_query = self.tokenizer(query) 
        
# ex. array([2.77258872, 5.3162481 , 2.77258872])
# passage embedding과 query vector간의 계산 완료!
doc_scores = self.BM25.get_scores(tokenized_query)
        
# score 높은 순으로 index 정렬
doc_indices=np.argsort(-doc_scores) 
# top_k만큼 뽑기!
return doc_scores[doc_indices[:k]], doc_indices[:k]

2-4. get_relevant_doc_bulk_BM25 함수 (baseline get_relevant_doc_bulk 함수였던 것)

print("Build BM25 score, indices")
tokenized_queries= [self.tokenizer(i) for i in queries]        
doc_scores = []
doc_indices = []
for i in tqdm(tokenized_queries):
    scores = self.BM25.get_scores(i)
    ## score가 0이거나 너무 낮은 점수가 꽤 있는 걸 확인하여 score 제한을 어느 정도 걸어서 받아도 될 것 같습니다!
    ## 저는 가장 높은 점수*0.5 기준으로 넘으면 넣는 식으로 했었습니다!
   
    # score 오름차순
    sorted_scores = np.sort(scores)[::-1]
    # index 오름차순
    sorted_indices = np.argsort(scores)[::-1]
    
    doc_scores.append(sorted_scores[:k])
    doc_indices.append(sorted_indices[:k])
return doc_scores, doc_indices

2-5. retrieve_BM25 함수 (baseline retrieve 함수였던 것)

기존 retrieve 함수에서 바뀐 부분은 주석으로 표현했습니다!

# 바뀐 부분!
## assert self.p_embedding is not None, "get_sparse_embedding() 메소드를 먼저 수행해줘야합니다."
assert self.BM25 is not None, "get_sparse_BM25() 메소드를 먼저 수행해줘야합니다."

if isinstance(query_or_dataset, str):
    # 바뀐 부분!
    ## doc_scores, doc_indices = self.get_relevant_doc(query_or_dataset, k=topk)
    doc_scores, doc_indices = self.get_relevant_doc_BM25(query_or_dataset, k=topk)
    print("[Search query]\n", query_or_dataset, "\n")

    for i in range(topk):
        print(f"Top-{i+1} passage with score {doc_scores[i]:4f}")
        print(self.contexts[doc_indices[i]])

    return (doc_scores, [self.contexts[doc_indices[i]] for i in range(topk)])

elif isinstance(query_or_dataset, Dataset):
    # Retrieve한 Passage를 pd.DataFrame으로 반환합니다.
    total = []
    with timer("query exhaustive search"):
        # 바뀐 부분!
        ## doc_scores, doc_indices = self.get_relevant_doc_bulk(query_or_dataset["question"], k=topk)
        doc_scores, doc_indices = self.get_relevant_doc_bulk_BM25(query_or_dataset['question'], k=topk)
    for idx, example in enumerate(
        tqdm(query_or_dataset, desc="BM25 retrieval: ")
    ):
        
        tmp = {
            # Query와 해당 id를 반환합니다.
            "question": example["question"],
            "id": example["id"],
            # Retrieve한 Passage의 id, context를 반환합니다.
            "context_id": doc_indices[idx],
            "context": " ".join(
                [self.contexts[pid] for pid in doc_indices[idx]]
            ),
        }
        if "context" in example.keys() and "answers" in example.keys():
            # validation 데이터를 사용하면 ground_truth context와 answer도 반환합니다.
            tmp["original_context"] = example["context"]
            tmp["answers"] = example["answers"]
        total.append(tmp)
        
    cqas = pd.DataFrame(total)
    return cqas

3. train.py 적용 부분

  • main 함수 변경 부분

오피스아워에서 말씀해주신 retriever train 부분을 적용했습니다!

train_retrieval

if data_args.train_retrieval:
    retriever = SparseRetrieval(tokenize_fn=tokenizer.tokenize,
                                data_path="../data",
                                context_path="wikipedia_documents.json")
    retriever.get_sparse_BM25()

    
# do_train mrc model 혹은 do_eval mrc model
if training_args.do_train or training_args.do_eval:
    run_mrc(data_args, training_args, model_args, datasets, tokenizer, model)

4. inference.py 변경 부분

  • run_sparse_retrieval 함수 변경 부분
# Query에 맞는 Passage들을 Retrieval 합니다.
# retriever 설정
retriever = SparseRetrieval(
    tokenize_fn=tokenize_fn, data_path=data_path, context_path=context_path
)
    
# Passage Embedding 만들기
#retriever.get_sparse_embedding()
retriever.get_sparse_BM25()
df = retriever.retrieve_BM25(datasets['validation'], topk=data_args.top_k_retrieval)

5. 리더보드 결과

결과

첫 번째 결과와 두 번째 결과에서 개요를 보시면 top_k_retrieval를 2와 5로 다르게 줬는데 같은 결과가 나왔습니다.
-> 이 부분은 baseline을 제대로 파악하지 않고 해서 그렇게 나와서 그 부분은 수정했습니다! (아직 리더보드로는 확인 못 했습니다 => 해결 완료)

첫 번째와 세 번째 조건은 top_k_retrieval=2, klue/roberta-large로 했습니다!

첫 번째와 세 번째 조건 차이는 BM25 사용과 baseline sparse retrieval 사용 차이입니다!

이상으로, BM25 적용 방법과 결과였습니다! 항상 도움이 되도록 노력하겠습니다! :)

레벤슈타인 거리 측정을 통한 유사 단어 예측 함수

적용 배경

이 아이디어를 적용하고자 한 이유를 다시 한 번 더 말씀드리자면,
정답을 확인 시 괄호 안에 영어,한글을 제외한 한자,일본어,다른 나라 언어 등이 있는데 tokenizer vocab에 존재하지 않아서 UNK로 출력이 됩니다. 따라서 UNK를 최소화하고자, 데이터팀에선 ()안의 span을 value로() 앞 span을 key로 사용하여 dictionary를 만들고자 합니다.
하지만, 결과로 출력된 start_index가 다를 경우, ground truth보다 길거나 짧은 span이 생성될 수 있습니다.
이 경우 사전의 key와 정확히 matching되지 않으니 () 안의 값을 가져올 수 없습니다.
따라서 레벤슈타인 거리를 활용하여 사전의 key값과 가장 유사한 span으로 복원을 해준다면 원하는 결과를 가져올 수 있을거라 판단됩니다.


필요한 변수

한글의 유니코드 번호를 입력해줍니다.
번호는 아래와 같습니다.

kor_begin = 44032 # 한글 시작
kor_end = 55203 # 한글 끝
chosung_base = 588 # 초성, base* chosung_list[index]
jungsung_base = 28 # 중성 base* chosung_list[index]
jaum_begin = 12593 #자음 시작
jaum_end = 12622 #자음 끝
moum_begin = 12623 #모음 시작
moum_end = 12643 #모음 끝
  • 초성, 중성, 종성, 자음, 모음 리스트를 생성합니다.
chosung_list = [ 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 
        'ㅅ', 'ㅆ', 'ㅇ' , 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']

jungsung_list = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 
        'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 
        'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 
        'ㅡ', 'ㅢ', 'ㅣ']

jongsung_list = [
    ' ', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ',
        'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 
        'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 
        'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']

jaum_list = ['ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄸ', 'ㄹ', 
              'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 
              'ㅃ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']

moum_list = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 
              'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']

전체 코드

코드 전체는 아래와 같고, 맨 밑에 결과를 토대로 코드를 디코딩해보겠습니다.

def leven_compose(chosung, jungsung, jongsung):
    char = chr(
        kor_begin +
        chosung_base * chosung_list.index(chosung) +
        jungsung_base * jungsung_list.index(jungsung) +
        jongsung_list.index(jongsung)
    )
    return char

def leven_decompose(c):
    # print('decompose에서 호출')
    # print(c)
    if not leven_character_is_korean(c):
        return c.upper() # return None에서 수정
    i = ord(c)
    if (jaum_begin <= i <= jaum_end):
        return (c, ' ', ' ')
    if (moum_begin <= i <= moum_end):
        return (' ', c, ' ')

    # decomposition rule
    i -= kor_begin
    cho  = i // chosung_base
    jung = ( i - cho * chosung_base ) // jungsung_base 
    jong = ( i - cho * chosung_base - jung * jungsung_base )    
    return (chosung_list[cho], jungsung_list[jung], jongsung_list[jong])

def leven_character_is_korean(c):
    # print('characteriskorean에서 호출')
    # print(c)
    i = ord(c)
    return ((kor_begin <= i <= kor_end) or
            (jaum_begin <= i <= jaum_end) or
            (moum_begin <= i <= moum_end))

def levenshtein(s1, s2, cost=None, debug=False):
    if len(s1) < len(s2):
        return levenshtein(s2, s1, debug=debug)

    if len(s2) == 0:
        return len(s1)

    if cost is None:
        cost = {}

    # changed
    def substitution_cost(c1, c2):
        if c1 == c2:
            return 0
        return cost.get((c1, c2), 1)

    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            # Changed
            substitutions = previous_row[j] + substitution_cost(c1, c2)
            current_row.append(min(insertions, deletions, substitutions))

        if debug:
            print(current_row[1:])

        previous_row = current_row

    return previous_row[-1]

def jamo_levenshtein(s1, s2, debug=False):
    if len(s1) < len(s2):
        return jamo_levenshtein(s2, s1, debug)

    if len(s2) == 0:
        return len(s1)

    def substitution_cost(c1, c2):
        if c1 == c2:
            return 0
        return levenshtein(leven_decompose(c1), leven_decompose(c2))/3

    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            # Changed
            substitutions = previous_row[j] + substitution_cost(c1, c2)
            current_row.append(min(insertions, deletions, substitutions))

        # if debug:
        # print(['%.3f'%v for v in current_row[1:]])

        previous_row = current_row

    return previous_row[-1]

def return_correct_name_by_leven(data_list, target_name):
  def get_target(data_list,target_name):
    return list(map(lambda x,: jamo_levenshtein(x,target_name), data_list))
  leven_dist = get_target(data_list, target_name)
  return data_list[np.argmin(leven_dist)]

디코딩

아래와 같이 [하이닉스, 삼성, 엘지]가 있을 때 "ㅎ닉스"가 들어오면 하이닉스를 찾을 수 있어야 합니다.
자 하나씩 살펴봅시다.

return_correct_name_by_leven(["하이닉스","삼성","엘지"], "ㅎ닉스")

우선 data_list와 target_name이 들어왔을 때, 자모단위로 레벤슈타인 거리를 계산하고, np.argmin을 통하여 거리가 짧은, 즉 가장 유사한 단어를 출력하는 것을 볼 수 있습니다.
여기서 레벤슈타인 거리를 설명하는 것은 어려우니, 링크를 보고 이론을 보시면 됩니다. 생각보단 간단해요.link!!
자, 이제 jamo_levenshtein를 알아볼 차례입니다.

def return_correct_name_by_leven(data_list, target_name):
  def get_target(data_list,target_name):
    return list(map(lambda x,: jamo_levenshtein(x,target_name), data_list))
  leven_dist = get_target(data_list, target_name)
  return data_list[np.argmin(leven_dist)]

생각보다 길어 보이지만 아닙니다.

  • 우선 s1이 s2보다 긴 단어가 들어오도록 해줍니다.
  • 만약 s2 단어가 None이면 거리는 단어 길이가 그대로 출력됩니다.
    자 이제 링크의 table을 만들 차례입니다.
    따라서 제거는 현재 row에서 +1을, 삽입은 이전 row에서 +1을 해주지만, 변경은 단순하진 않습니다.
    바로 jamo단위로 레벤슈타인 비용을 측정합니다.
    그리고 그 중 가장 cost가 작은 방법을 선택하여 진행하도록 전체 과정이 진행됩니다.
    그렇다면, 다음은 leven_decompose를 살펴보겠습니다.
def jamo_levenshtein(s1, s2, debug=False):
    if len(s1) < len(s2):
        return jamo_levenshtein(s2, s1, debug)

    if len(s2) == 0:
        return len(s1)

    def substitution_cost(c1, c2):
        if c1 == c2:
            return 0
        return levenshtein(leven_decompose(c1), leven_decompose(c2))/3

    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            # Changed
            substitutions = previous_row[j] + substitution_cost(c1, c2)
            current_row.append(min(insertions, deletions, substitutions))

        # if debug:
        # print(['%.3f'%v for v in current_row[1:]])

        previous_row = current_row

    return previous_row[-1]

leven_decompose() 함수의 역할은, 한국어 단어에 대해서 초성,중성,종성으로 분리하는 역할을 수행합니다.
그렇게 해야만, 앞 코드에서 substitutions cost를 jamo 단위로 판단하여 더욱 세밀하게 매길 수 있습니다.
decomposition rule은 아래를 보시면 됩니다.

def leven_decompose(c):
    if not leven_character_is_korean(c):
        return c.upper() # return None에서 수정
    i = ord(c)
    if (jaum_begin <= i <= jaum_end):
        return (c, ' ', ' ')
    if (moum_begin <= i <= moum_end):
        return (' ', c, ' ')

    # decomposition rule
    i -= kor_begin
    cho  = i // chosung_base
    jung = ( i - cho * chosung_base ) // jungsung_base 
    jong = ( i - cho * chosung_base - jung * jungsung_base )    
    return (chosung_list[cho], jungsung_list[jung], jongsung_list[jong])

def leven_character_is_korean(c):
    # print('characteriskorean에서 호출')
    # print(c)
    i = ord(c)
    return ((kor_begin <= i <= kor_end) or
            (jaum_begin <= i <= jaum_end) or
            (moum_begin <= i <= moum_end))

여기까지가 모든 코드입니다.
단, 만약 decomposition과 반대로, composition을 원할 경우 아래 코드를 참고하시면 됩니다.

def leven_compose(chosung, jungsung, jongsung):
    char = chr(
        kor_begin +
        chosung_base * chosung_list.index(chosung) +
        jungsung_base * jungsung_list.index(jungsung) +
        jongsung_list.index(jongsung)
    )
    return char

이러한 과정을 통해, "ㅎ닉스"가 입력으로 들어오더라도 하이닉스가 출력 됨을 알 수 있습니다.

return_correct_name_by_leven(["하이닉스","삼성","엘지"], "ㅎ닉스")
>>> '하이닉스'

추가 정보..

참고로, 이 과정들을 줄일 수 있는 훌륭한 라이브러리가 존재합니다.
바로 "jamo" library입니다.

pip install jamo

단, 그 원리를 이해하고자 이 코드를 올리니 참고해보시면 좋을 거 같습니다.
jamo 말고 제 코드를 사용할 경우 numpy만 있으면 됩니다.

간단한 설명은 월요일 오늘!

scheduler를 변경하는 방법!!

우선 trainer_qa.py 파일에 아래와 같은 코드를 추가하여 오버라이딩을 해주시면 됩니다.
여기서 num_training_steps은 전체 학습 steps 입니다. 이 부분은 아래 설명드리도록 하겠습니다.
num_cycles는 cosine 패턴을 몇 번 반복할 지 정해주는 옵션입니다.

from transformers import AdamW, get_cosine_with_hard_restarts_schedule_with_warmup
    def create_optimizer_and_scheduler(self, num_training_steps: int, num_cycles:int = 1, another_scheduler_flag=False):
        if not another_scheduler_flag:
            self.create_optimizer()
            self.create_scheduler(num_training_steps=num_training_steps, optimizer=self.optimizer)
        else:
            optimizer_kwargs = {
                    "betas": (self.args.adam_beta1, self.args.adam_beta2),
                    "eps": self.args.adam_epsilon,
                    "lr" : self.args.learning_rate,
                }

            self.optimizer = AdamW(self.model.parameters(), **optimizer_kwargs)
            self.lr_scheduler = get_cosine_with_hard_restarts_schedule_with_warmup(
                                self.optimizer, num_warmup_steps=self.args.warmup_steps, 
                                num_training_steps= num_training_steps,
                                num_cycles = num_cycles)

자 이제 train.py 파일로 넘어가겠습니다.
trainer = QuestionAnsweringTrainer~~~ 밑에 아래와 같이 추가해줍니다.

trainer = QuestionAnsweringTrainer( 
        model=model,
        args=training_args,
        train_dataset=train_dataset if training_args.do_train else None,
        eval_dataset=eval_dataset if training_args.do_eval else None,
        eval_examples=datasets["validation"] if training_args.do_eval else None,
        tokenizer=tokenizer,
        data_collator=data_collator,
        post_process_function=post_processing_function,
        compute_metrics=compute_metrics,
        )
    
   total_steps = math.ceil(len(train_dataset)/training_args.per_device_train_batch_size)
   trainer.create_optimizer_and_scheduler(total_steps, data_args.num_cycles, data_args.another_scheduler_flag)

마지막으로 각종 arguments를 새롭게 추가했으니, arguments.py 파일로 넘어갑니다.
DataTrainingArguments에 다음과 같은 arguments를 추가해줍니다.

    another_scheduler_flag :bool = field(
        default=False,
        metadata={"help": "create another scheduler"}
    )
    num_cycles :bool = field(
        default=1,
        metadata={"help": "cycles for get_cosine_schedule_with_warmup"}

제 기준에선, 실행 시 명령어는 다음과 같습니다.
python train.py --do_train --output_dir outputs/train --wandb_name ep1+pp1+rtt_q_t100_85_scdr-re_w500 --overwrite_cache --overwrite_output_dir --num_train_epochs 1 --preprocessing_pattern 1 --rtt_dataset_name ./csv/papago_ner.csv --rtt_dataset_name ./csv/papago_ner.csv --data_selected question --logging_steps 50 --another_scheduler_flag --warmup_steps 500 --num_cycles 2

최적화된 값은 테스트 후 진행하겠습니다.

단, 만약 cosine restart가 아닌, cosine annealing를 사용하고자 할 경우 코드를 조금 바꿔주시면 됩니다.

#self.lr_scheduler = get_cosine_with_hard_restarts_schedule_with_warmup(
#                            self.optimizer, num_warmup_steps=self.args.warmup_steps, 
#                            num_training_steps= num_training_steps,
#                            num_cycles = num_cycles)

self.lr_scheduler = CosineAnnealingWarmupRestarts(self.optimizer, first_cycle_steps=100, cycle_mult=1.0, max_lr=5e-05, min_lr=1e-07, warmup_steps=50, gamma=0.5)

단, CosineAnnealingWarmupRestarts 관련 arguments는 아직 구현하지 않은 상태이니 필요하실 경우 따로 써주시면 됩니다.
그리고 추가로, 관련 라이브러리 설치가 필요합니다.
아래 코드 설치 부탁드립니다.

!pip install 'git+https://github.com/katsura-jp/pytorch-cosine-annealing-with-warmup'

관련 내용은 다음 깃허브를 참고하시면 됩니다. => https://github.com/katsura-jp/pytorch-cosine-annealing-with-warmup"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.