diff --git a/docs/sources/notebooks/04_model_training_pipeline.ipynb b/docs/sources/notebooks/04_model_training_pipeline.ipynb index 2584565..054d34f 100644 --- a/docs/sources/notebooks/04_model_training_pipeline.ipynb +++ b/docs/sources/notebooks/04_model_training_pipeline.ipynb @@ -351,29 +351,8 @@ "source": [ "target_col = \"Exited\"\n", "id_cols = [\"CustomerId\"]\n", - "cat_cols = [\n", - " \"Country\",\n", - " \"Gender\",\n", - " \"HasCreditCard\",\n", - " \"IsActiveMember\",\n", - " \"CustomerFeedback_sentiment3\",\n", - " \"CustomerFeedback_sentiment5\",\n", - " \"Surname_Country\",\n", - " \"Surname_Country_region\",\n", - " \"Surname_Country_subregion\",\n", - " \"Country_region\",\n", - " \"Country_subregion\",\n", - " \"is_native\",\n", - " \"Country_hemisphere\",\n", - " \"Country_IncomeGroup\",\n", - " \"Surname_Country_IncomeGroup\",\n", - " \"working_class\",\n", - " \"stage_of_life\",\n", - " \"generation\",\n", - "]\n", - "cont_cols = df_pd.drop(\n", - " columns=id_cols + cat_cols + [target_col]\n", - ").columns.values.tolist()" + "cat_cols = [\"Country\", \"Gender\", \"HasCreditCard\", \"IsActiveMember\",\"CustomerFeedback_sentiment3\", \"CustomerFeedback_sentiment5\", \"Surname_Country\", \"Surname_Country_region\", \"Surname_Country_subregion\", \"Country_region\", \"Country_subregion\", \"is_native\", \"Country_hemisphere\", \"Country_IncomeGroup\", \"Surname_Country_IncomeGroup\", \"working_class\", \"stage_of_life\", \"generation\"]\n", + "cont_cols = df_pd.drop(columns=id_cols + cat_cols + [target_col]).columns.values.tolist()" ] }, { @@ -387,22 +366,15 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "valid_size = 0.2\n", "test_size = 0.5\n", "random_state = 1\n", - "df_train, df_valid = train_test_split(\n", - " df_pd, test_size=valid_size, stratify=df_pd[target_col], random_state=random_state\n", - ")\n", - "df_valid, df_test = train_test_split(\n", - " df_valid,\n", - " test_size=test_size,\n", - " stratify=df_valid[target_col],\n", - " random_state=random_state,\n", - ")" + "df_train, df_valid = train_test_split(df_pd, test_size=valid_size, stratify=df_pd[target_col], random_state=random_state)\n", + "df_valid, df_test = train_test_split(df_valid, test_size=test_size, stratify=df_valid[target_col], random_state=random_state)" ] }, { @@ -412,11 +384,11 @@ "outputs": [], "source": [ "prepare_data = PreprocessData(\n", - " id_cols=id_cols,\n", - " target_col=target_col,\n", - " cat_cols=cat_cols,\n", - " cont_cols=cont_cols,\n", - ")\n", + " id_cols=id_cols,\n", + " target_col=target_col,\n", + " cat_cols=cat_cols,\n", + " cont_cols=cont_cols,\n", + " )\n", "# this should be fitted only on training data\n", "_ = prepare_data.fit(df=df_pd)" ] @@ -432,7 +404,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "[I 2024-05-17 10:13:50,499] A new study created in memory with name: no-name-fa589f64-d8f0-4cbb-b929-f47ba65e30cf\n", + "[I 2024-06-18 21:05:47,850] A new study created in memory with name: no-name-72ade7dc-1867-41c2-b5a4-df3b58765bc7\n", "feature_fraction, val_score: inf: 0%| | 0/7 [00:00" + "" ] }, "execution_count": 12, @@ -1813,7 +1785,7 @@ "os.environ[\"MLFLOW_S3_ENDPOINT_URL\"] = f\"http://10.152.183.156:9000\"\n", "\n", "mlflow.set_tracking_uri(\"http://\" + mlflow_host + \":\" + mlflow_port)\n", - "experiment_id = get_or_create_experiment(\"ecovadis_assignment\")\n", + "experiment_id = get_or_create_experiment(\"ecovadis\")\n", "mlflow.set_experiment(experiment_id=experiment_id)" ] }, @@ -1846,10 +1818,11 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024/05/17 10:18:43 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", - "2024/05/17 10:18:43 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", - "Registered model 'ecovadis_lgbm_model' already exists. Creating a new version of this model...\n", - "2024/05/17 10:18:49 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: ecovadis_lgbm_model, version 2\n" + "2024/06/18 21:23:54 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", + "2024/06/18 21:23:54 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", + "2024/06/18 21:23:58 WARNING mlflow.utils.environment: Encountered an unexpected error while inferring pip requirements (model URI: /tmp/tmpfsv2c_xg/model, flavor: python_function). Fall back to return ['cloudpickle==2.2.1']. Set logging level to DEBUG to see the full traceback. \n", + "Successfully registered model 'ecovadis_lgbm_model'.\n", + "2024/06/18 21:27:30 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: ecovadis_lgbm_model, version 1\n" ] }, { @@ -1857,31 +1830,29 @@ "output_type": "stream", "text": [ "Run ID:\n", - "18b4ab4d40bb4476b2cbb70b4b6a72a0\n", + "761906b692e94248940c6dbf6b12c23c\n", "Model uri:\n", - "s3://mlflow/2/18b4ab4d40bb4476b2cbb70b4b6a72a0/artifacts/ecovadis_model\n" + "s3://mlflow/6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "Created version '2' of model 'ecovadis_lgbm_model'.\n" + "Created version '1' of model 'ecovadis_lgbm_model'.\n" ] } ], "source": [ "run_name = \"init_run\"\n", - "with mlflow.start_run(\n", - " experiment_id=experiment_id, run_name=run_name, nested=True\n", - ") as run:\n", + "with mlflow.start_run(experiment_id=experiment_id, run_name=run_name, nested=True) as run:\n", " mlflow.log_params(trainer.optimizer.best)\n", " mlflow.log_metrics(metrics_dict_flattened)\n", "\n", " # Log tags\n", " mlflow.set_tags(\n", " tags={\n", - " \"project\": \"SUCCESS6G\",\n", + " \"project\": \"ecovadis\",\n", " \"optimizer_engine\": \"optuna\",\n", " \"model_family\": \"ligtgbm\",\n", " \"feature_set_version\": 1,\n", @@ -1892,11 +1863,10 @@ "\n", " artifact_path = \"ecovadis_model\"\n", " registered_model_name = \"ecovadis_lgbm_model\"\n", - " mlflow.pyfunc.log_model(\n", - " python_model=trainer,\n", - " artifact_path=artifact_path,\n", - " registered_model_name=registered_model_name,\n", - " )\n", + " mlflow.pyfunc.log_model(python_model=trainer, \n", + " artifact_path=artifact_path, \n", + " registered_model_name=registered_model_name,\n", + " code_paths=[\"churn_pred\"]) \n", " model_uri = mlflow.get_artifact_uri(artifact_path)\n", " print(f\"Run ID:\\n{run.info.run_id}\\nModel uri:\\n{model_uri}\")" ] @@ -1921,8 +1891,13 @@ "source": [ "## Predictions testing\n", "\n", - "**IMPORTANT**: the predictions are in raw_score format(for testing to see if I get same predictions), i.e. strange numbers and not classes in t\n", - "\n", + "**IMPORTANT**: the predictions are in raw_score format(for testing to see if I get same predictions), i.e. strange numbers and not classes in the output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "### Example data" ] }, @@ -2090,7 +2065,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -2126,13 +2101,13 @@ " 'generation': {0: 'gen_z', 1: 'gen_z'}}" ] }, - "execution_count": 17, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df_pd.iloc[:2].transpose().to_dict(orient=\"index\")" + "df_pd.iloc[:2].transpose().to_dict(orient='index')" ] }, { @@ -2183,7 +2158,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -2213,11 +2188,11 @@ " \n", " \n", " 0\n", - " -17.300401\n", + " -9.958064\n", " \n", " \n", " 1\n", - " -15.238104\n", + " -12.560959\n", " \n", " \n", "\n", @@ -2225,11 +2200,11 @@ ], "text/plain": [ " Exited\n", - "0 -17.300401\n", - "1 -15.238104" + "0 -9.958064\n", + "1 -12.560959" ] }, - "execution_count": 18, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -2247,16 +2222,16 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'s3://mlflow/2/18b4ab4d40bb4476b2cbb70b4b6a72a0/artifacts/ecovadis_model'" + "'s3://mlflow/6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model'" ] }, - "execution_count": 19, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -2267,18 +2242,18 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7a52da8ba0824137afb5ff0aee49b7a5", + "model_id": "77802466506f4dc39246073fa5da6015", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Downloading artifacts: 0%| | 0/9 [00:00" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loaded_trainer.unwrap_python_model()" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024/06/18 21:31:34 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", + "2024/06/18 21:31:34 INFO mlflow.types.utils: Unsupported type hint: , skipping schema inference\n", + "2024/06/18 21:31:37 WARNING mlflow.utils.environment: Encountered an unexpected error while inferring pip requirements (model URI: data/model, flavor: python_function). Fall back to return ['cloudpickle==2.2.1']. Set logging level to DEBUG to see the full traceback. \n" + ] + } + ], + "source": [ + "# mlflow.statsmodels.save_model(loaded_trainer, 'data/model')#, serialization_format='pickle')\n", + "mlflow.pyfunc.save_model(\"data/model\", python_model=loaded_trainer.unwrap_python_model())" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, "outputs": [ { "data": { @@ -2321,11 +2337,11 @@ " \n", " \n", " 0\n", - " -17.300401\n", + " -9.958064\n", " \n", " \n", " 1\n", - " -15.238104\n", + " -12.560959\n", " \n", " \n", "\n", @@ -2333,11 +2349,11 @@ ], "text/plain": [ " Exited\n", - "0 -17.300401\n", - "1 -15.238104" + "0 -9.958064\n", + "1 -12.560959" ] }, - "execution_count": 21, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -2356,16 +2372,16 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'s3://mlflow/2/18b4ab4d40bb4476b2cbb70b4b6a72a0/artifacts/ecovadis_model'" + "'s3://mlflow/6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model'" ] }, - "execution_count": 22, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -2376,29 +2392,29 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Downloading artifacts: 100%|█████████████████████| 1/1 [00:00<00:00, 577.81it/s]\n", - "2024/05/17 10:19:38 INFO mlflow.models.flavor_backend_registry: Selected backend for flavor 'python_function'\n", - "Downloading artifacts: 100%|██████████████████████| 9/9 [00:00<00:00, 44.62it/s]\n", - "2024/05/17 10:19:38 INFO mlflow.pyfunc.backend: === Running command 'exec gunicorn --timeout=60 -b 127.0.0.1:5000 -w 1 ${GUNICORN_CMD_ARGS} -- mlflow.pyfunc.scoring_server.wsgi:app'\n", - "[2024-05-17 10:19:39 +0000] [8811] [INFO] Starting gunicorn 22.0.0\n", - "[2024-05-17 10:19:39 +0000] [8811] [INFO] Listening at: http://127.0.0.1:5000 (8811)\n", - "[2024-05-17 10:19:39 +0000] [8811] [INFO] Using worker: sync\n", - "[2024-05-17 10:19:39 +0000] [8812] [INFO] Booting worker with pid: 8812\n", + "Downloading artifacts: 100%|███████████████████| 61/61 [00:00<00:00, 691.42it/s]\n", + "2024/06/18 21:32:14 INFO mlflow.models.flavor_backend_registry: Selected backend for flavor 'python_function'\n", + "Downloading artifacts: 100%|███████████████████| 61/61 [00:00<00:00, 304.59it/s]\n", + "2024/06/18 21:32:15 INFO mlflow.pyfunc.backend: === Running command 'exec gunicorn --timeout=60 -b 127.0.0.1:5000 -w 1 ${GUNICORN_CMD_ARGS} -- mlflow.pyfunc.scoring_server.wsgi:app'\n", + "[2024-06-18 21:32:15 +0000] [1380] [INFO] Starting gunicorn 22.0.0\n", + "[2024-06-18 21:32:15 +0000] [1380] [INFO] Listening at: http://127.0.0.1:5000 (1380)\n", + "[2024-06-18 21:32:15 +0000] [1380] [INFO] Using worker: sync\n", + "[2024-06-18 21:32:15 +0000] [1381] [INFO] Booting worker with pid: 1381\n", "^C\n", - "[2024-05-17 10:23:52 +0000] [8811] [INFO] Handling signal: int\n", - "[2024-05-17 10:23:52 +0000] [8812] [INFO] Worker exiting (pid: 8812)\n" + "[2024-06-18 21:36:45 +0000] [1380] [INFO] Handling signal: int\n", + "[2024-06-18 21:36:45 +0000] [1381] [INFO] Worker exiting (pid: 1381)\n" ] } ], "source": [ - "! mlflow models serve -m \"s3://mlflow/2/18b4ab4d40bb4476b2cbb70b4b6a72a0/artifacts/ecovadis_model\" --env-manager local -p 5000" + "! mlflow models serve -m \"s3://mlflow/6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model\" --env-manager local -p 5000" ] }, { @@ -2408,8 +2424,8 @@ "output from terminal:\n", "\n", "```\n", - "root@jupyter-5uperpalo:~# curl -X POST -H \"Content-Type:application/json\" --data '{\"inputs\": {\"CustomerId\": [15787619, 15770309], \"CreditScore\": [844, 656], \"Country\": [\"France\", \"France\"], \"Gender\": [\"Male\", \"Male\"], \"Age\": [18, 18], \"Tenure\": [2, 10], \"Balance (EUR)\": [160980.03, 151762.74], \"NumberOfProducts\": [1, 1], \"HasCreditCard\": [\"0\", \"0\"], \"IsActiveMember\": [\"0\", \"1\"], \"EstimatedSalary\": [145936.28, 127014.32], \"CustomerFeedback_sentiment3\": [\"neutral\", \"neutral\"], \"CustomerFeedback_sentiment5\": [\"4 stars\", \"1 star\"], \"Surname_Country\": [\"Taiwan\", \"United States\"], \"Surname_Country_region\": [\"Asia\", \"Americas\"], \"Surname_Country_subregion\": [\"Eastern Asia\", \"Northern America\"], \"Country_region\": [\"Europe\", \"Europe\"], \"Country_subregion\": [\"Western Europe\", \"Western Europe\"], \"is_native\": [\"0\", \"0\"], \"Country_hemisphere\": [\"northern\", \"northern\"], \"Country_gdp_per_capita\": [57594.03402, 57594.03402], \"Country_IncomeGroup\": [\"High income\", \"High income\"], \"Surname_Country_gdp_per_capita\": [32756.0, 76329.58227], \"Surname_Country_IncomeGroup\": [\"None\", \"High income\"], \"working_class\": [\"working_age\", \"working_age\"], \"stage_of_life\": [\"teen\", \"teen\"], \"generation\": [\"gen_z\", \"gen_z\"]}}' http://127.0.0.1:5000/invocations\n", - "{\"predictions\": [{\"Exited\": -17.300400783182656}, {\"Exited\": -15.2381037279264}]}\n", + "root@jupyter-5uperpalo:~/assignment/ecovadis_assignment# curl -X POST -H \"Content-Type:application/json\" --data '{\"inputs\": {\"CustomerId\": [15787619, 15770309], \"CreditScore\": [844, 656], \"Country\": [\"France\", \"France\"], \"Gender\": [\"Male\", \"Male\"], \"Age\": [18, 18], \"Tenure\": [2, 10], \"Balance (EUR)\": [160980.03, 151762.74], \"NumberOfProducts\": [1, 1], \"HasCreditCard\": [\"0\", \"0\"], \"IsActiveMember\": [\"0\", \"1\"], \"EstimatedSalary\": [145936.28, 127014.32], \"CustomerFeedback_sentiment3\": [\"neutral\", \"neutral\"], \"CustomerFeedback_sentiment5\": [\"4 stars\", \"1 star\"], \"Surname_Country\": [\"Taiwan\", \"United States\"], \"Surname_Country_region\": [\"Asia\", \"Americas\"], \"Surname_Country_subregion\": [\"Eastern Asia\", \"Northern America\"], \"Country_region\": [\"Europe\", \"Europe\"], \"Country_subregion\": [\"Western Europe\", \"Western Europe\"], \"is_native\": [\"0\", \"0\"], \"Country_hemisphere\": [\"northern\", \"northern\"], \"Country_gdp_per_capita\": [57594.03402, 57594.03402], \"Country_IncomeGroup\": [\"High income\", \"High income\"], \"Surname_Country_gdp_per_capita\": [32756.0, 76329.58227], \"Surname_Country_IncomeGroup\": [\"None\", \"High income\"], \"working_class\": [\"working_age\", \"working_age\"], \"stage_of_life\": [\"teen\", \"teen\"], \"generation\": [\"gen_z\", \"gen_z\"]}}' http://127.0.0.1:5000/invocations\n", + "{\"predictions\": [{\"Exited\": -9.958063834023024}, {\"Exited\": -12.560959268044444}]}\n", "```" ] }, @@ -2417,10 +2433,490 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# [TBD] Model deployment using Kserve\n", - "* https://mlflow.org/docs/latest/deployment/deploy-model-to-kubernetes/tutorial.html?highlight=kserve#step-7-deploying-the-model-to-kserve\n", + "# Model deployment using Kserve + Knative + Istio\n", + "\n", + "**reasons:**\n", + "* Kserve is less mature than Seldon core but is primarly adopted by Kubeflow and MLflow\n", + "* \"Serverless installation using Knative\" is a primary installation option of Ksserve as *KServe Serverless installation enables autoscaling based on request volume and supports scale down to and from zero. It also supports revision management and canary rollout based on revisions.*\n", + "* Istio is a primary choice of service routing for Knative\n", + "* Kserve uses MLserver from Seldoncore but according to \"quick internet search\" it is adopted more by community than Seldon-core as Seldon-core focuses more on enterprise version of its software \n", + "\n", + "**issues:**\n", + "* Kserve uses MLserver which ignores `requirements.txt` and needs prepackaged conda environment in tar.gz file\n", + "* Kserve uses old version of MLserver(1.3.2) resulting in incorrect usage of `conda-unpack`\n", + "* related code has to be included with model and conda environment \n", + "* mlserver with correct version has to be defined in the conda env:\n", + "```\n", + " - mlserver==1.3.5\n", + " - mlserver-mlflow==1.3.5\n", + "```\n", + "\n", + "**related github issue discussions/troubleshooting:**\n", + "* https://github.com/kserve/kserve/issues/3733#issuecomment-2168261646\n", + "* https://github.com/SeldonIO/MLServer/issues/1801" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create conda environment, tar.gz it and upload it to Minio" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from minio import Minio\n", "\n", - "**Current ISSUE**: the readiness probe is killing the pod: `Readiness probe failed: Get \"http://10.1.4.209:8012/\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`" + "# to download conda.yaml\n", + "client = Minio(\"10.152.183.156:9000\", \"minioadmin\", \"minioadmin\", secure=False)\n", + "bucket = \"mlflow\"\n", + "object_name = \"6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model/conda.yaml\"\n", + "file_path = \"notebooks/conda.yaml\"\n", + "client.fget_object(bucket_name=bucket, object_name=object_name, file_path=file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "following commands were run in terminal to create environment:\n", + "```\n", + "pip install conda_pack\n", + "conda env create --force -f conda.yaml\n", + "conda-pack --name mlflow-env --output environment.tar.gz --force --ignore-editable-packages --ignore-missing-files\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# upload environmnet.tar.gz\n", + "object_name = \"6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model/environment.tar.gz\"\n", + "file_path = \"notebooks/environment.tar.gz\"\n", + "client.fput_object(bucket_name=bucket, object_name=object_name, file_path=file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Kserver Inference Service in Kubernetes\n", + "```\n", + "apiVersion: \"serving.kserve.io/v1beta1\"\n", + "kind: \"InferenceService\"\n", + "metadata:\n", + " name: \"custom-ecovadis-model\"\n", + " namespace: \"mlflow-kserve-success6g\"\n", + "spec:\n", + " predictor:\n", + " serviceAccountName: success6g\n", + " model:\n", + " modelFormat:\n", + " name: mlflow\n", + " protocolVersion: v2\n", + " storageUri: \"s3://mlflow/6/761906b692e94248940c6dbf6b12c23c/artifacts/ecovadis_model\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test the Inference service" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "export INGRESS_NAME=istio-ingressgateway\n", + "export INGRESS_NS=istio-system\n", + "export INGRESS_PORT=$(kubectl -n \"${INGRESS_NS}\" get service \"${INGRESS_NAME}\" -o jsonpath='{.spec.ports[?(@.name==\"http2\")].nodePort}')\n", + "export INGRESS_HOST=$(kubectl get po -l istio=ingressgateway -n \"${INGRESS_NS}\" -o jsonpath='{.items[0].status.hostIP}')\n", + "export SERVICE_HOSTNAME=$(kubectl get inferenceservice custom-ecovadis-model -n mlflow-kserve-success6g -o jsonpath='{.status.url}' | cut -d \"/\" -f 3)\n", + "\n", + "curl -v \\\n", + " -H \"Host: ${SERVICE_HOSTNAME}\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -d @./test.json \\\n", + " http://${INGRESS_HOST}:${INGRESS_PORT}/v2/models/custom-ecovadis-model/infer\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SUMMARY \n", + "\n", + "This unfortunately crashes due to the size of conda environment that is being downloaded from minio database by Kserve. COnda env has 3.36GB due to language models (libraries). \n", + "\n", + "**Note:** this approach was succesfully tested on a custom model that used small conda env ~360MB" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## WORKAROUND\n", + "\n", + "1. save/serve(deploy) only the LightGBM model \n", + "2. preprocess the data locally; this could be done in production using (i) [Kserve InferenceGpraph](https://kserve.github.io/website/master/modelserving/inference_graph/#concepts) or (ii) [pipelines in Kubeflow](https://www.kubeflow.org/docs/components/pipelines/v1/sdk/build-pipeline/)\n", + "3. send preprocessed data to API" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Successfully registered model 'ecovadis_raw_lgbm_model'.\n", + "2024/06/19 05:43:13 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: ecovadis_raw_lgbm_model, version 1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run ID:\n", + "9a74f8e9087044b9b9d4935a268c26d0\n", + "Model uri:\n", + "s3://mlflow/6/9a74f8e9087044b9b9d4935a268c26d0/artifacts/ecovadis_raw_model\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Created version '1' of model 'ecovadis_raw_lgbm_model'.\n" + ] + } + ], + "source": [ + "run_name = \"init_run\"\n", + "with mlflow.start_run(experiment_id=experiment_id, run_name=run_name, nested=True) as run:\n", + " mlflow.log_params(trainer.optimizer.best)\n", + " mlflow.log_metrics(metrics_dict_flattened)\n", + "\n", + " # Log tags\n", + " mlflow.set_tags(\n", + " tags={\n", + " \"project\": \"ecovadis\",\n", + " \"optimizer_engine\": \"optuna\",\n", + " \"model_family\": \"ligtgbm\",\n", + " \"feature_set_version\": 1,\n", + " }\n", + " )\n", + " # Log figure - for future fun\n", + " # mlflow.log_figure(figure=correlation_plot, artifact_file=\"correlation_plot.png\")\n", + "\n", + " artifact_path = \"ecovadis_raw_model\"\n", + " registered_model_name = \"ecovadis_raw_lgbm_model\"\n", + " mlflow.lightgbm.log_model(\n", + " lgb_model=trainer.model,\n", + " artifact_path=artifact_path, \n", + " registered_model_name=registered_model_name)\n", + " # mlflow.pyfunc.log_model(python_model=trainer, \n", + " # artifact_path=artifact_path, \n", + " # registered_model_name=registered_model_name,\n", + " # code_paths=[\"churn_pred\"]) \n", + " model_uri = mlflow.get_artifact_uri(artifact_path)\n", + " print(f\"Run ID:\\n{run.info.run_id}\\nModel uri:\\n{model_uri}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "df_infser_test = df_pd.iloc[:2]\n", + "for prep in trainer.preprocessors:\n", + " df_infser_test = prep.transform(df_infser_test)\n", + " if hasattr(trainer, \"optimizer\"):\n", + " if hasattr(trainer.optimizer, \"best_to_drop\"): # type: ignore\n", + " df_infser_test.drop(\n", + " columns=trainer.optimizer.best_to_drop, # type: ignore\n", + " inplace=True,\n", + " )\n", + "df_infser_test = df_infser_test.drop(columns=trainer.id_cols + [trainer.target_col])" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'is_native': {0: 1.0, 1: 1.0},\n", + " 'generation': {0: 1.0, 1: 1.0},\n", + " 'Surname_Country': {0: 1.0, 1: 2.0},\n", + " 'Country_subregion': {0: 1.0, 1: 1.0},\n", + " 'Surname_Country_subregion': {0: 1.0, 1: 2.0},\n", + " 'Surname_Country_IncomeGroup': {0: 1.0, 1: 2.0},\n", + " 'stage_of_life': {0: 1.0, 1: 1.0},\n", + " 'IsActiveMember': {0: 1.0, 1: 2.0},\n", + " 'Country': {0: 1.0, 1: 1.0},\n", + " 'CustomerFeedback_sentiment5': {0: 1.0, 1: 2.0},\n", + " 'CustomerFeedback_sentiment3': {0: 1.0, 1: 1.0},\n", + " 'Gender': {0: 1.0, 1: 1.0},\n", + " 'Surname_Country_region': {0: 1.0, 1: 2.0},\n", + " 'working_class': {0: 1.0, 1: 1.0},\n", + " 'HasCreditCard': {0: 1.0, 1: 1.0},\n", + " 'EstimatedSalary': {0: 145936.28125, 1: 127014.3203125},\n", + " 'Age': {0: 18.0, 1: 18.0},\n", + " 'Tenure': {0: 2.0, 1: 10.0},\n", + " 'NumberOfProducts': {0: 1.0, 1: 1.0},\n", + " 'Country_gdp_per_capita': {0: 57594.03515625, 1: 57594.03515625},\n", + " 'Surname_Country_gdp_per_capita': {0: 32756.0, 1: 76329.5859375},\n", + " 'CreditScore': {0: 844.0, 1: 656.0},\n", + " 'Balance (EUR)': {0: 160980.03125, 1: 151762.734375}}" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_infser_test.transpose().to_dict(orient='index')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "create test.json:\n", + "```\n", + "cat test.json \n", + "{\n", + " \"parameters\": {\n", + " \"content_type\": \"pd\"\n", + " },\n", + " \"inputs\": [ \n", + " {\n", + " \"name\": \"is_native\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"generation\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Surname_Country\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Country_subregion\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Surname_Country_subregion\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Surname_Country_IncomeGroup\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"stage_of_life\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"IsActiveMember\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Country\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"BYTES\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"CustomerFeedback_sentiment5\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"CustomerFeedback_sentiment3\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Gender\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Surname_Country_region\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"working_class\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"HasCreditCard\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"EstimatedSalary\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [145936.28]\n", + " },\n", + " {\n", + " \"name\": \"Age\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [18]\n", + " },\n", + " {\n", + " \"name\": \"Tenure\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [2]\n", + " },\n", + " {\n", + " \"name\": \"NumberOfProducts\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [1.0]\n", + " },\n", + " {\n", + " \"name\": \"Country_gdp_per_capita\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [57594.03402]\n", + " },\n", + " {\n", + " \"name\": \"Surname_Country_gdp_per_capita\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [32756.0]\n", + " },\n", + " {\n", + " \"name\": \"CreditScore\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [844.0]\n", + " },\n", + " {\n", + " \"name\": \"Balance (EUR)\",\n", + " \"shape\": [1],\n", + " \"datatype\": \"FP64\",\n", + " \"data\": [160980.03125]\n", + " }\n", + " ]\n", + "\n", + "```\n", + "\n", + "test service:\n", + "```\n", + "export INGRESS_NAME=istio-ingressgateway\n", + "export INGRESS_NS=istio-system\n", + "export SERVICE_HOSTNAME=custom-ecovadis-raw-model.mlflow-kserve-success6g.example.com\n", + "export INGRESS_PORT=$(kubectl -n \"${INGRESS_NS}\" get service \"${INGRESS_NAME}\" -o jsonpath='{.spec.ports[?(@.name==\"http2\")].nodePort}')\n", + "export SERVICE_HOSTNAME=$(kubectl get inferenceservice custom-ecovadis-raw-model -n mlflow-kserve-success6g -o jsonpath='{.status.url}' | cut -d \"/\" -f 3)\n", + "\n", + "curl -v \\\n", + " -H \"Host: ${SERVICE_HOSTNAME}\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -d @./test.json \\\n", + " http://${INGRESS_HOST}:${INGRESS_PORT}/v2/models/custom-ecovadis-raw-model/infer\n", + "```\n", + "output:\n", + "```\n", + "{INGRESS_HOST}:${INGRESS_PORT}/v2/models/custom-ecovadis-raw-model/infer\n", + "* Trying 10.1.24.50:32702...\n", + "* Connected to 10.1.24.50 (10.1.24.50) port 32702 (#0)\n", + "> POST /v2/models/custom-ecovadis-raw-model/infer HTTP/1.1\n", + "> Host: custom-ecovadis-raw-model.mlflow-kserve-success6g.example.com\n", + "> User-Agent: curl/8.1.1\n", + "> Accept: */*\n", + "> Content-Type: application/json\n", + "> Content-Length: 2823\n", + "> \n", + "< HTTP/1.1 200 OK\n", + "< ce-endpoint: custom-ecovadis-raw-model\n", + "< ce-id: 0cd6e630-3a2b-44de-8284-299a10cf5283\n", + "< ce-inferenceservicename: mlserver\n", + "< ce-modelid: custom-ecovadis-raw-model\n", + "< ce-namespace: mlflow-kserve-success6g\n", + "< ce-requestid: 0cd6e630-3a2b-44de-8284-299a10cf5283\n", + "< ce-source: io.seldon.serving.deployment.mlserver.mlflow-kserve-success6g\n", + "< ce-specversion: 0.3\n", + "< ce-type: io.seldon.serving.inference.response\n", + "< content-length: 251\n", + "< content-type: application/json\n", + "< date: Wed, 19 Jun 2024 06:49:46 GMT\n", + "< server: istio-envoy\n", + "< x-envoy-upstream-service-time: 712\n", + "< \n", + "* Connection #0 to host 10.1.24.50 left intact\n", + "{\"model_name\":\"custom-ecovadis-raw-model\",\"id\":\"0cd6e630-3a2b-44de-8284-299a10cf5283\",\"parameters\":{\"content_type\":\"np\"},\"outputs\":[{\"name\":\"output-1\",\"shape\":[1,1],\"datatype\":\"FP64\",\"parameters\":{\"content_type\":\"np\"},\"data\":[-9.958064]}]}\n", + "```" ] } ],