2 puntos por GN⁺ 2024-07-02 | 1 comentarios | Compartir por WhatsApp
  • Los programas que extraen información ambigua suelen convertirse en código espagueti a medida que se acumulan reglas de excepción, y esta lógica basada en estados puede pensarse como una red neuronal recurrente (RNN)
  • El ejemplo de encontrar referencias a código C en mensajes de revisión de código se implementa como un clasificador escrito a mano que rastrea como estado patrones de tokens como identifieropen_parenclose_paren
  • Esta regla muestra 100% de precisión en el ejemplo, pero omite casos como if (err) goto cleanup;, por lo que el recall se queda en 50%; al agregar más reglas, State y las ramas se vuelven más complejos
  • La misma máquina de estados puede codificarse mediante el estado oculto y los cálculos por capas de una RNN; para hacerla entrenable, se necesitan ReLU, sigmoid y pesos y sesgos entrenables en lugar de funciones indicadoras binarias
  • Implementaciones como Elman RNN, GRU y LSTM en PyTorch, junto con el problema del desvanecimiento del gradiente en secuencias largas de tokens, se vuelven limitaciones prácticas del entrenamiento; el propio proceso de definir dataset, etiquetas y función de pérdida también ayuda a diseñar reglas manuales

Cómo la extracción de datos ambiguos se convierte en código espagueti

  • Los programas de investigación que extraen información de datos crudos se vuelven rápidamente complejos cuando los datos no siguen una especificación clara o tienen formas inusuales
  • Ejemplos de tareas incluyen identificar empresas y ejecutivos en artículos de noticias, etiquetar contratos de compras públicas por tipo de servicio y determinar si mensajes de ingenieros contienen código de programa
  • Si se quiere una salida perfecta, se puede revisar cuidadosamente cada observación y escribir pruebas unitarias para casos representativos
    • Tanto R como Python ofrecen bibliotecas de testing para esto
  • En situaciones que requieren reglas de juicio complejas, como apodos, sinónimos o la frontera entre inglés y código, las reglas manuales se vuelven frágiles con facilidad
  • Los algoritmos de entrenamiento de redes neuronales cambian el problema: en vez de que una persona siga ajustando esas combinaciones de reglas, las buscan en los datos

Encontrar referencias a código en mensajes de revisión de código

  • El objetivo es detectar si un mensaje enviado durante una revisión de código hace referencia explícita a código de programa
  • Se asume que la base de código observada está escrita en C
  • Los mensajes representativos incluyen referencias a código como las siguientes
    • render_ipa_alloc()
    • FTPSACK
    • debug_error()
    • NULL
    • IS_ERROR()
    • aarch64, amd64
    • if (err) goto cleanup;
  • Candidatos de reglas simples fallan de distintas maneras
    • La regla de considerar código a una palabra seguida de paréntesis captura casos como render_ipa_alloc(), pero omite if (err) goto cleanup;
    • La regla de considerar código a una palabra en mayúsculas captura FTPSACK e IS_ERROR(), pero produce falsos positivos con siglas como AFAICT
    • La regla de considerar código a una palabra que no es inglés puede marcar erróneamente como código términos de ingeniería o nombres de arquitecturas
  • Para mejorar las reglas 2 y 3 se necesita una lista de siglas y términos especializados como AFAICT, LGTM, USD, COVID, aarch64, amd64

Clasificador hecho con una máquina de estados escrita a mano

  • Un algoritmo simple decide en dos etapas si un mensaje contiene código
    • Preprocesamiento: convierte el mensaje en una secuencia de tokens que reflejan elementos sintácticos del código C
    • Inferencia: verifica si la secuencia de tokens satisface las reglas
  • La regla 1 considera el patrón underscore_identifieropen_parenclose_paren como una referencia a código
  • La implementación en Python guarda el estado de los tokens anteriores con una clase de datos State
    • previous_was_identifier
    • previous_was_open_paren
    • previous_previous_was_identifier
    • seen_code
  • contains_code recorre los tokens llamando a process y al final devuelve state.seen_code
  • process establece seen_code en True si el token actual es close_paren, el anterior fue open_paren y el anterior a ese fue un identificador
  • En el ejemplo, este clasificador muestra cero falsos positivos y 100% de precisión, pero omite muchos casos, por lo que el recall se queda en 50%
  • Al agregar la regla 2 aumentan los campos de State y las ramas if/elif/else; cuanto más se refinan las reglas, más difícil se vuelve el mantenimiento

