[2]:
# Copyright 2020 NVIDIA Corporation. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

74830c3f30fc49d18a990f56c1de60b5

NVTabular demo on Rossmann data

Overview

NVTabular is a feature engineering and preprocessing library for tabular data designed to quickly and easily manipulate terabyte scale datasets used to train deep learning based recommender systems. It provides a high level abstraction to simplify code and accelerates computation on the GPU using the RAPIDS cuDF library.

Learning objectives

This notebook demonstrates the steps for carrying out data preprocessing, transformation and loading with NVTabular on the Kaggle Rossmann dataset. Rossmann operates over 3,000 drug stores in 7 European countries. Historical sales data for 1,115 Rossmann stores are provided. The task is to forecast the “Sales” column for the test set.

The following example will illustrate how to use NVTabular to preprocess and load tabular data for training neural networks in both PyTorch and TensorFlow. We’ll use a dataset built by FastAI for solving the Kaggle Rossmann Store Sales competition. Some cuDF preprocessing is required to build the appropriate feature set, so make sure to run rossmann-store-sales-preproc.ipynb first before going through this notebook.

[1]:
import os
import math
import nvtabular as nvt
import glob

Preparing our dataset

Let’s start by defining some of the a priori information about our data, including its schema (what columns to use and what sorts of variables they represent), as well as the location of the files corresponding to some particular sampling from this schema. Note that throughout, I’ll use UPPERCASE variables to represent this sort of a priori information that you might usually encode using commandline arguments or config files.

[2]:
DATA_DIR = os.environ.get("OUTPUT_DATA_DIR", "./data")

CATEGORICAL_COLUMNS = [
    'Store', 'DayOfWeek', 'Year', 'Month', 'Day', 'StateHoliday', 'CompetitionMonthsOpen',
    'Promo2Weeks', 'StoreType', 'Assortment', 'PromoInterval', 'CompetitionOpenSinceYear', 'Promo2SinceYear',
    'State', 'Week', 'Events', 'Promo_fw', 'Promo_bw', 'StateHoliday_fw', 'StateHoliday_bw',
    'SchoolHoliday_fw', 'SchoolHoliday_bw'
]

CONTINUOUS_COLUMNS = [
    'CompetitionDistance', 'Max_TemperatureC', 'Mean_TemperatureC', 'Min_TemperatureC',
   'Max_Humidity', 'Mean_Humidity', 'Min_Humidity', 'Max_Wind_SpeedKm_h',
   'Mean_Wind_SpeedKm_h', 'CloudCover', 'trend', 'trend_DE',
   'AfterStateHoliday', 'BeforeStateHoliday', 'Promo', 'SchoolHoliday'
]
LABEL_COLUMNS = ['Sales']

COLUMNS = CATEGORICAL_COLUMNS + CONTINUOUS_COLUMNS + LABEL_COLUMNS

What files are available to train on in our data directory?

[3]:
! ls $DATA_DIR
ross_pre  test.csv  train.csv  valid.csv

train.csv and valid.csv seem like good candidates, let’s use those.

[4]:
TRAIN_PATH = os.path.join(DATA_DIR, 'train.csv')
VALID_PATH = os.path.join(DATA_DIR, 'valid.csv')

Workflows and Preprocessing

A Workflow is used to represent the chains of feature engineering and preprocessing operations performed on a dataset, and is instantiated with a description of the dataset’s schema so that it can keep track of how columns transform with each operation.

[5]:
# note that here, we want to perform a normalization transformation on the label
# column. Since NVT doesn't support transforming label columns right now, we'll
# pretend it's a regular continuous column during our feature engineering phase
proc = nvt.Workflow(
    cat_names=CATEGORICAL_COLUMNS,
    cont_names=CONTINUOUS_COLUMNS,
    label_name=LABEL_COLUMNS
)

Ops

We add operations to a Workflow by leveraging the add_(cat|cont)_feature and add_(cat|cont)_preprocess methods for categorical and continuous variables, respectively. When we’re done adding ops, we call the finalize method to let the Workflow build a representation of its outputs.

[6]:
proc.add_cont_feature(nvt.ops.FillMissing())
proc.add_cont_preprocess(nvt.ops.LogOp(columns=LABEL_COLUMNS))
proc.add_cont_preprocess(nvt.ops.Normalize())
proc.add_cat_preprocess(nvt.ops.Categorify())
proc.finalize()

