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

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

실습 5 — 모델 레지스트리로 배포하고 되돌리기

실습 4의 챔피언에 도전자를 세웁니다. 10월에 상담원이 손으로 분류한 문의 40개(inquiry_oct.csv, 지어낸 연습용 데이터)를 학습 데이터에 더해 새 버전을 등록하고, 견준 뒤 별칭을 옮깁니다. 문제가 생긴 상황을 가정해 되돌리기도 해 봅니다. 실습 4를 먼저 실행해야 합니다.

1단계. 챔피언 불러오기

Python
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.700

2단계. 도전자를 학습해 등록하기

Python
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단계. 견주고 별칭 옮기기

Python
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단계. 되돌리기

Python
# 새 챔피언을 내보낸 뒤 문제가 생겼다고 가정한다: 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.77540개 가운데 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) 은 분류마다의 재현율을 돌려줍니다. 평균 정확도만 보면 한 분류가 무너진 것을 놓칠 수 있습니다.
  • 되돌리기는 별칭 하나를 옮기는 일입니다. 모델 파일을 다시 복사하거나 서비스 코드를 고치지 않습니다.
← 이전 절19장 · 실습 4 — MLflow 로 실험 기록하기다음 절 →19장 · 실습 6 — 학습과 배포 파이프라인