Pasar una máquina de estados a una RNN

  • contains_code y process son una máquina de estados, y una máquina de estados puede codificarse como una red neuronal recurrente (RNN)
  • Una RNN procesa la secuencia de tokens uno por uno y aproxima la probabilidad condicional de que el mensaje contenga código
  • El valor correspondiente al State de Python se representa como el estado oculto de la RNN
    • State_0 es el estado inicial
    • Cada State_t se calcula pasando el token actual y el estado anterior a la función f
    • El estado final pasa por la capa de salida g y se convierte en el resultado de clasificación
  • La RNN de ejemplo usa tres capas ocultas
    • La primera capa guarda o copia el token actual y el estado anterior
    • La segunda capa verifica el patrón correspondiente a la regla 1
    • La tercera capa recuerda si ya vio un patrón de código
  • Los tokens se representan como vectores binarios en formato one-hot
  • Para imitar tal cual el algoritmo escrito a mano, se puede usar la función indicadora binaria 1{x > 0}
    • Permite mantener las capas ocultas como valores binarios
    • Pero no es adecuada para entrenamiento, porque su derivada es 0 casi en todas partes
  • También se podría verificar el patrón con el producto de identifier, open_paren y close_paren, pero en capas ocultas binarias la misma verificación puede expresarse con una suma
  • Giles et al. (1992) se vincula como un caso de uso de una RNN de segundo orden para descubrir máquinas de estados

Convertirla en una red entrenable

  • Para entrenar, se usa ReLU en lugar de una función indicadora binaria
  • Las constantes numéricas se reemplazan por pesos y sesgos, y el descenso por gradiente estima esos parámetros
  • La capa de salida calcula el valor de probabilidad final con una función de activación sigmoid
  • Esta forma puede cargarse en PyTorch para entrenarla, pero si se entrena tal cual, su rendimiento no es sobresaliente
  • Una razón del bajo rendimiento es que la arquitectura no es común, por lo que una mayor parte del procedimiento de entrenamiento se ejecuta en el código de conexión de Python y se aprovechan menos las implementaciones de las bibliotecas C++ de PyTorch

Implementación en PyTorch y limitaciones de los mensajes largos

  • torch.nn.RNN de PyTorch ofrece una implementación basada en Elman RNN
  • La arquitectura de ejemplo y Elman RNN difieren en la forma en que conectan las capas ocultas
    • En la arquitectura de ejemplo, la primera capa del token t recibe como entrada la tercera capa del token t-1, y cada capa recibe como entrada solo la capa inmediatamente anterior
    • En Elman RNN, cada capa oculta también recibe como entrada el estado del paso anterior de esa misma capa
    • La primera capa oculta de Elman RNN no recibe como entrada la capa final del paso anterior
  • Los mensajes reales de revisión de código pueden ser largos, y los mensajes largos producen secuencias largas de tokens
  • En secuencias largas, aunque el descenso por gradiente funcione en teoría, pueden aparecer problemas de estabilidad numérica por el desvanecimiento del gradiente
  • Elman RNN también puede ser vulnerable a este problema, y GRU o LSTM podrían rendir mejor en la tarea de detección de código

