1 pontos por GN⁺ 2023-08-15 | 1 comentários | Compartilhar no WhatsApp
  • Outlines é uma ferramenta de saída estruturada para LLMs que permite definir o tipo de saída desejado durante a geração, garantindo que os dados correspondam exatamente a essa estrutura
  • Em vez de corrigir saídas inválidas após a geração com parsing, expressões regulares e código frágil, ela impõe diretamente uma estrutura válida na etapa de geração
  • O uso segue o formato model(prompt, output_type), permitindo especificar a saída de acordo com o sistema de tipos do Python, como Literal["Yes", "No"], int e modelos Pydantic
  • Os tipos de saída suportados incluem opções predefinidas, Function Calls baseadas em assinatura de função, esquemas JSON/Pydantic, padrões de expressão regular e imposição de estrutura baseada em gramáticas
  • A integração com modelos é dividida entre suporte a servidor com vLLM e Ollama, suporte a modelos locais com transformers e llama.cpp, e suporte via API com OpenAI, Gemini e Dottxt
  • Exemplos de fluxo de trabalho incluem converter e-mails de clientes em tickets de suporte, transformar descrições de produtos em dados categorizados, retornar informações estruturadas ou "I don't know" a partir de descrições incompletas de eventos, classificar documentos em categorias predefinidas e converter pedidos de reunião em linguagem natural em parâmetros de função
  • Os templates de prompt oferecem suporte a outlines.Template.from_string com base em Jinja e ao carregamento de arquivos, para separar prompts complexos do código e reutilizá-los
  • A instalação é feita com pip install outlines, e a .txt API está atualmente marcada como early access

1 comentários

 
GN⁺ 2023-08-15
Opiniões no Hacker News
  • Esta biblioteca parece usar uma ideia simples no mecanismo: mascarar parte do espaço vocabular e avançar de forma eficiente a cada etapa de tempo, o que é excelente.
    Mas, como alguém que já usou bibliotecas que impõem uma estrutura de saída sobre um LLM base, fico curioso se modelos base como o Llama2 realmente funcionam bem.
    Pela minha experiência, é quase “de jeito nenhum”; para fazer funcionar de fato, foi preciso bastante instruction tuning específico para o caso de uso.
    Além disso, mascarar o espaço de estados a posteriori durante a geração em um modelo com instruction tuning acaba mudando a distribuição de geração, então parece contraintuitivo que isso também possa prejudicar o instruction tuning.

    • Não vejo muito motivo para usar o llama-2 original. No HF Hub há muitas versões fortes do llama-2 ajustadas finamente para instruções, e esses modelos fariam o trabalho muito melhor. Por exemplo, há o Beluga-2 da Stability-AI: https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderb...
      Sobre o segundo ponto, se o objetivo for, por exemplo, fazer o modelo gerar apenas JSON, isso é 100% possível restringindo quais tokens de saída podem ou não ser usados.
    • Fiquei bem impressionado com o Llama 2 13B e, quanto mais uso, mais penso que talvez ele tenha utilidade real além de ser um brinquedo de LLM local.
      Como dá para usar a GPU no M2 Mac, estou usando a versão MLC com o plugin https://github.com/simonw/llm-mlc.
    • Pelo menos em geração de código, orientar a geração em nível de token conseguiu melhorar bastante até modelos base.
      No artigo “Guiding Language Models of Code with Global Context using Monitors” (https://arxiv.org/abs/2306.10763), foi proposta a Monitor Guided Decoding, que conecta o LLM à análise estática para orientar a geração de código com consistência de tipos.
      Sem nenhum fine-tuning, ao orientar a geração em nível de token em pontos específicos por meio de análise estática, a qualidade do código gerado melhorou muito tanto em compilabilidade quanto em correspondência com a resposta correta. Até modelos muito pequenos (1,1B) passaram a gerar mais código compilável do que modelos muito maiores (175B), além de melhorar a correspondência com a resposta correta.
    • Se “o mascaramento a posteriori durante a geração em um modelo com instruction tuning muda a distribuição de geração”, isso não é o que já se fazia em desenvolvimento orientado a testes?
      A principal diferença é apenas que a função geradora era uma pessoa, não um LLM. Não vejo por que não remover o humano do meio.
    • O instruction tuning em si é relativamente “trivial”; o realmente difícil é lidar com condições de borda.
      No código tradicional, condições de borda eram literalmente algo próximo de pequenos casos especiais, mas em LLMs não dá para saber o que vai fazê-los sair por uma direção absurda, e o código de parsing precisa aguentar esse caos.
      Em outras palavras, parece que a proporção de casos que precisam ser tratados como condições de borda aumentou drasticamente.
  • Com o GPT-4, dá para fazê-lo retornar JSON válido colocando apenas exemplos na mensagem de sistema, e funciona nove em cada dez vezes.
    Mas ainda é probabilístico, e nove em dez não é suficiente.
    Às vezes ele também produz respostas alucinadas como {"key1": "value1", "key2": "value2" for i in range(n)}.
    Se você refaz o prompt junto com a mensagem de erro de parsing, normalmente isso se resolve na segunda tentativa.
    Mas escapar aspas duplas e caracteres de quebra de linha é menos confiável. Mesmo dando vários exemplos, ele só escapa corretamente em cerca de metade das vezes, e mesmo refazendo o prompt com o erro de escape, a taxa de sucesso fica por volta de 50%.

    • Repetir o prompt quando ocorre erro é uma abordagem que a nova biblioteca TypeChat da Microsoft também usa: https://github.com/microsoft/TypeChat
      O prompt em questão está aqui: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
      Acho que abordagens baseadas em gramática como a mostrada aqui, ou como https://github.com/ggerganov/llama.cpp/pull/1773, são soluções muito mais elegantes.
    • Tive resultados melhores pedindo saída em XML do que em JSON. (1) XML permite incluir mais linguagem e significado reais, e LLMs gostam disso; (2) dá para tornar o parser mais tolerante.
      Entendo que as pessoas queiram JSON, mas para mim é parecido com tentar fazer um gato nadar. No fim até pode dar certo, mas não é a inclinação natural.
    • Usando a chamada de função do ChatGPT, consigo obter JSON válido 100% das vezes no GPT-4, desde que não cometa erros no prompt.
      O principal erro é não fornecer uma saída de escape. Como o LLM tenta encontrar a resposta correta, se você passa textos e pede para ele retornar dados estruturados, mas algum texto está vazio, fica difícil determinar a resposta correta e surgem alucinações.
      A solução é criar uma saída de escape, por exemplo incluindo entre os argumentos um booleano como textIsMissing. Levando em conta esse padrão de falha, funciona sem problemas.
    • Pedi ao GPT-4 para retornar uma amostra de código PHP dentro de um JSON arbitrário, e ele já falhou no linter de JSON na primeira tentativa.
      Tentei várias vezes de novo e ainda pedi correções em seguida, mas ele não passou na validação; nunca gerou JSON 100% válido, então acabei desistindo.
    • A geração com restrições gramaticais tem duas grandes vantagens.
      Primeiro, usa menos tokens porque não é preciso colocar exemplos demais no prompt.
      Segundo, sofre menos com problemas de esquecimento.
      Uma vantagem menor é que dá para controlar exatamente onde a saída desejada deve começar. Ainda assim, no conjunto, vejo como um bom recurso adicional, não como algo absolutamente essencial.
  • Uma grande parte do poder dos LLMs está na distribuição de probabilidade calibrada das respostas, e esta técnica provavelmente parece abrir mão dessa capacidade. Fico me perguntando por que isso seria suficiente
    Como exemplo simples, suponha que as únicas saídas possíveis do LLM sejam “hello world”, “food”, “hello” e “good day”, e que, sem prompt, todas tenham a mesma probabilidade. Vamos supor que a gramática imponha apenas a restrição de que deve haver um espaço em algum lugar da saída
    Se amostrarmos a saída do LLM até que ela passe pela gramática, receberemos “hello world” e “good day” com a mesma probabilidade. Mas, aplicando a técnica do site, “hello world” aparece duas vezes mais frequentemente que “good day”
    O problema central é que, mesmo que algum prefixo de resposta tivesse probabilidade extremamente baixa de levar a uma resposta válida, essa técnica, assumindo sucesso, acaba produzindo uma resposta válida a partir daquele prefixo. Se houver independência suficiente nos lugares certos, tudo bem, mas em modelos autorregressivos erros correlacionados se acumulam rapidamente
    Perguntando especificamente sobre JSON: quando um LLM produz uma resposta que não segue o schema, ele tende a produzir mais ou menos erros factuais, alucinações, strings truncadas, omissões de personagens principais etc.? Se a taxa de erros factuais estiver relacionada de forma não trivial à taxa de erros de schema, esse caminho é mais perigoso do que parece. Vendo como uma palavra específica ou uma sequência de palavras grudadas pode influenciar bastante a saída de um LLM, parece bem provável que detalhes como conformidade com schema também se espalhem para outras características da saída

    • Se for geração de múltipla escolha, como neste caso, quando uma das saídas possíveis não corresponde à regex, basta excluí-la da geração
      Tentei pensar em um exemplo em que “um prefixo de resposta tinha probabilidade muito baixa de levar a uma resposta válida, mas a técnica, de algum modo, monta uma resposta válida e causa um problema”, porém não me veio nada bom. Se houver um bom exemplo, pode ser uma questão de pesquisa interessante
  • Relacionado a isso, o LLama.cpp implementou amostragem baseada em gramática no mês passado
    https://news.ycombinator.com/item?id=36819906
    https://github.com/ggerganov/llama.cpp/pull/1773

    • Como descrito no artigo acima, nossa abordagem também pode ser estendida para amostragem baseada em gramática. PR relacionado: https://github.com/normal-computing/outlines/pull/178
      Nosso método é muito mais eficiente. O llama.cpp percorre todo o vocabulário, cerca de 50 mil tokens, a cada etapa para criar a máscara
      Nós criamos um índice na inicialização e, a cada etapa, para construir a máscara, basta fazer uma consulta em dicionário. É uma troca de velocidade por memória, e a amostragem fica tão rápida quanto a amostragem padrão
    • Na mesma época, também tínhamos uma implementação de condução guiada por gramática: https://github.com/normal-computing/outlines/pull/131
      Pelo número de artigos relacionados, provavelmente havia várias outras em outros lugares. O ponto aqui e no trabalho atual é oferecer condução a custo muito baixo; no caso de regex, isso já estava implementado havia algum tempo, e depois foi expandido para JSON
  • Obrigado por criar algo assim; o princípio de funcionamento é uma ideia tão óbvia que é surpreendente que as plataformas de primeira linha ainda não tenham feito isso
    Fico curioso sobre como isso poderia ser usado em outras tarefas que exigem entrada estruturada além de JSON

    • Eu entendia que a tecnologia de LLMs está hoje em uma corrida armamentista muito acelerada e muda drasticamente a cada poucos meses
      Pode ser resultado de recursos limitados de desenvolvimento. Seria surpreendente faltar uma funcionalidade básica dessas em uma tecnologia de 10 anos, mas, em uma tecnologia de IA em plena corrida armamentista, parece compreensível que ainda faltem conveniências
    • Expandimos essa abordagem para amostragem baseada em gramática, como descrito no artigo linkado acima. O PR relacionado é https://github.com/normal-computing/outlines/pull/178
    • “Guiding Language Models of Code with Global Context using Monitors”, em https://arxiv.org/abs/2306.10763, mostra como fazer modelos de linguagem gerarem código sem desreferências alucinadas
  • Não entendo bem como isso difere dos projetos a seguir
    https://github.com/1rgs/jsonformer
    https://github.com/newhouseb/clownfish
    https://github.com/mkuchnik/relm
    https://github.com/ggerganov/llama.cpp/pull/1773
    https://github.com/Shopify/torch-grammar
    No geral, existem muitos desses sistemas de condução baseados em logits, e o motivo de não ganharem muita tração é que os modelos de melhor desempenho ficam atrás de APIs REST que não permitem esse tipo de abordagem minuciosa
    Como esses modelos têm desempenho muito melhor, as pessoas geralmente se contentam em fazer a requisição de novo até sair no formato correto. No GPT-4, pela minha experiência, essas falhas também são bem raras

    • Obrigado por apontar clownfish e relm. Pelo que sei, as outras bibliotecas percorrem todo o vocabulário a cada etapa da geração
      Nós percorremos o vocabulário uma vez na inicialização para criar um índice, e depois a geração é tão rápida quanto a geração padrão
  • Explicando de outro jeito: a cada token que o LLM gera, vocês atualizam uma “máscara” de viés de logits para que o próximo token só possa ser um token JSON válido? Muito legal

    • Imagino que ele tenha que continuar gerando até a string inteira ficar válida; não sei o que acontece se entrar em um loop
      Não tenho certeza de como esse método consegue garantir 100% de verdade
    • Isso mesmo. E dá para atualizar a máscara com uma busca em dicionário, sem percorrer todo o vocabulário. Percorrer tudo é lento
    • Como os tokens JSON não correspondem exatamente aos logits, também é necessária alguma forma de beam search ou amostragem por rejeição
      Edit: o artigo explica essa parte com mais cuidado
    • Na verdade, é uma técnica bem antiga, e muitas bibliotecas fazem isso. Não entendo bem o que há de tão extraordinário nesta biblioteca
  • Esse Brandon Willard é o Brandon Willard, breakdancer de Detroit?
    Edit: é sim! https://brandonwillard.github.io/

    • Sim. Foi há bastante tempo, mas foi muito divertido
  • remilouf, como você disse que seu histórico é em “programação probabilística, relacional e simbólica”, imagino que entenda que gerar texto a partir de uma gramática regular, de uma gramática livre de contexto ou, na prática, de uma gramática de qualquer nível não é um problema
    Por exemplo, em uma linguagem relacional como Prolog, isso é muito fácil se for dada apenas a gramática na notação Definite Clause Grammars
    Pelo que vejo, esta abordagem exige que o usuário forneça a gramática. Então fico me perguntando qual é a vantagem de usar um LLM para gerar texto
    Por que não simplesmente executar a gramática como um gerador para produzir o texto desejado? Assim daria para reduzir, desde o início, o grande esforço e custo de treinar um LLM. Se o objetivo é apenas gerar texto estruturado, também me pergunto por que seria necessário um LLM, que é um modelo de linguagem natural

    • Isso não produziria uma saída totalmente aleatória, embora válida? Aqui é preciso uma saída válida relacionada à solicitação
      Mesmo que você queira apenas texto estruturado, o motivo para precisar de um LLM é analisar texto não estruturado escrito por humanos e devolver dados estruturados que uma máquina possa usar
    • O objetivo não é gerar uma string aleatória qualquer que satisfaça a gramática
      Se a solicitação for “quais são os 10 primeiros dígitos de pi?” e a resposta estiver restrita à regex "[0-9]+\.[0-9]+", o objetivo é receber a resposta correta, 3.1415926535, e não apenas uma string aleatória que se encaixe no padrão, como “1.2346789”
    • IanCal já disse tudo. Mas uma abordagem alternativa que usa LLMs com miniKanren pode ser vista em https://arxiv.org/abs/1809.02840
  • Interessante; recentemente nós também criamos uma ferramenta parecida para restringir a saída do llama para que corresponda a uma interface TypeScript[1]
    Acredito firmemente que garantias sobre o formato de saída serão importantes nas próximas décadas, conforme LLMs passem a ser usados em aplicações reais, não de brinquedo
    [1] https://github.com/ggerganov/llama.cpp/discussions/2494