Alfonso Zamora
Cloud Engineer
Introducción
El objetivo principal de este artículo es presentar una solución para el análisis y la ingeniería de datos desde el punto de vista del personal de negocio, sin requerir unos conocimientos técnicos especializados.
Las compañías disponen de una gran cantidad de procesos de ingeniería del dato para sacarle el mayor valor a su negocio, y en ocasiones, soluciones muy complejas para el caso de uso requerido. Desde aquí, proponemos simplificar la operativa para que un usuario de negocio, que anteriormente no podía llevar a cabo el desarrollo y la implementación de la parte técnica, ahora será autosuficiente, y podrá implementar sus propias soluciones técnicas con lenguaje natural.
Para poder cumplir nuestro objetivo, vamos a hacer uso de distintos servicios de la plataforma Google Cloud para crear tanto la infraestructura necesaria como los distintos componentes tecnológicos para poder sacar todo el valor a la información empresarial.
Antes de comenzar
Antes de comenzar con el desarrollo del artículo, vamos a explicar algunos conceptos básicos sobre los servicios y sobre distintos frameworks de trabajo que vamos a utilizar para la implementación:
- Cloud Storage[1]: Es un servicio de almacenamiento en la nube proporcionado por Google Cloud Platform (GCP) que permite a los usuarios almacenar y recuperar datos de manera segura y escalable.
- BigQuery[2]: Es un servicio de análisis de datos totalmente administrado que permite realizar consultas SQL en conjuntos de datos masivos en GCP. Es especialmente eficaz para el análisis de datos a gran escala.
- Terraform[3]: Es una herramienta de infraestructura como código (IaC) desarrollada por HashiCorp. Permite a los usuarios describir y gestionar la infraestructura utilizando archivos de configuración en el lenguaje HashiCorp Configuration Language (HCL). Con Terraform, puedes definir recursos y proveedores de manera declarativa, facilitando la creación y gestión de infraestructuras en plataformas como AWS, Azure y Google Cloud.
- PySpark[4]: Es una interfaz de Python para Apache Spark, un marco de procesamiento distribuido de código abierto. PySpark facilita el desarrollo de aplicaciones de análisis de datos paralelas y distribuidas utilizando la potencia de Spark.
- Dataproc[5]: Es un servicio de gestión de clústeres para Apache Spark y Hadoop en GCP que permite ejecutar eficientemente tareas de análisis y procesamiento de datos a gran escala. Dataproc admite la ejecución de código PySpark, facilitando la realización de operaciones distribuidas en grandes conjuntos de datos en la infraestructura de Google Cloud.
Qué es un LLM
Un LLM (Large Language Model) es un tipo de algoritmo de inteligencia artificial (IA) que utiliza técnicas de deep learning y enormes conjuntos de datos para comprender, resumir, generar y predecir nuevos contenidos. Un ejemplo de LLM podría ser ChatGPT que hace uso del modelo GPT desarrollado por OpenAI.
En nuestro caso, vamos a hacer uso del modelo Codey[6] (code-bison) que es un modelo implementado por Google que está optimizado para generar código ya que ha sido entrenado para esta especialización que se encuentra dentro del stack de VertexAI[7]
Y no solo es importante el modelo que vamos a utilizar, sino también el cómo lo vamos a utilizar. Con esto, me refiero a que es necesario comprender los parámetros de entrada que afectan directamente a las respuestas que nos dará nuestro modelo, en los que podemos destacar los siguientes:
- Temperatura (temperature): Este parámetro controla la aleatoriedad en las predicciones del modelo. Una temperatura baja, como 0.1, genera resultados más deterministas y enfocados, mientras que una temperatura alta, como 0.8, introduce más variabilidad y creatividad en las respuestas del modelo.
- Prefix (Prompt): El prompt es el texto de entrada que se proporciona al modelo para iniciar la generación de texto. La elección del prompt es crucial, ya que guía al modelo sobre la tarea específica que se espera realizar. La formulación del prompt puede influir en la calidad y relevancia de las respuestas del modelo, aunque hay que tener en cuenta la longitud para que cumpla con el número máximo de tokens de entrada que es 6144.
- Tokens de salida (max_output_tokens): Este parámetro limita el número máximo de tokens que se generarán en la salida. Controlar este valor es útil para evitar respuestas excesivamente largas o para ajustar la longitud de la salida según los requisitos específicos de la aplicación.
- Recuento de candidatos (candidate_count): Este parámetro controla el número de respuestas candidatas que el modelo genera antes de seleccionar la mejor opción. Un valor más alto puede ser útil para explorar diversas respuestas potenciales, pero también aumentará el costo computacional.
Desarrollo del prompt
Una vez que hemos definido los parámetros y sabemos bien para qué sirve cada uno de ellos y comprendemos lo que es un prompt, vamos a enfocarnos en cómo utilizarlo e implementar uno que se pueda adaptar a nuestras necesidades.
Como se ha comentado anteriormente, el objetivo es generar tanto código Pyspark como terraform para poder realizar las tareas de creación de infraestructura y tratamiento del dato. Como son tareas totalmente distintas, como primera decisión importante para nuestro prompt se ha optado por dividirlo en dos partes específicas para que cada prompt esté entrenado con ejemplos para generar un lenguaje u otro.
Para cada uno de los prompt, se hace una introducción para especificar cuál va a ser el objetivo y qué peticiones se van a realizar, y después una serie de ejemplos en los que se le da un input en lenguaje natural simulando una petición, y posteriormente también se le da el output deseado para asignar el texto al código específico. El objetivo es generar un prompt estructurado que pueda ser procesado de manera eficiente por el modelo para que en los siguientes casos, pueda asociar los ejemplos que tiene disponibles con respuestas adecuadas.
Vamos a poner en práctica estos pequeños tips para ver una de las entradas al prompt de terraform:
input: I need to read a file named `myfile.csv` inside the folder `data/input` in a bucket called `application-data-input`, and I need to create a new dataset and table in BigQuery called `tst_tf_ai`. The process has to read the file, remove the first three characters of the name, and write the result in BigQuery. The schema of the table will be ‘name’ of String type and ‘surname’ of String type. The infrastructure has to be deployed in the project `project-cloud-223` in GCP, and region `europe-southwest1`.
output: «`hcl
terraform {
required_version = «>= 0.15»
required_providers {
google = «~> 5.0»
}
}
resource «google_storage_bucket» «temporary_files» {
project = «project-cloud-223»
name = «temporary-files-dataproc»
location = «europe-southwest1»
force_destroy = true
}
resource «google_storage_bucket» «bucket1» {
project = «project-cloud-223»
name = «application-data-input»
location = «europe-southwest1»
force_destroy = true
}
resource «google_storage_bucket_object» «folder1» {
name = «data/input/»
bucket = google_storage_bucket.bucket1.name
content = » «
}
resource «google_bigquery_dataset» «dataset» {
project = «project-cloud-223»
dataset_id = «tst_tf_ai»
friendly_name = «tst_tf_ai»
description = «Description»
location = «europe-southwest1»
}
resource «google_bigquery_table» «table» {
project = «project-cloud-223»
dataset_id = google_bigquery_dataset.dataset.dataset_id
deletion_protection = false
table_id = «tst_tf_ai»
schema = <<EOF
[
{
«name»: «name»,
«type»: «STRING»,
«mode»: «NULLABLE»,
«description»: «The name»
},
{
«name»: «surname»,
«type»: «STRING»,
«mode»: «NULLABLE»,
«description»: «The surname»
}
]
EOF
}
«`
Es importante implementar ejemplos lo más parecido posible a tu caso de uso para que las respuestas sean más precisas, y también que tenga bastantes ejemplos con variedad de peticiones para que sea más inteligente a la hora de devolver las respuestas. Una de las prácticas para que sea más interactiva la implementación del prompt, puede ser ir probando con distintas peticiones, y si no es capaz de hacer lo que se le ha pedido, se debería modificar las instrucciones.
Como hemos podido observar, el desarrollo del prompt sí necesitamos conocimientos técnicos para poder traducir las peticiones a código, por lo que esta tarea sí se debería de abordar por una persona técnica para posteriormente evadir a la persona de negocio. En otras palabras, necesitamos que una persona técnica genere la primera base de conocimiento para que luego las personas de negocio puedan hacer uso de este tipo de herramientas.
También se ha podido ver, que la generación de código en terraform es más compleja que la generación en Pyspark, por lo que se han requerido de más ejemplos de entrada en la realización del prompt de terraform para que se ajuste a nuestro caso de uso. Por ejemplo, hemos aplicado en los ejemplos que en terraform siempre cree un bucket temporal (temporary-files-dataproc) para que pueda ser utilizado por Dataproc.
Casos prácticos
Se han realizado tres ejemplos con peticiones distintas, requiriendo más o menos infraestructura y transformaciones para ver si nuestro prompt es lo suficientemente robusto.
En el archivo ai_gen.py vemos el código necesario para hacer las peticiones y los tres ejemplos, en el que cabe destacar la configuración escogida para los parámetros del modelo:
- Se ha decidido darle valor 1 a candidate_count para que no tenga más que una respuesta final válida para devolver. Además que como se ha comentado, aumentar este número también lleva aumento de costes.
- El max_output_tokens se ha decidido 2048 que es el mayor número de tokens para este modelo, ya que si se necesita generar una respuesta con diversas transformaciones, no falle por esta limitación.
- La temperatura se ha variado entre el código terraform y Pyspark, para terraform se ha optado por 0 para que siempre dé la respuesta que se considera más cercana a nuestro prompt para que no genere más de lo estrictamente necesario para nuestro objetivo. En cambio para Pyspark se ha optado por 0.2 que es una temperatura baja para que no sea muy creativo, pero para que también pueda darnos diversas respuestas con cada llamada para también poder hacer pruebas de rendimiento entre ellas.
Vamos a realizar un ejemplo de petición que está disponible en el siguiente repositorio github, en el que está detallado en el README paso por paso para poder ejecutarlo tú mismo. La petición es la siguiente:
In the realm of ‘customer_table,’ my objective is the seamless integration of pivotal fields such as ‘customer_id’, ‘name’, and ‘email’. These components promise to furnish crucial insights into the essence of our valued customer base.
Conversely, when delving into the nuances of ‘sales_table,’ the envisioned tapestry includes essential elements like ‘order_id’ ‘product’ ‘price’, ‘amount’ and ‘customer_id’. Theseattributes, meticulously curated, will play a pivotal role in the nuanced exploration and analysis of sales-related data.
The ‘bigtable_info’ table will have all the fields resulting from the union of the two tables, ‘customer_table’ and ‘sales_table.’ Here, the outcome of joining the two tables by the ‘customer_id’ numeric field will be stored.
Furthermore, as part of our meticulous data collection strategy, I plan to inaugurate a dedicated Google Cloud Storage bucket christened ‘sales-etl-bucket.’ This repository is strategically designed to serve as a robust container for collating data, particularly hailing from CSV files. This endeavor is set to enhance the efficiency and organizational prowess of our data management processes.
To enhance the structure, two distinct subfolders, ‘input/sales’ and ‘input/customers’ have been ingeniously incorporated within the ‘sales-etl-bucket,’ ensuring a refined organization of the incoming data streams.
You will need to read the CSV files within the ‘input/sales’ folder to write the data into the ‘sales_table’. Additionally, you should perform the same operation with the files from the ‘input/customers’ folder to write the data into the ‘customer_table’. Finally, you are required to perform a join between the sales and customers information based on the ‘customer_id’ number field and write the merged information into the ‘bigtable_info’.
Ejecutamos y vamos a analizar las salidas generadas en las carpetas generated/customers/python/etl.py y generated/terraform/main.tf.:
terraform { required_version = ">= 0.15" required_providers { google = "~> 5.0" } } resource "google_storage_bucket" "temporary_files" { project = "my-cloud-project" name = "temporary-files-dataproc" location = "europe-southwest1" force_destroy = true } resource "google_storage_bucket" "sales_etl_bucket" { project = "my-cloud-project" name = "sales-etl-bucket" location = "europe-southwest1" force_destroy = true } resource "google_storage_bucket_object" "sales_folder" { name = "input/sales/" bucket = google_storage_bucket.sales_etl_bucket.name content = " " } resource "google_storage_bucket_object" "customers_folder" { name = "input/customers/" bucket = google_storage_bucket.sales_etl_bucket.name content = " " } resource "google_bigquery_dataset" "raw_sales_data" { project = "my-cloud-project" dataset_id = "raw_sales_data" friendly_name = "Raw Sales Data" description = "This dataset contains the raw sales data." location = "europe-southwest1" } resource "google_bigquery_table" "customer_table" { project = "my-cloud-project" dataset_id = google_bigquery_dataset.raw_sales_data.dataset_id deletion_protection = false table_id = "customer_table" schema = <<EOF [ { "name": "customer_id", "type": "INT64", "mode": "REQUIRED", "description": "The customer ID." }, { "name": "name", "type": "STRING", "mode": "NULLABLE", "description": "The customer's name." }, { "name": "email", "type": "STRING", "mode": "NULLABLE", "description": "The customer's email address." } ] EOF } resource "google_bigquery_table" "sales_table" { project = "my-cloud-project" dataset_id = google_bigquery_dataset.raw_sales_data.dataset_id deletion_protection = false table_id = "sales_table" schema = <<EOF [ { "name": "order_id", "type": "INT64", "mode": "REQUIRED", "description": "The order ID." }, { "name": "product", "type": "STRING", "mode": "NULLABLE", "description": "The product name." }, { "name": "price", "type": "FLOAT64", "mode": "NULLABLE", "description": "The product price." }, { "name": "amount", "type": "INT64", "mode": "NULLABLE", "description": "The product amount." }, { "name": "customer_id", "type": "INT64", "mode": "REQUIRED", "description": "The customer ID." } ] EOF } resource "google_bigquery_dataset" "master_sales_data" { project = "my-cloud-project" dataset_id = "master_sales_data" friendly_name = "Master Sales Data" description = "This dataset contains the master sales data." location = "europe-southwest1" } resource "google_bigquery_table" "bigtable_info" { project = "my-cloud-project" dataset_id = google_bigquery_dataset.master_sales_data.dataset_id deletion_protection = false table_id = "bigtable_info" schema = <<EOF [ { "name": "customer_id", "type": "INT64", "mode": "REQUIRED", "description": "The customer ID." }, { "name": "name", "type": "STRING", "mode": "NULLABLE", "description": "The customer's name." }, { "name": "email", "type": "STRING", "mode": "NULLABLE", "description": "The customer's email address." }, { "name": "order_id", "type": "INT64", "mode": "REQUIRED", "description": "The order ID." }, { "name": "product", "type": "STRING", "mode": "NULLABLE", "description": "The product name." }, { "name": "price", "type": "FLOAT64", "mode": "NULLABLE", "description": "The product price." }, { "name": "amount", "type": "INT64", "mode": "NULLABLE", "description": "The product amount." } ] EOF }
Vemos que va a añadir un bucket con dos carpetas para poder subir los datos a la carpeta correspondiente dependiendo si es sales o customer. También ha creado dos datasets, uno para la capa de raw para tener los datos según nos han venido de origen con las tablas de customers y sales, y el segundo dataset para la tabla final que almacenará el resultado de juntar la información de ambas tablas.
Vamos a revisar el código pyspark:
from pyspark.sql import SparkSession from pyspark.sql import functions as F # Configure Spark session spark = SparkSession.builder.appName("SalesETL").getOrCreate() # Define file paths sales_path = "gs://sales-etl-bucket/input/sales/*.csv" customers_path = "gs://sales-etl-bucket/input/customers/*.csv" # Read CSV files into DataFrames sales_df = spark.read.csv(sales_path, header=True, inferSchema=True) customers_df = spark.read.csv(customers_path, header=True, inferSchema=True) # Write tables to BigQuery sales_df.write.format("bigquery") \ .option("temporaryGcsBucket", "temporary-files-dataproc") \ .option("table", "raw_sales_data.sales_table") \ .mode("overwrite") \ .save() customers_df.write.format("bigquery") \ .option("temporaryGcsBucket", "temporary-files-dataproc") \ .option("table", "raw_sales_data.customer_table") \ .mode("overwrite") \ .save() # Join sales and customers tables bigtable_info_df = sales_df.join(customers_df, on="customer_id", how="inner") # Write joined table to BigQuery bigtable_info_df.write.format("bigquery") \ .option("temporaryGcsBucket", "temporary-files-dataproc") \ .option("table", "master_sales_data.bigtable_info") \ .mode("overwrite") \ .save() # Stop the Spark session spark.stop()
Se puede observar que el código generado realiza la lectura por cada una de las carpetas e inserta cada dato en su tabla correspondiente.
Para poder asegurarnos de que el ejemplo está bien realizado, podemos seguir los pasos del README en el repositorio GitHub[8] para aplicar los cambios en el código terraform, subir los ficheros de ejemplo que tenemos en la carpeta example_data y a ejecutar un Batch en Dataproc.
Finalmente, vemos si la información que se ha almacenado en BigQuery es correcta:
- Tabla customer:
- Tabla sales:
- Tabla final:
De esta forma, hemos conseguido de a través de lenguaje natural, tener un proceso funcional totalmente operativo. Hay otro ejemplo que se puede ejecutar, aunque también animo a hacer más ejemplos, o incluso mejorar el prompt, para poder meterle ejemplos más complejos, y también adaptarlo a tu caso de uso.
Conclusiones y Recomendaciones
Al ser ejemplos muy concretos sobre unas tecnologías tan concretas, cuando se hace un cambio en el prompt en cualquier ejemplo puede afectar a los resultados, o también, modificar alguna palabra de la petición de entrada. Esto se traduce en que el prompt no es lo suficientemente robusto como para poder asimilar distintas expresiones sin afectar al código generado. Para poder tener un prompt y un sistema productivo, se necesita más entrenamiento y distinta variedad tanto de soluciones, peticiones, expresiones,. … Con todo ello, finalmente podremos tener una primera versión que poder presentar a nuestro usuario de negocio para que sea autónomo.
Especificar el máximo detalle posible a un LLM es crucial para obtener resultados precisos y contextuales. Aquí hay varios consejos que debemos tener en cuenta para poder tener un resultado adecuado:
- Claridad y Concisión:
- Sé claro y conciso en tu prompt, evitando oraciones largas y complicadas.
- Define claramente el problema o la tarea que deseas que el modelo aborde.
- Especificidad:
- Proporciona detalles específicos sobre lo que estás buscando. Cuanto más preciso seas, mejores resultados obtendrás.
- Variabilidad y Diversidad:
- Considera incluir diferentes tipos de ejemplos o casos para evaluar la capacidad del modelo para manejar la variabilidad.
- Feedback Iterativo:
- Si es posible, realiza iteraciones en tu prompt basándote en los resultados obtenidos y el feedback del modelo.
- Prueba y Ajuste:
- Antes de usar el prompt de manera extensa, realiza pruebas con ejemplos y ajusta según sea necesario para obtener resultados deseados.
Perspectivas Futuras
En el ámbito de LLM, las líneas futuras de desarrollo se centran en mejorar la eficiencia y la accesibilidad de la implementación de modelos de lenguaje. Aquí se detallan algunas mejoras clave que podrían potenciar significativamente la experiencia del usuario y la eficacia del sistema:
1. Uso de distintos modelos de LLM:
La inclusión de una función que permita a los usuarios comparar los resultados generados por diferentes modelos sería esencial. Esta característica proporcionaría a los usuarios información valiosa sobre el rendimiento relativo de los modelos disponibles, ayudándoles a seleccionar el modelo más adecuado para sus necesidades específicas en términos de precisión, velocidad y recursos requeridos.
2. Capacidad de retroalimentación del usuario:
Implementar un sistema de retroalimentación que permita a los usuarios calificar y proporcionar comentarios sobre las respuestas generadas podría ser útil para mejorar continuamente la calidad del modelo. Esta información podría utilizarse para ajustar y refinar el modelo a lo largo del tiempo, adaptándose a las preferencias y necesidades cambiantes de los usuarios.
3. RAG (Retrieval-augmented generation)
RAG (Retrieval-augmented generation) es un enfoque que combina la generación de texto y la recuperación de información para mejorar las respuestas de los modelos de lenguaje. Implica el uso de mecanismos de recuperación para obtener información relevante de una base de datos o corpus textual, que luego se integra en el proceso de generación de texto para mejorar la calidad y la coherencia de las respuestas generadas.
Enlaces de interés
Cloud Storage[1]: https://cloud.google.com/storage/docs
BigQuery[2]: https://cloud.google.com/bigquery/docs
Terraform[3]: https://developer.hashicorp.com/terraform/docs
PySpark[4]: https://spark.apache.org/docs/latest/api/python/index.html
Dataproc[5]: https://cloud.google.com/dataproc/docs
Codey[6]: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-generation
VertexAI[7]: https://cloud.google.com/vertex-ai/docs
GitHub[8]: https://github.com/alfonsozamorac/etl-genai