{ "cells": [ { "attachments": {}, "cell_type": "markdown", "id": "6900b762", "metadata": {}, "source": [ "# Basic ML example (Prefect Workflow)\n", "\n", "Here, we use the PrefectWorkflow backend for the Basic ML example. Again, we reimplement sklearn [example](https://scikit-learn.org/stable/auto_examples/linear_model/plot_sparse_logistic_regression_mnist.html#sphx-glr-auto-examples-linear-model-plot-sparse-logistic-regression-mnist-py) as Function Fuse workflow, using identical frontend workflow definition as in the LocalWorkflow and RayWorkflow examples, but providing Prefect-specific options in the backend.\n", "\n", "We can optionally activate a Prefect server to view information about workflow runs in a browser:\n", "\n", "```console\n", "$ prefect server start\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "id": "3c9f7325", "metadata": {}, "source": [ "## The workflow" ] }, { "cell_type": "code", "execution_count": 6, "id": "c843a80a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/hm/miniconda3/envs/pytorch_cuda/lib/python3.10/site-packages/prefect/flows.py:244: UserWarning: A flow named 'classifier' and defined at '/home/hm/CardiacFilament/functionfuse/functionfuse/backends/addons/prefectback.py:134' conflicts with another flow. Consider specifying a unique `name` parameter in the flow definition:\n", "\n", " `@flow(name='my_unique_name', ...)`\n", " warnings.warn(\n" ] } ], "source": [ "from functionfuse import workflow\n", "from functionfuse.backends.addons.prefectback import PrefectWorkflow\n", "from functionfuse.storage import storage_factory\n", "\n", "import os\n", "\n", "\n", "# Frontend\n", "\n", "@workflow\n", "def openml_dataset():\n", " from sklearn.datasets import fetch_openml\n", " from sklearn.utils import check_random_state\n", "\n", " X, y = fetch_openml(\"mnist_784\", version=1, return_X_y=True, as_frame=False, parser=\"pandas\")\n", " random_state = check_random_state(0)\n", " permutation = random_state.permutation(X.shape[0])\n", " X = X[permutation]\n", " y = y[permutation]\n", " X = X.reshape((X.shape[0], -1))\n", " return X, y\n", "\n", "\n", "@workflow\n", "def train_test_split(X, y, train_samples, test_size):\n", " from sklearn.model_selection import train_test_split\n", "\n", " X_train, X_test, y_train, y_test = train_test_split(\n", " X, y, train_size=train_samples, test_size=test_size\n", " )\n", " return {\"X_train\": X_train, \"X_test\": X_test, \"y_train\": y_train, \"y_test\": y_test}\n", "\n", "@workflow\n", "def train_model(X, y):\n", " from sklearn.preprocessing import StandardScaler\n", " from sklearn.pipeline import make_pipeline\n", " from sklearn.linear_model import LogisticRegression\n", "\n", " train_samples = len(X)\n", " clf = make_pipeline(StandardScaler(), LogisticRegression(C=50.0 / train_samples, penalty=\"l1\", solver=\"saga\", tol=0.1))\n", " clf.fit(X, y)\n", " return clf\n", "\n", "\n", "dataset = openml_dataset().set_name(\"dataset\")\n", "X, y = dataset[0], dataset[1]\n", "dataset_split = train_test_split(X, y, train_samples = 5000, test_size = 10000).set_name(\"dataset_split\")\n", "model = train_model(dataset_split[\"X_train\"], dataset_split[\"y_train\"]).set_name(\"model\")\n", "\n", "\n", "# Prefect Backend\n", "\n", "# Example of specifying a task_runner\n", "from prefect.task_runners import SequentialTaskRunner, ConcurrentTaskRunner\n", "prefect_flow_args = {\n", " \"description\": \"Basic ML workflow from sklearn\",\n", " \"task_runner\": ConcurrentTaskRunner()\n", "}\n", "\n", "prefect_workflow = PrefectWorkflow(dataset, workflow_name=\"classifier\")\n", "opt = {\n", " \"kind\": \"file\",\n", " \"options\": {\n", " \"path\": \"storage\"\n", " }\n", "}\n", "storage = storage_factory(opt)\n", "# prefect_workflow.set_storage(storage)\n", "\n", "# Example of setting Prefect Task options for a specific Node\n", "from prefect.tasks import task_input_hash\n", "prefect_workflow.query(pattern=\"^model$\").set_task_args({\"cache_key_fn\": task_input_hash})\n", "\n", "# Optionally create the Prefect Flow prior to running:\n", "prefect_workflow.generate_flow()\n", "print(type(prefect_workflow.flow))" ] }, { "cell_type": "code", "execution_count": 7, "id": "132dac43", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
16:58:07.160 | INFO    | prefect.engine - Created flow run 'fortunate-stingray' for flow 'classifier'\n",
       "
\n" ], "text/plain": [ "16:58:07.160 | \u001b[36mINFO\u001b[0m | prefect.engine - Created flow run\u001b[35m 'fortunate-stingray'\u001b[0m for flow\u001b[1;35m 'classifier'\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "/home/hm/miniconda3/envs/pytorch_cuda/lib/python3.10/site-packages/prefect/tasks.py:298: UserWarning: A task named 'openml_dataset' and defined at '/tmp/ipykernel_10727/1591802011.py:10' conflicts with another task. Consider specifying a unique `name` parameter in the task definition:\n", "\n", " `@task(name='my_unique_name', ...)`\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ "
16:58:07.293 | INFO    | Flow run 'fortunate-stingray' - Created task run 'openml_dataset-0' for task 'openml_dataset'\n",
       "
\n" ], "text/plain": [ "16:58:07.293 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Created task run 'openml_dataset-0' for task 'openml_dataset'\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:07.296 | INFO    | Flow run 'fortunate-stingray' - Executing 'openml_dataset-0' immediately...\n",
       "
\n" ], "text/plain": [ "16:58:07.296 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Executing 'openml_dataset-0' immediately...\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:16.453 | INFO    | Task run 'openml_dataset-0' - Finished in state Completed()\n",
       "
\n" ], "text/plain": [ "16:58:16.453 | \u001b[36mINFO\u001b[0m | Task run 'openml_dataset-0' - Finished in state \u001b[32mCompleted\u001b[0m()\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "/home/hm/miniconda3/envs/pytorch_cuda/lib/python3.10/site-packages/prefect/tasks.py:298: UserWarning: A task named 'train_test_split' and defined at '/tmp/ipykernel_10727/1591802011.py:24' conflicts with another task. Consider specifying a unique `name` parameter in the task definition:\n", "\n", " `@task(name='my_unique_name', ...)`\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ "
16:58:16.486 | INFO    | Flow run 'fortunate-stingray' - Created task run 'train_test_split-0' for task 'train_test_split'\n",
       "
\n" ], "text/plain": [ "16:58:16.486 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Created task run 'train_test_split-0' for task 'train_test_split'\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:16.488 | INFO    | Flow run 'fortunate-stingray' - Executing 'train_test_split-0' immediately...\n",
       "
\n" ], "text/plain": [ "16:58:16.488 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Executing 'train_test_split-0' immediately...\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:16.614 | INFO    | Task run 'train_test_split-0' - Finished in state Completed()\n",
       "
\n" ], "text/plain": [ "16:58:16.614 | \u001b[36mINFO\u001b[0m | Task run 'train_test_split-0' - Finished in state \u001b[32mCompleted\u001b[0m()\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "/home/hm/miniconda3/envs/pytorch_cuda/lib/python3.10/site-packages/prefect/tasks.py:298: UserWarning: A task named 'train_model' and defined at '/tmp/ipykernel_10727/1591802011.py:33' conflicts with another task. Consider specifying a unique `name` parameter in the task definition:\n", "\n", " `@task(name='my_unique_name', ...)`\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ "
16:58:16.645 | INFO    | Flow run 'fortunate-stingray' - Created task run 'train_model-0' for task 'train_model'\n",
       "
\n" ], "text/plain": [ "16:58:16.645 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Created task run 'train_model-0' for task 'train_model'\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:16.647 | INFO    | Flow run 'fortunate-stingray' - Executing 'train_model-0' immediately...\n",
       "
\n" ], "text/plain": [ "16:58:16.647 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Executing 'train_model-0' immediately...\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:19.180 | INFO    | Task run 'train_model-0' - Finished in state Completed()\n",
       "
\n" ], "text/plain": [ "16:58:19.180 | \u001b[36mINFO\u001b[0m | Task run 'train_model-0' - Finished in state \u001b[32mCompleted\u001b[0m()\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
16:58:19.215 | INFO    | Flow run 'fortunate-stingray' - Finished in state Completed()\n",
       "
\n" ], "text/plain": [ "16:58:19.215 | \u001b[36mINFO\u001b[0m | Flow run\u001b[35m 'fortunate-stingray'\u001b[0m - Finished in state \u001b[32mCompleted\u001b[0m()\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "_ = prefect_workflow.run()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "35ae3fdf", "metadata": {}, "source": [ "## Prefect Server\n", "\n", "A Prefect Flow is created within the functionfuse Workflow, and the Flow Run is \n", "logged to the Prefect Server. The Flow name matches the workflow name. Each \n", "Node in the functionfuse Workflow becomes a Prefect Task with a name matching\n", "the Node.func name.\n", "\n", "![Flow Run in Prefect Server](/images/PrefectServerBasicML.png)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "f2d7826a", "metadata": {}, "source": [ "## Model Prediction\n", "\n", "The code above is designed to train models and save the workflow data in the storage. Typically this code should be placed in a separate Python module. We placed the workflow code in the Jupyter Notebook for demonstration purposes only. Final model analysis and data visualization are performed from the stored data in the Jupyter Notebook as shown below." ] }, { "cell_type": "code", "execution_count": 3, "id": "225a0031", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "All graph node names: \n", "['dataset', 'dataset_split', 'model']\n" ] } ], "source": [ "import pprint\n", "from functionfuse.storage import storage_factory\n", "\n", "the_workflow_name = \"classifier\"\n", "storage_path = \"storage\"\n", "opt = {\n", " \"kind\": \"file\",\n", " \"options\": {\n", " \"path\": storage_path\n", " }\n", "}\n", "storage = storage_factory(opt)\n", "all_tasks = storage.list_tasks(workflow_name=the_workflow_name, pattern=\"*\")\n", "\n", "pp = pprint.PrettyPrinter(width=141, compact=True)\n", "print(\"All graph node names: \")\n", "pp.pprint(all_tasks)" ] }, { "cell_type": "code", "execution_count": 4, "id": "5e8dcb15", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sparsity with L1 penalty: 80.24%\n", "Test score with L1 penalty: 0.8349\n" ] }, { "data": { "text/plain": [ "Text(0.5, 0.98, 'Classification vector for...')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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VqxYIdddd52Ew2G55ZZbRETkyiuvlLFjx8qjjz4qfr9fli9fLllZWSIiMm/ePIlGo/L2229Lbm6urFy5UvLy8qyPs2HDBtmxY4dMmTKlLRYMBmXSpEny97//Xb71rW8dxyuAZEqH9YeuK13XX3V1tfh8viPOD+khHddgNBqVX/ziFxKJRGTMmDHH/qSRMtJl/cViMbnqqqvk5ptvlpEjRx7fk051yc5+XHj//feNiJjnn3/+qGMlLuuNd88995hTTz21bTscDpuFCxdax44aNcrccccdCc3xnXfeMSJitm3b5olfd911ZsqUKQndB1JTOqy/Q/HNR9eSbuvPGGMaGhrMqaeeaq688sp23R6pJZ3W4Msvv2xyc3ONz+cz5eXl5oMPPjim2yP1pMv6++EPf2gmT55sYrGYMcZ06W8+ukXBuTFGRA5cxeBY/fa3v5UzzjhDysrKJC8vT77//e/L5s2b237+f//v/5U5c+bIueeeKz/60Y9k/fr1bT/7l3/5F7nrrrvk9NNPlwULFsjHH3981MeLn6Mxpl3zRupIp/WHrifd1l9zc7PMmDFDYrGYPPLII8c8Z6SedFqDZ599tixfvlz+/ve/y/nnny9f+9rXZNeuXcc8b6SOdFh///u//ysPPvigLFy4sFt85usWycfQoUPF5/PJZ599dky3e++992TGjBkydepUeeWVV2TZsmVy2223STQabRtzxx13yKeffirTpk2TRYsWyYgRI+SFF14QEZE5c+bI559/LldddZWsWLFCvvCFL8jDDz9sfayysjIROXA1hkPt2rVLevbseUzzRmpJh/WHriud1l9zc7N87Wtfkw0bNsibb74p+fn5x/6EkXLSaQ3m5ubKkCFDZPz48fL4449LZmamPP7448f+pJEy0mH9/fWvf5Vdu3ZJv379JDMzUzIzM2XTpk1y0003yYABA9r93FNWMr92cen8888/5mKj++67zwwaNMgzdvbs2SYSiRz2cWbMmGGmT59u/dmtt95qRo0aZf3ZwYLzH//4x22xpqYmCs67iFRff4fitKuuJx3WXzQaNZdccokZOXKk2bVr1+GfDNJSOqxBm8GDB5sFCxYc022QelJ9/e3Zs8esWLHC86+8vNx85zvfMatWrTryk0tD3eKbDxGRRx55RFpbW2XcuHHyu9/9TtauXSufffaZPPTQQzJhwgTr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SYf1F2/nzp3y6quvylNPPXXsTxgpJ13W4BlnnCEvvfSSXHvttVJeXi6LFy+WNWvWyIMPPnh8LwCSKh3WX1NTk4iI50wDv98vgUBA/va3v8mcOXPa+/RTU7KzHxfef/99IyLm+eefP+pYict6491zzz3m1FNPbdsOh8Nm4cKF1rGjRo0yd9xxR8LzfPbZZ80rr7xiVqxYYV566SUzZswYM3LkSNPY2JjwfSD1pMP62759uxEREwqFzP3332+WLVtm7r77buPz+czixYsTug+kpnRYf/F+/OMfm8LCQtPQ0NCu2yO1pMsabGpqMldffbUREZOZmWkCgYB5+umnE749UlM6rL9oNGr69+9vvvrVr5rKykrT1NRk7r77biMiZsqUKQndRzrpFsnHe++9d9QFdVD8uOeee86cfvrppmfPniY3N9cEg0FTUlLS9vMFCxaYzMxMc84555i7777brFu3ru1njz32mMnMzDQTJ040t99+u/noo4+Oad4VFRUmKyvL/O53vzum2yG1pMP627ZtmxER8/Wvf90Tnz59upkxY0biTxYpJx3WX7zhw4eb+fPnJzweqS1d1uC9995rhg0bZl566SXz0UcfmYcfftjk5eWZN99885ifM1JHuqy/Dz/80IwZM8aIiPH7/ea8884zU6dONVOnTj3m55zqukXysXfvXuPz+cwPf/jDo449dOG9++67xu/3m7vuusssWbLErFmzxtx5550mEol4brN69Wpz//33m8mTJ5tAIODJrjdv3mweffRRc+mll5qsrCzz0EMPHdPchwwZYn70ox8d022QWtJh/TU1NZnMzEzzH//xH574LbfcYiZOnHhsTxgpJR3W36HefvttIyJm+fLlx/Q8kbrSYQ3W19ebrKws88orr3jis2fPNuedd96xPWGklHRYf4eqqqoyu3btMsYYM27cODN37tzEn2ya6BbJhzHGnH/++cdcbHTfffeZQYMGecbOnj1bLbxDzZgxw0yfPt36s1tvvdWMGjUq4Tnv2bPHBINB89RTTyV8G6SmdFh/EyZMUAXnl1xyifo2BOknHdbfQTNnzvSc1oCuIdXXYHV1tRER84c//MET/+Y3v2kmT5582MdDekj19WezZs0ak5GRYd54442Eb5Muuk2fj0ceeURaW1tl3Lhx8rvf/U7Wrl0rn332mTz00EMyYcIE622GDBkimzdvlmeffVbWr18vDz30kLzwwgttP29oaJD58+fL4sWLZdOmTfLOO+/IkiVL5MQTTxQRkRtvvFHeeOMN2bBhgyxdulQWLVrU9rN4tbW18u1vf1veffdd2bhxoyxevFimT58uPXr0kEsvvbTjXxA4lerrT0Tk5ptvlt/85jfy2GOPybp16+SnP/2pvPzyyzJ37tyOfTHgXDqsPxGR/fv3y3PPPdf1iiuR8mswPz9fJk2aJDfffLMsXrxYNmzYIAsXLpSnn36aY3AXkOrrT0Tkueeek8WLF8vnn38uv//972Xy5MlyySWXyJQpUzr2xUgFyc5+XKqoqDDz5s0z/fv3N4FAwPTu3dtcdNFF5q233mobI3Hn+918882muLjY5OXlmcsvv9w88MADbVlvU1OTmTFjhunbt68JBAKmvLzczJ8/v61Icv78+Wbw4MFt5wheddVVZs+ePda51dfXmylTppiSkhKTlZVl+vXrZ2bOnGk2b97cWS8HHEvl9XfQ448/boYMGWKys7PNmDFjzIsvvtjRLwOSJB3W33/913+ZnJwcU1VV1dFPHykg1dfg9u3bzaxZs0x5ebnJzs42w4cPNz/5yU9MLBbrjJcDjqX6+nvwwQdNnz592j4Dfu973zNNTU2d8VIknc8YY5Ka/QAAAADoFrrNaVcAAAAAkovkAwAAAIATJB8AAAAAnCD5AAAAAOAEyQcAAAAAJ0g+AAAAADhB8gEAAADACZIPAAAAAE6QfAAAAABwguQDAAAAgBMkHwAAAACc+H+ueoJwa7C66AAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "clf = storage.read_task(workflow_name=the_workflow_name, task_name=\"model\")\n", "dataset_split = storage.read_task(workflow_name=the_workflow_name, task_name=\"dataset_split\")\n", "X_test, y_test = dataset_split[\"X_test\"], dataset_split[\"y_test\"]\n", "\n", "lr = clf.named_steps[\"logisticregression\"]\n", "\n", "sparsity = np.mean(lr.coef_ == 0) * 100\n", "score = clf.score(X_test, y_test)\n", "# print('Best C % .4f' % clf.C_)\n", "print(\"Sparsity with L1 penalty: %.2f%%\" % sparsity)\n", "print(\"Test score with L1 penalty: %.4f\" % score)\n", " \n", "\n", "\n", "coef = lr.coef_.copy()\n", "plt.figure(figsize=(10, 5))\n", "scale = np.abs(coef).max()\n", "for i in range(10):\n", " l1_plot = plt.subplot(2, 5, i + 1)\n", " l1_plot.imshow(\n", " coef[i].reshape(28, 28),\n", " interpolation=\"nearest\",\n", " cmap=plt.cm.RdBu,\n", " vmin=-scale,\n", " vmax=scale,\n", " )\n", " l1_plot.set_xticks(())\n", " l1_plot.set_yticks(())\n", " l1_plot.set_xlabel(\"Class %i\" % i)\n", "plt.suptitle(\"Classification vector for...\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8" } }, "nbformat": 4, "nbformat_minor": 5 }