19장. MLOps 와 LLMOps, 워크플로 자동화
실습 4 — MLflow 로 실험 기록하기
17장의 문의 분류기를 여러 설정으로 학습하며 MLflow 에 기록합니다. 특성 두 가지(글자 조각 TF-IDF, e5 임베딩)와 규제 세기 C 네 가지로 여덟 번 학습하고, 기록을 표로 뽑아 가장 나은 모델을 레지스트리에 등록합니다. 학습은 inquiry_train.csv(틀로 만든 문의 200개), 시험은 10장의 손으로 쓴 inquiries.csv(40개)로 합니다. 둘 다 지어낸 연습용 데이터입니다. MLflow 3.17.2 가 있어야 합니다.
1단계. 기록할 곳 정하기
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 879fd8372cac2단계. 설정을 바꿔 가며 기록하기
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))기록한 실행: 83단계. 기록을 표로 뽑아 견주기
# 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.6014단계. 가장 나은 모델을 등록하기
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로 별칭이 가리키는 버전을 찾습니다.