├── .env.example ├── .env.test ├── .github └── workflows │ └── ci_tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Deep Learning MNIST prediction model with Keras.ipynb ├── Dockerfile ├── README.md ├── app ├── Makefile ├── app │ ├── __init__.py │ ├── api.py │ ├── model.py │ ├── static │ │ └── 4.jpg │ └── templates │ │ └── dlflask.html ├── config.py ├── mnist_model.keras ├── server.py └── tests │ ├── __init__.py │ ├── conftest.py │ └── test_app.py ├── docker-compose.yml ├── nginx └── conf.d │ └── local.conf ├── pyproject.toml ├── requirements.txt └── requirements_dev.txt /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=app 2 | SECRET_KEY=app 3 | FLASK_DEBUG=0 4 | MODEL_PATH=/app/mnist_model.keras 5 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | APP_NAME=app 2 | SECRET_KEY=app 3 | FLASK_DEBUG=0 4 | MODEL_PATH=../mnist_model.keras 5 | -------------------------------------------------------------------------------- /.github/workflows/ci_tests.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-24.04 8 | strategy: 9 | matrix: 10 | python: [ "3.12" ] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Load .env file 15 | uses: xom9ikk/dotenv@v2.3.0 16 | with: 17 | path: . 18 | mode: test 19 | - name: Setup python ${{ matrix.python }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python }} 23 | - name: Install dependencies 24 | run: | 25 | python3 -m pip install --upgrade pip 26 | python3 -m pip install --upgrade setuptools 27 | python3 -m pip install -r requirements.txt 28 | - name: Lint with Ruff 29 | run: ruff check --output-format=github . 30 | continue-on-error: true 31 | - name: Run tests 32 | run: | 33 | cd app 34 | make test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .aider* 106 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.11.10 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | types_or: [ python, pyi, jupyter ] 9 | # Run the formatter. 10 | - id: ruff-format 11 | types_or: [ python, pyi, jupyter ] 12 | -------------------------------------------------------------------------------- /Deep Learning MNIST prediction model with Keras.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "The MNIST (Modified National Institute of Standards and Technology) dataset consists of images of handwritten digits that is used for training and testing image processing systems" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "This is a multiclass classification problem in which the goal is to predict a single discrete label (0,1,2,3,4,5,6,7,8,9)\n", 15 | "\n", 16 | "This notebook is used to generate a predictive model for the production system" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "import os\n", 26 | "\n", 27 | "import matplotlib.pyplot as plt # plotting\n", 28 | "import numpy as np # linear algebra\n", 29 | "from skimage import io, transform, util\n", 30 | "from tensorflow.keras import layers, models\n", 31 | "from tensorflow.keras.datasets import mnist # mnist dataset\n", 32 | "from tensorflow.keras.models import load_model\n", 33 | "from tensorflow.keras.utils import to_categorical" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "Loading MNIST dataset" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "(train_images, train_labels), (test_images, test_labels) = mnist.load_data()" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 3, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "(60000, 28, 28)" 61 | ] 62 | }, 63 | "execution_count": 3, 64 | "metadata": {}, 65 | "output_type": "execute_result" 66 | } 67 | ], 68 | "source": [ 69 | "train_images.shape" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 4, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "data": { 79 | "text/plain": [ 80 | "(10000, 28, 28)" 81 | ] 82 | }, 83 | "execution_count": 4, 84 | "metadata": {}, 85 | "output_type": "execute_result" 86 | } 87 | ], 88 | "source": [ 89 | "test_images.shape" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "Linear model definition" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "The output of the network is a layer of size 10 with a probability distribution over the 10 different classes" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 5, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "model = models.Sequential()\n", 113 | "model.add(layers.Dense(512, activation=\"relu\", input_shape=(28 * 28,)))\n", 114 | "model.add(layers.Dense(10, activation=\"softmax\"))" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "Compiling the model" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "The chosen loss function is categorical_crossentropy because is a multiclass classification problem" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 6, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "model.compile(optimizer=\"rmsprop\", loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Encoding the data" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 7, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "train_images_prepared = train_images.reshape((60000, 28 * 28))\n", 154 | "train_images_prepared = train_images_prepared.astype(\"float32\") / 255\n", 155 | "\n", 156 | "test_images_prepared = test_images.reshape((10000, 28 * 28))\n", 157 | "test_images_prepared = test_images_prepared.astype(\"float32\") / 255" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 8, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "train_labels_one_hot = to_categorical(train_labels)\n", 167 | "test_labels_one_hot = to_categorical(test_labels)" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "Setting a validation set of 6000 samples from 60000 training images" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 9, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "train_images_val = train_images_prepared[:6000]\n", 184 | "train_images_partial = train_images_prepared[6000:]\n", 185 | "\n", 186 | "train_labels_val = train_labels_one_hot[:6000]\n", 187 | "train_labels_partial = train_labels_one_hot[6000:]" 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "Training the model for 10 epochs or passes over the entire dataset" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 10, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "name": "stdout", 204 | "output_type": "stream", 205 | "text": [ 206 | "Epoch 1/10\n" 207 | ] 208 | }, 209 | { 210 | "name": "stderr", 211 | "output_type": "stream", 212 | "text": [ 213 | "2024-03-31 21:18:56.862940: W external/local_tsl/tsl/framework/cpu_allocator_impl.cc:83] Allocation of 169344000 exceeds 10% of free system memory.\n" 214 | ] 215 | }, 216 | { 217 | "name": "stdout", 218 | "output_type": "stream", 219 | "text": [ 220 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.8670 - loss: 0.4630 - val_accuracy: 0.9535 - val_loss: 0.1597\n", 221 | "Epoch 2/10\n", 222 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9642 - loss: 0.1230 - val_accuracy: 0.9687 - val_loss: 0.1054\n", 223 | "Epoch 3/10\n", 224 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9777 - loss: 0.0754 - val_accuracy: 0.9762 - val_loss: 0.0829\n", 225 | "Epoch 4/10\n", 226 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9848 - loss: 0.0531 - val_accuracy: 0.9810 - val_loss: 0.0675\n", 227 | "Epoch 5/10\n", 228 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9887 - loss: 0.0384 - val_accuracy: 0.9807 - val_loss: 0.0637\n", 229 | "Epoch 6/10\n", 230 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9917 - loss: 0.0285 - val_accuracy: 0.9803 - val_loss: 0.0656\n", 231 | "Epoch 7/10\n", 232 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9940 - loss: 0.0220 - val_accuracy: 0.9805 - val_loss: 0.0666\n", 233 | "Epoch 8/10\n", 234 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9960 - loss: 0.0152 - val_accuracy: 0.9813 - val_loss: 0.0594\n", 235 | "Epoch 9/10\n", 236 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9970 - loss: 0.0121 - val_accuracy: 0.9783 - val_loss: 0.0678\n", 237 | "Epoch 10/10\n", 238 | "\u001b[1m422/422\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9983 - loss: 0.0086 - val_accuracy: 0.9822 - val_loss: 0.0673\n" 239 | ] 240 | } 241 | ], 242 | "source": [ 243 | "history = model.fit(\n", 244 | " train_images_partial,\n", 245 | " train_labels_partial,\n", 246 | " epochs=10,\n", 247 | " batch_size=128,\n", 248 | " validation_data=(train_images_val, train_labels_val),\n", 249 | ")" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "metadata": {}, 255 | "source": [ 256 | "Plotting training and validation loss" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": 11, 262 | "metadata": {}, 263 | "outputs": [ 264 | { 265 | "data": { 266 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAHHCAYAAABXx+fLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABVjElEQVR4nO3de3zO9f/H8ce1sRM2c9qhzYaENJTDYomyb6O+shwaX2Xkm29yDH2R01CRKDlE+hY6SQpJUYiSQ4RFkiinMEK2Zmxc+/z++Px22WXDzte263m/3T431/W+3tfnen1sdT29P+/P+2MxDMNARERExIm4OLoAERERkaKmACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACRSTPXs2ZPQ0NA8vTcuLg6LxVKwBRUzhw8fxmKxsGDBgiL93A0bNmCxWNiwYYOtLac/q8KqOTQ0lJ49exboPnNiwYIFWCwWDh8+XOSfLZJfCkAiuWSxWHK0Zf6CFMmvzZs3ExcXx/nz5x1dikipUMbRBYiUNO+++67d83feeYc1a9Zkaa9Xr16+PufNN98kPT09T+8dPXo0I0aMyNfnS87l52eVU5s3b2b8+PH07NmTihUr2r22f/9+XFz071mR3FAAEsmlxx57zO751q1bWbNmTZb2a6WkpODl5ZXjzylbtmye6gMoU6YMZcroP++ikp+fVUFwd3d36OeLlET6J4NIIWjdujV33HEHO3bs4N5778XLy4vnnnsOgE8//ZSHHnqIwMBA3N3dqVWrFhMnTsRqtdrt49p5JRnzR6ZOncq8efOoVasW7u7uNG3alO3bt9u9N7s5QBaLhf79+7N8+XLuuOMO3N3dqV+/PqtXr85S/4YNG2jSpAkeHh7UqlWLN954I8fzijZu3EiXLl2oXr067u7uBAcH88wzz3Dx4sUsx1e+fHmOHz9OdHQ05cuXp2rVqgwbNizL38X58+fp2bMnPj4+VKxYkdjY2BydCvrhhx+wWCwsXLgwy2tffvklFouFlStXAnDkyBGefvpp6tSpg6enJ5UrV6ZLly45mt+S3RygnNa8e/duevbsSc2aNfHw8MDf358nnniCs2fP2vrExcXx7LPPAlCjRg3badaM2rKbA/T777/TpUsXKlWqhJeXF3fffTeff/65XZ+M+UwfffQRL7zwAkFBQXh4eNCmTRsOHjx40+O+ntdff5369evj7u5OYGAg/fr1y3LsBw4coFOnTvj7++Ph4UFQUBBdu3YlMTHR1mfNmjXcc889VKxYkfLly1OnTh3bf0ci+aV/IooUkrNnz9KuXTu6du3KY489hp+fH2BOHC1fvjxDhgyhfPnyfP3114wdO5akpCRefvnlm+73gw8+4O+//+Y///kPFouFKVOm0LFjR37//febjkR89913LF26lKeffpoKFSowY8YMOnXqxNGjR6lcuTIAu3btom3btgQEBDB+/HisVisTJkygatWqOTruJUuWkJKSQt++falcuTLbtm1j5syZ/PHHHyxZssSur9VqJSoqivDwcKZOncratWuZNm0atWrVom/fvgAYhkGHDh347rvveOqpp6hXrx7Lli0jNjb2prU0adKEmjVr8tFHH2Xpv3jxYnx9fYmKigJg+/btbN68ma5duxIUFMThw4eZM2cOrVu35ueff87V6F1ual6zZg2///47vXr1wt/fn7179zJv3jz27t3L1q1bsVgsdOzYkV9//ZVFixbx6quvUqVKFYDr/kxOnTpFixYtSElJYeDAgVSuXJmFCxfy8MMP8/HHH/PII4/Y9Z88eTIuLi4MGzaMxMREpkyZQvfu3fn+++9zfMwZ4uLiGD9+PJGRkfTt25f9+/czZ84ctm/fzqZNmyhbtixpaWlERUWRmprKgAED8Pf35/jx46xcuZLz58/j4+PD3r17+ec//0mDBg2YMGEC7u7uHDx4kE2bNuW6JpFsGSKSL/369TOu/U+pVatWBmDMnTs3S/+UlJQsbf/5z38MLy8v49KlS7a22NhYIyQkxPb80KFDBmBUrlzZOHfunK39008/NQDjs88+s7WNGzcuS02A4ebmZhw8eNDW9uOPPxqAMXPmTFtb+/btDS8vL+P48eO2tgMHDhhlypTJss/sZHd8kyZNMiwWi3HkyBG74wOMCRMm2PW98847jcaNG9ueL1++3ACMKVOm2NquXLlitGzZ0gCM+fPn37CekSNHGmXLlrX7O0tNTTUqVqxoPPHEEzese8uWLQZgvPPOO7a29evXG4Cxfv16u2PJ/LPKTc3Zfe6iRYsMwPj2229tbS+//LIBGIcOHcrSPyQkxIiNjbU9Hzx4sAEYGzdutLX9/fffRo0aNYzQ0FDD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EblWTm/1lM9bQuVJuXLl7J4PGzaMZcuW8eKLL7Jx40bi4+MJCwsjLS3thvspW7as3XOLxUJ6enqu+hvZnSMsRMHBwezfv5/XX38dT09Pnn76ae69914uX75MhQoV2LlzJ4sWLSIgIICxY8fSsGHDYnkzXAWgItCxI3z8Mdxyi317UJDZXhhXMYiIlHQlaQR906ZN9OzZk0ceeYSwsDD8/f05fPhwkdbg4+ODn58f27dvt7VZrVZ27tyZq/3Uq1ePTZs22bVt2rSJ22+/3fbc09OT9u3bM2PGDDZs2MCWLVvYs2cPAGXKlCEyMpIpU6awe/duDh8+zNdff52PIyscugy+iHTsaF7qrpWgRURyJmMEvXNnM+xkHugobiPotWvXZunSpbRv3x6LxcKYMWNuOJJTWAYMGMCkSZO49dZbqVu3LjNnzuSvv/7K1e0jnn32WR599FHuvPNOIiMj+eyzz1i6dKntqrYFCxZgtVoJDw/Hy8uL9957D09PT0JCQli5ciW///479957L76+vnzxxRekp6dTp06dwjrkPFMAKkKurtC6taOrEBEpOTJG0LNbB2j69OIzgv7KK6/wxBNP0KJFC6pUqcLw4cNJSkoq8jqGDx9OQkICPXr0wNXVlT59+hAVFZWru6ZHR0fz2muvMXXqVAYNGkSNGjWYP38+rf//C6xixYpMnjyZIUOGYLVaCQsL47PPPqNy5cpUrFiRpUuXEhcXx6VLl6hduzaLFi2ifv36hXTEeWcxivrkYQmQlJSEj48PiYmJeHt7O7ocEZES6dKlSxw6dIgaNWrg4eGRr33pXop5k56eTr169Xj00UeZOHGio8spEDf6vcrN97dGgEREpNjTCHrOHDlyhK+++opWrVqRmprKrFmzOHToEP/6178cXVqxo0nQIiIipYSLiwsLFiygadOmREREsGfPHtauXUu9evUcXVqxoxEgERGRUiI4ODjLFVySPY0AiYiIiNNRABIRERGnowAkIiIiTkcBSERERJyOApCIiIg4HQUgERERcToKQCIiIoWgdevWDB482PY8NDSU6dOn3/A9FouF5cuX5/uzC2o/NxIXF0ejRo0K9TMKkwKQiIhIJu3bt6dt27bZvrZx40YsFgu7d+/O9X63b99Onz598lueneuFkJMnT9KuXbsC/azSRgFIREQkk969e7NmzRr+yHz31f83f/58mjRpQoMGDXK936pVq+Ll5VUQJd6Uv78/7u7uRfJZJZUCkIiISCb//Oc/qVq1KgsWLLBrT05OZsmSJfTu3ZuzZ8/SrVs3brnlFry8vAgLC2PRokU33O+1p8AOHDjAvffei4eHB7fffjtr1qzJ8p7hw4dz22234eXlRc2aNRkzZgyXL18GYMGCBYwfP54ff/wRi8WCxWKx1XztKbA9e/Zw//334+npSeXKlenTpw/Jycm213v27El0dDRTp04lICCAypUr069fP9tn5UR6ejoTJkwgKCgId3d3GjVqxOrVq22vp6Wl0b9/fwICAvDw8CAkJIRJkyYBYBgGcXFxVK9eHXd3dwIDAxk4cGCOPzsvdCsMEREpMoYBKSmO+WwvL7BYbt6vTJky9OjRgwULFjBq1Cgs//+mJUuWYLVa6datG8nJyTRu3Jjhw4fj7e3N559/zuOPP06tWrVo1qzZTT8jPT2djh074ufnx/fff09iYqLdfKEMFSpUYMGCBQQGBrJnzx6efPJJKlSowH//+19iYmL46aefWL16NWvXrgXAx8cnyz4uXLhAVFQUzZs3Z/v27Zw+fZp///vf9O/f3y7krV+/noCAANavX8/BgweJiYmhUaNGPPnkkzf/SwNee+01pk2bxhtvvMGdd97J22+/zcMPP8zevXupXbs2M2bMYMWKFXz00UdUr16dY8eOcezYMQA++eQTXn31VT788EPq169PQkICP/74Y44+N88MySIxMdEAjMTEREeXIiJSYl28eNH4+eefjYsXL9rakpMNw4xBRb8lJ+e89n379hmAsX79eltby5Ytjccee+y673nooYeMoUOH2p63atXKGDRokO15SEiI8eqrrxqGYRhffvmlUaZMGeP48eO211etWmUAxrJly677GS+//LLRuHFj2/Nx48YZDRs2zNIv837mzZtn+Pr6GsmZ/gI+//xzw8XFxUhISDAMwzBiY2ONkJAQ48qVK7Y+Xbp0MWJiYq5by7WfHRgYaLzwwgt2fZo2bWo8/fTThmEYxoABA4z777/fSE9Pz7KvadOmGbfddpuRlpZ23c/LkN3vVYbcfH/rFJiIiMg16tatS4sWLXj77bcBOHjwIBs3bqR3794AWK1WJk6cSFhYGJUqVaJ8+fJ8+eWXHD16NEf737dvH8HBwQQGBtramjdvnqXf4sWLiYiIwN/fn/LlyzN69Ogcf0bmz2rYsCHlypWztUVERJCens7+/fttbfXr18fV1dX2PCAggNOnT+foM5KSkjhx4gQRERF27REREezbtw8wT7PFx8dTp04dBg4cyFdffWXr16VLFy5evEjNmjV58sknWbZsGVeuXMnVceaWApCIiBQZLy9ITnbMltv5x7179+aTTz7h77//Zv78+dSqVYtWrVoB8PLLL/Paa68xfPhw1q9fT3x8PFFRUaSlpRXY39WWLVvo3r07Dz74ICtXrmTXrl2MGjWqQD8js7Jly9o9t1gspKenF9j+77rrLg4dOsTEiRO5ePEijz76KJ07dwbMu9jv37+f119/HU9PT55++mnuvffeXM1Byi3NARIRkSJjsUCmgYhi7dFHH2XQoEF88MEHvPPOO/Tt29c2H2jTpk106NCBxx57DDDn9Pz666/cfvvtOdp3vXr1OHbsGCdPniQgIACArVu32vXZvHkzISEhjBo1ytZ25MgRuz5ubm5YrdabftaCBQu4cOGCbRRo06ZNuLi4UKdOnRzVezPe3t4EBgayadMmW0jM+JzMc6K8vb2JiYkhJiaGzp0707ZtW86dO0elSpXw9PSkffv2tG/fnn79+lG3bl327NnDXXfdVSA1XksBSEREJBvly5cnJiaGkSNHkpSURM+ePW2v1a5dm48//pjNmzfj6+vLK6+8wqlTp3IcgCIjI7ntttuIjY3l5ZdfJikpyS7oZHzG0aNH+fDDD2natCmff/45y5Yts+sTGhrKoUOHiI+PJygoiAoVKmS5/L179+6MGzeO2NhY4uLi+PPPPxkwYACPP/44fn5+efvLycazzz7LuHHjqFWrFo0aNWL+/PnEx8fz/vvvA/DKK68QEBDAnXfeiYuLC0uWLMHf35+KFSuyYMECrFYr4eHheHl58d577+Hp6UlISEiB1XctnQITERG5jt69e/PXX38RFRVlN19n9OjR3HXXXURFRdG6dWv8/f2Jjo7O8X5dXFxYtmwZFy9epFmzZvz73//mhRdesOvz8MMP88wzz9C/f38aNWrE5s2bGTNmjF2fTp060bZtW+677z6qVq2a7aX4Xl5efPnll5w7d46mTZvSuXNn2rRpw6xZs3L3l3ETAwcOZMiQIQwdOpSwsDBWr17NihUrqF27NmBe0TZlyhSaNGlC06ZNOXz4MF988QUuLi5UrFiRN998k4iICBo0aMDatWv57LPPqFy5coHWmJnFMAyj0PZeQiUlJeHj40NiYiLe3t6OLkdEpES6dOkShw4dokaNGnh4eDi6HCklbvR7lZvvb40AiYiIiNNRABIRERGnowAkIiIiTkcBSERERJxOsQhAs2fPJjQ0FA8PD8LDw9m2bdt1+7755pu0bNkSX19ffH19iYyMzNK/Z8+ethvDZWxt27Yt7MMQEZFs6FobKUgF9fvk8AC0ePFihgwZwrhx49i5cycNGzYkKirqustvb9iwgW7durF+/Xq2bNlCcHAwDzzwAMePH7fr17ZtW06ePGnbbnaXXhERKVgZKwunOOrup1IqZfw+XbtydW45/DL48PBwmjZtaluPID09neDgYAYMGMCIESNu+n6r1Yqvry+zZs2iR48egDkCdP78eZYvX56nmnQZvIhIwTh58iTnz5+nWrVqeHl52VZSFsktwzBISUnh9OnTVKxY0baCdma5+f526ErQaWlp7Nixg5EjR9raXFxciIyMZMuWLTnaR0pKCpcvX6ZSpUp27Rs2bKBatWr4+vpy//338/zzz193QaXU1FRSU1Ntz5OSkvJwNCIici1/f3+AHN9UU+RmKlasaPu9yg+HBqAzZ85gtVqzLMXt5+fHL7/8kqN9DB8+nMDAQCIjI21tbdu2pWPHjtSoUYPffvuN5557jnbt2rFlyxa7O91mmDRpEuPHj8/fwYiISBYWi4WAgACqVatWqDe2FOdQtmzZbL/H86JE3wts8uTJfPjhh2zYsMFuNciuXbvaHoeFhdGgQQNq1arFhg0baNOmTZb9jBw5kiFDhtieJyUlERwcXLjFi4g4EVdX1wL74hIpCA6dBF2lShVcXV05deqUXfupU6duOrw1depUJk+ezFdffUWDBg1u2LdmzZpUqVKFgwcPZvu6u7s73t7edpuIiIiUXg4NQG5ubjRu3Jh169bZ2tLT01m3bh3Nmze/7vumTJnCxIkTWb16NU2aNLnp5/zxxx+cPXs22wlTIiIi4nwcfhn8kCFDePPNN1m4cCH79u2jb9++XLhwgV69egHQo0cPu0nSL730EmPGjOHtt98mNDSUhIQEEhISSE5OBiA5OZlnn32WrVu3cvjwYdatW0eHDh249dZbiYqKcsgxioiISPHi8DlAMTEx/Pnnn4wdO5aEhAQaNWrE6tWrbROjjx49iovL1Zw2Z84c0tLS6Ny5s91+xo0bR1xcHK6uruzevZuFCxdy/vx5AgMDeeCBB5g4cSLu7u5FemwiIiJSPDl8HaDiSOsAiYiIlDy5+f52+CkwERERkaKmACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnE6xCECzZ88mNDQUDw8PwsPD2bZt23X7vvnmm7Rs2RJfX198fX2JjIzM0t8wDMaOHUtAQACenp5ERkZy4MCBwj4MERERKSEcHoAWL17MkCFDGDduHDt37qRhw4ZERUVx+vTpbPtv2LCBbt26sX79erZs2UJwcDAPPPAAx48ft/WZMmUKM2bMYO7cuXz//feUK1eOqKgoLl26VFSHJSIiIsWYxTAMw5EFhIeH07RpU2bNmgVAeno6wcHBDBgwgBEjRtz0/VarFV9fX2bNmkWPHj0wDIPAwECGDh3KsGHDAEhMTMTPz48FCxbQtWvXm+4zKSkJHx8fEhMT8fb2zt8BioiISJHIzfe3Q0eA0tLS2LFjB5GRkbY2FxcXIiMj2bJlS472kZKSwuXLl6lUqRIAhw4dIiEhwW6fPj4+hIeHX3efqampJCUl2W0iIiJSejk0AJ05cwar1Yqfn59du5+fHwkJCTnax/DhwwkMDLQFnoz35WafkyZNwsfHx7YFBwfn9lBERESkBHH4HKD8mDx5Mh9++CHLli3Dw8Mjz/sZOXIkiYmJtu3YsWMFWKWIiIgUN2Uc+eFVqlTB1dWVU6dO2bWfOnUKf3//G7536tSpTJ48mbVr19KgQQNbe8b7Tp06RUBAgN0+GzVqlO2+3N3dcXd3z+NRiIiISEnj0BEgNzc3GjduzLp162xt6enprFu3jubNm1/3fVOmTGHixImsXr2aJk2a2L1Wo0YN/P397faZlJTE999/f8N9ioiIiPNw6AgQwJAhQ4iNjaVJkyY0a9aM6dOnc+HCBXr16gVAjx49uOWWW5g0aRIAL730EmPHjuWDDz4gNDTUNq+nfPnylC9fHovFwuDBg3n++eepXbs2NWrUYMyYMQQGBhIdHe2owxQREZFixOEBKCYmhj///JOxY8eSkJBAo0aNWL16tW0S89GjR3FxuTpQNWfOHNLS0ujcubPdfsaNG0dcXBwA//3vf7lw4QJ9+vTh/Pnz3HPPPaxevTpf84RERESk9HD4OkDFkdYBEhERKXlKzDpAIiIiIo6gACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwFIREREnI4CkIiIiDgdBSARERFxOgpAIiIi4nQUgERERMTpKACJiIiI01EAEhEREaejACQiIiJORwGoCB09Cm3awK5djq5ERETEuSkAFaHRo+HrryEmBv7+29HViIiIOC8FoCI0fToEB8OBA/DUU2AYjq5IRETEOSkAFaFKlWDRInB1hQ8+gPnzHV2RiIiIc1IAKmIREfD88+bj/v1h717H1iMiIuKMFIAc4L//hQcegIsX4dFHISXF0RWJiIg4FwUgB3BxgXffBX9/+PlnGDDA0RWJiIg4FwUgB6lWzZwH5OICb78N773n6IpEREScR54C0LFjx/jjjz9sz7dt28bgwYOZN29egRXmDO67D8aONR8/9RT8+qtj6xEREXEWeQpA//rXv1i/fj0ACQkJ/OMf/2Dbtm2MGjWKCRMm5Gpfs2fPJjQ0FA8PD8LDw9m2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b+jXrx9bt25lzZo1XL58mQceeIALFy44ujSnt337dt544w0aNGjg6FKc2l9//UVERARly5Zl1apV/Pzzz0ybNg1fX19Hl+aUXnrpJebMmcOsWbPYt28fL730ElOmTGHmzJmOLs0pXLhwgYYNGzJ79uxsX58yZQozZsxg7ty5fP/995QrV46oqCguXbpUJPXpMnjJsz///JNq1arxzTffcO+99zq6HKeVnJzMXXfdxeuvv87zzz9Po0aNmD59uqPLckojRoxg06ZNbNy40dGlCPDPf/4TPz8/3nrrLVtbp06d8PT05L333nNgZc7HYrGwbNkyoqOjAXP0JzAwkKFDhzJs2DAAEhMT8fPzY8GCBXTt2rXQa9IIkORZYmIiAJUqVXJwJc6tX79+PPTQQ0RGRjq6FKe3YsUKmjRpQpcuXahWrRp33nknb775pqPLclotWrRg3bp1/PrrrwD8+OOPfPfdd7Rr187BlcmhQ4dISEiw+/+Wj48P4eHhbNmypUhq0M1QJU/S09MZPHgwERER3HHHHY4ux2l9+OGH7Ny5k+3btzu6FAF+//135syZw5AhQ3juuefYvn07AwcOxM3NjdjYWEeX53RGjBhBUlISdevWxdXVFavVygsvvED37t0dXZrTS0hIAMDPz8+u3c/Pz/ZaYVMAkjzp168fP/30E999952jS3Fax44dY9CgQaxZswYPDw9HlyOY/zBo0qQJL774IgB33nknP/30E3PnzlUAcoCPPvqI999/nw8++ID69esTHx/P4MGDCQwM1M9DdApMcq9///6sXLmS9evXExQU5OhynNaOHTs4ffo0d911F2XKlKFMmTJ88803zJgxgzJlymC1Wh1dotMJCAjg9ttvt2urV68eR48edVBFzu3ZZ59lxIgRdO3albCwMB5//HGeeeYZJk2a5OjSnJ6/vz8Ap06dsms/deqU7bXCpgAkOWYYBv3792fZsmV8/fXX1KhRw9ElObU2bdqwZ88e4uPjbVuTJk3o3r078fHxuLq6OrpEpxMREZFlaYhff/2VkJAQB1Xk3FJSUnBxsf+ac3V1JT093UEVSYYaNWrg7+/PunXrbG1JSUl8//33NG/evEhq0CkwybF+/frxwQcf8Omnn1KhQgXbeVofHx88PT0dXJ3zqVChQpb5V+XKlaNy5cqal+UgzzzzDC1atODFF1/k0UcfZdu2bcybN4958+Y5ujSn1L59e1544QWqV69O/fr12bVrF6+88gpPPPGEo0tzCsnJyRw8eND2/NChQ8THx1OpUiWqV6/O4MGDef7556lduzY1atRgzJgxBAYG2q4UK3SGSA4B2W7z5893dGny/1q1amUMGjTI0WU4tc8++8y44447DHd3d6Nu3brGvHnzHF2S00pKSjIGDRpkVK9e3fDw8DBq1qxpjBo1ykhNTXV0aU5h/fr12X5nxMbGGoZhGOnp6caYMWMMPz8/w93d3WjTpo2xf//+IqtP6wCJiIiI09EcIBEREXE6CkAiIiLidBSARERExOkoAImIiIjTUQASERERp6MAJCIiIk5HAUhEREScjgKQiMh1WCwWli9f7ugyRKQQKACJSLHUs2dPLBZLlq1t27aOLk1ESgHdC0xEiq22bdsyf/58uzZ3d3cHVSMipYlGgESk2HJ3d8ff399u8/X1BczTU3PmzKFdu3Z4enpSs2ZNPv74Y7v379mzh/vvvx9PT08qV65Mnz59SE5Otuvz9ttvU79+fdzd3QkICKB///52r585c4ZHHnkELy8vateuzYoVK2yv/fXXX3Tv3p2qVavi6elJ7dq1swQ2ESmeFIBEpMQaM2YMnTp14scff6R79+507dqVffv2AXDhwgWioqLw9fVl+/btLFmyhLVr19oFnDlz5tCvXz/69OnDnj17WLFiBbfeeqvdZ4wfP55HH32U3bt38+CDD9K9e3fOnTtn+/yff/6ZVatWsW/fPubMmUOVKlWK7i9ARPKuyG67KiKSC7GxsYarq6tRrlw5u+2FF14wDMMwAOOpp56ye094eLjRt29fwzAMY968eYavr6+RnJxse/3zzz83XFxcjISEBMMwDCMwMNAYNWrUdWsAjNGjR9ueJycnG4CxatUqwzAMo3379kavXr0K5oBFpEhpDpCIFFv33Xcfc+bMsWurVKmS7XHz5s3tXmvevDnx8fEA7Nu3j4YNG1KuXDnb6xEREaSnp7N//34sFgsnTpygTZs2N6yhQYMGtsflypXD29ub06dPA9C3b186derEzp07eeCBB4iOjqZFixZ5OlYRKVoKQCJSbJUrVy7LKamC4unpmaN+ZcuWtXtusVhIT08HoF27dhw5coQvvviCNWvW0KZNG/r168fUqVMLvF4RKViaAyQiJdbWrVuzPK9Xrx4A9erV48cff+TChQu21zdt2oSLiwt16tShQoUKhIaGsm7dunzVULVqVWJjY3nvvfeYPn068+bNy9f+RKRoaARIRIqt1NRUEhIS7NrKlCljm2i8ZMkSmjRpwj333MP777/Ptm3beOuttwDo3r0748aNIzY2lri4OP78808GDBjA448/jp+fHwBxcXE89dRTVKtWjXbt2vH333+zadMmBgwYkKP6xo4dS+PGjalfvz6pqamsXLnSFsBEpHhTABKRYmv16tUEBATYtdWpU4dffvkFMK/Q+vDDD3n66acJCAhg0aJF3H777QB4eXnx5ZdfMmjQIJo2bYqXlxedOnXilVdese0rNjaWS5cu8eqrrzJs2DCqVKlC586dc1yfm5sbI0eO5PDhw3h6etKyZUs+/PDDAjhyESlsFsMwDEcXISKSWxaLhWXLlhEdHe3oUkSkBNIcIBEREXE6CkAiIiLidDQHSERKJJ29F5H80AiQiIiIOB0FIBEREXE6CkAiIiLidBSARERExOkoAImIiIjTUQASERERp6MAJCIiIk5HAUhEREScjgKQiIiIOJ3/A/sy+m9lyh65AAAAAElFTkSuQmCC", 267 | "text/plain": [ 268 | "
" 269 | ] 270 | }, 271 | "metadata": {}, 272 | "output_type": "display_data" 273 | } 274 | ], 275 | "source": [ 276 | "loss = history.history[\"loss\"]\n", 277 | "validation_loss = history.history[\"val_loss\"]\n", 278 | "epochs = range(1, len(loss) + 1)\n", 279 | "plt.plot(epochs, loss, \"bo\", label=\"Training loss\")\n", 280 | "plt.plot(epochs, validation_loss, \"b\", label=\"Validation loss\")\n", 281 | "plt.title(\"Training and validation loss\")\n", 282 | "plt.xlabel(\"Epochs\")\n", 283 | "plt.ylabel(\"Loss\")\n", 284 | "plt.legend()\n", 285 | "\n", 286 | "plt.show()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "Plotting training and validation accuracy" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 12, 299 | "metadata": {}, 300 | "outputs": [ 301 | { 302 | "data": { 303 | "image/png": "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qV27NrKzszF37lxcvnwZq1at0nWOzhUZGYkjR45g6NChOHnyJM6dO4cFCxbg7t27ujrvv/8+bt68iSVLlrDzM1EFVpa7TRRHjx7A1avAzp3A2rXSv1eulH7yAzABkkVZy4LzM3PmTFSuXBktW7ZEly5dEBgYiGbNmpV6HGPGjEGfPn0QEhICf39/WFlZITAwsMAZiqOiotCsWTMEBgaiXbt2umTmeePHj8e///1vTJgwAfXq1UNwcLCu75GpqSl+++03VKtWDZ06dUKjRo3wxRdf6J6MHhgYiPHjx2P06NHw9fXFo0eP9J5blx9vb2/MnDkTX375JRo2bIg1a9YgOjpar84bb7yB3377DSdOnECLFi3g7++PH374QW9eJltbWwQFBcHKyqrA6QCIlK48j5wCyna3ieIyNgbatQP69JH+lesPfj4Kw4DSehRGWXkeSnmj1WpRr149vPfee5g6darc4cimffv2aNCgAebMmVPix+ajMKgiKO0JZ18HjQZwd5c6PBv6tlappGu6coXfH0DRHoXBmaBllJsFU8GuXbuG3377DW3btkVWVhbmzZuHK1eu4P3335c7NFk8ePAAu3btwq5du/DNN9/IHQ5RmZQ7curFpCF35FRZam0vSG63iZ49pWTn+espa90myhveAqMyz8jICMuXL4evry9atWqFU6dOYceOHahXr57cocmiadOm6N+/P7788kvUrVtX7nCIypyKMnIqV3npNlHesAWIyjw3Nzfs27dP7jDKjNIeiUdU3hRl5FR5aYXv0UN6Tha7TZQcJkBERFShVLSRU7nYbaJk8RYYERFVKBVx5BSVPCZARERUoZSnCWdJPkyAiIgoj/I8f055mnCW5MMEiIiI9MTESHPPvP028P770r/u7uXjoZu5OHKKXoadoImISKeizJ8DcOQUFYwtQFQk7dq1Q3h4uG7d3d0ds2bNKnAflUqFrVu3vvK5S+o4RGRYRZs/Byg7j12gsocJkEJ06dIFHTp0MLhtz549UKlUOHnyZJGPe+TIEQwaNOhVw9MzadIkNGnSJE/57du30bFjxxI9FxE9U5GePE70MkyAFOLDDz9EXFwcbhr47bZs2TI0b94cjRs3LvJxHRwcYGFhURIhvpSTkxPMzMxK5VxlydOnT+UOgRSios6fQ2QIEyCFePfdd+Hg4IDly5frlaenp2Pjxo34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VA7t2ZO35ed5QgA3bkj1iIgoLyZAROXQ7dslW4+ISGmYABGVQ87OJVuPiEhpmAARlUOtW0ujvVQqw9tVKsDNTapHRER5MQEiKoeMjaWh7kDeJCh3fdYszgdERJQfJkBE5VSPHsCmTYCrq3559epSOecBIiLKHydCJCrHevQAunblTNBEREXFBIionDM2Btq1kzsKIqLyhbfAiIiIqNSkpACLFwN798obB1uAiIiI6LW6dUt6PM+mTdIte60W6NMHeOst+WJiAkREREQl7to1YPNmadm/X39b8+aAv788ceViAkREREQl4uJFKeHZtAn480/9bf7+0oOae/QA3N1lCU8PEyAiIip3hADS0oD796Xl3r1nrw0tWVlAixbA229Lgwbs7eW+gorjzJlnLT0nTjwrNzKSRqX27Al07553yg65MQEiKuc0GuD334GrVwEzs2eLWl24dVPT/GeUJnrdtFogNbVwSczz2x88kD77RfHnn8A330ivGzeWkqG33wbatgXs7Er80iosIYCTJ6VWns2bgbNnn20zNgbeeQcICgK6dQMcHWUL86VUQgghdxBlTVpaGmxtbZGamgobGxu5wyEyKCUFWLoUWLRISn5exfOJUVGSp6LWLcy6EcemlksaDfDwYdGSmNxE5lW+hSwsgCpV8l+qVpX+1Wqlzre//w6cPq1/DCMjoGnTZwlR69aAtfUrvR0VjhBSApl7e+vSpWfbKlUC/vlPKen517+k91wuRfn+lj0Bmj9/PqZPn46kpCR4e3tj7ty5aNGihcG62dnZiI6OxooVK5CYmIi6deviyy+/RIcOHXR1NBoNJk2ahNWrVyMpKQkuLi7o378/oqKioCrkn7lMgKisEgLYtw9YsED6JfT0qVRuZyeNpsjOBjIzpeb+3MXQena2rJdRoEqVXl9yVdR1k3LYRi6E9GWfkyMtGk3Br1+2PTNTSlIKSmLu35eSn1dhZfXyJObFpXJlwNy86OdKSQF275aSoZ07gfPn9bcbGwO+vlIy9M47QMuWUqKlNFotcODAs9tb168/26ZWAx06SLe33n0XsLWVL87nlZsEaMOGDQgJCcHChQvh5+eHWbNmYePGjTh//jyqVauWp/6YMWOwevVqLFmyBF5eXti+fTsiIiKwf/9+NG3aFADw+eefY+bMmVixYgUaNGiAP//8E2FhYZg2bRo++eST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", 304 | "text/plain": [ 305 | "
" 306 | ] 307 | }, 308 | "metadata": {}, 309 | "output_type": "display_data" 310 | } 311 | ], 312 | "source": [ 313 | "acc = history.history[\"accuracy\"]\n", 314 | "validation_acc = history.history[\"val_accuracy\"]\n", 315 | "\n", 316 | "plt.plot(epochs, acc, \"bo\", label=\"Training accuracy\")\n", 317 | "plt.plot(epochs, validation_acc, \"b\", label=\"Validation accuracy\")\n", 318 | "plt.title(\"Training and validation accuracy\")\n", 319 | "plt.xlabel(\"Epochs\")\n", 320 | "plt.ylabel(\"Loss\")\n", 321 | "plt.legend()\n", 322 | "\n", 323 | "plt.show()" 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "metadata": {}, 329 | "source": [ 330 | "Overfit begins after 5 epochs" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "metadata": {}, 336 | "source": [ 337 | "Retraining the model from scratch for 5 epochs to avoid overfit. Note that validation set is not used for this time" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": 13, 343 | "metadata": {}, 344 | "outputs": [ 345 | { 346 | "name": "stdout", 347 | "output_type": "stream", 348 | "text": [ 349 | "Epoch 1/5\n", 350 | "\u001b[1m 1/469\u001b[0m \u001b[37m━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[1m1:26\u001b[0m 186ms/step - accuracy: 0.0781 - loss: 2.4094" 351 | ] 352 | }, 353 | { 354 | "name": "stderr", 355 | "output_type": "stream", 356 | "text": [ 357 | "2024-03-31 21:19:29.842693: W external/local_tsl/tsl/framework/cpu_allocator_impl.cc:83] Allocation of 188160000 exceeds 10% of free system memory.\n" 358 | ] 359 | }, 360 | { 361 | "name": "stdout", 362 | "output_type": "stream", 363 | "text": [ 364 | "\u001b[1m469/469\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.8703 - loss: 0.4504\n", 365 | "Epoch 2/5\n", 366 | "\u001b[1m469/469\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9659 - loss: 0.1192\n", 367 | "Epoch 3/5\n", 368 | "\u001b[1m469/469\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9788 - loss: 0.0717\n", 369 | "Epoch 4/5\n", 370 | "\u001b[1m469/469\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9858 - loss: 0.0498\n", 371 | "Epoch 5/5\n", 372 | "\u001b[1m469/469\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 2ms/step - accuracy: 0.9893 - loss: 0.0373\n" 373 | ] 374 | } 375 | ], 376 | "source": [ 377 | "model = models.Sequential()\n", 378 | "model.add(layers.Dense(512, activation=\"relu\", input_shape=(28 * 28,)))\n", 379 | "model.add(layers.Dense(10, activation=\"softmax\"))\n", 380 | "model.compile(optimizer=\"rmsprop\", loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\n", 381 | "history = model.fit(train_images_prepared, train_labels_one_hot, epochs=5, batch_size=128)" 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "Save MNIST model for production system" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 14, 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [ 397 | "model.save(\"app/mnist_model.keras\")\n", 398 | "del model\n", 399 | "model = load_model(\"app/mnist_model.keras\")" 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "metadata": {}, 405 | "source": [ 406 | "Model evaluation" 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 15, 412 | "metadata": {}, 413 | "outputs": [ 414 | { 415 | "name": "stdout", 416 | "output_type": "stream", 417 | "text": [ 418 | "\u001b[1m313/313\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 699us/step - accuracy: 0.9745 - loss: 0.0776\n" 419 | ] 420 | } 421 | ], 422 | "source": [ 423 | "final_loss, final_acc = model.evaluate(test_images_prepared, test_labels_one_hot)" 424 | ] 425 | }, 426 | { 427 | "cell_type": "markdown", 428 | "metadata": {}, 429 | "source": [ 430 | "Final results" 431 | ] 432 | }, 433 | { 434 | "cell_type": "code", 435 | "execution_count": 16, 436 | "metadata": {}, 437 | "outputs": [ 438 | { 439 | "name": "stdout", 440 | "output_type": "stream", 441 | "text": [ 442 | "Final loss: 0.06699982285499573\n", 443 | "Final accuracy: 0.9779999852180481\n" 444 | ] 445 | } 446 | ], 447 | "source": [ 448 | "print(f\"Final loss: {final_loss}\")\n", 449 | "print(f\"Final accuracy: {final_acc}\")" 450 | ] 451 | }, 452 | { 453 | "cell_type": "markdown", 454 | "metadata": {}, 455 | "source": [ 456 | "Generating predictions for two samples of numbers 4 and 9 from test images" 457 | ] 458 | }, 459 | { 460 | "cell_type": "markdown", 461 | "metadata": {}, 462 | "source": [ 463 | "First sample is a number 4 from test images" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": 17, 469 | "metadata": {}, 470 | "outputs": [ 471 | { 472 | "data": { 473 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaAAAAGdCAYAAABU0qcqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAa/UlEQVR4nO3df2zU9R3H8dfx60Rpr6ulvXYUVkBFBeqG0DUoQ2go3UJE2OKvJeAMRixmwJymRgH3I90wcUbTYZZMmFFEMQJKHE6LLXG2LCAE2I+GNlVKoEUh3JUihdHP/iDcPGiF73HXd3t9PpJvYu++n37ffr306Zf7cvU555wAAOhm/awHAAD0TQQIAGCCAAEATBAgAIAJAgQAMEGAAAAmCBAAwAQBAgCYGGA9wIU6Ojp06NAhpaSkyOfzWY8DAPDIOafW1lbl5OSoX7+ur3N6XIAOHTqk3Nxc6zEAAFeoqalJw4YN6/L5HheglJQUSecGT01NNZ4GAOBVOBxWbm5u5Od5VxIWoIqKCj377LNqbm5Wfn6+XnzxRU2aNOmS687/sVtqaioBAoBe7FJvoyTkJoQ33nhDS5cu1fLly/Xpp58qPz9fxcXFOnLkSCIOBwDohRISoOeee04LFizQAw88oJtuukkvvfSSrr76ar388suJOBwAoBeKe4BOnz6tnTt3qqio6P8H6ddPRUVFqqmpuWj/9vZ2hcPhqA0AkPziHqAvv/xSZ8+eVVZWVtTjWVlZam5uvmj/8vJyBQKByMYdcADQN5j/RdSysjKFQqHI1tTUZD0SAKAbxP0uuIyMDPXv318tLS1Rj7e0tCgYDF60v9/vl9/vj/cYAIAeLu5XQIMGDdKECRNUWVkZeayjo0OVlZUqLCyM9+EAAL1UQv4e0NKlSzVv3jzdeuutmjRpkp5//nm1tbXpgQceSMThAAC9UEICdPfdd+uLL77QsmXL1NzcrFtuuUVbtmy56MYEAEDf5XPOOeshvi4cDisQCCgUCvFJCADQC13uz3Hzu+AAAH0TAQIAmCBAAAATBAgAYIIAAQBMECAAgAkCBAAwQYAAACYIEADABAECAJggQAAAEwQIAGCCAAEATBAgAIAJAgQAMEGAAAAmCBAAwAQBAgCYIEAAABMECABgggABAEwQIACACQIEADBBgAAAJggQAMAEAQIAmCBAAAATBAgAYIIAAQBMECAAgAkCBAAwQYAAACYIEADABAECAJggQAAAEwQIAGCCAAEATBAgAIAJAgQAMEGAAAAmCBAAwAQBAgCYIEAAABMECABgggABAEwQIACACQIEADBBgAAAJggQAMAEAQIAmCBAAAATBAgAYGKA9QDApXz66aee18yZMyemY3322WcxrUNs/va3v3lec+ONN3pek5ub63kNEo8rIACACQIEADAR9wCtWLFCPp8vahszZky8DwMA6OUS8h7QzTffrA8//PD/BxnAW00AgGgJKcOAAQMUDAYT8a0BAEkiIe8B7d+/Xzk5ORo5cqTuv/9+HThwoMt929vbFQ6HozYAQPKLe4AKCgq0Zs0abdmyRatWrVJjY6Nuv/12tba2drp/eXm5AoFAZON2SQDoG+IeoJKSEv3kJz/R+PHjVVxcrPfee0/Hjx/Xm2++2en+ZWVlCoVCka2pqSneIwEAeqCE3x2Qlpam66+/XvX19Z0+7/f75ff7Ez0GAKCHSfjfAzpx4oQaGhqUnZ2d6EMBAHqRuAfoscceU3V1tT777DN98sknuuuuu9S/f3/de++98T4UAKAXi/sfwR08eFD33nuvjh49qqFDh+q2225TbW2thg4dGu9DAQB6sbgHaN26dfH+lujj3n//fc9r2tvbEzAJ4u2dd97xvObll1/2vIafSz0TnwUHADBBgAAAJggQAMAEAQIAmCBAAAATBAgAYIIAAQBMECAAgAkCBAAwQYAAACYIEADABAECAJhI+C+kA77uv//9r+c17733XgImQU9w6623el7z3HPPeV7T1tbmeY0kXXPNNTGtw+XhCggAYIIAAQBMECAAgAkCBAAwQYAAACYIEADABAECAJggQAAAEwQIAGCCAAEATBAgAIAJAgQAMEGAAAAm+DRsdKuPPvrI85pPPvnE85onnnjC8xp0v2PHjnle889//tPzmpMnT3peI/Fp2InGFRAAwAQBAgCYIEAAABMECABgggABAEwQIACACQIEADBBgAAAJggQAMAEAQIAmCBAAAATBAgAYIIPI0XM9u7d63nNPffc43nN6NGjPa958sknPa9B93vnnXesR4AhroAAACYIEADABAECAJggQAAAEwQIAGCCAAEATBAgAIAJAgQAMEGAAAAmCBAAwAQBAgCYIEAAABN8GCli9tvf/tbzmpMnT3pe8+qrr3peM2TIEM9r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", 474 | "text/plain": [ 475 | "
" 476 | ] 477 | }, 478 | "metadata": {}, 479 | "output_type": "display_data" 480 | } 481 | ], 482 | "source": [ 483 | "digit = test_images[4]\n", 484 | "\n", 485 | "plt.imshow(digit, cmap=plt.cm.binary)\n", 486 | "plt.show()" 487 | ] 488 | }, 489 | { 490 | "cell_type": "code", 491 | "execution_count": 18, 492 | "metadata": {}, 493 | "outputs": [ 494 | { 495 | "data": { 496 | "text/plain": [ 497 | "4" 498 | ] 499 | }, 500 | "execution_count": 18, 501 | "metadata": {}, 502 | "output_type": "execute_result" 503 | } 504 | ], 505 | "source": [ 506 | "test_labels[4]" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": 19, 512 | "metadata": {}, 513 | "outputs": [ 514 | { 515 | "name": "stdout", 516 | "output_type": "stream", 517 | "text": [ 518 | "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 20ms/step\n", 519 | "[[5.1081292e-07 3.1363423e-09 2.1023111e-06 7.7408027e-09 9.9883264e-01\n", 520 | " 4.4669033e-08 1.4959137e-05 2.1703441e-05 2.2524851e-07 1.1278475e-03]]\n" 521 | ] 522 | } 523 | ], 524 | "source": [ 525 | "prediction = model.predict(test_images_prepared[4:5], batch_size=784)\n", 526 | "print(prediction)" 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "The highest likelihood is above 0.99 for number 4" 534 | ] 535 | }, 536 | { 537 | "cell_type": "markdown", 538 | "metadata": {}, 539 | "source": [ 540 | "Generate predictions for example image" 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": 20, 546 | "metadata": {}, 547 | "outputs": [], 548 | "source": [ 549 | "APP_ROOT = os.path.dirname(os.path.abspath(\"Deep Learning MNIST prediction model with Keras.ipynb\"))\n", 550 | "APP_STATIC = os.path.join(APP_ROOT, \"app/app/static\")\n", 551 | "\n", 552 | "filename = \"4.jpg\"\n", 553 | "path_to_file = os.path.join(APP_STATIC, filename)\n", 554 | "image = io.imread(path_to_file, as_gray=True) # read as grayscale" 555 | ] 556 | }, 557 | { 558 | "cell_type": "code", 559 | "execution_count": 21, 560 | "metadata": {}, 561 | "outputs": [], 562 | "source": [ 563 | "def preprocess_image(image):\n", 564 | " # invert grayscale image\n", 565 | " image = util.invert(image)\n", 566 | " # resize image and prepare it for model\n", 567 | " image = transform.resize(image, (28, 28), anti_aliasing=True, mode=\"constant\")\n", 568 | " image = np.array(image)\n", 569 | " plt.imshow(image, cmap=plt.cm.binary)\n", 570 | " plt.show()\n", 571 | " image = image.reshape((1, 28 * 28))\n", 572 | "\n", 573 | " return image" 574 | ] 575 | }, 576 | { 577 | "cell_type": "code", 578 | "execution_count": 22, 579 | "metadata": {}, 580 | "outputs": [ 581 | { 582 | "data": { 583 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaAAAAGdCAYAAABU0qcqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAcbklEQVR4nO3df2xV9f3H8ddtoReQ9mKt7W1HwYIKm0CXMagdyhelAbrMiJIFf/wBxEBkxQw7p+miom5JN0yc0TD8Z4O5iD9IBKIxJFpoia6wUEVC1IY2HT9GWwauvVCgFPr5/kG8eqWgn8u9fbeX5yM5Cb33vHrfHo59cXpPPw0455wAAOhnadYDAACuThQQAMAEBQQAMEEBAQBMUEAAABMUEADABAUEADBBAQEATAyxHuDbent7deTIEWVmZioQCFiPAwDw5JzTiRMnVFBQoLS0S1/nDLgCOnLkiAoLC63HAABcoUOHDmn06NGXfH7AFVBmZqakC4NnZWUZTwMA8BWJRFRYWBj9en4pSSugNWvW6Pnnn1dbW5uKi4v18ssva/r06d+Z++rbbllZWRQQAAxi3/U2SlJuQnjzzTdVWVmpVatW6eOPP1ZxcbHmzp2ro0ePJuPlAACDUFIK6IUXXtDSpUu1ZMkS/ehHP9Irr7yiESNG6G9/+1syXg4AMAglvIDOnj2rhoYGlZWVff0iaWkqKytTfX39Rft3d3crEonEbACA1JfwAjp27JjOnz+vvLy8mMfz8vLU1tZ20f7V1dUKhULRjTvgAODqYP6DqFVVVers7Ixuhw4dsh4JANAPEn4XXE5OjtLT09Xe3h7zeHt7u8Lh8EX7B4NBBYPBRI8BABjgEn4FlJGRoalTp6qmpib6WG9vr2pqalRaWprolwMADFJJ+TmgyspKLVq0SD/96U81ffp0vfjii+rq6tKSJUuS8XIAgEEoKQW0cOFC/fe//9XTTz+ttrY2/fjHP9bWrVsvujEBAHD1CjjnnPUQ3xSJRBQKhdTZ2clKCAAwCH3fr+Pmd8EBAK5OFBAAwAQFBAAwQQEBAExQQAAAExQQAMAEBQQAMEEBAQBMUEAAABMUEADABAUEADBBAQEATCRlNWxcHc6fP++dOXPmjHdmxIgR3plAIOCdwZXpr3WN+btNHVwBAQBMUEAAABMUEADABAUEADBBAQEATFBAAAATFBAAwAQFBAAwQQEBAExQQAAAExQQAMAEBQQAMEEBAQBMsBo24l7FeMeOHd6ZjRs3emeqqqq8M4WFhd4ZfO3s2bPemW3btnlnRo4c6Z2ZMWOGd4YVtAcmroAAACYoIACACQoIAGCCAgIAmKCAAAAmKCAAgAkKCABgggICAJiggAAAJiggAIAJCggAYIICAgCYYDFS6NixY3Hl1q5d653Zt2+fd+axxx7zzuDKNDU1eWeeeOIJ78zChQu9M6Wlpd6Z9PR07wySjysgAIAJCggAYIICAgCYoIAAACYoIACACQoIAGCCAgIAmKCAAAAmKCAAgAkKCABgggICAJiggAAAJliMNMU457wzH330UVyvtX37du/MkiVLvDPhcNg7gwt6e3vjyn344YfemQMHDnhnrr/+eu9MIBDwzmBg4goIAGCCAgIAmEh4AT3zzDMKBAIx28SJExP9MgCAQS4p7wHdcsst+uCDD75+kSG81QQAiJWUZhgyZAhvHAMALisp7wHt379fBQUFGjdunB588EEdPHjwkvt2d3crEonEbACA1JfwAiopKdH69eu1detWrV27Vi0tLbr99tt14sSJPvevrq5WKBSKboWFhYkeCQAwACW8gMrLy/XLX/5SU6ZM0dy5c/Xee++po6NDb731Vp/7V1VVqbOzM7odOnQo0SMBAAagpN8dMGrUKN18881qamrq8/lgMKhgMJjsMQAAA0zSfw7o5MmTam5uVn5+frJfCgAwiCS8gB577DHV1dXp3//+t/75z3/qnnvuUXp6uu6///5EvxQAYBBL+LfgDh8+rPvvv1/Hjx/X9ddfr9tuu007d+6Ma80nAEDqSngBvfHGG4n+lPDQ3d3tnfnmDw37GDlypHcmnivhESNGeGdwwblz5+LKff75596Z9PR078xNN93knUlLYwWxVMHfJADABAUEADBBAQEATFBAAAATFBAAwAQFBAAwQQEBAExQQAAAExQQAMAEBQQAMEEBAQBMUEAAABNJ/4V06F8dHR3emYaGhrhea9q0ad6Z8ePHx/VaiE88i9NK0oEDB7wzQ4cO9c5kZWV5Z5A6uAICAJiggAAAJiggAIAJCggAYIICAgCYoIAAACYoIACACQoIAGCCAgIAmKCAAAAmKCAAgAkKCABgggICAJhgNewBzDnnnfn000+9M4cOHfLOSNKSJUu8MyNHjozrtRCfY8eO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XHvttd4Z6esfrQCSibXgAAAmKCAAgAkKCABgggICAJiggAAAJiggAIAJCggAYIICAgCYoIAAACYoIACACQoIAGCCAgIAmKCAAAAm4vqNqACuzLlz57wzX375pXcmMzPTOyOJ31KMfsEVEADABAUEADBBAQEATFBAAAATFBAAwAQFBAAwQQEBAExQQAAAExQQAMAEBQQAMEEBAQBMUEAAABMsRgoYOH36tHemo6PDOxMOh70zEouRon9wBQQAMEEBAQBMUEAAABMUEADABAUEADBBAQEATFBAAAATFBAAwAQFBAAwQQEBAExQQAAAExQQAMAEi5ECBk6ePOmd+d///uedmTZtmndGkjIyMuLKAT64AgIAmKCAAAAmKCAAgAkKCABgggICAJiggAAAJiggAIAJCggAYIICAgCYoIAAACYoIACACQoIAGCCxUgBA0OG+P+vFw6HvTM/+9nPvDNSfPMBvrgCAgCYoIAAACa8Cqi6ulrTpk1TZmamcnNzNX/+fDU2NsbsM2vWLAUCgZjt4YcfTujQAIDBz6uA6urqVFFRoZ07d+r9999XT0+P5syZo66urpj9li5dqtbW1ui2evXqhA4NABj8vN5p3Lp1a8zH69evV25urhoaGjRz5szo4yNGjIjrDVMAwNXjit4D6uzslCRlZ2fHPP7aa68pJydHkyZNUlVVlU6dOnXJz9Hd3a1IJBKzAQBSX9z3Wvb29mrlypWaMWOGJk2aFH38gQce0NixY1VQUKC9e/fqiSeeUGNjo95+++0+P091dbWeffbZeMcAAAxScRdQRUWF9u3bpw8//DDm8WXLlkX/PHnyZOXn52v27Nlqbm7W+PHjL/o8VVVVqqysjH4ciURUWFgY71gAgEEirgJasWKF3n33Xe3YsUOjR4++7L4lJSWSpKampj4LKBgMKhgMxjMGAGAQ8yog55weeeQRbdq0SbW1tSoqKvrOzJ49eyRJ+fn5cQ0IAEhNXgVUUVGhDRs2aMuWLcrMzFRbW5skKRQKafjw4WpubtaGDRv085//XNddd5327t2rRx99VDNnztSUKVOS8h8AABicvApo7dq1ki78sOk3rVu3TosXL1ZGRoY++OADvfjii+rq6lJhYaEWLFigJ598MmEDAwBSg/e34C6nsLBQdXV1VzQQAODqwJK3gIGCggLvzMsvv+ydueGGG7wzkpSW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", 584 | "text/plain": [ 585 | "
" 586 | ] 587 | }, 588 | "metadata": {}, 589 | "output_type": "display_data" 590 | } 591 | ], 592 | "source": [ 593 | "preprocessed_image = preprocess_image(image)" 594 | ] 595 | }, 596 | { 597 | "cell_type": "code", 598 | "execution_count": 23, 599 | "metadata": {}, 600 | "outputs": [ 601 | { 602 | "name": "stdout", 603 | "output_type": "stream", 604 | "text": [ 605 | "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 8ms/step\n" 606 | ] 607 | }, 608 | { 609 | "data": { 610 | "text/plain": [ 611 | "array([[8.2700979e-08, 1.6669065e-04, 4.8218979e-05, 2.3290573e-05,\n", 612 | " 9.9914443e-01, 1.4074722e-06, 2.4940262e-05, 4.9085240e-04,\n", 613 | " 4.4384862e-05, 5.5692170e-05]], dtype=float32)" 614 | ] 615 | }, 616 | "execution_count": 23, 617 | "metadata": {}, 618 | "output_type": "execute_result" 619 | } 620 | ], 621 | "source": [ 622 | "preds = model.predict(preprocessed_image)\n", 623 | "preds" 624 | ] 625 | }, 626 | { 627 | "cell_type": "markdown", 628 | "metadata": {}, 629 | "source": [ 630 | "The highest likelihood is above 0.99 for number 4" 631 | ] 632 | }, 633 | { 634 | "cell_type": "code", 635 | "execution_count": 24, 636 | "metadata": {}, 637 | "outputs": [ 638 | { 639 | "data": { 640 | "text/plain": [ 641 | "4" 642 | ] 643 | }, 644 | "execution_count": 24, 645 | "metadata": {}, 646 | "output_type": "execute_result" 647 | } 648 | ], 649 | "source": [ 650 | "np.argmax(preds)" 651 | ] 652 | } 653 | ], 654 | "metadata": { 655 | "kernelspec": { 656 | "display_name": "Python 3 (ipykernel)", 657 | "language": "python", 658 | "name": "python3" 659 | }, 660 | "language_info": { 661 | "codemirror_mode": { 662 | "name": "ipython", 663 | "version": 3 664 | }, 665 | "file_extension": ".py", 666 | "mimetype": "text/x-python", 667 | "name": "python", 668 | "nbconvert_exporter": "python", 669 | "pygments_lexer": "ipython3", 670 | "version": "3.10.12" 671 | } 672 | }, 673 | "nbformat": 4, 674 | "nbformat_minor": 4 675 | } 676 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bookworm 2 | ENV PYTHONUNBUFFERED=1 3 | RUN apt-get update \ 4 | && apt-get install -y make \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | RUN mkdir -p /app 8 | WORKDIR /app 9 | COPY requirements.txt /app 10 | RUN python3 -m venv . 11 | RUN python3 -m pip install pip==25.1.1 12 | RUN python3 -m pip install setuptools==80.7.1 13 | RUN python3 -m pip install --no-cache-dir -r requirements.txt 14 | COPY ./app /app 15 | EXPOSE 5000 16 | CMD ["gunicorn", "--bind", ":5000", "server:app"] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEEP LEARNING ON FLASK 2 | 3 | This repository stores a test to demonstrate skills mainly with [Python], [Keras], [Flask], [Docker], [Jupyter Notebook], [microservices], [REST API] and [GitHub Actions]. 4 | 5 | - [PURPOSE](#purpose) 6 | - [DEPENDENCIES](#dependencies) 7 | - [PYTHON VIRTUAL ENVIRONMENT](#python-virtual-environment) 8 | - [REPOSITORY CONTENT](#repository-content) 9 | - [ARCHITECTURE](#architecture) 10 | - [DEEP LEARNING MODEL](#deep-learning-model) 11 | - [HOW TO RUN DEEP LEARNING ON FLASK WITH DOCKER COMPOSE](#how-to-run-deep-learning-on-flask-with-docker-compose) 12 | - [TEST SERVER \& REST API](#test-server--rest-api) 13 | - [CREDITS](#credits) 14 | 15 | ## PURPOSE 16 | 17 | The goal is to deploy on [Flask] a [Deep Learning] model as a microservice. The model is used to predict handwritten digits and it has been previously trained on a [Jupyter Notebook]. [REST API] are utilized to communicate with the deployed model. e.g. send image to be analized and return the generated predictions to the client. [GitHub Actions] are employed to implement CI/CD workflows in the project. 18 | 19 | ## DEPENDENCIES 20 | 21 | The code has been tested using: 22 | 23 | - [Python] (3.12): an interpreted high-level programming language for general-purpose programming. 24 | - [Jupyter Lab] (4.4): a web-based interactive development environment for [Jupyter Notebooks], code and data. 25 | - [Flask] (3.1): a microframework for [Python] based on Werkzeug, Jinja 2 and good intentions. 26 | - [Gunicorn] (23.0): a [Python] [WSGI] HTTP Server for UNIX. 27 | - [NGINX] (1.27): a free, open-source, high-performance HTTP server, reverse proxy, and IMAP/POP3 proxy server. 28 | - [Docker] (28.1): an open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud. 29 | - [Docker Compose] (2.35): a tool for defining and running multi-container [Docker] applications. 30 | - [Keras] ([TensorFlow] built-in): a high-level neural networks API, written in [Python] and capable of running on top of [TensorFlow]. 31 | - [TensorFlow] (2.19): an open source software [Deep Learning] library for high performance numerical computation using data flow graphs. 32 | - [Matplotlib] (3.10): a plotting library for [Python] and its numerical mathematics extension [NumPy]. 33 | - [NumPy] (2.1): a library for [Python], adding support for large, multi-dimensional arrays and matrices, along with a large collection of high-level mathematical functions to operate on these arrays. 34 | - [Ruff] (0.11): An extremely fast Python linter and code formatter, written in Rust. 35 | - [scikit-image] (0.25): a collection of algorithms for image processing with [Python]. 36 | 37 | ### PYTHON VIRTUAL ENVIRONMENT 38 | 39 | Virtual environment (=**.venv**) can be generated from **requirements_dev.txt** file located in the repository. 40 | 41 | Command to configure virtual environment with [venv]: 42 | 43 | ```bash 44 | ~/deeplearning_flask$ python3 -m venv .venv 45 | ~/deeplearning_flask$ source .venv/bin/activate 46 | (.venv)~/deeplearning_flask$ python3 -m pip install pip==25.1.1 47 | (.venv)~/deeplearning_flask$ python3 -m pip install setuptools==80.7.1 48 | (.venv)~/deeplearning_flask$ python3 -m pip install -r requirements_dev.txt 49 | (.venv)~/deeplearning_flask$ pre-commit install 50 | ``` 51 | 52 | ## REPOSITORY CONTENT 53 | 54 | The repository main folder contains: 55 | 56 | ```bash 57 | deeplearning_flask 58 | ├── .env.example 59 | ├── .env.test 60 | ├── .github 61 | │ └── workflows 62 | │ └── ci_tests.yml 63 | ├── .gitignore 64 | ├── .pre-commit-config.yaml 65 | ├── app 66 | │ ├── app 67 | │ │ ├── __init__.py 68 | │ │ ├── api.py 69 | │ │ ├── model.py 70 | │ │ ├── static 71 | │ │ │ └── 4.jpg 72 | │ │ └── templates 73 | │ │ └── dlflask.html 74 | │ ├── config.py 75 | │ ├── Makefile 76 | │ ├── mnist_model.keras 77 | │ ├── server.py 78 | │ └── tests 79 | │ ├── __init__.py 80 | │ ├── conftest.py 81 | │ └── test_app.py 82 | ├── Deep Learning MNIST prediction model with Keras.ipynb 83 | ├── docker-compose.yml 84 | ├── Dockerfile 85 | ├── nginx 86 | │ └── conf.d 87 | │ └── local.conf 88 | ├── pyproject.toml 89 | ├── README.md 90 | ├── requirements.txt 91 | └── requirements_dev.txt 92 | ``` 93 | 94 | ## ARCHITECTURE 95 | 96 | The architecture created with [Docker Compose] uses two different [Docker] containers for: 97 | 98 | - [NGINX]. 99 | - [Flask] and [Gunicorn]. 100 | 101 | The following diagram illustrates the architecture in blocks: 102 | 103 | ```mermaid 104 | flowchart LR; 105 | Client<-->NGINX; 106 | NGINX<--brigde-->Gunicorn; 107 | subgraph web; 108 | Gunicorn<-->Flask; 109 | end; 110 | ``` 111 | 112 | ## DEEP LEARNING MODEL 113 | 114 | The definition and training of the [Deep Learning] MNIST model was done through a notebook in [Jupyter Lab]. The employed notebook is stored in the main folder, to run it use the command shown below: 115 | 116 | ```bash 117 | (.venv)~/deeplearning_flask$ jupyter lab Deep\ Learning\ MNIST\ prediction\ model\ with\ Keras.ipynb 118 | ``` 119 | 120 | ## HOW TO RUN DEEP LEARNING ON FLASK WITH DOCKER COMPOSE 121 | 122 | The steps and commands to run the [Deep Learning] model on the [Flask] server with [Docker Compose] are described below. 123 | 124 | Before executing [Docker Compose] is strongly recommended to close other applications to free up resources and ports to avoid potential issues. Then [Docker Compose] can be executed to build services. 125 | 126 | ```bash 127 | ~/deeplearning_flask$ docker compose build 128 | ``` 129 | 130 | Next step consists in executing [Docker Compose] up command. 131 | 132 | ```bash 133 | ~/deeplearning_flask$ docker compose up 134 | ``` 135 | 136 | If everything goes fine at the end it should appear something similar to: 137 | 138 | ```bash 139 | ... 140 | ... 141 | web_1 | 2020-06-04 19:30:17.818273: I tensorflow/compiler/xla/service/service.cc:176] StreamExecutor device (0): Host, Default Version 142 | ``` 143 | 144 | ## TEST SERVER & REST API 145 | 146 | There are different ways to check that the server is running properly. One is opening a web browser such as Chrome or Firefox and paste the following URL: 147 | 148 | ```bash 149 | http://127.0.0.1/ 150 | ``` 151 | 152 | The web browser should show the text "Deep Learning on Flask". 153 | 154 | [REST API] can be tested with [pytest] or [curl]. 155 | 156 | It is possible to execute tests of [Flask] microservice created with [pytest] from inside the [Flask] [Docker] container using [Makefile]: 157 | 158 | ```bash 159 | ~/deeplearning_flask$ docker exec -it deeplearning_flask-web-1 /bin/bash 160 | ~/app# make test 161 | ... 162 | ============================= test session starts ============================== 163 | platform linux -- Python 3.12.9, pytest-8.3.5, pluggy-1.5.0 164 | rootdir: /app/tests 165 | collected 2 items 166 | 167 | test_app.py .. [100%] 168 | ``` 169 | 170 | Those tests are also automatically executed with CI/CD workflows implemented with [GitHub Actions] for every push and pull request in the project repository. 171 | 172 | A POST example using [curl] from outside [Docker] container is shown below: 173 | 174 | ```bash 175 | ~/deeplearning_flask$ curl -F file=@app/app/static/4.jpg -X POST 'http://127.0.0.1/api/predictlabel' | json_pp 176 | % Total % Received % Xferd Average Speed Time Time Time Current 177 | Dload Upload Total Spent Left Speed 178 | 100 11650 100 489 100 11161 321 7347 0:00:01 0:00:01 --:--:-- 7664 179 | { 180 | "most_probable_label" : "4", 181 | "predictions" : [ 182 | { 183 | "label" : "0", 184 | "probability" : "8.270098e-08" 185 | }, 186 | { 187 | "label" : "1", 188 | "probability" : "0.00016669065" 189 | }, 190 | { 191 | "label" : "2", 192 | "probability" : "4.821898e-05" 193 | }, 194 | { 195 | "label" : "3", 196 | "probability" : "2.3290573e-05" 197 | }, 198 | { 199 | "label" : "4", 200 | "probability" : "0.99914443" 201 | }, 202 | { 203 | "label" : "5", 204 | "probability" : "1.4074722e-06" 205 | }, 206 | { 207 | "label" : "6", 208 | "probability" : "2.4940262e-05" 209 | }, 210 | { 211 | "label" : "7", 212 | "probability" : "0.0004908524" 213 | }, 214 | { 215 | "label" : "8", 216 | "probability" : "4.4384862e-05" 217 | }, 218 | { 219 | "label" : "9", 220 | "probability" : "5.569217e-05" 221 | } 222 | ], 223 | "success" : true 224 | } 225 | ``` 226 | 227 | ## CREDITS 228 | 229 | author: alvertogit 230 | copyright: 2018-2025 231 | 232 | [Python]: https://www.python.org/ 233 | [Flask]: https://flask.palletsprojects.com/en/1.1.x/ 234 | [Gunicorn]: https://gunicorn.org/ 235 | [WSGI]: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface 236 | [NGINX]: https://www.nginx.com/ 237 | [Docker]: https://www.docker.com/ 238 | [microservices]: https://en.wikipedia.org/wiki/Microservices 239 | [REST API]: https://en.wikipedia.org/wiki/Representational_state_transfer 240 | [GitHub Actions]: https://github.com/features/actions 241 | [Docker Compose]: https://github.com/docker/compose 242 | [venv]: https://docs.python.org/3/library/venv.html 243 | [Jupyter Lab]: https://jupyter.org/ 244 | [Jupyter Notebook]: https://jupyter.org/ 245 | [Jupyter Notebooks]: https://jupyter.org/ 246 | [Deep Learning]: https://en.wikipedia.org/wiki/Deep_learning 247 | [Keras]: https://keras.io/ 248 | [TensorFlow]: https://www.tensorflow.org/ 249 | [Matplotlib]: https://matplotlib.org/ 250 | [NumPy]: https://numpy.org/ 251 | [scikit-image]: https://scikit-image.org/ 252 | [curl]: https://curl.haxx.se/ 253 | [pytest]: https://docs.pytest.org/en/latest/ 254 | [Makefile]: https://en.wikipedia.org/wiki/Makefile 255 | [Ruff]: https://docs.astral.sh/ruff/ 256 | -------------------------------------------------------------------------------- /app/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | default: test 4 | 5 | test: 6 | cd tests \ 7 | && pytest 8 | -------------------------------------------------------------------------------- /app/app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | __init__.py: Flask server with Deep Learning model. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | from config import config 10 | from flask import Flask, render_template 11 | 12 | from .api import api 13 | from .model import init_model 14 | 15 | 16 | def create_app(config_name: str = "default") -> Flask: 17 | """Create and configure an instance of the Flask application.""" 18 | app = Flask(__name__) 19 | app.config.from_object(config[config_name]) 20 | config[config_name].init_app(app) 21 | 22 | with app.app_context(): 23 | app.config["model"] = init_model() 24 | 25 | app.register_blueprint(api, url_prefix="/api") 26 | 27 | @app.route("/dlflask", methods=["GET"]) 28 | def dlflask(): 29 | return render_template("dlflask.html") 30 | 31 | @app.route("/", methods=["GET"]) 32 | def index(): 33 | return "Deep Learning on Flask" 34 | 35 | return app 36 | -------------------------------------------------------------------------------- /app/app/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | api.py: api views used by Flask server. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | import io 10 | 11 | from flask import Blueprint, Response, jsonify, request 12 | from skimage.io import imread 13 | 14 | from .model import current_app, np, preprocess_image 15 | 16 | api = Blueprint("api", __name__) 17 | 18 | 19 | @api.route("/predictlabel", methods=["POST"]) 20 | def predict() -> Response: 21 | """ 22 | Predict the label of an uploaded image with the Deep Learning model. 23 | 24 | Returns: 25 | dict: The JSON response with the prediction results dictionary. 26 | """ 27 | 28 | result = {"success": False} 29 | 30 | if request.method == "POST" and request.files.get("file"): 31 | # read image as grayscale 32 | image_req = request.files["file"].read() 33 | request.files["file"].close() 34 | image = imread(io.BytesIO(image_req), as_gray=True) 35 | 36 | # preprocess the image for model 37 | preprocessed_image = preprocess_image(image) 38 | 39 | # classify the input image generating a list of predictions 40 | model = current_app.config["model"] 41 | predictions = model.predict(preprocessed_image) 42 | 43 | # add generated predictions to result 44 | result["predictions"] = [ 45 | {"label": str(i), "probability": str(pred)} for i, pred in enumerate(predictions[0]) 46 | ] 47 | result["most_probable_label"] = str(np.argmax(predictions[0])) 48 | result["success"] = True 49 | 50 | return jsonify(result) 51 | -------------------------------------------------------------------------------- /app/app/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | model.py: Functions related to Deep Learning model based on Keras. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | import numpy as np 10 | from flask import current_app 11 | from skimage import transform, util 12 | from tensorflow.keras import Model 13 | from tensorflow.keras.models import load_model 14 | 15 | 16 | def init_model() -> Model: 17 | """ 18 | Load the pre-trained Deep Learning model. 19 | 20 | Returns: 21 | model (tensorflow.keras.Model): The loaded Deep Learning model. 22 | """ 23 | 24 | model = load_model(current_app.config["MODEL_PATH"]) 25 | model.make_predict_function() 26 | return model 27 | 28 | 29 | def preprocess_image(image: np.ndarray) -> np.ndarray: 30 | """ 31 | Preprocess an image for the Deep Learning model. 32 | 33 | Args: 34 | image (numpy.ndarray): The input image. 35 | 36 | Returns: 37 | preprocessed_image (numpy.ndarray): The preprocessed image. 38 | """ 39 | 40 | # invert grayscale image 41 | inverted_image = util.invert(image) 42 | 43 | # resize and reshape image 44 | resized_image = transform.resize(inverted_image, (28, 28), anti_aliasing=True, mode="constant") 45 | resized_image = np.array(resized_image) 46 | preprocessed_image = resized_image.reshape((1, 28 * 28)) 47 | 48 | return preprocessed_image 49 | -------------------------------------------------------------------------------- /app/app/static/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvertogit/deeplearning_flask/c61d4f2be02cef85787a178bcae696d0b9ddd9c0/app/app/static/4.jpg -------------------------------------------------------------------------------- /app/app/templates/dlflask.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Predict handwritten digits 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Predict handwritten digits

13 |

A handwritten example image of a digit is shown below.

14 | 15 |

The value of the digit that can be predicted using a Deep Learning model.

16 | 17 |

POST example using curl

18 |
19 |
20 |
curl -F file=@app/app/static/4.jpg -X POST 'http://127.0.0.1/api/predictlabel'
21 |
22 |
23 | {
24 |   "most_probable_label": "4",
25 |   "predictions": [
26 |     {
27 |       "label" : "0",
28 |       "probability" : "8.270098e-08"
29 |     },
30 |     {
31 |       "label" : "1",
32 |       "probability" : "0.00016669065"
33 |     },
34 |     {
35 |       "label" : "2",
36 |       "probability" : "4.821898e-05"
37 |     },
38 |     {
39 |       "label" : "3",
40 |       "probability" : "2.3290573e-05"
41 |     },
42 |     {
43 |       "label" : "4",
44 |       "probability" : "0.99914443"
45 |     },
46 |     {
47 |       "label" : "5",
48 |       "probability" : "1.4074722e-06"
49 |     },
50 |     {
51 |       "label" : "6",
52 |       "probability" : "2.4940262e-05"
53 |     },
54 |     {
55 |       "label" : "7",
56 |       "probability" : "0.0004908524"
57 |     },
58 |     {
59 |       "label" : "8",
60 |       "probability" : "4.4384862e-05"
61 |     },
62 |     {
63 |       "label" : "9",
64 |       "probability" : "5.569217e-05"
65 |     }
66 |   ],
67 |   "success": true
68 | }
69 |       
70 |
71 |
72 |
73 |
74 |
CREDITS
75 |

author: alvertogit
copyright: 2018-2025

76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | config.py: Configurations used by Flask server. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | import os 10 | 11 | from flask import Flask 12 | 13 | 14 | class DefaultConfig: 15 | """ 16 | Default configuration class. 17 | """ 18 | 19 | SECRET_KEY = os.environ.get("SECRET_KEY") 20 | if not SECRET_KEY: 21 | raise ValueError("SECRET KEY NOT FOUND!") 22 | 23 | MODEL_PATH = os.environ.get("MODEL_PATH") or "/app/mnist_model.keras" 24 | 25 | @staticmethod 26 | def init_app(app: Flask) -> None: 27 | """ 28 | Initialize the application with the default configuration. 29 | """ 30 | 31 | print("PRODUCTION CONFIG") 32 | 33 | 34 | class DevConfig(DefaultConfig): 35 | """ 36 | Development configuration class. 37 | """ 38 | 39 | DEBUG = True 40 | 41 | @classmethod 42 | def init_app(cls, app: Flask) -> None: 43 | """ 44 | Initialize the application with the development configuration. 45 | """ 46 | 47 | print("DEVELOPMENT CONFIG") 48 | 49 | 50 | class TestConfig(DefaultConfig): 51 | """ 52 | Testing configuration class. 53 | """ 54 | 55 | TESTING = True 56 | 57 | @classmethod 58 | def init_app(cls, app: Flask) -> None: 59 | """ 60 | Initialize the application with the testing configuration. 61 | """ 62 | 63 | print("TESTING CONFIG") 64 | 65 | 66 | config = { 67 | "development": DevConfig, 68 | "testing": TestConfig, 69 | "production": DefaultConfig, 70 | "default": DefaultConfig, 71 | } 72 | -------------------------------------------------------------------------------- /app/mnist_model.keras: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvertogit/deeplearning_flask/c61d4f2be02cef85787a178bcae696d0b9ddd9c0/app/mnist_model.keras -------------------------------------------------------------------------------- /app/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | server.py: Run Flask server with Deep Learning model. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | from app import create_app 10 | 11 | app = create_app() 12 | 13 | if __name__ == "__main__": 14 | app.run(host="0.0.0.0", port=5000) 15 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvertogit/deeplearning_flask/c61d4f2be02cef85787a178bcae696d0b9ddd9c0/app/tests/__init__.py -------------------------------------------------------------------------------- /app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | conftest.py: It contents fixture functions used in tests. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | import pytest 10 | from flask import Flask 11 | from flask.testing import FlaskClient 12 | 13 | from app import create_app 14 | 15 | 16 | @pytest.fixture 17 | def app() -> Flask: 18 | """ 19 | Create a Flask app instance for testing. 20 | 21 | Returns: 22 | flask.Flask: The Flask app instance. 23 | """ 24 | 25 | app = create_app("testing") 26 | return app 27 | 28 | 29 | @pytest.fixture 30 | def client(app: Flask) -> FlaskClient: 31 | """ 32 | Create a Flask test client for the app. 33 | 34 | Args: 35 | app (flask.Flask): The Flask app instance. 36 | 37 | Returns: 38 | flask.testing.FlaskClient: The Flask test client. 39 | """ 40 | 41 | return app.test_client() 42 | -------------------------------------------------------------------------------- /app/tests/test_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_app.py: It contents flask app tests. 3 | """ 4 | 5 | __author__ = "alvertogit" 6 | __copyright__ = "Copyright 2018-2025" 7 | 8 | 9 | import json 10 | 11 | from flask.testing import FlaskClient 12 | 13 | 14 | def test_index(client: FlaskClient) -> None: 15 | """ 16 | Test the index route. 17 | 18 | Args: 19 | client (flask.testing.FlaskClient): The Flask test client. 20 | """ 21 | 22 | response = client.get("/") 23 | 24 | # assert response status 25 | assert response.status_code == 200 26 | 27 | # assert response data 28 | assert response.data == b"Deep Learning on Flask" 29 | 30 | 31 | def test_api(client: FlaskClient) -> None: 32 | """ 33 | Test the API endpoint to predict the label of an uploaded image with the Deep Learning model. 34 | 35 | Args: 36 | client (flask.testing.FlaskClient): The Flask test client. 37 | """ 38 | 39 | # server REST API endpoint url and example image path 40 | SERVER_URL = "http://127.0.0.1:5000/api/predictlabel" 41 | IMAGE_PATH = "../app/static/4.jpg" 42 | 43 | # create payload with image for request 44 | with open(IMAGE_PATH, "rb") as image: 45 | payload = {"file": image} 46 | response = client.post(SERVER_URL, data=payload) 47 | 48 | # assert response status 49 | assert response.status_code == 200 50 | 51 | # JSON format 52 | try: 53 | json_response = json.loads(response.data.decode("utf8")) 54 | except ValueError as e: 55 | print(e) 56 | exit(1) 57 | 58 | # successful 59 | if json_response["success"]: 60 | # most probable label 61 | print(json_response["most_probable_label"]) 62 | 63 | # predictions 64 | for dic in json_response["predictions"]: 65 | print(f"label {dic['label']} probability: {dic['probability']}") 66 | 67 | # assert the most probable label is 4 68 | assert json_response["most_probable_label"] == "4" 69 | # failed 70 | else: 71 | raise AssertionError("API endpoint /predictlabel failed") 72 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: . 4 | env_file: 5 | - .env.example 6 | volumes: 7 | - static_volume:/app/app/static 8 | networks: 9 | - nginx_network 10 | 11 | nginx: 12 | image: nginx:1.27-alpine 13 | ports: 14 | - 80:80 15 | volumes: 16 | - ./nginx/conf.d:/etc/nginx/conf.d 17 | - static_volume:/app/app/static:ro 18 | depends_on: 19 | - web 20 | networks: 21 | - nginx_network 22 | 23 | networks: 24 | nginx_network: 25 | driver: bridge 26 | 27 | volumes: 28 | static_volume: 29 | -------------------------------------------------------------------------------- /nginx/conf.d/local.conf: -------------------------------------------------------------------------------- 1 | # upstream server (Gunicorn application) 2 | upstream web_server { 3 | # docker automatically resolves the correct address as it has the same name as the service "web" 4 | server web:5000; 5 | } 6 | 7 | # main server 8 | server { 9 | 10 | listen 80; 11 | server_name localhost; 12 | 13 | client_body_buffer_size 4M; 14 | client_max_body_size 4M; 15 | 16 | location / { 17 | # all passed to Gunicorn 18 | proxy_pass http://web_server; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | proxy_set_header Host $host; 21 | proxy_redirect off; 22 | } 23 | 24 | location /static/ { 25 | alias /app/app/static/; 26 | } 27 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "deeplearning_flask" 3 | version = "1.0.0" 4 | authors = [ 5 | { name="alvertogit" }, 6 | ] 7 | description = "An example project combining Deep Learning and Flask" 8 | readme = "README.md" 9 | classifiers = [ 10 | "Programming Language :: Python :: 3", 11 | ] 12 | 13 | [project.urls] 14 | Homepage = "https://github.com/alvertogit/deeplearning_flask" 15 | 16 | [tool.ruff] 17 | extend-include = ["*.ipynb"] 18 | 19 | line-length = 100 20 | target-version = "py312" 21 | 22 | [tool.ruff.lint] 23 | select = [ 24 | # pycodestyle 25 | "E", 26 | "W", 27 | # Pyflakes 28 | "F", 29 | # pyupgrade 30 | "UP", 31 | # flake8-bugbear 32 | "B", 33 | # flake8-simplify 34 | "SIM", 35 | # isort 36 | "I", 37 | ] 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.1.1 2 | gunicorn==23.0.0 3 | numpy==2.1.3 4 | Pillow==11.2.1 5 | pre-commit==4.2.0 6 | pur==7.3.3 7 | pytest==8.3.5 8 | requests==2.32.3 9 | ruff==0.11.10 10 | scikit-image==0.25.2 11 | tensorflow==2.19.0 12 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | jupyterlab==4.4.2 3 | matplotlib==3.10.3 4 | --------------------------------------------------------------------------------