From 90b5a7aeb26c8be2eb2cd2a00bd4c53b6b265b26 Mon Sep 17 00:00:00 2001 From: guyl Date: Sat, 4 Dec 2021 12:37:23 +0200 Subject: [PATCH 1/6] Added PyTorch to the mask-detection demo --- .../1-training-and-evaluation.ipynb | 5 +- mask-detection/2-serving.ipynb | 14 +- mask-detection/3-automatic-pipeline.ipynb | 5 +- mask-detection/pytorch/serving.py | 76 +++++ .../pytorch/training-and-evaluation.py | 296 ++++++++++++++++++ mask-detection/tf-keras/serving.py | 3 +- .../tf-keras/training-and-evaluation.py | 5 +- 7 files changed, 384 insertions(+), 20 deletions(-) create mode 100644 mask-detection/pytorch/serving.py create mode 100644 mask-detection/pytorch/training-and-evaluation.py diff --git a/mask-detection/1-training-and-evaluation.ipynb b/mask-detection/1-training-and-evaluation.ipynb index acac9018..b3ed9772 100644 --- a/mask-detection/1-training-and-evaluation.ipynb +++ b/mask-detection/1-training-and-evaluation.ipynb @@ -412,7 +412,8 @@ "metadata": {}, "outputs": [], "source": [ - "framework = \"tf-keras\"" + "framework = \"tf-keras\"\n", + "# framework = \"pytorch\"" ] }, { @@ -1641,4 +1642,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/mask-detection/2-serving.ipynb b/mask-detection/2-serving.ipynb index 1dce31f4..41317b32 100644 --- a/mask-detection/2-serving.ipynb +++ b/mask-detection/2-serving.ipynb @@ -70,7 +70,8 @@ "metadata": {}, "outputs": [], "source": [ - "framework = \"tf-keras\"" + "framework = \"tf-keras\"\n", + "# framework = \"pytorch\"" ] }, { @@ -559,17 +560,8 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" - }, - "pycharm": { - "stem_cell": { - "cell_type": "raw", - "metadata": { - "collapsed": false - }, - "source": [] - } } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/mask-detection/3-automatic-pipeline.ipynb b/mask-detection/3-automatic-pipeline.ipynb index c4a587c3..7bedc4ce 100644 --- a/mask-detection/3-automatic-pipeline.ipynb +++ b/mask-detection/3-automatic-pipeline.ipynb @@ -79,7 +79,8 @@ "metadata": {}, "outputs": [], "source": [ - "framework = \"tf-keras\"" + "framework = \"tf-keras\"\n", + "# framework = \"pytorch\"" ] }, { @@ -718,4 +719,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/mask-detection/pytorch/serving.py b/mask-detection/pytorch/serving.py new file mode 100644 index 00000000..3ec39726 --- /dev/null +++ b/mask-detection/pytorch/serving.py @@ -0,0 +1,76 @@ +import urllib.request +from typing import Dict, List, Union + +import numpy as np +import torchvision +from PIL import Image + + +def resize(event: Dict) -> List[Image.Image]: + """ + Read images urls into numpy arrays and resize them to MobileNetV2 standard size of 224x224. + + :param event: A dictionary with the images urls at the 'data_url' key. + + :returns: A list of all the resized images as numpy arrays. + """ + # Read the images urls passed: + images_urls = event["data_url"] + + # Initialize an empty list for the resized images: + resized_images = [] + + # Go through the images urls and read and resize them: + for image_url in images_urls: + # Get the image: + urllib.request.urlretrieve(image_url, "temp.png") + image = Image.open("temp.png") + # Resize it: + image = image.resize((224, 224)) + # Collect it: + resized_images.append(image) + + return resized_images + + +def preprocess(images: List[Image.Image]) -> Dict[str, List[np.ndarray]]: + """ + Run the given images through MobileNetV2 preprocessing so they will be ready to be inferred through the mask + detection model. + + :param images: A list of images to preprocess. + + :returns: A dictionary for the PyTorchModelServer, with the preprocessed images in the 'inputs' key. + """ + transforms_composition = torchvision.transforms.Compose( + [ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] + ), + ] + ) + + preprocessed_images = [transforms_composition(image).numpy() for image in images] + + return {"inputs": preprocessed_images} + + +def postprocess(model_response: dict) -> Dict[str, Union[int, float]]: + """ + Read the predicted classes probabilities response from the PyTorchModelServer and parse them into a dictionary with + the results. + + :param model_response: The PyTorchModelServer response with the predicted probabilities. + + :returns: A dictionary with the parsed prediction. + """ + # Read the prediction from the model: + prediction = np.squeeze(model_response["outputs"]) + + # Parse and return: + return { + "class": int(np.argmax(prediction)), + "with_mask": float(prediction[0]), + "without_mask": float(prediction[1]), + } diff --git a/mask-detection/pytorch/training-and-evaluation.py b/mask-detection/pytorch/training-and-evaluation.py new file mode 100644 index 00000000..888bebe1 --- /dev/null +++ b/mask-detection/pytorch/training-and-evaluation.py @@ -0,0 +1,296 @@ +import os +from typing import Callable, List, Tuple + +import mlrun +import mlrun.frameworks.pytorch as mlrun_torch +import torch +import torchvision +from PIL import Image +from sklearn.model_selection import train_test_split +from torch import Tensor +from torch.nn import Module +from torch.utils.data import DataLoader, Dataset +from torchvision.transforms import InterpolationMode + + +class MaskDetectionDataset(Dataset): + """ + The mask detection dataset, including the data augmentations and preprocessing. + """ + + def __init__( + self, images: List[Image.Image], labels: Tensor, is_training: bool = True + ): + """ + Initialize a new dataset for training / evaluating the mask detection model. + + :param images: The images. + :param labels: The labels. + :param is_training: Whether to initialize a training set (apply the augmentations) or an evaluation / validation + set. + """ + # Compose the transformations: + augmentations = torchvision.transforms.Compose( + [ + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.RandomRotation(degrees=20), + torchvision.transforms.RandomResizedCrop( + size=(224, 224), + ratio=(0.85, 1.15), + interpolation=InterpolationMode.NEAREST, + ), + ] + ) # type: Callable[[Image.Image], Image.Image] + preprocess = torchvision.transforms.Compose( + [ + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] + ), + ] + ) # type: Callable[[Image.Image], Tensor] + + # Perform augmentations: + if is_training: + images = [augmentations(image) for image in images] + + # Preprocess the images: + images = [preprocess(image) for image in images] + + # Store this dataset's data: + self._images = images + self._labels = labels.type(dtype=torch.float32) + + def __getitem__(self, index: int) -> Tuple[Tensor, Tensor]: + """ + Return an image and its label at the given index. + + :param index: The index to get the dataset's item. + + :return: The 'i' item as a tuple of tensors: (image, label). + """ + return self._images[index], self._labels[index] + + def __len__(self) -> int: + """ + Returns the amount of images in the dataset. + + :return: The amount of images in the dataset. + """ + return len(self._images) + + +class MaskDetector(Module): + """ + The mask detector module, using MobileNetV2's features for transfer learning. + """ + + def __init__(self): + """ + Initialize a model, downloading MobileNetV2's weights. + """ + super(MaskDetector, self).__init__() + + # The model will be based on MobileNetV2: + self.mobilenet_v2 = torchvision.models.mobilenet_v2(pretrained=True) + + # Construct the head of the model that will be placed on top of the the base model: + self.mask_detection = torch.nn.Sequential( + torch.nn.AvgPool2d(kernel_size=(7, 7)), + torch.nn.Flatten(), + torch.nn.Linear(in_features=1280, out_features=128), + torch.nn.ReLU(), + torch.nn.Dropout(p=0.5), + torch.nn.Linear(in_features=128, out_features=2), + torch.nn.Softmax(dim=1), + ) + + # Loop over layers in MobilenetV2 and freeze them so they will not be updated during the first training process: + for child in self.mobilenet_v2.children(): + for parameter in child.parameters(): + parameter.requires_grad = False + + def forward(self, x) -> Tensor: + """ + Infer the given input through the model. + + :param x: An image to infer. + + :return: The model's prediction. + """ + x = self.mobilenet_v2.features(x) + x = self.mask_detection(x) + return x + + +def _get_datasets( + dataset_path: str, + batch_size: int, + is_evaluation: bool = False, +): + """ + Create the training and validation or evaluation datasets from the given path. + + :param dataset_path: Path to the main directory with the with mask and without mask images directories. + :param batch_size: The batch size to use in the datasets. + :param is_evaluation: Whether to return a tuple of training and evaluation datasets or just an evaluation dataset. + + :returns: If is_evaluation is False, a tuple of (Training dataset, Validation dataset). Otherwise, the Evaluation + dataset. + """ + # Build the dataset going through the classes directories and collecting the images: + images = [] + labels = [] + for label, directory in enumerate(["with_mask", "without_mask"]): + images_directory = os.path.join(dataset_path, directory) + images_files = [ + os.path.join(images_directory, file) + for file in os.listdir(images_directory) + if os.path.isfile(os.path.join(images_directory, file)) + ] + for image_file in images_files: + images.append( + Image.open(os.path.join(images_directory, image_file)).resize( + (224, 224) + ) + ) + labels.append(label) + + # Perform one-hot encoding on the labels: + labels = torch.tensor(labels) + labels = torch.nn.functional.one_hot(labels) + + # Check if its an evaluation, if so, use the entire data: + if is_evaluation: + # Construct the dataset: + evaluation_set = MaskDetectionDataset(images=images, labels=labels) + # Construct the data loader: + evaluation_set = DataLoader( + dataset=evaluation_set, batch_size=batch_size, shuffle=False + ) + return evaluation_set + + # Split the dataset into training and validation sets: + x_train, x_test, y_train, y_test = train_test_split( + images, + labels, + test_size=0.2, + stratify=labels, + random_state=42, + ) + + # Construct the datasets: + training_set = MaskDetectionDataset(images=x_train, labels=y_train) + validation_set = MaskDetectionDataset( + images=x_test, labels=y_test, is_training=False + ) + + # Construct the data loaders: + training_set = DataLoader(dataset=training_set, batch_size=batch_size, shuffle=True) + validation_set = DataLoader( + dataset=validation_set, batch_size=batch_size, shuffle=False + ) + + return training_set, validation_set + + +def accuracy(y_pred: Tensor, y_true: Tensor) -> float: + """ + Accuracy metric. + + :param y_pred: The model's prediction. + :param y_true: The ground truth. + + :return: The accuracy metric value. + """ + return 1 - (torch.norm(y_true - y_pred) / y_true.size()[0]).item() + + +def train( + context: mlrun.MLClientCtx, + dataset_path: str, + batch_size: int = 32, + lr: float = 1e-4, + epochs: int = 3, +): + """ + The training handler. Create the Mask Detection model and run training using the given parameters. The training is + orchestrated by MLRun.frameworks.pytorch. + + :param context: The MLRun Function's context. + :param dataset_path: Dataset path to get the datasets from. + :param batch_size: Batch size to use for the datasets. + :param lr: The learning rate for the Adam optimizer. + :param epochs: The amount of epochs to train. + """ + # Get the datasets: + training_set, validation_set = _get_datasets( + dataset_path=dataset_path, + batch_size=batch_size, + ) + + # Initialize the model: + model = MaskDetector() + + # Initialize the optimizer: + optimizer = torch.optim.Adam(lr=lr, params=model.parameters()) + + # Initialize the loss: + loss = torch.nn.MSELoss() + + # Train the head of the network: + mlrun_torch.train( + model=model, + training_set=training_set, + loss_function=loss, + optimizer=optimizer, + validation_set=validation_set, + metric_functions=[accuracy], + epochs=epochs, + training_iterations=35, + model_name="mask_detector", + custom_objects_map={"training-and-evaluation.py": "MaskDetector"}, + custom_objects_directory=os.path.abspath("./pytorch"), + context=context, + ) + + +def evaluate( + context: mlrun.MLClientCtx, + model_path: str, + dataset_path: str, + batch_size: int, +): + """ + The evaluation handler. Load the Mask Detection model and run an evaluation on the given parameters. The evaluation + is orchestrated by MLRun.frameworks.pytorch. + + :param context: The MLRun Function's context. + :param model_path: Path to the model object to evaluate. + :param dataset_path: Dataset path to get the evaluation set from. + :param batch_size: Batch size to use for the evaluation. + """ + # Get the dataset: + evaluation_set = _get_datasets( + dataset_path=dataset_path, batch_size=batch_size, is_evaluation=True + ) + + # Load the model using MLRun's model handler: + model_handler = mlrun_torch.PyTorchModelHandler( + model_name="mask_detector", model_path=model_path, context=context + ) + model_handler.load() + + # Initialize the loss: + loss = torch.nn.MSELoss() + + # Evaluate: + mlrun_torch.evaluate( + model=model_handler.model, + dataset=evaluation_set, + loss_function=loss, + metric_functions=[accuracy], + model_name="mask_detector", + model_path=model_path, + context=context, + ) diff --git a/mask-detection/tf-keras/serving.py b/mask-detection/tf-keras/serving.py index 82b95e35..60c21d3e 100644 --- a/mask-detection/tf-keras/serving.py +++ b/mask-detection/tf-keras/serving.py @@ -1,9 +1,8 @@ -from typing import List, Dict, Union import urllib.request +from typing import Dict, List, Union import numpy as np from PIL import Image - from tensorflow import keras diff --git a/mask-detection/tf-keras/training-and-evaluation.py b/mask-detection/tf-keras/training-and-evaluation.py index 7a2f7aea..7ac94558 100644 --- a/mask-detection/tf-keras/training-and-evaluation.py +++ b/mask-detection/tf-keras/training-and-evaluation.py @@ -1,10 +1,9 @@ import os -import numpy as np +import numpy as np +import tensorflow as tf from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelBinarizer - -import tensorflow as tf from tensorflow import keras for gpu in tf.config.experimental.list_physical_devices("GPU"): From 9e285e2460d5d029e2d7177e4b72c4c715a55b21 Mon Sep 17 00:00:00 2001 From: guyl Date: Sat, 4 Dec 2021 13:12:57 +0200 Subject: [PATCH 2/6] Applied isort and black on the code files --- .../pytorch/training-and-evaluation.py | 18 ++++-------------- .../tf-keras/training-and-evaluation.py | 19 ++++--------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/mask-detection/pytorch/training-and-evaluation.py b/mask-detection/pytorch/training-and-evaluation.py index 888bebe1..7788968b 100644 --- a/mask-detection/pytorch/training-and-evaluation.py +++ b/mask-detection/pytorch/training-and-evaluation.py @@ -124,9 +124,7 @@ def forward(self, x) -> Tensor: def _get_datasets( - dataset_path: str, - batch_size: int, - is_evaluation: bool = False, + dataset_path: str, batch_size: int, is_evaluation: bool = False, ): """ Create the training and validation or evaluation datasets from the given path. @@ -172,11 +170,7 @@ def _get_datasets( # Split the dataset into training and validation sets: x_train, x_test, y_train, y_test = train_test_split( - images, - labels, - test_size=0.2, - stratify=labels, - random_state=42, + images, labels, test_size=0.2, stratify=labels, random_state=42, ) # Construct the datasets: @@ -225,8 +219,7 @@ def train( """ # Get the datasets: training_set, validation_set = _get_datasets( - dataset_path=dataset_path, - batch_size=batch_size, + dataset_path=dataset_path, batch_size=batch_size, ) # Initialize the model: @@ -256,10 +249,7 @@ def train( def evaluate( - context: mlrun.MLClientCtx, - model_path: str, - dataset_path: str, - batch_size: int, + context: mlrun.MLClientCtx, model_path: str, dataset_path: str, batch_size: int, ): """ The evaluation handler. Load the Mask Detection model and run an evaluation on the given parameters. The evaluation diff --git a/mask-detection/tf-keras/training-and-evaluation.py b/mask-detection/tf-keras/training-and-evaluation.py index 7ac94558..cfd24609 100644 --- a/mask-detection/tf-keras/training-and-evaluation.py +++ b/mask-detection/tf-keras/training-and-evaluation.py @@ -14,9 +14,7 @@ def _get_datasets( - dataset_path: str, - batch_size: int, - is_evaluation: bool = False, + dataset_path: str, batch_size: int, is_evaluation: bool = False, ): """ Create the training and validation or evaluation datasets from the given path. @@ -61,11 +59,7 @@ def _get_datasets( # Split the dataset into training and validation sets: x_train, x_test, y_train, y_test = train_test_split( - images, - labels, - test_size=0.2, - stratify=labels, - random_state=42, + images, labels, test_size=0.2, stratify=labels, random_state=42, ) # Construct the training image generator for data augmentation: @@ -151,9 +145,7 @@ def train( # Compile the model: model.compile( - optimizer=optimizer, - loss="categorical_crossentropy", - metrics=["accuracy"], + optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy"], ) # Train the head of the network: @@ -167,10 +159,7 @@ def train( def evaluate( - context: mlrun.MLClientCtx, - model_path: str, - dataset_path: str, - batch_size: int, + context: mlrun.MLClientCtx, model_path: str, dataset_path: str, batch_size: int, ): """ The evaluation handler. Load the Mask Detection model and run an evaluation on the given parameters. The evaluation From 21ec56f6da93fb151376a33ae9617d8e64071a2a Mon Sep 17 00:00:00 2001 From: guyl Date: Sat, 4 Dec 2021 13:25:56 +0200 Subject: [PATCH 3/6] Updated the README --- mask-detection/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mask-detection/README.md b/mask-detection/README.md index 5861e3eb..7cfb76d9 100644 --- a/mask-detection/README.md +++ b/mask-detection/README.md @@ -5,8 +5,7 @@ In the following demo we will demonstrate how to use MLRun to create a mask dete ### Key Technologies: -* Either [**TF.Keras**](https://www.tensorflow.org/api_docs/python/tf/keras) to train and evaluate the model, -* or [**PyTorch**](https://pytorch.org/) (Will be added soon) +* Either [**TF.Keras**](https://www.tensorflow.org/api_docs/python/tf/keras) or [**PyTorch**](https://pytorch.org/) to train and evaluate the model * [**Horovod**](https://horovod.ai/) to run distributed training * [**ONNX**](https://onnx.ai/) to optimize and accelerate the model's performance * [**Nuclio**](https://nuclio.io/) to create a high-performance serverless Serving function From 83813db56363a456e81f9c816c776b9f9e7ee288 Mon Sep 17 00:00:00 2001 From: guyl Date: Fri, 10 Dec 2021 04:21:33 +0200 Subject: [PATCH 4/6] Final edits for PyTorch version --- README.md | 2 +- .../1-training-and-evaluation.ipynb | 69 ++++++++++++++++--- mask-detection/2-serving.ipynb | 28 +++++++- mask-detection/3-automatic-pipeline.ipynb | 6 +- mask-detection/pytorch/serving.py | 5 +- .../pytorch/training-and-evaluation.py | 20 ++---- 6 files changed, 99 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 5540bc44..4f16ced6 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ To run the MLRun demos, first do the following: ## Mask Detection Demo The [Mask detection](./mask-detection/README.md) demo is a 3 notebooks demo where we: -1. **Train and evaluate** a model for detecting whether a person is wearing a mask in an image using Tensorflow.Keras or PyTorch (coming soon). +1. **Train and evaluate** a model for detecting whether a person is wearing a mask in an image using Tensorflow.Keras or PyTorch. 2. **Serve** the model as a serverless function in a http endpoint. 3. Write an **automatic pipeline** where we download a dataset of images, train and evaluate, optimize the model (using ONNX) and serve it. diff --git a/mask-detection/1-training-and-evaluation.ipynb b/mask-detection/1-training-and-evaluation.ipynb index b3ed9772..969727a7 100644 --- a/mask-detection/1-training-and-evaluation.ipynb +++ b/mask-detection/1-training-and-evaluation.ipynb @@ -20,7 +20,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Before we continue, we need to setup some requirements:" + "Before we continue, we need to install MLRun and the framework of choice (comment and uncomment the framework you wish to use):" ] }, { @@ -30,8 +30,14 @@ "outputs": [], "source": [ "!pip install mlrun\n", - "!pip install -U tensorflow==2.4.1\n", - "!pip install -U typing-extensions" + "!pip install -U typing-extensions\n", + "\n", + "########## For TF.Keras: ##########\n", + "!pip install -U tensorflow==2.4.4\n", + "\n", + "########## For PyTorch: ##########\n", + "# !pip install -U torch==1.10\n", + "# !pip install -U torchvision==0.11.1" ] }, { @@ -424,18 +430,56 @@ "\n", "The code is taken from the python file [training-and-evaluation.py](tf-keras/training-and-evaluation.py). It is classic and straightforward, we: \n", "1. Use `_get_datasets` to get the training and validation datasets (on evaluation - the evaluation dataset).\n", - "2. Use `_get_model` to build our classifier - simple transfer learning from MobileNetV2.\n", + "2. Use `_get_model` to build our classifier - simple transfer learning from MobileNetV2 (`keras.applications`).\n", "3. Call `train` to train the model.\n", "4. Call `evaluate` to evaluate the model.\n", "\n", - "Taking this code one step further is **MLRun**'s framework for `tf.keras`: \n", + "Taking this code one step further is **MLRun**'s framework for `tf.keras`:\n", "\n", "```python\n", "# Apply MLRun's interface for tf.keras:\n", "mlrun_tf_keras.apply_mlrun(model=model, context=context, ...)\n", "```\n", "\n", - "With just one line of code, it seamlessly provides:\n", + "With just one line of code, it seamlessly provides **automatic logging** (for both MLRun and Tensorboard) and **distributed training** by wrapping the `fit` and `evaluate` methods of `tf.keras.Model`.\n", + "\n", + "In addition, in the `evaluate` method code, we use the `TFKerasModelHandler` class. This class supports loading, saving and logging `tf.keras` models with ease, enabling easy versioning of the model and his results, artifacts and custom objects." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PyTorch\n", + "\n", + "The code is taken from the python file [training-and-evaluation.py](pytorch/training-and-evaluation.py). It is classic and straightforward, we:\n", + "1. Use `_get_datasets` to get the training and validation datasets (on evaluation - the evaluation dataset). The function is initiazliing a `MaskDetectionDataset` to handle our images.\n", + "2. Initialize our `MaskDetector` classifier class - a simple transfer learning from MobileNetV2 (`torchvision.models`).\n", + "3. Call `train` to train the model.\n", + "4. Call `evaluate` to evaluate the model.\n", + "\n", + "Taking this code one step further is **MLRun**'s framework for `torch`:\n", + "\n", + "```python\n", + "import mlrun.frameworks.pytorch as mlrun_torch\n", + "```\n", + "\n", + "`mlrun_torch` is providing what we call \"shortcut functions\" for using PyTorch with ease:\n", + "* `train` - Training a model.\n", + "* `evaluate` - Evaluating a model.\n", + "\n", + "Both functions enable **automatic logging** (for both MLRun and Tensorboard) and **distributed training** by simply pass the following parameters: `auto_log: bool` and `use_horovod: bool`.\n", + "\n", + "In addition, you can choose to use our classes directly:\n", + "* `PyTorchMLRunInterface` - the interface for training, evaluating and predicting a PyTorch model. Our code is highly generic and should fit for any type of model.\n", + "* If you wish to use your own training code, to get automatic logging you will simply need to use our callback mechanism with `CallbackHandler`.\n", + "* `PyTorchModelHandler` - supports loading, saving and logging `torch` models with ease, enabling easy versioning of the model and his results, artifacts and custom objects." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Both **TF.Keras** and **PyTorch** has the same features regarding MLRun's automatic logging and distributed training orchastration:\n", "* **Automatic logging**: auto-log your training and model to both **Tensorboard** and **MLRun**. Additional settings can be passed onto this method to gain extra logging capabilities, like:\n", " * Weights histograms and distributions\n", " * Weights statistics\n", @@ -444,14 +488,14 @@ " * Logging frequency and more\n", "* **Distributed training with Horovod**: Horovod will be initialized and used automatically if the MLRun Function's `kind` attribute is equal to `\"mpijob\"`, there won't be any additional changes needed to the original code! More on that later in [section 6](#section_6)\n", "\n", - "In addition, in the `evaluate` method code, we use the `mlrun.frameworks.tf_keras.TFKerasModelHandler` class. This class supports loading, saving and logging `tf.keras` models with ease, enabling easy versioning of the model and his results, artifacts and custom objects.\n", - "\n", "We suggest reading the documentation for further use, or like in this example, use the default settings." - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "\n", "## 4. Create the MLRun Function\n", @@ -459,7 +503,10 @@ "We will use MLRun's `mlrun.code_to_function` to create a MLRun Function from our code in the above mentioned python file. Notice our MLRun Function will have two handlers: `train` and `evaluate`.\n", "\n", "We wish to run the training first as a Job, so we will set the `kind` parameter to `\"job\"`." - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", diff --git a/mask-detection/2-serving.ipynb b/mask-detection/2-serving.ipynb index 41317b32..ed496f16 100644 --- a/mask-detection/2-serving.ipynb +++ b/mask-detection/2-serving.ipynb @@ -91,6 +91,25 @@ "4. `post-process` - Parse the prediction probabilities and wrap them in a dictionary with which to respond." ] }, + { + "cell_type": "markdown", + "source": [ + "### PyTorch\n", + "\n", + "The code is taken from the python file [serving.py](pytorch/serving.py). Our data will go through the following structure:\n", + "1. `resize` - Read the URL into an array and resize it to 224x224.\n", + "2. `preprocess` - Use `torchvision.transforms` to normalize the images for MobileNetV2.\n", + "3. `mlrun.frameworks.pytorch.PyTorchModelServer` - Infer the inputs through the model and return the predictions. It can be imported from:\n", + " ```python\n", + " from mlrun.frameworks.pytorch import PyTorchModelServer\n", + " ```\n", + " This class can be inherited and its pre-process, post-process, predict and explain methods can be overridden. In this demo, we will be using the defaults to showcase the topology feature of our serving functions.\n", + "4. `post-process` - Parse the prediction probabilities and wrap them in a dictionary with which to respond." + ], + "metadata": { + "collapsed": false + } + }, { "cell_type": "markdown", "metadata": {}, @@ -218,10 +237,17 @@ "# Set the topology and get the graph object:\n", "graph = serving_function.set_topology(\"flow\", engine=\"async\")\n", "\n", + "# Choose the ModelServer according to the selected framework:\n", + "model_server_class = (\n", + " \"mlrun.frameworks.tf_keras.TFKerasModelServer\"\n", + " if framework == \"tf-keras\"\n", + " else \"mlrun.frameworks.pytorch.PyTorchModelServer\"\n", + ")\n", + "\n", "# Build the serving graph:\n", "graph.to(handler=\"resize\", name=\"resize\")\\\n", " .to(handler=\"preprocess\", name=\"preprocess\")\\\n", - " .to(class_name=\"mlrun.frameworks.tf_keras.TFKerasModelServer\", name=\"mask_detector\", model_path=project.get_artifact_uri(\"mask_detector\"))\\\n", + " .to(class_name=model_server_class, name=\"detect_mask\", model_path=project.get_artifact_uri(\"mask_detector\"))\\\n", " .to(handler=\"postprocess\", name=\"postprocess\").respond()\n", "\n", "# Plot to graph:\n", diff --git a/mask-detection/3-automatic-pipeline.ipynb b/mask-detection/3-automatic-pipeline.ipynb index 7bedc4ce..fc44ec09 100644 --- a/mask-detection/3-automatic-pipeline.ipynb +++ b/mask-detection/3-automatic-pipeline.ipynb @@ -210,7 +210,7 @@ "):\n", " # Get our project object:\n", " project = mlrun.get_current_project()\n", - " \n", + "\n", " # Write down the ONNX requirements:\n", " onnx_requirements = [\n", " \"onnx~=1.10.1\",\n", @@ -277,8 +277,8 @@ " handler=\"to_onnx\",\n", " name=\"optimizing\",\n", " params={\n", - " \"model_name\": 'mask_detector',\n", " \"model_path\": training_run.outputs['mask_detector'],\n", + " \"onnx_model_name\": 'onnx_mask_detector'\n", " },\n", " outputs=[\"onnx_mask_detector\"],\n", " ).after(build_condition)\n", @@ -310,7 +310,7 @@ " # Build the serving graph:\n", " graph.to(handler=\"resize\", name=\"resize\")\\\n", " .to(handler=\"preprocess\", name=\"preprocess\")\\\n", - " .to(\"mlrun.frameworks.onnx.ONNXModelServer\", \"onnx_mask_detector\", model_path=project.get_artifact_uri(\"onnx_mask_detector\"))\\\n", + " .to(class_name=\"mlrun.frameworks.onnx.ONNXModelServer\", name=\"onnx_mask_detector\", model_path=project.get_artifact_uri(\"onnx_mask_detector\"))\\\n", " .to(handler=\"postprocess\", name=\"postprocess\").respond()\n", " # Set the desired requirements:\n", " serving_function.with_requirements(requirements=onnx_requirements)\n", diff --git a/mask-detection/pytorch/serving.py b/mask-detection/pytorch/serving.py index 3ec39726..99684636 100644 --- a/mask-detection/pytorch/serving.py +++ b/mask-detection/pytorch/serving.py @@ -42,6 +42,7 @@ def preprocess(images: List[Image.Image]) -> Dict[str, List[np.ndarray]]: :returns: A dictionary for the PyTorchModelServer, with the preprocessed images in the 'inputs' key. """ + # Prepare the transforms composition: transforms_composition = torchvision.transforms.Compose( [ torchvision.transforms.ToTensor(), @@ -51,7 +52,9 @@ def preprocess(images: List[Image.Image]) -> Dict[str, List[np.ndarray]]: ] ) - preprocessed_images = [transforms_composition(image).numpy() for image in images] + # Apply the transforms: + preprocessed_images = [np.expand_dims(transforms_composition(image).numpy(), 0) for image in images] + preprocessed_images = [np.vstack(preprocessed_images)] return {"inputs": preprocessed_images} diff --git a/mask-detection/pytorch/training-and-evaluation.py b/mask-detection/pytorch/training-and-evaluation.py index 7788968b..9ac26803 100644 --- a/mask-detection/pytorch/training-and-evaluation.py +++ b/mask-detection/pytorch/training-and-evaluation.py @@ -67,7 +67,7 @@ def __getitem__(self, index: int) -> Tuple[Tensor, Tensor]: :param index: The index to get the dataset's item. - :return: The 'i' item as a tuple of tensors: (image, label). + :returns: The 'i' item as a tuple of tensors: (image, label). """ return self._images[index], self._labels[index] @@ -75,7 +75,7 @@ def __len__(self) -> int: """ Returns the amount of images in the dataset. - :return: The amount of images in the dataset. + :returns: The amount of images in the dataset. """ return len(self._images) @@ -116,7 +116,7 @@ def forward(self, x) -> Tensor: :param x: An image to infer. - :return: The model's prediction. + :returns: The model's prediction. """ x = self.mobilenet_v2.features(x) x = self.mask_detection(x) @@ -195,7 +195,7 @@ def accuracy(y_pred: Tensor, y_true: Tensor) -> float: :param y_pred: The model's prediction. :param y_true: The ground truth. - :return: The accuracy metric value. + :returns: The accuracy metric value. """ return 1 - (torch.norm(y_true - y_pred) / y_true.size()[0]).item() @@ -243,7 +243,7 @@ def train( training_iterations=35, model_name="mask_detector", custom_objects_map={"training-and-evaluation.py": "MaskDetector"}, - custom_objects_directory=os.path.abspath("./pytorch"), + custom_objects_directory=os.path.join(os.path.dirname(dataset_path), "pytorch"), context=context, ) @@ -265,22 +265,14 @@ def evaluate( dataset_path=dataset_path, batch_size=batch_size, is_evaluation=True ) - # Load the model using MLRun's model handler: - model_handler = mlrun_torch.PyTorchModelHandler( - model_name="mask_detector", model_path=model_path, context=context - ) - model_handler.load() - # Initialize the loss: loss = torch.nn.MSELoss() # Evaluate: mlrun_torch.evaluate( - model=model_handler.model, + model_path=model_path, dataset=evaluation_set, loss_function=loss, metric_functions=[accuracy], - model_name="mask_detector", - model_path=model_path, context=context, ) From fbc442eed8653f1b9c60ce04fbcdb19c6e441e1d Mon Sep 17 00:00:00 2001 From: alexiguazio <71078625+alexiguazio@users.noreply.github.com> Date: Sun, 12 Dec 2021 14:53:16 +0200 Subject: [PATCH 5/6] Update 1-training-and-evaluation.ipynb --- mask-detection/1-training-and-evaluation.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mask-detection/1-training-and-evaluation.ipynb b/mask-detection/1-training-and-evaluation.ipynb index 969727a7..8f91e7ad 100644 --- a/mask-detection/1-training-and-evaluation.ipynb +++ b/mask-detection/1-training-and-evaluation.ipynb @@ -96,7 +96,7 @@ "\n", "### 2.1. Import a Function\n", "\n", - "We will download the images using `open_archive` - a function from MLRun's functions marketplace. We will import the fucntion using `mlrun.import_function` and describe it to get the function's documentation:" + "We will download the images using `open_archive` - a function from MLRun's functions marketplace. We will import the function using `mlrun.import_function` and describe it to get the function's documentation:" ] }, { @@ -428,7 +428,7 @@ "source": [ "### TF.Keras\n", "\n", - "The code is taken from the python file [training-and-evaluation.py](tf-keras/training-and-evaluation.py). It is classic and straightforward, we: \n", + "The code is taken from the python file [training-and-evaluation.py](tf-keras/training-and-evaluation.py), which is classic and straightforward. We: \n", "1. Use `_get_datasets` to get the training and validation datasets (on evaluation - the evaluation dataset).\n", "2. Use `_get_model` to build our classifier - simple transfer learning from MobileNetV2 (`keras.applications`).\n", "3. Call `train` to train the model.\n", @@ -452,7 +452,7 @@ "source": [ "### PyTorch\n", "\n", - "The code is taken from the python file [training-and-evaluation.py](pytorch/training-and-evaluation.py). It is classic and straightforward, we:\n", + "The code is taken from the python file [training-and-evaluation.py](pytorch/training-and-evaluation.py), which is classic and straightforward. We:\n", "1. Use `_get_datasets` to get the training and validation datasets (on evaluation - the evaluation dataset). The function is initiazliing a `MaskDetectionDataset` to handle our images.\n", "2. Initialize our `MaskDetector` classifier class - a simple transfer learning from MobileNetV2 (`torchvision.models`).\n", "3. Call `train` to train the model.\n", @@ -468,7 +468,7 @@ "* `train` - Training a model.\n", "* `evaluate` - Evaluating a model.\n", "\n", - "Both functions enable **automatic logging** (for both MLRun and Tensorboard) and **distributed training** by simply pass the following parameters: `auto_log: bool` and `use_horovod: bool`.\n", + "Both functions enable **automatic logging** (for both MLRun and Tensorboard) and **distributed training** by simply passing the following parameters: `auto_log: bool` and `use_horovod: bool`.\n", "\n", "In addition, you can choose to use our classes directly:\n", "* `PyTorchMLRunInterface` - the interface for training, evaluating and predicting a PyTorch model. Our code is highly generic and should fit for any type of model.\n", @@ -1130,7 +1130,7 @@ "\n", "## 6. Run Distributed Training Using Horovod\n", "\n", - "Now we can see the second benefit of MLRun, we can **distribute** our model **training** across **multiple workers** (i.e., perform distributed training), assign **GPUs**, and more. We don't need to bother with Dockerfiles or K8s YAML configuration files — MLRun does all of this for us. All is needed to be done, is create our function with `kind=\"mpijob\"`.\n", + "Now we can see the second benefit of MLRun, we can **distribute** our model **training** across **multiple workers** (i.e., perform distributed training), assign **GPUs**, and more. We don't need to bother with Dockerfiles or K8s YAML configuration files — MLRun does all of this for us. We will simply create our function with `kind=\"mpijob\"`.\n", "\n", "> **Notice**: for this demo, in order to use GPUs in training, set the `use_gpu` variable to `True`. This will later assign the required configurations to use the GPUs and pass the correct image to support GPUs (image with CUDA libraries)." ] @@ -1197,7 +1197,7 @@ "source": [ "Call run, and notice each epoch is shorter as we now have 2 workers instead of 1. As the 2 workers will print a lot of outputs we would rather wait for completion and then show the results. For that, we will pass `watch=False` and use the run objects function `wait_for_completion` and `show`. \n", "\n", - "In order to see the logs, you are welcome to go into the UI by clicking the blue hyperlink \"**click here**\" after running the function and see the logs there:" + "To see the logs, you can go into the UI by clicking the blue hyperlink \"**click here**\" after running the function:" ] }, { @@ -1689,4 +1689,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From a7b0e225d0d47a32dd3db86171881e02644d6ddd Mon Sep 17 00:00:00 2001 From: alexiguazio <71078625+alexiguazio@users.noreply.github.com> Date: Sun, 12 Dec 2021 14:58:39 +0200 Subject: [PATCH 6/6] Update 3-automatic-pipeline.ipynb --- mask-detection/3-automatic-pipeline.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mask-detection/3-automatic-pipeline.ipynb b/mask-detection/3-automatic-pipeline.ipynb index fc44ec09..8dbfec06 100644 --- a/mask-detection/3-automatic-pipeline.ipynb +++ b/mask-detection/3-automatic-pipeline.ipynb @@ -6,7 +6,7 @@ "source": [ "# Mask Detection Demo - Automatic Pipeline (3 / 3)\n", "\n", - "The following example demonstrates how to package a project and how to run an automatic pipeline for training, evaluating, optimizing and serving the mask detection model using our saved MLRun functions from the previous notebooks.\n", + "The following example demonstrates how to package a project and how to run an automatic pipeline to train, evaluate, optimize and serve the mask detection model using our saved MLRun functions from the previous notebooks.\n", "\n", "1. [Set up the project](#section_1)\n", "2. [Write and save the workflow](#section_2)\n", @@ -322,7 +322,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that after running the cell above, the `workflow.py` file is created. Saving your workflow to file allows you to use it if run the project from a different environment.\n", + "Note that after running the cell above, the `workflow.py` file is created. Saving your workflow to file allows you to run the project from a different environment.\n", "\n", "In order to take this project with the functions we set and the workflow we saved over to a different environemnt, first set the workflow to the project. The workflow can be set using `project.set_workflow`. After setting it, we will save the project by calling `project.save`. When loaded, it can be run from another environment from both code and from cli. For more information regarding saving and loading a MLRun project, see the [documentation](https://docs.mlrun.org/en/latest/projects/overview.html)." ] @@ -719,4 +719,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}