모델 데모 앱으로 공유
실험 노트북에서 최적 파라미터를 찾았습니다. 이제 팀원에게 "이 모델을 한번 써봐"라고 말하고 싶습니다. 하지만 ml_experiment.py를 넘겨줘도 상대방이 파라미터 의미를 모르면 어색합니다. 코드 없이 데이터를 넣고 예측 결과만 바로 보는 데모 앱이 훨씬 전달력이 좋습니다.
이 챕터에서는 두 가지를 합니다. 학습된 모델을 파일로 저장하고, 저장된 모델을 불러와 입력 위젯으로 예측을 시연하는 demo.py를 만듭니다. marimo run demo.py로 실행하면 코드가 숨겨진 예측 앱이 됩니다.
모델 저장
최적 파라미터를 확인한 뒤 모델을 파일로 저장합니다. Python 표준 라이브러리인 pickle을 쓸 수도 있지만, 여기서는 scikit-learn 공식 권장 방식인 joblib를 씁니다.
ml_experiment.py에서 저장을 담당하는 셀입니다.
import joblibsave_button = mo.ui.run_button(label="현재 모델 저장")save_button
import joblibmo.stop(not save_button.value, mo.md("저장 버튼을 누르면 현재 학습된 모델을 파일로 저장합니다."))# 저장 대상: train_and_evaluate에서 반환된 trained_modeljoblib.dump(trained_model, "best_model.pkl")# SVC를 사용한 경우 스케일러도 함께 저장if model_selector.value == "SVC": joblib.dump(scaler, "scaler.pkl")mo.md("모델이 **best_model.pkl** 로 저장되었습니다.")
joblib.dump(객체, 경로) 한 줄로 저장하고, joblib.load(경로) 한 줄로 불러옵니다. 내부적으로 pickle 기반이지만 numpy 배열이 포함된 모델에 최적화되어 있습니다.
저장 셀도 mo.stop과 저장 코드를 같은 셀에 두었습니다. Ch 02에서 다룬 이유와 같습니다. 별도 셀에 두면 save_button 의존성이 사라져 게이트가 작동하지 않습니다.
demo.py 작성
demo.py는 ml_experiment.py와 독립적인 파일입니다. 저장된 모델 파일만 읽어서 예측을 수행합니다. 같은 디렉터리에 best_model.pkl이 있다고 가정합니다.
셀 1: 라이브러리 및 모델 로드
import marimo as moimport joblibimport numpy as np# 저장된 모델 로드model = joblib.load("best_model.pkl")mo.md("""## 와인 품종 예측 데모특성 값을 입력하면 와인 품종을 예측합니다.- **클래스 0**: 바롤로 계열- **클래스 1**: 그리뇰리노 계열- **클래스 2**: 바르베라 계열""")
셀 2: 입력 위젯
와인 데이터셋의 13개 특성 중 4개를 슬라이더로 제어합니다.
alcohol = mo.ui.slider(11.0, 15.0, step=0.1, value=13.0, label="Alcohol (%)")malic_acid = mo.ui.slider(0.5, 6.0, step=0.1, value=2.3, label="Malic acid")ash = mo.ui.slider(1.0, 4.0, step=0.1, value=2.4, label="Ash")total_phenols = mo.ui.slider(0.5, 4.0, step=0.1, value=2.3, label="Total phenols")input_panel = mo.vstack([ mo.md("### 특성 입력"), alcohol, malic_acid, ash, total_phenols,])input_panel
셀 3: 예측
import numpy as np# 나머지 9개 특성은 wine 데이터셋 대략 평균값으로 채움# alcalinity_of_ash≈19.5, magnesium≈99.7, flavanoids≈2.0,# nonflavanoid_phenols≈0.36, proanthocyanins≈1.6,# color_intensity≈5.1, hue≈0.96, od280_od315≈2.6, proline≈746sample = np.array([[ alcohol.value, # alcohol malic_acid.value, # malic_acid ash.value, # ash 19.5, # alcalinity_of_ash 99.7, # magnesium total_phenols.value, # total_phenols 2.0, # flavanoids 0.36, # nonflavanoid_phenols 1.6, # proanthocyanins 5.1, # color_intensity 0.96, # hue 2.6, # od280_od315_of_diluted_wines 746.0, # proline]])# predict는 2D 배열을 받아야 함 — 1D를 넘기면 에러predicted_class = model.predict(sample)[0]class_names = ["클래스 0 (바롤로 계열)", "클래스 1 (그리뇰리노 계열)", "클래스 2 (바르베라 계열)"]mo.md(f"""### 예측 결과입력한 특성값을 기준으로 예측한 품종은 **{class_names[predicted_class]}** 입니다.> 이 데모는 4개 특성만 슬라이더로 제어하고 나머지는 데이터셋 평균값을 사용합니다.> 실제 적용에서는 모든 특성을 입력받는 것이 정확합니다.""")
model.predict(sample)에서 sample이 반드시 2D 배열이어야 합니다. np.array([...]) 형태(1D)를 넘기면 scikit-learn이 에러를 냅니다. np.array([[...]])로 바깥에 대괄호를 한 번 더 감싸서 2D를 보장합니다.
나머지 특성 9개에는 0 대신 데이터셋 실제 평균값을 씁니다. 특히 magnesium(약 100)과 proline(약 750)은 0으로 넣으면 예측이 한 클래스로 쏠리기 때문에 데모로서 의미가 없어집니다.
데모 앱 실행
marimo run demo.py
Running application... URL: http://localhost:2718
브라우저에서 http://localhost:2718을 열면 코드가 숨겨지고 입력 위젯과 예측 결과만 보이는 앱 화면이 나타납니다. 슬라이더를 움직이면 예측 결과가 즉시 갱신됩니다.
팀원에게 이 앱을 공유하려면 PART 07 Ch 04에서 다룬 방법을 씁니다. --host 0.0.0.0 --port 8080 옵션으로 외부 접근을 허용하거나, PART 09에서 다룰 Docker 배포 또는 WASM 내보내기를 활용합니다.
SVC 모델을 저장했을 때
SVC를 사용한 경우 best_model.pkl과 함께 scaler.pkl도 저장됩니다. demo.py에서 SVC 모델을 불러올 때는 스케일러도 함께 불러와야 합니다.
import joblibimport numpy as npmodel = joblib.load("best_model.pkl")scaler = joblib.load("scaler.pkl")# SVC는 스케일링된 데이터에 맞게 학습됨# 예측 전에 입력을 동일하게 스케일링 필요sample_scaled = scaler.transform(sample)predicted_class = model.predict(sample_scaled)[0]
모델을 저장할 때 어떤 전처리가 적용됐는지 함께 기록해두는 것이 중요합니다. 전처리 없이 예측하면 스케일이 달라서 결과가 틀립니다.
ml_experiment.py 전체 셀 구성 확인
Ch 01부터 Ch 04까지 추가된 셀을 포함한 최종 구성입니다.
셀 1: 라이브러리 임포트 + 데이터 로드 + 분할 + 스케일링셀 2: dataset_summary (데이터셋 정보)셀 3: model_selector (드롭다운)셀 4: 파라미터 위젯 정의셀 5: param_panel (동적 구성)셀 6: control_panel (모델 선택기 + 파라미터 패널)셀 7: run_button (학습 트리거 — 방법 1 선택 시)셀 8: build_estimator 함수 정의셀 9: train_and_evaluate 함수 (@mo.cache)셀 10: 학습·평가 실행 (trained_model, test_accuracy)셀 11: 현재 실험 결과 출력셀 12: get_history, set_history (mo.state)셀 13: log_button_with_action셀 14: history_display (기록 테이블)셀 15: comparison_chart (비교 차트)셀 16: results_panel (레이아웃)셀 17: save_button셀 18: mo.stop + joblib.dump
방법 1(run_button)을 사용하지 않는다면 셀 7은 생략합니다. 방법 3(@mo.cache)을 기본으로 채택하면 셀 9에서 trained_model과 test_accuracy가 정의되어 이후 셀들이 이를 참조합니다.
정리
joblib.dump(model, "best_model.pkl")로 저장하고joblib.load("best_model.pkl")로 불러옵니다. numpy 배열이 포함된 scikit-learn 모델에 적합합니다.- SVC를 저장할 때는 모델과 스케일러를 함께 저장합니다. 예측 시 동일한 스케일링을 적용해야 올바른 결과가 나옵니다.
model.predict에는 반드시 2D 배열을 넘깁니다. 단일 샘플은np.array([[특성1, 특성2, ...]])로 감쌉니다.- 나머지 특성에 0을 채우면 예측이 왜곡될 수 있습니다. 데이터셋 평균값을 사용하세요.
demo.py를ml_experiment.py와 분리하면 실험 코드와 배포 코드가 섞이지 않습니다. 저장된 모델 파일만 공유하면 상대방은 전체 실험 노트북 없이 예측 앱만 실행할 수 있습니다.marimo run demo.py로 앱 모드를 실행하면 코드가 숨겨지고 위젯과 예측 결과만 화면에 나타납니다.