LLM 엔지니어링19장 · MLOps 와 LLMOps, 워크플로 자동화18 / 23← 이전목차다음 →MetaContents

19장. MLOps 와 LLMOps, 워크플로 자동화

실습 4 — MLflow 로 실험 기록하기

17장의 문의 분류기를 여러 설정으로 학습하며 MLflow 에 기록합니다. 특성 두 가지(글자 조각 TF-IDF, e5 임베딩)와 규제 세기 C 네 가지로 여덟 번 학습하고, 기록을 표로 뽑아 가장 나은 모델을 레지스트리에 등록합니다. 학습은 inquiry_train.csv(틀로 만든 문의 200개), 시험은 10장의 손으로 쓴 inquiries.csv(40개)로 합니다. 둘 다 지어낸 연습용 데이터입니다. MLflow 3.17.2 가 있어야 합니다.

1단계. 기록할 곳 정하기

Python
import hashlib
import shutil
from pathlib import Path

import mlflow
import mlflow.sklearn
import pandas as pd
from mlflow import MlflowClient

# 다시 돌려도 같은 결과가 나오게 지난 기록을 지운다
Path("out_mlflow.db").unlink(missing_ok=True)
shutil.rmtree("out_mlruns", ignore_errors=True)
# 기록은 SQLite 파일 하나에, 모델 같은 파일(산출물)은 out_mlruns 폴더에 둔다
mlflow.set_tracking_uri("sqlite:///out_mlflow.db")
mlflow.create_experiment("문의분류", artifact_location=Path("out_mlruns").resolve().as_uri())
mlflow.set_experiment("문의분류")

train = pd.read_csv("inquiry_train.csv")
test = pd.read_csv("inquiries.csv")
# 데이터의 지문: 같은 파일로 학습했는지 나중에 확인할 수 있게 기록에 남긴다
data_fp = hashlib.sha256(Path("inquiry_train.csv").read_bytes()).hexdigest()[:12]
# 결과: 학습·시험 행 수와 지문
print(len(train), len(test), data_fp)
출력
200 40 879fd8372cac

2단계. 설정을 바꿔 가며 기록하기

Python
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.pipeline import make_pipeline

emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
# e5 는 문장 앞에 "query: " 를 붙여 임베딩한다(7차시). 학습·시험 문장을 한 번씩만 임베딩해 둔다
E_train = emb.encode(["query: " + q for q in train["문의"]], normalize_embeddings=True)
E_test = emb.encode(["query: " + q for q in test["문의"]], normalize_embeddings=True)

models = {}
for features in ["tfidf", "e5"]:
    for C in [0.3, 1, 3, 10]:
        name = f"{features}-C{C}"
        # start_run: 실행 하나를 연다. with 블록이 끝나면 실행이 닫힌다
        with mlflow.start_run(run_name=name):
            if features == "tfidf":
                model = make_pipeline(TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3)),
                                      LogisticRegression(C=C, max_iter=1000)).fit(train["문의"], train["분류"])
                pred = model.predict(test["문의"])
            else:
                model = LogisticRegression(C=C, max_iter=1000).fit(E_train, train["분류"])
                pred = model.predict(E_test)
            # 설정값(파라미터), 점수(지표), 메모(태그)를 남긴다
            mlflow.log_params({"features": features, "C": C, "train_rows": len(train)})
            mlflow.log_metrics({"accuracy": (pred == test["분류"]).mean(),
                                "macro_f1": f1_score(test["분류"], pred, average="macro")})
            mlflow.set_tag("data_fp", data_fp)
        models[name] = model
print("기록한 실행:", len(models))
출력
기록한 실행: 8

3단계. 기록을 표로 뽑아 견주기

Python
# search_runs: 실험의 실행을 표(DataFrame)로 돌려준다. 정확도, 같으면 macro F1 이 높은 차례
runs = mlflow.search_runs(experiment_names=["문의분류"], order_by=["metrics.accuracy DESC", "metrics.macro_f1 DESC"])
table = runs[["tags.mlflow.runName", "params.features", "params.C", "metrics.accuracy", "metrics.macro_f1"]]
table.columns = ["실행", "특성", "C", "정확도", "macro_f1"]
# 결과: 여덟 실행의 점수(소수 셋째 자리)
print(table.round(3).to_string(index=False))
출력
        실행    특성   C   정확도  macro_f1
   e5-C0.3    e5 0.3 0.700     0.674
    e5-C10    e5  10 0.700     0.652
     e5-C3    e5   3 0.700     0.652
 tfidf-C10 tfidf  10 0.675     0.671
     e5-C1    e5   1 0.675     0.630
  tfidf-C3 tfidf   3 0.625     0.628
tfidf-C0.3 tfidf 0.3 0.625     0.621
  tfidf-C1 tfidf   1 0.600     0.601

4단계. 가장 나은 모델을 등록하기

Python
best = runs.iloc[0]
best_name = best["tags.mlflow.runName"]
# 고른 실행에 모델 파일을 남기고, 레지스트리의 'inquiry-clf' 에 새 버전으로 등록한다
with mlflow.start_run(run_id=best["run_id"]):
    info = mlflow.sklearn.log_model(models[best_name], name="model", registered_model_name="inquiry-clf",
                                    pip_requirements=["scikit-learn==1.9.1"])
client = MlflowClient()
version = info.registered_model_version        # 새로 붙은 버전 번호
# 서비스가 부를 이름표: champion. 이 모델은 e5 임베딩 위의 분류기라 임베딩 모델 이름도 버전에 적는다
client.set_registered_model_alias("inquiry-clf", "champion", version)
client.set_model_version_tag("inquiry-clf", str(version), "embedding", "intfloat/multilingual-e5-small")
mv = client.get_model_version_by_alias("inquiry-clf", "champion")
# 결과: 등록한 실행, 버전, 별칭
print(best_name, "→ inquiry-clf 버전", mv.version, "· 별칭", mv.aliases, f"· 정확도 {best['metrics.accuracy']:.3f}")
출력
e5-C0.3 → inquiry-clf 버전 1 · 별칭 ['champion'] · 정확도 0.700

이 실습이 뜻하는 것

이 실습은 찌개 레시피를 여러 번 바꿔 끓여 보며 요리 노트에 적는 일과 같습니다. 노트와 냄비 보관함을 정하고, 어떤 재료로 끓였는지 재료의 지문까지 적어 둡니다(1단계). 육수 두 가지와 간 세기 네 가지로 여덟 번 끓이고, 매번 설정과 맛 점수를 적습니다(2단계). 노트를 표로 펼쳐 점수 차례로 줄 세우고(3단계), 가장 맛있는 냄비를 메뉴판에 올려 "오늘의 메뉴" 표를 붙입니다(4단계). 여기서 요리 노트가 MLflow(ML 은 Machine Learning, 머신러닝)의 실험 기록, 냄비 한 번이 실행(run), 육수가 특성(글자 조각 TF-IDF 와 e5 임베딩), 간 세기가 C, 메뉴판이 모델 레지스트리, "오늘의 메뉴" 표가 champion 별칭입니다. TF-IDF 는 Term Frequency-Inverse Document Frequency(자주 나오되 흔하지 않은 조각에 무게를 주는 방법)입니다. 비유와 다른 점은, 맛 점수를 사람의 혀가 아니라 시험 문의 40개 가운데 몇 개를 맞혔는지로 매긴다는 것입니다.

단계출력뜻
1. 데이터200 40 879fd8372cac학습 문의 200개, 시험 문의 40개입니다. 끝의 12글자는 학습 파일의 지문으로, 나중에 같은 파일로 학습했는지 확인할 때 씁니다
2. 기록기록한 실행: 8특성 2가지 × C 4가지 = 8번 학습했고, 모두 기록에 남았습니다
3. 1등e5-C0.3 정확도 0.700 · macro_f1 0.674정확도 0.700 으로 같은 셋(e5-C0.3, e5-C10, e5-C3) 가운데 macro F1(분류마다 F1 점수를 고르게 평균한 값)이 가장 높아 맨 위에 왔습니다. 40개 가운데 28개를 맞힌 셈입니다
3. 꼴찌tfidf-C1 정확도 0.600같은 데이터라도 설정에 따라 정확도가 0.600 에서 0.700 까지 갈립니다
3. 특성 견주기e5 넷 가운데 셋이 0.700 · tfidf 가장 높은 것 0.675이 시험 묶음에서는 e5 임베딩이 글자 조각 TF-IDF 보다 대체로 나았습니다
4. 등록e5-C0.3 → inquiry-clf 버전 1 · 별칭 ['champion']1등 모델을 레지스트리에 버전 1로 올리고 champion 이름표를 붙였습니다

한 줄로 줄이면, 여러 번의 학습을 설정, 점수, 데이터 지문과 함께 한곳에 적어 두고, 가장 나은 모델에 이름표를 붙여 내놓을 준비를 한 실습입니다. 실습 5는 이 champion 에 도전자를 세우고, 실습 6은 이 기록 위에 파이프라인을 쌓습니다. 시험 문의가 40개뿐이라 정확도 0.025 는 한 문제 차이입니다. 0.700 과 0.675 의 차이를 "e5 가 확실히 낫다"로 읽기에는 시험 묶음이 작습니다.

코드 설명

  • mlflow.set_tracking_uri("sqlite:///out_mlflow.db") 는 기록을 SQLite 파일에 두게 합니다. create_experiment(..., artifact_location=...) 로 모델 파일을 둘 폴더를 정합니다.
  • with mlflow.start_run(run_name=...) 안에서 log_params, log_metrics, set_tag 로 실행 하나의 설정, 점수, 메모를 남깁니다.
  • mlflow.search_runs(..., order_by=[...]) 는 기록을 표로 돌려줍니다. 열 이름은 params., metrics., tags. 로 시작합니다.
  • log_model(..., registered_model_name=...) 은 모델 파일을 남기고 레지스트리에 새 버전으로 등록합니다. pip_requirements 를 주면 패키지 목록을 알아내는 시간을 아낍니다.
  • set_registered_model_alias(이름, "champion", 버전) 은 별칭을 붙이고, get_model_version_by_alias 로 별칭이 가리키는 버전을 찾습니다.
← 이전 절19장 · 실습 3 — 색인 갱신 작업 자동화다음 절 →19장 · 실습 5 — 모델 레지스트리로 배포하고 되돌리기