diff --git a/README.md b/README.md index 0a9f6a77..d1a1c030 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ In order to understand the tutorials you need to be familiar with general concep - [WandB](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/integrations/wandb): Build a machine learning model with Weights & Biases. - [Great Expectations](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/integrations/great_expectations): Introduction to Great Expectations concepts and classes which are relevant for integration with the Hopsworks MLOps platform. - [Neo4j](integrations/neo4j): Perform Anti-money laundering (AML) predictions using Neo4j Graph representation of transactions. + - [Polars](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/advanced_tutorials/polars/quickstart.ipynb) : Introductory tutorial on using Polars. - [Monitoring](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/integrations/monitoring): How to implement feature monitoring in your production pipeline. - [Bytewax](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/integrations/bytewax): Real time feature computation using Bytewax. - [Apache Beam](https://github.com/logicalclocks/hopsworks-tutorials/tree/master/integrations/java/beam): Real time feature computation using Apache Beam, Google Cloud Dataflow and Hopsworks Feature Store. diff --git a/advanced_tutorials/polars/quickstart_polars.ipynb b/advanced_tutorials/polars/quickstart_polars.ipynb new file mode 100644 index 00000000..b9581ef4 --- /dev/null +++ b/advanced_tutorials/polars/quickstart_polars.ipynb @@ -0,0 +1,1788 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "8WLB6QFXksxw" + }, + "source": [ + "![Screenshot from 2022-06-16 14-24-57.png](data:image/png;base64,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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/logicalclocks/hopsworks-tutorials/blob/master/quickstart.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the quick start tutorial for using **Polars** with the **Hopsworks Feature Store**. As part of this tutorial, you will work with data related to credit card transactions. \n", + "The objective of this tutorial is to demonstrate how to use polars with the Hopworks Feature Store with a goal of training and saving a model that can predict fraudulent transactions. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NpwpPe1wxQ5M" + }, + "source": [ + "## 💽 Loading the Data \n", + "\n", + "The data you will use comes from three different CSV files:\n", + "\n", + "* credit_cards.csv: information such as the expiration date and provider.\n", + "* transactions.csv: events containing information about when a credit card was used, such as a timestamp, location, and the amount spent. A boolean fraud_label variable (True/False) tells us whether a transaction was fraudulent or not.\n", + "* profiles.csv: credit card user information such as birthdate and city of residence.\n", + "\n", + "In a production system, these CSV files would originate from separate data sources or tables, and probably separate data pipelines. All three files have a common credit card number column cc_num, which you will use later to join features together from the different datasets.\n", + "\n", + "Now, you can go ahead and load the data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import joblib\n", + "import os\n", + "import time\n", + "\n", + "import polars as pl\n", + "import numpy as np\n", + "from matplotlib import pyplot\n", + "import seaborn as sns\n", + "from math import radians\n", + "\n", + "import xgboost as xgb\n", + "from sklearn.metrics import confusion_matrix\n", + "from sklearn.metrics import f1_score\n", + "\n", + "# Mute warnings\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 143 + }, + "id": "ARrJ_Bp5xMIk", + "outputId": "14e7a020-e04a-40d5-fdea-c4ba71f8a034" + }, + "outputs": [], + "source": [ + "# Specify the window length as \"4h\"\n", + "window_len = \"4h\"\n", + "\n", + "# Specify the URL for the data\n", + "url = \"https://repo.hops.works/master/hopsworks-tutorials/data/card_fraud_data/\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Read the 'credit_cards.csv' file\n", + "credit_cards_df = pl.read_csv(url + \"credit_cards.csv\")\n", + "\n", + "# Read the 'profiles.csv' file\n", + "# Parse the 'birthdate' column as dates\n", + "profiles_df = pl.read_csv(url + \"profiles.csv\", try_parse_dates=True)\n", + "\n", + "# Read the 'transactions.csv' file\n", + "# Parse the 'datetime' column as dates\n", + "trans_df = pl.read_csv(url + \"transactions.csv\", try_parse_dates=True)\n", + "\n", + "# Display the first 3 rows of the 'transactions.csv' DataFrame\n", + "trans_df.head(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HPq2qUtNxjaM" + }, + "source": [ + "## 🛠️ Feature Engineering \n", + "\n", + "Fraudulent transactions can differ from regular ones in many different ways. Typical red flags would for instance be a large transaction volume/frequency in the span of a few hours. It could also be the case that elderly people in particular are targeted by fraudsters. To facilitate model learning, we will create additional features based on these patterns. In particular, we will create two types of features:\n", + "\n", + "* Features that aggregate data from different data sources. This could for instance be the age of a customer at the time of a transaction, which combines the birthdate feature from profiles.csv with the datetime feature from transactions.csv.\n", + "* Features that aggregate data from multiple time steps. An example of this could be the transaction frequency of a credit card in the span of a few hours, which is computed using a window function.\n", + "\n", + "Now you are ready to start with the first category.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "ngEPnNzAxqsJ", + "outputId": "c8cf6082-1d1d-4bf7-9d81-ac2294146a27" + }, + "outputs": [], + "source": [ + "# Merge the 'trans_df' DataFrame with the 'profiles_df' DataFrame based on the 'cc_num' column\n", + "age_df = trans_df.join(profiles_df, on=\"cc_num\", how=\"left\")\n", + "\n", + "# Merge the 'trans_df' DataFrame with the 'credit_cards_df' DataFrame based on the 'cc_num' column\n", + "card_expiry_df = trans_df.join(credit_cards_df, on=\"cc_num\", how=\"left\")\n", + "\n", + "# Convert the 'expires' column to datetime format\n", + "card_expiry_df = card_expiry_df.with_columns(pl.col(\"expires\").str.to_datetime(\"%m/%y\"))\n", + "\n", + "# Compute the age at the time of each transaction and store it in the 'age_at_transaction' column\n", + "trans_df = trans_df.with_columns(age_at_transaction = (age_df[\"datetime\"] - age_df[\"birthdate\"]).dt.days()/365)\n", + "\n", + "# Compute the days until the card expires and store it in the 'days_until_card_expires' column\n", + "trans_df = trans_df.with_columns(days_until_card_expires = (card_expiry_df[\"expires\"] - card_expiry_df[\"datetime\"]).dt.days())\n", + "\n", + "# Display the 'age_at_transaction' and 'days_until_card_expires' columns for the first few rows\n", + "trans_df[[\"age_at_transaction\", \"days_until_card_expires\"]].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zEC12W4ux2Uk" + }, + "source": [ + "The next step is that you will create features from aggregations that are computed over every credit card over multiple time steps.\n", + "\n", + "You start by computing a feature that captures the physical distance between consecutive transactions, which we will call `loc_delta`. Here, you will use Haversine distance to quantify the distance between two longitude and latitude coordinates.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rQ-g4ETOx4O5" + }, + "outputs": [], + "source": [ + "# Sort the 'trans_df' DataFrame based on the 'datetime' column in ascending order\n", + "trans_df = trans_df.sort(\"datetime\")\n", + "\n", + "# Convert the 'longitude' and 'latitude' columns to radians\n", + "trans_df = trans_df.with_columns(pl.col(\"latitude\").map_elements(radians),\n", + " pl.col(\"longitude\").map_elements(radians))\n", + "\n", + "# Define a function to compute Haversine distance between consecutive coordinates\n", + "def haversine(long, lat):\n", + " \"\"\"Compute Haversine distance between each consecutive coordinate in (long, lat).\"\"\"\n", + "\n", + " # Shift the longitude and latitude columns to get consecutive values\n", + " long_shifted = long.shift()\n", + " lat_shifted = lat.shift()\n", + "\n", + " # Calculate the differences in longitude and latitude\n", + " long_diff = long_shifted - long\n", + " lat_diff = lat_shifted - lat\n", + "\n", + " # Haversine formula to compute distance\n", + " a = np.sin(lat_diff/2.0)**2\n", + " b = np.cos(lat) * np.cos(lat_shifted) * np.sin(long_diff/2.0)**2\n", + " c = 2*np.arcsin(np.sqrt(a + b))\n", + "\n", + " return c\n", + "\n", + "# Apply the haversine function to compute the 'loc_delta' column\n", + "trans_df = trans_df.with_columns(trans_df.groupby(\"cc_num\")\n", + " .agg(pl.map_groups(exprs=[\"longitude\", \"latitude\"], function = lambda x : haversine(x[0], x[1]))\n", + " .alias(\"loc_delta\")).explode(\"loc_delta\").fill_null(0).select(\"loc_delta\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a_MHfwYsGfbo" + }, + "source": [ + "Next you will compute windowed aggregates. Here you will use 4-hour windows, but feel free to experiment with different window lengths by setting `window_len` below to a value of your choice." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "jmywmIVKGgLR", + "outputId": "32ad2881-9a27-483f-c8e4-9ef94af6dd6e" + }, + "outputs": [], + "source": [ + "window_aggs_df = trans_df[[\"cc_num\", \"amount\", \"datetime\", \"loc_delta\"]].rolling(\n", + " period=window_len, \n", + " index_column=\"datetime\",\n", + " by=[\"cc_num\"]\n", + ").agg(pl.col(\"amount\").mean().alias(\"trans_volume_mavg\"),\n", + " pl.col(\"amount\").std().alias(\"trans_volume_mstd\"),\n", + " pl.col(\"amount\").count().alias(\"trans_freq\"),\n", + " pl.col(\"loc_delta\").mean().alias(\"loc_delta_mavg\"),).fill_null(0)\n", + "window_aggs_df.tail()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yB90r9qszLe2" + }, + "source": [ + "## 🪄 Creating Feature Groups \n", + "\n", + "A feature group can be seen as a collection of conceptually related features that are computed together at the same cadence. In your case, you will create a feature group for the transaction data and a feature group for the windowed aggregations on the transaction data. Both will have `tid` as primary key, which will allow you to join them together to create training data in a follow-on tutorial.\n", + "\n", + "Feature groups provide a namespace for features, so two features are allowed to have the same name as long as they belong to different feature groups. For instance, in a real-life setting we would likely want to experiment with different window lengths. In that case, we can create feature groups with identical schema for each window length.\n", + "\n", + "Before you can create a feature group we need to connect to our feature store.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "WFmD_15TzMHX", + "outputId": "6acf8632-6993-485c-fb2a-31f27f7b462f" + }, + "outputs": [], + "source": [ + "import hopsworks\n", + "\n", + "project = hopsworks.login(host = \"c.app.hopsworks.ai\", \n", + " api_key_value=\"pDqRJZfnqRvbmZ7b.PtADpllk5K808lKYuGiF4zQddu0uJb2EoxSPVJOU8iQlHk7Vo9tXlJNBhxfPZWpd\",\n", + " port=443)\n", + "\n", + "fs = project.get_feature_store()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get or create the 'transactions' feature group\n", + "trans_fg = fs.get_or_create_feature_group(\n", + " name=\"transactions\",\n", + " version=1,\n", + " description=\"Transaction data\",\n", + " primary_key=[\"cc_num\"],\n", + " event_time=\"datetime\",\n", + " online_enabled=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A full list of arguments can be found in the [documentation](https://docs.hopsworks.ai/feature-store-api/latest/generated/api/feature_store_api/#create_feature_group).\n", + "\n", + "At this point, you have only specified some metadata for the feature group. It does not store any data or even have a schema defined for the data. To make the feature group persistent you need to populate it with its associated data using the `insert` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Insert data into feature group\n", + "trans_fg.insert(trans_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Update feature descriptions\n", + "feature_descriptions = [\n", + " {\"name\": \"tid\", \"description\": \"Transaction id\"},\n", + " {\"name\": \"datetime\", \"description\": \"Transaction time\"},\n", + " {\"name\": \"cc_num\", \"description\": \"Number of the credit card performing the transaction\"},\n", + " {\"name\": \"category\", \"description\": \"Expense category\"},\n", + " {\"name\": \"amount\", \"description\": \"Dollar amount of the transaction\"},\n", + " {\"name\": \"latitude\", \"description\": \"Transaction location latitude\"},\n", + " {\"name\": \"longitude\", \"description\": \"Transaction location longitude\"},\n", + " {\"name\": \"city\", \"description\": \"City in which the transaction was made\"},\n", + " {\"name\": \"country\", \"description\": \"Country in which the transaction was made\"},\n", + " {\"name\": \"fraud_label\", \"description\": \"Whether the transaction was fraudulent or not\"},\n", + " {\"name\": \"age_at_transaction\", \"description\": \"Age of the card holder when the transaction was made\"},\n", + " {\"name\": \"days_until_card_expires\", \"description\": \"Card validity days left when the transaction was made\"},\n", + " {\"name\": \"loc_delta\", \"description\": \"Haversine distance between this transaction location and the previous transaction location from the same card\"},\n", + "]\n", + "\n", + "for desc in feature_descriptions: \n", + " trans_fg.update_feature_description(desc[\"name\"], desc[\"description\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At the creation of the feature group, you will be prompted with an URL that will directly link to it; there you will be able to explore some of the aspects of your newly created feature group.\n", + "\n", + "[//]: <> (insert GIF here)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can move on and do the same thing for the feature group with our windows aggregation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get or create the 'transactions' feature group with aggregations using specified window len\n", + "window_aggs_fg = fs.get_or_create_feature_group(\n", + " name=f\"transactions_{window_len}_aggs\",\n", + " version=1,\n", + " description=f\"Aggregate transaction data over {window_len} windows.\",\n", + " primary_key=[\"cc_num\"],\n", + " event_time=\"datetime\",\n", + " online_enabled=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Insert data into feature group\n", + "window_aggs_fg.insert(window_aggs_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Update feature descriptions\n", + "feature_descriptions = [\n", + " {\"name\": \"datetime\", \"description\": \"Transaction time\"},\n", + " {\"name\": \"cc_num\", \"description\": \"Number of the credit card performing the transaction\"},\n", + " {\"name\": \"loc_delta_mavg\", \"description\": \"Moving average of location difference between consecutive transactions from the same card\"},\n", + " {\"name\": \"trans_freq\", \"description\": \"Moving average of transaction frequency from the same card\"},\n", + " {\"name\": \"trans_volume_mavg\", \"description\": \"Moving average of transaction volume from the same card\"},\n", + " {\"name\": \"trans_volume_mstd\", \"description\": \"Moving standard deviation of transaction volume from the same card\"},\n", + "]\n", + "\n", + "for desc in feature_descriptions: \n", + " window_aggs_fg.update_feature_description(desc[\"name\"], desc[\"description\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### 🔪 Feature Selection \n", + "\n", + "You will start by selecting all the features you want to include for model training/inference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Select features for training data\n", + "selected_features = trans_fg.select([\"fraud_label\", \"category\", \"amount\", \"age_at_transaction\", \"days_until_card_expires\", \"loc_delta\"])\\\n", + " .join(window_aggs_fg.select_except([\"cc_num\"]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment this if you would like to view your selected features\n", + "# selected_features.show(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Recall that you computed the features in `transactions_4h_aggs_fraud_batch_fg` using 4-hour aggregates. If you had created multiple feature groups with identical schema for different window lengths, and wanted to include them in the join you would need to include a prefix argument in the join to avoid feature name clash. See the [documentation](https://docs.hopsworks.ai/feature-store-api/latest/generated/api/query_api/#join) for more details." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 🤖 Transformation Functions \n", + "\n", + "\n", + "You will preprocess our data using *min-max scaling* on numerical features and *label encoding* on categorical features. To do this you simply define a mapping between our features and transformation functions. This ensures that transformation functions such as *min-max scaling* are fitted only on the training data (and not the validation/test data), which ensures that there is no data leakage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load transformation functions.\n", + "label_encoder = fs.get_transformation_function(name=\"label_encoder\")\n", + "\n", + "# Map features to transformations.\n", + "transformation_functions = {\n", + " \"category\": label_encoder,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ⚙️ Feature View Creation \n", + "\n", + "The Feature View is the collection of features (from feature groups) and transformation functions used to train models and serve precomputed features to deployed models.\n", + "\n", + "The Feature View includes all of the features defined in the query object you created earlier. It can additionally include filters, one or more columns identified as the target(s) (or label) and the set of transformation functions and the features they are applied to. \n", + "\n", + "You create a Feature View with `fs.create_feature_view()`. \n", + "You retrieve a reference to an existing feature view with: `fs.get_feature_view('transactions_view',version=1)`.\n", + "In addition you can use `fs.get_or_create_feature_view()` method in order to retrieve existing feature view or create if it does not exist.\n", + "This code first tries to get a reference to the feature_view, if it doesn't exist it creates the feature_view." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get or create the 'transactions_view' feature view\n", + "feature_view = fs.get_or_create_feature_view(\n", + " name='transactions_view',\n", + " version=1,\n", + " query=selected_features,\n", + " labels=[\"fraud_label\"],\n", + " transformation_functions=transformation_functions,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## 🏋️ Training Dataset Creation\n", + "\n", + "In Hopsworks training data is a query where the projection (set of features) is determined by the parent FeatureView with an optional snapshot on disk of the data returned by the query.\n", + "\n", + "**Training Dataset may contain splits such as:** \n", + "* Training set - the subset of training data used to train a model.\n", + "* Validation set - the subset of training data used to evaluate hparams when training a model\n", + "* Test set - the holdout subset of training data used to evaluate a mode\n", + "\n", + "Training dataset is created using `feature_view.train_test_split()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "TEST_SIZE = 0.2\n", + "\n", + "X_train, X_test, y_train, y_test = feature_view.train_test_split(\n", + " description='transactions fraud training dataset',\n", + " test_size=TEST_SIZE,\n", + " dataframe_type=\"polars\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Combining training data and labels for sorting\n", + "training_data = pl.concat([X_train, y_train], how=\"horizontal\")\n", + "\n", + "# Sort the training features DataFrame based on the 'datetime' column\n", + "training_data = training_data.sort(\"datetime\")\n", + "\n", + "X_train = training_data.select(pl.exclude(\"fraud_label\"))\n", + "\n", + "y_train = training_data.select(\"fraud_label\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Combining training data and labels for sorting\n", + "test_data = pl.concat([X_test, y_test], how=\"horizontal\")\n", + "\n", + "# Sort the test features DataFrame based on the 'datetime' column\n", + "test_data = test_data.sort(\"datetime\")\n", + "\n", + "X_test = test_data.select(pl.exclude(\"fraud_label\"))\n", + "\n", + "y_test = test_data.select(\"fraud_label\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Drop the 'datetime' column from the training features DataFrame 'X_train'\n", + "X_train = X_train.drop([\"datetime\"])\n", + "\n", + "# Drop the 'datetime' column from the test features DataFrame 'X_test'\n", + "X_test = X_test.drop([\"datetime\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the normalized value counts of the target variable 'y_train'\n", + "y_train[\"fraud_label\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that the distribution is extremely skewed, which is natural considering that fraudulent transactions make up a tiny part of all transactions. Thus you should somehow address the class imbalance. There are many approaches for this, such as weighting the loss function, over- or undersampling, creating synthetic data, or modifying the decision threshold. In this example, you will use the simplest method which is to just supply a class weight parameter to our learning algorithm. The class weight will affect how much importance is attached to each class, which in our case means that higher importance will be placed on positive (fraudulent) samples." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 🧬 Modeling\n", + "\n", + "Next you will train a model. Here, you set larger class weight for the positive class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create an XGBoost classifier\n", + "clf = xgb.XGBClassifier()\n", + "\n", + "# Fit XGBoost classifier to the training data\n", + "clf.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Predict the training data using the trained classifier\n", + "y_pred_train = clf.predict(X_train)\n", + "\n", + "# Predict the test data using the trained classifier\n", + "y_pred_test = clf.predict(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute f1 score\n", + "metrics = {\n", + " \"f1_score\": f1_score(y_test, y_pred_test, average='macro')\n", + "}\n", + "metrics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Calculate and print the confusion matrix for the test predictions\n", + "results = confusion_matrix(y_test, y_pred_test)\n", + "print(results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a DataFrame for the confusion matrix results\n", + "df_cm = pl.DataFrame(\n", + " results, \n", + ")\n", + "\n", + "# Create a heatmap using seaborn with annotations\n", + "cm = sns.heatmap(df_cm, annot=True)\n", + "\n", + "# Get the figure and display it\n", + "fig = cm.get_figure()\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### ⚙️ Model Schema\n", + "\n", + "The model needs to be set up with a [Model Schema](https://docs.hopsworks.ai/3.0/user_guides/mlops/registry/model_schema/), which describes the inputs and outputs for a model.\n", + "\n", + "A Model Schema can be automatically generated from training examples, as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from hsml.schema import Schema\n", + "from hsml.model_schema import ModelSchema\n", + "\n", + "# Create a Schema for the input features using the values of X_train\n", + "input_schema = Schema(X_train.to_numpy())\n", + "\n", + "# Create a Schema for the output using y_train\n", + "output_schema = Schema(y_train.to_numpy())\n", + "\n", + "# Create a ModelSchema using the defined input and output schemas\n", + "model_schema = ModelSchema(input_schema=input_schema, output_schema=output_schema)\n", + "\n", + "# Convert the model schema to a dictionary for inspection\n", + "model_schema.to_dict()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 📝 Register model\n", + "\n", + "One of the features in Hopsworks is the model registry. This is where we can store different versions of models and compare their performance. Models from the registry can then be served as API endpoints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Specify the directory name for saving the model and related artifacts\n", + "model_dir = \"quickstart_fraud_model\"\n", + "\n", + "# Check if the directory already exists; if not, create it\n", + "if not os.path.isdir(model_dir):\n", + " os.mkdir(model_dir)\n", + "\n", + "# Save the trained XGBoost classifier to a joblib file in the specified directory\n", + "joblib.dump(clf, model_dir + '/xgboost_model.pkl')\n", + "\n", + "# Save the confusion matrix heatmap figure to an image file in the specified directory\n", + "fig.savefig(model_dir + \"/confusion_matrix.png\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the model registry\n", + "mr = project.get_model_registry()\n", + "\n", + "# Create a Python model named \"fraud\" in the model registry\n", + "fraud_model = mr.python.create_model(\n", + " name=\"fraud\", \n", + " metrics=metrics, # Specify the metrics used to evaluate the model\n", + " model_schema=model_schema, # Use the previously defined model schema\n", + " input_example=[4700702588013561], # Provide an input example for testing deployments\n", + " description=\"Quickstart Fraud Predictor\", # Add a description for the model\n", + ")\n", + "\n", + "# Save the model to the specified directory\n", + "fraud_model.save(model_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## 🚀 Model Deployment\n", + "\n", + "\n", + "### About Model Serving\n", + "Models can be served via KFServing or \"default\" serving, which means a Docker container exposing a Flask server. For KFServing models, or models written in Tensorflow, you do not need to write a prediction file (see the section below). However, for sklearn models using default serving, you do need to proceed to write a prediction file.\n", + "\n", + "In order to use KFServing, you must have Kubernetes installed and enabled on your cluster." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 📎 Predictor script for Python models\n", + "\n", + "\n", + "Scikit-learn and XGBoost models are deployed as Python models, in which case you need to provide a **Predict** class that implements the **predict** method. The **predict()** method invokes the model on the inputs and returns the prediction as a list.\n", + "\n", + "The **init()** method is run when the predictor is loaded into memory, loading the model from the local directory it is materialized to, *ARTIFACT_FILES_PATH*.\n", + "\n", + "The directive \"%%writefile\" writes out the cell before to the given Python file. We will use the **predict_example.py** file to create a deployment for our model. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile predict_example.py\n", + "import os\n", + "import numpy as np\n", + "import hsfs\n", + "import joblib\n", + "\n", + "\n", + "class Predict(object):\n", + "\n", + " def __init__(self):\n", + " \"\"\" Initializes the serving state, reads a trained model\"\"\" \n", + " # Get feature store handle\n", + " fs_conn = hsfs.connection()\n", + " self.fs = fs_conn.get_feature_store()\n", + " \n", + " # Get feature view\n", + " self.fv = self.fs.get_feature_view(\"transactions_view\", 1)\n", + " \n", + " # Initialize serving\n", + " self.fv.init_serving(1)\n", + "\n", + " # Load the trained model\n", + " self.model = joblib.load(os.environ[\"ARTIFACT_FILES_PATH\"] + \"/xgboost_model.pkl\")\n", + " print(\"Initialization Complete\")\n", + "\n", + " def predict(self, inputs):\n", + " \"\"\" Serves a prediction request usign a trained model\"\"\"\n", + " feature_vector = self.fv.get_feature_vector({\"cc_num\": inputs[0][0]})\n", + " feature_vector = feature_vector[:-1]\n", + " \n", + " return self.model.predict(np.asarray(feature_vector).reshape(1, -1)).tolist() # Numpy Arrays are not JSON serializable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you wonder why we use the path Models/fraud_tutorial_model/1/model.pkl, it is useful to know that the Data Sets tab in the Hopsworks UI lets you browse among the different files in the project. Registered models will be found underneath the Models directory. Since you saved you model with the name fraud_tutorial_model, that's the directory you should look in. 1 is just the version of the model you want to deploy.\n", + "\n", + "This script needs to be put into a known location in the Hopsworks file system. Let's call the file predict_example.py and put it in the Models directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the dataset API from the project\n", + "dataset_api = project.get_dataset_api()\n", + "\n", + "# Specify the file to upload (\"predict_example.py\") to the \"Models\" directory, and allow overwriting\n", + "uploaded_file_path = dataset_api.upload(\"predict_example.py\", \"Models\", overwrite=True)\n", + "\n", + "# Construct the full path to the uploaded predictor script\n", + "predictor_script_path = os.path.join(\"/Projects\", project.name, uploaded_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 👩🏻‍🔬 Create the deployment\n", + "\n", + "Here, you fetch the model you want from the model registry and define a configuration for the deployment. For the configuration, you need to specify the serving type (default or KFserving)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Deploy the fraud model\n", + "deployment = fraud_model.deploy(\n", + " name=\"fraud\", # Specify the deployment name\n", + " script_file=predictor_script_path, # Provide the path to the predictor script\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Deployment is warming up...\")\n", + "time.sleep(45)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The deployment has now been registered. However, to start it you need to run the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start the deployment and wait for it to be running, with a maximum waiting time of 180 seconds\n", + "deployment.start(await_running=180)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the current state of the deployment and describe its details\n", + "deployment_state = deployment.get_state().describe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QKCTKfcaimxo" + }, + "source": [ + "## 📡 Test your Model with an Inference Request \n", + "\n", + "Finally you can start making predictions with your model! \n", + "\n", + "Send inference requests to the deployed model as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 52 + }, + "id": "aL3-2W39tC-u", + "outputId": "fbda67a5-ce89-49ff-f113-a7dc8bbc2b6d" + }, + "outputs": [], + "source": [ + "# Make predictions using the deployed model\n", + "predictions = deployment.predict(\n", + " inputs=fraud_model.input_example,\n", + ")\n", + "predictions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 👾 Try out your Model Interactively \n", + "\n", + "We will build a user interface with Gradio to allow you to enter a credit card category and amount to see if the credit card transaction will be marked as suspected of fraud or not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install gradio --quiet\n", + "!pip install typing-extensions==4.3.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import gradio as gr\n", + "import numpy as np\n", + "\n", + "def greet(credit_card_example):\n", + " cc_data = credit_card_example.iloc[0].astype(\"float\")\n", + " # Add missing feature values to the feature vector. Here we hard-code the values,\n", + " # but if you enable the Online Feature Store, you could retrieve them with the following commented out code\n", + " # entry = { \"cc_num\" : credit_card_example[0]}\n", + " # passed_features = {\"category\": credit_card_example[0], \"amount\" : credit_card_example[1]}\n", + " # feature_vector = feature_view.get_feature_vector(entry, passed_features)\n", + " res = deployment.predict(inputs=cc_data.tolist())\n", + " res = res[\"predictions\"][0]\n", + " if res == 0 :\n", + " return \"Not Suspected of Fraud\"\n", + " return \"Suspected of Fraud\"\n", + "\n", + "credit_card_example = gr.Dataframe(\n", + " headers=[\"Credit card number\"],\n", + " value=[[fraud_model.input_example[0]]]\n", + ")\n", + "\n", + "demo = gr.Interface(greet, \n", + " credit_card_example,\n", + " \"text\",\n", + " title=\"Live Credit Card Fraud Detector\",\n", + " description=\"Enter credit card transaction details.\",\n", + " allow_flagging=\"never\"\n", + ")\n", + "\n", + "\n", + "demo.launch(share=True, debug=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HmwNUVbHjO5I" + }, + "source": [ + "## 🥳 Next Steps\n", + "\n", + "Congratulations you've now completed the quickstart example for Managed Hopsworks.\n", + "\n", + "\n", + "Check out our other tutorials on ➡ https://github.com/logicalclocks/hopsworks-tutorials\n", + "\n", + "Or documentation at ➡ https://docs.hopsworks.ai" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "quickstart.ipynb", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.18" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "192f266700894002b24eeff9b2136db3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1b7c2a4d212646239113f831eeafc1cd": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "24536f939afa41f1acff4e55fae4423c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3f7e59807b824e86b47319bd47e32650": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_66c0d3f6020845d7a4203f33abf33818", + "IPY_MODEL_f22754bd6225452f8253ecff644c31d5", + "IPY_MODEL_77e516d3f3ce4c5488bf68825d260061" + ], + "layout": "IPY_MODEL_f91eeaa7449541819c9970f42963e2dd" + } + }, + "43c0b243b2c9417abc5072f82ba22457": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_4e326043046247f5ae5bfac3306cb145", + "IPY_MODEL_a16b8eae9f614a44a9ee7f7ea900d1f2", + "IPY_MODEL_a0e314c90c5a413d9701a6efcded884b" + ], + "layout": "IPY_MODEL_24536f939afa41f1acff4e55fae4423c" + } + }, + "4559fa2eaf7348ae9dd5d1cfdd3e0bd5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "4e326043046247f5ae5bfac3306cb145": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a26b3a1aa96f4eeaaad89d4d662fa010", + "placeholder": "​", + "style": "IPY_MODEL_ed9e0fc80c9e483cb7aafad007b57ea0", + "value": "Deployment is running: 100%" + } + }, + "5fd0bd75549a47ae8a3042f1e0c61ba5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "66c0d3f6020845d7a4203f33abf33818": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d1c010a576d94f8bbcc6f535741f514b", + "placeholder": "​", + "style": "IPY_MODEL_5fd0bd75549a47ae8a3042f1e0c61ba5", + "value": "Model export complete: 100%" + } + }, + "71ad3ff88d62426abda44fdb300b0484": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "7529396c14464b7b9e349c6ba003cddc": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "77e516d3f3ce4c5488bf68825d260061": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_192f266700894002b24eeff9b2136db3", + "placeholder": "​", + "style": "IPY_MODEL_1b7c2a4d212646239113f831eeafc1cd", + "value": " 6/6 [00:25<00:00, 5.19s/it]" + } + }, + "7887571c2ea4415080f60ea18be441b1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a0e314c90c5a413d9701a6efcded884b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7529396c14464b7b9e349c6ba003cddc", + "placeholder": "​", + "style": "IPY_MODEL_71ad3ff88d62426abda44fdb300b0484", + "value": " 1/1 [00:20<00:00, 5.12s/it]" + } + }, + "a16b8eae9f614a44a9ee7f7ea900d1f2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7887571c2ea4415080f60ea18be441b1", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_4559fa2eaf7348ae9dd5d1cfdd3e0bd5", + "value": 1 + } + }, + "a26b3a1aa96f4eeaaad89d4d662fa010": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d1c010a576d94f8bbcc6f535741f514b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d6462acb3ac6479f942e1a1b8da309a8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "ed9e0fc80c9e483cb7aafad007b57ea0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "f22754bd6225452f8253ecff644c31d5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f2ccbe863c224765bbe755fcb0ba4be7", + "max": 6, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_d6462acb3ac6479f942e1a1b8da309a8", + "value": 6 + } + }, + "f2ccbe863c224765bbe755fcb0ba4be7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f91eeaa7449541819c9970f42963e2dd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}