Home / Uncategorized / Como treinar um modelo de 350M com GRPO para gerar JSON válido em 100 passos

Como treinar um modelo de 350M com GRPO para gerar JSON válido em 100 passos

Este artigo explica, com base no mecanismo, um experimento publicado no blog da Hugging Face que mostra como usar aprendizado por reforço para deixar um modelo pequeno melhor em produzir JSON e YAML válidos. Não é uma novidade de arquitetura, é uma receita reproduzível. E o interessante está justamente aí: dá para replicar em uma GPU gratuita.

O fato

Em 3 de setembro de 2026, Leonie Monigatti, Ben Burtenshaw e Sergio Paniego publicaram no blog da Hugging Face uma receita pública e barata para fazer um modelo pequeno melhorar em conformidade com saída estruturada. Eles fizeram fine-tuning do LFM2.5-350M com Group Relative Policy Optimization (GRPO) usando a biblioteca TRL e avaliaram no benchmark IFStruct. A execução completa usa cerca de 500 amostras e 100 passos de treino, pequena o bastante para uma GPU gratuita de Colab ou Kaggle. Os resultados relatados mostram que mesmo um fine-tuning leve move o desempenho de 22,6% para 29,7% no benchmark IFStruct.

O ganho parece modesto em números absolutos, mas o interessante está em onde ele acontece e no custo para chegar lá. Vale entender o mecanismo antes de julgar o resultado.

O que é IFStruct e por que ele mede algo específico

IFStruct é uma avaliação de seguimento de instrução para tarefas de saída estruturada. Ele testa se um modelo consegue produzir JSON ou YAML válido que corresponda a um schema alvo, como “Produza uma receita de muffins de mirtilo em JSON com este schema”. A pontuação é binária: uma resposta passa apenas se toda restrição estrutural pedida for satisfeita. A dificuldade é calibrada para ser discriminativa em modelos de baixa a média capacidade e saturar próximo de 100% na fronteira do que os melhores modelos conseguem.

Isso importa porque a maioria dos benchmarks tradicionais mistura saída estruturada com outras coisas. Aqui, o teste isola uma pergunta útil: o modelo consegue devolver a forma pedida, com os campos certos, no formato pedido (JSON ou YAML), com contagem de itens correta, sem inventar chave extra? É exatamente esse tipo de falha que quebra uma pipeline em produção.

Como o GRPO funciona por baixo do capô

GRPO (Group Relative Policy Optimization) é um método de aprendizado por reforço. Foi introduzido no DeepSeekMath, um LLM refinado para raciocínio matemático avançado, e ganhou atenção ampla depois do DeepSeek-R1. O ponto central que interessa aqui é como ele evita a máquina pesada do PPO.

Em PPO clássico usado em RLHF, você precisa treinar um modelo crítico separado que estima o valor esperado de cada resposta. Esse crítico costuma ter o mesmo tamanho do modelo que está sendo treinado, o que dobra o custo. GRPO evita esse overhead estimando a função de vantagem usando ranking relativo dentro de um grupo de saídas amostradas, em vez de depender de uma rede de valor separada.

O fluxo do GRPO, passo a passo:

  • Amostragem em grupo: para um dado problema, o modelo gera várias soluções possíveis, formando um grupo de saídas.
  • Atribuição de recompensa: cada solução é avaliada e recebe uma recompensa baseada na sua corretude ou qualidade. A média das recompensas do grupo serve como baseline.
  • A vantagem de cada trajetória é calculada normalizando sua recompensa contra o grupo, usando média e desvio padrão. Uma resposta melhor que a média do grupo tem vantagem positiva, uma pior tem negativa.
  • Atualização da política: o modelo atualiza seus parâmetros comparando a recompensa de cada solução com o baseline do grupo, reforçando soluções acima da média e desencorajando as abaixo da média.

Uma analogia rápida: imagine um professor corrigindo oito redações do mesmo aluno sobre o mesmo tema. Em vez de dar uma nota absoluta comparando com um gabarito ideal (o crítico), ele ordena as oito entre si e diz “faça mais do que estas três de cima, menos do que estas três de baixo”. O sinal de treino sai da comparação interna do grupo. Voltando ao mecanismo: a atualização da política usa uma função objetivo com clipping e uma penalidade de divergência KL para garantir aprendizado estável, o que impede que o modelo se afaste demais do ponto de partida.

