From e9c6a12af47e91220cdedc7e59b3d3a8cacb1d29 Mon Sep 17 00:00:00 2001 From: yaron haviv Date: Fri, 7 Jan 2022 02:39:14 +0200 Subject: [PATCH 1/6] add netops demo --- network-operations/01-ingest.ipynb | 1403 +++++++++++++++++ .../02-training-and-deployment.ipynb | 647 ++++++++ network-operations/README.md | 97 ++ network-operations/src/generator.py | 188 +++ .../src/metric_configurations.yaml | 49 + network-operations/src/workflow.py | 52 + 6 files changed, 2436 insertions(+) create mode 100644 network-operations/01-ingest.ipynb create mode 100644 network-operations/02-training-and-deployment.ipynb create mode 100644 network-operations/README.md create mode 100644 network-operations/src/generator.py create mode 100644 network-operations/src/metric_configurations.yaml create mode 100644 network-operations/src/workflow.py diff --git a/network-operations/01-ingest.ipynb b/network-operations/01-ingest.ipynb new file mode 100644 index 00000000..f43c991d --- /dev/null +++ b/network-operations/01-ingest.ipynb @@ -0,0 +1,1403 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Network Operations Demo - Data Ingestion & Preparation\n", + "This project demonstrates how to build an automated machine-learning (ML) pipeline for predicting network outages based on network-device telemetry, also known as Network Operations (NetOps). The demo covers how to setup a real-time system for data ingestion, feature engineering, model training, deployment and monitoring on top of Iguazio's Data Science Platform. We use the open source MLRun MLOps orchestration framework and Feature Store in combination with the open source Nuclio serverless engine as a runtime.\n", + "\n", + "The demo consists of:\n", + "1. Building and testing features from three sources (device metadata, real-time device metrics, and real-time device labels) using the feature store\n", + "2. Ingesting the data using batch (for testing) or real-time (for production)\n", + "3. Train and test the model with data from the feature-store\n", + "4. Deploying the model as part of a real-time feature engineering and inference pipeline\n", + "5. Real-time model and metrics monitoring, drift detection\n", + "\n", + "**In this notebook:**\n", + "* [**Part 1: Create and configure an MLRun project**]()\n", + "* [**Part 2: Define and test the feature engineering pipeline**]()\n", + "* [**Part 3: Ingest the features data using batch or real-time**]()\n", + "\n", + "in the next [**02-training-and-deployment**](./02-training-and-deployment.ipynb) notebook you can learn how to build automated pipelines which train, test, and deploy models using data from the feature store.\n", + "\n", + "**Please run the following ONCE to install the required packages, and restart the notebook kernel right after:**" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting pytimeparse\n", + " Downloading pytimeparse-1.1.8-py2.py3-none-any.whl (10.0 kB)\n", + "Installing collected packages: pytimeparse\n", + "Successfully installed pytimeparse-1.1.8\n" + ] + } + ], + "source": [ + "!pip install pytimeparse faker\n", + "!pip install -i https://test.pypi.org/simple/ v3io-generator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 1: Create and configure an MLRun project" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-05 23:04:12,849 [info] created and saved project network-operations\n" + ] + } + ], + "source": [ + "# Import general utilities for the entire notebook\n", + "import os\n", + "import json\n", + "import yaml\n", + "import mlrun \n", + "import mlrun.feature_store as fstore # MLRun's feature store\n", + "\n", + "# Create or get the project\n", + "project = mlrun.get_or_create_project('network-operations', \"./\", user_project=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define project parameters, artifacts, and functions\n", + "Throughout the project, we will require some files, configurations, streams, etc. to be shared with the different functions. \n", + "MLRun allows us to easily do this by using the project parameters (`project.params`) or using artifacts which are available to all the functions in the project. \n", + "\n", + "We will define 2 stream paths for the network-device-metrics and network-device-labels, and a network-device static data KV table for our feature sets. \n", + "We will also log the parameters for the metrics generator (simulator) which will be used by different functions in the project.\n", + "\n", + "The functions/code used in the project are registered (using `set_function`) allowing reference to code/functions by name and CI/CD pipelines." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Set the feature set names \n", + "device_metrics_fs_name = 'device_metrics'\n", + "device_labels_fs_name = 'device_labels'\n", + "static_data_fs_name = 'static'\n", + "metric_configurations_path = \"./src/metric_configurations.yaml\"\n", + "\n", + "# load the metrics configuration for local use and log it as an artifact for use by the functions\n", + "with open(metric_configurations_path, \"r\") as fp:\n", + " metrics_configuration = yaml.safe_load(fp)\n", + "metrics_configuration_uri = project.log_artifact(\"metric_configurations\", local_path=metric_configurations_path).uri\n", + "\n", + "# Create iguazio v3io stream and device metrics push API endpoint\n", + "device_metrics_stream = f'v3io:///projects/{project.name}/streams/{device_metrics_fs_name}'\n", + "device_labels_stream = f'v3io:///projects/{project.name}/streams/{device_labels_fs_name}'\n", + "\n", + "# Set the configuration and stream paths as a project parameters so it could be picked up by the functions\n", + "project.params = {\n", + " 'metrics_configuration_uri': metrics_configuration_uri,\n", + " 'device_metrics_stream': device_metrics_stream,\n", + " 'device_labels_stream': device_labels_stream,\n", + "}\n", + "\n", + "# register the metrics generator function\n", + "project.set_function('src/generator.py', name='metrics-generator', kind='nuclio', image='mlrun/mlrun')\n", + "\n", + "project.save()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# uncomment the line below to print the project spec\n", + "# print(project.to_yaml())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 2: Define and test the feature engineering pipelines\n", + "\n", + "Our model is using 2 input datasets and target labels dataset (y):\n", + "* Static data and metadata per device (location, model, etc.)\n", + "* Real-time metrics per device (CPU and memory usage, throughput, latency, etc.)\n", + "* Real-time Labels (indications for device failures/errors, etc..) \n", + "\n", + "We use the feature store to create 3 different **feature sets** (one per dataset) and apply various aggregations and transformations " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get data sample (simulated) for developing the features\n", + "We use the `v3io_generator` simulator to generate fake device and metrics data with real-world behavior, the exact data schema and patterns are defined in the [**metrics configuration file**](./src/metric_configurations.yaml), it can be adjusted to simulate your specific features and time-series metrics.\n", + "\n", + "The [**generator function**](./src/generator.py) is imported locally (for testing), later in the notebook we will demonstrate how it can also be deployed as a real-time Nuclio function which continuously sends real-time metrics and labels over streams (for testing real-time ingestion/processing).\n", + "\n", + "We use the generator and retrieve sample data (Static devices dataset, Real-time metrics, Real-time Labels) for developing and testing our features." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "creating deployment\n" + ] + } + ], + "source": [ + "from src.generator import get_sample\n", + "\n", + "metrics_df, labels_df, static_df = get_sample(metrics_configuration, project=project)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature Set 1 - Static Network Devices data/metadata \n", + "This feature set will hold all the static network-device data. As such it will be represented and ingested as a table. \n", + "Since such static data usually holds a lot of categorical properties about the device, we will use a `one hot encoder` to encode our categorical features to a supported format and We will save this dataset results to a No-Sql (Key Value) table. \n", + "\n", + "After defining the feature set we will plot it's computational graph so we can easily review it in the notebook, preview it to test the wanted results and deploy it." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "mlrun-flow\n", + "\n", + "\n", + "\n", + "_start\n", + "\n", + "start\n", + "\n", + "\n", + "\n", + "OneHotEncoder\n", + "\n", + "OneHotEncoder\n", + "\n", + "\n", + "\n", + "_start->OneHotEncoder\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "\n", + "OneHotEncoder->parquet\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "nosql\n", + "\n", + "\n", + "nosql\n", + "\n", + "\n", + "\n", + "OneHotEncoder->nosql\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mlrun.feature_store.steps import OneHotEncoder\n", + "\n", + "# Define the feature set\n", + "static_fs = fstore.FeatureSet('static', \n", + " entities=['device'], \n", + " description='Static data for the devices')\n", + "\n", + "# Setup the One Hot Encoder\n", + "one_hot_encoder_mapping = {'country': metrics_configuration['country'],\n", + " 'model': metrics_configuration['models']}\n", + "\n", + "# Append the one hot encoder to the feature set's processing graph\n", + "static_fs.graph.to(OneHotEncoder(mapping=one_hot_encoder_mapping))\n", + "\n", + "# Set the default targets for the feature set (parquet & no-sql)\n", + "static_fs.set_targets()\n", + "\n", + "# Show a plot of the feature set's processing graph\n", + "static_fs.plot(with_targets=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
country_Acountry_Bcountry_Ccountry_Dcountry_Ecountry_Fcountry_Gmodel_0model_1model_2model_3model_4model_5model_6model_7model_8model_9
device
893616499315100000010000000010
081309862918500000100010000000
601335934657500001000000100000
911070755737800100000000000010
342035133682100000010000000010
\n", + "
" + ], + "text/plain": [ + " country_A country_B country_C country_D country_E \\\n", + "device \n", + "8936164993151 0 0 0 0 0 \n", + "0813098629185 0 0 0 0 0 \n", + "6013359346575 0 0 0 0 1 \n", + "9110707557378 0 0 1 0 0 \n", + "3420351336821 0 0 0 0 0 \n", + "\n", + " country_F country_G model_0 model_1 model_2 model_3 \\\n", + "device \n", + "8936164993151 0 1 0 0 0 0 \n", + "0813098629185 1 0 0 0 1 0 \n", + "6013359346575 0 0 0 0 0 0 \n", + "9110707557378 0 0 0 0 0 0 \n", + "3420351336821 0 1 0 0 0 0 \n", + "\n", + " model_4 model_5 model_6 model_7 model_8 model_9 \n", + "device \n", + "8936164993151 0 0 0 0 1 0 \n", + "0813098629185 0 0 0 0 0 0 \n", + "6013359346575 1 0 0 0 0 0 \n", + "9110707557378 0 0 0 0 1 0 \n", + "3420351336821 0 0 0 0 1 0 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Preview the feature sets computation results\n", + "# (this is how the data will be finally saved to our feature store)\n", + "# * The preview also serves to infer the schema of the feature set \n", + "# * which could be later saved with the feature set.\n", + "fstore.preview(static_fs, static_df).head()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Save the feature set with the inferred schema to the feature store DB\n", + "static_fs.save()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature Set 2 - Network Device Metrics\n", + "The device metrics feature set holds the network-device telemetry, including:\n", + "- CPU Utilization\n", + "- Throughput\n", + "- Packet Loss\n", + "- Latency\n", + "\n", + "This feature set represents ingestion of real-time timeseries data via an incoming data stream. On top of this time series data we will perform rolling aggregations of `mean`, `min`, `max` on top of `1 hour` and `6 hours` windows.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "mlrun-flow\n", + "\n", + "\n", + "\n", + "_start\n", + "\n", + "start\n", + "\n", + "\n", + "\n", + "Aggregates\n", + "\n", + "Aggregates\n", + "\n", + "\n", + "\n", + "_start->Aggregates\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "\n", + "Aggregates->parquet\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "nosql\n", + "\n", + "\n", + "nosql\n", + "\n", + "\n", + "\n", + "Aggregates->nosql\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define the feature set\n", + "device_metrics_set = fstore.FeatureSet(\"device_metrics\", \n", + " entities=[\"device\"], \n", + " timestamp_key='timestamp',\n", + " description=\"Collected network device metrics\")\n", + "\n", + "# Add aggregations to all metrics\n", + "metrics = ['cpu_utilization', 'throughput', 'latency', 'packet_loss']\n", + "for metric in metrics:\n", + " # add a specific metric aggregation\n", + " device_metrics_set.add_aggregation(name=metric, \n", + " column=metric, \n", + " operations=['avg', 'min', 'max'], \n", + " windows=['1h', '6h'], period='10m')\n", + "\n", + "# Add the default targets (Parquet / No-Sql) and plot the data pipeline (graph)\n", + "device_metrics_set.set_targets()\n", + "device_metrics_set.plot(with_targets=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
cpu_utilization_min_1hcpu_utilization_min_6hcpu_utilization_max_1hcpu_utilization_max_6hcpu_utilization_avg_1hcpu_utilization_avg_6hthroughput_min_1hthroughput_min_6hthroughput_max_1hthroughput_max_6h...packet_loss_min_6hpacket_loss_max_1hpacket_loss_max_6hpacket_loss_avg_1hpacket_loss_avg_6hcpu_utilizationlatencypacket_lossthroughputtimestamp
device
8936164993151100.0100.0100.0100.0100.0100.00.00.00.00.0...50.050.050.050.050.0100.0100.050.00.02022-01-04 23:12:59
0813098629185100.0100.0100.0100.0100.0100.00.00.00.00.0...50.050.050.050.050.0100.0100.050.00.02022-01-04 23:12:59
6013359346575100.0100.0100.0100.0100.0100.00.00.00.00.0...50.050.050.050.050.0100.0100.050.00.02022-01-04 23:12:59
9110707557378100.0100.0100.0100.0100.0100.00.00.00.00.0...50.050.050.050.050.0100.0100.050.00.02022-01-04 23:12:59
3420351336821100.0100.0100.0100.0100.0100.00.00.00.00.0...50.050.050.050.050.0100.0100.050.00.02022-01-04 23:12:59
\n", + "