Datasets

In general, the Ops in our Workflow will require measurements of statistical properties of our data in order to be leveraged. For example, the Normalize op requires measurements of the dataset mean and standard deviation, and the Categorify op requires an accounting of all the categories a particular feature can manifest. However, we frequently need to measure these properties across datasets which are too large to fit into GPU memory (or CPU memory for that matter) at once.

NVTabular solves this by providing the Dataset class, which breaks a set of parquet or csv files into into a collection of cudf.DataFrame chunks that can fit in device memory. Under the hood, the data decomposition corresponds to the construction of a dask_cudf.DataFrame object. By representing our dataset as a lazily-evaluated Dask collection, we can handle the calculation of complex global statistics (and later, can also iterate over the partitions while feeding data into a neural network).

[7]:
train_dataset = nvt.Dataset(TRAIN_PATH)
valid_dataset = nvt.Dataset(VALID_PATH)

Now that we have our datasets, we’ll apply our Workflow to them and save the results out to parquet files for fast reading at train time. We’ll also measure and record statistics on our training set using the record_stats=True kwarg so that our Workflow can use them at apply time.

[8]:
PREPROCESS_DIR = os.path.join(DATA_DIR, 'ross_pre')
PREPROCESS_DIR_TRAIN = os.path.join(PREPROCESS_DIR, 'train')
PREPROCESS_DIR_VALID = os.path.join(PREPROCESS_DIR, 'valid')

! rm -rf $PREPROCESS_DIR # remove previous trials
! mkdir -p $PREPROCESS_DIR_TRAIN
! mkdir -p $PREPROCESS_DIR_VALID
[9]:
proc.apply(train_dataset, record_stats=True, output_path=PREPROCESS_DIR_TRAIN, shuffle=nvt.io.Shuffle.PER_WORKER, out_files_per_proc=2)
proc.apply(valid_dataset, record_stats=False, output_path=PREPROCESS_DIR_VALID, shuffle=None)

Finalize columns

The FastAI workflow will use nvtabular.loader.torch.TorchAsyncItr, which will map a dataset to its corresponding PyTorch tensors. In order to make sure it runs correctly, we’ll call the create_final_cols method to let the Workflow know to build the output dataset schema, and then we’ll be sure to remove instances of the label column that got added to that schema when we performed processing on it.

[10]:
proc.create_final_cols()
# using log op and normalize on sales column causes it to get added to
# continuous columns_ctx, so we'll remove it here
while True:
    try:
        proc.columns_ctx['final']['cols']['continuous'].remove(LABEL_COLUMNS[0])
    except ValueError:
        break

Training a Network

Now that our data is preprocessed and saved out, we can leverage datasets to read through the preprocessed parquet files in an online fashion to train neural networks.

We’ll start by setting some universal hyperparameters for our model and optimizer. These settings will be shared across all of the frameworks that we explore below.

If you’re interested in contributing to NVTabular, feel free to take this challenge on and submit a pull request if successful. 12% RMSPE is achievable using the Novograd optimizer, but we know of no Novograd implementation for TensorFlow that supports sparse gradients, and so we are not including that solution below.

[12]:
EMBEDDING_DROPOUT_RATE = 0.04
DROPOUT_RATES = [0.001, 0.01]
HIDDEN_DIMS = [1000, 500]
BATCH_SIZE = 65536
LEARNING_RATE = 0.001
EPOCHS = 25

# TODO: Calculate on the fly rather than recalling from previous analysis.
MAX_SALES_IN_TRAINING_SET = 38722.0
MAX_LOG_SALES_PREDICTION = 1.2 * math.log(MAX_SALES_IN_TRAINING_SET + 1.0)

# It's possible to use defaults defined within NVTabular.
EMBEDDING_TABLE_SHAPES = {
    column: shape for column, shape in
        nvt.ops.get_embedding_sizes(proc).items()
}

# Here, however, we will use fast.ai's rule for embedding sizes.
for col in EMBEDDING_TABLE_SHAPES:
    EMBEDDING_TABLE_SHAPES[col] = (EMBEDDING_TABLE_SHAPES[col][0], min(600, round(1.6 * EMBEDDING_TABLE_SHAPES[col][0] ** 0.56)))

