End to End Machine Learning Pipeline With MLOps Tools (MLFlow+DVC+Flask+Heroku+EvidentlyAI+Github Actions)的實作
| 參考文件: MLOps演進.md | 參考專案: git結合dvc的示範步驟 | 參考codebase: shanakaChathu/churn_model |
- 建立目錄和git初始化:
mkdir end-to-end-mlops-pipeline && cd end-to-end-mlops-pipeline
git init
- 上傳set_remote_url.sh和push.sh (從別的專案get這兩個檔案):
git add ./set_remote_url.sh ./push.sh
git ci -m "add set_remote_url.sh and push.sh"
- 設定git remote:
./set_remote_url.sh
建立虛擬環境 (可做可不做)
conda create -n churn_model python=3.7 -y
conda activate churn_model
pip install cookiecutter,mlflow,flask,pytest,evidently
dvc的安裝請參考dvc install
cookiecutter https://github.com/drivendata/cookiecutter-data-science
- 我將churn_model目錄再獨立出來 (這跟End to End Machine Learning Pipeline With MLOps Tools網站的作法不同)
vi churn_model/.gitignore
- 將 /data/ 改成 #/data/ (避免churn_model/data/external/train.csv無法git add)。
- 將 *.db 改成 #*.db (避免churn_model/mlflow.db無法git add)。
- 新增以下設定:
# MLflow artifacts artifacts/
- 這樣才能在/data/中做
dvc add
等動作。
git add churn_model
git ci -m \
"Add files generated by running cookiecutter \
(need manually comment /data/ and *.db in the churn_model/.gitignore to avoid 'dvc add' fail)"
vi churn_model/requirements.txt
以下是上傳Heroku執行時需使用的套件 (版號則是本專案release v1.0時所使用的),請代換成以下內容 (請點開)
numpy==1.21.5
pandas==1.3.5
scikit-learn==1.0.2
mlflow==1.23.1
joblib==1.1.0
Flask==2.0.2
pytest==7.0.1
git add churn_model/requirements.txt
gi ci -m "Modify requirements.txt for the project needs"
- 在Github上建立一個repository,並命名為end-to-end-mlops-pipeline。
cd end-to-end-mlops-pipeline
dvc init
git ci -m "Initialize DVC"
- 查出google drive中欲放data的資料夾id (e.g. DB/)
- 從google drive網頁中的DB裡新增一個end-to-end-mlops-pipeline資料夾
- 以
dvc remote add
設定與google drive資料夾的連結:
dvc remote add -d myremote gdrive://1-2FMzxYYkkb8H0_zPXrvkncmfxXtlZZ2/end-to-end-mlops-pipeline
git add .dvc/config
git ci -m "Configure remote repo"
- 從Kaggle這裡下載train.csv,並儲存至churn_model/data/external/中。
cd churn_model/data/external/
dvc add train.csv
git add .gitignore train.csv.dvc
git ci -m "Add dvc configure files for train.csv"
dvc push
- 建立以下參數檔和原始碼共5個檔案:
- churn_model/params.yaml
- churn_model/src/data中包含load_data.py、split_data.py
- churn_model/src/models中包含train_model.py、production_model_selection.py
- train_model.py: 訓練model的程式
- production_model_selection.py: 從mlflow server所記錄的metrics數據中,選出accuracy最高的model,dump到models/model.joblib
- churn_model/params.yaml
cd churn_model/
git add .
git ci -m "Create the source code inside the src folder"
- 其實在churn_model/src/models中還包含model_monitor.py,是執行evidently用來輸出model效能report的程式。這會在Step 13中提到。
- 建立churn_model/dvc.yaml。dvc.yaml是
dvc repro
會參照的檔案,用來自動執行一連串的程式。
cd churn_model/
git add dvc.yaml
git ci -m "Pipeline Creation"
- 另開terminal視窗執行以下命令:
cd churn_model
mlflow server --backend-store-uri \
sqlite:///mlflow.db --default-artifact-root ./artifacts --host 0.0.0.0 -p 1234
- 在原來的terminal視窗:
- 用
dvc repro --dry
看stage的執行順序是否和dvc stage list
相同 (不知道為什麼一開始的順序是不對的)。 - 例如
dvc stage list
正確執行順序為raw_dataset_creation -> split_data -> model_train -> log_production_model。 - 如果順序不同的話,因為dvc.yaml的dep和outs的關係,raw_dataset_creation、split_data和model_train是綁在一起的,所以只要執行
dvc repro model_train
,就會連帶的執行raw_dataset_creation -> split_data -> model_train。之後再另外執行dvc repro log_production_model
。 - 如果順序相同,就可直接執行
dvc repro
,不需分開執行dvc repro model_train
和dvc repro log_production_model
。
- 用
- 打開瀏覽器輸入
localhost:1234
,就可以看到rain_model.py和production_model_selection.py中mlflow api所記錄的訊息。- 按
Download CSV
,將runs.csv儲存到churn_model/reports中。
- 按
- 執行
git add
增加由dvc repro
所產生的幾個檔案: data/processed/.gitignore、data/raw/.gitignore、dvc.lock、mlflow.db、models/.gitignore、reports/runs.csv。
git add .
git ci -m "Add files from running mlflow server and 'dvc repro'"
- 預期每一次當code、data (e.g. train.csv)或params.yaml有變動時就要執一次mlflow server和dvc repro,來記錄model metrics的變化。
- 原則是: 只要有一個值發生變化就要記錄一次,避免多個變數同時發生卻只記錄一次 (這樣不容易追蹤變化的原因)。
Flask這個套件提供了不少架設網站需要的基本工具,包括路由(Routes)、網頁模板(templates)、權限(authorization)等等。所以我們要用Flask來建立本地端的網頁。確定一切運作正常就可以將上面所有和這部份的網頁程式碼都部署到Heroku上。
- 在churn_model資料夾中建立app.py。
- 在churn_model中建立webapp資料夾。
- 建立網頁的基本框架: (從這裡下載,放至churn_model/webapp中)
app.py --> Flask在這邊
webapp
├── model_webapp_dir
│ └── model.joblib --> 會從churn_model/models/model.joblib拷貝到這裡來
├── static
│ ├── css
│ │ └── main.css
│ └── sctipt
│ └── index.js
└── templates
├── 404.html
├── base.html
└── index.html
- 拷貝train好的model檔到model_webapp_dir:
cd churn_model
cp models/model.joblib webapp/model_webapp_dir/
models/不是放正式model的地方 (所以models/.gitignore中有model.joblib),而webapp/model_webapp_dir/才是放正式model的地方 (有git控管)
cd churn_model
git add app.py webapp
git ci -m "Add app.py and webapp files that will be used by flask"
- 建立tests資料夾
- 基本框架:
tests
├── __init__.py --> 空檔
└── test_config.py --> 透過程式中已定義好incorrect_values的值,傳進上層churn_model/app.py的form_response()中,
如果回傳如同NotANumber()的話,表示判斷正確 (為非正確的值)。
- 執行程式:
cd churn_model
pytest -v
pytest會自動搜尋可執行測試的程式。
- 執行結果: (PASSED則為判斷正確)
============================================================ test session starts ==================================================
platform linux -- Python 3.7.11, pytest-7.0.1, pluggy-1.0.0 -- /home/alan/miniconda3/envs/churn_model/bin/python
cachedir: .pytest_cache
rootdir: /home/alan/tmp/end-to-end-mlops-pipeline/churn_model
plugins: anyio-3.5.0
collected 1 item
tests/test_config.py::test_form_response_incorrect_values PASSED [100%]
============================================================= 1 passed in 0.37s ===================================================
cd churn_model
git add tests
git ci -m "Add the unitest files that will be used by pytest"
- ./push.sh
在Heroku網站上新增一個app name、設定與Github repo的連結、設定Authorization token (其app name和token在之後的Step 12會被用到) 和建立Heroku執行的第一個檔案Procfile。參考Heroku和Github Actions操作步驟 (影片)。
- 從User request到Flask的傳遞流程: 會先傳給Heroku nginx (不過似乎Heroku已經處理好nginx,整個過程都不需要處理到nginx相關指令),再給Procfile呼叫Heroku的gunicorn,再傳到app.py的Flask。架構如下:
- 按Create new app,app name設定為churnmodel123
- Deploy / Deployment method: 選擇connect to Github
- Connect to GitHub: 選擇本專案的名稱end-to-end-mlops-pipeline,可只輸入end,然後按Search
- 選擇seagarwu/end-to-end-mlops-pipeline,並按Connect
- 在Wait for CI to pass before deploy的方框打勾,並按Enable Automatic Deploys
- Account Settings / Applications / Authorizations / Creation Authorizations: 建立1個authorization
看起來所建立的authorization適用所有Heroku上的app name,所以只要建立一個即可。
- Description填上
Github Actions (ci-cd.yaml)
- 按Save則會產生Authorization token。之後會在Step 12填在Github的Actions secrets中。
- Description填上
- requirements.txt和setup.py需移到根目錄 (move from ./churn_model/ to .)。Heroku執行時看起來只找根目錄的requirements.txt和setup.py。
mv churn_model/{requirements.txt,setup.py} .
git add requirements.txt churn_model/requirements.txt setup.py churn_model/setup.py
git ci -m "Because heroku can only find requirements.txt and setup.py at to root, move then from ./churn_model/ to ."
./push.sh
- 在根目錄建立Procfile檔案 (內容如下),此為Heroku執行時的第一個檔案。
web: cd churn_model && gunicorn app:app
意思是先進入churn_model目錄中再執行gunicorn(web server),帶入的程式是app.py的app function。
git add Procfile
git ci -m "Add Procfile for Heroku use"
./push.sh
- 此時用
heroku run bash -a churnmodel123
連進Heroku repo時可以看到目前的目錄都是空的。
在Github網站的end-to-end-mlops-pipeline repo的Actions secrets設定中填入Step 11的Authorization token,並使用Github Actions建立CI-CD pipeline。這樣當Github repo有變動時,ci-cd.yaml就會自動將repo的所有資料全都部署到Heroku中。
- 設定Actions secrets:
- end-to-end-mlops-pipeline / Settings / Secrets / Actions /Actions secrets: 按New repository secret,建立2個
- Name填入HEROKU_API_TOKEN,Value是Step 11的Authorization token
- Name填入HEROKU_APP_NAME,Value是Step 11的app name為churnmodel123
- end-to-end-mlops-pipeline / Settings / Secrets / Actions /Actions secrets: 按New repository secret,建立2個
- 在根目錄建立.github/workflows/ci-cd.yaml
- ci-cd.yaml透過HEROKU_API_TOKEN和HEROKU_APP_NAME連結Heroku的app name (churnmodel123)。
- 當此repo有任何git commit到Github時都會觸發ci-cd.yaml檔案。
git add .github
git ci -m "Add ci-cd.yaml in .github/workflows"
./push.sh
此動作完成後,在Github Actions就會開始執行ci-cd.yaml的building和對Heroku server的部署workflow。
- 如果Heroku部署成功,此時用
heroku run bash -a churnmodel123
連進Heroku repo時就會看到跟Github repo一樣的檔案 (但還會多了幾個Heroku自建的檔案和目錄)。 - debug小技巧:
- 如果ci-cd.yaml啟動後,從Github/Actions/All Workflows/目前正在跑的workflow中,如果看到fail並卡在Deploy to Heroku,其錯誤訊息為
error: failed to push some refs to 'https://git.heroku.com/***.git'
,則可以嘗試修改ci-cd.yaml,直接將HEROKU_API_TOKEN和HEROKU_APP_NAME代換成實際的值試試看。
- 如果ci-cd.yaml啟動後,從Github/Actions/All Workflows/目前正在跑的workflow中,如果看到fail並卡在Deploy to Heroku,其錯誤訊息為
models/model.joblib是production_model_selection.py根據max accuracy所產生的model檔。而再拷貝到web_app/model_webapp_dir/model.joblib是為了網頁執行預測時給app.py的predict()使用的,也就是當需部署一版更高正確率的model時(需
git commit
時)所需做的動作。
- 建立deploy_best_model.sh
#!/bin/sh -v
cd churn_model/
cp models/model.joblib webapp/model_webapp_dir/
git add webapp/model_webapp_dir/model.joblib
git ci webapp/model_webapp_dir/model.joblib
cd ..
./push.sh
chmod +x deploy_best_model.sh
git add deploy_best_model.sh
git ci -m "Add deploy_best_model.sh that copy the model of max accuracy from models/ to webapp/model_webapp_dir/"
./push.sh
- 執行deploy_best_model.sh,在git ci時手動填入model version和accuracy數值。
- 在Heroku網站的Deploy / Deployment method: 選擇Use Heroku CLI
git remote add heroku https://git.heroku.com/churnmodel123.git
git push heroku main
(奇怪的是,當改變Heroku的Registered Authorizations,也就是Github Actions所對應的HEROKU_API_TOKEN值時,居然還可以git push!)
git log heroku/main
heroku login
heroku git:clone -a churnmodel123
git remote add heroku https://git.heroku.com/churnmodel123.git
cd churnmodel123
git add .
git commit
git push heroku main
git log
heroku logs
heroku run bash -a churnmodel123
heroku run bash
連進去的應該是一個container,包含linux bash、git server (所以才能執行heroku git:clone
)、web server(gunicorn) ... 等app。
隨著時間推移,model的正確率會越來越低,因此需要使用新資料再重新train一次model。我們需要持續性的監督model的效能,而Evidently是一個監督model效能的工具。它也使用統計方法來監測model drift和concept drift、data drift等問題。(但是從示範網頁看起來,只是根據新舊dataset做data drift檢查,而跟model無關,猜想應該只是沒有示範到model drift的檢查功能而已)
- 比較的檔案: churn_model/data/raw/train.csv (代表的是舊資料) 和train_new.csv (代表的是新資料)
- 為了測試方便,直接將churn_model/data/raw/train.csv拷貝成train_new.csv
- 示範網頁的流程有問題,正常來說,新dataset會放進churn_model/data/external/train.csv,然後跑
dvc repro raw_dataset_creation
之後會產生data/raw/train.csv。但是用model_monitor.py執行時卻希望train.csv為舊的,而train_new.csv為新的,這樣很奇怪。因此我做了些改變,將新舊之間做了調換。當需要執行model_monitor.py之前,須先手動將data/raw/train.csv改成train_old.csv,而執行model_monitor.sh之後,比較的就是train_old.csv和train.csv (新的),這樣就正確了。
- 示範網頁的流程有問題,正常來說,新dataset會放進churn_model/data/external/train.csv,然後跑
- 為了測試方便,直接將churn_model/data/raw/train.csv拷貝成train_new.csv
- 建立churn_model/src/model/model_monitor.py
- 功能: 比較train_old.csv和train.csv,根據params.yaml輸出report成reports/data_and_target_drift_dashboard.html
cd churn_model/src/model
git add model_monitor.py
git ci -m "Add model_monitor.py for model monitoring"
./push.sh
- 執行model_monitor.py:
cd churn_model
python src/models/model_monitor.py --config=params.yaml
- churn_model/mlflow_srv.sh
#!/bin/sh
mlflow server --backend-store-uri sqlite:///mlflow.db --default-artifact-root ./artifacts --host 0.0.0.0 -p 1234
- churn_model/train_model.sh
#!/bin/sh
# the built model is model.joblib that saved in the models/
dvc repro model_train
dvc repro log_production_model
- churn_model/monitor_model.sh
#!/bin/sh -v
python src/models/model_monitor.py --config=params.yaml
cd churn_model
chmod +x mlflow_srv.sh train_model.sh monitor_model.sh
git add mlflow_srv.sh train_model.sh monitor_model.sh
gi ci -m "Add mlflow_srv.sh, train_model.sh, monitor_model.sh that are integration shell scripts"
./push.sh
當kaggle資料放進churn_model/data/external、修改好app.py、src/裡的程式碼和params.yaml、ci-cd.yaml、也完成git、dvc、Github Actions、Heroku設定,就可以執行以下的shell script檔。
- churn_model/mlflow_srv.sh
- 執行一便即可。
- train_model.sh
- 每當要train model時就要執行一次。
- monitor_model.sh
- 每當執行之前,須先手動將raw/train.csv改成raw/train_old.csv再執行monitor_model.sh。
- deploy_best_model.sh
- 用在需跑predict網頁前 (會先
git push github main
去trigger Github Actions做deploy到Heroku的動作)。
- 用在需跑predict網頁前 (會先
製作docker image最簡單的作法是只要使用requirements.txt、Dockerfile、build.sh、run.sh和docker_push.sh就可以產生docker image和上傳至container registry
- docker image所需的檔案:
- requirements.txt
直接使用本專案的requirements.txt即可
- Dockerfile
FROM python:3.9.9 WORKDIR /app COPY ./requirements.txt /app/requirements.txt RUN pip install -r requirements.txt COPY . /app WORKDIR /app/churn_model CMD ["python3", "app.py"]
如同Step 9在本地端執行app.py
- build.sh
#!/bin/sh -v docker build -t localhost:4000/seagarwu/end-to-end-mlops-pipeline:v0
v0是release的版號,每當出新版時就要更新此版號 (e.g. v1.0)。最好tag、release和docker image的版號都是相同的
- run.sh
#!/bin/sh -v docker run -d -it -v /mnt/g/我的雲端硬碟/docker/end-to-end-mlops-pipeline/data:/app/data \ -p 5000:5000 --name end-to-end-mlops-pipeline localhost:4000/seagarwu/end-to-end-mlops-pipeline:v0
v0是release的版號,每當出新版時就要更新此版號 (e.g. v1.0)。最好tag、release和docker image的版號都是相同的
- requirements.txt
- 上傳至自建的container registry (Harbor) 所需的檔案:
- docker_push.sh
#!/bin/sh -v docker login localhost:4000 docker push localhost:4000/seagarwu/end-to-end-mlops-pipeline:v0
v0是要release的版號,每當出新版時就要更新此版號 (e.g. v1.0)。最好tag、release和docker image的版號都是相同的
- docker_push.sh
chmod +x build.sh run.sh docker_push.sh
git add Dockerfile build.sh run.sh docker_push.sh
git ci -m "Add Dockerfile build.sh run.sh docker_push.sh for docker image"
./push.sh
- 執行:
- 建立docker image:
./build.sh
- 建立docker container並執行:
./run.sh
- 上傳docker image至container registry:
./docker_push.sh
- 建立docker image:
每當覺得短期不再變動時就可以發佈新的release,步驟包含修改與image相關的build.sh和run.sh的版號、上傳docker image和建立新的git tag、Github release
- 修改build.sh、run.sh和docker_push.sh版號為準備發佈的版號 e.g. v1.0。
- 建立及執行docker image:
./build.sh ./run.sh
用
docker image ls
檢查docker image版號是否正確。執行Docker Desktop的end-to-end-mlops-pipeline container看是否運作正常和網頁是否可正常呼叫
git add build.sh run.sh docker_push.sh
git ci -m "Publish new release v1.0"
./push.sh
- 上傳docker image至Harbor:
- 啟動Harbor registry service
- 執行Docker Desktop
- 在harbor目錄,執行
docker-compose up -d
- 執行上傳動作:
./docker_push.sh
- 啟動Harbor registry service
- 建立new release:
- 建立新的git tag:
git tag v1.0 xxxx
(xxxx是commit的hash id)刪除本地端tag:
git tag -d v1.0
- 上傳tag至Githab:
git push github v1.0
刪除遠端tag:
git push github :v1.0
- 建立Github release:
- 從網頁中找尋
Create a new release
,Choose a tag
選擇剛建立好的tag e.g. v1.0,填上title和description就可以按Publish release
。- description可以參考run.sh加入執行docker image的命令:
docker run -d -it -v /mnt/g/我的雲端硬碟/docker/end-to-end-mlops-pipeline/data:/app/data -p 5000:5000 --name end-to-end-mlops-pipeline localhost:4000/seagarwu/end-to-end-mlops-pipeline:v1.0
- description可以參考run.sh加入執行docker image的命令:
- 從網頁中找尋
- 建立新的git tag:
基本上,到此已完成所有工作,包含MLOps pipeline整個過程,和發佈new release需要的tag、Github release和docker image。
未來需優化的地方
- model優化主要是針對params.yaml做修改,所以未來只要有一支程式(e.g. optuna)能設定參數範圍,並將參數自動填到params.yaml,然後再執行train_model.sh就可以完成model的優化。
- 找看看model registry和feature store都是用哪個app來做。
- 示範網頁在evidently只比較兩個新舊dataset檔案,但在程式交易上不只一個dataset檔案。