AI TECH/TIL

[P stage] Week6 Today I Learn

prefer_all 2022. 11. 2. 12:07

이번주엔 첫 대회를 진행하였다. 지금까지 배워온 이론을 바탕으로, 코드를 작성해 데이터를 만져보고 모델을 돌려보는 작업을 했다.

<목차>
1. 과제에 대한 이해
2. 데이터 EDA
3. 데이터 Augmentation
4. 템플릿화
- wandb, sweep
5. 그 외 시도들
- k-fold
- freeze

- ensemble
6. 모델

 

Semantic Text Similarity(STS)

STS란 두 텍스트가 얼마나 유사한지 판단하는 NLP Task이다. 일반적으로 두 개의 문장을 입력하고, 이러한 문장쌍이 얼마나 의미적으로 서로 유사한지를 판단한다.
보고서나 논문 등의 정보 전달 목적의 글에서 중복된 문장들은 가독성을 떨어뜨리는데, STS는 이러한 중복 문장 제거의 교정작업을 도와준다.

Textual Entailment (TE)

STS와 TE, 두 문제의 가장 큰 차이점은 ‘방향성’이다.
STS는 두 문장이 서로 동등한 양방향성을 가정하고 진행되지만, TE의 경우 방향성이 존재한다.
예를 들어 자동차는 운송수단이지만, 운송수단 집합에 반드시 자동차만 있는 것은 아닙니다. 또한 출력 형태에 대해서도 차이가 있다.
TE, STS 모두 관계 유사도에 대해 참/거짓으로 판단할 수 있지만, STS는 수치화된 점수를 출력할 수도 있다.

 

STS의 장점

STS의 수치화 가능한 양방향성은 정보 추출, 질문-답변 및 요약과 같은 NLP 작업에 널리 활용되고 있다.
실제 어플리케이션으로는 데이터 증강, 챗봇의 질문 제안, 혹은 중복 문장 탐지 등에 응용되고 있다.


NLP 데이터는 어떻게 EDA((Exploratory Data Analysis)를 진행해야 할까?

- EDA란? 데이터를 분석하기 전에 그래프나 통계적인 방법으로 자료를 직관적으로 바라보는 과정

- EDA의 목적: 데이터의 분포 및 값을 검토함으로써 데이터가 표현하는 현상을 더 잘 이해하고, 데이터에 대한 잠재적인 문제를 발견할 수 있다.

- EDA의 과정:

1. 데이터를 전체적으로 살펴보기 : 데이터에 문제가 없는지 확인

 

[결측치 확인] 
결측치: NA와 같은 누락된 값

해결 방안
- 결측치가 있는 데이터 삭제
- 특정 값으로 일간 치환
- 숫자라면 보간법 활용   ex)  1, 2, ??, 4, 5 => 1, 2, 3, 4, 5
- 결측치 예측 모델을 추가적으로 활용
[데이터 분포]
- Train/Dev/Test 데이터 개수 및 비율 확인
데이터 수가 충분한 가, Dev와 Test 데이터 비율이 일치하는 가 확인
- 정답 Label의 개수, 종류, 분포를 확인
- tokenizing을 통한 Train/Dev 문장 문석
[train 데이터 라벨링 관련]
라벨에 따른 데이터 수가 적절하게 분포되어 있는 지
어떤 기준으로 라벨링이 되어있는지
오라벨링된 데이터는 없는지


[데이터 내용적인 부분]
중복된 데이터는 없는지
positive pair은 충분한지
*positive pair은 contrastive learning에서 같은 이미지로부터 augment된 데이터를 의미하는데, 여기서는 라벨이 상대적으로 유사한 데이터로 사용


[NLP task]
문어체인지, 대화체인지
문법이 잘 적용된 데이터인지?, 띄어쓰기가 제대로 되어있는지?, 비문은 없는지?
글자 외, 이모티콘이나, 다른 특수한 표현은 없는지?

 

2. 데이터의 개별 속성값을 관찰 : 각 속성 값이 예측한 범위와 분포를 갖는지 확인. 만약 그렇지 않다면, 이유가 무엇인지를 확인.

  • 수치가 이상한 데이터는 없는지
  • 값의 범위가 데이터 스키마(설명서)와 동일한지

3. 속성 간의 관계에 초점을 맞추어, 개별 속성 관찰에서 찾아내지 못했던 패턴을 발견 (상관관계, 시각화 등)

 

도출 결과

라벨이 5인 데이터가 적어서 증강이 필요하다.


Data Augmentation

데이터 증강을 위해 일반적인 방법들 중 하나인 AEDA, back-translation, special token 생성을,
위 task에서 특수한 방법으로 sentence가 공통인 데이터 처리를 시도해봤다.

 

AEDA

NLP에서 Text Augmentation 방법은 크게 텍스트의 일부를 변형하여 데이터를 증강하는 방법생성모델을 사용하여 새로운 텍스트를 생성하여 데이터를 증강하는 방법이 있다.

그 중에서 가장 손쉽게 접근할 수 있는 방법은 KoEDA 라이브러리를 사용하는 것이었다. KoEDA는 EDA AEDA 논문에서 소개된 방식을 한국어 Wordnet 으로 Porting하여 공개한 오픈소스 라이브러리이다.

 

Easy Data Augmentation라는 논문에서는 데이터를 다음의 네 가지 기법을 통해 자연어 데이터를 증강하고자 한다.

  • 유의어로 교체(Synonym Replacement, SR): 문장에서 랜덤으로 stop words가 아닌 n 개의 단어들을 선택해 임의로 선택한 동의어들 중 하나로 바꾸는 기법.
  • 랜덤 삽입(Random Insertion, RI): 문장 내에서 stop word를 제외한 나머지 단어들 중에서, 랜덤으로 선택한 단어의 동의어를 임의로 정한다. 그리고 동의어를 문장 내 임의의 자리에 넣는걸 n번 반복한다.
  • 랜덤 교체(Random Swap, RS): 무작위로 문장 내에서 두 단어를 선택하고 위치를 바꾼다. 이것도 n번 반복
  • 랜덤 삭제(Random Deletion, RD): 확률 p를 통해 문장 내에 있는 각 단어들을 랜덤하게 삭제한다.

데이터셋이 적다는 가정에서, 데이터셋이 500개일 때 EDA를 포함하면 평균적으로 3%의 성능이 증가함을 확인할 수 있다. full set일 때도 EDA를 사용하면 평균적으로 성능의 향상이 있었다. 하지만 우리가 사용할 BERT 등의 선학습 모델은 거대 데이터셋으로 선학습되었기에 데이터셋의 개선 효과를 못 볼 수도 있다고 한다. 한 문장에 대해서 몇 개의 문장을 만들건지에 따라 α값에 조정이 필요하며, 4문장 이하는 p=0.1, 4문장 초과는 p=0.05 정도의 확률값으로 데이터를 변형하는게 가장 성능이 좋았다고 저술되어있다.

 

하지만 텍스트 데이터의 특성상, 위치를 바꾸거나 일부 단어를 제거하는 것은 결국 본 문장의 의미를 손실시키는 행위이기 때문에 AEDA 방법론이 등장하게 된다. AEDA는 문장을 손실시키지 않게 하기 위해 Special character를 문장 곳곳에 배치하는 방법론으로, 역시 많은 특수문자가 들어가게 되면 성능이 떨어지기 때문에 적절한 확률값을 찾는 것이 중요하다.

 

한글 자연어 처리 패키지 konlpy 

konlpy는 형태소 등을 알아서 분석해주는 편리한 패키지이지만, konlpy 내 클래스는 Java 기반이기 때문에 그냥 pip install konlpy로 설치할 수 없다. 아래와 같은 과정을 거쳐야 하는데
1. JAVA 설치 https://www.oracle.com/java/technologies/javase-downloads.html
2. JAVA_HOME 환경변수 설정JPype 다운로드 및 설치 https://www.lfd.uci.edu/~gohlke/pythonlibs/#jpype
    이때, 파이썬 버전과 맞게 다운로드해야만 cmd창에서 'not a valid wheel filename' 에러가 안 뜬다
4. konlpy 설치


- 별희님이 AEDA 코드를 작성해주셨는데 대회가 끝나면 첨부할 예정이다.

도출 결과
AEDA를 사용해서 sentence1,2를 모두 바꾼 데이터셋이 가장 성능이 높았다.


Data Augmentation - special token 생성


- 진호님이 special token 코드를 작성해주셨는데 대회가 끝나면 첨부할 예정이다.
참고한 수도 코드이다.

special_tokens_dict = {'additional_special_tokens': ['[C1]','[C2]','[C3]','[C4]']}
        num_added_toks = tokenizer.add_special_tokens(special_tokens_dict)
        model.resize_token_embeddings(len(tokenizer))


중요한 점은 model.py에서 임베딩 값을 바꾸는 코드를 추가해야 한다는 것이다.

self.model.resize_token_embeddings(50137) # special token

Template

템플릿은 sweep이 없는 버전을 사용하다가 branch를 새로 파서 sweep이 있는 버전을 추가로 만들었다. (yaml 파일 때문에)

1.
별희님의 템플릿을 익히고 내 것으로 만드려고 노력해봤다. 이름하여 스폰지 학습법~
pytorch lightning은 처음 써봤다.

📁pytorch-template/

├── train.py - 모델을 학습한다.
seed_everything 함수: seed 결정 / main 함수: Dataloader을 불러오고, Model과 Trainer을 설정한다
├── inference.py - 저장된 모델로 예측을 진행한다.
output 형식(submission_format.csv)을 불러와서 예측된 결과로 바꿔주고, output.csv로 출력한다
├── requirements.txt - pytorch, pandas, wandb 등의 버전 정보를 저장한다 / pip install -r requirements.txt

├── 📁data/ input 데이터(train.csv, dev.csv, test.csv)를 저장한다. 저작권 이슈가 있으므로 .gitignore을 사용해 remote에 올리지 않도록 주의한다

├── 📁dataloader/ 데이터를 로드해온다.
│ └── data_loaders.py - baseline의 train.py에 있던 Dataset 클래스(tokenization, preprocessing, setup 등의 함수)도 포함되어있다. pytorch_lightning을 사용했다

├── 📁models/
│ ├── model.py - baseline의 train.py에 있던 Model 클래스
│ ├── optimizer.py - config에서 optimizer, scheduler 정보를 받아와서 설정한다.
│ └── loss_function.py - loss 함수로 L1loss를 설정한다

├── 📁configs/ config.json 파일을 사용하지 않고 argparse를 사용해서 argument를 설정한다.
모델 정보, wandb 플젝명, 데이터 경로 등을 수정할 수 있다.
│ └── configs.py

├── 📁trainer/ - trainers
│ └── trainer.py - earlystopping 등을 설정한다.

└── 📁notebook/ .ipynb 파일들을 모아뒀다.

 

WandB 코드 상 연결

모델을 학습할 때 wanb로 기록하는 것이므로 train.py 에 관련 코드가 있다.

from pytorch_lightning.loggers import WandbLogger
 wandb_logger = WandbLogger(name=exp_name, project=config.wandb_project)
 ...
 ...
 trainer = Trainer(config, wandb_logger).trainer

 

WandB 특정 프로젝트에 연결하고 싶은 경우 어떤 key값을 써야할까?

key는 계정당 하나이기 때문에 새로 발급 받는 것이 아니다. 위 링크에서 확인 가능 https://wandb.ai/authorize
커맨드 창에서 아래와 같이 입력하고 key값을 입력한다.

wandb login --relogin

이후 아래와 같이 입력하고 어느 프로젝트에 연결할 지 설정하면 된다.

wandb init



2.

실험하고 싶은 옵션이 추가될 때마다 코드가 길어져서 한 눈에 보기 어렵다.
이때, 관련 있는 인자끼지 구조화할 수 있는 방안이 바로 yaml을 활용하는 것이다.

yaml이란?
구성 파일 작성에 사용되는 데이터 표현 양식으로 가독성이 좋다는 장점이 있다.


관련 있는 인자는 아래와 같이 뭉치고 (.yaml 파일)


train.py, inference.py 에서 아래와 같이 간단하게 활용 가능하다.

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--config", type=str, default="base_config")
    args, _ = parser.parse_known_args()
    config = OmegaConf.load(f"./configs/{args.config}.yaml")

Checkpoint

trainer.py에서 설정 가능하다.
우선 필요한 모듈 import를 한다.

import pytorch_lightning as pl
from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping

 

Early Stopping

early_stop_callback = EarlyStopping(
            monitor="val_loss", min_delta=0.00, patience=3, verbose=False, mode="min"
        )

 

최적의 모델 찾기

checkpoint_callback = ModelCheckpoint(
                monitor= "val_pearson",
                mode='max',
                dirpath="./checkpoint/"
                #save_top_k=1
        )

path를 지정해주면 별도의 로깅 없이 간편하다

 

super(Trainer, self).__init__(
            accelerator="gpu",
            devices=config.train.gpus,
            max_epochs=config.train.max_epoch,
            logger=wandb_logger,
            log_every_n_steps=1,
            callbacks=[early_stop_callback, checkpoint_callback],
        )

K-fold Dataloader

k-fold에 필요한 parameter을 config에 추가한다.
setup 함수를 제외한 나머지 부분은 Dataloader과 유사하다.

def setup(self, stage="fit"):
        if stage == "fit":
            # 데이터 준비
            total_data = pd.read_csv(self.train_path)
            total_input, total_targets = self.preprocessing(total_data)
            total_dataset = Dataset(total_input, total_targets)

            # 데이터 셋 num_splits 번 fold
            kf = KFold(n_splits=self.num_splits, shuffle=self.shuffle, random_state=self.split_seed)
            all_splits = [k for k in kf.split(total_dataset)]

            # k번째 fold 된 데이터셋의 index 선택
            train_indexes, val_indexes = all_splits[self.k]
            train_indexes, val_indexes = train_indexes.tolist(), val_indexes.tolist()

            # fold한 index에 따라 데이터셋 분할
            self.train_dataset = [total_dataset[x] for x in train_indexes] 
            self.val_dataset = [total_dataset[x] for x in val_indexes]

        else: # 평가 데이터 준비
            test_data = pd.read_csv(self.test_path)
            test_inputs, test_targets = self.preprocessing(test_data)
            self.test_dataset = Dataset(test_inputs, test_targets)

            predict_data = pd.read_csv(self.predict_path)
            predict_inputs, predict_targets = self.preprocessing(predict_data)
            self.predict_dataset = Dataset(predict_inputs, [])

Freeze


Ensemble(앙상블)

참고