본문으로 건너뛰기
01. MLflow 기본과 로컬 실험 기록

01. MLflow 기본과 로컬 실험 기록

✎ 이 문서 수정하기 GitHub에서 수정

학습 코드를 실행할 때마다 설정과 결과를 같은 기준으로 남기는 것부터 시작했다. 이 편은 Azure 구독 없이 로컬 Python에서 MLflow를 익히는 단계다.

도구의 전체 범위는 MLflow 기능과 구성에, MLOps에서의 위치는 목차의 MLflow와 BentoML 설명에 두었다. 여기서는 MLflow의 실험 기록부터 실제 코드로 확인한다.

준비 조건과 이번 편의 결과

Python 3.11과 PowerShell을 사용한다. 새 실습 폴더를 만들고 아래 파일을 저장한다. 이 편을 마치면 서로 다른 C를 가진 세 개의 run, 검증 보고서와 모델 파일이 남아야 한다. AML 플러그인과 workspace는 아직 필요하지 않다.

실험 기록의 단위

개념이 실습에서의 의미예시
Experiment비교할 실행을 묶는 단위mlflow-wine-study
Run한 번의 학습과 기록C=0.1인 학습 한 번
Parameter실행에 사용한 설정C, 분할 시드, 데이터 지문
Metric실행 결과의 수치검증 정확도, 검증 log loss
Artifact파일 형태의 결과분류 보고서, 학습 코드
MLflow Model모델과 입출력 및 환경 정보의 묶음model 경로의 Pipeline
Registered Model이름 아래 버전으로 관리할 모델wine-logistic-study의 특정 버전

Tracking URI는 기록을 보낼 대상을 가리킨다. 학습을 실행할 컴퓨터를 정하는 값은 아니다. 이 구분이 로컬 실습에서 AML로 넘어갈 때의 기준이다.

모델을 아티팩트로 올리는 것과 MLflow 모델로 기록하는 것도 다르다. log_artifact()는 파일을 올리고, mlflow.sklearn.log_model()은 모델 파일에 더해 MLmodel, 의존성, 입출력 signature 같은 정보를 묶는다.

실습 환경과 파일 구성

명령은 Windows PowerShell을 기준으로 적었다. 별도 실습 폴더에서 실행하며, Python 3.11이 설치되어 있다는 전제다. 가상환경 활성화 없이 해당 환경의 실행 파일을 직접 호출한다.

New-Item -ItemType Directory -Path mlflow-aml-lab
Set-Location .\mlflow-aml-lab
py -3.11 -m venv .venv
New-Item -ItemType Directory -Path src

이 편에서 만드는 파일은 requirements.txt와 src\train.py다. 뒤의 편에서도 같은 폴더를 사용하며 필요한 파일을 추가한다.

mlflow-aml-lab
  requirements.txt
  src
    train.py

패키지 버전

이 예제는 MLflow 전체의 최신 기능보다 AML 모델 연동 경로에 맞췄다. 공식 모델 로깅 문서는 현재 MLflow 2.16.x 이하를 사용하도록 명시하고 있어 2.16.2로 고정했다. MLflow 3.x의 예제를 섞지 않는다.

연결된 모델 변환 문서에는 name=을 사용하는 예시도 있다. 이 노트의 실행 코드는 2.16.2 기준의 artifact_path=로 통일했다. 서로 다른 문서의 예제를 가져올 때 호출 인자와 설치 버전을 함께 확인할 부분이다.

requirements.txt:

mlflow==2.16.2
scikit-learn==1.5.1
pandas==2.2.3
numpy==1.26.4
sqlalchemy==2.0.43
.\.venv\Scripts\python.exe -m pip install -r .\requirements.txt
.\.venv\Scripts\python.exe -m pip check

SQLite 저장소에 사용하는 SQLAlchemy도 함께 고정했다. MLflow 버전만 고정해도 나머지 패키지의 새 버전이 설치되면서 실행이 달라질 수 있기 때문이다. 직접 지정하지 않은 의존성은 설치 시점에 결정되므로, 같은 조합으로 환경을 다시 만들려면 설치 후 pip freeze 결과도 보관한다.

.\.venv\Scripts\python.exe -m pip freeze | Set-Content -Encoding utf8 .\requirements.local.lock.txt

이 버전 고정은 예제의 실행 기준이다. 공개 MLflow 서버 운영을 위한 보안 기준이나 최신 버전 권고를 뜻하지 않는다. 아래 UI는 로컬 루프백 주소에서만 사용한다.

원격 기록용 클라이언트만 필요하면 mlflow-skinny를 검토할 수 있다. 다만 SQLite 저장소, 로컬 서버와 UI를 포함하는 이번 실습에서는 전체 mlflow 패키지를 사용한다.

로컬에서 한 번의 학습 기록

데이터와 평가 기준

전체 데이터를 학습 60%, 검증 20%, 테스트 20% 비율로 나눈다. 표본 수는 분할 과정에서 정수로 조정된다. 시드와 분할 방식은 실행마다 같게 유지하고, C만 바꾼다.

C는 Logistic Regression의 규제 강도와 반대 방향으로 움직이는 값이다. 이 실습에서는 검증 정확도가 높은 후보를 고르고, 정확도가 같으면 검증 log loss가 낮은 후보를 고른다. 테스트 데이터는 후보 선택에 쓰지 않는다.

전처리인 StandardScaler와 분류기를 하나의 Pipeline으로 묶는다. 학습 데이터로만 전처리를 학습하고, 검증과 테스트에는 이미 학습한 변환을 적용하기 위해서다.

학습 코드

src\train.py:

import argparse
import hashlib
import platform
from importlib.metadata import version
from time import perf_counter

import mlflow
import mlflow.sklearn
from mlflow.models import infer_signature
from sklearn.datasets import load_wine
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, log_loss
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler


def load_data():
    dataset = load_wine(as_frame=True)
    fingerprint = hashlib.sha256(
        dataset.frame.to_csv(index=False).encode("utf-8")
    ).hexdigest()
    X_train, X_holdout, y_train, y_holdout = train_test_split(
        dataset.data,
        dataset.target,
        test_size=0.4,
        random_state=42,
        stratify=dataset.target,
    )
    X_val, X_test, y_val, y_test = train_test_split(
        X_holdout,
        y_holdout,
        test_size=0.5,
        random_state=42,
        stratify=y_holdout,
    )
    return X_train, X_val, X_test, y_train, y_val, y_test, fingerprint


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--C", type=float, required=True)
    parser.add_argument("--autolog", action="store_true")
    args = parser.parse_args()
    if args.C <= 0:
        parser.error("--C must be greater than zero")

    X_train, X_val, _, y_train, y_val, _, fingerprint = load_data()
    model = Pipeline([
        ("scale", StandardScaler()),
        ("classifier", LogisticRegression(
            C=args.C, max_iter=1000, random_state=42
        )),
    ])

    if args.autolog:
        mlflow.sklearn.autolog(log_models=False, log_datasets=False)

    with mlflow.start_run() as run:
        mlflow.set_tags({
            "study": "mlflow-aml",
            "model_type": "scaled-logistic-regression",
        })
        mlflow.log_params({
            "C": args.C,
            "split_seed": 42,
            "dataset": "sklearn.load_wine",
            "dataset_sha256": fingerprint,
            "train_rows": len(X_train),
            "validation_rows": len(X_val),
        })

        started = perf_counter()
        model.fit(X_train, y_train)
        fit_seconds = perf_counter() - started
        predictions = model.predict(X_val)
        probabilities = model.predict_proba(X_val)
        mlflow.log_metrics({
            "validation_accuracy": accuracy_score(y_val, predictions),
            "validation_log_loss": log_loss(y_val, probabilities),
            "fit_seconds": fit_seconds,
        })
        mlflow.log_dict(
            classification_report(
                y_val, predictions, output_dict=True, zero_division=0
            ),
            "reports/validation.json",
        )
        mlflow.log_artifact(__file__, artifact_path="source")
        mlflow.log_dict(
            {
                "python": platform.python_version(),
                "packages": {
                    name: version(name)
                    for name in [
                        "mlflow", "scikit-learn", "pandas", "numpy",
                        "cloudpickle",
                    ]
                },
            },
            "environment.json",
        )
        mlflow.sklearn.log_model(
            sk_model=model,
            artifact_path="model",
            signature=infer_signature(X_train, model.predict(X_train)),
            input_example=X_train.head(3),
        )
        print(f"tracking_uri={mlflow.get_tracking_uri()}")
        print(f"run_id={run.info.run_id}")


if __name__ == "__main__":
    main()

Tracking URI와 experiment 이름을 학습 코드에 고정하지 않았다. 로컬에서는 환경 변수로 지정하고, AML Job에서는 서비스가 제공하는 실행 문맥을 사용한다.

with mlflow.start_run()은 실행의 시작과 종료를 묶는다. 예외가 발생하면 정상 완료와 구분된 실행 상태가 남는다. 실행 하나에서 C를 계속 덮어쓰는 대신 값마다 별도 run을 만든다.

세 번의 실행

모든 명령은 실습 폴더의 루트에서 실행한다. 상대 SQLite 경로를 사용하므로 작업 폴더가 바뀌면 다른 DB를 읽게 된다.

$env:MLFLOW_TRACKING_URI = "sqlite:///mlflow.db"
$env:MLFLOW_REGISTRY_URI = $env:MLFLOW_TRACKING_URI
$env:MLFLOW_EXPERIMENT_NAME = "mlflow-wine-study"

foreach ($c in @("0.01", "0.1", "1.0")) {
    .\.venv\Scripts\python.exe .\src\train.py --C $c
    if ($LASTEXITCODE -ne 0) { throw "Training failed for C=$c" }
}

