{
"cells": [
{
"cell_type": "raw",
"id": "fb28bffb",
"metadata": {},
"source": [
"
Run in Google Colab"
]
},
{
"cell_type": "markdown",
"id": "bd403cec",
"metadata": {},
"source": [
"# Autoencoders in SciKeras\n",
"\n",
"Autencoders are an approach to use nearual networks to distill data into it's most important features, thereby compressing the data.\n",
"We will be following the [Keras tutorial](https://blog.keras.io/building-autoencoders-in-keras.html) on the topic, which goes much more in depth and breadth than we will here.\n",
"You are highly encouraged to check out that tutorial if you want to learn about autoencoders in the general sense.\n",
"\n",
"## Table of contents\n",
"\n",
"* [1. Setup](#1.-Setup)\n",
"* [2. Data](#2.-Data)\n",
"* [3. Define Keras Model](#3.-Define-Keras-Model)\n",
"* [4. Training](#4.-Training)\n",
"* [5. Explore Results](#5.-Explore-Results)\n",
"* [6. Deep AutoEncoder](#6.-Deep-AutoEncoder)\n",
"\n",
"## 1. Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "651d6e81",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:35.382842Z",
"iopub.status.busy": "2022-10-14T16:50:35.382570Z",
"iopub.status.idle": "2022-10-14T16:50:37.647882Z",
"shell.execute_reply": "2022-10-14T16:50:37.647277Z"
}
},
"outputs": [],
"source": [
"try:\n",
" import scikeras\n",
"except ImportError:\n",
" !python -m pip install scikeras"
]
},
{
"cell_type": "markdown",
"id": "317207fd",
"metadata": {},
"source": [
"Silence TensorFlow logging to keep output succinct."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a0859ea9",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:37.654249Z",
"iopub.status.busy": "2022-10-14T16:50:37.651703Z",
"iopub.status.idle": "2022-10-14T16:50:37.660099Z",
"shell.execute_reply": "2022-10-14T16:50:37.659594Z"
}
},
"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": "48ba7f7f",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:37.665161Z",
"iopub.status.busy": "2022-10-14T16:50:37.663625Z",
"iopub.status.idle": "2022-10-14T16:50:37.907124Z",
"shell.execute_reply": "2022-10-14T16:50:37.905259Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"from scikeras.wrappers import KerasClassifier, KerasRegressor\n",
"from tensorflow import keras"
]
},
{
"cell_type": "markdown",
"id": "af357d15",
"metadata": {},
"source": [
"## 2. Data\n",
"\n",
"We load the dataset from the Keras tutorial. The dataset consists of images of cats and dogs."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "b74cdbad",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:37.915145Z",
"iopub.status.busy": "2022-10-14T16:50:37.911204Z",
"iopub.status.idle": "2022-10-14T16:50:38.443076Z",
"shell.execute_reply": "2022-10-14T16:50:38.442472Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(60000, 784)\n",
"(10000, 784)\n"
]
}
],
"source": [
"from tensorflow.keras.datasets import mnist\n",
"import numpy as np\n",
"\n",
"\n",
"(x_train, _), (x_test, _) = mnist.load_data()\n",
"x_train = x_train.astype('float32') / 255.\n",
"x_test = x_test.astype('float32') / 255.\n",
"x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))\n",
"x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))\n",
"print(x_train.shape)\n",
"print(x_test.shape)"
]
},
{
"cell_type": "markdown",
"id": "c9f0e289",
"metadata": {},
"source": [
"## 3. Define Keras Model\n",
"\n",
"We will be defining a very simple autencoder. We define _three_ model architectures:\n",
"\n",
"1. An encoder: a series of densly connected layers culminating in an \"output\" layer that determines the encoding dimensions.\n",
"2. A decoder: takes the output of the encoder as it's input and reconstructs the original data.\n",
"3. An autoencoder: a chain of the encoder and decoder that directly connects them for training purposes.\n",
"\n",
"The only variable we give our model is the encoding dimensions, which will be a hyperparemter of our final transformer.\n",
"\n",
"The encoder and decoder are views to the first/last layers of the autoencoder model.\n",
"They'll be directly used in `transform` and `inverse_transform`, so we'll create some SciKeras models with those layers\n",
"and save them as in `encoder_model_` and `decoder_model_`. All three models are created within `_keras_build_fn`.\n",
"\n",
"For a background on chaining Functional Models like this, see [All models are callable](https://keras.io/guides/functional_api/#all-models-are-callable-just-like-layers) in the Keras docs."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b4d6c6c8",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:38.447964Z",
"iopub.status.busy": "2022-10-14T16:50:38.446816Z",
"iopub.status.idle": "2022-10-14T16:50:38.459164Z",
"shell.execute_reply": "2022-10-14T16:50:38.458636Z"
}
},
"outputs": [],
"source": [
"from typing import Dict, Any\n",
"\n",
"from sklearn.base import TransformerMixin\n",
"from sklearn.metrics import mean_squared_error\n",
"from scikeras.wrappers import BaseWrapper\n",
"\n",
"\n",
"class AutoEncoder(BaseWrapper, TransformerMixin):\n",
" \"\"\"A class that enables transform and fit_transform.\n",
" \"\"\"\n",
"\n",
" encoder_model_: BaseWrapper\n",
" decoder_model_: BaseWrapper\n",
" \n",
" def _keras_build_fn(self, encoding_dim: int, meta: Dict[str, Any]):\n",
" n_features_in = meta[\"n_features_in_\"]\n",
"\n",
" encoder_input = keras.Input(shape=(n_features_in,))\n",
" encoder_output = keras.layers.Dense(encoding_dim, activation='relu')(encoder_input)\n",
" encoder_model = keras.Model(encoder_input, encoder_output)\n",
"\n",
" decoder_input = keras.Input(shape=(encoding_dim,))\n",
" decoder_output = keras.layers.Dense(n_features_in, activation='sigmoid', name=\"decoder\")(decoder_input)\n",
" decoder_model = keras.Model(decoder_input, decoder_output)\n",
" \n",
" autoencoder_input = keras.Input(shape=(n_features_in,))\n",
" encoded_img = encoder_model(autoencoder_input)\n",
" reconstructed_img = decoder_model(encoded_img)\n",
"\n",
" autoencoder_model = keras.Model(autoencoder_input, reconstructed_img)\n",
"\n",
" self.encoder_model_ = BaseWrapper(encoder_model, verbose=self.verbose)\n",
" self.decoder_model_ = BaseWrapper(decoder_model, verbose=self.verbose)\n",
"\n",
" return autoencoder_model\n",
" \n",
" def _initialize(self, X, y=None):\n",
" X, _ = super()._initialize(X=X, y=y)\n",
" # since encoder_model_ and decoder_model_ share layers (and their weights)\n",
" # X_tf here come from random weights, but we only use it to initialize our models\n",
" X_tf = self.encoder_model_.initialize(X).predict(X)\n",
" self.decoder_model_.initialize(X_tf)\n",
" return X, X\n",
"\n",
" def initialize(self, X):\n",
" self._initialize(X=X, y=X)\n",
" return self\n",
"\n",
" def fit(self, X, *, sample_weight=None) -> \"AutoEncoder\":\n",
" super().fit(X=X, y=X, sample_weight=sample_weight)\n",
" # at this point, encoder_model_ and decoder_model_\n",
" # are both \"fitted\" because they share layers w/ model_\n",
" # which is fit in the above call\n",
" return self\n",
"\n",
" def score(self, X) -> float:\n",
" # Note: we use 1-MSE as the score\n",
" # With MSE, \"larger is better\", but Scikit-Learn\n",
" # always maximizes the score (e.g. in GridSearch)\n",
" return 1 - mean_squared_error(self.predict(X), X)\n",
"\n",
" def transform(self, X) -> np.ndarray:\n",
" X: np.ndarray = self.feature_encoder_.transform(X)\n",
" return self.encoder_model_.predict(X)\n",
"\n",
" def inverse_transform(self, X_tf: np.ndarray):\n",
" X: np.ndarray = self.decoder_model_.predict(X_tf)\n",
" return self.feature_encoder_.inverse_transform(X)"
]
},
{
"cell_type": "markdown",
"id": "d68e18e7",
"metadata": {},
"source": [
"Next, we wrap the Keras Model with Scikeras. Note that for our encoder/decoder estimators, we do not need to provide a loss function since no training will be done.\n",
"We do however need to have the `fit_model` and `encoding_dim` so that these will be settable by `BaseWrapper.set_params`."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d118b58e",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:38.463730Z",
"iopub.status.busy": "2022-10-14T16:50:38.462599Z",
"iopub.status.idle": "2022-10-14T16:50:38.467264Z",
"shell.execute_reply": "2022-10-14T16:50:38.466747Z"
}
},
"outputs": [],
"source": [
"autoencoder = AutoEncoder(\n",
" loss=\"binary_crossentropy\",\n",
" encoding_dim=32,\n",
" random_state=0,\n",
" epochs=5,\n",
" verbose=False,\n",
" optimizer=\"adam\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "c7e11dc8",
"metadata": {},
"source": [
"## 4. Training\n",
"\n",
"To train the model, we pass the input images as both the features and the target.\n",
"This will train the layers to compress the data as accurately as possible between the encoder and decoder.\n",
"Note that we only pass the `X` parameter, since we defined the mapping `y=X` in `KerasTransformer.fit` above."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "820ea287",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:50:38.471704Z",
"iopub.status.busy": "2022-10-14T16:50:38.470591Z",
"iopub.status.idle": "2022-10-14T16:51:02.857358Z",
"shell.execute_reply": "2022-10-14T16:51:02.856591Z"
}
},
"outputs": [],
"source": [
"_ = autoencoder.fit(X=x_train)"
]
},
{
"cell_type": "markdown",
"id": "8f25c617",
"metadata": {},
"source": [
"Next, we round trip the test dataset and explore the performance of the autoencoder."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "143f25d8",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:02.861109Z",
"iopub.status.busy": "2022-10-14T16:51:02.860739Z",
"iopub.status.idle": "2022-10-14T16:51:04.270287Z",
"shell.execute_reply": "2022-10-14T16:51:04.269457Z"
}
},
"outputs": [],
"source": [
"roundtrip_imgs = autoencoder.inverse_transform(autoencoder.transform(x_test))"
]
},
{
"cell_type": "markdown",
"id": "06d46f1c",
"metadata": {},
"source": [
"## 5. Explore Results\n",
"\n",
"Let's compare our inputs to lossy decoded outputs:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "11cb6f22",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:04.274317Z",
"iopub.status.busy": "2022-10-14T16:51:04.273976Z",
"iopub.status.idle": "2022-10-14T16:51:06.274050Z",
"shell.execute_reply": "2022-10-14T16:51:06.273262Z"
}
},
"outputs": [
{
"data": {
"image/png": "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jz461KZOnRpqP/3pT9/ECP/P2WefXWvfcccdjV534403hpp7loGvPMdmQepbbbVVqGXBrxtvvHGtve+++4Y+I0aMCLVyrcv6HHbYYaFWztWqqqp777031OidLBS4lK1jxx9/fK39+9//PvS5/fbbOx4XC5c//elPoXb11VeHWvkZx6qrrhr6fOtb3wq1uXPn9jmGLGw7C+VuomnQ9euvv15rX3DBBaHPxz/+8VCbPHlyR+PqFd+YAAAAAAAAWmNjAgAAAAAAaI2NCQAAAAAAoDU2JgAAAAAAgNYIv27JHnvsEWqDBw8OtauuuirU/vKXv/RkTPROFhq2+eabN3rtNddcU2uXwVDQhk033TTUstCnc889t43h0E8+9rGPhVoZsjU/2WuvvUJts802q7Wzf19WK8OvefNmzJgRalnQYRkQO3LkyNDnueee69q4mhg9enSoNQmArKqquuGGG7o9HAa47bbbrtY+8MADG71u+vTptfYTTzzRtTHR/6ZNmxZqZVhnFt75uc99rmdjqqqqWmONNWrtQYMGhT7ZWv2Zz3ymV0Oih6688spau1x3qiqGWldVHjLdJCC2/HlVVVVHHnlkrX3xxReHPmuvvXaoZaGu2bUrvbP88svX2tk185AhQ0LtuOOOq7WPPfbY0OfMM88MtZtuuinUygDjhx56KPS55557Qq204YYbhlr2WZxz8cAze/bsUNt3331Dbdlll621P//5z4c+//Iv/xJqzz77bKhNmDCh1s7mefaZytZbbx1qnTrrrLNq7WOOOSb0ef7557v283rFNyYAAAAAAIDW2JgAAAAAAABaY2MCAAAAAABojYyJHhk6dGitvdtuu4U+L7/8cqhleQKvvPJK9wZGT4waNarWzp7tlmWKZMpnts6cObPjcUFTK664Yq29/fbbhz4PPPBAqF1wwQU9GxP9L8tkGIjK59tWVVVtsMEGoZatzU1MnTo11Jyb5132PNiHH3441N7znvfU2pdccknoc/rpp3dtXBtttFGolc9cX2211UKfJs/Wrqr5O6eFzpTXiYss0uz/hl1xxRW9GA68ofLZ79naluVcZOdKBr4yo+m9731v6JNlyg0fPrzPY3/7298OtWzuzJkzp9Y+//zzQ5/sWfC77rprqK255pq1dnZdQfd89atfrbU/9alPdXSc7Lx4xBFHNKr1UraulZmgVVVVBxxwQAujYV6VeQvZutJNP/vZz0KtScZElsOX/W395Cc/qbVfe+215oMbQHxjAgAAAAAAaI2NCQAAAAAAoDU2JgAAAAAAgNbYmAAAAAAAAFoj/LpHjj766Fp7s802C30uu+yyULvxxht7NiZ659Of/nStvdVWWzV63e9+97tQywLQodc+/OEP19qjR48Off7whz+0NBp4c774xS+G2pFHHtnRsR577LFQ+9CHPhRqEyZM6Oj4vLHsHDho0KBa+53vfGfo86tf/aprY3jmmWdCrQx/XW655To+fhlUx4Jvv/3267NPGchYVVX1/e9/vwejgf+z//77h9oHP/jBWjsL4Xz22Wd7Nib615VXXhlq2Rp24IEHhlq5jpVB6lUVg64zX/7yl0Nt/fXXD7W999471MqfmV3D0T1lePA555wT+vzyl78MtcUWq38Uucoqq4Q+WSB225ZffvlQy/4ejj322Fr7v//7v3s2Jgamz372s6HWaSj6xz72sVDr5r3OQNP/f+kAAAAAAMBCw8YEAAAAAADQGhsTAAAAAABAa2xMAAAAAAAArRF+3QVZCOOXvvSlWvuFF14IfU488cSejYl2fepTn+rodUcddVSozZw5c16HA2/auHHj+uwzbdq0FkYCfbv00ktr7XXXXbdrx7733ntD7YYbbuja8Xlj999/f6i9973vrbXHjx8f+qy11lpdG8O5557bZ5+f/vSnoXbQQQc1Ov7s2bPf9JiYf4wdOzbUspDY0hNPPBFqt9xyS1fGBP/M7rvv3mefiy++ONRuvfXWXgyHASoLxM5q3ZKdJ7NQ5Sz8eqeddqq1R44cGfo899xz8zA6/tFrr71Wa2fnrXXWWafP47ztbW8LtcGDB4faCSecEGpbbbVVn8fvpkGDBoXaFlts0eoY6H8f+chHau0yAL2qYsh75p577gm1888/v/OBzYd8YwIAAAAAAGiNjQkAAAAAAKA1NiYAAAAAAIDW2JgAAAAAAABaI/z6TRo1alSofetb3wq1RRddtNYugzqrqqpuuumm7g2M+VIWxvXKK6905djTp09vdOwsVGr48OF9Hn/ZZZcNtU5DwMvQrKqqqs997nO19qxZszo6Ns3sueeeffa56KKLWhgJA0kW7rbIIn3/n4YmYZpVVVVnnXVWrT1mzJhGryvH8Prrrzd6XRN77bVX145Fb9x+++2Nar30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\n",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
"n = 10 # How many digits we will display\n",
"plt.figure(figsize=(20, 4))\n",
"for i in range(n):\n",
" # Display original\n",
" ax = plt.subplot(2, n, i + 1)\n",
" plt.imshow(x_test[i].reshape(28, 28))\n",
" plt.gray()\n",
" ax.get_xaxis().set_visible(False)\n",
" ax.get_yaxis().set_visible(False)\n",
"\n",
" # Display reconstruction\n",
" ax = plt.subplot(2, n, i + 1 + n)\n",
" plt.imshow(roundtrip_imgs[i].reshape(28, 28))\n",
" plt.gray()\n",
" ax.get_xaxis().set_visible(False)\n",
" ax.get_yaxis().set_visible(False)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "99cdc8f1",
"metadata": {},
"source": [
"What about the compression? Let's check the sizes of the arrays."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "700216c9",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:06.279548Z",
"iopub.status.busy": "2022-10-14T16:51:06.277982Z",
"iopub.status.idle": "2022-10-14T16:51:06.975837Z",
"shell.execute_reply": "2022-10-14T16:51:06.973152Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"x_test size (in MB): 29.91\n",
"encoded_imgs size (in MB): 1.22\n",
"Compression ratio: 1/25\n"
]
}
],
"source": [
"encoded_imgs = autoencoder.transform(x_test)\n",
"print(f\"x_test size (in MB): {x_test.nbytes/1024**2:.2f}\")\n",
"print(f\"encoded_imgs size (in MB): {encoded_imgs.nbytes/1024**2:.2f}\")\n",
"cr = round((encoded_imgs.nbytes/x_test.nbytes), 2)\n",
"print(f\"Compression ratio: 1/{1/cr:.0f}\")"
]
},
{
"cell_type": "markdown",
"id": "25a30862",
"metadata": {},
"source": [
"## 6. Deep AutoEncoder"
]
},
{
"cell_type": "markdown",
"id": "7d92a2f2",
"metadata": {},
"source": [
"We can easily expand our model to be a deep autoencoder by adding some hidden layers. All we have to do is add a parameter `hidden_layer_sizes` and use it in `_keras_build_fn` to build hidden layers.\n",
"For simplicity, we use a single `hidden_layer_sizes` parameter and mirror it across the encoding layers and decoding layers, but there is nothing forcing us to build symetrical models."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "ff82ac94",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:06.982562Z",
"iopub.status.busy": "2022-10-14T16:51:06.980558Z",
"iopub.status.idle": "2022-10-14T16:51:06.993827Z",
"shell.execute_reply": "2022-10-14T16:51:06.993131Z"
}
},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"\n",
"class DeepAutoEncoder(AutoEncoder):\n",
" \"\"\"A class that enables transform and fit_transform.\n",
" \"\"\"\n",
" \n",
" def _keras_build_fn(self, encoding_dim: int, hidden_layer_sizes: List[str], meta: Dict[str, Any]):\n",
" n_features_in = meta[\"n_features_in_\"]\n",
"\n",
" encoder_input = keras.Input(shape=(n_features_in,))\n",
" x = encoder_input\n",
" for layer_size in hidden_layer_sizes:\n",
" x = keras.layers.Dense(layer_size, activation='relu')(x)\n",
" encoder_output = keras.layers.Dense(encoding_dim, activation='relu')(x)\n",
" encoder_model = keras.Model(encoder_input, encoder_output)\n",
"\n",
" decoder_input = keras.Input(shape=(encoding_dim,))\n",
" x = decoder_input\n",
" for layer_size in reversed(hidden_layer_sizes):\n",
" x = keras.layers.Dense(layer_size, activation='relu')(x)\n",
" decoder_output = keras.layers.Dense(n_features_in, activation='sigmoid', name=\"decoder\")(x)\n",
" decoder_model = keras.Model(decoder_input, decoder_output)\n",
"\n",
" autoencoder_input = keras.Input(shape=(n_features_in,))\n",
" encoded_img = encoder_model(autoencoder_input)\n",
" reconstructed_img = decoder_model(encoded_img)\n",
"\n",
" autoencoder_model = keras.Model(autoencoder_input, reconstructed_img)\n",
"\n",
" self.encoder_model_ = BaseWrapper(encoder_model, verbose=self.verbose)\n",
" self.decoder_model_ = BaseWrapper(decoder_model, verbose=self.verbose)\n",
"\n",
" return autoencoder_model"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "19edd385",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:06.997219Z",
"iopub.status.busy": "2022-10-14T16:51:06.996857Z",
"iopub.status.idle": "2022-10-14T16:51:43.599219Z",
"shell.execute_reply": "2022-10-14T16:51:43.598358Z"
}
},
"outputs": [],
"source": [
"deep = DeepAutoEncoder(\n",
" loss=\"binary_crossentropy\",\n",
" encoding_dim=32,\n",
" hidden_layer_sizes=[128],\n",
" random_state=0,\n",
" epochs=5,\n",
" verbose=False,\n",
" optimizer=\"adam\",\n",
")\n",
"_ = deep.fit(X=x_train)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "f3e5835e",
"metadata": {
"execution": {
"iopub.execute_input": "2022-10-14T16:51:43.603128Z",
"iopub.status.busy": "2022-10-14T16:51:43.602884Z",
"iopub.status.idle": "2022-10-14T16:51:45.175459Z",
"shell.execute_reply": "2022-10-14T16:51:45.174803Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1-MSE for training set (higher is better)\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"AutoEncoder: 0.9899\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Deep AutoEncoder: 0.9916\n"
]
}
],
"source": [
"print(\"1-MSE for training set (higher is better)\\n\")\n",
"score = autoencoder.score(X=x_test)\n",
"print(f\"AutoEncoder: {score:.4f}\")\n",
"\n",
"score = deep.score(X=x_test)\n",
"print(f\"Deep AutoEncoder: {score:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "8878487a",
"metadata": {},
"source": [
"Suprisingly, our score got worse. It's possible that that because of the extra trainable variables, our deep model trains slower than our simple model.\n",
"\n",
"Check out the [Keras tutorial](https://blog.keras.io/building-autoencoders-in-keras.html) to see the difference after 100 epochs of training, as well as more architectures and applications for AutoEncoders!"
]
}
],
"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.8.14"
}
},
"nbformat": 4,
"nbformat_minor": 5
}