얼마 전 회사 내에서 을 주제로 세미나를 진행했다. 테스트 주도 개발의 핵심은 빠른 피드백을 통한 개발 방향 조정이다. 내가 쓴 코드가 정말 목표를 이루는 데 맞는 코드인지 확인하는 것이다. 복잡도가 매우 큰 머신러닝 프로젝트에서 소프트웨어 개발 방법론인 TDD를 사용하여 복잡도를 줄일 수 있다는 것이 내 세미나의 주장이었다. 그리고 질문이 들어왔다.
머신러닝은 일반적인 소프트웨어 개발처럼 정답이 정해져 있고 그것을 해결하는 과정이 아니라, 데이터에서 정답을 추론하는 것에 가까운데, 어떻게 정답이 정해져 있는 TDD를 사용할 수 있나요?
사실 어렵다. 머신러닝/딥러닝의 구조는 매우 복잡하다. 수정한 코드가, 바꾼 모델의 구조가 정확히 어떤 Output을 내는 지 정확히 예측하기 힘들다. 게다가 데이터 전처리, 모델 빌드, 훈련, 평가라는 일련의 과정은 linear하게 흘러가지 않는다. 이번 2021 Pycon KR에서도 코드 수정 없이 AI 모델 배포, 머신러닝 개발 프레임워크 등 머신러닝의 개발 관련 주제가 여러 등장한 이유다.
그럼에도 불구하고 내가 머신러닝 TDD를 고집하는 이유는 문서화를 통한 프로젝트 관리다. 머신러닝 엔지니어라면 바쁜 프로젝트 일정 속에서 Jupyter Notebook 파일이 몇 개나 쌓여있는 경험을 한다. 게다가 거기엔 코드는 마구잡이로 순서가 뒤얽힌 채로 적혀있다. TDD의 테스트 케이스 작성은 이러한 소모적인 개발 시간을 줄여준다.
TDD에서는 테스트를 작성한다. 하나의 테스트는 다음 테스트로 이어지는 이정표(Milestone)가 된다. 테스트 케이스를 작성함으로써 프로젝트의 단계 별 기준을 세운다. 또한 Unit 테스트를 작성하기 위해 계속해서 모듈화가 요구된다. 모듈화는 코드의 재사용성을 높임으로써 데이터 노가다 같은 작업을 줄여준다.
내가 ML에 TDD를 적용하기 위해 노력하면서 꼭 지키려고 하는 두 가지다. Git과 테스트 작성은 유기적인 관계다. 모델을 훈련시키다가도 데이터의 구조를 수정해야할 때면 반드시 목표로하는 테스트를 작성 후에 데이터를 수정한다. 테스트의 Coverage가 프로젝트의 모든 단계에 이르도록 하는 것보다는, 이슈가 생길 때마다 테스트부터 작성하는 것을 목표로 훈련하고 있다. 여러가지 실험도 해보는 중인데, processing, modeling, pipeline 등의 branch를 만들어 해당하는 분야의 코드를 적을 떄는 checkout해서 개발해 보기도 한다. 아직 까지는 실험 중이다.
여러 실험을 통해 잠정적으로 확정한 머신러닝 분야에서 TDD를 적용하는 Repository 구조다. src에 프로젝트를 모듈화하고 이를 tests에서 테스트한다. Jupyter Notebook으로 한 실험 및 테스트 결과를 notes에 저장한다. 아무리 노력을 해봐도 Jupyter Notebook으로 코드와 문서를 동시에 잡는 것은 불가능했다. 한꺼번에 정리하려다 보니 머릿속이 터져버린다.
테스트에는 PyTest를 쓴다. 파이썬의 기본 Testing Tool인 UnitTest보다 간편하다. fixture를 decorator로 관리하는 것도 매우 편하다. 내가 작성한 테스트 코드(부끄럽지만)를 보고싶다면 내 공개 레포를 참고하시길 바란다.
@pytest.mark.skip('No difference found')
def test_random_forest_drop_unimportant_feature(dataset, selected_dataset):
X_train, X_test, y_train, y_test = dataset
model_orig = RandomForestClassifier(n_estimators=100, oob_score=True)
model_orig.fit(X_train, y_train)
score_orig = model_orig.score(X_test, y_test)
print(f"{model_orig.__class__.__name__} score: {score_orig}")
print(f"{model_orig.__class__.__name__} oob score: {model_orig.oob_score_}")
assert score_orig >= 0.8, f"{model_orig.__class__.__name__} failed"
X_train, X_test, y_train, y_test = selected_dataset
model = RandomForestClassifier(n_estimators=100, oob_score=True)
model.fit(X_train, y_train)
score = model.score(X_test, y_test)
print(f"{model.__class__.__name__} score: {score}")
print(f"{model.__class__.__name__} oob score: {model.oob_score_}")
assert score > score_orig, "Model trained with selected feature score is not better than original"
예를 들어 RandomForestClassifier를 테스트해보자. 첫 번째 빌드에서 Feature Importance를 확인하고 0.01이하는 제외하고 훈련을 시켰을 때 성능이 올라갈 지를 확인하고자 한다. 기존의 dataset
Fixture에서 feature를 제외한 selected_dataset
이란 fixture를 만든다. 동일한 RandomForestClassifier를 빌드하고 훈련시킨다. 마지막 assert
문에서 score
를 비교한다.
결과적으로 차이가 없었다. 다른 소프트웨어 개발과 달리 이 테스트는 반복해서 할 필요가 없다. decorator로 skip
을 표시해주고 이유를 단다. 마지막으로 notes/model.md
에 결과를 적는다.
머신러닝 개발의 소프트웨어 방법론 적용을 익히기 위해서 해보고 싶은 다음 단계들이 있다. 하나하나 씩 적용해보며 좋으면 글로 써봐야지.