19장. MLOps 와 LLMOps, 워크플로 자동화
실습 5 — 모델 레지스트리로 배포하고 되돌리기
실습 4의 챔피언에 도전자를 세웁니다. 10월에 상담원이 손으로 분류한 문의 40개(inquiry_oct.csv, 지어낸 연습용 데이터)를 학습 데이터에 더해 새 버전을 등록하고, 견준 뒤 별칭을 옮깁니다. 문제가 생긴 상황을 가정해 되돌리기도 해 봅니다. 실습 4를 먼저 실행해야 합니다.
1단계. 챔피언 불러오기
import mlflow
import mlflow.sklearn
import numpy as np
import pandas as pd
from mlflow import MlflowClient
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score, recall_score
mlflow.set_tracking_uri("sqlite:///out_mlflow.db") # 실습 4의 기록
mlflow.set_experiment("문의분류")
client = MlflowClient()
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
def embed(texts):
return emb.encode(["query: " + t for t in texts], normalize_embeddings=True)
test = pd.read_csv("inquiries.csv")
E_test = embed(test["문의"])
# 서비스처럼 버전 번호가 아니라 별칭으로 부른다
champion = mlflow.sklearn.load_model("models:/inquiry-clf@champion")
mv = client.get_model_version_by_alias("inquiry-clf", "champion")
champ_C = float(client.get_run(mv.run_id).data.params["C"]) # 챔피언을 만든 실행의 설정을 읽는다
champ_acc = (champion.predict(E_test) == test["분류"]).mean()
# 결과: 챔피언의 버전, 설정, 시험 정확도
print("챔피언 버전", mv.version, "· C", champ_C, f"· 정확도 {champ_acc:.3f}")챔피언 버전 1 · C 0.3 · 정확도 0.7002단계. 도전자를 학습해 등록하기
train = pd.concat([pd.read_csv("inquiry_train.csv"), pd.read_csv("inquiry_oct.csv")], ignore_index=True)
# 챔피언과 같은 설정(C)으로, 데이터만 늘려 학습한다. 무엇이 달라졌는지 하나로 좁히기 위해서다
with mlflow.start_run(run_name="e5-oct"):
challenger = LogisticRegression(C=champ_C, max_iter=1000).fit(embed(train["문의"]), train["분류"])
pred = challenger.predict(E_test)
mlflow.log_params({"features": "e5", "C": champ_C, "train_rows": len(train), "batches": "inquiry_oct.csv"})
mlflow.log_metrics({"accuracy": (pred == test["분류"]).mean(), "macro_f1": f1_score(test["분류"], pred, average="macro")})
info = mlflow.sklearn.log_model(challenger, name="model", registered_model_name="inquiry-clf",
pip_requirements=["scikit-learn==1.9.1"])
new_version = info.registered_model_version
client.set_registered_model_alias("inquiry-clf", "challenger", new_version)
# 결과: 도전자의 학습 행 수와 버전
print("도전자 버전", new_version, "· 학습", len(train), "행")도전자 버전 2 · 학습 240 행3단계. 견주고 별칭 옮기기
labels = sorted(test["분류"].unique())
compare = pd.DataFrame({
"챔피언": recall_score(test["분류"], champion.predict(E_test), labels=labels, average=None),
"도전자": recall_score(test["분류"], challenger.predict(E_test), labels=labels, average=None),
}, index=labels)
# 결과: 분류마다의 재현율(그 분류의 문의를 몇 할 맞혔나)과 전체 정확도
print(compare.round(3))
chal_acc = (challenger.predict(E_test) == test["분류"]).mean()
print(f"정확도 챔피언 {champ_acc:.3f} · 도전자 {chal_acc:.3f}")
if chal_acc >= champ_acc: # 낮지 않으면 올린다(실습 6에서 이 판단을 게이트로 묶는다)
client.set_registered_model_alias("inquiry-clf", "previous", mv.version)
client.set_registered_model_alias("inquiry-clf", "champion", new_version)
client.delete_registered_model_alias("inquiry-clf", "challenger")
print("별칭:", client.get_registered_model("inquiry-clf").aliases) 챔피언 도전자
결제 0.667 0.778
교환반품 0.625 0.875
기타 0.286 0.286
배송 0.875 0.875
회원 1.000 1.000
정확도 챔피언 0.700 · 도전자 0.775
별칭: {'champion': 2, 'previous': 1}4단계. 되돌리기
# 새 챔피언을 내보낸 뒤 문제가 생겼다고 가정한다: champion 을 previous 가 가리키는 버전으로 옮긴다
prev = client.get_model_version_by_alias("inquiry-clf", "previous").version
client.set_registered_model_alias("inquiry-clf", "champion", prev)
# 서비스 코드는 그대로 '@champion' 을 부르는데, 받는 모델이 바뀐다
again = mlflow.sklearn.load_model("models:/inquiry-clf@champion")
print("되돌린 뒤 champion →", client.get_model_version_by_alias("inquiry-clf", "champion").version,
f"· 정확도 {(again.predict(E_test) == test['분류']).mean():.3f}")
# 확인이 끝나 다시 새 버전으로 돌려 둔다(실습 6이 이 상태에서 시작한다)
client.set_registered_model_alias("inquiry-clf", "champion", new_version)
print("별칭:", client.get_registered_model("inquiry-clf").aliases)되돌린 뒤 champion → 1 · 정확도 0.700
별칭: {'champion': 2, 'previous': 1}이 실습이 뜻하는 것
이 실습은 동네 축구팀의 주장 완장을 넘기는 일과 같습니다. 지금 주장의 기록을 확인하고(1단계), 같은 훈련법에 연습 경기를 더 뛴 후보를 세웁니다(2단계). 포지션마다 견줘 후보가 낫거나 같으면 주장 완장을 넘기고, 전 주장에게는 "직전 주장" 완장을 줍니다(3단계). 새 주장이 문제를 일으켰다고 보고 완장을 전 주장에게 돌렸다가, 확인이 끝나 다시 새 주장에게 돌려 둡니다(4단계). 여기서 완장이 별칭(champion, previous, challenger), 선수가 모델 버전, 연습 경기가 10월 문의 40개, 포지션별 기록이 분류마다의 재현율입니다. 비유와 다른 점은, 선수는 경기를 뛰며 실력이 바뀌지만 등록된 모델 버전은 한번 등록되면 바뀌지 않는다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 챔피언 | 버전 1 · C 0.3 · 정확도 0.700 | 실습 4의 챔피언을 별칭으로 불러왔고, 그 모델을 만든 설정(C 0.3)도 기록에서 읽었습니다 |
| 2. 도전자 | 버전 2 · 학습 240 행 | 기본 200개에 10월 문의 40개를 더해 학습했습니다. 설정은 같고 데이터만 늘렸습니다 |
| 3. 분류별 | 결제 0.667 → 0.778, 교환반품 0.625 → 0.875 | 도전자가 결제와 교환반품 문의를 더 많이 맞혔습니다. 배송(0.875)과 회원(1.000)은 그대로입니다 |
| 3. 기타 | 0.286 → 0.286 | 기타 분류는 둘 다 낮고 나아지지 않았습니다. 평균 정확도만 보면 놓치기 쉬운 약점입니다 |
| 3. 정확도 | 챔피언 0.700 · 도전자 0.775 | 40개 가운데 28개에서 31개로 늘었습니다 |
| 3. 별칭 | {'champion': 2, 'previous': 1} | 도전자가 챔피언이 되고, 옛 챔피언은 previous 로 남았습니다 |
| 4. 되돌리기 | 되돌린 뒤 champion → 1 · 정확도 0.700 | 별칭 하나만 옮기자 같은 코드가 옛 모델을 받아 옛 점수가 나왔습니다 |
| 4. 원래대로 | {'champion': 2, 'previous': 1} | 실습 6이 시작할 상태로 다시 돌려 두었습니다 |
한 줄로 줄이면, 새 모델을 내놓는 일과 되돌리는 일이 "이름표 하나 옮기기"로 끝난다는 것을 보인 실습입니다. 3단계의 "낮지 않으면 올린다"는 판단은 실습 6에서 게이트로 묶여 저절로 돌게 됩니다. 정확도 차이 0.075 는 40문제 가운데 세 문제입니다. 기타 분류의 재현율 0.286 처럼 도전자도 여전히 약한 곳이 있으니, "더 낫다"는 이 시험 묶음에서의 이야기입니다.
코드 설명
mlflow.sklearn.load_model("models:/inquiry-clf@champion")은 별칭이 가리키는 버전의 모델을 불러옵니다. 별칭을 옮기면 같은 코드가 다른 모델을 받습니다.client.get_run(mv.run_id).data.params로 그 버전을 만든 실행의 설정을 읽습니다. 버전에서 실행으로, 실행에서 데이터 지문으로 거슬러 갈 수 있습니다.log_model이 돌려준info.registered_model_version이 새로 붙은 버전 번호입니다.recall_score(..., average=None)은 분류마다의 재현율을 돌려줍니다. 평균 정확도만 보면 한 분류가 무너진 것을 놓칠 수 있습니다.- 되돌리기는 별칭 하나를 옮기는 일입니다. 모델 파일을 다시 복사하거나 서비스 코드를 고치지 않습니다.