SQLite에는 실행 메타데이터가, 로컬 아티팩트 저장소에는 보고서와 모델 파일이 남는다. 이 예제의 기본 로컬 아티팩트 위치는 mlruns다. DB 파일만 복사하면 모델 파일까지 옮겨지는 것은 아니다.

UI에서 확인할 항목

같은 폴더에서 별도 PowerShell 창을 열고 실행한다.

.\.venv\Scripts\mlflow.exe ui --backend-store-uri sqlite:///mlflow.db --host 127.0.0.1 --port 5000

브라우저에서 http://127.0.0.1:5000을 연다. UI 종료는 해당 터미널의 Ctrl+C로 처리한다.

확인 위치남아 있어야 할 내용
Experimentmlflow-wine-study 아래 세 번의 실행
Parameters서로 다른 C, 같은 데이터 지문과 분할 시드
Metrics검증 정확도, 검증 log loss, 학습 시간
Artifactsreports/validation.json, source/train.py, environment.json
Modelmodel 아래 MLmodel과 모델 파일 및 환경 정보

Signature는 모델이 기대하는 열과 타입을 설명한다. 입력 예시는 호출 형태를 확인하는 자료다. 두 정보가 데이터 품질이나 업무상 정확성까지 보장하지는 않는다.

기록 API를 고르는 기준

메트릭과 파일 기록 문서는 기록의 타입에 따라 API를 나눈다. 보고서를 출력한 것과 구조화된 메트릭으로 저장한 것은 다르다.

남길 정보사용할 API이번 예제와의 연결
실행 설정log_param(), log_params()C, 분할 시드, 데이터 지문
숫자로 비교할 결과log_metric(), log_metrics()검증 정확도와 log loss
단계별로 변하는 숫자log_metric()의 stepepoch별 손실 등을 기록할 때 확장
JSON 또는 YAML 보고서log_dict()클래스별 검증 보고서
기존 파일log_artifact(), log_artifacts()학습 코드와 추가 결과 파일
이미지와 그래프log_image(), log_figure()혼동행렬이나 시각화로 확장
실행을 분류할 정보set_tag(), set_tags()이번 학습을 구분하는 study tag

AML Job의 print() 출력은 진단 로그에서 볼 수 있지만 메트릭 차트의 값으로 자동 변환되지는 않는다. 코드의 오류나 진행 상황은 로그에, 실행 간 비교할 숫자는 MLflow 메트릭에 남기는 식으로 나누었다.

Autologging을 더했을 때의 차이

수동 기록을 확인한 뒤 자동 기록을 더하는 순서로 잡았다.

.\.venv\Scripts\python.exe .\src\train.py --C 0.1 --autolog

--autolog는 mlflow.sklearn.autolog()를 켠다. 이 예제에서는 모델의 자동 기록을 끄고, 같은 log_model()에서 signature와 입력 예시를 기록한다. 자동 기록과 수동 기록으로 같은 모델을 중복해서 남기지 않기 위해서다.

자동 기록으로 늘어나는 항목은 프레임워크와 버전에 따라 달라진다. 업무상 평가 기준, 데이터 이용 조건, 선택 이유까지 자동으로 기록되는 것은 아니다. 이 실습의 검증 메트릭과 데이터 지문은 수동으로 남긴다.

확인할 결과

처음 세 번의 실행과 --autolog 실행은 서로 다른 run이다. 자동 기록 예제까지 실행하면 run 수가 하나 더 늘어난다. 설정은 Parameters, 숫자는 Metrics, 보고서와 코드는 Artifacts에서 확인한다. 다음 편에서는 이 기록을 UI가 아닌 코드로 다시 조회한다.

오류 확인

증상확인할 부분
mlflow를 import하지 못함.venv 안의 Python으로 설치하고 실행했는지
SQLAlchemy 관련 import 오류고정한 requirements.txt와 실제 설치 버전이 같은지
기록이 다른 DB에 나타남현재 작업 폴더와 MLFLOW_TRACKING_URI
--C 0 실행이 중단됨양수만 받도록 한 입력 검사이며 정상 동작
UI 포트를 사용할 수 없음이미 사용 중인 포트인지 확인하고 다른 로컬 포트를 선택

정리와 생각할 점

UI는 실행한 터미널에서 Ctrl+C로 종료한다. 다음 편에서 사용할 DB와 아티팩트는 남긴다.

  • 학습 성공과 모델 기록 성공을 같은 것으로 보고 있지 않은가.
  • DB 파일만 남기고 mlruns를 잃으면 어떤 기록을 다시 읽을 수 없는가.
  • 자동 기록 외에 데이터와 평가 조건을 설명할 정보는 무엇인가.

참고 자료

시리즈 목차, 다음: 02. 실행 비교와 메트릭 및 아티팩트 조회

마지막 수정 일자