El resumen de código es el proceso de generar una descripción en lenguaje natural de fragmentos de código. Las tareas comunes de resumen de código son explorar una nueva base de código, aprender un nuevo lenguaje de programación o generar comentarios de código y explicaciones de funciones. Generar un resumen de un fragmento de código es similar a generar un resumen de texto de un documento en lenguaje natural. Se diferencia porque el modelo de lenguaje grande (LLM) que genera el resumen necesita entender el lenguaje de programación que está leyendo mientras identifica la lógica subyacente de lo que intenta lograr.
Los resúmenes de código son una parte valiosa del desarrollo de software, ya que ayudan con el mantenimiento del software mediante la realización automática de resúmenes de código fuente, la creación de resúmenes en lenguaje natural para la documentación o el análisis de bases de código a gran escala. Los modelos de LLM más nuevos que utilizan un enfoque basado en transformadores pueden actuar como modelos de resumen de código o también ejecutar la generación de código. Estas funciones son posibles porque los modelos se han entrenado con grandes conjuntos de datos creados a partir de fuentes como los repositorios de Github que incluyen código y comentarios junto con documentación para ese código.
Antes de que se popularizaran los LLM, los enfoques para la generación de resúmenes de código requerían analizar la semántica del código y generar un árbol de sintaxis abstracta (AST) de cada identificador del código, que luego podía usarse para generar documentación.1,2 Con la llegada del aprendizaje profundo y las redes neuronales, se abandonaron los enfoques basados en la informática por enfoques que tomaban más prestado de la metodología de la traducción automática neuronal3,4.
Para los modelos de transformadores, las ventanas de contexto más grandes llevan a mejores resultados. Muchos de los modelos de Granite Code de última generación, como Granite-8B-Code-Instruct-128K, tienen una ventana de contexto de 128k. Una ventana de contexto más grande permite que el modelo contenga más texto en una memoria de trabajo. Esto ayuda a monitorear momentos y detalles clave en un chat prolongado, o en un documento o base de código extenso. Esta memoria de trabajo permite que un chatbot basado en LLM genere respuestas que tienen sentido en el momento inmediato y en un contexto más largo, ayudándoles a superar a los modelos con ventanas de contexto más pequeñas tanto por evaluación humana como por métricas de evaluación.5
Las ventanas de contexto más grandes permiten que los modelos guarden más texto en su memoria de trabajo, lo que ayuda a realizar un seguimiento de los momentos y detalles clave en chats prolongados o documentos o base de código extensos.
Cuando ChatGPT se introdujo por primera vez, su ventana de contexto era de 4000 tokens. Si una conversación superaba el límite de 3000 palabras de la interfaz de chat, era probable que el chatbot alucinara y se desviara del tema. Hoy en día, el estándar es de 32 000 tokens, y la industria cambia a 128 000 tokens. Eso es aproximadamente la longitud de un libro de 250 páginas. IBM ahora tiene dos modelos Granite con una ventana de 128 000 tokens y más están en camino.
En este paso, lo guiaremos a través de la creación de una cuenta de IBM para acceder a Jupyter Notebooks.
1. Inicie sesión en watsonx.ai usando su cuenta de IBM® Cloud.
2. Haga clic en + para crear un nuevo proyecto.
a. Seleccione Crear un proyecto vacío.
b. Ingrese un nombre de proyecto en el campo Nombre.
c. Cree un Cloud Object Storage para almacenar los activos de su proyecto si aún no se ha creado.
d. Seleccione Crear.
3. Cree un Jupyter Notebook.
a. Seleccione la pestaña Activos en su entorno de proyecto.
b. Haga clic en Nuevo activo.
c. En el panel izquierdo, seleccione la opción Trabajar con modelos.
d. Haga clic en Trabajar con datos y modelos usando notebooks de Python y R.
e. Ingrese un nombre para su notebook en el campo Nombre. Elija Runtime 23.1 en Python (4 vCPU 16 GB de RAM) para definir la configuración.
f. Seleccione Crear.
4. Configure una instancia de watsonx.ai Runtime y una clave de API
a. Cree una instancia de servicio watsonx.ai Runtime (seleccione la región adecuada y elija el plan Lite, que es una instancia gratuita).
b. Genere una clave de API.
c. Asocie la instancia del servicio watsonx.ai Runtime al proyecto que creó en watsonx.ai.
Primero, instalaremos la biblioteca Hugging Face Hub de código abierto para descargar modelos:
Ahora podemos descargar Granite-8B-Code-Instruct-128K:
Ahora podemos comenzar a usar Granite Code Instruct.
Vamos a darle a nuestro modelo una llamada de función bastante compleja de la biblioteca GluonTS, tomada de su repositorio GitHub. Este es un bloque largo de código para pegar en una instrucción, así que lo almacenaremos en una variable:
ll_func_2 = """ def call( self, data: torch.Tensor, weights: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: assert ( data.shape == weights.shape ), "data and observed_indicator must have same shape" with torch.no_grad(): observed_data = torch.where(weights == 1, data, torch.nan) med = torch.nanmedian(observed_data, dim=self.dim, keepdim=True).values q1 = torch.nanquantile(observed_data, 0.25, dim=self.dim, keepdim=True) q3 = torch.nanquantile(observed_data, 0.75, dim=self.dim, keepdim=True) iqr = q3 - q1 # if observed data is all zeros, nanmedian returns nan loc = torch.where(torch.isnan(med), torch.zeros_like(med), med) scale = torch.where(torch.isnan(iqr), torch.ones_like(iqr), iqr) scale = torch.maximum(scale, torch.full_like(iqr, self.minimum_scale)) scaled_data = (data - loc) / scale if not self.keepdim: loc = torch.squeeze(loc, dim=self.dim) scale = torch.squeeze(scale, dim=self.dim) # assert no nans in scaled data, loc or scale assert not torch.any(torch.isnan(scaled_data)) assert not torch.any(torch.isnan(loc)) assert not torch.any(torch.isnan(scale)) assert not torch.any(scale == 0) return scaled_data, loc, scaledef call( self, data: torch.Tensor, weights: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: assert ( data.shape == weights.shape ), "data and observed_indicator must have same shape"
Ahora podemos pedirle a Granite Code un resumen tokenizando nuestro chat y luego callingmodel.generate()
chat = [
{ "role": "user", "content": "Explain what this function does" + ll_func_2 },
]
chat = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
# have the encoder turn text into output tokens
inputs = tokenizer(chat, return_tensors="pt").input_ids
start_index = inputs.shape[-1]
# generate output tokens
output = model.generate(inputs, max_new_tokens=1500)
# have the decoder turn output tokens into text
output = tokenizer.batch_decode(output[start_index:])
# loop over the batch to print, in this example the batch size is 1
print(output[0])
Vamos a revisar el resumen generado:
Respuesta: Esta función realiza la normalización de datos mediante el método de rango intercuartílico (IQR). Se necesitan dos argumentos: datos y pesos. El argumento de datos es un tensor de forma (batch_size, sequence_length, hidden_size), donde batch_size es el número de muestras en un lote, sequence_length es la longitud de la secuencia de entrada y hidden_size es la dimensionalidad del estado oculto. El argumento de pesos es un tensor de forma (batch_size, sequence_length), donde cada elemento es 0 o 1, lo que indica si el elemento correspondiente en el tensor de datos se observa o falta. La función primero comprueba que los tensores de datos y pesos tengan la misma forma. Luego, calcula la mediana y el IQR de los puntos de datos observados (aquellos con un peso de 1) utilizando las funciones torch.nanmedian y torch.nanquantile. La función torch.nanmedian calcula la mediana a lo largo de la dimensión especificada (`self.dim`), mientras que la función torch.nanquantile calcula los percentiles 25 y 75. A continuación, la función calcula los parámetros de ubicación (`loc`) y escala (`scale`) de la distribución normalizada. La ubicación se establece en la mediana de los puntos de datos observados y la escala se establece en el IQR de los puntos de datos observados. Si los puntos de datos observados son todos ceros, la función establece la ubicación en cero. La función también garantiza que la escala sea al menos un valor mínimo (`self.minimum_scale`) para evitar la división por cero. Finalmente, la función calcula los datos normalizados (`scaled_data`) restando la ubicación y dividiendo por la escala. Si el argumento keepdim se establece en False, la función elimina la dimensión especificada de los tensores de ubicación y escala. La función también realiza aserciones para garantizar que los tensores de datos, ubicación y escala normalizados no contengan ningún valor NaN o ceros en la escala. La función devuelve una tupla que contiene los tensores de datos, ubicación y escala normalizados.
La descripción es altamente detallada y describe con precisión la función que pasamos y el algoritmo subyacente.
La generación de documentación es una poderosa aplicación de resumen de código, ya que no solo explica el código a cualquier futuro usuario que lea el código, sino que también puede ayudar a generar documentación utilizando una herramienta como pydoc.
Ahora podemos pedir a Granite Code que genere docstrings para una función. Debido a la ventana de contexto más grande podemos pasar una función completa junto con una solicitud para generar un docstring.
Escriba una cadena de documentación de Python con el formato correcto para la siguiente función:
Granite Code devuelve una excelente documentación en formato Python:
Extrae un objeto JSON de la cadena HTML dada utilizando la clave especificada y el carácter de parada. Args: html (str): la cadena HTML de entrada para extraer el objeto JSON. key (str): la clave utilizada para localizar el inicio del objeto JSON en la cadena HTML. num_chars (int, opcional): el número de caracteres después de la clave para incluir en el objeto JSON extraído. El valor predeterminado es 2. stop (str, opcional): el carácter que marca el final del objeto JSON en la cadena HTML. El valor predeterminado es '"'. Returns: str: el objeto JSON extraído como una cadena.
Ahora, si queremos, podemos reutilizar la instrucción anterior que todavía está presente en la ventana de contexto grande:
chat = "Rewrite the previous function to be a class method and include the docstrings." # have the encoder turn text into output tokens inputs = tokenizer(chat, return_tensors="pt").input_ids start_index = inputs.shape[-1] # generate output tokens output = model.generate(inputs, max_new_tokens=1500) # have the decoder turn output tokens into text output = tokenizer.batch_decode(output[start_index:]) # loop over the batch to print, in this example the batch size is 1 print(output[0])
Esto devuelve
class HtmlJsonExtractor: def init(self, html): """ Initializes the HtmlJsonExtractor object with the given HTML string. Args: html (str): The input HTML string to extract JSON objects from. """ self.html = html @classmethod def extract(cls, html, key, num_chars=2, stop='"'): """ Extracts a JSON object from the given HTML string using the specified key and stop character. Args: html (str): The input HTML string to extract the JSON object from. key (str): The key used to locate the start of the JSON object in the HTML string. num_chars (int, optional): The number of characters after the key to include in the extracted JSON object. Defaults to 2. stop (str, optional): The character that marks the end of the JSON object in the HTML string. Defaults to '"'. Returns: str: The extracted JSON object as a string. """ pos_begin = html.find(key) + len(key) + num_chars pos_end = html.find(stop, pos_begin) return html[pos_begin:pos_end]
El modelo ahora ha creado una clase con un nombre apropiado, ha convertido el código anterior en un método de esa clase e insertado nuestros docstrings.
Ahora vamos a dar instrucciones al modelo para que nos ayude a optimizar un fragmento de código:
chat = """Please optimize the following code: def fib(n): if (n < 2): return 1 else: return fib(n-1) + fib(n-2)"""
El código de Granite devuelve:
La función de Python dada fib(n) calcula el enésimo número de Fibonacci mediante recursividad. Sin embargo, esta implementación tiene una complejidad temporal de O(2^n), que es ineficiente para grandes entradas. Podemos
optimizarla mediante programación dinámica o memorización para reducir la complejidad del tiempo a O(n). Aquí hay una versión optimizada de la función:
```python
def fib_optimized(n, memo={}):
if n < 2:
return 1
elif n not in memo:
memo[n] = fib_optimized(n-1, memo) + fib_optimized(n-2, memo)
return memo[n]
```
Esta versión optimizada utiliza una nota de diccionario para almacenar números de Fibonacci calculados previamente. Al calcular el enésimo número de Fibonacci, primero verifica si ya se calculó y almacenó en memo. Si es así, devuelve el valor almacenado en lugar de volver a calcularlo, lo que ahorra tiempo y mejora el rendimiento.
En este caso, el modelo no solo explicó qué falla en la función en términos de eficiencia y le dio a la implementación una complejidad Big-O, sino que también proporcionó una mejor implementación. Proporcionar una revisión y explicaciones es una forma de aprovechar la generación de resúmenes y, como parte de la generación de código más general, ayuda a los desarrolladores a mejorar.
En este tutorial, aprendió sobre el resumen de código y utilizó el modelo Granite Code con una ventana de contexto ampliada de 128 000 tokens para generar explicaciones del código Python. También utilizamos instrucciones para generar nueva documentación, agregar una clase alrededor de un fragmento de código y usar la ventana de contexto ampliada para agregar esa documentación a una nueva ventana de código. Finalmente, hicimos que Granite Code analizara y resumiera un fragmento de código y explicara cómo podría mejorarse.
Acelere la entrega de software con IBM® Bob , su socio de IA para un desarrollo seguro y orientado a la intención.
Desarrolle, despliegue y gestione aplicaciones de IA más rápido con herramientas preparadas para empresas.
Redefina los sistemas heredados con una modernización inteligente de la IA.
1 Sonia Haiduc, Jairo Aponte, Andrian Marcus, “Supporting program comprehension with source code summarization,” ICSE ‘10: Proceedings of the 32nd ACM/IEEE International Conference on Software Engineering https://doi.org/10.1145/3377811.3380383.
2 Paul W. McBurney, Collin McMillan, “Automatic Source Code Summarization of Context for Java Methods,” https://ieeexplore.ieee.org/document/7181703.
3 Chen Lin, Zhichao Ouyang, Junqing Zhuang, Jianqiang Chen, Hui Li, Rongxin Wu, “Improving Code Summarization with Block-wise Abstract Syntax Tree Splitting” IEEE/ACM, International Conference on Program Comprehension (ICPC 2021) https://arxiv.org/abs/2103.07845.
4 Jian Zhang, Xu Wang, Hongyu Zhang, Hailong Sun, Xudong Liu, “Retrieval-based neural source code summarization,” ICSE ‘10: Proceedings of the 32nd ACM/IEEE International Conference on Software Engineering, https://doi.org/10.1145/1810295.1810335.
5 Xinyi Hou, Yanjie Zhao, Yue Huang, Zhou Yang, Kailong Wang, Li Li, Xiapu Luo, David Jin, John Grundy, Haoyu Wang, “Large Language Models for Software Engineering: A Systematic Literature Review”, https://arxiv.org/abs/2308.10620.