{ "cells": [ { "cell_type": "raw", "id": "3b0d7091", "metadata": {}, "source": [ "Run in Google Colab" ] }, { "cell_type": "markdown", "id": "e0294a03", "metadata": {}, "source": [ "# Meta Estimators in SciKeras\n", "\n", "In this notebook, we implement sklearn ensemble and tree meta-estimators backed by a Keras MLP model.\n", "\n", "## Table of contents\n", "\n", "* [1. Setup](#1.-Setup)\n", "* [2. Defining the Keras Model](#2.-Defining-the-Keras-Model)\n", " * [2.1 Building a boosting ensemble](#2.1-Building-a-boosting-ensemble)\n", "* [3. Testing with a toy dataset](#3.-Testing-with-a-toy-dataset)\n", "* [4. Bagging ensemble](#4.-Bagging-ensemble)\n", "\n", "## 1. Setup" ] }, { "cell_type": "code", "execution_count": 1, "id": "b3882af1", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:24:55.432524Z", "iopub.status.busy": "2024-04-11T22:24:55.431866Z", "iopub.status.idle": "2024-04-11T22:25:02.905860Z", "shell.execute_reply": "2024-04-11T22:25:02.905194Z" } }, "outputs": [], "source": [ "try:\n", " import scikeras\n", "except ImportError:\n", " !python -m pip install scikeras[tensorflow]" ] }, { "cell_type": "markdown", "id": "e7a5b32c", "metadata": {}, "source": [ "Silence TensorFlow logging to keep output succinct." ] }, { "cell_type": "code", "execution_count": 2, "id": "ec73b7bc", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:02.909082Z", "iopub.status.busy": "2024-04-11T22:25:02.908419Z", "iopub.status.idle": "2024-04-11T22:25:02.912389Z", "shell.execute_reply": "2024-04-11T22:25:02.911714Z" } }, "outputs": [], "source": [ "import warnings\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": "cab89a42", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:02.914692Z", "iopub.status.busy": "2024-04-11T22:25:02.914496Z", "iopub.status.idle": "2024-04-11T22:25:04.243083Z", "shell.execute_reply": "2024-04-11T22:25:04.242393Z" } }, "outputs": [], "source": [ "import numpy as np\n", "from scikeras.wrappers import KerasClassifier, KerasRegressor\n", "import keras" ] }, { "cell_type": "markdown", "id": "858ca39c", "metadata": {}, "source": [ "## 2. Defining the Keras Model\n", "\n", "We borrow our MLPClassifier implementation from the [MLPClassifier notebook](https://colab.research.google.com/github/adriangb/scikeras/blob/master/notebooks/MLPClassifier_and_MLPRegressor.ipynb)." ] }, { "cell_type": "code", "execution_count": 4, "id": "94f1d9da", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:04.246386Z", "iopub.status.busy": "2024-04-11T22:25:04.245798Z", "iopub.status.idle": "2024-04-11T22:25:04.251891Z", "shell.execute_reply": "2024-04-11T22:25:04.251300Z" } }, "outputs": [], "source": [ "from typing import Dict, Iterable, Any\n", "\n", "\n", "def get_clf_model(hidden_layer_sizes: Iterable[int], meta: Dict[str, Any], compile_kwargs: Dict[str, Any]):\n", " model = keras.Sequential()\n", " inp = keras.layers.Input(shape=(meta[\"n_features_in_\"],))\n", " model.add(inp)\n", " for hidden_layer_size in hidden_layer_sizes:\n", " layer = keras.layers.Dense(hidden_layer_size, activation=\"relu\")\n", " model.add(layer)\n", " if meta[\"target_type_\"] == \"binary\":\n", " n_output_units = 1\n", " output_activation = \"sigmoid\"\n", " loss = \"binary_crossentropy\"\n", " elif meta[\"target_type_\"] == \"multiclass\":\n", " n_output_units = meta[\"n_classes_\"]\n", " output_activation = \"softmax\"\n", " loss = \"sparse_categorical_crossentropy\"\n", " else:\n", " raise NotImplementedError(f\"Unsupported task type: {meta['target_type_']}\")\n", " out = keras.layers.Dense(n_output_units, activation=output_activation)\n", " model.add(out)\n", " model.compile(loss=loss, optimizer=compile_kwargs[\"optimizer\"])\n", " return model" ] }, { "cell_type": "markdown", "id": "b902fb4e", "metadata": {}, "source": [ "Next we wrap this Keras model with SciKeras" ] }, { "cell_type": "code", "execution_count": 5, "id": "56042d3d", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:04.254369Z", "iopub.status.busy": "2024-04-11T22:25:04.254014Z", "iopub.status.idle": "2024-04-11T22:25:04.256981Z", "shell.execute_reply": "2024-04-11T22:25:04.256501Z" } }, "outputs": [], "source": [ "clf = KerasClassifier(\n", " model=get_clf_model,\n", " hidden_layer_sizes=(100, ),\n", " optimizer=\"adam\",\n", " optimizer__learning_rate=0.001,\n", " verbose=0,\n", " random_state=0,\n", ")" ] }, { "cell_type": "markdown", "id": "78eb36c9", "metadata": {}, "source": [ "### 2.1 Building a boosting ensemble\n", "\n", "Because SciKeras estimators are fully compliant with the Scikit-Learn API, we can make use of Scikit-Learn's built in utilities. In particular example, we will use `AdaBoostClassifier` from `sklearn.ensemble.AdaBoostClassifier`, but the process is the same for most Scikit-Learn meta-estimators.\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "dbbfd1b7", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:04.259422Z", "iopub.status.busy": "2024-04-11T22:25:04.258988Z", "iopub.status.idle": "2024-04-11T22:25:04.597206Z", "shell.execute_reply": "2024-04-11T22:25:04.596625Z" } }, "outputs": [], "source": [ "from sklearn.ensemble import AdaBoostClassifier\n", "\n", "\n", "adaboost = AdaBoostClassifier(estimator=clf, random_state=0)" ] }, { "cell_type": "markdown", "id": "1ad6bf64", "metadata": {}, "source": [ "## 3. Testing with a toy dataset\n", "\n", "Before continouing, we will run a small test to make sure we get somewhat reasonable results.\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "1fe80cdb", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:25:04.599942Z", "iopub.status.busy": "2024-04-11T22:25:04.599580Z", "iopub.status.idle": "2024-04-11T22:26:03.759390Z", "shell.execute_reply": "2024-04-11T22:26:03.758764Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/runner/work/scikeras/scikeras/.venv/lib/python3.12/site-packages/sklearn/ensemble/_weight_boosting.py:519: FutureWarning: The SAMME.R algorithm (the default) is deprecated and will be removed in 1.6. Use the SAMME algorithm to circumvent this warning.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Single score: 0.50\n", "AdaBoost score: 0.87\n" ] } ], "source": [ "from sklearn.datasets import make_moons\n", "\n", "\n", "X, y = make_moons()\n", "\n", "single_score = clf.fit(X, y).score(X, y)\n", "\n", "adaboost_score = adaboost.fit(X, y).score(X, y)\n", "\n", "print(f\"Single score: {single_score:.2f}\")\n", "print(f\"AdaBoost score: {adaboost_score:.2f}\")" ] }, { "cell_type": "markdown", "id": "943e3ac7", "metadata": {}, "source": [ "We see that the score for the AdaBoost classifier is slightly higher than that of an individual MLPRegressor instance. We can explore the individual classifiers, and see that each one is composed of a Keras Model with it's own individual weights.\n" ] }, { "cell_type": "code", "execution_count": 8, "id": "565e7a7d", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:26:03.762230Z", "iopub.status.busy": "2024-04-11T22:26:03.761587Z", "iopub.status.idle": "2024-04-11T22:26:03.770688Z", "shell.execute_reply": "2024-04-11T22:26:03.770153Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[-0.00940828 0.04692359 0.08673294 -0.20467542 0.11149351]\n", "[ 0.22922313 0.20061073 0.21357909 0.0311804 -0.09936447]\n" ] } ], "source": [ "print(adaboost.estimators_[0].model_.get_weights()[0][0, :5]) # first sub-estimator\n", "print(adaboost.estimators_[1].model_.get_weights()[0][0, :5]) # second sub-estimator" ] }, { "cell_type": "markdown", "id": "a3606f7b", "metadata": {}, "source": [ "## 4. Bagging ensemble\n", "\n", "For comparison, we run the same test with an ensemble built using `sklearn.ensemble.BaggingClassifier`." ] }, { "cell_type": "code", "execution_count": 9, "id": "16e6fbdb", "metadata": { "execution": { "iopub.execute_input": "2024-04-11T22:26:03.773105Z", "iopub.status.busy": "2024-04-11T22:26:03.772698Z", "iopub.status.idle": "2024-04-11T22:26:13.963674Z", "shell.execute_reply": "2024-04-11T22:26:13.962714Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "WARNING:tensorflow:5 out of the last 9 calls to .one_step_on_data_distributed at 0x7f9b64cc16c0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n", "WARNING:tensorflow:6 out of the last 12 calls to .one_step_on_data_distributed at 0x7f9b64cc16c0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n", "WARNING:tensorflow:5 out of the last 9 calls to .one_step_on_data_distributed at 0x7f7858710f40> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Bagging score: 0.73\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "WARNING:tensorflow:6 out of the last 12 calls to .one_step_on_data_distributed at 0x7f7858710f40> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n" ] } ], "source": [ "from sklearn.ensemble import BaggingClassifier\n", "\n", "\n", "bagging = BaggingClassifier(estimator=clf, random_state=0, n_jobs=-1)\n", "\n", "bagging_score = bagging.fit(X, y).score(X, y)\n", "\n", "print(f\"Bagging score: {bagging_score:.2f}\")" ] } ], "metadata": { "jupytext": { "formats": "ipynb,md" }, "kernelspec": { "display_name": "Python 3", "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.12.2" } }, "nbformat": 4, "nbformat_minor": 5 }