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).

Top: Ejemplo de tarea de generación de código del benchmark CodeContests en el que la descripción del problema y los casos de prueba públicos se proporcionan como entradas al modelo. Abajo: Ilustramos un proceso típico de resolución de problemas en el que un desarrollador intenta resolver el problema de forma iterativa, revisando y reutilizando partes de sus códigos previamente desarrollados hasta quedar satisfecho.

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.

Una visión general de CodeChain: primero se instruye a un LLM preentrenado con una cadena de pensamiento para que genere un conjunto de soluciones modularizadas. A continuación, los submódulos generados se extraen de las soluciones potencialmente correctas y se agrupan en distintos clusters semánticos. Los centroides de los clusters se seleccionan como submódulos representativos para condicionar la siguiente ronda de autorrevisión. El modelo recibe instrucciones para reutilizar o adaptar estos módulos en sus soluciones revisadas.

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.

Un ejemplo de salida de generación de código modularizado: en primer lugar, el modelo debe esbozar los submódulos necesarios, cada uno de los cuales consta de una cabecera de función y un docstrip que describe el uso previsto. Posteriormente, el modelo implementa cada módulo completamente en código y los integra como partes de la solución final completa.

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.

Resultados de la validación APPS por pass@1 (%): probamos CodeChain+GPT3.5 para 5 rondas de auto-revisión y reportamos pass@1 en cada nivel de dificultad del problema. Utilizando GPT3.5 como modelo base, lo comparamos con enfoques relacionados, incluyendo Self-debug (con feedback de pruebas unitarias (UT) o explicaciones (explicaciones)) y Reflexion.

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}
}

Explore más

Entradas recomendadas