데이터과학 유망주의 매일 글쓰기 — 프로젝트 4–3
계속된 BERT 작업
# BERT
오늘 한일:
어제는 BERT에 데이터를 넣기 위한 전처리 작업을 했다. 오늘 그 작업을 마무리하고, 실제로 BERT를 모델링하는데 성공했다. 그래서 어제에 이어 오늘은 무슨 작업을 했는지 간략하게 되짚어 보기로 했다.
어제까지 데이터를 PyTorch Tensor로 변환하는 작업을 했다. 오늘은 가장 먼저 GPU를 초기화하는 작업을 했다. 딥러닝 연산은 무거울 수 있기 때문에 되도록 GPU를 활용하는 것이 권장되기 때문이다. 알고보니 PyTorch 모듈은 GPU를 돌릴 수 있는 CUDA라는 라이브러리를 지원했다. 이 라이브러리는 그래픽 카드 회사로 유명한 NVIDIA나 AMD가 모두 활용하는, GPU세계의 필수 도구이다. 이 CUDA를 사용할 수 있는 디바이스를 PyTorch 모듈로 부터 초기화하고, 나중에 계산이 끝났을 때, CPU로 다시 가져와 결과를 테스트하기 위해 활용한다.
다음으로 모델을 초기화했다. 라벨의 타깃은 0과 1로 이루어져있어, 식별할 라벨의 수는 2로 설정했다. 이후 사용할 Optimizer를 초기화했는데, 먼저 AdamW를 사용해보기로 했다. 학습률(learning rate)은 0.00001정도로 매우 낮게 설정했고, 이 학습률이 0으로 나뉘어져 계산이 엉키는 것을 방지하기 위해 epsilion값을 알맞게 설정했다. 또한, Learning Rate Decay를 위한 스케쥴러를 설정해, 모델의 학습 능력을 높이기 위한 준비를 했다.
모델의 성능을 측정할 수 있는 함수와, 모델이 학습하는데 걸린 시간을 계산하는 함수를 지정했다. 시간 정보가 중요한 이유는, 모델의 학습이 빠를 수록 좋기 때문이다. 일반적으로 딥러닝은 방대한 데이터를 처리하여 높은 정확도를 얻기 위해 사용하는데, 시간이 많이 걸릴 여지가 많다. 조금이라도 시간을 줄일 수 있다면 더 좋은 모델이 될 것이기 때문에, 시간 정보를 모델의 중요한 지표로 활용가능하다.
# 옵티마이저 설정optimizer = AdamW(model.parameters(),lr = 1e-5, # 학습률eps = 1e-8 # 0으로 나누는 것을 방지하기 위한 epsilon 값)# 에폭수epochs = 3# 총 훈련 스텝total_steps = len(train_dataloader) * epochs# Learning rate decay를 위한 스케줄러scheduler = get_linear_schedule_with_warmup(optimizer,num_warmup_steps = 0,num_training_steps = total_steps)# 정확도 계산 함수def accuracy_measure(y_pred, y):pred_flattened = np.argmax(y_pred, axis=1).flatten()y_flattened = y.flatten()return np.sum(pred_flattened == y_flattened) / len(y_flattened)# 시간 표시 함수def time_elapsed(elapsed):# 반올림elapsed = int(round((elapsed)))# hh:mm:ss으로 형태 변경return str(datetime.timedelta(seconds=elapsed))
본격적으로 모델을 학습할 수 있는 준비를 마치고, 재현을 위해 랜덤시드를 고정했다. 일반적으로 Random State는 42로 설정한다. Gradient를 초기화하여, Optimizer가 Gradient Descent를 진행할 때 도움이 되도록 한다.
# 재현을 위해 랜덤시드 고정seed_val = 42random.seed(seed_val)np.random.seed(seed_val)torch.manual_seed(seed_val)torch.cuda.manual_seed_all(seed_val)# 그래디언트 초기화model.zero_grad()
이제 마지막으로 본격적인 학습을 시작한다. 전체적으로 훈련과 검증 두 부분으로 나뉘어지는데, 훈련 단계에서는 정해진 에폭수 만큼 학습을 반복한다. 시간을 초기화하고, 비용함수를 게산해야하기 때문에 이 역시 초기화한다. 이전에 설정한 batch_size 만큼, 설정한 데이터로더에서 나누어서 사용한다. 시간이 얼마나 경과 했는지는 이전에 설정한 함수를 활용한다. 배치를 GPU에 입력하면 GPU는 입력된 배치를 처리하기 시작한다. 여기서 Forward Propagation으로 비용함수를 계산하고, Backward Propagation로 Gradient Descent를 수행하며 학습을 하게된다. 이전에 설정한 스케줄러로 learning rate decay를 실시하고, 설정한 batch_size 만큼 데이터를 사용하면, gradient를 다시 초기화하여, 다음 batch_size만큼의 데이터를 사져온다. 이 작업을 반복하면서 모든 데이터를 사용하게 되면, 평균 에러와 평균 학습 시간을 게산한다.
훈련이 끝나면 검증을 실행하는데, 전반적인 과정은 훈련단계와 비슷하다. 다만, 마지막에 GPU의 예측 결과를 CPU로 가져와, 실제 라벨과 비교하여 정확도를 계산하게된다. 최종 출력 정확도와 소요시간을 계산하며 모든 과정을 마무리한다.
# 모델링 시작(학습)for epoch_i in range(0, epochs):print("")print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))print('Training...')# 시작 시간 설정t0 = time.time()# 로스 초기화total_loss = 0# 훈련모드로 변경model.train()# 데이터로더에서 배치만큼 반복하여 가져옴for step, batch in enumerate(train_dataloader):# 경과 정보 표시if step % 500 == 0 and not step == 0:elapsed = time_elapsed(time.time() - t0)print(' Batch {:>5,} of {:>5,}. Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))# 배치를 GPU에 넣음batch = tuple(t.to(device) for t in batch)# 배치에서 데이터 추출b_input_ids, b_input_mask, b_labels = batch# Forward 수행outputs = model(b_input_ids,token_type_ids=None,attention_mask=b_input_mask,labels=b_labels)# 로스 구함loss = outputs[0]# 총 로스 계산total_loss += loss.item()# Backward 수행으로 그래디언트 계산loss.backward()# 그래디언트 클리핑torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)# 그래디언트를 통해 가중치 파라미터 업데이트optimizer.step()# 스케줄러로 학습률 감소scheduler.step()# 그래디언트 초기화model.zero_grad()# 평균 로스 계산avg_train_loss = total_loss / len(train_dataloader)print("")print(" Average training loss: {0:.2f}".format(avg_train_loss))print(" Training epcoh took: {:}".format(time_elapsed(time.time() - t0)))# 검증 과정print("")print("Running Validation...")#시작 시간 설정t0 = time.time()# 평가모드로 변경model.eval()# 변수 초기화eval_loss, eval_accuracy = 0, 0nb_eval_steps, nb_eval_examples = 0, 0# 데이터로더에서 배치만큼 반복하여 가져옴for batch in val_dataloader:# 배치를 GPU에 넣음batch = tuple(t.to(device) for t in batch)# 배치에서 데이터 추출b_input_ids, b_input_mask, b_labels = batch# 그래디언트 계산 안함with torch.no_grad():# Forward 수행outputs = model(b_input_ids,token_type_ids=None,attention_mask=b_input_mask)# 로스 구함logits = outputs[0]# CPU로 데이터 이동logits = logits.detach().cpu().numpy()label_ids = b_labels.to('cpu').numpy()# 출력 로짓과 라벨을 비교하여 정확도 계산tmp_eval_accuracy = accuracy_measure(logits, label_ids)eval_accuracy += tmp_eval_accuracynb_eval_steps += 1print(" Accuracy: {0:.2f}".format(eval_accuracy/nb_eval_steps))print(" Validation took: {:}".format(time_elapsed(time.time() - t0)))print("")print("Training complete!")
내일 할일:
이제 BERT 모델로 학습을 완료했으니, 다음으로 테스트 데이터로도 잘 나오는지 확인을 해야한다. 이후, 교차 검증(Cross Validation)을 통해 얼마나 더 모델이 최적화 될 수 있는지를 하이퍼파라미터를 조정하여 알아보아야한다. Optimizer 설정시 정했던 학습률 및 epsilon값, optimizer 종류 및 batch_size, epoch 등을 조정해 볼 수 있을 것 같다.
물론 시간이 너무 오래 걸리고 GPU를 너무 많이 쓰지 않도록 주의하는 것이 좋겠다. 다행인것은 이미지 데이터를 다룰 때 보다 GPU를 사용하는 양은 현저히 적다는 것이다. 내가 텍스트를 사랑하는 것에 대한 예상외의 이득이라고나 할까?
BERT를 사용해보니, 이 모델이 생각보다 구동하기 복잡하지 않았다는 느낌이 들었다. 물론 그 뒤에서 돌아가는 작동 원리나 수학적 이론은 이해하지 못하지만, 어쨌든 코드 자체는 상당히 구현하고 돌려보기 쉽게 정리가 되어있었다. 물론, 이렇게 되기까지는 많은 분들의 노력이 있었을 것이다. 다시한번 라이브러리를 정리하는 분들께 감사한 마음을 가지게 된다.
교차 검증을 한 이후에는, 내가 또 할 수 있는 것이 무엇이 있을지 고민해보고 싶다. 이전 NLP과정에서 조금 더 효과적인 시각화 툴을 사용하거나 Topic Modelling등을 해보고 싶다. 하지만, 가장 중요한 것은 모든 것을 시간 내에 끝내는 것이다. 일단 모델링을 가장 최우선으로 하고, 이후에 추가적인 기능을 보완할 계획이다. 하지만, 2일 정도는 여유를 가지고, 마지막날에는 파워포인트 등 발표를 준비하고 동영상으로 내가 모델링한 결과를 데모하는 시간도 잘 할애해야한다. 이번 주 일요일까지 되도록 프로젝트의 모든 코딩을 마무리하는 것이 목표다.
계획대로 언제나 잘 되지 않을 수도 있지만, 될 수 있는한 최고의 프로젝트를 만들고 싶다. 이번 자연어처리 프로젝트가 내가 가장 해보고 싶은 것이었던 만큼 후회 없이 마무리하고 싶다. 그러자면 충분히 시간을 할애하여, 완성도를 높여야 할 것이다. 하지만, 모든 기본적인 요구사항을 만족시키는 것이 먼저다. 이번에도, 시간 분배를 잘해서 만족스러운 프로젝트를 완성시킬 수 있기를 바라고, 또 그렇게 하기 위해 노력할 것이다. 자신만의 의미있는 프로젝트를 하는 모든 이들이 오늘도 앞으로 최대한 나아가기를 바란다.
참조:
BERT 모델링의 코드는 아래의 자료를 참조하였다.