Por que isso combina com saída estruturada: GRPO pode ser aplicado a qualquer tarefa verificável em que a corretude da resposta possa ser determinada. Em raciocínio matemático, a corretude pode ser verificada comparando com o gabarito. Validar se um JSON é parseável, se tem os campos certos e se casa com um schema é exatamente isso: verificável programaticamente, sem juiz humano.

Como o experimento foi montado

Dados de treino

Os autores usaram o dataset nvidia/Nemotron-RL-instruction_following-structured_outputs, que pareia cada prompt com um JSON Schema alvo e uma contagem esperada de campos. Apenas 500 amostras foram usadas para o treino. Como a distribuição do Nemotron não bate exatamente com o que o IFStruct cobra, eles fizeram dois ajustes nos prompts: 40% receberam uma instrução anexada de “retorne a saída dentro de um bloco de código cercado”, para que o modelo aprenda a seguir a instrução de formato em vez de sempre emitir JSON cru. Uma fatia disjunta de 20% foi convertida em tarefas de array de nível superior (o schema é envolvido em um array com uma contagem de itens exigida), o que treina saída de lista pura e conformidade de contagem de itens.

Modelo e LoRA

O modelo é o LFM2.5-350M da Liquid AI. Como ele tem arquitetura híbrida (atenção mais convolução), o LoRA precisa mirar módulos específicos:

lora_config = LoraConfig(
 r=16,
 lora_alpha=32,
 bias="none",
 task_type="CAUSAL_LM",
 target_modules=[
 "q_proj", "k_proj", "v_proj", "out_proj",
 "in_proj", "w1", "w2", "w3",
 ],
)

Isso treina cerca de 6M de parâmetros, aproximadamente 1,66% do modelo. É o tipo de detalhe que vale copiar direto: se você trocar o modelo base, precisa conferir os nomes de módulo do modelo novo, não é só reaproveitar a lista.

Funções de recompensa

Aqui está o coração da receita. Três funções, cada uma no intervalo [0, 1]:

  • json_format_reward: a saída é parseável e está na forma pedida? Crédito total (1.0) para a forma pedida (com ou sem bloco de código cercado), 0.2 para a forma errada mas parseável, 0.0 para saída não parseável.
  • field_count_reward: o objeto tem o número esperado de campos de topo? Igualdade exata vale 1.0, e o score decai linearmente conforme a diferença aumenta.
  • schema_validation_reward: a saída valida contra o JSON Schema da linha? Conta cada violação de restrição e libera crédito parcial só quando os campos obrigatórios estão presentes.

As três são combinadas por soma ponderada com reward_weights=[1.0, 0.5, 2.0]. A validação de schema pesa mais porque é a saída realmente útil.

Configuração de treino

from trl import GRPOConfig

training_args = GRPOConfig(
 output_dir="./outputs/lfm25-350m-nemotron-schema-grpo",
 learning_rate=5e-5,
 max_steps=100,
 warmup_steps=10,
 num_generations=8, # completions por grupo
 per_device_train_batch_size=4,
 gradient_accumulation_steps=8,
 steps_per_generation=2,
 max_completion_length=1024, # espaço para JSON aninhado
 mask_truncated_completions=False,
 temperature=1.1, # sampling mais quente mantém grupo variado
 beta=0.01, # penalidade KL contra o modelo de referência
 reward_weights=[1.0, 0.5, 2.0],
 logging_steps=1,
 save_steps=100,
)

Dois detalhes que valem atenção: num_generations=8 é o tamanho do grupo do GRPO, o mínimo prático para o cálculo de vantagem relativa ter algum sinal. E temperature=1.1 não é para produção, é para o treino: sem variação dentro do grupo, todas as amostras têm a mesma recompensa e o gradiente vira zero.

O que os resultados dizem na prática

Comparação no mesmo stack de serviço (llama.cpp em BF16 GGUF), rodando as 2000 amostras do IFStruct:

  • Overall: 22,6% para 29,7% (+7,1)
  • JSON: 18,0% para 31,9% (+13,9)
  • YAML: 27,2% para 27,5% (+0,3)
  • Wrapper key: 28,5% para 29,7% (+1,2)
  • Bare list: 16,6% para 29,7% (+13,1)

