1o9cygxvhoxdqpq2dyorshg.png
Imagen generada por Dall-e

La ciencia de datos alcanza su máximo potencial en el mundo real. Tengo la intención de compartir conocimientos adquiridos a partir de varios proyectos de producción en los que he participado.

Durante mis años de trabajo como científico de datos, he conocido a muchos estudiantes interesados ​​en convertirse en uno de ellos o recién graduados que están empezando. Comenzar una carrera en ciencia de datos, como en cualquier campo, implica una curva de aprendizaje pronunciada.

Una muy buena pregunta que me siguen haciendo es: He aprendido mucho sobre los aspectos teóricos de la ciencia de datos, pero ¿cómo se ve un ejemplo del mundo real?

Quiero compartir pequeños fragmentos de trabajo de diferentes proyectos en los que he estado trabajando a lo largo de mi carrera. Aunque algunos tengan algunos años, solo escribiré sobre temas que sigo considerando relevantes. Intentaré mantener una visión general clara y concisa, para que los nuevos colegas que aspiren a ello puedan hacerse una idea de lo que puede venir. Pero también quiero detenerme y analizar los detalles, de los que espero que los desarrolladores más experimentados puedan sacar algunas conclusiones.

Caso de negocio

Ahora, analicemos en profundidad el caso de negocio específico que impulsó esta iniciativa. El equipo estaba formado por un director de proyectos, las partes interesadas del cliente y yo mismo. El cliente necesitaba una forma de pronosticar el uso de un servicio específico. La razón detrás de esto era la asignación de recursos para mantener el servicio y los precios dinámicos. La experiencia con el comportamiento sobre el uso del servicio se conservaba principalmente en los compañeros de trabajo cualificados, y esta aplicación era una forma de ser más resilientes ante su jubilación junto con sus conocimientos. Además, se pensó que el proceso de incorporación de nuevos empleados sería más fácil con este tipo de herramienta a mano.

Configuración de datos y análisis

Los datos tenían muchas características, tanto categóricas como numéricas. Para el caso de uso, era necesario pronosticar el uso con un horizonte dinámico, es decir, hacer predicciones para diferentes períodos de tiempo en el futuro. También era necesario pronosticar muchos valores, correlacionados y no correlacionados.

Estas series temporales multivariadas hicieron que la atención se centrara principalmente en la experimentación con modelos basados ​​en series temporales, pero finalmente se adoptó Tabnet, un modelo que procesa los datos de forma tabular.

La arquitectura Tabnet tiene varias características interesantes. Este artículo no profundizará en los detalles del modelo, pero para obtener los antecedentes teóricos, recomiendo investigar un poco. Si no encuentras buenos recursos, te recomiendo que consultes Este artículo una buena visión general o Este documento para una exploración más profunda.

Como marco de trabajo de ajuste de hiperparámetros, se utilizó Optuna. También hay otros marcos de trabajo en Python que se pueden utilizar, pero aún no he encontrado una razón para no utilizar Optuna. Optuna se utilizó como un ajuste de hiperparámetros bayesiano, guardado en el disco. Otras características utilizadas son la detención temprana y el inicio en caliente. La detención temprana se utiliza para ahorrar recursos, no dejando que los ensayos que no parecen prometedores se ejecuten durante demasiado tiempo. El inicio en caliente es la capacidad de comenzar desde ensayos anteriores. Esto me resulta útil cuando llegan nuevos datos y no tener que comenzar el ajuste desde cero.

Los anchos de los parámetros iniciales se establecerán según lo recomendado en el Documentación de Tabnet o de los rangos de parámetros discutidos en el Papel tabulador.

Para transmitir la naturaleza heterocedástica de los residuos, se implementó Tabnet como un modelo de regresión cuantil. Para hacer esto, o para implementar cualquier modelo de esta manera, se utilizó Función de pérdida de pinballSe utilizó una función de pérdida con cuantiles superiores e inferiores adecuados. Esta función de pérdida tiene una función de pérdida sesgada, que castiga los errores de manera desigual según sean positivos o negativos.

Tutorial con código

Los requisitos utilizados para estos fragmentos son los siguientes.

pytorch-tabnet==4.1.0
optuna==3.6.1
pandas==2.1.4

Código para definir el modelo.

import os

from pytorch_tabnet.tab_model import TabNetRegressor
import pandas as pd
import numpy as np

from utils import CostumPinballLoss

class mediumTabnetModel:

def __init__(self,
model_file_name,
dependent_variables=None,
independent_variables=None,
batch_size=16_000,
n_a=8,
n_steps=3,
n_independent=2,
n_shared=2,
cat_idxs=[],
cat_dims=[],
quantile=None):
self.model_file_name = model_file_name
self.quantile = quantile
self.clf = TabNetRegressor(n_d=n_a,
n_a=n_a,
cat_idxs=cat_idxs,
cat_dims=cat_dims,
n_steps=n_steps,
n_independent=n_independent,
n_shared=n_shared)
self.batch_size = batch_size
self.independent_variables = independent_variables
self.dependent_variables = dependent_variables
self.cat_idxs = cat_idxs # Indexes for categorical values.
self.cat_dims = cat_dims # Dimensions for categorical values.
self.ram_data = None

def fit(self, training_dir, train_date_split):

if self.ram_data is None:
data_path = os.path.join(training_dir, self.training_data_file)
df = pd.read_parquet(data_path)

df_train = df[df['dates'] < train_date_split]
df_val = df[df['dates'] >= train_date_split]

x_train = df_train[self.independent_variables].values.astype(np.int16)
y_train = df_train[self.dependent_variables].values.astype(np.int32)

x_valid = df_val[self.independent_variables].values.astype(np.int16)
y_valid = df_val[self.dependent_variables].values.astype(np.int32)

self.ram_data = {'x_train': x_train,
'y_train': y_train,
'x_val': x_valid,
'y_val': y_valid}

self.clf.fit(self.ram_data['x_train'],
self.ram_data['y_train'],
eval_set=[(self.ram_data['x_val'],
self.ram_data['y_val'])],
batch_size=self.batch_size,
drop_last=True,
loss_fn=CostumPinballLoss(quantile=self.quantile),
eval_metric=[CostumPinballLoss(quantile=self.quantile)],
patience=3)

feat_score = dict(zip(self.independent_variables, self.clf.feature_importances_))
feat_score = dict(sorted(feat_score.items(), key=lambda item: item[1]))
self.feature_importances_dict = feat_score
# Dict of feature importance and importance score, ordered.

Como marco de manipulación de datos se utilizó Pandas. También recomendaría utilizar Polars, ya que es un marco más eficiente.

La implementación de Tabnet incluye un atributo de importancia de características local y global predefinido para el modelo ajustado. El funcionamiento interno de este atributo se puede estudiar en el artículo publicado anteriormente, pero, en el caso de uso comercial, esto cumple dos propósitos:

  • Comprobación de cordura: el cliente puede validar el modelo.
  • Información empresarial: el modelo puede proporcionar nuevos conocimientos sobre el negocio al cliente.

Junto con los expertos en la materia. En la aplicación final, se incluyó la interpretabilidad para que se la mostrara al usuario. Debido a la anonimización de los datos, no se profundizará en la interpretabilidad en este artículo, sino que se guardará para un caso en el que se puedan analizar y mostrar las verdaderas características que se incluyen en el modelo.

Código para los pasos de ajuste y búsqueda.

import optuna
import numpy as np

def define_model(trial):
n_shared = trial.suggest_int('n_shared', 1, 7)
logging.info(f'n_shared: {n_shared}')

n_independent = trial.suggest_int('n_independent', 1, 16)
logging.info(f'n_independent: {n_independent}')

n_steps = trial.suggest_int('n_steps', 2, 8)
logging.info(f'n_steps: {n_steps}')

n_a = trial.suggest_int('n_a', 4, 32)
logging.info(f'n_a: {n_a}')

batch_size = trial.suggest_int('batch_size', 256, 18000)
logging.info(f'batch_size: {batch_size}')

clf = mediumTabnetModel(model_file_name=model_file_name,
dependent_variables=y_ls,
independent_variables=x_ls,
n_a=n_a,
cat_idxs=cat_idxs,
cat_dims=cat_dims,
n_steps=n_steps,
n_independent=n_independent,
n_shared=n_shared,
batch_size=batch_size,
training_data_file=training_data_file)

return clf

def objective(trial):
clf = define_model(trial)

clf.fit(os.path.join(args.training_data_directory, args.dataset),
df[int(len(df) * split_test)])

y_pred = clf.predict(predict_data)
y_true = np.array(predict_data[y_ls].values).astype(np.int32)

metric_value = call_metrics(y_true, y_pred)

return metric_value

study = optuna.create_study(direction='minimize',
storage='sqlite:///db.sqlite3',
study_name=model_name,
load_if_exists=True)

study.optimize(objective,
n_trials=50)

Los datos se dividen en un conjunto de entrenamiento, uno de validación y uno de prueba. Los usos de los diferentes conjuntos de datos son los siguientes:

  • Tren. Este es el conjunto de datos del que aprende el modelo. En este proyecto consta en un 80 %.
  • Validación. Es el conjunto de datos a partir del cual Optuna calcula sus métricas y, por lo tanto, la métrica está optimizada para el 10 % de los datos de este proyecto.
  • Prueba. Este es el conjunto de datos que se utiliza para determinar el verdadero rendimiento del modelo. Si esta métrica no es lo suficientemente buena, puede que valga la pena volver a investigar otros modelos. Este conjunto de datos también se utiliza para decidir cuándo es el momento de detener el ajuste de hiperparámetros. También es sobre la base de este conjunto de datos que se derivan los KPI y se comparten las visualizaciones con las partes interesadas.

Una última observación es que, para imitar el comportamiento de cuando se implementa el modelo, en la medida de lo posible, los conjuntos de datos se dividen en el tiempo. Esto significa que los datos del primer 80 % del período se destinan a la parte de entrenamiento, el siguiente 10 % se destina a la validación y el 10 % más reciente a la prueba.