5 rows × 29 columns

\n", + "
" + ], + "text/plain": [ + " cpu_utilization_min_1h cpu_utilization_min_6h \\\n", + "device \n", + "8936164993151 100.0 100.0 \n", + "0813098629185 100.0 100.0 \n", + "6013359346575 100.0 100.0 \n", + "9110707557378 100.0 100.0 \n", + "3420351336821 100.0 100.0 \n", + "\n", + " cpu_utilization_max_1h cpu_utilization_max_6h \\\n", + "device \n", + "8936164993151 100.0 100.0 \n", + "0813098629185 100.0 100.0 \n", + "6013359346575 100.0 100.0 \n", + "9110707557378 100.0 100.0 \n", + "3420351336821 100.0 100.0 \n", + "\n", + " cpu_utilization_avg_1h cpu_utilization_avg_6h \\\n", + "device \n", + "8936164993151 100.0 100.0 \n", + "0813098629185 100.0 100.0 \n", + "6013359346575 100.0 100.0 \n", + "9110707557378 100.0 100.0 \n", + "3420351336821 100.0 100.0 \n", + "\n", + " throughput_min_1h throughput_min_6h throughput_max_1h \\\n", + "device \n", + "8936164993151 0.0 0.0 0.0 \n", + "0813098629185 0.0 0.0 0.0 \n", + "6013359346575 0.0 0.0 0.0 \n", + "9110707557378 0.0 0.0 0.0 \n", + "3420351336821 0.0 0.0 0.0 \n", + "\n", + " throughput_max_6h ... packet_loss_min_6h packet_loss_max_1h \\\n", + "device ... \n", + "8936164993151 0.0 ... 50.0 50.0 \n", + "0813098629185 0.0 ... 50.0 50.0 \n", + "6013359346575 0.0 ... 50.0 50.0 \n", + "9110707557378 0.0 ... 50.0 50.0 \n", + "3420351336821 0.0 ... 50.0 50.0 \n", + "\n", + " packet_loss_max_6h packet_loss_avg_1h packet_loss_avg_6h \\\n", + "device \n", + "8936164993151 50.0 50.0 50.0 \n", + "0813098629185 50.0 50.0 50.0 \n", + "6013359346575 50.0 50.0 50.0 \n", + "9110707557378 50.0 50.0 50.0 \n", + "3420351336821 50.0 50.0 50.0 \n", + "\n", + " cpu_utilization latency packet_loss throughput \\\n", + "device \n", + "8936164993151 100.0 100.0 50.0 0.0 \n", + "0813098629185 100.0 100.0 50.0 0.0 \n", + "6013359346575 100.0 100.0 50.0 0.0 \n", + "9110707557378 100.0 100.0 50.0 0.0 \n", + "3420351336821 100.0 100.0 50.0 0.0 \n", + "\n", + " timestamp \n", + "device \n", + "8936164993151 2022-01-04 23:12:59 \n", + "0813098629185 2022-01-04 23:12:59 \n", + "6013359346575 2022-01-04 23:12:59 \n", + "9110707557378 2022-01-04 23:12:59 \n", + "3420351336821 2022-01-04 23:12:59 \n", + "\n", + "[5 rows x 29 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We preview and test the calculated features before the deployment \n", + "fstore.preview(device_metrics_set, metrics_df).head()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Save the device metrics feature set with the inferred schema\n", + "device_metrics_set.save()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature Set 3 - Network Device Labels\n", + "This feature set represents incoming failure labels for our devices. The labels are ingested through their own stream to simulate them arriving through a different process and apply the asof merging with the related metrics through the feature store.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "mlrun-flow\n", + "\n", + "\n", + "\n", + "_start\n", + "\n", + "start\n", + "\n", + "\n", + "\n", + "Aggregates\n", + "\n", + "Aggregates\n", + "\n", + "\n", + "\n", + "_start->Aggregates\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "parquet\n", + "\n", + "\n", + "\n", + "Aggregates->parquet\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "device_labels_set = fstore.FeatureSet(\"device_labels\", \n", + " entities=[fstore.Entity(\"device\")], \n", + " timestamp_key='timestamp',\n", + " description=\"Collected network device labels\")\n", + "\n", + "# Add aggregations\n", + "metrics = ['cpu_utilization']\n", + "for metric in metrics:\n", + " device_labels_set.add_aggregation(name=metric, \n", + " column=metric, \n", + " operations=['max'], \n", + " windows=['1h'], period='10m')\n", + "\n", + "# specify only Parquet (offline) target since its not used for real-time\n", + "device_labels_set.set_targets(['parquet'], with_defaults=False)\n", + "device_labels_set.plot(with_targets=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Converting input from bool to for compatibility.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
cpu_utilization_max_1hcpu_utilization_is_errorlatency_is_errorpacket_loss_is_errorthroughput_is_erroris_errortimestamp
device
8936164993151NaNTrueTrueTrueTrueTrue2022-01-04 23:12:59
0813098629185NaNTrueTrueTrueTrueTrue2022-01-04 23:12:59
6013359346575NaNTrueTrueTrueTrueTrue2022-01-04 23:12:59
9110707557378NaNTrueTrueTrueTrueTrue2022-01-04 23:12:59
3420351336821NaNTrueTrueTrueTrueTrue2022-01-04 23:12:59
\n", + "
" + ], + "text/plain": [ + " cpu_utilization_max_1h cpu_utilization_is_error \\\n", + "device \n", + "8936164993151 NaN True \n", + "0813098629185 NaN True \n", + "6013359346575 NaN True \n", + "9110707557378 NaN True \n", + "3420351336821 NaN True \n", + "\n", + " latency_is_error packet_loss_is_error throughput_is_error \\\n", + "device \n", + "8936164993151 True True True \n", + "0813098629185 True True True \n", + "6013359346575 True True True \n", + "9110707557378 True True True \n", + "3420351336821 True True True \n", + "\n", + " is_error timestamp \n", + "device \n", + "8936164993151 True 2022-01-04 23:12:59 \n", + "0813098629185 True 2022-01-04 23:12:59 \n", + "6013359346575 True 2022-01-04 23:12:59 \n", + "9110707557378 True 2022-01-04 23:12:59 \n", + "3420351336821 True 2022-01-04 23:12:59 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Preview and test the `device_labels_set` features\n", + "fstore.preview(device_labels_set, labels_df).head()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "device_labels_set.save()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "## Part 3: Ingest the features data using batch or real-time\n", + "\n", + "In order to use the features in training or serving we need to ingest the data into the feature store, there are 3 ways we can use:\n", + "1. Direct ingestion - ingest the data directly from the client/notebook (interactively) \n", + "2. Batch/scheduled ingestion - create a service/job which will ingest data from the source (e.g. file, DB, ..)\n", + "3. Real-time/Streaming ingestion - create an online service which accepts real-time events (from a stream, http, etc.) and push them into the feature store\n", + "\n", + "Direct and batch ingestion are achieved using the `ingest()` method, while real-time ingestion is done using the `deploy_ingestion_service()` method, we will demonstrate both methods in the following sections, the direct ingestion is great for development and testing while the real-time ingestion is mainly used in production." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Direct/batch ingestion of the sample data \n", + "\n", + "In order to run training or test our serving we need to ingest and transform the input datasets and store the results in the feature store, the simplest way is to use the `ingest()` method and specify the feature-set and the source (Dataframe, file, etc.).\n", + "\n", + "We can specify the desired target if we want to overwrite the default behaviour, e.g. set `targets=[ParquetTarget()]` to specify that the data will only be written to parquet files and will not be written to the NoSQL DB (meaning you cannot run real-time serving)\n", + "\n", + "The `ingest()` method have many other args/options, see the documentation for details.\n", + "\n", + "**Once the data is ingested we can run next [02-training-and-deployment](./02-training-and-deployment.ipynb) notebook**" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "from mlrun.datastore.targets import ParquetTarget\n", + "\n", + "# ingest the static device data\n", + "fstore.ingest(static_fs, static_df)\n", + "\n", + "# ingest the device metrics\n", + "fstore.ingest(device_metrics_set, metrics_df)\n", + "\n", + "# ingest the labels\n", + "_ = fstore.ingest(device_labels_set, labels_df, targets=[ParquetTarget()])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "### Real-time ingestion\n", + "\n", + "In production the data will arrive in real-time via a stream, the ingestion service will use real-time Nuclio functions which listen on the event stream or HTTP endpoint and ingest the data while running the set of real-time transformations and aggregations.\n", + "\n", + "To simulate the real-time streams, we create a real-time generator function which generates semi-random data and write it into streams, the feature ingestion services will read from those streams, transform the data and write the results in parallel into offline (parquet files) and online (NoSQL DB) data targets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create the real-time generator function\n", + "We deploy the [**metrics generator function**](src/generator.py) which generate the following simulated data: \n", + "- Static table with all the network devices, their model and manufacturing country\n", + "- Metrics stream with telemtry data\n", + "- Labels stream with labels for the device status\n", + "\n", + "The description and statistics for the simulated data are defined in the [**metrics configuration file**](./src/metric_configurations.yaml).\n", + "\n", + "The generator function will be deployed as a Nuclio function on top of our Kubernetes cluster and will be called every 1 minute as defined via the cron trigger to produce new data and push it to the relevant streams. Later on, MLRun's feature store ingestion functions will listen to these streams and tables and ingest the data to the feature store. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-05 23:06:04,118 [info] Starting remote function deploy\n", + "2022-01-05 23:06:04 (info) Deploying function\n", + "2022-01-05 23:06:04 (info) Building\n", + "2022-01-05 23:06:05 (info) Staging files and preparing base images\n", + "2022-01-05 23:06:05 (info) Building processor image\n", + "2022-01-05 23:06:19 (info) Build complete\n", + "2022-01-05 23:06:26 (info) Function deploy complete\n", + "> 2022-01-05 23:06:27,454 [info] successfully deployed function: {'internal_invocation_urls': ['nuclio-network-operations-admin-device-metrics-generator.default-tenant.svc.cluster.local:8080'], 'external_invocation_urls': ['network-operations-admin-device-metrics-generator-netw-sy6w7wpw.default-tenant.app.yh41.iguazio-cd1.com/']}\n" + ] + }, + { + "data": { + "text/plain": [ + "'http://network-operations-admin-device-metrics-generator-netw-sy6w7wpw.default-tenant.app.yh41.iguazio-cd1.com/'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mlrun import code_to_function, mount_v3io\n", + "import nuclio\n", + "\n", + "# Configure and deploy the metrics generator function\n", + "fn = project.get_function('metrics-generator')\n", + "\n", + "# Set the necessary pip installs and build commands to build the image\n", + "fn.spec.build.commands = ['pip install pytimeparse faker', \n", + " 'pip install -i https://test.pypi.org/simple/ v3io-generator']\n", + "\n", + "# Add a cron trigger to run the function every set interval\n", + "fn.add_trigger('cron', nuclio.triggers.CronTrigger(interval='1m'))\n", + "\n", + "# Deploy the function on top of our kubernetes cluster\n", + "fn.deploy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Deploy the device metrics feature set ingestion endpoint\n", + "\n", + "Next we deploy the device metrics feature set processing pipeline over real-time serverless (Nuclio) function, we specify the source as a `StreamSource` with the path to the `device_metrics_stream`, and can specify which fields are used to determine the index and timestamp key." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-05 23:06:27,765 [info] Starting remote function deploy\n", + "2022-01-05 23:06:27 (info) Deploying function\n", + "2022-01-05 23:06:27 (info) Building\n", + "2022-01-05 23:06:28 (info) Staging files and preparing base images\n", + "2022-01-05 23:06:28 (info) Building processor image\n", + "2022-01-05 23:06:29 (info) Build complete\n", + "2022-01-05 23:06:35 (info) Function deploy complete\n", + "> 2022-01-05 23:06:36,065 [info] successfully deployed function: {'internal_invocation_urls': ['nuclio-network-operations-admin-ingest-device-metrics.default-tenant.svc.cluster.local:8080'], 'external_invocation_urls': ['network-operations-admin-ingest-device-metrics-network-7pp2f6tl.default-tenant.app.yh41.iguazio-cd1.com/']}\n" + ] + } + ], + "source": [ + "# Define the V3IO Stream Source from which the events data (in json format) are read.\n", + "# we will define which fields in the json struct are used as `key` and `time` fields.\n", + "source = mlrun.datastore.sources.StreamSource(path=device_metrics_stream , key_field='device', time_field='timestamp')\n", + "\n", + "# Create a real-time serverless function definition to deploy the ingestion pipeline on.\n", + "# the serving runtimes enables the deployment of our feature set's computational graph\n", + "function = (mlrun.new_function('ingest-device-metrics', kind='serving')).with_code(body=\" \") #, image='mlrun/mlrun'\n", + "\n", + "# Create run configuration from the function\n", + "run_config = fstore.RunConfig(function=function)\n", + "\n", + "# Deploy the transactions feature set's ingestion service using the feature set\n", + "# and all the defined resources above.\n", + "device_metrics_set_endpoint = fstore.deploy_ingestion_service(featureset=device_metrics_set,\n", + " source=source,\n", + " run_config=run_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Deploy device labels endpoint\n", + "We deploy the device labels feature set processing pipeline over real-time serverless (Nuclio) function, we specify the source as a `StreamSource` with the path to the `device_labels_stream`, and can specify which fields are used to determine the index and timestamp key." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-05 23:06:36,332 [info] Starting remote function deploy\n", + "2022-01-05 23:06:36 (info) Deploying function\n", + "2022-01-05 23:06:36 (info) Building\n", + "2022-01-05 23:06:36 (info) Staging files and preparing base images\n", + "2022-01-05 23:06:36 (info) Building processor image\n", + "2022-01-05 23:06:38 (info) Build complete\n", + "2022-01-05 23:06:43 (info) Function deploy complete\n", + "> 2022-01-05 23:06:43,691 [info] successfully deployed function: {'internal_invocation_urls': ['nuclio-network-operations-admin-ingest-device-labels.default-tenant.svc.cluster.local:8080'], 'external_invocation_urls': ['network-operations-admin-ingest-device-labels-network-j1ebsp9b.default-tenant.app.yh41.iguazio-cd1.com/']}\n" + ] + } + ], + "source": [ + "# Define the V3IO Stream Source from which the events data (in json format) are read.\n", + "source = mlrun.datastore.sources.StreamSource(path=device_labels_stream , key_field='device', time_field='timestamp')\n", + "\n", + "# Create a real-time serverless function definition to deploy the ingestion pipeline on.\n", + "# the serving runtimes enables the deployment of our feature set's computational graph\n", + "function = (mlrun.new_function('ingest-device-labels', kind='serving')).with_code(body=\" \")\n", + "\n", + "# Create run configuration from the function\n", + "run_config = fstore.RunConfig(function=function)\n", + "\n", + "# Deploy the transactions feature set's ingestion service using the feature set\n", + "# and all the defined resources above.\n", + "device_labels_set_endpoint = fstore.deploy_ingestion_service(featureset=device_labels_set,\n", + " source=source,\n", + " run_config=run_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "**Done! now we can move to the next [02-training-and-deployment](./02-training-and-deployment.ipynb) notebook**" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/network-operations/02-training-and-deployment.ipynb b/network-operations/02-training-and-deployment.ipynb new file mode 100644 index 00000000..fe23db22 --- /dev/null +++ b/network-operations/02-training-and-deployment.ipynb @@ -0,0 +1,647 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Network Operations Demo - Train, Test, and Deploy\n", + "\n", + "This project demonstrates how to build an automated machine-learning (ML) pipeline for predicting network outages based on network-device telemetry. This notebook is the second part (out of 2) of the demo, in this part we demonstrate how to train, test and deploy a model and use offline and real-time data from the feature store.\n", + "\n", + "**In this notebook :**\n", + "* **Create a Feature Vector which consist of data joined from the three feature sets we created**\n", + "* **Create an offline dataset from the feature vector to feed our ML training process**\n", + "* **Run automated ML Pipeline which train, test, and deploy the model**\n", + "* **Test the deployed real-time serving function**\n", + "\n", + "When we finish this notebook, we should have a running network-device failure prediction system." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get and init the MLRun project" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-06 00:43:34,055 [info] loaded project network-operations from MLRun DB\n" + ] + } + ], + "source": [ + "import os\n", + "import numpy as np\n", + "import mlrun\n", + "import mlrun.feature_store as fstore\n", + "\n", + "# Create the project\n", + "project = mlrun.get_or_create_project('network-operations', \"./\", user_project=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a new Feature Vector\n", + "We want to create a single dataset that will contain data from the static devices dataset, the device metrics, and the labels.\n", + "We define a **Feature Vector** and specify the desired features, when the vector will be retrived the feature store will automatically and correctly join the data from the different feature sets based on the entity (index) keys and the timestamp values.\n", + "\n", + "We will now define and save the `device_features` feature vector" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the `device_features` Feature Vector\n", + "fv = fstore.FeatureVector('device_features',\n", + " features=['device_metrics.*', 'static.*'], \n", + " label_feature='device_labels.is_error')\n", + "\n", + "# Save the Feature Vector to MLRun's feature store DB\n", + "fv.save()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get an offline dataset for the feature vector\n", + "Once we have defined our feature vector and we ingested some data, we can request the feature store to create an offline dataset for us - e.g. a snapshot of the data between the dates we want available to be loaded as parquet or csv files or as a pandas Dataframe.\n", + "\n", + "We can later reference the created offline dataset via a special artifact url (`fv.url`).\n", + "\n", + "**Make sure you run this AFTER the feature set data was ingested (using batch or real-time)**" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-06 23:41:56,892 [info] wrote target: {'name': 'parquet', 'kind': 'parquet', 'path': 'v3io:///projects/network-operations-admin/FeatureStore/device_features/parquet/vectors/device_features-latest.parquet', 'status': 'ready', 'updated': '2022-01-06T23:41:56.891986+00:00', 'size': 1867880}\n", + "\n", + "Training set shape: (16600, 46)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
cpu_utilization_avg_1hcpu_utilization_avg_6hcpu_utilization_min_1hcpu_utilization_min_6hcpu_utilization_max_1hcpu_utilization_max_6hthroughput_avg_1hthroughput_avg_6hthroughput_min_1hthroughput_min_6h...model_1model_2model_3model_4model_5model_6model_7model_8model_9is_error
058.69321458.69321458.69321458.69321458.69321458.693214252.768164252.768164252.768164252.768164...000000100False
167.60944967.60944967.60944967.60944967.60944967.609449252.041031252.041031252.041031252.041031...000000010False
284.43536784.43536784.43536784.43536784.43536784.435367239.414858239.414858239.414858239.414858...000000100False
376.54439476.54439476.54439476.54439476.54439476.544394217.954234217.954234217.954234217.954234...010000000False
478.24536278.24536278.24536278.24536278.24536278.245362227.595164227.595164227.595164227.595164...000000001False
\n", + "

5 rows × 46 columns

\n", + "
" + ], + "text/plain": [ + " cpu_utilization_avg_1h cpu_utilization_avg_6h cpu_utilization_min_1h \\\n", + "0 58.693214 58.693214 58.693214 \n", + "1 67.609449 67.609449 67.609449 \n", + "2 84.435367 84.435367 84.435367 \n", + "3 76.544394 76.544394 76.544394 \n", + "4 78.245362 78.245362 78.245362 \n", + "\n", + " cpu_utilization_min_6h cpu_utilization_max_1h cpu_utilization_max_6h \\\n", + "0 58.693214 58.693214 58.693214 \n", + "1 67.609449 67.609449 67.609449 \n", + "2 84.435367 84.435367 84.435367 \n", + "3 76.544394 76.544394 76.544394 \n", + "4 78.245362 78.245362 78.245362 \n", + "\n", + " throughput_avg_1h throughput_avg_6h throughput_min_1h throughput_min_6h \\\n", + "0 252.768164 252.768164 252.768164 252.768164 \n", + "1 252.041031 252.041031 252.041031 252.041031 \n", + "2 239.414858 239.414858 239.414858 239.414858 \n", + "3 217.954234 217.954234 217.954234 217.954234 \n", + "4 227.595164 227.595164 227.595164 227.595164 \n", + "\n", + " ... model_1 model_2 model_3 model_4 model_5 model_6 model_7 \\\n", + "0 ... 0 0 0 0 0 0 1 \n", + "1 ... 0 0 0 0 0 0 0 \n", + "2 ... 0 0 0 0 0 0 1 \n", + "3 ... 0 1 0 0 0 0 0 \n", + "4 ... 0 0 0 0 0 0 0 \n", + "\n", + " model_8 model_9 is_error \n", + "0 0 0 False \n", + "1 1 0 False \n", + "2 0 0 False \n", + "3 0 0 False \n", + "4 0 1 False \n", + "\n", + "[5 rows x 46 columns]" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Request (get or create) the offline dataset from the feature store and save to a parquet target\n", + "dataset_ref = fstore.get_offline_features(fv, target=mlrun.datastore.targets.ParquetTarget())\n", + "\n", + "# Get the generated offline dataset as a pandas DataFrame\n", + "dataset = dataset_ref.to_dataframe()\n", + "print(\"\\nTraining set shape:\", dataset.shape)\n", + "dataset.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([False, True])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# verify that the dataset contain proper labels (must have both True & False values)\n", + "dataset.is_error.unique()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model training and deployment using the feature vector\n", + "Now that we have our dataset ready for training, we need to define our model training, testing and deployment process.\n", + "\n", + "We build an automated ML pipeline which uses pre-baked serverless training, testing and serving functions from [MLRun's functions marketplace](https://www.mlrun.org/marketplace/), the pipeline has three steps:\n", + "* train a model using data from the feature vector we created and save it to the model registry\n", + "* run model test/evaluation with portion of the data\n", + "* deploy a real-time serving function which use the newly trained model and enrich/impute the features with data from the real-time feature vector \n", + "\n", + "You can see the [**workflow code**](./src/workflow.py), we can run this workflow locally, in a CI/CD framework, or over Kubeflow. In practice we may create different workflows for development and production.\n", + "\n", + "The workflow/pipeline can be executed using the MLRun SDK (`project.run()` method) or using CLI commands (`mlrun project`), and can run directly from the source repo (GIT), see details in MLRun [**Projects and Automation documentation**](https://docs.mlrun.org/en/latest/projects/overview.html).\n", + "\n", + "We run the workflow and can set arguments and destination for the different artifacts, the pipeline progress will be shown in the notebook, alternatively we can check the progress, logs, ertifacts, etc. in MLRun UI.\n", + "\n", + "If we want to run the same using CLI we need to type:\n", + "\n", + "```python\n", + " mlrun project -n myproj -r ./src/workflow.py .\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "kfp\n", + "\n", + "\n", + "\n", + "netops-demo-prxk7-1212022885\n", + "\n", + "test\n", + "\n", + "\n", + "\n", + "netops-demo-prxk7-3972841449\n", + "\n", + "train\n", + "\n", + "\n", + "\n", + "netops-demo-prxk7-3972841449->netops-demo-prxk7-1212022885\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "netops-demo-prxk7-4190852259\n", + "\n", + "\n", + "\n", + "\n", + "deploy-serving\n", + "\n", + "\n", + "\n", + "netops-demo-prxk7-3972841449->netops-demo-prxk7-4190852259\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "

Run Results

Workflow cfa99d6f-300a-4254-b3c4-7625ecf25e85 finished, state=Succeeded
click the hyper links below to see detailed results
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidstartstatenameresultsartifacts
Jan 06 00:22:32completedtest
accuracy=0.9857142857142858
test-error=0.014285714285714285
rocauc=0.9991464663707749
brier_score=0.010732142857142858
f1-score=0.9702970297029703
precision_score=0.9702970297029703
recall_score=0.9702970297029703
probability-calibration
confusion-matrix
feature-importances
precision-recall-binary
roc-binary
test_set_preds
Jan 06 00:22:16completedtrain
accuracy=1.0
test-error=0.0
rocauc=1.0
brier_score=0.0016115646258503403
f1-score=1.0
precision_score=1.0
recall_score=1.0
test_set
probability-calibration
confusion-matrix
feature-importances
precision-recall-binary
roc-binary
model
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import os\n", + "pipeline_path = mlrun.mlconf.artifact_path\n", + "model_name = \"netops\"\n", + "\n", + "# run the workflow\n", + "run_id = project.run(\n", + " workflow_path=\"./src/workflow.py\",\n", + " arguments={\"model_name\": model_name}, \n", + " artifact_path=os.path.join(pipeline_path, \"pipeline\", '{{workflow.uid}}'),\n", + " watch=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test the Live Model Endpoint\n", + "To test the live model endpoint we will first grab a list of IDs from the static feature set we produced. We will then use this IDs and send them through a loop to our live endpoint." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Grab IDs from the static devices table" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Devices sample: ['8936164993151' '0813098629185' '6013359346575' '9110707557378']\n" + ] + } + ], + "source": [ + "# Load the static feature set\n", + "fset = fstore.get_feature_set('static')\n", + "\n", + "# Get a dataframe from the feature set\n", + "devices = fset.to_dataframe().reset_index()['device'].values\n", + "print('Devices sample:', devices[:4])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Send a sample ID to the model endpoint" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-06 00:09:54,691 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n" + ] + }, + { + "data": { + "text/plain": [ + "{'id': 'aa139402-400e-4d3d-8d60-0e12210b0487',\n", + " 'model_name': 'netops',\n", + " 'outputs': [False]}" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "serving_fn = project.get_function('serving')\n", + "serving_fn.invoke(path=f'/v2/models/{model_name}/infer', body={'inputs': [[devices[0]]]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Continously send IDs to the model " + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> 2022-01-06 00:09:56,243 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n", + "Sent: [['4753365617775'], ['7709108040279']]\n", + "Response: {'id': 'b30382b7-270d-4461-95ae-c4b800f67aca', 'model_name': 'netops', 'outputs': [False, False]}\n", + "Predictions: [(['4753365617775'], False), (['7709108040279'], False)]\n", + "> 2022-01-06 00:10:06,299 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n", + "Sent: [['2238314358399'], ['8936164993151']]\n", + "Response: {'id': '523356b4-90e2-41f5-b273-0da9e97327ab', 'model_name': 'netops', 'outputs': [False, False]}\n", + "Predictions: [(['2238314358399'], False), (['8936164993151'], False)]\n", + "> 2022-01-06 00:10:16,375 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n", + "Sent: [['8936164993151'], ['9755997921470']]\n", + "Response: {'id': '23d662a2-0827-4927-8d8a-745c54f0fe0a', 'model_name': 'netops', 'outputs': [False, False]}\n", + "Predictions: [(['8936164993151'], False), (['9755997921470'], False)]\n", + "> 2022-01-06 00:10:26,415 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n", + "Sent: [['0813098629185'], ['7709108040279']]\n", + "Response: {'id': '66a54748-f5d7-4dfd-9053-fe8654dc829d', 'model_name': 'netops', 'outputs': [True, False]}\n", + "Predictions: [(['0813098629185'], True), (['7709108040279'], False)]\n", + "> 2022-01-06 00:10:36,455 [info] invoking function: {'method': 'POST', 'path': 'http://nuclio-network-operations-admin-serving.default-tenant.svc.cluster.local:8080/v2/models/netops/infer'}\n", + "Sent: [['8268799948646'], ['0813098629185']]\n", + "Response: {'id': '3e961d09-3825-431f-a4e7-1f86c4571d44', 'model_name': 'netops', 'outputs': [True, False]}\n", + "Predictions: [(['8268799948646'], True), (['0813098629185'], False)]\n" + ] + } + ], + "source": [ + "import random\n", + "import time\n", + "\n", + "MSGS_TO_SEND = 5\n", + "IDS_PER_MSG = 2\n", + "TIMEOUT_BETWEEN_SENDS = 10\n", + "for i in range(MSGS_TO_SEND):\n", + " ids_for_prediction = [[random.choice(devices)] for i in range(IDS_PER_MSG)]\n", + " resp = serving_fn.invoke(path=f'/v2/models/{model_name}/infer', body={'inputs': ids_for_prediction})\n", + " print('Sent:', ids_for_prediction)\n", + " print('Response:', resp)\n", + " print('Predictions:', list(zip(ids_for_prediction, resp['outputs'])))\n", + " time.sleep(TIMEOUT_BETWEEN_SENDS)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/network-operations/README.md b/network-operations/README.md new file mode 100644 index 00000000..2be7039a --- /dev/null +++ b/network-operations/README.md @@ -0,0 +1,97 @@ +# NetOps Demo: Predictive Network Operations/Telemetry + +[Overview](#overview) | [Running the Demo](#demo-run) | [Demo Flow](#demo-flow) | [Notebooks and Code](#notebooks-and-code) + +## Overview + +This demo demonstrates how to build an automated machine-learning (ML) pipeline for predicting network outages based on network-device telemetry, also known as Network Operations (NetOps). +The demo implements both model training and inference, including model monitoring and concept-drift detection. +The demo simulates telemetry network data for running the pipeline. + +The demo demonstrates how to + +- Manage MLRun projects. +- Use GitHub as a source for functions to use in pipeline workflows. +- Use MLRun logging to track results and artifacts. +- Use MLRun's Feature Store for feature engineering. +- Deploy a live-endpoints production pipeline. + +> **Note:** The demo applications are tested on the [Iguazio Data Science Platform](https://www.iguazio.com) ("the platform"), and use the platform's data store ("v3io"). +> Contact [Iguazio support](mailto:support@iguazio.com) to request a free trial of the platform. + + +## Running the Demo + + +### Prerequisites + +Before you begin, ensure that you have the following: + +- A [Kubernetes](https://kubernetes.io/) and [Nuclio](https://nuclio.io/). +- An installation of MLRun with a running MLRun service and an MLRun dashboard. + See details in the [MLRun README](https://github.com/mlrun/mlrun). + + +### Execution Steps + +Execute the following steps to run the demo: + +1. Fork the [mlrun/demos](https://github.com/mlrun/demos) Git repository to your GitHub account. + +2. In a client or notebook that is properly configured with MLRun, run the following code; replace `` with the name of your mlrun/demos GitHub fork: + ``` + mlrun project demos/network-operations/ -u git://github.com//demos/network-operations.git + ``` + +3. Run the [**01-ingest.ipynb**](01-ingest.ipynb) notebook to create the feature sets and deploy the data generator and live ingestion endpoints. + +4. Open the [**02-fv-training.ipynb**](02-fv-training.ipynb) notebook and follow the instructions to create a Feature Vector, train a model and deploy it for live real-time predictions. + + +## Demo Flow + +The demo implements three main operations: + +- [**Feature Ingestion & Engineering**](#feature-creation) — Create a feature set for the network-device telemetry and deploy live ingestion endpoints. +- [**Model Training**](#model-training) — Create a Feature Vector from the available features and retrieve a Dataset. Create a model from this dataset for network device failure prediction. +- [**Model Deployment & Monitoring**](#model-Deployment-and-monitoring) — Deploy the generated model to a live endpoint and monitor it through grafana. + + +### Feature Creation + +In the feature creation stage we use MLRun's Feature Store to first define the features we are want to ingest and the operations we want to apply to them. +In this demo we create 3 unique feature sets: +- **network-device metrics** — real-time network-device telemtry data such as cpu utilization, packet loss, latency, throughput. We will run real-time aggregations on top of these metrics with different time windows. +- **static network-device data** — static device data such as his model and manufacturing country. We will one-hot-encode the categorical features to have them ready for model ingestion. +- **network-device failure label indicator** — a stream representing the label responses. The Feature Store will later match them with the correct sample for dataset creation. + + + +### Model Training + +In the model training stage we will use MLRun's Feature Store to define the Feature Vector we want to use for our model from the features we created and the labels we ingest. We will create a dataset from the feature vector and feed it to our [sklearn classifier training function](https://github.com/mlrun/functions/blob/master/sklearn_classifier/sklearn_classifier.ipynb) from the [MLRun marketplace functions hub](https://github.com/mlrun/functions) and create a network-device failure prediction model. + + + +### Model Deployment & Monitoring + +In this stage we will use MLRun's model server to deploy our trained model to production. We will use the `EnrichmentModelRouter` to retrieve the online feature vector automatically upon receiving a network-device id and predict device failures. + +We will then be able to use MLRun's Model Montoring through Grafana to see our model performance, feature analytics and drift metrics. + + + +## Notebooks and Code + + +### Notebooks + +- [**01-ingest.ipynb**](01-ingest.ipynb) — the 1st demo step notebook. including project setup, genetaor deployment, feature sets creation and deployment. +- [**02-training.ipynb**](02-fv-training.ipynb) — the 2nd demo step notebook. including feature vecto creation, dataset creation, model training, deployment and testing. +- [**src/generator.py**](src/generator.py) — a nuclio function to generate live network-device telemetry and publish it to a v3io stream. + + +### Project-Configuration Files + +- [**src/metric_configurations.yaml**](src/metric_configurations.yaml) — a data-generator configurations file. defines the metrics for the demo's generator network-device telemetry data. + diff --git a/network-operations/src/generator.py b/network-operations/src/generator.py new file mode 100644 index 00000000..b14941a3 --- /dev/null +++ b/network-operations/src/generator.py @@ -0,0 +1,188 @@ +import os +import yaml +import pandas as pd +import datetime +import json +import mlrun +from typing import Dict +import copy +import random +import mlrun.feature_store as fstore + +# Data generator +from v3io_generator import metrics_generator, deployment_generator + + +def _create_deployment(country, models, project=None): + print("creating deployment") + # Create meta-data factory + dep_gen = deployment_generator.deployment_generator() + faker = dep_gen.get_faker() + + # Design meta-data + num_devices = 20 + dep_gen.add_level("device", number=num_devices, level_type=faker.msisdn) + + # Create meta-data + deployment_df = dep_gen.generate_deployment() + + # Add static metrics + static_df = copy.copy(deployment_df) + static_df["country"] = [random.choice(country) for i in range(num_devices)] + static_df["model"] = [random.choice(models) for i in range(num_devices)] + + # Add stub data + deployment_df["cpu_utilization"] = 70 + deployment_df["latency"] = 0 + deployment_df["packet_loss"] = 0 + deployment_df["throughput"] = 290 + + if project: + # save the simulated dataset for future use + project.log_dataset("deployment", df=deployment_df, format="parquet") + project.log_dataset("static", df=static_df, format="parquet") + + return deployment_df, static_df + + +def get_or_create_deployment(country, models, project=None, create_new=False): + if project and not create_new: + try: + static_df = mlrun.get_dataitem(project.get_artifact_uri("static")).as_df() + deployment_df = mlrun.get_dataitem( + project.get_artifact_uri("deployment") + ).as_df() + return deployment_df.reset_index(), static_df + except: + pass + + # Create deployment + return _create_deployment(country, models, project) + + +def get_data_from_sample(context, data: Dict, as_df: bool = False) -> Dict: + deployment_levels = ( + context.deployment_levels + if context and hasattr(context, "deployment_levels") + else ["device"] + ) + label_col_indicator = ( + context.label_col_indicator + if context and hasattr(context, "label_col_indicator") + else "is_error" + ) + base_columns = deployment_levels + ["timestamp"] + metrics = {k: v for k, v in data.items() if label_col_indicator not in k} + labels = { + k: v for k, v in data.items() if label_col_indicator in k or k in base_columns + } + + if as_df: + metrics = pd.DataFrame.from_dict(metrics) + labels = pd.DataFrame.from_dict(labels) + + return metrics, labels + + +def get_sample( + metrics_configuration: dict, + as_df: bool = True, + project=None, + ticks=5, + create_new=False, +): + deployment_df, static_df = get_or_create_deployment( + metrics_configuration["country"], + metrics_configuration["models"], + project=project, + create_new=create_new, + ) + initial_timestamp = int( + os.getenv( + "initial_timestamp", + (datetime.datetime.now() - datetime.timedelta(days=1)).timestamp(), + ) + ) + met_gen = metrics_generator.Generator_df( + metrics_configuration, + user_hierarchy=deployment_df, + initial_timestamp=initial_timestamp, + ) + + generator = met_gen.generate(as_df=True) + for i in range(100): + sample = next(generator) + metrics_df, labels_df = get_data_from_sample(None, sample, as_df) + for i in range(ticks): + sample = next(generator) + metrics2_df, labels2_df = get_data_from_sample(None, sample, as_df) + metrics_df = metrics_df.append(metrics2_df) + labels_df = labels_df.append(labels2_df) + return metrics_df, labels_df, static_df + + +def init_context(context): + + # Get metrics configuration + project = mlrun.get_run_db().get_project(mlrun.mlconf.default_project) + params = project.params + config = mlrun.get_dataitem(params["metrics_configuration_uri"]).get() + metrics_configuration = yaml.safe_load(config) + + # Generate or create deployment + deployment_df, static_deployment = get_or_create_deployment( + metrics_configuration["country"], + metrics_configuration["models"], + project=project, + ) + + static_set = fstore.get_feature_set("static") + fstore.ingest(static_set, static_deployment) + + setattr(context, "label_col_indicator", "error") + setattr(context, "deployment_levels", ["device"]) + + # Create metrics generator + initial_timestamp = int( + os.getenv( + "initial_timestamp", + (datetime.datetime.now() - datetime.timedelta(days=1)).timestamp(), + ) + ) + met_gen = metrics_generator.Generator_df( + metrics_configuration, + user_hierarchy=deployment_df, + initial_timestamp=initial_timestamp, + ) + generator = met_gen.generate(as_df=True) + setattr(context, "metrics_generator", generator) + + # Metrics pusher + device_metrics_pusher = mlrun.datastore.get_stream_pusher( + params["device_metrics_stream"] + ) + setattr(context, "device_metrics_pusher", device_metrics_pusher) + + # Labels pusher + device_labels_pusher = mlrun.datastore.get_stream_pusher( + params["device_labels_stream"] + ) + setattr(context, "device_labels_pusher", device_labels_pusher) + + +def handler(context, event): + for i in range(10): + # Generate sample from all devices in the network + device_metrics = json.loads( + next(context.metrics_generator) + .reset_index() + .to_json(orient="records", date_unit="s") + ) + for metric in device_metrics: + # Split the data to features and labels + metrics, labels = get_data_from_sample(context, metric) + + # Push the data to the appropriate streams + context.device_metrics_pusher.push(metrics) + context.device_labels_pusher.push(labels) + return metrics, labels diff --git a/network-operations/src/metric_configurations.yaml b/network-operations/src/metric_configurations.yaml new file mode 100644 index 00000000..f3b7e923 --- /dev/null +++ b/network-operations/src/metric_configurations.yaml @@ -0,0 +1,49 @@ +errors: {length_in_ticks: 50, rate_in_ticks: 5} +timestamps: {interval: 5s, stochastic_interval: true} +models: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +country: [A, B, C, D, E, F, G] +metrics: + cpu_utilization: + accuracy: 2 + distribution: normal + distribution_params: {mu: 70, noise: 0, sigma: 10} + is_threshold_below: true + past_based_value: false + produce_max: false + produce_min: false + validation: + distribution: {max: 1, min: -1, validate: false} + metric: {max: 100, min: 0, validate: true} + latency: + accuracy: 2 + distribution: normal + distribution_params: {mu: 0, noise: 0, sigma: 5} + is_threshold_below: true + past_based_value: false + produce_max: false + produce_min: false + validation: + distribution: {max: 1, min: -1, validate: false} + metric: {max: 100, min: 0, validate: true} + packet_loss: + accuracy: 0 + distribution: normal + distribution_params: {mu: 0, noise: 0, sigma: 2} + is_threshold_below: true + past_based_value: false + produce_max: false + produce_min: false + validation: + distribution: {max: 1, min: -1, validate: false} + metric: {max: 50, min: 0, validate: true} + throughput: + accuracy: 2 + distribution: normal + distribution_params: {mu: 250, noise: 0, sigma: 20} + is_threshold_below: false + past_based_value: false + produce_max: false + produce_min: false + validation: + distribution: {max: 1, min: -1, validate: false} + metric: {max: 300, min: 0, validate: true} diff --git a/network-operations/src/workflow.py b/network-operations/src/workflow.py new file mode 100644 index 00000000..4918131d --- /dev/null +++ b/network-operations/src/workflow.py @@ -0,0 +1,52 @@ +import mlrun +from kfp import dsl + + +# Create a Kubeflow Pipelines pipeline +@dsl.pipeline(name="netops-demo") +def kfpipeline( + label_column="is_error", + model_name="netops", + model_pkg_class="sklearn.ensemble.RandomForestClassifier", +): + project = mlrun.get_current_project() + feature_vector_uri = project.get_artifact_uri("device_features", "feature-vector") + + # Train a model + train = mlrun.run_function( + mlrun.import_function("hub://sklearn_classifier", new_name="train"), + params={"label_column": label_column, "model_pkg_class": model_pkg_class}, + inputs={"dataset": feature_vector_uri}, + outputs=["model", "test_set"], + ) + + # Test and visualize the model + mlrun.run_function( + mlrun.import_function("hub://test_classifier", new_name="test"), + params={"label_column": label_column}, + inputs={ + "models_path": train.outputs["model"], + "test_set": train.outputs["test_set"], + }, + ) + + # import the standard ML model serving function + serving_fn = mlrun.import_function("hub://v2_model_server", new_name="serving") + + # set the serving topology to simple model routing + # with data enrichment and imputing from the feature vector + serving_fn.set_topology( + "router", + "mlrun.serving.routers.EnrichmentModelRouter", + feature_vector_uri=str(feature_vector_uri), + impute_policy={"*": "$mean"}, + ) + + # Add model monitoring to the model + serving_fn.set_tracking() + + # Deploy the trained model as a serverless function + mlrun.deploy_function( + serving_fn, + models=[{"key": str(model_name), "model_path": train.outputs["model"]}], + ) From b239058d1b94fcd1bd2d2c9b1940e1a62e5d27be Mon Sep 17 00:00:00 2001 From: yaron haviv Date: Fri, 7 Jan 2022 02:55:24 +0200 Subject: [PATCH 2/6] update --- network-operations/01-ingest.ipynb | 6 +++--- network-operations/README.md | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/network-operations/01-ingest.ipynb b/network-operations/01-ingest.ipynb index f43c991d..4b2a8e1d 100644 --- a/network-operations/01-ingest.ipynb +++ b/network-operations/01-ingest.ipynb @@ -15,9 +15,9 @@ "5. Real-time model and metrics monitoring, drift detection\n", "\n", "**In this notebook:**\n", - "* [**Part 1: Create and configure an MLRun project**]()\n", - "* [**Part 2: Define and test the feature engineering pipeline**]()\n", - "* [**Part 3: Ingest the features data using batch or real-time**]()\n", + "* [**Part 1: Create and configure an MLRun project**](#Part-1:-Create-and-configure-an-MLRun-project)\n", + "* [**Part 2: Define and test the feature engineering pipeline**](#Part-2:-Define-and-test-the-feature-engineering-pipelines)\n", + "* [**Part 3: Ingest the features data using batch or real-time**](#Part-3:-Ingest-the-features-data-using-batch-or-real-time)\n", "\n", "in the next [**02-training-and-deployment**](./02-training-and-deployment.ipynb) notebook you can learn how to build automated pipelines which train, test, and deploy models using data from the feature store.\n", "\n", diff --git a/network-operations/README.md b/network-operations/README.md index 2be7039a..8f39353e 100644 --- a/network-operations/README.md +++ b/network-operations/README.md @@ -45,16 +45,17 @@ Execute the following steps to run the demo: 3. Run the [**01-ingest.ipynb**](01-ingest.ipynb) notebook to create the feature sets and deploy the data generator and live ingestion endpoints. -4. Open the [**02-fv-training.ipynb**](02-fv-training.ipynb) notebook and follow the instructions to create a Feature Vector, train a model and deploy it for live real-time predictions. +4. Open the [**02-training-and-deployment.ipynb**](02-training-and-deployment.ipynb) notebook and follow the instructions to create a Feature Vector, and run an automated pipeline to train a model and deploy it for live real-time predictions. ## Demo Flow -The demo implements three main operations: - -- [**Feature Ingestion & Engineering**](#feature-creation) — Create a feature set for the network-device telemetry and deploy live ingestion endpoints. -- [**Model Training**](#model-training) — Create a Feature Vector from the available features and retrieve a Dataset. Create a model from this dataset for network device failure prediction. -- [**Model Deployment & Monitoring**](#model-Deployment-and-monitoring) — Deploy the generated model to a live endpoint and monitor it through grafana. +The demo consists of: +1. Building and testing features from three sources (device metadata, real-time device metrics, and real-time device labels) using the feature store +2. Ingesting the data using batch (for testing) or real-time (for production) +3. Train and test the model with data from the feature-store +4. Deploying the model as part of a real-time feature engineering and inference pipeline +5. Real-time model and metrics monitoring, drift detection ### Feature Creation @@ -84,12 +85,12 @@ We will then be able to use MLRun's Model Montoring through Grafana to see our m ## Notebooks and Code -### Notebooks +### Notebooks and Code - [**01-ingest.ipynb**](01-ingest.ipynb) — the 1st demo step notebook. including project setup, genetaor deployment, feature sets creation and deployment. -- [**02-training.ipynb**](02-fv-training.ipynb) — the 2nd demo step notebook. including feature vecto creation, dataset creation, model training, deployment and testing. +- [**02-training-and-deployment.ipynb**](02-training-and-deployment.ipynb) — the 2nd demo step notebook. including feature vecto creation, dataset creation, model training, deployment and testing. - [**src/generator.py**](src/generator.py) — a nuclio function to generate live network-device telemetry and publish it to a v3io stream. - +- [**src/workflow.py**](src/workflow.py) — ML Pipeline for training, tests, and model deployment ### Project-Configuration Files From a7efc1b62b2940e85f69d11c4b28f83dae74add1 Mon Sep 17 00:00:00 2001 From: yaron haviv Date: Sun, 9 Jan 2022 17:08:02 +0200 Subject: [PATCH 3/6] configurable fields --- network-operations/01-ingest.ipynb | 8 +--- network-operations/src/generator.py | 44 +++++++++++-------- .../src/metric_configurations.yaml | 9 +++- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/network-operations/01-ingest.ipynb b/network-operations/01-ingest.ipynb index 4b2a8e1d..e86d3098 100644 --- a/network-operations/01-ingest.ipynb +++ b/network-operations/01-ingest.ipynb @@ -279,12 +279,8 @@ " entities=['device'], \n", " description='Static data for the devices')\n", "\n", - "# Setup the One Hot Encoder\n", - "one_hot_encoder_mapping = {'country': metrics_configuration['country'],\n", - " 'model': metrics_configuration['models']}\n", - "\n", - "# Append the one hot encoder to the feature set's processing graph\n", - "static_fs.graph.to(OneHotEncoder(mapping=one_hot_encoder_mapping))\n", + "# Append the one hot encoder to the feature set's processing graph, use values from configuration\n", + "static_fs.graph.to(OneHotEncoder(mapping=metrics_configuration['static']))\n", "\n", "# Set the default targets for the feature set (parquet & no-sql)\n", "static_fs.set_targets()\n", diff --git a/network-operations/src/generator.py b/network-operations/src/generator.py index b14941a3..e3f2f582 100644 --- a/network-operations/src/generator.py +++ b/network-operations/src/generator.py @@ -13,29 +13,42 @@ from v3io_generator import metrics_generator, deployment_generator -def _create_deployment(country, models, project=None): +def _create_deployment(metrics_configuration, project=None): print("creating deployment") # Create meta-data factory dep_gen = deployment_generator.deployment_generator() faker = dep_gen.get_faker() # Design meta-data - num_devices = 20 - dep_gen.add_level("device", number=num_devices, level_type=faker.msisdn) + deployment_configs = metrics_configuration["deployment"] + for level, level_configs in deployment_configs.items(): + dep_gen.add_level( + level, + number=level_configs["num_items"], + level_type=getattr(faker, level_configs["faker"]), + ) # Create meta-data deployment_df = dep_gen.generate_deployment() # Add static metrics static_df = copy.copy(deployment_df) - static_df["country"] = [random.choice(country) for i in range(num_devices)] - static_df["model"] = [random.choice(models) for i in range(num_devices)] + static_data_configs = metrics_configuration["static"] + for static_feature, static_feature_values in static_data_configs.items(): + if str(static_feature_values).startswith("range"): + static_df[static_feature] = [ + random.choice(eval(static_feature_values)) + for i in range(static_df.shape[0]) + ] + else: + static_df[static_feature] = [ + random.choice(static_feature_values) for i in range(static_df.shape[0]) + ] # Add stub data - deployment_df["cpu_utilization"] = 70 - deployment_df["latency"] = 0 - deployment_df["packet_loss"] = 0 - deployment_df["throughput"] = 290 + for metric, params in metrics_configuration["metrics"].items(): + value = params["distribution_params"].get("mu", 0) + deployment_df[metric] = value if project: # save the simulated dataset for future use @@ -45,7 +58,7 @@ def _create_deployment(country, models, project=None): return deployment_df, static_df -def get_or_create_deployment(country, models, project=None, create_new=False): +def get_or_create_deployment(metrics_configuration, project=None, create_new=False): if project and not create_new: try: static_df = mlrun.get_dataitem(project.get_artifact_uri("static")).as_df() @@ -57,7 +70,7 @@ def get_or_create_deployment(country, models, project=None, create_new=False): pass # Create deployment - return _create_deployment(country, models, project) + return _create_deployment(metrics_configuration, project) def get_data_from_sample(context, data: Dict, as_df: bool = False) -> Dict: @@ -92,10 +105,7 @@ def get_sample( create_new=False, ): deployment_df, static_df = get_or_create_deployment( - metrics_configuration["country"], - metrics_configuration["models"], - project=project, - create_new=create_new, + metrics_configuration, project=project, create_new=create_new, ) initial_timestamp = int( os.getenv( @@ -131,9 +141,7 @@ def init_context(context): # Generate or create deployment deployment_df, static_deployment = get_or_create_deployment( - metrics_configuration["country"], - metrics_configuration["models"], - project=project, + metrics_configuration, project=project, ) static_set = fstore.get_feature_set("static") diff --git a/network-operations/src/metric_configurations.yaml b/network-operations/src/metric_configurations.yaml index f3b7e923..a91b6a31 100644 --- a/network-operations/src/metric_configurations.yaml +++ b/network-operations/src/metric_configurations.yaml @@ -1,7 +1,12 @@ errors: {length_in_ticks: 50, rate_in_ticks: 5} timestamps: {interval: 5s, stochastic_interval: true} -models: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -country: [A, B, C, D, E, F, G] +deployment: + device: + faker: msisdn + num_items: 20 +static: + models: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + country: [A, B, C, D, E, F, G] metrics: cpu_utilization: accuracy: 2 From 5696164e89ebd82323eda7d4cc98710c74fad747 Mon Sep 17 00:00:00 2001 From: yaron haviv Date: Sun, 9 Jan 2022 23:23:07 +0200 Subject: [PATCH 4/6] update docs --- network-operations/.test | 2 + network-operations/README.md | 54 +++++++++++++++++-------- network-operations/images/pipeline.png | Bin 0 -> 113532 bytes 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 network-operations/.test create mode 100644 network-operations/images/pipeline.png diff --git a/network-operations/.test b/network-operations/.test new file mode 100644 index 00000000..f61d8e45 --- /dev/null +++ b/network-operations/.test @@ -0,0 +1,2 @@ +01-ingest.ipynb +02-training-and-deployment.ipynb diff --git a/network-operations/README.md b/network-operations/README.md index 8f39353e..7656685d 100644 --- a/network-operations/README.md +++ b/network-operations/README.md @@ -5,16 +5,17 @@ ## Overview This demo demonstrates how to build an automated machine-learning (ML) pipeline for predicting network outages based on network-device telemetry, also known as Network Operations (NetOps). -The demo implements both model training and inference, including model monitoring and concept-drift detection. -The demo simulates telemetry network data for running the pipeline. +The demo implements feature engineering, model training, testing, inference, and model monitoring (with concept-drift detection). +The demo is using a offline/real-time metrics simulator to generate semi-random network telemetry data that will be used across the pipeline. The demo demonstrates how to - Manage MLRun projects. - Use GitHub as a source for functions to use in pipeline workflows. -- Use MLRun logging to track results and artifacts. -- Use MLRun's Feature Store for feature engineering. -- Deploy a live-endpoints production pipeline. +- Use MLRun Feature Store to ingest, calculate and serve offline and real-time features +- Use MLRun Pipeline orchestration for running an automated train, test, and deploy pipeline and tracking the results and artifacts +- Use MLRun Serving Graphs to build real-time ML applications and serve models. +- Use MLRun Model monitoring and drift analysis. > **Note:** The demo applications are tested on the [Iguazio Data Science Platform](https://www.iguazio.com) ("the platform"), and use the platform's data store ("v3io"). > Contact [Iguazio support](mailto:support@iguazio.com) to request a free trial of the platform. @@ -40,7 +41,8 @@ Execute the following steps to run the demo: 2. In a client or notebook that is properly configured with MLRun, run the following code; replace `` with the name of your mlrun/demos GitHub fork: ``` - mlrun project demos/network-operations/ -u git://github.com//demos/network-operations.git + git clone https://github.com/.git + cd demos/network-operations ``` 3. Run the [**01-ingest.ipynb**](01-ingest.ipynb) notebook to create the feature sets and deploy the data generator and live ingestion endpoints. @@ -51,14 +53,14 @@ Execute the following steps to run the demo: ## Demo Flow The demo consists of: -1. Building and testing features from three sources (device metadata, real-time device metrics, and real-time device labels) using the feature store -2. Ingesting the data using batch (for testing) or real-time (for production) -3. Train and test the model with data from the feature-store -4. Deploying the model as part of a real-time feature engineering and inference pipeline -5. Real-time model and metrics monitoring, drift detection +1. **Building** and testing features from three sources (device metadata, real-time device metrics, and real-time device labels) using the feature store +2. **Ingesting** the data using batch (for testing) or real-time (for production) +3. **Running** an automated ML pipeline (train, test, and deploy the model with data from the feature-store) +4. **Testing** the deployed real-time feature engineering and inference pipeline +5. **Monitoring** the model serving data, metrics and detecting drift -### Feature Creation +### Feature Creation and Ingestion In the feature creation stage we use MLRun's Feature Store to first define the features we are want to ingest and the operations we want to apply to them. In this demo we create 3 unique feature sets: @@ -66,19 +68,36 @@ In this demo we create 3 unique feature sets: - **static network-device data** — static device data such as his model and manufacturing country. We will one-hot-encode the categorical features to have them ready for model ingestion. - **network-device failure label indicator** — a stream representing the label responses. The Feature Store will later match them with the correct sample for dataset creation. +After the 3 features sets are defined we demonstrate how to ingest the features data using batch or real-time techniques. -### Model Training +### Automated Model Training and Deployment (CI/CD) -In the model training stage we will use MLRun's Feature Store to define the Feature Vector we want to use for our model from the features we created and the labels we ingest. We will create a dataset from the feature vector and feed it to our [sklearn classifier training function](https://github.com/mlrun/functions/blob/master/sklearn_classifier/sklearn_classifier.ipynb) from the [MLRun marketplace functions hub](https://github.com/mlrun/functions) and create a network-device failure prediction model. +Now that we have our dataset ready for training, we need to define our model training, testing and deployment process. +We build an automated ML pipeline which uses pre-baked serverless training, testing and serving functions from [MLRun's functions marketplace](https://www.mlrun.org/marketplace/), the pipeline has three steps: +* **Train** a model using data from the feature vector we created and save it to the model registry +* **Test** and evaluate the model with portion of the data +* **Deploy** a real-time serving function which use the newly trained model and enrich/impute the features with data from the real-time feature vector + +![](images/pipeline.png) + +You can see the [**workflow code**](./src/workflow.py), we can run this workflow locally, in a CI/CD framework, or over Kubeflow. +In practice we may create different workflows for development and production. + +The workflow/pipeline can be executed using the MLRun SDK (`project.run()` method) or using CLI commands (`mlrun project`), +and can run directly from the source repo (GIT), see details in MLRun [**Projects and Automation documentation**](https://docs.mlrun.org/en/latest/projects/overview.html). -### Model Deployment & Monitoring +### Data and Model Monitoring -In this stage we will use MLRun's model server to deploy our trained model to production. We will use the `EnrichmentModelRouter` to retrieve the online feature vector automatically upon receiving a network-device id and predict device failures. +The deployed real-time pipeline generates real-time telemetry and gathers the model inputs and outputs data, the telemetry +metrics and real-time data are used for: +* monitoring model activity, performance, health +* monitoring real-time model and data statistics, drift and accuracy +* gathering production data for use in post analysis (explanability) and re-training -We will then be able to use MLRun's Model Montoring through Grafana to see our model performance, feature analytics and drift metrics. +We will then be able to view MLRun's Model Montoring results in MLRun UI or in Grafana dashboards. @@ -91,6 +110,7 @@ We will then be able to use MLRun's Model Montoring through Grafana to see our m - [**02-training-and-deployment.ipynb**](02-training-and-deployment.ipynb) — the 2nd demo step notebook. including feature vecto creation, dataset creation, model training, deployment and testing. - [**src/generator.py**](src/generator.py) — a nuclio function to generate live network-device telemetry and publish it to a v3io stream. - [**src/workflow.py**](src/workflow.py) — ML Pipeline for training, tests, and model deployment + ### Project-Configuration Files diff --git a/network-operations/images/pipeline.png b/network-operations/images/pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..8ac41e4fe01a0dcef75894bfa25aa2a8208bf6a2 GIT binary patch literal 113532 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 Date: Mon, 10 Jan 2022 17:14:55 +0200 Subject: [PATCH 5/6] Updated TF version --- mask-detection/1-training-and-evaluation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mask-detection/1-training-and-evaluation.ipynb b/mask-detection/1-training-and-evaluation.ipynb index 8f91e7ad..73a23ed2 100644 --- a/mask-detection/1-training-and-evaluation.ipynb +++ b/mask-detection/1-training-and-evaluation.ipynb @@ -33,7 +33,7 @@ "!pip install -U typing-extensions\n", "\n", "########## For TF.Keras: ##########\n", - "!pip install -U tensorflow==2.4.4\n", + "!pip install -U tensorflow==2.7.0\n", "\n", "########## For PyTorch: ##########\n", "# !pip install -U torch==1.10\n", @@ -1689,4 +1689,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From 2f15ac1dd602fd9aa0ec33c7c1f0809acd7947e0 Mon Sep 17 00:00:00 2001 From: yaron haviv Date: Thu, 13 Jan 2022 16:39:31 +0200 Subject: [PATCH 6/6] update RT ingestion --- network-operations/01-ingest.ipynb | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/network-operations/01-ingest.ipynb b/network-operations/01-ingest.ipynb index e86d3098..15fad8ef 100644 --- a/network-operations/01-ingest.ipynb +++ b/network-operations/01-ingest.ipynb @@ -1305,18 +1305,10 @@ "# we will define which fields in the json struct are used as `key` and `time` fields.\n", "source = mlrun.datastore.sources.StreamSource(path=device_metrics_stream , key_field='device', time_field='timestamp')\n", "\n", - "# Create a real-time serverless function definition to deploy the ingestion pipeline on.\n", - "# the serving runtimes enables the deployment of our feature set's computational graph\n", - "function = (mlrun.new_function('ingest-device-metrics', kind='serving')).with_code(body=\" \") #, image='mlrun/mlrun'\n", - "\n", - "# Create run configuration from the function\n", - "run_config = fstore.RunConfig(function=function)\n", - "\n", "# Deploy the transactions feature set's ingestion service using the feature set\n", "# and all the defined resources above.\n", - "device_metrics_set_endpoint = fstore.deploy_ingestion_service(featureset=device_metrics_set,\n", - " source=source,\n", - " run_config=run_config)" + "device_metrics_set_endpoint = fstore.deploy_ingestion_service(\n", + " featureset=device_metrics_set, source=source)" ] }, { @@ -1351,18 +1343,9 @@ "# Define the V3IO Stream Source from which the events data (in json format) are read.\n", "source = mlrun.datastore.sources.StreamSource(path=device_labels_stream , key_field='device', time_field='timestamp')\n", "\n", - "# Create a real-time serverless function definition to deploy the ingestion pipeline on.\n", - "# the serving runtimes enables the deployment of our feature set's computational graph\n", - "function = (mlrun.new_function('ingest-device-labels', kind='serving')).with_code(body=\" \")\n", - "\n", - "# Create run configuration from the function\n", - "run_config = fstore.RunConfig(function=function)\n", - "\n", "# Deploy the transactions feature set's ingestion service using the feature set\n", - "# and all the defined resources above.\n", - "device_labels_set_endpoint = fstore.deploy_ingestion_service(featureset=device_labels_set,\n", - " source=source,\n", - " run_config=run_config)" + "device_labels_set_endpoint = fstore.deploy_ingestion_service(\n", + " featureset=device_labels_set, source=source)" ] }, {