{ "cells": [ { "cell_type": "raw", "id": "313392d3", "metadata": {}, "source": [ "Run in Google Colab" ] }, { "cell_type": "markdown", "id": "270660b6", "metadata": {}, "source": [ "# Sparse Inputs" ] }, { "cell_type": "markdown", "id": "619f5911", "metadata": {}, "source": [ "SciKeras supports sparse inputs (`X`/features).\n", "You don't have to do anything special for this to work, you can just pass a sparse matrix to `fit()`.\n", "\n", "In this notebook, we'll demonstrate how this works and compare memory consumption of sparse inputs to dense inputs." ] }, { "cell_type": "markdown", "id": "0b0f3154", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "execution_count": 1, "id": "eb543c89", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:51.214939Z", "iopub.status.busy": "2022-10-14T16:51:51.214682Z", "iopub.status.idle": "2022-10-14T16:51:55.304708Z", "shell.execute_reply": "2022-10-14T16:51:55.304063Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Collecting memory_profiler\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Downloading memory_profiler-0.60.0.tar.gz (38 kB)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Preparing metadata (setup.py) ... \u001b[?25l-" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\b \b\\" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\b \bdone\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\u001b[?25hRequirement already satisfied: psutil in /home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages (from memory_profiler) (5.9.2)\r\n", "Building wheels for collected packages: memory_profiler\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Building wheel for memory_profiler (setup.py) ... \u001b[?25l-" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\b \b\\" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\b \b|" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\b \bdone\r\n", "\u001b[?25h Created wheel for memory_profiler: filename=memory_profiler-0.60.0-py3-none-any.whl size=31267 sha256=53a9e045284d81a31069de19d473a8891b26ccc5fcccdfb88d4e60062c87fe3d\r\n", " Stored in directory: /home/runner/.cache/pip/wheels/01/ca/8b/b518dd2aef69635ad6fcab87069c9c52f355a2e9c5d4c02da9\r\n", "Successfully built memory_profiler\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Installing collected packages: memory_profiler\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Successfully installed memory_profiler-0.60.0\r\n" ] } ], "source": [ "!pip install memory_profiler\n", "%load_ext memory_profiler" ] }, { "cell_type": "code", "execution_count": 2, "id": "f73f710a", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:55.309053Z", "iopub.status.busy": "2022-10-14T16:51:55.308479Z", "iopub.status.idle": "2022-10-14T16:51:57.361732Z", "shell.execute_reply": "2022-10-14T16:51:57.361073Z" } }, "outputs": [], "source": [ "import warnings\n", "import os\n", "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n", "from tensorflow import get_logger\n", "get_logger().setLevel('ERROR')\n", "warnings.filterwarnings(\"ignore\", message=\"Setting the random state for TF\")" ] }, { "cell_type": "code", "execution_count": 3, "id": "eedb68ef", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.371361Z", "iopub.status.busy": "2022-10-14T16:51:57.370690Z", "iopub.status.idle": "2022-10-14T16:51:57.387604Z", "shell.execute_reply": "2022-10-14T16:51:57.386895Z" } }, "outputs": [], "source": [ "try:\n", " import scikeras\n", "except ImportError:\n", " !python -m pip install scikeras" ] }, { "cell_type": "code", "execution_count": 4, "id": "2cefcb7a", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.390805Z", "iopub.status.busy": "2022-10-14T16:51:57.390567Z", "iopub.status.idle": "2022-10-14T16:51:57.618823Z", "shell.execute_reply": "2022-10-14T16:51:57.618118Z" } }, "outputs": [], "source": [ "import scipy\n", "import numpy as np\n", "from scikeras.wrappers import KerasRegressor\n", "from sklearn.preprocessing import OneHotEncoder\n", "from sklearn.pipeline import Pipeline\n", "from tensorflow import keras" ] }, { "cell_type": "markdown", "id": "f700b457", "metadata": {}, "source": [ "## Data\n", "\n", "The dataset we'll be using is designed to demostrate a worst-case/best-case scenario for dense and sparse input features respectively.\n", "It consists of a single categorical feature with equal number of categories as rows.\n", "This means the one-hot encoded representation will require as many columns as it does rows, making it very ineffienct to store as a dense matrix but very efficient to store as a sparse matrix." ] }, { "cell_type": "code", "execution_count": 5, "id": "47b5f036", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.626160Z", "iopub.status.busy": "2022-10-14T16:51:57.624700Z", "iopub.status.idle": "2022-10-14T16:51:57.632068Z", "shell.execute_reply": "2022-10-14T16:51:57.630291Z" } }, "outputs": [], "source": [ "N_SAMPLES = 20_000 # hand tuned to be ~4GB peak\n", "\n", "X = np.arange(0, N_SAMPLES).reshape(-1, 1)\n", "y = np.random.uniform(0, 1, size=(X.shape[0],))" ] }, { "cell_type": "markdown", "id": "6e1e7d5f", "metadata": {}, "source": [ "## Model\n", "\n", "The model here is nothing special, just a basic multilayer perceptron with one hidden layer." ] }, { "cell_type": "code", "execution_count": 6, "id": "023a4faf", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.640124Z", "iopub.status.busy": "2022-10-14T16:51:57.638790Z", "iopub.status.idle": "2022-10-14T16:51:57.644397Z", "shell.execute_reply": "2022-10-14T16:51:57.643888Z" } }, "outputs": [], "source": [ "def get_clf(meta) -> keras.Model:\n", " n_features_in_ = meta[\"n_features_in_\"]\n", " model = keras.models.Sequential()\n", " model.add(keras.layers.Input(shape=(n_features_in_,)))\n", " # a single hidden layer\n", " model.add(keras.layers.Dense(100, activation=\"relu\"))\n", " model.add(keras.layers.Dense(1))\n", " return model" ] }, { "cell_type": "markdown", "id": "09423f21", "metadata": {}, "source": [ "## Pipelines\n", "\n", "Here is where it gets interesting.\n", "We make two Scikit-Learn pipelines that use `OneHotEncoder`: one that uses `sparse=False` to force a dense matrix as the output and another that uses `sparse=True` (the default)." ] }, { "cell_type": "code", "execution_count": 7, "id": "5016b0d2", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.648867Z", "iopub.status.busy": "2022-10-14T16:51:57.647543Z", "iopub.status.idle": "2022-10-14T16:51:57.653195Z", "shell.execute_reply": "2022-10-14T16:51:57.652700Z" } }, "outputs": [], "source": [ "dense_pipeline = Pipeline(\n", " [\n", " (\"encoder\", OneHotEncoder(sparse=False)),\n", " (\"model\", KerasRegressor(get_clf, loss=\"mse\", epochs=5, verbose=False))\n", " ]\n", ")\n", "\n", "sparse_pipeline = Pipeline(\n", " [\n", " (\"encoder\", OneHotEncoder(sparse=True)),\n", " (\"model\", KerasRegressor(get_clf, loss=\"mse\", epochs=5, verbose=False))\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "e4f3b148", "metadata": {}, "source": [ "## Benchmark\n", "\n", "Our benchmark will be to just train each one of these pipelines and measure peak memory consumption." ] }, { "cell_type": "code", "execution_count": 8, "id": "5e7cb355", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:51:57.657541Z", "iopub.status.busy": "2022-10-14T16:51:57.656440Z", "iopub.status.idle": "2022-10-14T16:52:48.575769Z", "shell.execute_reply": "2022-10-14T16:52:48.575107Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "peak memory: 3540.61 MiB, increment: 3151.28 MiB\n" ] } ], "source": [ "%memit dense_pipeline.fit(X, y)" ] }, { "cell_type": "code", "execution_count": 9, "id": "07889aab", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:52:48.581168Z", "iopub.status.busy": "2022-10-14T16:52:48.580011Z", "iopub.status.idle": "2022-10-14T16:53:03.514445Z", "shell.execute_reply": "2022-10-14T16:53:03.513716Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_1/dense_2/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_1/dense_2/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_1/dense_2/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "peak memory: 678.02 MiB, increment: 62.62 MiB\n" ] } ], "source": [ "%memit sparse_pipeline.fit(X, y)" ] }, { "cell_type": "markdown", "id": "ebc982ec", "metadata": {}, "source": [ "You should see at least 100x more memory consumption **increment** in the dense pipeline." ] }, { "cell_type": "markdown", "id": "e574e0a8", "metadata": {}, "source": [ "### Runtime\n", "\n", "Using sparse inputs can have a drastic impact on memory usage, but it often (not always) hurts overall runtime." ] }, { "cell_type": "code", "execution_count": 10, "id": "d60058e5", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:53:03.517911Z", "iopub.status.busy": "2022-10-14T16:53:03.517654Z", "iopub.status.idle": "2022-10-14T16:57:08.731384Z", "shell.execute_reply": "2022-10-14T16:57:08.730648Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "27.8 s ± 1.27 s per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%timeit dense_pipeline.fit(X, y)" ] }, { "cell_type": "code", "execution_count": 11, "id": "ca9c9c51", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:57:08.734766Z", "iopub.status.busy": "2022-10-14T16:57:08.734530Z", "iopub.status.idle": "2022-10-14T16:58:31.717636Z", "shell.execute_reply": "2022-10-14T16:58:31.717048Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_10/dense_20/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_10/dense_20/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_10/dense_20/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_11/dense_22/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_11/dense_22/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_11/dense_22/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_12/dense_24/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_12/dense_24/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_12/dense_24/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_13/dense_26/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_13/dense_26/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_13/dense_26/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_14/dense_28/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_14/dense_28/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_14/dense_28/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_15/dense_30/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_15/dense_30/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_15/dense_30/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_16/dense_32/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_16/dense_32/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_16/dense_32/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_17/dense_34/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_17/dense_34/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_17/dense_34/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "10.5 s ± 360 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%timeit sparse_pipeline.fit(X, y)" ] }, { "cell_type": "markdown", "id": "f45c5929", "metadata": {}, "source": [ "## Tensorflow Datasets\n", "\n", "Tensorflow provides a whole suite of functionality around the [Dataset].\n", "Datasets are lazily evaluated, can be sparse and minimize the transformations required to feed data into the model.\n", "They are _a lot_ more performant and efficient at scale than using numpy datastructures, even sparse ones.\n", "\n", "SciKeras does not (and cannot) support Datasets directly because Scikit-Learn itself does not support them and SciKeras' outwards API is Scikit-Learn's API.\n", "You may want to explore breaking out of SciKeras and just using TensorFlow/Keras directly to see if Datasets can have a large impact for your use case.\n", "\n", "[Dataset]: https://www.tensorflow.org/api_docs/python/tf/data/Dataset" ] }, { "cell_type": "markdown", "id": "9392ba61", "metadata": {}, "source": [ "## Bonus: dtypes\n", "\n", "You might be able to save even more memory by changing the output dtype of `OneHotEncoder`." ] }, { "cell_type": "code", "execution_count": 12, "id": "1e0b8659", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:58:31.722724Z", "iopub.status.busy": "2022-10-14T16:58:31.722247Z", "iopub.status.idle": "2022-10-14T16:58:31.727403Z", "shell.execute_reply": "2022-10-14T16:58:31.726788Z" } }, "outputs": [], "source": [ "sparse_pipline_uint8 = Pipeline(\n", " [\n", " (\"encoder\", OneHotEncoder(sparse=True, dtype=np.uint8)),\n", " (\"model\", KerasRegressor(get_clf, loss=\"mse\", epochs=5, verbose=False))\n", " ]\n", ")" ] }, { "cell_type": "code", "execution_count": 13, "id": "b2884d99", "metadata": { "execution": { "iopub.execute_input": "2022-10-14T16:58:31.730528Z", "iopub.status.busy": "2022-10-14T16:58:31.729921Z", "iopub.status.idle": "2022-10-14T16:58:41.427477Z", "shell.execute_reply": "2022-10-14T16:58:41.426661Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.8/site-packages/tensorflow/python/framework/indexed_slices.py:444: UserWarning: Converting sparse IndexedSlices(IndexedSlices(indices=Tensor(\"gradient_tape/sequential_18/dense_36/embedding_lookup_sparse/Reshape_1:0\", shape=(None,), dtype=int32), values=Tensor(\"gradient_tape/sequential_18/dense_36/embedding_lookup_sparse/Reshape:0\", shape=(None, 100), dtype=float32), dense_shape=Tensor(\"gradient_tape/sequential_18/dense_36/embedding_lookup_sparse/Cast:0\", shape=(2,), dtype=int32))) to a dense Tensor of unknown shape. This may consume a large amount of memory.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "peak memory: 1069.02 MiB, increment: 0.44 MiB\n" ] } ], "source": [ "%memit sparse_pipline_uint8.fit(X, y)" ] } ], "metadata": { "jupytext": { "formats": "ipynb,md" }, "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.8.14" } }, "nbformat": 4, "nbformat_minor": 5 }