{"id":692,"date":"2026-09-05T01:05:12","date_gmt":"2026-09-05T04:05:12","guid":{"rendered":"https:\/\/yellowkode.com\/blog\/grpo-lfm25-350m-json-estruturado-100-passos\/"},"modified":"2026-09-05T01:05:12","modified_gmt":"2026-09-05T04:05:12","slug":"grpo-lfm25-350m-json-estruturado-100-passos","status":"publish","type":"post","link":"https:\/\/yellowkode.com\/blog\/grpo-lfm25-350m-json-estruturado-100-passos\/","title":{"rendered":"Como treinar um modelo de 350M com GRPO para gerar JSON v\u00e1lido em 100 passos"},"content":{"rendered":"<p>Este artigo explica, com base no mecanismo, um experimento publicado no blog da Hugging Face que mostra como usar aprendizado por refor\u00e7o para deixar um modelo pequeno melhor em produzir JSON e YAML v\u00e1lidos. N\u00e3o \u00e9 uma novidade de arquitetura, \u00e9 uma receita reproduz\u00edvel. E o interessante est\u00e1 justamente a\u00ed: d\u00e1 para replicar em uma GPU gratuita.<\/p>\n<h2>O fato<\/h2>\n<p>Em 3 de setembro de 2026, Leonie Monigatti, Ben Burtenshaw e Sergio Paniego publicaram no blog da Hugging Face uma receita p\u00fablica e barata para fazer um modelo pequeno melhorar em conformidade com sa\u00edda 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\u00e7\u00e3o 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.<\/p>\n<p>O ganho parece modesto em n\u00fameros absolutos, mas o interessante est\u00e1 em onde ele acontece e no custo para chegar l\u00e1. Vale entender o mecanismo antes de julgar o resultado.<\/p>\n<h2>O que \u00e9 IFStruct e por que ele mede algo espec\u00edfico<\/h2>\n<p>IFStruct \u00e9 uma avalia\u00e7\u00e3o de seguimento de instru\u00e7\u00e3o para tarefas de sa\u00edda estruturada. Ele testa se um modelo consegue produzir JSON ou YAML v\u00e1lido que corresponda a um schema alvo, como &#8220;Produza uma receita de muffins de mirtilo em JSON com este schema&#8221;. A pontua\u00e7\u00e3o \u00e9 bin\u00e1ria: uma resposta passa apenas se toda restri\u00e7\u00e3o estrutural pedida for satisfeita. A dificuldade \u00e9 calibrada para ser discriminativa em modelos de baixa a m\u00e9dia capacidade e saturar pr\u00f3ximo de 100% na fronteira do que os melhores modelos conseguem.<\/p>\n<p>Isso importa porque a maioria dos benchmarks tradicionais mistura sa\u00edda estruturada com outras coisas. Aqui, o teste isola uma pergunta \u00fatil: 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? \u00c9 exatamente esse tipo de falha que quebra uma pipeline em produ\u00e7\u00e3o.<\/p>\n<h2>Como o GRPO funciona por baixo do cap\u00f4<\/h2>\n<p>GRPO (Group Relative Policy Optimization) \u00e9 um m\u00e9todo de aprendizado por refor\u00e7o. Foi introduzido no DeepSeekMath, um LLM refinado para racioc\u00ednio matem\u00e1tico avan\u00e7ado, e ganhou aten\u00e7\u00e3o ampla depois do DeepSeek-R1. O ponto central que interessa aqui \u00e9 como ele evita a m\u00e1quina pesada do PPO.<\/p>\n<p>Em PPO cl\u00e1ssico usado em RLHF, voc\u00ea precisa treinar um modelo cr\u00edtico separado que estima o valor esperado de cada resposta. Esse cr\u00edtico costuma ter o mesmo tamanho do modelo que est\u00e1 sendo treinado, o que dobra o custo. GRPO evita esse overhead estimando a fun\u00e7\u00e3o de vantagem usando ranking relativo dentro de um grupo de sa\u00eddas amostradas, em vez de depender de uma rede de valor separada.<\/p>\n<p>O fluxo do GRPO, passo a passo:<\/p>\n<ul>\n<li>Amostragem em grupo: para um dado problema, o modelo gera v\u00e1rias solu\u00e7\u00f5es poss\u00edveis, formando um grupo de sa\u00eddas.<\/li>\n<li>Atribui\u00e7\u00e3o de recompensa: cada solu\u00e7\u00e3o \u00e9 avaliada e recebe uma recompensa baseada na sua corretude ou qualidade. A m\u00e9dia das recompensas do grupo serve como baseline.<\/li>\n<li>A vantagem de cada trajet\u00f3ria \u00e9 calculada normalizando sua recompensa contra o grupo, usando m\u00e9dia e desvio padr\u00e3o. Uma resposta melhor que a m\u00e9dia do grupo tem vantagem positiva, uma pior tem negativa.<\/li>\n<li>Atualiza\u00e7\u00e3o da pol\u00edtica: o modelo atualiza seus par\u00e2metros comparando a recompensa de cada solu\u00e7\u00e3o com o baseline do grupo, refor\u00e7ando solu\u00e7\u00f5es acima da m\u00e9dia e desencorajando as abaixo da m\u00e9dia.<\/li>\n<\/ul>\n<p>Uma analogia r\u00e1pida: imagine um professor corrigindo oito reda\u00e7\u00f5es do mesmo aluno sobre o mesmo tema. Em vez de dar uma nota absoluta comparando com um gabarito ideal (o cr\u00edtico), ele ordena as oito entre si e diz &#8220;fa\u00e7a mais do que estas tr\u00eas de cima, menos do que estas tr\u00eas de baixo&#8221;. O sinal de treino sai da compara\u00e7\u00e3o interna do grupo. Voltando ao mecanismo: a atualiza\u00e7\u00e3o da pol\u00edtica usa uma fun\u00e7\u00e3o objetivo com clipping e uma penalidade de diverg\u00eancia KL para garantir aprendizado est\u00e1vel, o que impede que o modelo se afaste demais do ponto de partida.<\/p>\n<p>Por que isso combina com sa\u00edda estruturada: GRPO pode ser aplicado a qualquer tarefa verific\u00e1vel em que a corretude da resposta possa ser determinada. Em racioc\u00ednio matem\u00e1tico, a corretude pode ser verificada comparando com o gabarito. Validar se um JSON \u00e9 parse\u00e1vel, se tem os campos certos e se casa com um schema \u00e9 exatamente isso: verific\u00e1vel programaticamente, sem juiz humano.<\/p>\n<h2>Como o experimento foi montado<\/h2>\n<h3>Dados de treino<\/h3>\n<p>Os autores usaram o dataset <code>nvidia\/Nemotron-RL-instruction_following-structured_outputs<\/code>, 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\u00e7\u00e3o do Nemotron n\u00e3o bate exatamente com o que o IFStruct cobra, eles fizeram dois ajustes nos prompts: 40% receberam uma instru\u00e7\u00e3o anexada de &#8220;retorne a sa\u00edda dentro de um bloco de c\u00f3digo cercado&#8221;, para que o modelo aprenda a seguir a instru\u00e7\u00e3o de formato em vez de sempre emitir JSON cru. Uma fatia disjunta de 20% foi convertida em tarefas de array de n\u00edvel superior (o schema \u00e9 envolvido em um array com uma contagem de itens exigida), o que treina sa\u00edda de lista pura e conformidade de contagem de itens.<\/p>\n<h3>Modelo e LoRA<\/h3>\n<p>O modelo \u00e9 o LFM2.5-350M da Liquid AI. Como ele tem arquitetura h\u00edbrida (aten\u00e7\u00e3o mais convolu\u00e7\u00e3o), o LoRA precisa mirar m\u00f3dulos espec\u00edficos:<\/p>\n<pre><code>lora_config = LoraConfig(\n r=16,\n lora_alpha=32,\n bias=\"none\",\n task_type=\"CAUSAL_LM\",\n target_modules=[\n \"q_proj\", \"k_proj\", \"v_proj\", \"out_proj\",\n \"in_proj\", \"w1\", \"w2\", \"w3\",\n ],\n)<\/code><\/pre>\n<p>Isso treina cerca de 6M de par\u00e2metros, aproximadamente 1,66% do modelo. \u00c9 o tipo de detalhe que vale copiar direto: se voc\u00ea trocar o modelo base, precisa conferir os nomes de m\u00f3dulo do modelo novo, n\u00e3o \u00e9 s\u00f3 reaproveitar a lista.<\/p>\n<h3>Fun\u00e7\u00f5es de recompensa<\/h3>\n<p>Aqui est\u00e1 o cora\u00e7\u00e3o da receita. Tr\u00eas fun\u00e7\u00f5es, cada uma no intervalo [0, 1]:<\/p>\n<ul>\n<li><code>json_format_reward<\/code>: a sa\u00edda \u00e9 parse\u00e1vel e est\u00e1 na forma pedida? Cr\u00e9dito total (1.0) para a forma pedida (com ou sem bloco de c\u00f3digo cercado), 0.2 para a forma errada mas parse\u00e1vel, 0.0 para sa\u00edda n\u00e3o parse\u00e1vel.<\/li>\n<li><code>field_count_reward<\/code>: o objeto tem o n\u00famero esperado de campos de topo? Igualdade exata vale 1.0, e o score decai linearmente conforme a diferen\u00e7a aumenta.<\/li>\n<li><code>schema_validation_reward<\/code>: a sa\u00edda valida contra o JSON Schema da linha? Conta cada viola\u00e7\u00e3o de restri\u00e7\u00e3o e libera cr\u00e9dito parcial s\u00f3 quando os campos obrigat\u00f3rios est\u00e3o presentes.<\/li>\n<\/ul>\n<p>As tr\u00eas s\u00e3o combinadas por soma ponderada com <code>reward_weights=[1.0, 0.5, 2.0]<\/code>. A valida\u00e7\u00e3o de schema pesa mais porque \u00e9 a sa\u00edda realmente \u00fatil.<\/p>\n<h3>Configura\u00e7\u00e3o de treino<\/h3>\n<pre><code>from trl import GRPOConfig\n\ntraining_args = GRPOConfig(\n output_dir=\".\/outputs\/lfm25-350m-nemotron-schema-grpo\",\n learning_rate=5e-5,\n max_steps=100,\n warmup_steps=10,\n num_generations=8, # completions por grupo\n per_device_train_batch_size=4,\n gradient_accumulation_steps=8,\n steps_per_generation=2,\n max_completion_length=1024, # espa\u00e7o para JSON aninhado\n mask_truncated_completions=False,\n temperature=1.1, # sampling mais quente mant\u00e9m grupo variado\n beta=0.01, # penalidade KL contra o modelo de refer\u00eancia\n reward_weights=[1.0, 0.5, 2.0],\n logging_steps=1,\n save_steps=100,\n)<\/code><\/pre>\n<p>Dois detalhes que valem aten\u00e7\u00e3o: <code>num_generations=8<\/code> \u00e9 o tamanho do grupo do GRPO, o m\u00ednimo pr\u00e1tico para o c\u00e1lculo de vantagem relativa ter algum sinal. E <code>temperature=1.1<\/code> n\u00e3o \u00e9 para produ\u00e7\u00e3o, \u00e9 para o treino: sem varia\u00e7\u00e3o dentro do grupo, todas as amostras t\u00eam a mesma recompensa e o gradiente vira zero.<\/p>\n<h2>O que os resultados dizem na pr\u00e1tica<\/h2>\n<p>Compara\u00e7\u00e3o no mesmo stack de servi\u00e7o (llama.cpp em BF16 GGUF), rodando as 2000 amostras do IFStruct:<\/p>\n<ul>\n<li>Overall: 22,6% para 29,7% (+7,1)<\/li>\n<li>JSON: 18,0% para 31,9% (+13,9)<\/li>\n<li>YAML: 27,2% para 27,5% (+0,3)<\/li>\n<li>Wrapper key: 28,5% para 29,7% (+1,2)<\/li>\n<li>Bare list: 16,6% para 29,7% (+13,1)<\/li>\n<\/ul>\n<p>O padr\u00e3o \u00e9 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\u00e3o estava nos dados de treino, ficou praticamente igual. Isso \u00e9 evid\u00eancia de que o modelo aprendeu o que o sinal de recompensa e a distribui\u00e7\u00e3o dos dados pediram, nem mais nem menos.<\/p>\n<p>Para efeito de compara\u00e7\u00e3o de escala, o pr\u00f3prio artigo cita que isso ainda fica abaixo do Qwen3.5-2B (33,15%), mas \u00e9 um modelo de 350M chegando perto de um seis vezes maior por conta de fine-tuning espec\u00edfico de tarefa.<\/p>\n<h2>Onde isso quebra<\/h2>\n<p>Alguns limites reais, tirados direto do experimento:<\/p>\n<ul>\n<li><strong>YAML n\u00e3o melhorou<\/strong>. O treino foi todo com dados JSON. Se o seu caso de uso for YAML, essa receita n\u00e3o te serve como est\u00e1, precisa ampliar os dados.<\/li>\n<li><strong>Certas categorias continuam ruins<\/strong>. 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\u00edficos ou vocabul\u00e1rios fechados (unidades permitidas, por exemplo) continuam quebrando bastante.<\/li>\n<li><strong>O erro dominante permanece<\/strong>. Antes eram 7228 casos de &#8220;required field missing&#8221;, depois viraram 7331. O modelo ficou melhor em forma e em contagem, n\u00e3o em cobertura de todos os campos obrigat\u00f3rios. Isso sugere que a fun\u00e7\u00e3o de recompensa de schema, do jeito que est\u00e1 ponderada, n\u00e3o pressiona o suficiente contra aus\u00eancia de campos.<\/li>\n<li><strong>Lat\u00eancia n\u00e3o caiu<\/strong>. Ficou em 1518 ms contra 1453 ms do base. Fine-tuning aqui \u00e9 sobre acerto, n\u00e3o sobre velocidade.<\/li>\n<li><strong>S\u00f3 500 amostras e 100 passos<\/strong>. Uma receita curta \u00e9 uma faca de dois gumes: d\u00e1 para reproduzir em Colab gr\u00e1tis, mas o teto do que se aprende \u00e9 baixo. N\u00e3o confunda pipeline m\u00ednima que funciona com resultado final.<\/li>\n<\/ul>\n<h2>O que ainda n\u00e3o d\u00e1 para afirmar<\/h2>\n<p>O post da Hugging Face \u00e9 honesto em um ponto que vale repetir: o pr\u00f3prio blog do IFStruct relata que o LFM2.5-350M, treinado com RL em uma divis\u00e3o 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\u00fablica n\u00e3o tenta replicar aquele n\u00famero, \u00e9 uma pipeline pedag\u00f3gica menor. A partir do que est\u00e1 publicado, n\u00e3o \u00e9 poss\u00edvel dizer quanto do teto real do modelo essa configura\u00e7\u00e3o alcan\u00e7a nem quanto ganho adicional viria de mais dados ou mais passos.<\/p>\n<p>Tamb\u00e9m n\u00e3o h\u00e1 avalia\u00e7\u00e3o publicada do modelo ajustado fora do IFStruct. Se a otimiza\u00e7\u00e3o por recompensa de forma degradou capacidades gerais (racioc\u00ednio, seguimento de outras instru\u00e7\u00f5es), a fonte n\u00e3o esclarece. \u00c9 o risco cl\u00e1ssico de RL com recompensa estreita: voc\u00ea otimiza o que mede.<\/p>\n<h2>Como testar isso voc\u00ea mesmo<\/h2>\n<p>Um caminho pequeno e concreto:<\/p>\n<ol>\n<li>Abra o notebook do post da Hugging Face em um Colab ou Kaggle com GPU. Rode primeiro s\u00f3 o setup e um treino de 10 passos, n\u00e3o 100, para confirmar que tudo carrega.<\/li>\n<li>Antes de treinar, gere umas 20 amostras do modelo base com um prompt do IFStruct e olhe onde ele falha. Isso te d\u00e1 uma refer\u00eancia qualitativa, n\u00e3o s\u00f3 o n\u00famero agregado.<\/li>\n<li>Rode os 100 passos completos, salve o adapter LoRA sem mesclar ainda. Gere novamente as mesmas 20 amostras e compare.<\/li>\n<li>S\u00f3 depois fa\u00e7a o merge e a convers\u00e3o para GGUF. Se voc\u00ea quer servir localmente, o llama.cpp com o comando <code>llama-server<\/code> \u00e9 o caminho mais direto.<\/li>\n<li>Se quiser experimentar de verdade, mude uma coisa por vez: aumente para 300 passos, ou dobre <code>num_generations<\/code>, ou mude o peso da <code>schema_validation_reward<\/code>. Comparar duas rodadas com uma \u00fanica vari\u00e1vel mudada \u00e9 o que ensina.<\/li>\n<\/ol>\n<p>O ponto que fica desta receita \u00e9 menos sobre esses 7 pontos percentuais e mais sobre o m\u00e9todo: quando a tarefa \u00e9 verific\u00e1vel por c\u00f3digo, voc\u00ea n\u00e3o precisa de dataset rotulado por humano nem de cr\u00edtico separado. Uma fun\u00e7\u00e3o Python que devolve um n\u00famero entre 0 e 1 j\u00e1 \u00e9 sinal de treino suficiente. Isso muda o c\u00e1lculo de custo para melhorar modelos pequenos em tarefas espec\u00edficas.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Passo a passo t\u00e9cnico para fine-tuning do LFM2.5-350M com GRPO e TRL, melhorando sa\u00edda estruturada de 22,6% para 29,7% no benchmark IFStruct.<\/p>\n","protected":false},"author":2,"featured_media":691,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-692","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/posts\/692","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/comments?post=692"}],"version-history":[{"count":0,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/posts\/692\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/media\/691"}],"wp:attachment":[{"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/media?parent=692"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/categories?post=692"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/yellowkode.com\/blog\/wp-json\/wp\/v2\/tags?post=692"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}