본문 바로가기

Project 🖥

Naver AI rush 2022 회고

https://github.com/whatsbirddd/Fake-Documents-Detection

 

GitHub - whatsbirddd/Fake-Documents-Detection: CLOVA AI Rush 2022 @Naver AI Lab

CLOVA AI Rush 2022 @Naver AI Lab. Contribute to whatsbirddd/Fake-Documents-Detection development by creating an account on GitHub.

github.com

🌷 Overview

  • 결과 : 18위(0.93)까지 2라운드 진출인데 제가 19위(0.928)라서 2라운드는 못가게 되었네요 🥺..
  • 주제 : 엉터리 문서 검출
  • 엉터리 문서 정의 : 문맥이 맞지 않는 단어들로 구성된 문서로, 단어의 순서를 바꾸어도 전혀 말이 되지 않는 문장들이 포함된 문서
  • 데이터 불균형이 굉장히 심했음(130000:4000)
  • 최종 스코어 솔루션 : kr-electra + lstm기반의 classifier / focal loss / 데이터 샘플링으로 20000개 엉터리 문서를 만들어서 같이 train시킴
  • 데이터 샘플링 방법 : 이건 제가 그냥 실험해본 건데, 정상 문서들중에 유사도가 먼 문서2개를 뽑아서 섞어서 엉터리 데이터를 만들었음, 이방법으로 20000개의 추가 데이터를 만들었습니다.
    • 외부데이터로 했다면 성능을 더 높였을수도(1등 솔루션 - 외부데이터로 셔플)
  • 0.93이상의 솔루션을 보니까 언더샘플링으로 학습 배치때마다 정상문서와 엉터리 문서의 비율을 맞춰주는 게 정말 효과적이었더라구요! (학습방법도 많이 고민해봐야겠다는 걸 느꼈습니다.)

🌷 대회에 사용한 코드

  • main.py :
    • nsml(학습을 위한, 네이버에서 제공해주는 리소스를 잘 사용하기 위해 네이버에서 제공하는 라이브러리)
    • dataloader
  • model.py : 실험한 모델들 마구잡이로 넣어두었습니다😰
  • util.py : 실험을 위한 함수들을 넣어두었습니다.
    • preprocess : 전처리가 유효한가? 오탈자, 해시태그들(잘못 예측된 데이터들을 직접봤을 때 해시태그를 포함하고 있는 데이터들이 많았습니다…)
    • mixup : 엉터리 문서 생성을 위한 코드
    • focal loss vs weighted cross entropy vs cross entropy
      • weight를 준 크로스엔트로피는 도움이 되지 않았고
      • focal loss는 확실하게 도움이 되어서 선택!
  • train.py : 네이버에서 제공한 baseline 코드 거의 그대로 사용했습니다.

🌷 스스로에 대한 피드백

1. 여러개의 모델을 테스트 할 때

  1. models folder로 base모델마다 모듈(.py)로 만들어서 model = __import__('models').__dict__[modelname]로 사용
    • models” folder
      • 설명 : base (KRElectra,KoBert,KoBigBird) - classifier(avgpool-linear, bi-LSTM, CNN))
        • resnet은 make_layer라는 함수를 통해서 init에서 레이어를 예쁘게 작성하더라…🤭
      • init.py
        • .은 relative import가 가능하도록 하는 부분
        from .factory import create_model
        from .vgg import *
        from .sid_resnet import *
        from .wavelet_module import *
        from .detector import *
        
      • factory.py
        • model = import('models').dict[modelname]
          • 다른 디렉토리에 있더라도 쉽게 모듈을 가져와서 사용하는 방법임. 그러면 코드가 굉장히 간결해짐.
          파이썬 패키지 init.py 를 이용해서 만드는 법
        • 모델의 파라미터가 다르면 if로 나누어서 모델 생성해주면 될까?
        def create_model(modelname, hidden_size, output_size, embed_dim, num_layers, batch_first, bidirectional,maxlen):
        		model = __import__('models').__dict__[modelname](
        		        hidden_size = hidden_size
        		        output_size = output_size
        		        embed_dim = embed_dim
        		        num_layers = num_layers
        						bidirectional = bidirectional
        		        batch_first = batch_first
        		        maxlen = maxlen
        		    )
        
            if modelname == 'name1': #어떤 특별한 조건이 있다면 
        			model = classname(model, dataname)
        
            if checkpoint:
                assert os.path.isfile(checkpoint), "checkpoint does not exist"
                if modelname != 'name1':
                    model.model.load_state_dict(torch.load(checkpoint))
                else:
                    model.load_state_dict(torch.load(checkpoint))
                    
            return model
        
  2. 허깅페이스로 불러오는게 동일하기 때문에 model.py에서 모델이름 불러와서 관리
