텍스트 애널리틱스 수업 팀 프로젝트 - Finetuning Pipeline
2025-06-27
들어가기 전
프로젝트에 대한 전반적인 설명은 이전 글을 참고해주세요.
객체 지향이나 여러 디자인 원칙 등을 제대로 학습하지 않고 프로젝트를 진행했습니다. 전반적인 구조부터 코드의 설계가 단단히 잘못되었음을 인지하고 있었으나, 이를 리뷰해주는 사람이 없어 급한대로 LLM을 최대한 활용해 모듈을 분리했습니다.
이후 SPLADE 모델의 코드를 천천히 보며 추상화와 의존성 주입이 잘 된 코드를 확인할 수 있었고, 종강 이후 더 좋은 설계가 무엇일지 리팩토링 하는 시간을 가졌습니다. 이때 '파이썬 클린코드 (Clean Code in Python)'와 인프런 파이썬 강의를 수강하며 Python이란 언어 자체를 이해하고자 했습니다.
Finetuning 구조
src/finetune_pipeline에는 base 디렉토리에 공통 모듈을 작성하고, SPECTER2, DPR, SPLADE 세 모델의 디렉토리를 분리하여 작성했다. 각각의 모델 디렉토리마다 model.py, trainer.py를 정의한 뒤, finetune.py에서 두 클래스를 활용해 파인튜닝을 진행한다.
파일 구조
finetune_pipeline/
├── README.md
├── base/
│ ├── data.py
│ ├── loss.py
│ └── retrieval.py
├── data/
│ └── triplet_data.json
├── dpr/
│ ├── README.md
│ ├── finetune_inbatch.py
│ ├── finetune_triplet.py
│ ├── model.py
│ ├── trainer_inbatch.py
│ └── trainer_triplet.py
├── specter2/
│ ├── README.md
│ ├── evaluate.py
│ ├── finetune.py
│ ├── finetune_specter2_with_the_augmented_data.ipynb
│ ├── model.py
│ ├── trainer.py
├── splade/
│ ├── README.md
│ ├── finetune.py
│ ├── train.py
│ ├── trainer.py
│ ├── conf/
SPECTER2, DPR, SPLADE 세 모델은 Query와 Document를 따로 인코딩한다는 공통점이 있다. 이러한 구조를 추상화한 클래스를 base/retrieval.py
에 작성했다. (Retrieval class는 LitSearch Repositry를 많이 참고했다.)
현재 코드의 문제점과 해결 방향성
- 한 클래스에 부과된 많은 기능
- 각 모델을 정의한
model.py
를 보면 클래스 안에 여러 메서드가 존재한다. model loading, encoding, indexing, retrieval 등 매우 다양한 기능들이 혼재되어 있는데, 딱 봐도 하나의 클래스가 너무 많은 역할을 담당하고 있다. 위에서 언급된 기능들을 SPLADE 코드에서는 전부 분리된 클래스로 정의되어 있다. 이를 참고하여 리팩토링한다면 유지보수성은 물론 코드의 가독성이 매우 좋아질 것이다.
- 무분별하게 선언되는 상수값들
- 개발 동아리에서 프론트 기능을 작성할 때는 constants 디렉토리를 따로 두고 관리했지만, 이 코드에서는 우선 돌아가는 기능을 빠르게 작성하다보니 상수 관리를 제대로 하지 못했다. 선언된 값들이 혼재되지 않고 체계적으로 잘 관리할 필요가 있다.
리팩토링
리팩토링 과정에서는 SPLADE 레포지토리를 많이 참고했다. 특히 시간에 좇기며 프로젝트를 마무리할 때 SPLADE 코드를 보고, 내 코드와는 차원이 다르게 깔끔하단 걸 깨달았다. 어영부영 프로젝트는 마무리했지만 종강하자마자 곧장 코드 설계와 관련된 책을 읽고, SPLADE 코드를 참고하여 리팩토링하고자 했다. 리팩토링은 우선 가장 많이 시간을 쏟았던 SPECTER2 모델에 대해서만 진행했다.
리팩토링 코드 링크를 보면 구조는 다음과 같다.
src/refactored_pipeline/
├── baseline.py
├── finetune.py
├── conf/
│ └── finetune_specter2.yaml
├── datasets/
│ ├── data.py
│ └── triplet_data.json
├── losses/
│ └── loss.py
├── models/
│ └── base.py
├── tasks/
│ ├── base/
│ │ ├── base_trainer.py
│ │ ├── early_stopping.py
│ │ └── saver.py
│ ├── evaluator.py
│ └── trainer.py
├── utils/
│ ├── data_processing_utils.py
│ ├── os_utils.py
│ └── retrieval_utils.py
└── vessl/
├── baseline.yml
└── finetune.yml
기존 모델 클래스가 하던 일을 전부 분리했다. indexing, retrieval, finetuning 기능은 tasks에 정의되어 있고, 각각의 클래스들은 model을 파라미터로 받아 분리된 기능을 수행한다. 모델은 models/base.py의 Specter2Base가 추상 클래스인데(abc.ABC 상속) 생성자를 제외하고는 encode, _encode밖에 없다. 오직 인코딩만 담당하는 것이다.
현재는 SPECTER2 모델에 대해서만 리팩토링을 진행하고 있어서 Specter2Base 클래스가 adapter type과 같은 모델에 특수한 값을 지정하고 있다. 만일 다른 모델까지도 묶을 수 있는 상위 추상 클래스를 작성한다면 forward만 정의해야 할 것이다. 그리고 SPLADE는 MLM, DPR은 mean pooling, SPECTER는 CLS를 사용하므로 이에 대한 값을 파라미터로 받아 hidden state에 대해 각기 다른 출력을 반환하도록 작성해볼 수 있겠다. (실은 SPLADE에서 이러한 구조를 사용하고 있다.)
무엇보다 SPLADE 코드에서 가장 재미있었던 건 hydra를 사용한 상수 관리 및 logging과 학습을 도와주는 early_stopping, save 등과 같은 유틸리티들이었다. 이 중 유틸성 클래스들은 우선은 코드에 붙이긴 했지만, 6월 28일 기준으로 당장은 사용하고 있지 않다. 우선 리팩토링 자체가 최우선 목표였으므로... 지금은 log와 파일 관리, 그리고 hydra를 통한 상수값 관리만을 사용하고 있다.
리팩토링하고 나니 훨씬 가독성이 좋아졌다. 디버깅과 싸우며 삽질했던 지난 날이 떠오른다. 이렇게 깔끔한 구조였다면 그렇게 시간을 오래 잡아먹진 않았을텐데. 확실히 잘 쓴 남의 코드를 보고 이해하니 바라보는 게 훨씬 달라지는 듯!