TRAIN_PATHS = sorted(glob.glob(os.path.join(PREPROCESS_DIR_TRAIN, '*.parquet')))
VALID_PATHS = sorted(glob.glob(os.path.join(PREPROCESS_DIR_VALID, '*.parquet')))

The following shows the cardinality of each categorical variable along with its associated embedding size. Each entry is of the form (cardinality, embedding_size).

[13]:
EMBEDDING_TABLE_SHAPES
[13]:
{'Assortment': (4, 3),
 'CompetitionMonthsOpen': (26, 10),
 'CompetitionOpenSinceYear': (24, 9),
 'Day': (32, 11),
 'DayOfWeek': (8, 5),
 'Events': (22, 9),
 'Month': (13, 7),
 'Promo2SinceYear': (9, 5),
 'Promo2Weeks': (27, 10),
 'PromoInterval': (4, 3),
 'Promo_bw': (7, 5),
 'Promo_fw': (7, 5),
 'SchoolHoliday_bw': (9, 5),
 'SchoolHoliday_fw': (9, 5),
 'State': (13, 7),
 'StateHoliday': (3, 3),
 'StateHoliday_bw': (4, 3),
 'StateHoliday_fw': (4, 3),
 'Store': (1116, 81),
 'StoreType': (5, 4),
 'Week': (53, 15),
 'Year': (4, 3)}

Choose a Framework

We’re now ready to move on to framework-specific code.

The code for each framework can be run independently of the others, so feel free to skip to your framework of choice.

TensorFlow

TensorFlow: Preparing Datasets

KerasSequenceLoader wraps a lightweight iterator around a dataset object to handle chunking, shuffling, and application of any workflows (which can be applied online as a preprocessing step). For column names, can use either a list of string names or a list of TensorFlow feature_columns that will be used to feed the network

[ ]:
import tensorflow as tf

# we can control how much memory to give tensorflow with this environment variable
# IMPORTANT: make sure you do this before you initialize TF's runtime, otherwise
# it's too late and TF will have claimed all free GPU memory
os.environ['TF_MEMORY_ALLOCATION'] = "8192" # explicit MB
os.environ['TF_MEMORY_ALLOCATION'] = "0.5" # fraction of free memory
from nvtabular.loader.tensorflow import KerasSequenceLoader, KerasSequenceValidater

# cheap wrapper to keep things some semblance of neat
def make_categorical_embedding_column(name, dictionary_size, embedding_dim):
    return tf.feature_column.embedding_column(
        tf.feature_column.categorical_column_with_identity(name, dictionary_size),
        embedding_dim
    )

# instantiate our columns
categorical_columns = [
    make_categorical_embedding_column(name, *EMBEDDING_TABLE_SHAPES[name]) for
        name in CATEGORICAL_COLUMNS
]
continuous_columns = [
    tf.feature_column.numeric_column(name, (1,)) for name in CONTINUOUS_COLUMNS
]

# feed them to our datasets
train_dataset_tf = KerasSequenceLoader(
    TRAIN_PATHS, # you could also use a glob pattern
    feature_columns=categorical_columns+continuous_columns,
    batch_size=BATCH_SIZE,
    label_names=LABEL_COLUMNS,
    shuffle=True,
    buffer_size=0.06 # amount of data, as a fraction of GPU memory, to load at once
)

valid_dataset_tf = KerasSequenceLoader(
    VALID_PATHS, # you could also use a glob pattern
    feature_columns=categorical_columns+continuous_columns,
    batch_size=BATCH_SIZE*4,
    label_names=LABEL_COLUMNS,
    shuffle=False,
    buffer_size=0.06 # amount of data, as a fraction of GPU memory, to load at once
)

TensorFlow: Defining a Model

Using Keras, we can define the layers of our model and their parameters explicitly. Here, for the sake of consistency, we’ll mimic fast.ai’s TabularModel.

[18]:
# DenseFeatures layer needs a dictionary of {feature_name: input}
categorical_inputs = {}
for column_name in CATEGORICAL_COLUMNS:
    categorical_inputs[column_name] = tf.keras.Input(name=column_name, shape=(1,), dtype=tf.int64)
categorical_embedding_layer = tf.keras.layers.DenseFeatures(categorical_columns)
categorical_x = categorical_embedding_layer(categorical_inputs)
categorical_x = tf.keras.layers.Dropout(EMBEDDING_DROPOUT_RATE)(categorical_x)

# Just concatenating continuous, so can use a list
continuous_inputs = []
for column_name in CONTINUOUS_COLUMNS:
    continuous_inputs.append(tf.keras.Input(name=column_name, shape=(1,), dtype=tf.float32))
continuous_embedding_layer = tf.keras.layers.Concatenate(axis=1)
continuous_x = continuous_embedding_layer(continuous_inputs)
continuous_x = tf.keras.layers.BatchNormalization(epsilon=1e-5, momentum=0.1)(continuous_x)

# concatenate and build MLP
x = tf.keras.layers.Concatenate(axis=1)([categorical_x, continuous_x])
for dim, dropout_rate in zip(HIDDEN_DIMS, DROPOUT_RATES):
    x = tf.keras.layers.Dense(dim, activation='relu')(x)
    x = tf.keras.layers.BatchNormalization(epsilon=1e-5, momentum=0.1)(x)
    x = tf.keras.layers.Dropout(dropout_rate)(x)
x = tf.keras.layers.Dense(1, activation='linear')(x)

# TODO: Initialize model weights to fix saturation issues.
# For now, we'll just scale the output of our model directly before
# hitting the sigmoid.
x = 0.1 * x

x = MAX_LOG_SALES_PREDICTION * tf.keras.activations.sigmoid(x)

# combine all our inputs into a single list
# (note that you can still use .fit, .predict, etc. on a dict
# that maps input tensor names to input values)
inputs = list(categorical_inputs.values()) + continuous_inputs
tf_model = tf.keras.Model(inputs=inputs, outputs=x)

TensorFlow: Training

[19]:
def rmspe_tf(y_true, y_pred):
    # map back into "true" space by undoing transform
    y_true = tf.exp(y_true) - 1
    y_pred = tf.exp(y_pred) - 1

    percent_error = (y_true - y_pred) / y_true
    return tf.sqrt(tf.reduce_mean(percent_error**2))
[20]:
optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE)
tf_model.compile(optimizer, 'mse', metrics=[rmspe_tf])
[21]:
%%time

optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE)
tf_model.compile(optimizer, 'mse', metrics=[rmspe_tf])

validation_callback = KerasSequenceValidater(valid_dataset_tf)
history = tf_model.fit(
    train_dataset_tf,
    callbacks=[validation_callback],
    epochs=EPOCHS,
)
Epoch 1/25
13/13 [==============================] - 3s 196ms/step - loss: 5.9396 - rmspe_tf: 0.8938 - val_loss: 6.6637 - val_rmspe_tf: 0.8936
Epoch 2/25
13/13 [==============================] - 2s 183ms/step - loss: 5.2245 - rmspe_tf: 0.8907 - val_loss: 6.1371 - val_rmspe_tf: 0.8905
Epoch 3/25
13/13 [==============================] - 2s 183ms/step - loss: 4.5340 - rmspe_tf: 0.8758 - val_loss: 5.6532 - val_rmspe_tf: 0.8746
Epoch 4/25
13/13 [==============================] - 2s 182ms/step - loss: 3.6554 - rmspe_tf: 0.8463 - val_loss: 5.0402 - val_rmspe_tf: 0.8446
Epoch 5/25
13/13 [==============================] - 2s 179ms/step - loss: 2.6135 - rmspe_tf: 0.7926 - val_loss: 4.3402 - val_rmspe_tf: 0.7893
Epoch 6/25
13/13 [==============================] - 2s 178ms/step - loss: 1.5798 - rmspe_tf: 0.7018 - val_loss: 3.6970 - val_rmspe_tf: 0.6926
Epoch 7/25
13/13 [==============================] - 2s 177ms/step - loss: 0.7687 - rmspe_tf: 0.5661 - val_loss: 3.1945 - val_rmspe_tf: 0.5554
Epoch 8/25
13/13 [==============================] - 2s 179ms/step - loss: 0.2935 - rmspe_tf: 0.3976 - val_loss: 2.8964 - val_rmspe_tf: 0.3929
Epoch 9/25
13/13 [==============================] - 2s 175ms/step - loss: 0.0953 - rmspe_tf: 0.2716 - val_loss: 2.7912 - val_rmspe_tf: 0.2707
Epoch 10/25
13/13 [==============================] - 2s 183ms/step - loss: 0.0439 - rmspe_tf: 0.2224 - val_loss: 2.7896 - val_rmspe_tf: 0.2217
Epoch 11/25
13/13 [==============================] - 2s 185ms/step - loss: 0.0352 - rmspe_tf: 0.2313 - val_loss: 2.8199 - val_rmspe_tf: 0.2347
Epoch 12/25
13/13 [==============================] - 2s 185ms/step - loss: 0.0329 - rmspe_tf: 0.2238 - val_loss: 2.7948 - val_rmspe_tf: 0.2231
Epoch 13/25
13/13 [==============================] - 2s 180ms/step - loss: 0.0314 - rmspe_tf: 0.2107 - val_loss: 2.7997 - val_rmspe_tf: 0.2112
Epoch 14/25
13/13 [==============================] - 2s 186ms/step - loss: 0.0297 - rmspe_tf: 0.2126 - val_loss: 2.8045 - val_rmspe_tf: 0.2136
Epoch 15/25
13/13 [==============================] - 2s 179ms/step - loss: 0.0288 - rmspe_tf: 0.2044 - val_loss: 2.7987 - val_rmspe_tf: 0.2053
Epoch 16/25
13/13 [==============================] - 2s 182ms/step - loss: 0.0280 - rmspe_tf: 0.1959 - val_loss: 2.7946 - val_rmspe_tf: 0.1969
Epoch 17/25
13/13 [==============================] - 2s 179ms/step - loss: 0.0269 - rmspe_tf: 0.1930 - val_loss: 2.7928 - val_rmspe_tf: 0.1934
Epoch 18/25
13/13 [==============================] - 2s 181ms/step - loss: 0.0260 - rmspe_tf: 0.1917 - val_loss: 2.7913 - val_rmspe_tf: 0.1922
Epoch 19/25
13/13 [==============================] - 2s 179ms/step - loss: 0.0255 - rmspe_tf: 0.1897 - val_loss: 2.7991 - val_rmspe_tf: 0.1913
Epoch 20/25
13/13 [==============================] - 2s 178ms/step - loss: 0.0247 - rmspe_tf: 0.1864 - val_loss: 2.7876 - val_rmspe_tf: 0.1869
Epoch 21/25
13/13 [==============================] - 2s 180ms/step - loss: 0.0245 - rmspe_tf: 0.1873 - val_loss: 2.7923 - val_rmspe_tf: 0.1880
Epoch 22/25
13/13 [==============================] - 2s 183ms/step - loss: 0.0243 - rmspe_tf: 0.1863 - val_loss: 2.7913 - val_rmspe_tf: 0.1870
Epoch 23/25
13/13 [==============================] - 2s 182ms/step - loss: 0.0248 - rmspe_tf: 0.1868 - val_loss: 2.8033 - val_rmspe_tf: 0.1892
Epoch 24/25
13/13 [==============================] - 2s 185ms/step - loss: 0.0235 - rmspe_tf: 0.1778 - val_loss: 2.7982 - val_rmspe_tf: 0.1799
Epoch 25/25
13/13 [==============================] - 2s 177ms/step - loss: 0.0233 - rmspe_tf: 0.1763 - val_loss: 2.7765 - val_rmspe_tf: 0.1773
CPU times: user 2min 3s, sys: 29.3 s, total: 2min 33s
Wall time: 1min 9s

PyTorch

PyTorch: Preparing Datasets

[ ]:
import torch
from nvtabular.loader.torch import TorchAsyncItr, DLDataLoader
from nvtabular.framework_utils.torch.models import Model
from nvtabular.framework_utils.torch.utils import process_epoch

# TensorItrDataset returns a single batch of x_cat, x_cont, y.
collate_fn = lambda x: x

train_dataset = TorchAsyncItr(nvt.Dataset(TRAIN_PATHS), batch_size=BATCH_SIZE, cats=CATEGORICAL_COLUMNS, conts=CONTINUOUS_COLUMNS, labels=LABEL_COLUMNS)
train_loader = DLDataLoader(train_dataset, batch_size=None, collate_fn=collate_fn, pin_memory=False, num_workers=0)

valid_dataset = TorchAsyncItr(nvt.Dataset(VALID_PATHS), batch_size=BATCH_SIZE, cats=CATEGORICAL_COLUMNS, conts=CONTINUOUS_COLUMNS, labels=LABEL_COLUMNS)
valid_loader = DLDataLoader(valid_dataset, batch_size=None, collate_fn=collate_fn, pin_memory=False, num_workers=0)

PyTorch: Defining a Model

[ ]:
model = Model(
    embedding_table_shapes=EMBEDDING_TABLE_SHAPES,
    num_continuous=len(CONTINUOUS_COLUMNS),
    emb_dropout=EMBEDDING_DROPOUT_RATE,
    layer_hidden_dims=HIDDEN_DIMS,
    layer_dropout_rates=DROPOUT_RATES,
    max_output=MAX_LOG_SALES_PREDICTION
).to('cuda')

PyTorch: Training

[ ]:
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
[ ]:
def rmspe_func(y_pred, y):
    "Return y_pred and y to non-log space and compute RMSPE"
    y_pred, y = torch.exp(y_pred) - 1, torch.exp(y) - 1
    pct_var = (y_pred - y) / y
    return (pct_var**2).mean().pow(0.5)
[ ]:
%%time

for epoch in range(EPOCHS):
    train_loss, y_pred, y = process_epoch(train_loader, model, train=True, optimizer=optimizer)
    train_rmspe = rmspe_func(y_pred, y)
    valid_loss, y_pred, y = process_epoch(valid_loader, model, train=False)
    valid_rmspe = rmspe_func(y_pred, y)
    print(f'Epoch {epoch:02d}. Train loss: {train_loss:.4f}. Train RMSPE: {train_rmspe:.4f}. Valid loss: {valid_loss:.4f}. Valid RMSPE: {valid_rmspe:.4f}.')

fast.ai

fast.ai: Preparing Datasets

AsyncTensorBatchDatasetItr maps a symbolic dataset object to cat_features, cont_features, labels PyTorch tenosrs by iterating through the dataset and concatenating the results.

[ ]:
import fastai

fastai.__version__
[ ]:
import torch
from nvtabular.loader.torch import TorchAsyncItr, DLDataLoader
from fastai.tabular.data import TabularDataLoaders
from fastai.tabular.model import TabularModel
from fastai.basics import Learner
from fastai.basics import MSELossFlat
from fastai.callback.progress import ProgressCallback

def make_batched_dataloader(paths, columns, batch_size):
    dataset = nvt.Dataset(paths)
    ds_batch_sets = TorchAsyncItr(dataset,
                                  batch_size=batch_size,
                                  cats=CATEGORICAL_COLUMNS,
                                  conts=CONTINUOUS_COLUMNS,
                                  labels=LABEL_COLUMNS)
    return DLDataLoader(
        ds_batch_sets,
        batch_size=None,
        pin_memory=False,
        num_workers=0
    )

train_dataset_pt = make_batched_dataloader(TRAIN_PATHS, COLUMNS, BATCH_SIZE)
valid_dataset_pt = make_batched_dataloader(VALID_PATHS, COLUMNS, BATCH_SIZE*4)
[ ]:
databunch = TabularDataLoaders(train_dataset_pt, valid_dataset_pt)

fast.ai: Defining a Model

Next we’ll need to define the inputs that will feed our model and build an architecture on top of them. For now, we’ll just stick to a simple MLP model.

Using FastAI’s TabularModel, we can build an MLP under the hood by defining its high-level characteristics.

[ ]:
pt_model = TabularModel(
    emb_szs=list(EMBEDDING_TABLE_SHAPES.values()),
    n_cont=len(CONTINUOUS_COLUMNS),
    out_sz=1,
    layers=HIDDEN_DIMS,
    ps=DROPOUT_RATES,
    use_bn=True,
    embed_p=EMBEDDING_DROPOUT_RATE,
    y_range=torch.tensor([0.0, MAX_LOG_SALES_PREDICTION]),
).cuda()

fast.ai: Training

[ ]:
from fastai.torch_core import flatten_check

def exp_rmspe(pred, targ):
    "Exp RMSE between `pred` and `targ`."
    pred,targ = flatten_check(pred,targ)
    pred, targ = torch.exp(pred)-1, torch.exp(targ)-1
    pct_var = (targ - pred)/targ
    return torch.sqrt((pct_var**2).mean())

loss_func = MSELossFlat()
learner = Learner(databunch, pt_model, loss_func=loss_func, metrics=[exp_rmspe], cbs=ProgressCallback())
learner.fit(EPOCHS, LEARNING_RATE)
[ ]: