2 pontos por GN⁺ 2024-07-02 | 1 comentários | Compartilhar no WhatsApp
  • Programas de extração de informações ambíguas tendem a virar código espaguete à medida que regras de exceção se acumulam, e essa lógica baseada em estado pode ser repensada como uma rede neural recorrente (RNN)
  • Um exemplo de encontrar referências a código C em mensagens de code review é implementado como um classificador escrito à mão que rastreia padrões de tokens como identifieropen_parenclose_paren como estados
  • Essa regra mostra 100% de precisão no exemplo, mas deixa passar casos como if (err) goto cleanup;, fazendo o recall ficar em 50%; conforme regras são adicionadas, State e ramificações ficam mais complexos
  • A mesma máquina de estados pode ser codificada como o estado oculto e os cálculos de camadas de uma RNN; para torná-la treinável, são necessários ReLU, sigmoid e pesos e vieses treináveis em vez de funções indicadoras binárias
  • Implementações como Elman RNN, GRU e LSTM no PyTorch e o problema de desvanecimento do gradiente em longas sequências de tokens tornam-se limitações práticas do treinamento; o próprio processo de definir dataset, rótulos e função de perda também ajuda no projeto de regras manuais

Como a extração de dados ambíguos vira código espaguete

  • Programas de pesquisa que extraem informações de dados brutos veem suas regras ficarem rapidamente complexas quando os dados não seguem uma especificação clara ou têm formatos incomuns
  • Exemplos de tarefas incluem identificar empresas e executivos em notícias, rotular contratos de compras públicas por tipo de serviço e decidir se mensagens de engenheiros contêm código de programa
  • Se você quiser uma saída perfeita, pode inspecionar cada observação cuidadosamente e escrever testes unitários para casos representativos
    • Tanto R quanto Python oferecem bibliotecas de teste para isso
  • Em situações que exigem regras de julgamento complexas, como apelidos, sinônimos e a fronteira entre inglês e código, regras escritas à mão ficam frágeis com facilidade
  • Algoritmos de treinamento de redes neurais mudam o problema para encontrar essas combinações de regras nos dados, em vez de uma pessoa continuar ajustando-as manualmente

Encontrando referências a código em mensagens de code review

  • O objetivo é detectar se uma mensagem enviada durante um code review faz referência explícita a código de programa
  • Presume-se que a base de código observada seja escrita em C
  • Mensagens representativas incluem referências a código como:
    • render_ipa_alloc()
    • FTPSACK
    • debug_error()
    • NULL
    • IS_ERROR()
    • aarch64, amd64
    • if (err) goto cleanup;
  • Candidatas a regras simples falham de maneiras diferentes
    • Uma regra que considera código quando um parêntese vem depois de uma palavra captura casos como render_ipa_alloc(), mas deixa passar if (err) goto cleanup;
    • Uma regra que considera código palavras totalmente em maiúsculas captura FTPSACK e IS_ERROR(), mas gera falso positivo para abreviações como AFAICT
    • Uma regra que considera código palavras que não são inglês pode marcar falsamente até termos de engenharia ou nomes de arquitetura como código
  • Para melhorar as regras 2 e 3, seria necessária uma lista de abreviações e termos técnicos como AFAICT, LGTM, USD, COVID, aarch64 e amd64

Classificador feito com máquina de estados escrita à mão

  • O algoritmo simples decide em duas etapas se uma mensagem contém código
    • Pré-processamento: converte a mensagem em uma sequência de tokens que refletem elementos sintáticos do código C
    • Inferência: verifica se a sequência de tokens satisfaz as regras
  • A Rule 1 considera o padrão underscore_identifieropen_parenclose_paren como referência a código
  • A implementação em Python armazena o estado dos tokens anteriores em uma data class State
    • previous_was_identifier
    • previous_was_open_paren
    • previous_previous_was_identifier
    • seen_code
  • contains_code percorre os tokens chamando process e, ao final, retorna state.seen_code
  • process define seen_code como True quando o token atual é close_paren, o anterior é open_paren e o anterior a esse é um identificador
  • Esse classificador não tem falsos positivos e mostra 100% de precisão no exemplo, mas deixa muitos casos passarem, então o recall fica em 50%
  • Ao adicionar a Rule 2, os campos de State e as ramificações if/elif/else aumentam, e quanto mais as regras são refinadas, mais difícil fica a manutenção

Levando a máquina de estados para uma RNN

  • contains_code e process são uma máquina de estados, e máquinas de estados podem ser codificadas como redes neurais recorrentes (RNNs)
  • Uma RNN processa a sequência de tokens um por um e aproxima a probabilidade condicional de que a mensagem contenha código
  • O valor correspondente ao State em Python é representado pelo estado oculto da RNN
    • State_0 é o estado inicial
    • Cada State_t é calculado passando o token atual e o estado anterior para a função f
    • O estado final passa pela camada de saída g e vira o resultado da classificação
  • A RNN do exemplo usa três camadas ocultas
    • A primeira camada armazena ou copia o token atual e o estado anterior
    • A segunda camada verifica o padrão correspondente à Rule 1
    • A terceira camada lembra se já viu um padrão de código
  • Tokens são representados como vetores binários em formato one-hot
  • Para imitar exatamente o algoritmo escrito à mão, é possível usar a função indicadora binária 1{x > 0}
    • As camadas ocultas podem ser mantidas como valores binários
    • Mas isso é inadequado para treinamento, porque a derivada é 0 em quase todos os lugares
  • Também seria possível verificar o padrão pelo produto de identifier, open_paren e close_paren, mas, em camadas ocultas binárias, a mesma verificação pode ser expressa por soma
  • Giles et al. (1992) é citado como um caso de uso de RNN de segunda ordem para descobrir máquinas de estados

Transformando em uma rede treinável

  • Para treinar, usa-se ReLU em vez da função indicadora binária
  • Constantes numéricas são substituídas por pesos e vieses, e o gradiente descendente estima esses parâmetros
  • A camada de saída calcula o valor final de probabilidade com uma função de ativação sigmoid
  • Esse formato pode ser colocado no PyTorch e treinado, mas, se treinado como está, não tem desempenho excelente
  • Um dos motivos para o desempenho insuficiente é que a arquitetura não é convencional, então uma parte maior do procedimento de treinamento roda no código de conexão em Python, aproveitando menos as implementações das bibliotecas C++ do PyTorch

Implementação em PyTorch e limitações de mensagens longas

  • torch.nn.RNN, do PyTorch, oferece uma implementação baseada em Elman RNN
  • A arquitetura do exemplo e a Elman RNN diferem na forma de conexão das camadas ocultas
    • Na arquitetura do exemplo, a primeira camada do token t recebe como entrada a terceira camada do token t-1, e cada camada recebe como entrada apenas a camada imediatamente anterior
    • Na Elman RNN, cada camada oculta também recebe como entrada o estado do instante anterior da mesma camada
    • A primeira camada oculta da Elman RNN não recebe como entrada a camada final do instante anterior
  • Mensagens reais de code review podem ser longas, e mensagens longas levam a sequências longas de tokens
  • Em sequências longas, mesmo que o gradiente descendente funcione em teoria, podem surgir problemas de estabilidade numérica por causa do desvanecimento do gradiente
  • Elman RNNs também podem ser vulneráveis a esse problema, e GRU ou LSTM podem ter desempenho melhor na tarefa de detecção de código

Disciplina orientada por dados

  • RNNs transformam combinações de regras difíceis de manipular manualmente em um alvo de treinamento e forçam uma definição mais clara do problema
  • Para treinar a rede, é preciso:
    • escolher um dataset de treinamento e um dataset de validação
    • rotular previamente
    • definir uma função de perda que explicite o que o classificador deve alcançar e o que deve evitar
  • Esse processo revela zonas cinzentas inesperadas e torna os critérios de julgamento mais claros
  • Essa disciplina orientada por dados é útil também para problemas resolvidos com algoritmos escritos à mão, não apenas com redes neurais

1 comentários

 
GN⁺ 2024-07-02
Opiniões do Hacker News
  • O texto não trata muito de testes nem da obtenção de dados de treinamento, mas isso parece ser o ponto central.
    Um código que sentimos entender é aquele para o qual nós mesmos provamos, ainda que informalmente, que suas propriedades se generalizam para todas as entradas. Por exemplo, um algoritmo de ordenação ordena não só as listas testadas, mas qualquer lista.
    A parte incerta em redes neurais está em não saber como elas vão se generalizar. Se uma entrada não vista for só um pouco diferente, não há propriedades garantidas; e, para começo de conversa, pode ser um problema em que é difícil especificar matematicamente a propriedade desejada.
    Se for possível definir uma propriedade com clareza suficiente para usá-la em testes baseados em propriedades, como QuickCheck, dá para criar um grande volume de testes ou dados de treinamento por meio de aleatorização. Basta escrever testes que, partindo de um exemplo desejado, gerem possíveis variações de exemplos positivos e negativos.
    Não é uma prova, mas é um ponto de partida. Pelo menos, se for possível provar, dá para saber o que precisa ser provado.
    Com algo assim, depender de código espaguete e depender de uma rede neural parecem coisas bem parecidas. Se você quiser satisfazer também outras propriedades, basta acrescentar mais um teste baseado em propriedades. A rede neural poderia ser treinada em vez de modificada diretamente, mas também há assistência de IA para modificar código.
    Ainda assim, acho que eu confiaria mais no código. Pelo menos dá para fazer debugging.

  • É um texto interessante se lido como uma forma de criar uma rede neural que execute uma tarefa prática. Mas, se na próxima vez em que eu precisar fazer parsing de uma entrada alguém disser que vai seguir exatamente esse método, sinceramente não sei o que dizer.
    O autor pega o problema difícil de fazer parsing de entradas arbitrárias com padrões definidos de forma frouxa e diz, corretamente, que isso tem grande chance de virar código espaguete difícil de ler.
    Só que, como alternativa, propõe um código tão difícil de ler que ainda estamos estudando como ele funciona: uma rede neural.
    Dá para entender, mas não devemos supervalorizar algo totalmente impossível de interpretar em relação a algo apenas “feio”. Para algumas tarefas, um modelo de aprendizado de máquina pode ser o certo, mas em muitos casos, mesmo exigindo esforço, é melhor ter algo cujo funcionamento possamos ler e verificar do que algo em que isso é impossível.

    • Acho que o autor ter trazido código espaguete para a discussão é quase uma distração do ponto principal. Quando a saída de um algoritmo não é definida exatamente como função da entrada, mas há exemplos que você consegue mostrar, aí é onde aprendizado de máquina é útil.
      No fim, aprendizado de máquina apenas oferece mais uma opção. Se ela se encaixa bem depende dos resultados da avaliação e do nível de determinismo e explicabilidade exigido pelo algoritmo escolhido.
      O que chama atenção é se uma RNN é a escolha certa. Ela precisa de treinamento e pode exigir muito mais exemplos do que os disponíveis. Por outro lado, parece possível criar dados sintéticos de casos positivos e negativos com base nas regras conhecidas.
    • A abordagem de código espaguete é basicamente um sistema especialista. Pode ser vista como IA algorítmica à moda antiga. Fora de domínios limitados, sistemas desse tipo raramente funcionaram bem, porque a realidade é bagunçada demais.
      Um sistema em que dá para ver por que ele se comporta daquele jeito é bom, mas não significa nada se ele continua dando respostas erradas. No uso real, muitas vezes obter a resposta certa é mais importante do que saber como se chegou a ela.
    • Parece uma proposta de esconder o código espaguete feio atrás de uma elegante matriz de ponto flutuante 1000x1000.
  • Redes neurais contam com o teorema da aproximação universal de funções. Ele diz que é possível representar ou codificar qualquer função até o nível de precisão desejado[0].
    Mas não há um teorema dizendo que essa aproximação pode ser aprendida, nem como ela é aprendida.
    [0] https://en.m.wikipedia.org/wiki/Universal_approximation_theo...

    • Essa prova é frequentemente citada, mas o que ela de fato mostra é algo como a equivalência entre redes neurais e uma tabela de consulta. Uma tabela de consulta com memória suficiente consegue aproximar qualquer função.
      Isso está longe de explicar como redes neurais realistas e úteis, como redes neurais convolucionais, transformers e LSTMs, realmente funcionam.
    • Como observação, há muitos algoritmos que, muito antes das redes neurais, já foram provados como aproximadores universais de funções. Redes neurais não são as únicas nem foram as primeiras. Em muitos casos, há métodos bastante mais adequados do que redes neurais.
    • Não é qualquer função. Há restrições sobre os tipos de função aos quais o teorema da aproximação universal se aplica.
      Curiosamente, esse teorema trata de redes com uma única camada. Na prática, redes com várias camadas funcionam muito melhor.
    • Elas só conseguem modelar funções contínuas; mais precisamente, conseguem aproximar com precisão arbitrária qualquer função contínua sobre um subconjunto compacto de ℝⁿ, desde que haja neurônios suficientes.
    • Então fico curioso sobre o que “aprendizado” quer dizer.
  • É um texto realmente bom e, embora eu não tenha entendido completamente os conceitos matemáticos mais profundos em torno de RNNs, ele provoca várias ideias.
    Parece parecido com algo que venho explorando ultimamente: criar apps conectando-os a um algoritmo de encadeamento para frente. O autor usa uma RNN; eu estou construindo em cima do algoritmo Rete.
    Também acho poderosa a ideia de pensar em consumir a string de entrada caractere por caractere. Assim, deixamos a lógica de inferência para o algoritmo e escrevemos apenas uma camada muito fina de entrada e saída; o algoritmo cuida do resto.

  • O que torna este texto bom é que ele explica, a partir da experiência de treinamento, o que realmente significa transformar uma função em uma RNN, comparando isso com a RNN “com baterias incluídas” que vem no PyTorch.
    A pergunta é: foi dito que seria preciso adicionar três camadas ocultas à rede para modelar o estado; por que três? Fico curioso se isso é resultado das regras específicas que ele estava tentando implementar, ou se é o número de camadas geralmente usado ao implementar regras desse formato nessa arquitetura. Também me pergunto se uma estrutura Elman talvez conseguisse fazer isso com menos camadas.

    • Sobre a primeira pergunta, usar três camadas ocultas torna um pouco mais claro o que a rede está fazendo. Cada camada realiza uma etapa do cálculo.
      A primeira camada reúne o que é possível saber a partir do token atual e o que já se sabia após o cálculo do token anterior. A segunda camada verifica se a regra de decisão é satisfeita e decide se o token atual parece código de programa. A terceira camada compara essa decisão com as decisões referentes aos tokens anteriores.
      Acho que isso também poderia ser comprimido em uma única camada oculta. Como ReLU é suficiente para capturar a não linearidade, parece possível. Ainda não examinei o suficiente a correspondência com a estrutura Elman, então não sei responder.
  • RNNs foram completamente absorvidas pelos transformers? Fico me perguntando se dá para esquecer como lidar com RNNs e focar só em transformers

    • Para tornar essa pergunta mais complicada, vale olhar o artigo “Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention” - https://arxiv.org/pdf/2006.16236
      Ele mostra que uma definição específica e estreita de transformer — ou seja, transformers com mascaramento causal — é equivalente a RNNs, e vice-versa
      De forma parecida, Mamba(https://arxiv.org/abs/2312.00752), uma arquitetura em alta hoje, também tem unidades equivalentes a RNNs com gates. Pelo que sei, por motivos de desempenho, usa uma CNN equivalente no treinamento e uma RNN na inferência
    • Transformers têm contexto finito, mas RNNs não. Na prática, o sinal de gradiente das RNNs é limitado e se atenua por causa da retropropagação no tempo
      Na verdade, essa é a principal vantagem dos transformers. Relações de curta e longa distância não são mais difíceis nem mais fáceis. Mas, teoricamente, uma RNN pode lembrar um passado infinitamente distante
    • Se você quer virar doutor em machine learning ou pesquisador, não; fora isso, sim
      Trabalhei com machine learning/LLMs como engenheiro de pesquisa nos últimos 7 anos e também em um laboratório de pesquisa de uma FAANG, mas sempre só pensei que deveria aprender RNNs; nunca aprendi de fato nem precisei delas
  • Se isso parece interessante, vale dar uma olhada em programação genética. Vejo como uma abordagem mais simples para o mesmo problema, sem exigir matemática
    Os programas são recombinados com base na árvore de sintaxe abstrata e, se você fornece alguma heurística, o programa é otimizado de acordo com esse critério. A mágica está na função heurística, e você pode escolher o que quiser otimizar: velocidade, tamanho do programa, minimização de estruturas complexas ou chamadas de função, eficiência de rede, ou alguma combinação disso
    https://youtu.be/tTMpKrKkYXo

    • Também quero acrescentar o Humies Awards, que mostra resultados competitivos em nível humano. Só de folhear os artigos submetidos já dá para aprender bastante sobre o que é possível e impossível nessa área
      https://www.human-competitive.org/
  • Recentemente escrevi um post de blog explorando a ideia de fazer interface com LLMs locais para esse tipo de tarefa ambígua
    Acho que isso faz mais sentido do que codificar diretamente uma rede neural. Parece mais prático usar algo como llama.cpp para avaliar se um modelo pequeno consegue resolver o problema como está; se não conseguir, fazer fine-tuning e então integrar programaticamente com o llama.cpp usando o wrapper desejado

  • Redes neurais recorrentes podem ser usadas para computação arbitrária, e a equivalência com máquinas de Turing também já foi provada. Mas são completamente impraticáveis para essa tarefa
    A abordagem deste texto parece, de algum modo, uma máquina de estados aprendida. O texto se beneficiaria de um resumo mais longo, e “Python” não parece ter nenhuma relação real. Aprender a semântica real de Python seria bem difícil, dada a natureza da linguagem: não é que exista um padrão, a linguagem é aquilo que o CPython faz

    • O texto de 2015 do Karpathy sobre RNNs[1] mostrou que uma RNN treinada em nível de caracteres nas obras de Shakespeare conseguia gerar texto no estilo de Shakespeare, mesmo sem a coerência narrativa de um LLM
      Então por que ela não conseguiria lidar com linguagem natural formal, como comentários de code review?
      Nesse caso, a inferência era executada com uma entrada aleatória para gerar “Shakespeare” aleatório, mas a estrutura e o estilo da linguagem ainda assim foram aprendidos pela RNN. Talvez também possa ser usada para classificação
      1. https://karpathy.github.io/2015/05/21/rnn-effectiveness/
  • Primeiro você compila Python para uma rede neural e depois encaixa isso, meio na marra, em uma rede neural baseada em transformers
    Assim, uma Transformer Virtual Machine (TVM) consegue executar programas arbitrários
    Usando transfer learning — isto é, sobrepondo pesos uns aos outros —, um LLM pode “nascer” com algoritmos profundamente codificados