O padrão é claro: onde o treino mirou, o ganho apareceu. JSON com bloco cercado subiu quase 14 pontos. Lista pura no topo (o ajuste de 20% dos prompts) subiu 13 pontos. YAML, que não estava nos dados de treino, ficou praticamente igual. Isso é evidência de que o modelo aprendeu o que o sinal de recompensa e a distribuição dos dados pediram, nem mais nem menos.

Para efeito de comparação de escala, o próprio artigo cita que isso ainda fica abaixo do Qwen3.5-2B (33,15%), mas é um modelo de 350M chegando perto de um seis vezes maior por conta de fine-tuning específico de tarefa.

Onde isso quebra

Alguns limites reais, tirados direto do experimento:

  • YAML não melhorou. O treino foi todo com dados JSON. Se o seu caso de uso for YAML, essa receita não te serve como está, precisa ampliar os dados.
  • Certas categorias continuam ruins. Camera review foi de 7,2% para 6,0%, GPU review de 6,4% para 7,4%, recipe de 4,3% para 10,0%. Categorias com schemas muito específicos ou vocabulários fechados (unidades permitidas, por exemplo) continuam quebrando bastante.
  • O erro dominante permanece. Antes eram 7228 casos de “required field missing”, depois viraram 7331. O modelo ficou melhor em forma e em contagem, não em cobertura de todos os campos obrigatórios. Isso sugere que a função de recompensa de schema, do jeito que está ponderada, não pressiona o suficiente contra ausência de campos.
  • Latência não caiu. Ficou em 1518 ms contra 1453 ms do base. Fine-tuning aqui é sobre acerto, não sobre velocidade.
  • Só 500 amostras e 100 passos. Uma receita curta é uma faca de dois gumes: dá para reproduzir em Colab grátis, mas o teto do que se aprende é baixo. Não confunda pipeline mínima que funciona com resultado final.

O que ainda não dá para afirmar

O post da Hugging Face é honesto em um ponto que vale repetir: o próprio blog do IFStruct relata que o LFM2.5-350M, treinado com RL em uma divisão de treino dedicada e reservada, pode superar o desempenho de modelos bem maiores como Qwen3.5-4B e granite-4.0-h-tiny. A receita pública não tenta replicar aquele número, é uma pipeline pedagógica menor. A partir do que está publicado, não é possível dizer quanto do teto real do modelo essa configuração alcança nem quanto ganho adicional viria de mais dados ou mais passos.

Também não há avaliação publicada do modelo ajustado fora do IFStruct. Se a otimização por recompensa de forma degradou capacidades gerais (raciocínio, seguimento de outras instruções), a fonte não esclarece. É o risco clássico de RL com recompensa estreita: você otimiza o que mede.

Como testar isso você mesmo

Um caminho pequeno e concreto:

  1. Abra o notebook do post da Hugging Face em um Colab ou Kaggle com GPU. Rode primeiro só o setup e um treino de 10 passos, não 100, para confirmar que tudo carrega.
  2. Antes de treinar, gere umas 20 amostras do modelo base com um prompt do IFStruct e olhe onde ele falha. Isso te dá uma referência qualitativa, não só o número agregado.
  3. Rode os 100 passos completos, salve o adapter LoRA sem mesclar ainda. Gere novamente as mesmas 20 amostras e compare.
  4. Só depois faça o merge e a conversão para GGUF. Se você quer servir localmente, o llama.cpp com o comando llama-server é o caminho mais direto.
  5. Se quiser experimentar de verdade, mude uma coisa por vez: aumente para 300 passos, ou dobre num_generations, ou mude o peso da schema_validation_reward. Comparar duas rodadas com uma única variável mudada é o que ensina.

O ponto que fica desta receita é menos sobre esses 7 pontos percentuais e mais sobre o método: quando a tarefa é verificável por código, você não precisa de dataset rotulado por humano nem de crítico separado. Uma função Python que devolve um número entre 0 e 1 já é sinal de treino suficiente. Isso muda o cálculo de custo para melhorar modelos pequenos em tarefas específicas.

Deixe um Comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *