Entrenamiento y despliegue de modelos con BaseModelService

¿Qué es BaseModelService?

BaseModelService es una clase Python que se distribuye como parte del cliente de Python para Onesait Platform (OSP en adelante). El código de este cliente lo mantiene la comunidad de OSP en Github. Y puede ser instalado mediante pip:

 

1 pip install onesaitplatform-client-services

BaseModelService permite:

  • Entrenar modelos.

  • Reentrenar modelos para generar nuevas versiones de estos.

  • Desplegar los modelos entrenados.

  • Hacer inferencia con los modelos desplegados.

Todo ello se hace aprovechando las herramientas que OSP proporciona para:

  • La gestión de los datasets de entrenamiento.

  • El almacenaje de los modelos entrenados.

  • El control de sus distintas versiones.

  • El despliegue mediante microservicios.

BaseModelService abstrae al desarrollador del modelo la gestión de toda esta funcionalidad, permitiéndole hacer uso de ella de una manera sencilla. Como sugiere su nombre, BaseModelService es una clase madre a partir de la cual el desarrollador del modelo creará una clase hija que herede de ella.

La clase hija contendrá el código específico para entrenar un modelo concreto, guardarlo en una ruta local, cargar posteriormente la versión guardada también desde una ruta local y usarlo en inferencia. El desarrollador del modelo podrá usar cualquier tipo de librería de Python (scikit-learn, Tensorflow, PyTorch, etc.). Podrá también usar los mecanismos de guardado y carga de modelo que prefiera.

El resto de tareas de interacción con OSP ya han sido definidas en la clase madre BaseModelService: descarga del dataset desde un archivo del File Repository, o desde una ontología; guardado de los modelos entrenados en el File Repository, descarga de estos modelos desde dicho File Repository, control de las distintas versiones de un mismo modelo y selección de la versión preferida.

¿Qué hace falta tener en OSP?

OSP proporciona el soporte para la gestión y almacenaje de datasets y modelos. Para ello, hace falta configurar lo siguiente:

  • Un dataset en el File Repository. Será el dataset que se utilizará para entrenar el modelo.

  • Alternativamente, una ontología en la que se guarde el dataset en cuestión.

  • Una ontología en la que se registren las distintas versiones del modelo.

  • Un Digital Client al que se asocien las ontologías anteriores y permita el acceso a ellas.

La estructura de la ontología de registro de versiones del modelo ha de ser la siguiente. En este caso, la ontología creada se ha llamado SentimentAnalysisModels:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 { "$schema": "http://json-schema.org/draft-04/schema#", "title": "SentimentAnalysisModels", "type": "object", "required": [ "SentimentAnalysisModels" ], "properties": { "SentimentAnalysisModels": { "type": "string", "$ref": "#/datos" } }, "datos": { "description": "Info SentimentAnalysisModels", "type": "object", "required": [ "name", "description", "asset", "version", "metrics", "hyperparameters", "model_path", "date", "active" ], "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "asset": { "type": "string" }, "version": { "type": "string" }, "metrics": { "type": "array", "items": { "type": "object", "required": [ "name", "value" ], "properties": { "name": { "type": "string" }, "value": { "type": "string" }, "dtype": { "type": "string" } }, "additionalProperties": false }, "minItems": 0 }, "hyperparameters": { "type": "array", "items": { "type": "object", "required": [ "name", "value" ], "properties": { "name": { "type": "string" }, "value": { "type": "string" }, "dtype": { "type": "string" } }, "additionalProperties": false }, "minItems": 0 }, "model_path": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, "dataset_path": { "type": "string" }, "active": { "type": "boolean" }, "ontology_dataset": { "type": "string" } } }, "description": "Definition of trained models", "additionalProperties": true }

 

Los campos de la ontología, como se ve arriba, son los siguientes:

  • name: nombre del modelo.

  • description: descripción del modelo.

  • asset: nombre del activo en el cual se encuadra el modelo.

  • version: versión del modelo.

  • metrics: lista de criterios de evaluación del modelo (cada criterio de la lista consta de un campo name, nombre del criterio; un campo value, valor, para ese criterio; y dtype, tipo de dato del valor).

  • hyperparameters: lista de hiperparámetros con los cuales se ha entrenado el modelo (cada hiperparámetro de la lista consta de un campo name, nombre del hiperparámetro; otro value, valor del hiperparámetro; y dtype, tipo de dato del valor).

  • model_path: identificador del fichero correspondiente al modelo guardado en File Repository.

  • date: fecha y hora en la que se crea el modelo.

  • dataset_path: identificador del fichero correspondiente al dataset de entrenamiento utilizado para el entrenamiento en File Repository.

  • active: booleano que denota si una versión del modelo está activa. Normalmente, solo una versión del modelo estará activa, y será esta la que se carga como modelo servicializado.

  • ontology_dataset: nombre de la ontología en la que se ha guardado el dataset.

Creación de una clase hija de BaseModelService: SentimentAnalysisModelService

Para crear un objeto que gestione el entrenamiento, guardado, carga, despliegue y uso de un modelo concreto, se ha de crear una clase que herede de BaseModelService. En este tutorial se va a crear una clase que gestione modelos de sentiment analysis. Se le llamará, entonces, SentimentAnalysisModelService:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from onesaitplatform.model import BaseModelService class SentimentAnalysisModelService(BaseModelService): """Service for models of Sentiment Analysis""" def __init__(self, **kargs): """ YOUR CODE HERE """ super().__init__(**kargs) def load_model(self, model_path=None, hyperparameters=None): """Loads a previously trained model and save it a one or more object attributes""" """ YOUR CODE HERE """ def train(self, dataset_path=None, hyperparameters=None, model_path=None): """ Trains a model given a dataset and saves it in a local path. Returns a dictionary with the obtained metrics """ """ YOUR CODE HERE """ return metrics def predict(self, inputs=None): """Predicts given a model and an array of inputs""" """ YOUR CODE HERE """ return results

 

Como se ve arriba, la clase hija ha de sobrescribir los métodos init, load_model, train y predict de BaseModelService.

En concreto, se va a crear una clase SentimentAnalysisModelService que gestione modelos de sentiment analysis sobre datos del español. Será un clasificador binario de texto: el output será 0 para textos con sentiment negativo, 1 para los textos con sentiment positivo. Se va a usar para ello Tensorflow 2.x. Se construirá un perceptrón cuyo input será un bag of words con tf-idf. Serán modelos de juguete. No se pretende elaborar buenos modelos, solo mostrar cómo desarrollarlos de manera sencilla. El guardado del modelo se hará mediante h5 y pickle: un fichero con los pesos resultantes del entrenamiento y otro fichero para el tokenizador que se entrena para hacer el preprocesamiento del texto.

Se importan, entonces, las librerías y clases necesarias para construir el modelo tal y como se ha descrito. Junto a ellas, se importa la clase BaseModelService:

 

1 2 3 4 5 6 import numpy as np import tensorflow.keras from tensorflow.keras.preprocessing import text from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Activation, Dropout from onesaitplatform.model import BaseModelService

 

Sobrescribir el método init

En el método init se inicializarán los atributos que después se utilizarán en otros métodos para hacer referencia al modelo. En concreto, para el modelo de sentiment que se va a desarrollar en este tutorial, se van a habilitar dos atributos: uno en el que se guardará el modelo mismo (model), la red neuronal que devolverá 1 para textos de sentiment positivo y 0 para textos con sentiment negativo; y otro para el preprocesador de texto (preprocessor):

 

1 2 3 4 5 6 7 class SentimentAnalysisModelService(BaseModelService): """Service for models of Sentiment Analysis""" def __init__(self, **kargs): self.model = None self.preprocessor = None super().__init__(**kargs)

 

Sobrescribir el método load_model

El método load_model es el encargado de construir el modelo que va a servicializarse a partir del archivo o archivos en los que este haya sido previamente guardado. Este método se ejecuta en el momento en el que se crea el objeto. El constructor del objeto buscará en la ontología correspondiente de OSP el modelo adecuado y lo descargará, desde el File Repository de OSP, a un directorio local. Esta descarga contendrá exactamente los ficheros y/o directorios que hayan sido creados en el momento en el que se guardó el modelo (ver método train).

Al método load_model se le pasan estos dos parámetros:

  • model_path: es la ruta del directorio local donde se ubican los ficheros y/o directorios necesarios para cargar el modelo. El desarrollador asume que en esa ruta encontrará todos los ficheros y/o directorios que creó en el momento en el que guardó el modelo que ahora se pretende cargar. Por tanto, podrá ahora reconstruir el modelo a partir de dichos elementos.

  • hyperparameters: es un diccionario con todos los hyperparámetros que se usaron para entrenar el modelo. Pudieran ser necesarios para su reconstrucción. En este ejemplo no se utilizarán.

En concreto, para la clase SentimentAnalysisModelService se asume que los modelos se guardan en dos archivos: un h5 con la red neuronal de Tensorflow y un pickle con el objeto tokenizador que preprocesa el texto. Por tanto, se asume que estos dos archivos han de ser proporcionados dentro del directorio model_path:

  • model.h5

  • tokenizer.pkl

La red neuronal se guarda en el atributo model, mientras que el tokenizador se guarda en el atributo preprocessor (ambos previamente inicializados en el método init). Véase el código:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 class SentimentAnalysisModelService(BaseModelService): """Service for models of Sentiment Analysis""" ... def load_model(self, model_path=None, hyperparameters=None): """Loads a previously trained model and save it a one or more object attributes""" model_path = model_path + '/model.h5' preprocessor_path = model_path + '/preprocessor.pkl' model = keras.models.load_model(model_path) preprocessor = pickle.load(open(preprocessor_path,'rb')) self.model = model self.preprocessor = preprocessor

 

Sobrescribir el método train

El método train es el encargado de entrenar el modelo. Se ejecuta internamente cuando el desarrollador ejecuta uno de estos métodos, implementados en BaseModelService:

  • train_from_file_system: lanza el entrenamiento de un modelo a partir de un dataset previemente guardado en el File Repository de OSP.

  • train_from_ontology: lanza el entrenamiento de un modelo a partir de un dataset guardado en una ontología de OSP.

El método train recibe los siguientes parámetros:

  • dataset_path: es la ruta local al fichero en el que se provee el dataset de entrenamiento. Este fichero puede tener su origen en un fichero previamente guardado en el File Repository. En tal caso, tendrá exactamente el formato del fichero guardado. Si el origen del fichero fuera una ontología, esta se habrá convertido a un CSV con “,” como delimitador y tantas columnas como campos tengan los registros de la ontología.

  • hyperparameters: es un diccionario con los hiperparámetros que se le pasaron a los métodos train_from_file_system o train_from_ontology en el momento de lanzar el entrenamiento.

  • model_path: es la ruta al directorio local en el que hay que guardar los ficheros o directorios en los cuales se va a salvar el modelo una vez entrenado.

El desarrollador tendrá que leer el dataset desde el fichero local proporcionado en dataset_path. Con ello, alimentará el proceso de entrenamiento. Una vez terminado este, tendrá que guardar el modelo resultante en el directorio indicado en model_path. Además, el método train tiene que devolver un diccionario con las métricas de evaluación del modelo que el desarrollador considere necesarias.

En el caso de SentimentAnalysisModelTrain, se asumirá que el dataset de entrenamiento es un CSV con “,” como delimitador. Este dataset contendrá dos columnas:

  • text: con los textos que contienen las opiniones.

  • label: con un 1 para textos con opinión positiva y 0 para los de opinión negativa.

Entrenamos un modelo con Tensorflow2.x. Para el preprocesamiento de los textos usamos el tokenizador de Keras que convierte cada texto a un vector de n posiciones que representa un bag of words con tf-idf: cada posición del vector denota una palabra (siempre la misma) con un valor numérico (entre 0 y 1) que denota lo relevante que esa palabra es en el texto en cuestión. Véase el código:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class SentimentAnalysisModelService(BaseModelService): """Service for models of Sentiment Analysis""" ... def train(self, dataset_path=None, hyperparameters=None, model_path=None): """Trains a model given a dataset""" NUM_WORDS = hyperparameters['NUM_WORDS'] BATCH_SIZE = hyperparameters['BATCH_SIZE'] EPOCHS = hyperparameters['EPOCHS'] dataset = pd.read_csv(dataset_path, sep='\t') texts = dataset["text"].tolist() labels = dataset["label"].tolist() tokenizer = text.Tokenizer(num_words=NUM_WORDS) tokenizer.fit_on_texts(texts) X = tokenizer.texts_to_matrix(texts, mode="tfidf") y = np.array(labels) model = Sequential() model.add(Dense(250, input_shape=(10000,))) model.add(Activation("relu")) model.add(Dropout(0.2)) model.add(Dense(1)) model.add(Activation("sigmoid")) model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"]) model.summary() model.fit(X, Y, batch_size=BATCH_SIZE, epochs=EPOCHS, validation_split=0.25, verbose=2) evaluation = model.evaluate(x_test, y_test) metrics = {'loss': float(evaluation[0]), 'accuracy': float(evaluation[1])} nn_path = model_path + '/model.h5' preprocessor_path = model_path + '/tokenizer.h5' model.save(nn_path) pickle.dump(preprocessor, open(preprocessor_path,'wb+'), protocol=pickle.HIGHEST_PROTOCOL return metrics

 

Sobrescribir el método predict

El método predict recibe un parámetro (input) con la lista de inputs para los que se ha de hacer inferencia; calcula el output conforme al modelo, y lo devuelve en una lista. En concreto, para SentimentAnalysisModelService, se asume que el input es una lista de textos. Se tomarán, por un lado el modelo del atributo model, y por otro el preprocesador de texto del atributo preprocessor (ambos inicializados en init e instanciados en load_model) y con ellos se procesa el input. Se devuelven los resultados.

 

1 2 3 4 5 6 7 8 9 10 class SentimentAnalysisModelService(BaseModelService): """Service for models of Sentiment Analysis""" ... def predict(self, inputs=None): """Predicts given a model and an array of inputs""" X = self.preprocessor.texts_to_matrix(inputs, mode='tfidf') y = self.model.predict(X) return y

Crear un objeto, entrenar y predecir

Se asume que han sido creados los siguientes elementos en el despliegue de OSP de https://lab.onesaitplatform.com/:

  • Un dataset como archivo CSV con “,” como separador en el File Repository. El dataset tendrá dos columnas: text (con los textos) y label (con valor 1 o 0, donde 1 denota que el texto tiene sentiment positivo y 0 denota que el texto tiene sentiment negativo).

  • El mismo dataset en una ontología llamada SentimentAnalysisDataset, donde cada elemento tendrá un campo text y otro campo label.

  • Una ontología llamada SentimentAnalysisModels con la estructura mostrada arriba.

  • Un Digital Client asociado a las dos ontologías anteriores, llamado SentimentAnalysisDigitalClient.

  • La clase SentimentAnalisisModelService descrita arriba.

Con todo ello, se va a crear el objeto sentiment_analysis_model_service de clase SentimentAnalisisModelService:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 PARAMETERS = { 'PLATFORM_HOST': "lab.onesaitplatform.com", 'PLATFORM_PORT': 443, 'PLATFORM_DIGITAL_CLIENT': "SentimentAnalysisDigitalClient", 'PLATFORM_DIGITAL_CLIENT_TOKEN': "534f2eb845c746bd9a50cfab30273317", 'PLATFORM_DIGITAL_CLIENT_PROTOCOL': "https", 'PLATFORM_DIGITAL_CLIENT_AVOID_SSL_CERTIFICATE': True, 'PLATFORM_ONTOLOGY_MODELS': "SentimentAnalysisModels", 'PLATFORM_USER_TOKEN': "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmluY2lwYWwiOiJianJhbWlyZXoiLCJjbGllbnRJZCI6Im9uZXNhaXRwbGF0Zm9ybSIsInVzZXJfbmFtZSI6ImJqcmFtaXJleiIsInNjb3BlIjpbIm9wZW5pZCJdLCJuYW1lIjoiYmpyYW1pcmV6IiwiZXhwIjoxNjE3ODI2NjkzLCJncmFudFR5cGUiOiJwYXNzd29yZCIsInBhcmFtZXRlcnMiOnsidmVydGljYWwiOm51bGwsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJuYW1lIjoiYmpyYW1pcmV6In0sImF1dGhvcml0aWVzIjpbIlJPTEVfREFUQVNDSUVOVElTVCJdLCJqdGkiOiJmNGM2NDUzZC0xYTEyLTRkMGUtYTVlNy05ZmNlMDY4OTY1NDYiLCJjbGllbnRfaWQiOiJvbmVzYWl0cGxhdGZvcm0ifQ.Nz5cDvMjh361z4r6MMD2jUOpYSmUKVLkMThHDK0sg6o", 'TMP_FOLDER': '/tmp/', 'NAME': "SentimentAnalysis" } sentiment_analysis_model_service = SentimentAnalysisModelService(config=PARAMETERS)

 

Los parámetros que se le pasan al objeto son los siguientes:

  • PLATFORM_HOST: Host del despliegue de OSP en el que se va a trabajar. En este caso, lab.onesaitplatform.com .

  • PLATFORM_PORT: Puerto en el que está servida la OSP.

  • PLATFORM_DIGITAL_CLIENT: Nombre del Digital Client creado en OSP para dar acceso a las ontologías.

  • PLATFORM_DIGITAL_CLIENT_TOKEN: Token de autenticación correspondiente al Digital Client.

  • PLATFORM_DIGITAL_CLIENT_PROTOCOL: Protocolo bajo el cual se establecerán la comunicaciones con OSP.

  • PLATFORM_DIGITAL_CLIENT_AVOID_SSL_CERTIFICATE: True si se pretende establecer las conexiones sin certificado.

  • PLATFORM_ONTOLOGY_MODELS: Nombre de la ontología en la que se registrarán las distintas versiones del modelo creadas.

  • PLATFORM_USER_TOKEN: Token de autenticación de un usuario de OSP.

  • TMP_FOLDER: Directorio local que se utilizará como dirección local a la cual se descargarán temporalmente los elementos del File Repository de OSL; y en el cual se guardarán temporalmente los modelos antes de ser subidos al File Repository.

  • NAME: Nombre del model service.

Una vez se ha creado el objeto sentiment_analysis_model_service, este está dispuesto para entrenar versiones del modelo de sentiment analysis tal como se definió en la clase SentimentAnalysisModelService. Además, en el momento de crearse el objeto, si la ontología de OSP referenciada en PLATFORM_ONTOLOGY_MODELS contiene ya algún modelo en estado activo (active True), este se cargará en memoria y estará dispuesto para su uso mediante el método predict.

Para entrenar una versión del modelo, se puede ejecutar uno de estos dos métodos:

  • train_from_file_system: lanza el entrenamiento de un modelo a partir de un dataset previamente guardado en el File Repository de OSP.

  • train_from_ontology: lanza el entrenamiento de un modelo a partir de un dataset guardado en una ontología de OSP.

En el siguiente código se lanza el entrenamiento de un modelo a partir de un dataset previamente subido al File Repository de OSP:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MODEL_NAME = 'sentiment_analysis' MODEL_VERSION = '0' MODEL_DESCRIPTION = 'First version of the model for sentiment analysis' DATASET_FILE_ID = '605360b7cfb6d70134a3b1a0' HYPERPARAMETERS = { 'NUM_WORDS': 10000, 'BATCH_SIZE': 16, 'EPOCHS': 10, 'DROPOUT': 0.2, 'LEARNING_RATE': 0.001, } sentiment_analysis_model_service.train_from_file_system( name=MODEL_NAME, version=MODEL_VERSION, description=MODEL_DESCRIPTION, dataset_file_id=DATASET_FILE_ID, hyperparameters=HYPERPARAMETERS )

 

Nótese que el valor de DATASET_FILE_ID es el identificador del fichero que contiene el dataset en el File Repository de OSP.

Alternativamente, se puede entrenar el modelo a partir de un dataset previamente guardado en una ontología. En el siguiente código, la ontología es SentimentAnalysisDataset:

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MODEL_NAME = 'sentiment_analysis' MODEL_VERSION = '0' MODEL_DESCRIPTION = 'First version of the model for sentiment analysis' ONTOLOGY_DATASET = 'SentimentAnalysisDataset' HYPERPARAMETERS = { 'NUM_WORDS': 10000, 'BATCH_SIZE': 16, 'EPOCHS': 10, 'DROPOUT': 0.2, 'LEARNING_RATE': 0.001, } sentiment_analysis_model_service.train_from_ontology( name=MODEL_NAME, version=MODEL_VERSION, description=MODEL_DESCRIPTION, ontology_dataset=ONTOLOGY_DATASET, hyperparameters=HYPERPARAMETERS )

 

Una vez se ha entrenado con éxito una versión del modelo, esta será guardada en el File Repository de OSP, y será registrada en la ontología SentimentAnalysisModels. Téngase en cuenta que las versiones de los modelos se guardan como active False. Habrá que activar una de las versiones para que esta se disponibilice al crear un nuevo objeto SentimentAnalysisModelService.

Una vez existe una versión activa del modelo en SentimentAnalysisModels, al crearse una nueva instancia de SentimentAnalysisModelService, esta tendrá dicho modelo cargado y disponible para usarse en inferencia mediante el método process:

 

1 2 sequences = ['Esta es una opinión muy buena', 'Esta es una opinión muy mala'] results = sentiment_analysis_model_service.predict(inputs=sequences)