콘텐츠로 이동
Study NoteMLflow

10. 6개월 뒤 이 런을 다시 만들 수 있는가

결론부터
run을 재현하려면 코드·데이터·난수·환경 넷이 같아야 한다 — MLflow는 이 중 하나만 자동으로 잡아 준다
이 장에서 처음 나오는 말4개
digest지문
데이터 내용으로 계산한 짧은 해시. 파일 이름이 같아도 내용이 바뀌면 값이 달라진다.
dirty 작업 트리
커밋하지 않은 수정이 남아 있는 상태. 커밋 해시만으로는 그때 코드를 복원할 수 없다.
log_input
이 run이 어떤 데이터를 썼는지 dataset으로 연결하는 API다.
seed난수 시드
난수 생성의 출발점. 같은 seed면 같은 순서의 난수가 나온다.

“두 달 전 그 결과가 다시 안 나온다”의 원인은 대체로 넷 중 하나다. 지금 sshim-trader가 각각을 어디까지 잡고 있는지 먼저 정리한다.

축지금 상태빠진 것
코드mlflow.source.git.commit이 자동으로 붙는다미커밋 변경은 잡히지 않는다
데이터dataset param에 폴더 이름이 남는다같은 폴더의 내용이 바뀌면 알 수 없다
난수LGBM random_state=42가 param에 남는다평가·탐색 쪽 seed는 기록되지 않는다
환경없다Python·MLflow·LightGBM·pandas 버전이 어디에도 없다

코드 — 커밋 해시만으로는 부족하다

섹션 제목: “코드 — 커밋 해시만으로는 부족하다”

git 작업 트리 안에서 실행하면 MLflow가 mlflow.source.git.commit·branch·repoURL을 알아서 채운다. sshim-trader의 실제 DB에 이 tag가 들어 있으므로 이미 동작하고 있다.

문제는 연구 중에는 거의 항상 커밋하지 않은 상태라는 것이다. 피처 하나를 고쳐 보고 결과를 보는 게 연구인데, 그 시점의 커밋 해시는 수정 이전을 가리킨다. 해시는 있는데 그 해시로 되돌리면 다른 코드가 나온다.

한 줄로 표시할 수 있다.

import subprocess
dirty = bool(subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True).stdout.strip())
mlflow.set_tag("git_dirty", str(dirty))

git_dirty=True인 런은 “결과는 참고하되 재현은 보장 못 함”으로 읽는다. 검색으로 거를 수도 있다 — tags.git_dirty = 'False'가 신뢰할 수 있는 런의 집합이다. 채택을 결정하는 최종 검증은 항상 커밋된 상태에서 다시 돌리면 이 tag가 그 규율을 강제해 준다.

더 나가려면 git diff를 artifact로 붙인다.

diff = subprocess.run(["git", "diff", "HEAD"], capture_output=True, text=True).stdout
if diff:
mlflow.log_text(diff, "uncommitted.diff")

데이터 — 폴더 이름은 내용을 보장하지 않는다

섹션 제목: “데이터 — 폴더 이름은 내용을 보장하지 않는다”

지금은 params={"dataset": daily_dir.name}로 chart-nxt_day_202608이라는 이름이 남는다. 그런데 수집 스크립트를 다시 돌려 같은 폴더에 데이터를 더 채우면 이름은 그대로인데 내용이 다르다. 두 런의 param이 똑같은데 결과가 다른 상황이 되고, 원인을 찾을 단서가 없다.

MLflow의 dataset 추적이 이걸 푼다. 내용으로 digest를 계산해 함께 남긴다.

import mlflow
dataset = mlflow.data.from_pandas(
feat,
source=str(daily_dir),
name=daily_dir.name,
targets=label_col,
)
mlflow.log_input(dataset, context="walkforward")

실제로 계산해 보면 한 열의 값 하나만 달라져도 digest가 바뀐다.

원본 digest c2932457
한 값 수정 후 digest 3905fb3a

UI에서는 run에 연결된 dataset이 이름·digest·스키마·용도와 함께 보인다.

run에 연결된 데이터셋이 이름과 digest, 열 스키마, 사용 맥락과 함께 표시된 화면
이름 옆의 digest가 이 장의 요점이다. 같은 이름이라도 digest가 다르면 다른 데이터이고, 검색에서 datasets.digest로 거를 수 있다.출처: MLflow 공식 문서 — Dataset Tracking

context에는 그 데이터가 무슨 역할이었는지 적는다. sshim-trader라면 walkforward·holdout·backtest 정도가 맞다. 6장의 검색에서 datasets.digest와 datasets.context를 조건으로 쓸 수 있다.

난수 — 기록되지 않는 seed가 있다

섹션 제목: “난수 — 기록되지 않는 seed가 있다”

LGBM_PARAMS의 random_state=42는 param으로 남는다. 그런데 결과에 영향을 주는 seed가 더 있다.

seed어디에지금 기록되나
random_state=42LGBM_PARAMS예 — lgbm.random_state
TPESampler(seed=42)research_tune.py의 study 생성아니오
random_sharpe_distribution(seed=0)evaluate.py의 랜덤 베이스라인아니오
n_trials=300같은 함수의 랜덤 표본 수아니오

random_sharpe_mean·random_sharpe_std는 metric으로 남는데 그 값을 만든 seed와 표본 수는 안 남는다. z_vs_random이 이 분포를 기준으로 계산되므로, 베이스라인이 바뀌면 같은 전략의 z가 달라진다. common params에 두 줄을 더하는 게 맞다.

common_params = {
...,
"random_baseline_seed": 0,
"random_baseline_trials": 300,
"sampler_seed": 42,
}

환경 — 이 레포는 특히 취약하다

섹션 제목: “환경 — 이 레포는 특히 취약하다”

pyproject.toml에 이런 주석이 있다.

# mlflow 3.x가 메타데이터상 pandas<3을 핀하지만(미갱신 상한), 이 프로젝트가 쓰는
# 트래킹 경로는 pandas 3에서 동작 확인됨.
[tool.uv]
override-dependencies = ["pandas>=3.0.1"]

즉 의존성 해석기가 정상적으로는 만들지 않을 조합을 강제로 쓰고 있다. 이런 환경일수록 “그때 무슨 버전이었나”가 나중에 중요해진다. 런마다 몇 줄만 남기면 된다.

import sys, mlflow, lightgbm, pandas, numpy
mlflow.set_tags({
"env.python": sys.version.split()[0],
"env.mlflow": mlflow.__version__,
"env.lightgbm": lightgbm.__version__,
"env.pandas": pandas.__version__,
"env.numpy": numpy.__version__,
})

param이 아니라 tag로 두는 이유는 두 가지다. 실행 조건이 아니라 실행 환경이고, 나중에 정정할 수 있어야 한다.

더 확실한 방법은 lock 파일 자체를 붙이는 것이다. uv.lock은 커밋되어 있으므로 커밋 해시로도 추적되지만, git_dirty=True인 런에서는 그 보장이 없다.

mlflow.log_artifact("uv.lock") # 필요한 런에만

정리 — 런 하나에 붙을 최소 집합

섹션 제목: “정리 — 런 하나에 붙을 최소 집합”

지금까지의 내용을 합치면 재현 가능한 런은 이 정도를 갖는다.

종류항목
자동mlflow.source.git.commit · branch · mlflow.source.name · mlflow.user
taggit_dirty · env.python · env.mlflow · env.lightgbm · env.pandas · out_dir
param전략 조건 전체 + sampler_seed · random_baseline_seed · random_baseline_trials
datasetlog_input(from_pandas(...), context="walkforward")
artifactreport.html · summary.csv · (필요 시) uncommitted.diff · uv.lock

이걸 다 넣어도 log_run()은 여전히 실패를 삼킨다. 기록이 늘수록 그 계약이 더 중요해진다 — 재현 정보를 모으다가 연구가 멈추면 본말이 뒤집힌다.