Mi código Python es una red neuronal
(blog.gabornyeki.com)- 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
identifier–open_paren–close_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,Statey 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()FTPSACKdebug_error()NULLIS_ERROR()aarch64,amd64if (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 omiteif (err) goto cleanup; - La regla de considerar código a una palabra en mayúsculas captura
FTPSACKeIS_ERROR(), pero produce falsos positivos con siglas comoAFAICT - 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
- La regla de considerar código a una palabra seguida de paréntesis captura casos como
- 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_identifier–open_paren–close_parencomo una referencia a código - La implementación en Python guarda el estado de los tokens anteriores con una clase de datos
Stateprevious_was_identifierprevious_was_open_parenprevious_previous_was_identifierseen_code
contains_coderecorre los tokens llamando aprocessy al final devuelvestate.seen_codeprocessestableceseen_codeenTruesi el token actual esclose_paren, el anterior fueopen_pareny 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
Statey las ramasif/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_codeyprocessson 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
Statede Python se representa como el estado oculto de la RNNState_0es el estado inicial- Cada
State_tse calcula pasando el token actual y el estado anterior a la funciónf - El estado final pasa por la capa de salida
gy 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_parenyclose_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
trecibe como entrada la tercera capa del tokent-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
- En la arquitectura de ejemplo, la primera capa del token
- 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
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.
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.
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.
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...
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.
Curiosamente, este teorema trata sobre redes de una sola capa. En la práctica, tener varias capas funciona mucho mejor.
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.
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.
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.
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.
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
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.
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.
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.