TL;DR: Con CodeChain, un gran modelo de lenguaje (LLM) preentrenado puede resolver problemas de codificación desafiantes integrando la modularidad en muestras de generación y auto-mejorarse empleando una cadena de auto-revisiones en submódulos representativos. CodeChain puede lograr resultados de vanguardia tanto con modelos GPT de OpenAI como con LLM de código abierto en pruebas de referencia de codificación exigentes como APPS y CodeContests.
Limitaciones de los enfoques de generación de código con LLM de código
Los modelos de lenguaje de gran tamaño (LLM) ya son bastante competentes a la hora de resolver tareas de programación más sencillas como las de las pruebas de referencia HumanEval o MBPP. Sin embargo, la resolución de tareas de programación más complejas y competitivas sigue siendo un reto para estos modelos, posiblemente debido a su tendencia a generar soluciones como bloques de código monolíticos. Otro límite de este enfoque de generación es que los modelos simplemente generarían un gran número (varios miles) de soluciones de forma independiente, con la esperanza de que una de las soluciones superara todos los casos de prueba privados.
Por otro lado, en el entorno de desarrollo ágil actual, los desarrolladores experimentados suelen adoptar el concepto de modularidad en la programación. Ante un problema, escribirían instintivamente soluciones muy modularizadas: cada programa se divide en subtareas y submódulos lógicos de alto nivel. Los desarrolladores seguirían probando y analizando sus implementaciones, modificando los componentes modulares de sus soluciones desarrolladas previamente para mejorar eficazmente sus soluciones finales (véase la figura siguiente).
CodeChain – a new framework for modular and self-improved code generation
Inspirados en el proceso de resolución de problemas anterior, proponemos CodeChain, un novedoso marco de inferencia para mejorar la generación de código en LLMs a través de una cadena de autorrevisiones con submódulos representativos. Véase la siguiente figura para ilustrar cómo funciona CodeChain.
CodeChain consta de los siguientes pasos:
- Para incorporar la modularidad en la generación de código, primero adaptamos la técnica de la cadena de pensamiento (CoT) a las tareas de generación de código (véase la figura siguiente). En este paso, pedimos a los LLM que descompongan sus soluciones en segmentos modulares. Cada segmento modular representa una función abstracta que se destina a una subtarea lógica de alto nivel.
- Las muestras modularizadas generadas por los LLMs se dividen en segmentos modulares
- Las muestras modularizadas generadas se mejoran iterativamente mediante una cadena de autorrevisiones. En cada autorrevisión, realizamos lo siguiente:
- En primer lugar extraemos los submódulos encontrados en todos los programas generados. Cada submódulo extraído contiene información de alto nivel (incluido el uso previsto y docstrings de entrada/salida) y la implementación de código correspondiente.
- A continuación, transformamos estos submódulos en submódulos
- A continuación, transformamos estos módulos en un espacio de incrustación y los agrupamos en clústeres semánticos con k-means clustering.
- Dentro de cada clúster, los módulos se agrupan en clústeres semánticos
- Dentro de cada clúster, tomamos muestras de los submódulos centroides -definidos como aquellos que están más cerca del verdadero punto centroide de un clúster.
- Tratamos estos submódulos centroides como partes de código representativas y reutilizables. Este proceso se inspira en la forma en que un desarrollador suele reutilizar las partes más modularizadas y generalizadas de su código.
- A continuación, aumentamos el número de submódulos del centroide para que sean más representativos y reutilizables
- A continuación, aumentamos el mensaje original de la cadena de pensamiento con estos submódulos seleccionados. Con este prompt aumentado, instruimos al LLM para que reutilice/adapte los submódulos seleccionados y regenere un nuevo conjunto de soluciones.
- Los submódulos seleccionados se reutilizan en la cadena de pensamiento
- Seguimos repitiendo este proceso de generación-revisión, creando una cadena de autorrevisiones, cada una de ellas condicionada por partes de código modular reutilizables.
Con CodeChain, los LLM pueden recibir los conocimientos colectivos de los componentes modulares de todas las muestras de generaciones anteriores para mejorar sus generaciones futuras, imitando el proceso de resolución de problemas de un desarrollador experimentado.
CodeChain aumenta el rendimiento de los LLM, logrando resultados SoTA en APPS y CodeContests
Encontramos que al animar de forma natural al LLM a reutilizar los submódulos previamente desarrollados y verificados, CodeChain puede impulsar significativamente tanto la modularidad como la corrección de las soluciones generadas, logrando mejoras relativas pass@1 del 35% en APPS y del 76% en CodeContests. Se ha demostrado su eficacia tanto en los LLM de OpenAI como en los de código abierto, como WizardCoder.
Modelo | Tamaño | Introducción | Entrevista | Concurso | Todos |
---|---|---|---|---|---|
Codex | 12B | 4.14 | 0,14 | 0,02 | 0,92 |
CódigoT5 | 770M | 6,60 | 1,03 | 0,30 | 2,00 |
CódigoRL+CódigoT5 | 770M | 7,08 | 1,86 | 0,75 | 2,69 |
texto-davinci-002 | – | – | – | – | 7,48 |
Autoedición+texto-davinci-002 | – | – | – | – | 7,94 |
código-davinci-002 | – | 29,30 | 6,40 | 2,50 | 10,20 |
MagoCodificador | 15B | 26.04 | 4,21 | 0,81 | 7,90 |
Cadena de códigos+Codificador asistente | 15B | 26,29 | 7,49 | 3,75 | 10,50 |
GPT3.5 | – | 48,00 | 19,42 | 5,42 | 22,33 |
CadenaCódigo+GPT3.5 | – | 54,50 | 28,11 | 12,38 | 30,24 |
Resultados APPS por pass@1 (%)
Modelo | Tamaño | Val pass@1 | Val pase@5 | Pasar prueba@1 | Prueba pasa@5 | código-davinci-002 | – | – | – | 1,00 | – | WizardCoder | 15B | 1.11 | 3.18 | 1,98 | 3,27 | + Cadena de código | 15B | 2,35 | 3,29 | 2,48 | 3,30 | GPT3.5 | – | 6,81 | 16,23 | 5,82 | 11,16 | + Cadena de código | – | 12,86 | 16,91 | 10,27 | 14,11 |
---|
Resultados de CodeContests por pass@1 (%)
Cuando se comparó con enfoques relacionados como Self-repair, observamos ganancias de rendimiento relativas significativas al utilizar CodeChain. En concreto, evaluamos nuestro método en un subconjunto de 20 muestras de APPS, utilizando GPT3.5 y GPT4 como modelos base. Observamos que CodeChain puede mejorar el rendimiento con ambos modelos, con ganancias más significativas utilizando GPT4. CodeChain+GPT4 puede lograr un resultado SoTA de 61,50% pass@1 de media, superando incluso a Self-repair+GPT4 con feedback humano.
Modelo | Fuente de retroalimentación | Introducción | Entrevista | Concurso | Todos |
---|---|---|---|---|---|
Autorreparación+GPT4 | GPT4 | 42,64 | 19,33 | 3,67 | 33,30 |
Autorreparación+GPT4 | Humano | 62,21 | 45,67 | 14,67 | 52,60 |
GPT3.5 | – | 30,00 | 18,33 | 0,00 | 23,75 |
Cadena de código+GPT3.5 | Submódulos | 31,67 | 27,86 | 0,00 | 26,35 |
GPT4 | – | 42,86 | 18,33 | 13,33 | 34,75 |
Cadena de código+GPT4 | Submódulos | 71,07 | 55,00 | 23,33 | 61,50 |
Comparación con Auto-reparación: informamos de los resultados en el mismo subconjunto de 20 muestras en APPS test split utilizando GPT3.5 y GPT4 como modelos base.
En la figura siguiente, encontramos mejoras significativas en todos los niveles de dificultad de los problemas en APPS, con una ganancia de rendimiento óptima obtenida en la ronda de revisión 4. En un análisis más detallado, observamos que en diferentes niveles de dificultad de los problemas, CodeChain tiene diferentes tasas de mejora del rendimiento: los problemas más desafiantes (es decir, nivel de competencia y entrevista) tienden a beneficiarse más de CodeChain que los problemas básicos (es decir, nivel introductorio). Por último, en comparación con enfoques relacionados (autorrevisión que utiliza la retroalimentación de los resultados de las pruebas con explicaciones en lenguaje natural como Self-debug o autorreflexión como Reflexion), CodeChain puede lograr un mejor rendimiento, utilizando partes de código modularizadas como forma de retroalimentación para las salidas de auto-mejora.
Desde la siguiente figura, se pueden observar observaciones similares en los modelos de código abierto WizardCoder, con tendencias de rendimiento más claras en modelos de mayor tamaño, incluyendo parámetros de 7B, 15B y 34B. Esto concuerda con hallazgos recientes sobre la ley de escalado de los LLM, según la cual algunas características, como el seguimiento de instrucciones, sólo aparecen cuando el tamaño del modelo es suficientemente grande.
Para comprender la modularidad y reutilización de la generación de CodeChain, realizamos experimentos para evaluar estas cualidades en programas generados aleatoriamente. Observamos que cuando se utiliza CodeChain, GPT3.5 es más probable que genere programas con altos niveles de modularidad y reutilización, con la mayoría de las salidas calificadas de 3 a 5 en la escala de Likert. Esto es significativamente más alto que el enfoque convencional de generación directa, con alrededor del 80% del tiempo generando códigos no modulares o no reutilizables (es decir, puntuación 0).
Salidas de la generación de ejemplos
Descripción del problema
A tu amigo Edvin le encantan los idiomas. Uno de sus favoritos es hablar el Lenguaje de los Ladrones. Para los que no lo conozcáis, se trata de una transformación lingüística en la que todas las consonantes se duplican, con una «o» en medio. El resto se mantiene igual. Por ejemplo, la palabra «coche» se transformaría en «cocaror».
Tu amigo Edvin te iba a decir la contraseña de su web favorita para que comprobaras lo impresionante que es. Edvin está seguro de que nadie más que él y tú habláis la Lengua de los Ladrones, así que la encriptó usando esta sencilla técnica, y la escribió en una nota. Tras probar la contraseña y darte cuenta de que no funciona, descubres que Edvin estaba borracho durante la transformación. Ahora quieres averiguar de cuántas formas podría haber sido la contraseña originalmente.
Te dan una contraseña encriptada que Edvin ha intentado traducir al Lenguaje de los Ladrones. Su tarea es determinar cuántas contraseñas podrían producir la contraseña cifrada. Puedes suponer que lo único que Edvin hizo mal, fue que al escanear la palabra con sus ojos, a veces se le pasó transformar algunas consonantes usando la regla anterior. Edvin no es determinista en sus fracasos, es decir, si él falló para transformar una consonante una vez, no es necesario que él lo fallaría si ocurriera otra vez más tarde en la cadena.
Se considera vocal a una de las letras «a», «e», «i», «o» y «u». Una consonante es cualquier otra letra.
—–Input—–
Input consiste en una contraseña encriptada, la palabra resultante de la traducción de Edvins. La palabra puede tener hasta 1000000$ caracteres. Consistirá únicamente en letras minúsculas a-z.
—–Salida—–
La salida debe consistir en un número, el número de formas en que la contraseña podría haberse visto antes de que Edvin la tradujera. Como este número puede ser enorme, imprime el resto modulo $1000009$.
—–Ejemplos—–
Muestra de entrada 1:
coche
Muestra de salida 1:
1
Muestra Entrada 2:
coche
Muestra Salida 2:
2
Ejemplo de salida de generación (generación directa)
s = input()
n = len(s)
vow = set(["a", "e", "i", "o", "u"])
dp = [0]*(n+1)
mod = 10**6+9
dp[0] = 1
dp[1] = 1
para i en rango(2, n + 1):
if s[i-1] in vocal or s[i-2] in vocal or i - 2 < 0 or s[i-1] != s[i-2] or s[i-1] == 'o':
dp[i] = dp[i-1]
si no
dp[i] = dp[i-1] + dp[i-2]
dp[i] %= mod
print(dp[n])
Ejemplo de salida de generación (CodeChain ronda 0)
PASO 1: GENERAR SUBMÓDULOS:
def is_vowel(ch):
"""
Descripción: Esta función devuelve True si el carácter especificado es una vocal, False en caso contrario.
Entrada:
ch (char): Un carácter.
Salida:
(bool): True si el carácter es una vocal, False en caso contrario.
"""
def contar_palabras_originales_posibles(palabra):
"""
Descripción: Esta función cuenta las posibles palabras originales que podrían haberse transformado en la palabra dada del Lenguaje de los Ladrones.
Entrada:
palabra (str): Una palabra en Lenguaje Robber.
Salida:
count (int): Un recuento de posibles palabras originales.
"""
PASO 2: GENERAR CÓDIGO PYTHON
MOD = 1000009
def es_vocal(ch):
vocales = ['a', 'e', 'i', 'o', 'u']
return ch en vocales
def contar_palabras_originales_posibles(palabra):
N = len(palabra)
dp = [0]*(N+1)
dp[0] = 1
para i en rango(1, N+1):
dp[i] = dp[i-1]
if not is_vowel(word[i-1]) and i >= 2 and word[i-1] == word[i-2] and word[i-3] != 'o
dp[i] += dp[i-2]
dp[i] %= MOD
return dp[N]
palabra = input().strip()
print(cuenta_palabras_originales_posibles(palabra))
Ejemplo de salida de generación (CodeChain ronda 3)
PASO 1: GENERAR SUBMÓDULOS:
Dado que nos dan dos funciones muy relevantes «split_word» y «count_possible_passwords», podemos mantener estas funciones en nuestra solución. La función «initialize_vowels_and_modulo» puede ser útil para inicializar variables globales.
def split_word(palabra):
"""
Descripción: Esta función divide la palabra en una lista de elementos que pueden ser traducidos de nuevo al lenguaje Robber.
Entrada:
palabra (str): La palabra que necesita ser dividida.
Salida:
lista_palabra_dividida (lista): La lista de cadenas, donde cada cadena representa una posible letra original
"""
def initialize_vowels_and_modulo():
"""
Descripción: Esta función inicializa las variables vocales y módulo.
Salida:
vocales (conjunto): Un conjunto de vocales
modulo (int): La constante modulo
"""
def contar_contraseñas_posibles(lista_palabras):
"""
Descripción: Esta función cuenta el número de posibles contraseñas que podrían haber producido la palabra original.
Entrada:
lista_palabras (lista): Una lista con elementos de la palabra
Salida:
(int): El número de contraseñas posibles módulo 1000009
"""
PASO 2: GENERAR CÓDIGO PYTHON
def split_word(word):
i = 0
split_palabra_lista = []
while i < len(palabra):
if i + 2 < len(palabra) and palabra[i + 1] == 'o' and palabra[i + 2] != 'o':
split_word_list.append(palabra[i:i+3])
i += 3
si no:
split_word_list.append(palabra[i])
i += 1
return lista_palabra_dividida
def contar_contraseñas_posibles(lista_palabras, vocales, modulo):
dp = [0] * len(lista_palabras)
dp[0] = 1 if len(lista_palabras[0]) == 1 or lista_palabras[0][0] in vocales else 2
para i en rango(1, len(lista_palabras))
si len(lista_palabras[i]) == 1 o lista_palabras[i][0] en vocales:
dp[i] = dp[i - 1]
si no
dp[i] = (dp[i - 1] * 2) % modulo
devolver dp[-1]
def inicializar_vocales_y_módulo():
return set(['a', 'e', 'i', 'o', 'u']), 1000009
def resolver_tarea(palabra):
split_word_list = split_word(palabra)
vocales, modulo = inicializar_vocales_y_modulo()
return contar_contraseñas_posibles(lista_palabra_partida, vocales, modulo)
palabra = entrada()
print(resolver_tarea(palabra))
The Bottom Line
En CodeChain, introducimos la cadena de pensamiento para generar programas más modularizados, lo que crea límites naturales para que los modelos muestren partes de las soluciones para su reutilización y revisión. En cada paso de revisión, iteramos entre la selección de submódulos representativos y el aumento de la incitación a la cadena de pensamiento con estos submódulos seleccionados. Nuestros experimentos indican una mejora significativa del rendimiento de CodeChain cuando se utiliza OpenAI GPT o WizardCoder de código abierto como modelos base, logrando nuevos resultados SoTA en las pruebas APPS y CodeContests. Obsérvese que CodeChain puede complementarse con otros enfoques de autorrevisión como Self-debug y Reflexion combinando diferentes tipos de feedback (por ejemplo, explicación natural y reflexión) y seleccionando submódulos más diversos y representativos.
Citation
Para citar este trabajo, utilice el siguiente BibTeX:
@misc{le2023codechain,
title={CodeChain: Hacia la Generación de Código Modular a través de la Cadena de Autorrevisiones con Submódulos Representativos},
author={Hung Le y Hailin Chen y Amrita Saha y Akash Gokul y Doyen Sahoo y Shafiq Joty},
year={2023},
eprint={2310.08992},
archivePrefix={arXiv},
primaryClass={cs.AI}
}