Disciplina basada en datos

  • Una RNN convierte combinaciones de reglas difíciles de manejar a mano en un objetivo de aprendizaje, y obliga a definir el problema con más claridad
  • Para entrenar la red se necesita lo siguiente
    • Elegir un dataset de entrenamiento y un dataset de validación
    • Etiquetado previo
    • Una función de pérdida que especifique qué debe lograr el clasificador y qué debe evitar
  • Este proceso revela zonas grises inesperadas y vuelve más claros los criterios de juicio
  • Esta disciplina basada en datos también es útil para problemas que se resuelven con algoritmos escritos a mano, no solo con redes neuronales

1 comentarios

 
GN⁺ 2024-07-02
Opiniones de Hacker News
  • El artículo no aborda mucho las pruebas ni la obtención de datos de entrenamiento, pero esa parte parece ser lo central.
    El código que sentimos que entendemos es aquel para el que, aunque sea de manera informal, nos demostramos que sus propiedades generalizan a todas las entradas. Por ejemplo, un algoritmo de ordenamiento ordena no solo las listas probadas, sino cualquier lista.
    La incertidumbre en las redes neuronales está en no saber cómo van a generalizar. Si una entrada no vista difiere aunque sea un poco, no hay propiedades garantizadas; y, para empezar, puede ser un problema en el que sea difícil especificar matemáticamente la propiedad deseada.
    Si se puede definir alguna propiedad con suficiente claridad como para usarla en pruebas basadas en propiedades como QuickCheck, entonces se puede usar aleatorización para generar una gran cantidad de pruebas o datos de entrenamiento. Basta con escribir pruebas que, partiendo de un ejemplo deseado, generen posibles variaciones de ejemplos positivos y negativos.
    No es una prueba formal, pero es un punto de partida. Al menos, si se pudiera demostrar, sabríamos qué habría que demostrar.
    Si se cuenta con algo así, depender de código espagueti y depender de una red neuronal se ven bastante parecidos. Si queremos satisfacer otras propiedades, basta con agregar otra prueba basada en propiedades. La red neuronal podría entrenarse en lugar de modificarse directamente, pero también existe asistencia de IA para modificar código.
    Aun así, creo que confiaría más en el código. Al menos se puede hacer debugging.

  • Es un artículo interesante si se lee como una forma de crear una red neuronal que realiza una tarea práctica. Pero si la próxima vez que tenga que parsear una entrada alguien pretende seguir este enfoque tal cual, sinceramente no sé qué decir.
    El autor toma el problema difícil de parsear entradas arbitrarias con patrones definidos de forma laxa, y dice correctamente que eso tiene muchas probabilidades de producir código espagueti difícil de leer.
    Pero como alternativa propone código tan difícil de leer que todavía se está investigando cómo funciona, es decir, una red neuronal.
    Es entendible, pero no hay que sobrevalorar algo directamente imposible de interpretar por encima de algo “feo”. Para algunas tareas puede que un modelo de machine learning sea lo adecuado, pero en muchos casos, aunque requiera esfuerzo, es mejor algo que se pueda leer y verificar para entender por qué funciona así que algo con lo que eso sea imposible.

    • Creo que el hecho de que el autor mencione el código espagueti se acerca a desviar el punto. Si la salida del algoritmo no está definida exactamente como una función de la entrada, pero sí hay ejemplos para mostrar, ahí es donde el machine learning resulta útil.
      Al final, el machine learning solo ofrece una opción más. Que encaje bien depende de los resultados de evaluación y del nivel de determinismo y explicabilidad que requiera el algoritmo elegido.
      Lo que llama la atención es si una RNN es la elección correcta. Requiere entrenamiento y quizá necesite muchos más ejemplos de los que se tienen. Dicho eso, parece posible crear datos sintéticos de casos positivos y negativos a partir de reglas conocidas.
    • El enfoque de código espagueti es básicamente un sistema experto. Puede verse como IA algorítmica de la vieja escuela. Fuera de dominios limitados, este tipo de sistemas rara vez ha funcionado realmente bien; la realidad es demasiado desordenada.
      Está bien tener sistemas en los que se pueda ver por qué actúan de cierta manera, pero eso no sirve de mucho si siguen dando respuestas incorrectas. En el uso real, muchas veces es más importante obtener la respuesta correcta que saber cómo se llegó a ella.
    • Suena como si la propuesta fuera esconder el desagradable código espagueti detrás de una prolija matriz de punto flotante de 1000x1000.
  • En redes neuronales existe el teorema de aproximación universal de funciones. Dice que se puede representar o codificar cualquier función hasta el nivel de precisión deseado[0].
    Pero no hay un teorema que diga que esa aproximación se pueda aprender, ni cómo aprenderla.
    [0] https://en.m.wikipedia.org/wiki/Universal_approximation_theo...

    • Esa demostración se menciona con frecuencia, pero lo que muestra en la práctica es poco más que la equivalencia de una red neuronal con una tabla de consulta. Una tabla de consulta con memoria suficiente puede aproximar cualquier función.
      Eso está lejos de explicar cómo funcionan realmente las redes neuronales prácticas y útiles, como las redes convolucionales, los transformers o las LSTM.
    • Como referencia, hay muchos algoritmos que se demostró que eran aproximadores universales de funciones mucho antes que las redes neuronales. Las redes neuronales no son las únicas ni fueron las primeras. En muchos casos, hay bastantes métodos mucho más adecuados que una red neuronal.
    • No aplica a cualquier función. Hay restricciones sobre los tipos de funciones a los que se aplica el teorema de aproximación universal.
      Curiosamente, este teorema trata sobre redes de una sola capa. En la práctica, tener varias capas funciona mucho mejor.
    • Solo puede modelar funciones continuas; más precisamente, puede aproximar con precisión arbitraria cualquier función continua sobre un subconjunto compacto de ℝⁿ, siempre que haya suficientes neuronas.
    • Entonces me pregunto qué significa aprender.
  • Es un artículo realmente bueno y, aunque no entendí del todo los conceptos matemáticos más profundos alrededor de las RNN, me hizo pensar en varias cosas.
    Se siente parecido a algo que he estado explorando últimamente: construir una app vinculándola con un algoritmo de encadenamiento hacia adelante. El autor usa una RNN, y yo estoy intentando hacerlo con el algoritmo Rete.
    También me parece potente la idea de procesar la cadena de entrada carácter por carácter. Así dejamos la lógica de inferencia al algoritmo, y nosotros solo escribimos una lógica de entrada/salida muy delgada; el algoritmo se encarga del resto.

  • Lo bueno de este artículo es que explica, desde la experiencia de entrenamiento, qué significa en la práctica convertir una función en una RNN, y lo compara con una RNN “con pilas incluidas” incluida en PyTorch.
    La pregunta es que se decía que para modelar el estado había que agregar tres capas ocultas a la red: ¿por qué tres? Me pregunto si fue resultado de una regla específica que se intentaba implementar, o si es un número de capas que se usa en general al implementar reglas de este tipo con esta arquitectura. También me pregunto si con una arquitectura Elman habría sido posible con menos capas.

    • Sobre la primera pregunta, usar tres capas ocultas hace que sea un poco más claro qué está haciendo la red. Cada capa realiza un paso del cálculo.
      La primera capa reúne lo que se puede saber del token actual y lo que se sabía después de calcular el token anterior. La segunda capa verifica si se cumple la regla de decisión para determinar si el token actual parece código de programa. La tercera capa compara esa decisión con las decisiones sobre los tokens anteriores.
      Creo que esto también podría comprimirse en una sola capa oculta. Como ReLU es suficiente para capturar la no linealidad, parece posible. No he revisado lo suficiente la correspondencia con la arquitectura Elman, así que no sé la respuesta.
  • ¿Las RNN quedaron completamente absorbidas por los transformers? Me pregunto si ya podemos olvidarnos de cómo manejar RNN y enfocarnos solo en transformers.

    • Para complicar más esta pregunta, vale la pena ver el paper “Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention” - https://arxiv.org/pdf/2006.16236
      Ahí muestran que una definición específica y acotada de transformer, es decir, un transformer con enmascaramiento causal, es equivalente a una RNN, y también a la inversa.
      De forma similar, Mamba(https://arxiv.org/abs/2312.00752), una arquitectura muy popular últimamente, también tiene una unidad equivalente a una RNN con compuertas. Tengo entendido que, por razones de rendimiento, durante el entrenamiento usa una CNN equivalente y durante la inferencia usa una RNN.
    • Los transformers tienen un contexto finito, las RNN no. En la práctica, la señal de gradiente de una RNN está limitada y se atenúa por la retropropagación en el tiempo.
      Esa es, de hecho, la ventaja clave de los transformers. Las asociaciones a corta y larga distancia no son más difíciles ni más fáciles. Pero, en teoría, una RNN puede recordar un pasado infinitamente lejano.
    • Si quieres hacer un doctorado o ser investigador en machine learning, no; fuera de eso, sí.
      Durante los últimos 7 años trabajé con machine learning/LLM como ingeniero de investigación, incluso en laboratorios de investigación de FAANG, y aunque siempre pensé que debería aprender RNN, en realidad nunca las aprendí ni las necesité.
  • Si te parece interesante, vale la pena mirar la programación genética. La veo como un enfoque más simple para el mismo problema y no requiere matemáticas.
    Recombina programas con base en árboles de sintaxis abstracta y, si le das alguna heurística, optimiza el programa según ese criterio. La magia está en la función heurística, y puedes elegir qué optimizar: velocidad, longitud del programa, minimización de estructuras complejas o llamadas a funciones, eficiencia de red, o alguna combinación de todo eso.
    https://youtu.be/tTMpKrKkYXo

    • También quisiera agregar los Humies Awards, que muestran resultados competitivos a nivel humano. Con solo hojear los papers enviados se puede aprender mucho sobre qué es posible e imposible en este campo.
      https://www.human-competitive.org/
  • Hace poco escribí un post de blog explorando la idea de hacer interfaz con LLM locales para este tipo de tareas ambiguas.
    Me parece que eso tiene más sentido que codificar una red neuronal directamente. Usar algo como llama.cpp para evaluar si un modelo pequeño puede resolver el problema tal cual y, si no puede, hacerle fine-tuning y luego integrarlo programáticamente con llama.cpp mediante el wrapper que quieras parece más práctico.

  • Las redes neuronales recurrentes pueden usarse para cómputo arbitrario, y también se ha demostrado su equivalencia con las máquinas de Turing. Pero son completamente imprácticas para esa tarea.
    El enfoque de este artículo de algún modo parece una máquina de estados aprendida. Al artículo le vendría bien un resumen más largo, y “Python” no parece tener ninguna relación. Aprender la semántica real de Python sería bastante difícil por las características del lenguaje: no es que haya un estándar, sino que el lenguaje hace lo que hace CPython.

    • El artículo de Karpathy de 2015 sobre RNN[1] mostró que una RNN entrenada carácter por carácter con obras de Shakespeare podía generar texto con estilo shakespeariano, aunque sin la coherencia narrativa de un LLM.
      Entonces, ¿por qué no podría manejar lenguaje natural formal, como comentarios de revisión de código?
      En ese caso, la inferencia se ejecutaba con entradas aleatorias para crear un “Shakespeare” aleatorio, pero la estructura y el estilo del lenguaje aun así fueron aprendidos por la RNN. Tal vez también pueda usarse para clasificación.
      1. https://karpathy.github.io/2015/05/21/rnn-effectiveness/
  • Primero compilas Python a una red neuronal, y luego lo encajas a la fuerza sobre una red neuronal basada en transformers.
    Así, una Transformer Virtual Machine(TVM) puede ejecutar programas arbitrarios.
    Usando transferencia de aprendizaje, es decir, superponiendo pesos entre sí, un LLM puede “nacer” con algoritmos profundamente codificados.