class DocumentClassifier(nn.Module):   
    def __init__(self, model_name, hidden_size, output_size, embed_dim, num_layers, batch_first, bidirectional,maxlen):
        super(DocumentClassifier, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.embed_dim = embed_dim
        self.num_layers = num_layers
        self.batch_first = batch_first
        self.bidirectional = bidirectional
        self.maxlen = maxlen
        
        self.embeded = AutoModelForSequenceClassification.from_pretrained(model_name) #(batch, maxlen, embed)
        self.lstm = nn.LSTM(input_size = self.embed_dim,
            hidden_size = self.hidden_size,
            num_layers = self.num_layers,
            batch_first = self.batch_first,
            bidirectional = self.bidirectional
            )
        self.linear = nn.Sequential(
            nn.ReLU(),
            nn.BatchNorm1d(self.hidden_size*2),
            nn.Linear(self.hidden_size*2, self.output_size)
        )
        
    def forward(self, input_ids=None, token_type_ids=None, attention_mask=None,labels=None):
        #(last_hiddne_state, hidden_states, attentions, cross_attentions)
        x= self.embeded(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
        x = x[0]
        z, _ = self.lstm(x) #(output) = (batch, max_len, hidden_size), (h_n,c_n) 
        #z = z[:,-1,:].squeeze(1) #last hidden state (batch, hidden_size)
        z = nn.AvgPool2d((self.maxlen,1))(z).squeeze() #(batch_size, hidden_size)
        y_pred = self.linear(z)
        return y_pred

2. config 파일 활용하기

  • modelname_config.yml
    • predict_modelname_config.yml
    DATALOADER:
      num_workers: 32
      shuffle: False
      pin_memory:
      drop_last: False
    
    SEED:
      random_seed: 42
    
    PREDICT:
      model_name: 'kykim/bert-kor-base'
      trained_model_name: 'bert'
      batch_size: 64
      submission_name: 'bert'
    
    • train_modelname_config.yml
    TRAIN:
      data_type: 'original'
      model_name: 'kykim/bert-kor-base'
      num_epochs: 2
      batch_size: 64
      learning_rate: 2e-5
      early_stopping_patience: 5
      model:
      optimizer:
      scheduler:100
      momentum:
      weight_decay: 0.1
      loss_function:
    
    SEED:
      random_seed: 42
    
    DATALOADER:
      num_workers: 32
      shuffle:
      pin_memory:
      drop_last:
    
    # CONFIG
    parser = argparse.ArgumentParser()
    parser.add_argument('--config_name', type=str, required=True)
    args = parser.parse_args()
    CONFIG_NAME = args.config_name
    print(CONFIG_NAME)
    TRAIN_CONFIG_PATH = os.path.join(PROJECT_DIR, 'config', f'{CONFIG_NAME}.yml')
    config = load_yaml(TRAIN_CONFIG_PATH)
    
    # SEED
    RANDOM_SEED = config['SEED']['random_seed']
    
    # WANDB
    RUN_NAME = config['WANDB']['run_name']
    
    # TRAIN
    DATA_TYPE = config['TRAIN']['data_type'] # original or all 
    MODEL_NAME = config['TRAIN']['model_name']
    EPOCHS = config['TRAIN']['num_epochs']
    BATCH_SIZE = config['TRAIN']['batch_size']
    LEARNING_RATE = config['TRAIN']['learning_rate']
    EARLY_STOPPING_PATIENCE = config['TRAIN']['early_stopping_patience']
    OPTIMIZER = config['TRAIN']['optimizer']
    SCHEDULER = config['TRAIN']['scheduler']
    MOMENTUM = config['TRAIN']['momentum']
    WEIGHT_DECAY = config['TRAIN']['weight_decay']
    

3. Docker 이미지 사용하기

  • 사실 docker, requirements.txt, setup.py를 적재적소에 사용하는 방법을 모르겠다.

  • 칭찬할 점
    • 3주동안 파이토치에 익숙해지려고 노력했고, 매일 코드를 보았고, 계속 시도를 했다는 것
    • 허깅페이스와 친해짐
    • 무작정 시도만 하는 것이 아니라 결과를 바탕으로 보완이 된 시도를 했다는 것. 결과를 계속 보려고 했는 것
    • 파라미터 튜닝을 하다보니까 점점 다양한 파라미터들이 보이기 시작했다. 모델뿐만아니라 optimizer, loss function 등 이곳저곳에서 우당탕탕 실험해 볼 숫자들이 보였다.
  • 부족했던 점
    • 약간 조급했다? 조급하다보니까 내가 빠르게 돌릴 수 있는 것만 생각하게 되고, 지금 당장 시도할 수 있는 것만 하게되었다. 에러가 많이 날 가능성이 있고 코드 고쳐야할 것이 많다고 생각되는 부분들에 대해서는 쉽게 뒤로 미루었던 거 같다. 미루다보니까 태스크에서 잊혀졌다.
  • 솔루션 보완
    • 엉터리 문서 데이터셋을 만들 때 내부데이터로 셔플하는 방법으로 만들었는데, 외부 데이터로 셔플을 했으면 더 효과적이었을 것.
    • 오버샘플링 해볼걸.. 블로그에서 텍스트 데이터는 오버샘플링, 언더샘플링이 잘 작동하지 않는다는 말들을 보고 적용하지 않았던 게 잘못된 판단이었다. 직접 실험해보고 눈으로 봤어야 했는데, 남의 말만 듣고 시도하지 않았다는 게 아쉽네
    • 오히려 언더 샘플링을 통해서 두 라벨에 밸런스를 맞춰주는 것이 더 성능을 높이는 방법이었다.
    • k-fold 앙상블 다음에 꼭 써먹자. 학습 방법도 중요하다는 것을 깨달았다. 모델의 아키텍처에 대해서만 고민을 많이 했는데, 그것보다 학습 방법에도 눈을 돌려봤어야 했다!
    • 학습할 때에 엉터리문서와 정상문서의 비율을 맞춰서 학습해주는 것이 좀 더 효과적이었음.

'Project 🖥' 카테고리의 다른 글

project 07/19  (0) 2022.07.19
project 07/14  (0) 2022.07.14