<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Alysson Guimarães on Medium]]></title>
        <description><![CDATA[Stories by Alysson Guimarães on Medium]]></description>
        <link>https://medium.com/@k3ybladewielder?source=rss-ab4f983cc1ab------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*Q7KdQ-u9MKLVX217V7tTrg.webp</url>
            <title>Stories by Alysson Guimarães on Medium</title>
            <link>https://medium.com/@k3ybladewielder?source=rss-ab4f983cc1ab------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 06:11:31 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@k3ybladewielder/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Design de Uma Arquitetura Multi-Agente Guiada por Sistemas de Tipos Avançados]]></title>
            <link>https://k3ybladewielder.medium.com/design-de-uma-arquitetura-multi-agente-guiada-por-sistemas-de-tipos-avan%C3%A7ados-4a83f7a66a62?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a83f7a66a62</guid>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[formal-verification]]></category>
            <category><![CDATA[formal-methods]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 23:11:01 GMT</pubDate>
            <atom:updated>2026-10-30T23:36:15.073Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>TL;DR:</strong> Pedir para um único LLM gerar código e testar a própria lógica é a receita para alucinações silenciosas. Ao dividir o ciclo de desenvolvimento em um enxame de agentes adversariais (<em>Multi-Agent Swarm</em>) entre <strong>Especificador, Implementador, Falsificador e Verificador </strong>e usar sistemas de tipos avançados em <strong>Python</strong> e <strong>TypeScript</strong> como motores de prova, transformamos a geração probabilística de código em engenharia determinística.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XIuUBWhhkXG7Zmms8CIGCw.jpeg" /></figure><h3>O erro do Agente Monolítico e o “Vibe Coding”</h3><p>A primeira onda de desenvolvimento assistido por IA popularizou o que muitos chamam de <em>Vibe Coding</em>: o desenvolvedor descreve um requisito em linguagem natural, o LLM gera um bloco de código e, se parecer funcionar em dois ou três testes manuais, o código vai para produção.</p><p>O problema estrutural dessa abordagem está na natureza autorregressiva dos Grandes Modelos de Linguagem (LLMs). Quando um único agente recebe a missão de <strong>interpretar o requisito, modelar os dados, implementar o algoritmo e criar os testes unitários</strong>, ele sofre do mesmo viés cognitivo de um programador humano trabalhando sozinho — porém amplificado pela probabilidade estatística. Se o modelo alucinar uma premissa falsa na implementação, ele replicará exatamente a mesma premissa falsa ao gerar o teste unitário. O teste passa, o <em>build</em> fica verde e o bug vai para produção.</p><p>Nos artigos anteriores, vimos que <a href="https://k3ybladewielder.medium.com/tipos-s%C3%A3o-teoremas-programas-s%C3%A3o-provas-aa24dc7740a6"><strong>Tipos são Teoremas e Programas são Provas</strong></a> (Isomorfismo de Curry-Howard) e que <a href="https://k3ybladewielder.medium.com/como-refinement-types-e-design-by-contract-trazem-a-verifica%C3%A7%C3%A3o-formal-para-o-mundo-real-735b57eae172"><strong>Refinement Types</strong> aliados a <strong>Property-Based Testing (PBT)</strong> fecham o cerco contra estados inválidos</a>. Mas como escalar esse rigor usando IA generativa no dia a dia sem precisar de assistentes de prova acadêmicos?</p><ol><li><strong>Arquitetura de Enxames Adversariais (<em>Multi-Agent Swarms</em>):</strong> Separar as responsabilidades em agentes independentes com objetivos conflitantes (quem especifica não implementa; quem tenta quebrar não é quem escreveu).</li><li><strong>O Sistema de Tipos como Motor de Prova:</strong> Em vez de exigir provadores de teoremas externos complexos, utilizamos o próprio verificador estático de tipos (<em>Type Checker</em>) e validação de contratos em linguagens de produção (<strong>TypeScript</strong> e <strong>Python</strong>) como o oráculo determinístico que aprova ou rejeita o trabalho dos agentes.</li></ol><h3>O Sistema de Tipos como Motor de Prova Prático</h3><p>Quando falamos em “provar um teorema” no código, não precisamos necessariamente invocar táticas manuais em Coq ou Lean. Se modelarmos nossas estruturas de dados de forma que <strong>estados inválidos sejam irrepresentáveis</strong>, o próprio <em>Type Checker</em> (tsc --noEmit no TypeScript ou mypy --strict / pyright + CrossHair no Python) atua como um motor de prova construtiva.</p><p>Sob a ótica da programação orientada a dados (<em>Data-Oriented Programming</em>) e do <em>Type-Driven Development</em>:</p><ul><li>Definir um tipo imutável e restrito é <strong>enunciar o teorema</strong>.</li><li>Escrever uma função pura que transforma o Tipo <em>A</em> no Tipo <em>B</em> sem usar escapatórias (como any, as unknown ou # type: ignore) é <strong>fornecer a prova construtiva</strong>.</li><li>Quando o verificador de tipos analisa a árvore sintática (AST) e confirma que todas as ramificações, uniões discriminadas (<em>Discriminated Unions</em>) e restrições de tipos batem, ele está emitindo o seu <strong>Q.E.D.</strong></li></ul><h3>A Arquitetura do Enxame: Os 4 Agentes Especializados</h3><p>Para eliminar a complacência de um agente único, estruturamos o pipeline de geração de código como uma máquina de estados finitos onde quatro agentes operam em malha fechada, coordenados por um engenheiro humano:</p><pre>[Requisito do Engenheiro Humano]<br>                                  │<br>                                  ▼<br>               ┌──────────────────────────────────────┐<br>               │ 1. Agente Especificador (Arquiteto)  │<br>               │ (Define Contratos, Tipos &amp; Teoremas) │<br>               └──────────────────┬───────────────────┘<br>                                  │ (Assinaturas Imutáveis)<br>                                  ▼<br>               ┌──────────────────────────────────────┐<br>        ┌─────►│ 2. Agente Implementador (Construtor) │◄─────┐<br>        │      │   (Escreve Funções Puras p/ os Tipos)│      │<br>        │      └──────────────────┬───────────────────┘      │<br>        │                         │                          │<br>(Contraexemplo            [Código Candidato]          (Erro de Tipo /<br>  Reduzido)                       │                    Exaustividade)<br>        │                         ▼                          │<br>┌───────┴──────────────┐  ┌──────────────────────────────────┴───┐<br>│ 3. Agente            │  │ 4. Agente Verificador (Juiz de Tipos)│<br>│    Falsificador (PBT)├─►│    (Type Checker Estrito &amp; AST Audit)│<br>└──────────────────────┘  └──────────────────┬───────────────────┘<br>                                             │ (Tipos Habitados + 0 Falhas)<br>                                             ▼<br>                                  [Código Verificado]</pre><h4>1. O Agente Especificador (O Arquiteto de Tipos)</h4><ul><li><strong>Missão:</strong> Traduzir requisitos de negócio ambíguos em estruturas de dados imutáveis, contratos estritos e assinaturas de funções.</li><li><strong>Restrição Operacional:</strong> Ele é <strong>proibido</strong> de escrever a lógica de implementação (o corpo das funções deve conter apenas ... ou throw new Error(&quot;Not implemented&quot;)). Seu único entregável são os tipos (os teoremas que precisam ser habitados).</li></ul><h4>2. O Agente Implementador (O Construtor Funcional)</h4><ul><li><strong>Missão:</strong> Escrever a transformação pura de dados que satisfaz as assinaturas criadas pelo Especificador.</li><li><strong>Restrição Operacional:</strong> Ele tem acesso <strong>somente leitura</strong> às definições de tipos do Especificador. Ele não pode alterar a assinatura de uma função para facilitar o próprio trabalho, nem usar recursos de evasão de tipos (any, cast, ignore).</li></ul><h4>3. O Agente Falsificador (O Caçador de Invariantes via PBT)</h4><ul><li><strong>Missão:</strong> Assumir que o Agente Implementador errou. Ele analisa os tipos do Especificador e gera suítes de <strong>Property-Based Testing</strong> (usando Hypothesis no Python ou fast-check no TypeScript) para bombardear a implementação com milhares de casos extremos.</li><li><strong>Poder no Loop:</strong> Se encontrar uma falha lógica que passou pelo sistema de tipos, ele devolve o <em>shrunk counterexample</em> (contraexemplo mínimo) diretamente para o Implementador refazer o código.</li></ul><h4>4. O Agente Verificador (O Motor de Prova via Type System)</h4><ul><li><strong>Missão:</strong> Atuar como o porteiro formal determinístico. Ele compila a relação entre os tipos e a implementação usando o verificador estático em modo ultra-estrito, audita a AST para garantir que não houve burla nas regras de tipagem e executa verificadores simbólicos de contratos.</li><li><strong>Poder no Loop:</strong> Enquanto o compilador apontar erro de subtipagem ou falta de exaustividade em um <em>pattern matching</em>, o loop bloqueia a aprovação e força o Implementador a corrigir a prova (o código).</li></ul><h3>Estudo de Caso 1: TypeScript (Branded Types e Provas de Exaustividade)</h3><p>Imagine um sistema financeiro que processa estornos de pedidos. As regras de negócio exigem que:</p><ul><li>O valor do estorno seja estritamente positivo e nunca exceda o valor original pago.</li><li>Apenas pedidos no estado Pago possam sofrer estorno (pedidos Pendentes ou já Cancelados jamais podem passar pela função de estorno em tempo de compilação).</li></ul><p>Veja como o enxame resolve isso usando <strong>o sistema de tipos do TypeScript como motor de prova</strong>:</p><pre>// ============================================================================<br>// ETAPA 1: AGENTE ESPECIFICADOR (Define os Tipos/Teoremas - Imutavel)<br>// ============================================================================<br><br>// 1. Criamos Branded Types (Tipos Nominais Refinados) usando unique symbol.<br>// Isso impede que um &#39;number&#39; comum habite o tipo sem passar pela prova (Smart Constructor).<br>declare const CentavosPositivosBrand: unique symbol;<br>export type CentavosPositivos = number &amp; { readonly [CentavosPositivosBrand]: &quot;CentavosPositivos&quot; };<br>declare const ProvaEstornoValidoBrand: unique symbol;<br>export type EstornoValido = {<br>  readonly valorEstorno: CentavosPositivos;<br>  readonly saldoRestante: number;<br>  readonly [ProvaEstornoValidoBrand]: &quot;EstornoMenorOuIgualAoPago&quot;;<br>};<br>// 2. Modelamos a Maquina de Estados via Discriminated Unions.<br>// O estado &#39;PedidoPago&#39; e um tipo distinto de &#39;PedidoPendente&#39;.<br>export type PedidoPendente = { readonly status: &quot;PENDENTE&quot;; readonly id: string };<br>export type PedidoPago = { readonly status: &quot;PAGO&quot;; readonly id: string; readonly totalPago: CentavosPositivos };<br>export type PedidoEstornado = {<br>  readonly status: &quot;ESTORNADO&quot;;<br>  readonly id: string;<br>  readonly comprovante: EstornoValido;<br>};<br>export type Pedido = PedidoPendente | PedidoPago | PedidoEstornado;<br>// 3. O Teorema (Assinatura): Recebe APENAS &#39;PedidoPago&#39; (nunca &#39;Pedido&#39; generico)<br>// e exige uma prova de que o estorno e valido para emitir um &#39;PedidoEstornado&#39;.<br>export type ProcessarEstornoFn = (<br>  pedido: PedidoPago,<br>  solicitacaoCentavos: CentavosPositivos<br>) =&gt; PedidoEstornado | Error;<br><br>// ============================================================================<br>// ETAPA 2: AGENTE IMPLEMENTADOR (Habita os Tipos / Escreve a Prova Construtiva)<br>// ============================================================================<br>// 4. Construtor de Prova: o unico lugar autorizado a instanciar &#39;EstornoValido&#39;<br>// apos verificar a desigualdade matematica.<br>function construirProvaEstorno(<br>  pago: CentavosPositivos,<br>  solicitado: CentavosPositivos<br>): EstornoValido | null {<br>  if (solicitado &lt;= pago) {<br>    return {<br>      valorEstorno: solicitado,<br>      saldoRestante: pago - solicitado,<br>      [ProvaEstornoValidoBrand]: &quot;EstornoMenorOuIgualAoPago&quot;,<br>    };<br>  }<br>  return null;<br>}<br>// 5. Implementacao pura respeitando estritamente o contrato do Especificador.<br>export const processarEstorno: ProcessarEstornoFn = (pedido, solicitacaoCentavos) =&gt; {<br>  const prova = construirProvaEstorno(pedido.totalPago, solicitacaoCentavos);<br>  if (!prova) {<br>    return new Error(&quot;Violacao de invariante: estorno maior que o total pago.&quot;);<br>  }<br>  return {<br>    status: &quot;ESTORNADO&quot;,<br>    id: pedido.id,<br>    comprovante: prova,<br>  };<br>};<br><br>// ============================================================================<br>// ETAPA 3 e 4: AGENTE FALSIFICADOR (fast-check) + VERIFICADOR (tsc --strict)<br>// ============================================================================<br>import fc from &quot;fast-check&quot;;<br>// 6. O Agente Verificador garante em tempo de compilacao que tentar passar<br>// um &#39;PedidoPendente&#39; para &#39;processarEstorno&#39; sequer compila (Erro TS2345).<br>// 7. O Agente Falsificador gera milhares de pares (pago, solicitado) para<br>// tentar quebrar a invariante de saldoRestante &gt;= 0:<br>fc.assert(<br>  fc.property(<br>    fc.integer({ min: 1, max: 10_000_000 }),<br>    fc.integer({ min: 1, max: 10_000_000 }),<br>    (pagoBruto, solicitadoBruto) =&gt; {<br>      const pago = pagoBruto as CentavosPositivos;<br>      const solicitado = solicitadoBruto as CentavosPositivos;<br>      const pedido: PedidoPago = { status: &quot;PAGO&quot;, id: &quot;ped_1&quot;, totalPago: pago };<br>      const resultado = processarEstorno(pedido, solicitado);<br>      if (solicitado &lt;= pago) {<br>        return !(resultado instanceof Error) &amp;&amp; resultado.comprovante.saldoRestante &gt;= 0;<br>      } else {<br>        return resultado instanceof Error;<br>      }<br>    }<br>  )<br>);</pre><p>Em que:</p><ul><li><strong>Etapa 1 (Branded Types):</strong> O Agente Especificador cria tipos nominais usando unique symbol. Nenhum número comum pode ser atribuído a CentavosPositivos acidentalmente.</li><li><strong>Etapa 2 (União Discriminada):</strong> O estado do pedido é separado em tipos independentes (PedidoPendente, PedidoPago, PedidoEstornado).</li><li><strong>Etapa 3 (Assinatura como Teorema):</strong> A assinatura ProcessarEstornoFn restringe o domínio de entrada exclusivamente a PedidoPago. Isso prova em tempo de compilação que pedidos não pagos jamais serão estornados.</li><li><strong>Etapa 4 (Construtor de Prova):</strong> O Agente Implementador cria uma função isolada que verifica a condição matemática solicitado &lt;= pago antes de selar o objeto com a marca ProvaEstornoValidoBrand.</li><li><strong>Etapa 5 (Transformação Pura):</strong> A função principal consome a prova e constrói o estado final imutável PedidoEstornado.</li><li><strong>Etapa 6 e 7 (Verificação e Falsificação):</strong> O compilador TypeScript (tsc) valida a prova estática de tipos, enquanto o fast-check bombardeia a relação numérica para garantir que nunca haverá saldo residual negativo.</li></ul><h3>Estudo de Caso 2: Python (Tipos Imutáveis, Contratos e Execução Simbólica)</h3><p>No Python, combinamos uma arquitetura orientada a dados imutáveis (@dataclass(frozen=True) e NewType) com contratos formais (icontract) verificados tanto pelo mypy --strict quanto pelo motor de execução simbólica <strong>CrossHair</strong> (que traduz o código Python para o SMT Solver Z3 nos bastidores) e pelo <strong>Hypothesis</strong>.</p><p>Imagine um algoritmo de alocação de estoque que reserva unidades de um lote disponível sem jamais permitir estoque negativo ou perda de unidades na soma total (lei da conservação de massa/estoque):</p><pre>from typing import NewType, Union<br>from dataclasses import dataclass<br>import icontract<br>from hypothesis import given, strategies as st<br><br># ============================================================================<br># ETAPA 1: AGENTE ESPECIFICADOR (Modelagem de Dados Imutavel e Contratos)<br># ============================================================================<br># 1. Definimos tipos distintos no sistema de tipos estatico (mypy/pyright)<br>QuantidadePositiva = NewType(&quot;QuantidadePositiva&quot;, int)<br>@dataclass(frozen=True)<br>class LoteEstoque:<br>    sku: str<br>    disponivel: int<br>    reservado: int<br>@dataclass(frozen=True)<br>classErroAlocacao:<br>    motivo: str<br><br># 2. O Especificador define o teorema via pre e pos-condicoes formais.<br># A pos-condicao exige a prova da Lei de Conservacao de Estoque:<br># (disponivel_antes + reservado_antes) == (disponivel_depois + reservado_depois)<br>@icontract.require(lambda lote: lote.disponivel &gt;= 0 and lote.reservado &gt;= 0)<br>@icontract.require(lambda qtd_solicitada: qtd_solicitada &gt; 0)<br>@icontract.ensure(<br>    lambda lote, qtd_solicitada, result: (<br>        isinstance(result, (LoteEstoque, ErroAlocacao))<br>        and (<br>            isinstance(result, ErroAlocacao)<br>            or (<br>                result.disponivel &gt;= 0<br>                and result.reservado &gt;= lote.reservado<br>                and (result.disponivel + result.reservado) == (lote.disponivel + lote.reservado)<br>            )<br>        )<br>    )<br>)<br>def reservar_estoque(<br>    lote: LoteEstoque, qtd_solicitada: QuantidadePositiva<br>) -&gt; Union[LoteEstoque, ErroAlocacao]:<br>    # ========================================================================<br>    # ETAPA 2: AGENTE IMPLEMENTADOR (Preenche o corpo da funcao pura)<br>    # ========================================================================<br>    # 3. Transformacao pura de dados sem mutacao de estado in-place<br>    if qtd_solicitada &gt; lote.disponivel:<br>        return ErroAlocacao(motivo=&quot;Estoque insuficiente para reserva&quot;)<br>    return LoteEstoque(<br>        sku=lote.sku,<br>        disponivel=lote.disponivel - qtd_solicitada,<br>        reservado=lote.reservado + qtd_solicitada,<br>    )<br><br># ============================================================================<br># ETAPA 3 e 4: AGENTE FALSIFICADOR (Hypothesis) + VERIFICADOR (mypy + CrossHair)<br># ============================================================================<br># 4. O Agente Falsificador tenta violar o contrato gerando lotes e pedidos extremos<br>@given(<br>    disponivel=st.integers(min_value=0, max_value=10**6),<br>    reservado=st.integers(min_value=0, max_value=10**6),<br>    solicitado=st.integers(min_value=1, max_value=10**6),<br>)<br>def test_propriedade_conservacao_estoque(disponivel: int, reservado: int, solicitado: int) -&gt; None:<br>    lote_inicial = LoteEstoque(sku=&quot;PROD-01&quot;, disponivel=disponivel, reservado=reservado)<br>    qtd = QuantidadePositiva(solicitado)<br>    # 5. A propria execucao aciona os contratos do icontract como oraculo<br>    resultado = reservar_estoque(lote_inicial, qtd)<br>    if isinstance(resultado, LoteEstoque):<br>        assert resultado.disponivel + resultado.reservado == disponivel + reservado</pre><p>Em que:</p><ul><li><strong>Etapa 1 (Imutabilidade e Tipos):</strong> O Agente Especificador cria QuantidadePositiva via NewType e modela LoteEstoque com @dataclass(frozen=True), impedindo mutação de estado acidental durante a execução.</li><li><strong>Etapa 2 (Teorema de Conservação):</strong> Usando @icontract.require e @icontract.ensure, o Especificador crava a invariante fundamental: a soma entre o estoque disponível e o estoque reservado antes da função deve ser estritamente igual à soma após a função, e nenhum saldo pode ficar negativo.</li><li><strong>Etapa 3 (Implementação Funcional):</strong> O Agente Implementador escreve a lógica pura retornando uma nova instância de LoteEstoque ou ErroAlocacao (tratando erros como valores, e não como exceções cegas).</li><li><strong>Etapa 4 e 5 (Auditoria Automática):</strong> O Agente Falsificador roda o Hypothesis contra a função, enquanto o Agente Verificador roda mypy --strict (para garantir que int puro não seja passado onde se exige QuantidadePositiva) e crosshair check (para provar simbolicamente via Z3 que nenhuma combinação de inteiros consegue violar o @icontract.ensure).</li></ul><h3>Como Impedir que os Agentes “Trapaceiem”</h3><p>Um comportamento cumum e que pode esconder problemas quando colocamos agentes de IA em um loop guiado por verificadores de tipos é o <strong>Reward Hacking</strong>. Se o Agente Implementador não consegue fazer o código compilar após duas tentativas porque o sistema de tipos é rigoroso demais, a tendência estatística do LLM é tentar “calar” o compilador ou modificar o código.</p><p>Ele tentará injetar um as any ou @ts-ignore no TypeScript, ou um # type: ignore e cast(Any, ...) no Python. No pior dos casos, se ele tiver acesso ao arquivo de testes, tentará apagar a linha do assert que está falhando</p><p>Para que a arquitetura de enxame funcione com rigor matemático, o <strong>Agente Verificador</strong> não deve ser um LLM opinando sobre o código, mas sim um <strong>script determinístico de auditoria de AST (Árvore Sintática Abstrata)</strong> combinado com permissões de sistema de arquivos:</p><ul><li><strong>Isolamento de Escrita (File-System Sandboxing):</strong> O Agente Implementador só tem permissão de escrita em src/implementation.py (ou .ts). Os arquivos src/types_and_contracts.py (do Especificador) e tests/properties_test.py (do Falsificador) são montados como <strong>Read-Only</strong> para o Implementador.</li><li><strong>Bloqueio de Evasão de Tipos na AST:</strong> Antes mesmo de rodar o compilador, um parser leve (usando o módulo nativo ast do Python ou ts-morph / eslint no TypeScript) varre o código gerado pelo Implementador e rejeita instantaneamente qualquer commit que contenha:</li><li>Uso da palavra-chave Any ou cast;</li><li>Comentários contendo type: ignore, @ts-ignore, @ts-expect-error ou eslint-disable;</li><li>Uso de blocos try / except Exception: pass que engolem violações de contrato silenciosamente;</li><li>Mutação de argumentos de entrada.</li></ul><h3>Quando Usar e Quando Não Usar um Enxame Verificado</h3><p>Orquestrar quatro agentes em loop com verificadores estáticos e testes de propriedade consome mais tokens e tempo de processamento do que um simples <em>autocomplete</em>. A aplicação deve ser estratégica:</p><p><strong>Quando usar esta arquitetura:</strong></p><ul><li><strong>Regras de Negócio Core, Motores de Precificação e Finanças:</strong> Onde erros de arredondamento, estados inconsistentes ou transições ilegais geram perda financeira direta.</li><li><strong>Refatoração Automatizada de Sistemas Legados:</strong> O Especificador extrai os contratos do sistema antigo, o Falsificador gera a bateria de equivalência (<em>Differential Testing</em>) e o Implementador reescreve o código limpo até convergir.</li><li><strong>Geração de Módulos Autônomos e Bibliotecas Internas:</strong> Componentes com fronteiras bem definidas (entrada/saída clara) que serão consumidos por múltiplos serviços.</li></ul><p><strong>Quando NÃO usar:</strong></p><ul><li><strong>Prototipação Visual, UI/UX e Front-end Cosmético:</strong> Ajustar espaçamentos CSS, animações ou componentes puramente visuais onde a validação humana no navegador é instantânea.</li><li><strong>Scripts Descartáveis de Uso Único (<em>One-off Scripts</em>):</strong> Migrações simples ou análises exploratórias ad-hoc onde o custo de definir tipos formais supera a vida útil do código.</li></ul><h3>Conclusão: Do Programador Codificador ao Orquestrador de Invariantes</h3><p>A ascensão dos agentes de IA não diminui a importância dos Métodos Formais e da Teoria de Tipos, mas pelo contrário, <strong>ela os torna indispensáveis</strong>. Enquanto humanos escreviam código a 50 linhas por hora, a revisão manual conseguia conter parte dos danos. Quando agentes geram 5.000 linhas em minutos, revisar código no “olhômetro” torna-se humanamente impossível.</p><p>Ao estruturar o desenvolvimento como um <strong>Enxame Verificado</strong> , onde o humano atua no topo definindo a intenção arquitetural, o <strong>Especificador</strong> traduz isso em tipos rigorosos, o <strong>Implementador</strong> busca a solução, o <strong>Falsificador</strong> ataca com <em>Property-Based Testing</em> e o <strong>Verificador</strong> exige a prova do sistema de tipos, mudamos fundamentalmente a nossa relação com a IA.</p><p>Deixamos de pedir para a máquina <em>“escrever um código que parece funcionar”</em> e passamos a exigir que ela <strong>encontre uma prova construtiva que satisfaça o nosso sistema de tipos</strong>. A criatividade passa a ser estocástica e automatizada, mas a aceitação do código permanece 100% determinística, lógica e formal.</p><h3>Referências</h3><ul><li><strong>Brady, E. (2017).</strong> <em>Type-Driven Development with Idris.</em> Manning Publications.</li><li><strong>King, A. (2019).</strong> <em>Parse, don’t validate.</em> Lexi Lambda Blog.</li><li><strong>Sharapov, P., &amp; Schanely, P. (2024).</strong> <em>CrossHair: Symbolic Execution and Contract Verification for Python.</em> Documentação Oficial e Repositório Técnico.</li><li><strong>Dubien, N. (2023).</strong> <em>Property-Based Testing in TypeScript with fast-check.</em></li><li><strong>Wu, Q., Bansal, G., et al. (2023).</strong> <em>AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation.</em> Microsoft Research / arXiv:2308.08155.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a83f7a66a62" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Como Refinement Types e Design by Contract Trazem a Verificação Formal para o Mundo Real]]></title>
            <link>https://medium.com/data-hackers/como-refinement-types-e-design-by-contract-trazem-a-verifica%C3%A7%C3%A3o-formal-para-o-mundo-real-735b57eae172?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/735b57eae172</guid>
            <category><![CDATA[design-by-contract]]></category>
            <category><![CDATA[formal-methods]]></category>
            <category><![CDATA[refinement-types]]></category>
            <category><![CDATA[formal-verification]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 02:31:08 GMT</pubDate>
            <atom:updated>2026-10-29T02:38:53.356Z</atom:updated>
            <content:encoded><![CDATA[<blockquote><strong><em>TL;DR:</em></strong><em> Você não precisa reescrever sua base de código em Lean 4 ou Coq para colher os benefícios da verificação formal. Combinando </em><strong><em>Design by Contract (DbC)</em></strong><em> e </em><strong><em>Refinement Types</em></strong><em>, é possível acoplar predicados lógicos diretamente aos tipos que você já usa no dia a dia, delegando a prova matemática de ausência de bugs para </em>SMT Solvers<em> automáticos nos bastidores.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vu7pHdgSgZfGUNkIirOBNw.jpeg" /></figure><h3>A distrância entre a Academia e a Indústria</h3><p>No artigo anterior, exploramos como o <strong>Property-Based Testing (PBT)</strong> e o <strong>Theorem-Oriented Programming (TOP)</strong> elevam a qualidade do software da validação pontual para a certeza matemática. Sob o Isomorfismo de Curry-Howard, vimos que <a href="https://k3ybladewielder.medium.com/p-aa24dc7740a6"><em>tipos são teoremas e programas são provas</em></a>.</p><p>Mas quando um engenheiro de software tenta levar o <em>Theorem-Oriented Programming</em> puro para uma equipe de produto comercial, ele esbarra em uma parede pragmática: o <strong>custo da prova manual</strong>. As linguagens com tipos dependentes completos (como Lean, Rocq/Coq, Agda ou Idris) exigem que o desenvolvedor seja como um matemático ativo, escrevendo técnicas de indução estrutural para convencer o compilador de que a implementação respeita a especificação. Para grande maioria das empresas, o tempo gasto mantendo provas formais manuais simplesmente não fecha a conta do ROI diante de prazos agressivos.</p><p>Isso cria uma falsa dualidade na indústria: ou nos contentamos com sistemas de tipos tradicionais (como int, str, float em Python, TypeScript ou Rust), que deixam passar falhas lógicas graves, ou mergulhamos no rigor acadêmico dos assistentes de prova interativos.</p><p>Mas esse não é o fim da linha, existe uma solução bem prática nessa evolução: a união entre <strong>Design by Contract (DbC)</strong> e <strong>Refinement Types</strong> (Tipos de Refinamento), impulsionada por motores de <strong>Automated Theorem Proving (ATP)</strong> invisíveis ao desenvolvedor.</p><h3>A Cegueira dos Tipos Tradicionais (“Stringly &amp; Intly Typed”)</h3><p>Pense em uma função simples de domínio financeiro ou comercial: calcular o preço final de um produto aplicando uma margem de contribuição ou uma taxa de desconto. Em uma linguagem com tipagem estática tradicional (ou Python moderno com <em>type hints</em>), a assinatura costuma ser escrita assim:</p><pre>def aplicar_desconto(preco: float, percentual_desconto: float) -&gt; float:<br>    return preco * (1.0 - percentual_desconto)</pre><p>Para o verificador de tipos tradicional (mypy, pyright, compilador do Rust ou TypeScript), essa função é impecável: ela recebe dois números e retorna um número. Mas, do ponto de vista matemático e de domínio, o tipo float é um <strong>superconjunto perigosamente amplo</strong>:</p><ul><li>O que acontece se preco for -50.0?</li><li>E se percentual_desconto for 1.5 (gerando um preço final negativo, onde o sistema paga para o cliente levar o produto)?</li><li>E se percentual_desconto for float(&#39;nan&#39;) ou float(&#39;inf&#39;)?</li></ul><p>O sistema de tipos padrão verifica a <strong>representação na memória</strong> (quantos bytes ocupa e quais operações de CPU são válidas), mas é completamente cego à <strong>semântica do domínio</strong>. Para tapar esse buraco, espalhamos dezenas de <em>armengues</em> como if valor &lt; 0: raise ValueError(...) pelo código , que são validações posteriores que só explodem em tempo de execução (<em>runtime</em>), quando o dado inválido já contaminou o fluxo.</p><h3>Design by Contract (DbC): Especificação como Cidadã de Primeira Classe</h3><p>Cunhado por Bertrand Meyer na década de 1980 (com a linguagem Eiffel) e fundamentado na <a href="https://pt.wikipedia.org/wiki/L%C3%B3gica_de_Hoare"><strong>Lógica de Hoare</strong></a> (<em>{P} C {Q}</em>), o <em>Design by Contract</em> trata a relação entre uma função (<em>fornecedor</em>) e quem a chama (<em>cliente</em>) por meio de um contrato formal explícito, dividido em três pilares:</p><ul><li><strong>Pré-condições (</strong>requires<strong>):</strong> O que deve ser verdade <em>antes</em> da função ser executada. É a obrigação de quem chama. Se a pré-condição for violada, a função nem sequer tem obrigação de rodar; a culpa do bug é inequivocamente do chamador.</li><li><strong>Pós-condições (</strong>ensures<strong>):</strong> O que a função garante que será verdade <em>depois</em> que ela terminar, assumindo que a pré-condição foi respeitada. É a obrigação da implementação.</li><li><strong>Invariantes de Estado / Estrutura (</strong>invariant<strong>):</strong> Propriedades que devem permanecer verdadeiras durante todo o ciclo de vida de uma estrutura de dados ou a cada iteração de um loop.</li></ul><p>Em uma notação formal da Lógica de Hoare, representamos nosso cálculo de desconto como:</p><blockquote>{preco &gt; 0 ∧ 0 ≤ desconto &lt; 1} res := aplicar_Desconto(preco, desconto) {0 &lt; res ≤ preco}</blockquote><p>Em que:</p><ul><li><strong>Pré-condições: </strong>{preco &gt; 0 forall 0 ≤ desconto &lt; 1}, o preço oroginal precisa ser positivo <strong>e</strong> (∧) a taxa de desconto deve zero ou menor que um, ou 0% a 99.999…%.</li><li><strong>Comando:</strong> O opererador := representa a atribuição, em <em>aplicar_desconto(preco, desconto)</em> estamos executando a função para calcular preço x (1–0) e armazenando o valor do retornado na variável res(resultado)</li><li><strong>Pós-condição:</strong> Na pós condição {0 &lt; res ≤ preço} estamos garantindo que o preço final após o desconto continuará obrigatoriamente positivo e maior que zero.</li></ul><p><em>Mas por que essa fórmula prova a ausência de bugs?</em> Em um verificador formal (ou SMT Solver como o Z3), essa tripla é transformada em um teorema de implicação lógica pura:</p><blockquote>∀ preco, desconto ∈ ℝ, (preco &gt; 0 ∧ 0 ≤ desconto &lt; 1) ⟹ (0 &lt; preco × (1 — desconto ≤ preco)</blockquote><p>A demonstração algébrica que o compilador resolve nos bastidores é direta:</p><ol><li>Como <em>desconto &lt; 1</em>, subtraindo de <em>1</em> temos que <em>(1 — desconto) &gt; 0</em>.</li><li>Como o produto de dois números estritamente positivos (<em>preco &gt; 0 e 1 — desconto &gt; 0</em>) é sempre positivo, fica provado que <em>0 &lt; res</em>.</li><li>Como <em>desconto ≥ 0,</em> temos que <em>(1 — desconto) ≤ 1</em>. Multiplicando ambos os lados por <em>preco </em>(que é positivo), temos <em>preco </em>× <em>(1 — desconto) ≤ preco </em>×<em> 1</em>, provando que <em>res ≤ preco</em>.</li></ol><h3>Contratos Dinâmicos vs. Contratos Estáticos</h3><p>Você pode implementar DbC hoje mesmo em Python usando bibliotecas como icontract ou deal, decorando funções com @require(lambda x: x &gt; 0) e @ensure(lambda result, x: result &lt;= x).</p><ul><li><strong>A vantagem imediata:</strong> Seus contratos viram oráculos automáticos para o <strong>Property-Based Testing</strong> (o <em>Hypothesis</em> consegue ler os decoradores do icontract e gerar milhares de entradas válidas respeitando o @require para tentar quebrar o @ensure).</li><li><strong>A limitação:</strong> Contratos puramente dinâmicos ainda são checados em <em>runtime</em> (ou durante a suíte de testes). E se pudéssemos embutir esses contratos diretamente no <strong>sistema de tipos</strong> e verificá-los em tempo de compilação? É exatamente aí que entram os <em>Refinement Types</em>.</li></ul><h3>Refinement Types: Tipos com Predicados Lógicos</h3><p>Um <strong>Refinement Type</strong> (Tipo de Refinamento) é um tipo base tradicional (como ℤ, Int, Float ou List) filtrado por um predicado lógico decidível. Na notação matemática de teoria dos conjuntos (compreensão), escrevemos:</p><blockquote>{v : T | P(v)}</blockquote><p>Lê-se: <em>“O conjunto de todos os valores </em><strong><em>v</em></strong><em> do tipo base </em><strong><em>T</em></strong><em> tais que o predicado lógico </em><strong><em>P(v) </em></strong><em>seja verdadeiro”</em>.</p><p>Veja como conceitos vagos do dia a dia ganham precisão cirúrgica quando expressos como <em>Refinement Types</em>:</p><ul><li><strong>Natural Positivo (</strong>PosInt<strong>):</strong> <em>{x : Int | x &gt; 0}</em></li><li><strong>Taxa / Percentual Normalizado (</strong>Rate<strong>):</strong> <em>{r : Float | 0.0 ≤ r ≤ 1.0}</em></li><li><strong>Lista Não-Vazia (</strong>NonEmptyList<strong>):</strong> <em>{xs : List α | len(xs) &gt; 0}</em></li><li><strong>Índice Seguro de Vetor (</strong>ValidIdx<strong>):</strong> <em>{i : Int | 0 ≤ i &lt; len (arr)}</em></li></ul><p>Aqui está a diferença crucial entre <em>Theorem-Oriented Programming</em> clássico (Tipos Dependentes completos) e <em>Refinement Types. </em>Em <strong>Lean</strong> ou <strong>Coq</strong>, os predicados podem conter qualquer lógica arbitrária de ordem superior, o que torna a verificação matematicamente indecidível no caso geral — obrigando o humano a escrever a prova.</p><p>Nos <strong>Refinement Types</strong> (implementados em ferramentas como <strong>LiquidHaskell</strong>, <strong>Flux</strong> para Rust, <strong>Dafny</strong> ou <strong>Stainless</strong> para Scala), os predicados <em>P(v)</em> são estrategicamente restritos a lógicas decidíveis (como aritmética linear inteira, lógica de vetores de bits e teorias não interpretadas). Graças a essa restrição arquitetural, <strong>você escreve zero linhas de prova</strong>.</p><p>Quando você compila o código, o verificador extrai as condições de subtipagem (chamadas de <strong>Verification Conditions</strong> ou <strong>VCs)</strong> e as envia silenciosamente para um <strong>SMT Solver</strong> (<em>Satisfiability Modulo Theories</em>, como o <strong>Z3</strong> da Microsoft ou o <strong>cvc5</strong>).</p><pre>[Código + Refinement Types] <br>        │<br>        ▼ (Extração de Contratos)<br>[Condições de Verificação (VCs): Implicações Lógicas P(x) ⟹ Q(f(x))]<br>        │<br>        ▼ (Consulta Negada: ∃x tal que P(x) ∧ ¬Q(f(x))?)<br>[SMT Solver (Z3 / cvc5)]<br>        ├──► UNSAT (Impossível violar) ──► Compilação Aprovada (Prova Automática!)<br>        └──► SAT (Contraexemplo achado: x = 0) ──► Erro de Compilação Cirúrgico</pre><p>O SMT Solver tenta achar <strong>um único contraexemplo</strong> que satisfaça as pré-condições mas viole a pós-condição. Se o solver retornar UNSAT (<em>Unsatisfiable</em>), significa que é matematicamente impossível violar o contrato: seu teorema está provado automaticamente. Se retornar SAT, o compilador trava o build e já te mostra exatamente o valor que quebraria o sistema em produção.</p><h3>Como Isso Funciona no Código</h3><p>Para visualizar o poder dessa abordagem sem cair em abstrações puras, veja o exemplo abaixo como um problema clássico de falha em produção (<strong>divisão por zero e perda de invariantes em cálculos de lote)</strong> é resolvido estaticamente com contratos e refinamentos (usando a sintaxe limpa do <strong>Dafny</strong> / estilo <strong>Flux</strong>):</p><pre>// 1. Definimos um Refinement Type: um inteiro estritamente positivo<br>type PosInt = x: int | x &gt; 0 witness 1<br><br>// 2. Função com Design by Contract verificado estaticamente pelo Z3<br>method CalcularTicketMedio(receitaTotal: int, quantidadePedidos: PosInt) returns (ticket: int)<br>  // Pré-condição: receita não pode ser negativa<br>  requires receitaTotal &gt;= 0<br>  // Pós-condições provadas matematicamente para QUALQUER entrada:<br>  ensures ticket &gt;= 0<br>  ensures ticket &lt;= receitaTotal<br>{<br>  // O compilador sabe pelo Refinement Type &#39;PosInt&#39; que quantidadePedidos &gt;= 1.<br>  // Portanto, ele PROVA automaticamente:<br>  // 1) Impossibilidade absoluta de ZeroDivisionError<br>  // 2) Que a divisão inteira jamais será negativa nem maior que o numerador<br>  ticket := receitaTotal / quantidadePedidos;<br>}</pre><p>Se outro desenvolvedor tentar chamar CalcularTicketMedio(1000, total_clientes - clientes_inativos) sem antes provar no fluxo de controle (via um simples if total_clientes &gt; clientes_inativos) que o segundo argumento é estritamente maior que zero, <strong>o código sequer compila.</strong></p><p>Mas como aplicar essa mentalidade hoje em linguagens mainstream como <strong>Python</strong>, <strong>TypeScript</strong> ou <strong>Rust</strong>? Através do padrão arquitetural conhecido como <strong>Parse, Don’t Validate</strong> (popularizado por Alexis King), que materializa os <em>Refinement Types</em> por meio de <strong>Tipos Opacos / Newtypes</strong> e Construtores Inteligentes (<em>Smart Constructors</em>).</p><p>O erro comum em sistemas tradicionais é <strong>validar</strong>: você recebe um int, checa if x &gt; 0 na camada de entrada, mas continua passando o mesmo tipo primitivo int para as 15 funções internas do domínio. As funções internas não têm garantia no sistema de tipos de que a validação já ocorreu, forçando programação defensiva redundante.</p><p>Ao usar a mentalidade de <em>Refinement Types</em>:</p><ul><li><strong>A Fronteira Impura (I/O, API, Banco de Dados):</strong> Recebe dados brutos não confiáveis (int, str, dict) e aplica um <em>parser</em> estrito (como Pydantic com tipos restritos PositiveInt, confloat(ge=0, le=1) em Python, ou tipos refinados com NewType). Se falhar, rejeita imediatamente na borda.</li><li><strong>O Núcleo Funcional Puro (<em>Verified Core</em>):</strong> Só aceita os tipos já refinados em suas assinaturas. Dentro do núcleo, nenhuma função precisa fazer if x &lt; 0: raise ..., pois um valor inválido é <strong>irrepresentável</strong> nos tipos aceitos pelo núcleo.</li></ul><h3>Ferramentas por Ecossistema</h3><p>Se você quer introduzir <em>Refinement Types</em> e <em>Design by Contract</em> nos seus projetos atuais sem mudar para um assistente de provas acadêmico, esta é a <em>stack </em>estado da arte atual:</p><ul><li><strong>Rust:</strong> <strong>Flux</strong> (sistema de <em>Refinement Types</em> conectado ao compilador do Rust que usa SMT solvers para provar ausência de panics e acessos fora de limites em arrays), <strong>Kani</strong> (verificador de modelos bit-preciso mantido pela AWS) e <strong>Creusot</strong> (contratos dedutivos em Rust).</li><li><strong>Python:</strong> <strong>CrossHair</strong> (usa o SMT Solver Z3 e execução simbólica para verificar contratos icontract, deal ou assert em código Python puro estaticamente), aliado a <strong>Pydantic</strong> (Annotated constraints) + <strong>Hypothesis</strong> para fechar o loop híbrido.</li><li><strong>Haskell:</strong> <strong>LiquidHaskell</strong> (o padrão-ouro histórico de <em>Logically Qualified Data Types</em>, permitindo provar propriedades complexas de estruturas de dados usando o Z3 diretamente em comentários especiais).</li><li><strong>TypeScript / JavaScript:</strong> Embora não possua um SMT solver nativo no compilador, o uso de <strong>Branded Types</strong> (<em>Nominal Typing</em> via interseção { __brand: &#39;PositiveNumber&#39; }) combinado com <strong>Zod</strong> e <strong>fast-check</strong> simula a barreira arquitetural dos tipos de refinamento com perfeição.</li><li><strong>Linguagens Dedicadas à Engenharia Verificada:</strong> <strong>Dafny, </strong>usado fortemente pela AWS para verificar motores de autorização como o Cedar e sistemas criptográficos, compilando direto para Python, Go, C# e Java; e <strong>Ada / SPARK</strong> (padrão da indústria aeroespacial e ferroviária).</li></ul><h3>Quando Usar e Quando Não Usar</h3><p>Assim como nos testes de propriedade e nas provas formais completas, o segredo está na alocação cirúrgica de esforço:</p><p><strong>Onde Refinement Types e DbC brilham:</strong></p><ul><li><strong>Prevenção de Erros Aritméticos e de Limites:</strong> Divisão por zero, <em>overflow/underflow</em> numérico, índices de arrays fora do limite (<em>IndexOutOfBounds</em>) e fatiamento de matrizes/tensores.</li><li><strong>Modelagem de Domínio Financeiro e Comercial:</strong> Garantir por construção que quantidades em estoque, preços, taxas de juros, markups e saldos respeitem limites matemáticos estritos sem espalhar ifs defensivos pelo código.</li><li><strong>Máquinas de Estado e Protocolos:</strong> Garantir via pré-condições que uma função faturar_pedido(p) só possa ser invocada se o estado de p for estritamente PedidoAprovado e com itens.length &gt; 0.</li></ul><p><strong>Onde NÃO aplicar:</strong></p><ul><li><strong>Regras de Negócio com Semântica Externa (Não-Matemática):</strong> Um SMT Solver consegue provar que uma string tem tamanho maior que zero e segue um formato específico, mas não consegue provar matematicamente se um endereço de entrega existe no mundo real.</li><li><strong>Lógica Não-Linear Pesada:</strong> SMT Solvers são extremamente rápidos para aritmética linear (<em>2x + 3y ≤ 10</em>), mas podem sofrer <em>timeouts</em> (indecidibilidade prática) se você colocar polinômios de grau alto, funções trigonométricas ou hashes criptográficos dentro do predicado de refinamento.</li></ul><h3>Conclusão</h3><p>Durante décadas, a verificação formal foi vista como um castelo de marfim: admirada de longe pela sua beleza matemática, mas considerada impraticável para quem precisa colocar software em produção toda semana. <strong>Design by Contract</strong> e <strong>Refinement Types</strong> derrubam esse muro.</p><p>Ao delegar o trabalho braçal da demonstração matemática para <em>SMT Solvers</em> (como o Z3) e ao restringir os tipos primitivos com predicados lógicos claros, nós mudamos a natureza do nosso ciclo de desenvolvimento. Deixamos de usar testes apenas para caçar bugs que nós mesmos permitimos existir nas assinaturas frouxas das funções e passamos a desenhar <strong>espaços de estados onde o bug sequer consegue compilar</strong>.</p><p><strong>O caminho para o software livre de bugs não exige que todo programador se torne um doutor em teoria das categorias</strong>. Ele começa com um passo muito mais simples e poderoso: olhar para um int ou float na assinatura da sua função principal e declarar explicitamente qual é a verdade matemática que aquele dado carrega.</p><h3>Referências</h3><ul><li><strong>Meyer, B. (1992).</strong> <em>Applying “Design by Contract”.</em> IEEE Computer, 25(10), 40–51.</li><li><strong>Hoare, C. A. R. (1969).</strong> <em>An axiomatic basis for computer programming.</em> Communications of the ACM, 12(10), 576–580.</li><li><strong>Vazou, N., Seidel, E. L., Jhala, R., Vytiniotis, D., &amp; Peyton-Jones, S. (2014).</strong> <em>Refinement Types for Haskell.</em> ACM SIGPLAN Notices (ICFP ‘14), 49(9), 269–282.</li><li><strong>Lehmann, N., Geller, A. T., et al. (2023).</strong> <em>Flux: Liquid Types for Rust.</em> Proceedings of the ACM on Programming Languages, 7(PLDI), 1533–1557.</li><li><strong>De Moura, L., &amp; Bjørner, N. (2008).</strong> <em>Z3: An efficient SMT solver.</em> Tools and Algorithms for the Construction and Analysis of Systems (TACAS), 337–340.</li><li><strong>King, A. (2019).</strong> <em>Parse, don’t validate.</em> Blog técnico (Lexi Lambda).</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=735b57eae172" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/como-refinement-types-e-design-by-contract-trazem-a-verifica%C3%A7%C3%A3o-formal-para-o-mundo-real-735b57eae172">Como Refinement Types e Design by Contract Trazem a Verificação Formal para o Mundo Real</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tipos são Teoremas, Programas são Provas]]></title>
            <link>https://medium.com/data-hackers/tipos-s%C3%A3o-teoremas-programas-s%C3%A3o-provas-aa24dc7740a6?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/aa24dc7740a6</guid>
            <category><![CDATA[theorem-proving]]></category>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[formal-verification]]></category>
            <category><![CDATA[formal-methods]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 22:32:17 GMT</pubDate>
            <atom:updated>2026-10-25T23:49:37.142Z</atom:updated>
            <content:encoded><![CDATA[<blockquote><strong><em>TL;DR:</em></strong><em> Testes baseados em exemplos apenas confirmam que o seu código funciona para os cenários que você conseguiu imaginar. Para construir sistemas verdadeiramente resilientes, precisamos elevar o nível de garantia: migrando da validação pontual para a exploração estocástica de invariantes com </em><strong><em>Property-Based Testing (PBT)</em></strong><em> e, no limite do rigor, para a garantia matemática absoluta com </em><strong><em>Theorem-Oriented Programming (TOP)</em></strong><em>.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g9VPuQTrqGm64vXyAPBpuw.jpeg" /></figure><h3>Por que não podemos confiar em testes baseados em exemplos</h3><p>No final da década de 1960, Edsger W. Dijkstra cunhou uma das frases mais célebres — e desconfortáveis — da Ciência da Computação: <em>“Testes de software podem ser usados para mostrar a presença de bugs, mas nunca para mostrar a sua ausência”</em>. Meio século depois, a indústria de software continua presa a um paradoxo: escrevemos milhares de linhas de testes unitários tradicionais (<em>example-based tests</em>), alcançamos 95% de <em>code coverage</em> e, ainda assim, sistemas críticos colapsam em produção diante de um <em>edge case</em> não previsto.</p><p>O problema fundamental do teste baseado em exemplos é o <strong>viés cognitivo do autor</strong>. Quando um engenheiro escreve a função sort(lista) e em seguida escreve o teste assert sort([3, 1, 2]) == [1, 2, 3], o teste sofre exatamente das mesmas pontos cegos que a implementação. <strong>Testamos o &quot;caminho feliz&quot;</strong> e meia dúzia de casos de borda óbvios (lista vazia, elementos duplicados), mas deixamos descoberto um universo infinito de estados possíveis (NaN, <em>overflows</em> aritméticos, caracteres Unicode malformados, concorrência).</p><p>Se quisermos construir sistemas onde a inexistência de certas classes de bugs deixe de ser uma esperança e passe a ser uma propriedade arquitetural, precisamos repensar dois pilares: <strong>como validamos nosso código</strong> (Testes de Propriedade) e <strong>sob qual paradigma o construímos</strong> (Programação Orientada a Teoremas).</p><p><a href="https://k3ybladewielder.medium.com/engenharia-de-software-ag%C3%AAntica-3f07d1114239">Engenharia de Software Agêntica</a></p><h3>O Loop de Desenvolvimento Orientado à Corretude</h3><p>Na engenharia de software tradicional, o ciclo de desenvolvimento frequentemente trata o teste como uma etapa burocrática pós-implementação. Na engenharia de alta confiabilidade, o desenvolvimento opera como um <em>closed-loop feedback system</em>, cujo objetivo é estreitar o espaço de estados inválidos até que ele convirja para zero.</p><p>Esse fluxo opera em três estágios contínuos:</p><pre>      ┌──────────────────────────────────────────────────────────────┐<br>      │                                                              │<br>      ▼                                                              │<br>[1. Programação] ──► [2. Validação (PBT / Provas)] ──► [3. Correção] ┘<br>                                │<br>                                ▼ (Invariantes Satisfeitas / Q.E.D.)<br>                     [Código Livre da Classe de Bugs]</pre><ul><li><strong>1. Programação (Definição de Contratos e Transformações):</strong> Em vez de apenas acoplar estado e comportamento, o foco está na assinatura de tipos e na transformação pura de dados (um princípio central em <em>Data-Oriented</em> e <em>Functional Programming</em>). O código é escrito tendo em mente quais propriedades matemáticas ele deve preservar.</li></ul><p><a href="https://medium.com/data-hackers/programa%C3%A7%C3%A3o-orientada-a-dados-140c3bdd1b0d">Programação Orientada a Dados</a></p><ul><li><strong>2. Validação via Testes Robustos ou Verificação:</strong> O código é submetido a um oráculo. No nível estocástico, geradores massivos de entradas tentam violar as invariantes do sistema (<em>Property-Based Testing</em>). No nível formal, o verificador de tipos/provas (<em>Type Checker / Proof Assistant</em>) verifica se a implementação satisfaz a especificação lógica para <strong>todas</strong> as entradas possíveis.</li><li><strong>3. Correção e Refinamento de Invariantes:</strong> Quando uma falha é detectada — seja por um contraexemplo reduzido (<em>shrunk counterexample</em>) no teste de propriedade, seja por uma obrigação de prova não resolvida no compilador — , o desenvolvedor não aplica apenas um if paliativo. Ele analisa a falha estrutural: <em>O código violou a especificação ou a própria especificação estava incompleta?</em></li><li><strong>Repetição até a Convergência:</strong> O ciclo 1 -&gt; 2 -&gt; 3 se repete. A cada iteração, o espaço de busca de bugs é podado. O loop só termina quando o fuzzer/gerador é incapaz de encontrar contraexemplos após milhares de permutações ou quando o assistente de provas aceita o teorema (<em>Q.E.D.</em>).</li></ul><h3>Property-Based Testing (PBT)</h3><p>Popularizado pela biblioteca <strong>QuickCheck</strong> em Haskell (criada por Koen Claessen e John Hughes em 2000) e hoje presente em praticamente todas as linguagens modernas (como <strong>Hypothesis</strong> em Python, <strong>fast-check</strong> em TypeScript/JavaScript e <strong>proptest</strong> em Rust), o <em>Property-Based Testing</em> muda a pergunta fundamental do teste.</p><p>Em vez de perguntar <em>“Para a entrada X, a saída é Y?”</em>, o PBT pergunta: <strong>“Para qualquer entrada válida <em>x</em> pertencente a um domínio </strong>𝓓<strong>, a propriedade P(</strong>𝑓<strong>(x)) se mantém verdadeira?”</strong></p><p>Isso aproxima o teste de software do método científico de Karl Popper: não tentamos provar que o código está certo com meia dúzia de exemplos confirmirmatórios; tentamos <strong>falsificar</strong> a hipótese de que o código está correto gerando <strong>centenas ou milhares de entradas adversariais</strong>.</p><h3>A Anatomia de um Teste de Propriedade</h3><p>Para substituir exemplos fixos por propriedades universais, utilizamos padrões de design de propriedades:</p><ul><li><strong>Inversibilidade (Round-trip / Simetria):</strong> Se você serializa uma estrutura de dados para JSON/binário e a desserializa em seguida, deve obter exatamente o dado original:</li></ul><blockquote>∀x ∈ Dados, decode(encode(<em>x</em>)) = <em>x</em></blockquote><ul><li><strong>Idempotência:</strong> Aplicar uma operação múltiplas vezes produz o mesmo resultado que aplicá-la uma única vez (ex.: ordenação, normalização de texto, filtros de limpeza):</li></ul><blockquote>∀ x, 𝑓(𝑓(x)) = 𝑓(x)</blockquote><ul><li><strong>Invariantes Estruturais:</strong> Certas métricas não devem ser alteradas pela transformação. Ao ordenar uma lista, o tamanho da lista de saída deve ser igual ao da entrada, o conjunto de elementos deve ser exatamente o mesmo (permutação) e para todo índice <em>i</em>, L[i] ≤ L[i+1].</li><li><strong>Oráculo de Teste (Differential Testing):</strong> Comparar uma implementação altamente otimizada, complexa ou paralela 𝑓fast(<em>x</em>) contra uma implementação de referência simples, porém sabidamente correta 𝑓naive(<em>x</em>):</li><li>∀ x, 𝑓fast(<em>x</em>) = 𝑓naive(<em>x</em>)</li></ul><h3>Shrinking ou Redução de Contraexemplos</h3><p>Se um gerador aleatório descobre que a sua função de agregação financeira quebra com uma lista de 4.500 transações contendo floats extremos como [-1.4012984643e-45, 3.4028234e+38, ...], depurar esse erro manualmente seria um pesadelo.</p><p>É aqui que entra o <strong>Shrinking</strong>. Assim que o framework de PBT encontra uma entrada que viola a propriedade, ele interrompe a geração aleatória e inicia uma busca sistemática em árvore para <strong>simplificar o erro ao menor caso reprodutível possível</strong>. Ele remove elementos da lista, reduz números grandes para perto de zero e simplifica strings até encontrar a semente exata do bug — entregando no terminal algo limpo como:</p><blockquote><em>Falsifying example: processar_transacoes([0.0, -0.0, nan])</em></blockquote><p>O ciclo Programação -&gt; Validação -&gt; Correção ganha uma velocidade brutal porque o desenvolvedor não perde horas isolando o estado que causou a falha; a ferramenta entrega o diagnóstico cirúrgico.</p><h3>Quando Testar Não é Suficiente</h3><p>O <em>Property-Based Testing</em> reduz drasticamente a probabilidade de bugs, mas ainda opera no domínio probabilístico: se uma falha ocorre apenas quando um inteiro de 64 bits assume exatamente o valor 9007199254740993, a chance de um gerador estocástico sortear esse número específico é virtualmente nula.</p><p>E se pudéssemos testar <strong>todas</strong> as entradas possíveis — mesmo que o domínio seja infinito — em tempo de compilação? Essa é a premissa do <strong>Theorem-Oriented Programming</strong> (também conhecido como <em>Proof-Oriented Programming</em> ou <em>Verified Programming</em>), materializado em linguagens e assistentes de prova como <strong>Lean 4</strong>, <strong>Coq (Rocq)</strong>, <strong>Agda</strong>, <strong>Idris 2</strong>, <strong>F*, Dafny e Bend2</strong>.</p><h3>Tipos são Teoremas, Programas são Provas</h3><p>A fundação teórica do TOP é o <strong>Isomorfismo de Curry-Howard</strong>, uma correspondência profunda entre a lógica matemática e a teoria de tipos computacional</p><p>Sob a lente desse isomorfismo, uma <strong>proposição ou teorema</strong> na lógica matemática corresponde diretamente a um <strong>tipo</strong> (<em>A</em>) na teoria de tipos, enquanto a <strong>prova dessa proposição</strong> é materializada por um <strong>programa ou valor</strong> (<em>a : A</em>) que habita esse tipo. A partir dessa base, os conectivos lógicos traduzem-se perfeitamente em estruturas computacionais: a <strong>implicação</strong> (<em>A ⟹ B</em>) equivale a um <strong>tipo função</strong> (<em>A ⟶ B</em>), a <strong>conjunção</strong> (<em>A∧B</em>) é representada por um <strong>tipo produto ou tupla</strong> (<em>(A, B)</em>), e a <strong>disjunção</strong> (<em>A ∨ B</em>) manifesta-se como um <strong>tipo soma ou <em>tagged union</em></strong> (<em>Either</em> <em>A B</em>). Expandindo essa expressividade, o <strong>quantificador universal</strong> (<em>∀ x : A, P(x)</em>) é modelado por uma <strong>função dependente</strong> (<em>(x : A) ⟶ P(x)</em>), e uma <strong>proposição falsa</strong> (⊥) equivale ao <strong>tipo vazio</strong> (Empty ou Never) — um tipo desprovido de construtores e que, por definição, jamais pode ser instanciado.</p><p>Em linguagens tradicionais (como Python, Java ou mesmo Rust), o sistema de tipos é estático mas possui expressividade limitada: você pode dizer que uma função recebe List&lt;Int&gt;, mas não pode expressar no próprio tipo que a lista <strong>não está vazia</strong> ou que está <strong>ordenada</strong>.</p><p>No <em>Theorem-Oriented Programming</em>, utilizamos <strong>Tipos Dependentes</strong> (<em>Dependent Types</em>), ou seja, tipos que dependem de <em>valores</em>. Você pode definir o tipo Vect n a (um vetor de tamanho n contendo elementos do tipo a). A assinatura de uma função que concatena dois vetores passa a ser:</p><blockquote>append: Vect n, α <em>⟶</em> Vect m, α <em>⟶</em> Vect (n + m), α</blockquote><p>Se na sua implementação você esquecer um elemento ou duplicar um item, o código <strong>não falha em produção nem em um teste</strong>: ele <strong>não compila</strong>, pois o tipo resultante não será <em>Vect (n + m)</em>,α.</p><h3>Especificação + Implementação + Prova</h3><p>No paradigma orientado a teoremas, o desenvolvimento deixa de ser apenas a escrita de algoritmos e passa a ser a construção conjunta do algoritmo e do certificado matemático de sua corretude. Veja como isso se parece conceitualmente (usando a sintaxe de <strong>Lean 4</strong>):</p><pre>-- 1. Implementação: uma função simples que inverte uma lista<br>def inverter : List α → List α<br>  | []      =&gt; []<br>  | x :: xs =&gt; inverter xs ++ [x]<br><br>-- 2. Teorema (Especificação Formal): <br>-- Para qualquer lista &#39;xs&#39;, inverter duas vezes retorna a própria lista &#39;xs&#39;<br>theorem inverter_involucao (xs : List α) : inverter (inverter xs) = xs := by<br>  induction xs with<br>  | nil =&gt; rfl -- Caso base: lista vazia é trivial por reflexividade<br>  | cons x xs ih =&gt; <br>    -- Passo indutivo: usa a hipótese de indução (ih) e propriedades de concatenação<br>    simp [inverter, ih]</pre><p>Observe o que aconteceu acima: não executamos inverter para 1.000 listas aleatórias. Usamos <strong>indução estrutural</strong> para provar matematicamente que, para <strong>qualquer</strong> lista de <strong>qualquer</strong> tamanho (de zero ao infinito) e de <strong>qualquer</strong> tipo α, a propriedade de involução é 100% verdadeira. Não há espaço para <em>edge cases</em>.</p><h3>A Pirâmide de Confiabilidade</h3><p>Um erro comum ao conhecer métodos formais é achar que <em>Theorem-Oriented Programming</em> substitui <em>Property-Based Testing</em>, ou que é inviável aplicá-lo fora da academia. Na prática moderna de engenharia de software de alto nível, como no desenvolvimento do kernel Linux, compiladores como CompCert, protocolos criptográficos na AWS ou sistemas distribuídos, <strong>PBT e TOP trabalham em simbiose dentro do mesmo loop de desenvolvimento</strong>.</p><p><strong>Provar um teorema falso é impossível</strong> — e tentar fazer isso em um assistente de provas pode custar horas de frustração até o engenheiro perceber que o seu enunciado tinha uma falha sutil para o caso <em>x = 0</em>. Por isso, o estado da arte do loop Programação -&gt; Validação -&gt; Correção combina ambos.</p><p>Essas três abordagens distribuem-se ao longo de um espectro claro de mecanismo, cobertura, custo cognitivo e papel estratégico dentro do loop de desenvolvimento. Na base, os <strong>testes baseados em exemplos</strong> operam por meio de asserções manuais pontuais, oferecendo um escopo de cobertura <em>O(1)</em> restrito a alguns pontos discretos; em contrapartida, exigem baixo custo cognitivo e de tempo, cumprindo o papel de documentação viva e barreira rápida contra regressões. Em um degrau intermediário, o <strong>Property-Based Testing (PBT)</strong> utiliza geração estocástica aliada a <em>shrinking</em> para expandir a cobertura a <em>O(N) (</em>amostrando milhares de pontos do domínio ), o que demanda um custo cognitivo médio por exigir que o desenvolvedor pense em invariantes universais, atuando no loop como um motor de descoberta rápida de <em>edge cases</em> e falsificação de hipóteses. No topo do rigor, o <strong>Theorem-Oriented Programming (TOP)</strong> substitui a amostragem pela verificação formal e prova indutiva para atingir uma cobertura exaustiva e infinita (∀<em>x</em>); embora imponha um alto custo cognitivo e de tempo devido à necessidade de modelagem lógica e indução matemática, ele entrega a garantia absoluta indispensável para blindar o <em>core</em> algorítmico e crítico do sistema.</p><blockquote><strong><em>Dica de Ouro (Proof Engineering):</em></strong><em> Ferramentas modernas como </em><strong><em>QuickChick</em></strong><em> (para Coq) e </em><strong><em>Plausible</em></strong><em> (para Lean 4) executam </em>Property-Based Testing<em> </em><strong><em>antes</em></strong><em> que você tente escrever uma prova formal. Você enuncia o teorema e roda o PBT: se houver um bug na sua lógica, o gerador encontra um contraexemplo em milissegundos. Só quando o PBT passa sem encontrar falhas é que você investe tempo escrevendo a prova matemática formal.</em></blockquote><h3>Outras Técnicas Complementares</h3><p>Para enriquecer ainda a robustez do processo de engenharia de software e a arquitetura de qualidade de um projeto real, vale destacar três degraus intermediários entre o PBT e a prova formal completa:</p><ul><li><strong>Design by Contract (DbC) e Refinement Types:</strong> Linguagens e ferramentas como <strong>LiquidHaskell</strong>, <strong>Dafny</strong> ou <strong>Kani/Creusot</strong> (para Rust) permitem anotar funções com pré-condições (requires), pós-condições (ensures) e invariantes de loop. Em vez de o programador escrever a prova manualmente, o compilador delega a verificação para um <strong>SMT Solver</strong> (como o Z3), que resolve as equações lógicas automaticamente nos bastidores.</li><li><strong>Coverage-Guided Fuzzing:</strong> Enquanto o PBT gera entradas baseadas na estrutura dos tipos de dados, <em>fuzzers</em> guiados por cobertura (como AFL++, libFuzzer ou Atheris) instrumentam o código compilado para monitorar quais ramos (if/else) foram ativados. Se uma mutação na entrada descobre um novo caminho de execução nunca antes visitado, o fuzzer prioriza mutações a partir daquela entrada, explorando profundamente a máquina de estados do software.</li><li><strong>Model Checking (TLA+ / Alloy):</strong> Às vezes o código de cada função está perfeito, mas a interação concorrente ou distribuída entre três microsserviços gera um <em>deadlock</em> ou <em>race condition</em>. Especificações formais de arquitetura com TLA+ permitem testar o desenho do sistema antes mesmo de escrever a primeira linha de código em produção.</li></ul><h3>Quando Usar: Onde o Rigor Compensa o Custo</h3><p>Adotar <em>Property-Based Testing</em> (PBT) e <em>Theorem-Oriented Programming</em> (TOP) exige um investimento cognitivo maior do que escrever testes de exemplo tradicionais. A regra de ouro da engenharia de confiabilidade é aplicar o rigor de forma proporcional ao <strong>raio de explosão</strong> (<em>blast radius</em>) de uma eventual falha.</p><p><strong>Quando aplicar Property-Based Testing (PBT):</strong></p><ul><li><strong>Transformações Puras e Pipelines de Dados:</strong> Funções que filtram, agregam, normalizam ou transformam estruturas complexas sem depender de estado externo (o coração de arquiteturas orientadas a dados e funcionais).</li><li><strong>Pares de Serialização, Parsers e Compiladores:</strong> Qualquer operação que tenha uma inversa natural (JSON/Protobuf encode/decode, compressão/descompressão, criptografia, conversão de ASTs).</li><li><strong>Regras de Negócio Financeiras e Matemáticas:</strong> Motores de precificação, cálculo de margem de contribuição, rateio de descontos, impostos e algoritmos estatísticos onde erros de arredondamento de ponto flutuante (IEEE 754) ou <em>edge cases</em> numéricos geram prejuízo financeiro direto.</li><li><strong>Refatoração e Otimização de Performance:</strong> Ao substituir um algoritmo simples e lento por uma versão paralela, vetorizada ou altamente otimizada, usando a versão antiga como oráculo (<em>Differential Testing</em>).</li><li><strong>Máquinas de Estado Complexas (<em>Stateful Testing</em>):</strong> Sistemas de controle de estoque, filas de pedidos ou caches distribuídos, onde o bug não está em uma única chamada de função, mas em uma sequência específica de 15 operações intercaladas.</li></ul><p><strong>Quando aplicar Theorem-Oriented Programming (TOP) / Verificação Formal:</strong></p><ul><li><strong>Núcleo Crítico de Domínio (<em>Verified Core</em>):</strong> Quando uma falha é catastrófica, irreversível ou inaceitável — como protocolos criptográficos, <em>smart contracts</em>, kernels de sistemas operacionais, compiladores e sistemas aeroespaciais/médicos.</li><li><strong>Algoritmos Fundamentais e Estruturas de Dados Base:</strong> Bibliotecas internas que serão reutilizadas por centenas de outros serviços. Provar a corretude do componente base elimina uma classe inteira de bugs em toda a árvore de dependências.</li><li><strong>Invariantes de Segurança e Isolamento:</strong> Garantir matematicamente que um sistema multi-tenant nunca vaze dados de um cliente para outro, ou que uma árvore de permissões jamás conceda acesso indevido, independentemente da entrada.</li><li><strong>Validação de Modelos Simbólicos e Matemáticos:</strong> Sistemas que manipulam expressões algébricas, otimizadores de consulta (SQL/grafos) ou motores de inferência onde a equivalência semântica entre a entrada e a saída otimizada precisa ser garantida por construção.</li></ul><h3>Quando Não Usar: Evitando o Overengineering Formal</h3><p>Tentar provar formalmente cada linha de código de um sistema comercial ou escrever geradores complexos de PBT para código trivial é a receita perfeita para paralisar a entrega de produto. O pragmatismo exige reconhecer onde essas ferramentas trazem retorno decrescente ou negativo.</p><p><strong>Quando NÃO usar Property-Based Testing:</strong></p><ul><li><strong>Código “Cola” (<em>Glue Code</em>) e CRUDs Pass-Through:</strong> Endpoints que apenas recebem um payload HTTP, validam campos básicos e repassam diretamente para o banco de dados sem lógica de transformação intermediária. Testes de integração tradicionais resolvem isso com uma fração do esforço.</li><li><strong>Interfaces de Usuário (UI) e Layout Visual:</strong> Se o objetivo é verificar se um botão renderiza o texto correto ou se um modal abre ao clicar, testes baseados em exemplos (ou testes E2E pontuais) são muito mais claros e diretos.</li><li><strong>Falta de uma Propriedade Universal Clara (O “Acidente do Oráculo”):</strong> Se para testar a função 𝑓(x) com entradas aleatórias você acaba copiando e colando a própria implementação de 𝑓(x) dentro do teste para saber qual deveria ser o resultado, o seu teste de propriedade é apenas uma tautologia lenta.</li><li><strong>Integrações Pesadas de I/O Sem Isolamento:</strong> Como o PBT executa a mesma função de 100 a 10.000 vezes por teste, rodá-lo diretamente contra bancos de dados reais, sistemas de arquivos ou APIs de terceiros (sem separar o <em>core</em> funcional da camada de I/O) tornará sua esteira de CI/CD inviavelmente lenta.</li></ul><p><strong>Quando NÃO usar Theorem-Oriented Programming:</strong></p><ul><li><strong>Produtos em Fase de Descoberta (<em>Product-Market Fit</em>) e Requisitos Voláteis:</strong> Provas formais são extremamente rígidas. Se a regra de negócio muda toda semana após feedbacks de usuários, você passará 80% do tempo reescrevendo teoremas e consertando provas quebradas (<em>proof maintenance</em>) para especificações que serão descartadas na sprint seguinte.</li><li><strong>Fronteira Suja com o Mundo Real:</strong> O assistente de provas garante o que acontece <em>dentro</em> do modelo lógico, mas não pode provar que a rede não vai cair, que o usuário não vai fechar o navegador ou que uma API externa não vai mudar seu contrato JSON sem aviso prévio.</li><li><strong>Sistemas onde o Custo do Bug é Menor que o Custo da Prova:</strong> Se um bug em um relatório secundário pode ser corrigido em 10 minutos com um <em>hotfix</em> sem perdas graves, gastar três semanas provando formalmente a ausência de falhas naquele módulo é um erro econômico de alocação de engenharia.</li><li><strong>Equipes Sem Maturidade em Modelagem Funcional e Lógica:</strong> Tentar introduzir tipos dependentes ou assistentes de prova (como Lean ou Coq) em um time que ainda acopla regras de negócio a efeitos colaterais e mutação global gerará atrito severo. O caminho saudável é progressivo: primeiro isole o domínio com funções puras, depois adote <em>Property-Based Testing</em> e, por fim, aplique verificação formal cirurgicamente onde o risco justificar.</li></ul><h3>Conclusão</h3><p>Erradicar bugs não é uma questão de escrever <em>mais</em> testes unitários repetitivos, mas de elevar a expressividade das nossas garantias. O ciclo contínuo de <strong>Programação </strong><em>⟶</em><strong> Validação </strong><em>⟶</em><strong> Correção</strong> só atinge seu potencial máximo quando a etapa de validação é mais inteligente do que as suposições iniciais do programador.</p><p>Adotar <strong>Property-Based Testing</strong> é o primeiro grande salto de maturidade: ele força você a parar de pensar em valores isolados e começar a pensar no domínio matemático, nas simetrias e nas invariantes do seu sistema. Já o <strong>Theorem-Oriented Programming</strong> representa a fronteira final da engenharia de software — onde a distinção entre especificação, código e teste desaparece, e o compilador deixa de ser um mero tradutor de sintaxe para se tornar um parceiro matemático que garante a solidez inabalável do que construímos.</p><p>Você não precisa reescrever toda a sua base de código em Lean ou Coq amanhã. Mas da próxima vez que for escrever um teste unitário com valores mágicos escolhidos a dedo, faça uma pausa e pergunte a si mesmo: <em>“Qual é a propriedade universal que este código deve respeitar, não importa o que o mundo jogue contra ele?”</em> Essa simples mudança de mentalidade é o que separa o código que funciona por acaso do software projetado para não falhar.</p><h3>Referências</h3><ul><li><strong>Claessen, K., &amp; Hughes, J. (2000).</strong> <em>QuickCheck: a lightweight tool for random testing of Haskell programs.</em> ACM SIGPLAN Notices (ICFP ‘00), 35(9), 268–279. (O artigo seminal que introduziu Property-Based Testing e geração orientada a tipos).</li><li><strong>MacIver, D. R., Hatfield-Dodds, Z., et al. (2019).</strong> <em>Hypothesis: A new approach to property-based testing.</em> Journal of Open Source Software, 4(43), 1891. (Detalha o algoritmo moderno de <em>shrinking</em> integrado à geração de dados).</li><li><strong>Wadler, P. (2015).</strong> <em>Propositions as Types.</em> Communications of the ACM, 58(12), 75–84. (Uma excelente revisão histórica e técnica sobre o Isomorfismo de Curry-Howard e como a lógica se conecta à computação).</li><li><strong>Moura, L. de, &amp; Ullrich, S. (2021).</strong> <em>The Lean 4 Theorem Prover and Programming Language.</em> Automated Deduction — CADE 28, Lecture Notes in Computer Science, vol 12699. Springer. (Apresenta a arquitetura do Lean 4 como linguagem de programação de propósito geral e assistente de provas).</li><li><strong>Pierce, B. C., et al. (2024).</strong> <em>Software Foundations (Volumes 1–6).</em> Electronic textbook. Disponível em: [https://softwarefoundations.cis.upenn.edu/](https://softwarefoundations.cis.upenn.edu/). (A principal referência prática sobre verificação formal, provas indutivas e <em>QuickChick</em>).</li><li><strong>Leino, K. R. M. (2023).</strong> <em>Program Proofs.</em> MIT Press. (Aborda a transição prática do código imperativo/funcional para programas verificados usando invariantes de loop e SMT solvers).</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aa24dc7740a6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/tipos-s%C3%A3o-teoremas-programas-s%C3%A3o-provas-aa24dc7740a6">Tipos são Teoremas, Programas são Provas</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Engenharia de Software Agêntica]]></title>
            <link>https://medium.com/data-hackers/engenharia-de-software-ag%C3%AAntica-3f07d1114239?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/3f07d1114239</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[agentic-software-engineer]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 20:28:41 GMT</pubDate>
            <atom:updated>2026-10-26T14:39:21.428Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>Por que a transição de ferramentas de autocomplete para <em>Agent-as-a-Service</em> não é apenas uma melhoria de produtividade, mas a reestruturação da própria definição de software.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*deOYmYmuWP1_N8QBXFs86w.jpeg" /></figure><p>Por mais de cinco décadas a engenharia de software operou sobre uma premissa imutável: <strong>humanos decompõem problemas, traduzem regras de decisão em código-fonte estático e realizam a manutenção manual desse código à medida que o mundo muda</strong>.</p><p>Ao longo dos anos, refinamos os processos: migramos do <em>Waterfall</em> para o <em>Agile</em>, de monólitos para microsserviços, de deploys manuais para pipelines automatizados de CI/CD. Contudo, a mecânica fundamental permaneceu intocada: o código-fonte sempre foi o repositório perene das decisões tomadas por um desenvolvedor.</p><p>A popularização que aconteceu nos ultimos anos dos agentes autônomos baseados em Large Language Models (LLMs) não é apenas mais uma iteração de ferramentas como autocompletar código no VS Code. Estamos diante do nascimento da <strong>Engenharia de Software Agêntica ou <em>Agentic Software Engineering</em> (ASE). </strong>Um novo paradigma onde o código deixa de ser o sistema e passa a ser apenas um <strong>instrumento efêmero de raciocínio</strong>.</p><h3>1. O Teto da Complexidade Essencial</h3><p>No clássico ensaio <em>The Mythical Man-Month</em>, Fred Brooks dividiu a complexidade dos sistemas em duas vertentes: <strong>complexidade acidental </strong>e <strong>complexidade essencial. </strong>A <strong>complexidade acidental</strong> são as dificuldades impostas pelas linguagens, compiladores e infraestrutura. Já a <strong>complexidade essencial</strong>, são as dificuldades inerentes à própria natureza do problema a ser resolvido.</p><p>Nas últimas décadas, a indústria eliminou vastas camadas de complexidade acidental. Porém, a complexidade essencial permaneceu um gargalo intratável: em um sistema com <em>n</em> componentes com possíveis interações mútuas, o espaço de estados e topologias possíveis escala a uma ordem assintótica de <em>Θ(2^n²)</em>. Enquanto isso, <strong>a capacidade cognitiva do cérebro humano para processar essas interações permanece constante (<em>O(1)</em>)</strong>.</p><p>Tradicionalmente, respondemos a isso através de decomposição hierárquica, interfaces modulares e encapsulamento. Essas técnicas reduzem o fator constante, mas não mudam a curva assintótica.</p><p>Formalmente, um sistema tradicional de software pode ser definido por uma tupla <em>S = (C, D, E)</em>:</p><ul><li><strong>C</strong>: Recursos computacionais (CPU, memória, I/O).</li><li><strong>D</strong>: Regras de decisão determinísticas codificadas no código-fonte.</li><li><strong>E</strong>: Ambiente de execução.</li></ul><p>O gargalo fundamental está no conjunto <em>D</em>: <strong>toda a lógica precisa ser concebida e gravada estaticamente por humanos antes de qualquer execução</strong>. Qualquer mudança ambiental ou novo caso de borda exige que um humano compreenda o sistema, localize <em>D</em>, altere a lógica sem introduzir regressões e revalide todo o artefato.</p><h3>2. O Colapso do Intermediário: de “AI → Software” para “Agent → Result”</h3><p>A fase inicial da IA no desenvolvimento focou em aceleração pontual: usar LLMs para gerar trechos de código mais rápido dentro do ciclo clássico de vida do software. Denotamos isso como o pipeline:</p><blockquote>IA ⟶ Software (Código Estático) ⟶ Resultado</blockquote><p>Essa abordagem carrega três fraquezas estruturais:</p><ol><li><strong>Gargalo Humano Contínuo:</strong> O engenheiro continua no caminho crítico para cada decisão de arquitetura, merge e testes.</li><li><strong>Teto de Complexidade Intacto:</strong> O produto final ainda é um repositório rígido de código <em>D</em>, <strong>cuja manutenção se torna paradoxalmente mais cara pelo volume de código gerado por IA</strong>.</li><li><strong>Latência de Iteração:</strong> O fluxo completo (requisito ⟶ design ⟶ código ⟶ teste ⟶ deploy) <strong>continua limitado à velocidade de comunicação humana.</strong></li></ol><h4>O Novo Fluxo: O Agente é o Software</h4><p>A engenharia agêntica colapsa a distinção entre o software e sua execução:</p><blockquote>Agente ⟶ Resultado</blockquote><p>Em vez de construir um sistema estático intermediário, o agente opera como um sistema dinâmico unificado, formalizado pela tupla <em>A = (M, </em>𝓣<em>, </em>ℳ<em>, </em>Π<em>)</em>:</p><ul><li><strong>M</strong>: Modelo de linguagem atuando como motor central de raciocínio.</li><li><strong>𝓣:</strong> Conjunto de ferramentas executáveis (APIs, interpretadores, bases de dados).</li><li><strong>ℳ:</strong> Subsistema de memória (contexto de sessão, vetores de longo prazo e procedural).</li><li>Π <strong>(Pi)</strong>: Mecanismo de planejamento autônomo.</li></ul><blockquote>Em um sistema agêntico, o código gerado em tempo de execução não é o produto final — é apenas um artefato transitório gerado para resolver um subproblema e descartado em seguida.</blockquote><p>O agente escreve um script para consultar um endpoint, parsear um dado, validar uma hipótese, extrai o resultado necessário e descarta o script. <strong>O valor duradouro está na capacidade de orquestração e resolução de problemas, não nas linhas de código intermediárias.</strong></p><h3>3. Da Licença ao Agent-as-a-Service (AaaS)</h3><p>A história da entrega de software é marcada pela transferência contínua de complexidade para longe do usuário final, seguindo o padrão de Geração ⟶ Mecanismo Central ⟶ Proprietário da Complexidade ⟶ Modelo de Negócio ⟶ Exemplos:</p><ul><li><strong>Software 1.0 (Local) </strong>⟶ Código + dados locais ⟶ <strong>Usuário Final</strong> responsável pela instalação, manutenção, patches ⟶ Venda de Licença ⟶ Microsoft, Oracle clássicos</li><li><strong>Software 2.0 (SaaS) </strong>⟶ Código + dados em nuvem ⟶ <strong>Provedor </strong>responsável pela infraestrutura, escalabilidade, atualizações<strong> </strong>⟶ Assinatura (MRR/ARR)<strong> </strong>⟶ Salesforce, AWS</li><li><strong>Software 3.0 (AaaS) </strong>⟶ Agentes operando de ponta a ponta na nuvem ⟶ <strong>O Agente</strong> é responsável pelo entendimento, planejamento e execução ⟶ Baseado em Resultados (<em>Outcome-based</em>) ⟶OpenAI, Anthropic, ecossistemas ASE</li></ul><p>O SaaS liberou os negócios da gestão física de servidores. O modelo <strong>Agent-as-a-Service (AaaS)</strong> os liberta da necessidade de especificar minuciosamente o <em>como</em> computacional: basta articular claramente o <em>o quê</em> (a intenção).</p><h3>4. O Choque de Realidade: A Falha na Evolução Contínua</h3><p>Apesar do entusiasmo, a transição para a autonomia completa não ocorre sem atritos. A diferença entre tarefas pontuais e sistemas contínuos ficou evidente com o benchmark <strong>EvoClaw</strong> (2026):</p><ul><li>Em tarefas pontuais e isoladas de resolução de problemas (como no SWE-bench Verified), agentes conseguem taxas de sucesso elevadas (&gt; 80%).</li><li>Quando submetidos a cenários de <strong>evolução contínua</strong> — onde o agente precisa aplicar mudanças ao longo de um histórico progressivo de commits sem quebrar invariantes de longo prazo — , <strong>a taxa de sucesso desaba para no máximo 38%</strong> (uma queda de mais de 50%).</li></ul><h4>Os 4 Desafios Centrais da ASE:</h4><ol><li><strong>Deriva de Contexto (<em>Context Drift</em>):</strong> À medida que o histórico de alterações cresce, o agente perde a visão sistêmica das invariantes globais da aplicação.</li><li><strong>Propagação de Falhas em Cascata:</strong> Pequenos erros introduzidos em iterações iniciais acumulam-se de forma multiplicativa em commits posteriores.</li><li><strong>Débito Técnico:</strong> Agentes otimizam para o sucesso do teste imediato, sem avaliar a longo prazo o acoplamento ou a manutenibilidade do design gerado.</li><li><strong>Fidelidade de Verificação:</strong> A suíte de testes raramente é completa; agentes frequentemente produzem soluções que passam nos testes existentes, mas introduzem erros semânticos sutis em situações não testadas.</li></ol><h3>5. A Nova Função do Engenheiro: De Digitador a Arquiteto de Intenção</h3><p>A ascensão da engenharia agêntica não extingue o engenheiro; ela desloca o centro de gravidade da disciplina:</p><ul><li><strong>Engenharia de Intenção (<em>Intent Architecture</em>):</strong> Capacidade de articular requisitos, restrições e casos de borda com precisão suficiente para que enxames agênticos operem sem alucinações ou desvios.</li><li><strong>Orquestração Multi-Agente:</strong> Desenhar fluxos onde papéis especializados (Product Manager, Arquiteto, Engenheiro, Auditor de Qualidade) colaboram usando memória procedural compartilhada.</li><li><strong>Desenvolvimento Guiado por Avaliação (<em>Eval-Driven Engineering</em>):</strong> Projetar os frameworks de teste, observabilidade e pontuação semântica necessários para guiar a autocorreção dos agentes.</li><li><strong>Governança Ética e Segurança:</strong> Assegurar que os artefatos e desfechos respeitem diretrizes corporativas, conformidade regulatória e boas práticas de arquitetura.</li></ul><h3>Conclusão</h3><p>A emergência dos agentes de inteligência artificial não representa um atalho para desenvolvedores digitarem mais linhas de código por hora, mas uma reestruturação de primeiros princípios sobre o que o próprio software significa.</p><p>Ao desacoplar a resolução de problemas dos limites cognitivos humanos e transferir a geração da lógica de decisão para tempo de execução, a engenharia de software expande substancialmente sua fronteira do que é economicamente viável construir e manter.</p><p>Embora o desafio da evolução contínua e da preservação de integridade sistêmica mostre que a autonomia irrestrita ainda demanda maturação, o papel do desenvolvedor já está sendo redefinido: o futuro não pertence a quem digita código mais rápido, mas a quem sabe orquestrar agentes e desenhar intenções com rigor arquitetural.</p><h3>Referências</h3><ul><li><strong>Cao, Z. (2026).</strong> <em>Agentic Software: How AI Agents Are Restructuring the Software Paradigm</em>. arXiv preprint arXiv:2606.05608v2 [cs.SE].</li><li><strong>Brooks, F. P. (1975).</strong> <em>The Mythical Man-Month: Essays on Software Engineering</em>. Addison-Wesley.</li><li><strong>Deng, G. et al. (2026).</strong> <em>EvoClaw: Evaluating AI Agents on Continuous Software Evolution</em>. arXiv preprint arXiv:2603.13428.</li><li><strong>Karpathy, A. (2017).</strong> <em>Software 2.0</em>. Medium.</li><li><strong>Kumar, R., &amp; Ramagopal, P. (2026).</strong> <em>Agentic Engineering: How Swarms of AI Agents Are Redefining Software Engineering</em>. LangChain Blog.</li><li><strong>Wang, Y. et al. (2024).</strong> <em>Agents in Software Engineering: Survey, Landscape, and Vision</em>. arXiv preprint arXiv:2409.09030.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3f07d1114239" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/engenharia-de-software-ag%C3%AAntica-3f07d1114239">Engenharia de Software Agêntica</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Extraindo o máximo de performance do Python]]></title>
            <link>https://medium.com/data-hackers/extraindo-o-m%C3%A1ximo-de-performance-do-python-1f70b1f1a789?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/1f70b1f1a789</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Fri, 18 Sep 2026 13:48:18 GMT</pubDate>
            <atom:updated>2026-10-16T20:35:56.082Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/675/0*BGWQrwjuY284gNpn.jpg" /></figure><p>Há algumas semanas, conheci e comecei a ler sobre <strong>High Performance Computing (HPC)</strong> ou <strong>Computação de Alta Performance</strong>. No seu sentido tradicional, HPC refere-se ao uso de supercomputadores e clusters de processamento paralelo para resolver problemas computacionais extremamente complexos e intensivos em dados. Suas aplicações vão desde simulações astrofísicas e previsão meteorológica até modelagem molecular na medicina e treinamento de grandes modelos de inteligência artificial.</p><p>Inspirado por esse conceito, decidi pesquisar como trazer essa mesma mentalidade de ganho extremo de desempenho para o meu contexto do dia a dia: <strong>o de um cientista de dados que utiliza Python como principal ferramenta</strong>.</p><p>Apesar de Python ser mundialmente aclamado por sua simplicidade e expressividade, a linguagem traz o estigma histórico de ser “lenta” (que na verdade é skill issue). E encontrei duas obras fundamentais que provam o contrário e abordam o tema por ângulos complementares: os livros de <strong>Tiago Rodrigues [1]</strong> e de <strong>Micha Gorelick &amp; Ian Ozsvald [2]</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pEdSOHc_6tHD76PYx9Eafg.png" /><figcaption>Manning/O’reilly | Reprodução</figcaption></figure><p>Neste artigo, vamos explorar como extrair o desempenho máximo do Python dividindo a análise nas seguintes partes:</p><ol><li><strong>Otimização de performance do ponto de vista do hardware</strong> (<em>Micha Gorelick &amp; Ian Ozsvald</em>)</li><li><strong>Otimização de performance do ponto de vista do código e dados</strong> (<em>Tiago Rodrigues</em>)</li><li><strong>Os quatro pontos de interseção entre as duas obras</strong></li><li><strong>Matriz de divergência e escopo prático</strong></li><li><strong>Quando usar cada abordagem</strong></li><li><strong>Quando NÃO usar (cuidados e armadilhas)</strong></li><li><strong>Conclusão</strong></li></ol><h3>1. Otimização sob a Ótica do Hardware (Gorelick &amp; Ozsvald)</h3><p>A filosofia de Gorelick e Ozsvald baseia-se na premissa de que <strong>não é possível otimizar o que não se compreende no nível físico</strong>. Para extrair o desempenho máximo do Python, o desenvolvedor precisa enxergar o código não como uma abstração de alto nível, mas como instruções que interagem diretamente com a arquitetura do computador — CPU, hierarquia de memória e sistema operacional.</p><p>Abaixo estão os pilares centrais e ensinamentos práticos extraídos da obra:</p><h4>A. Compreensão da Arquitetura de Hardware e do Interpretador</h4><ul><li><strong>Entendimento dos Gargalos de CPU e Memória:</strong> O livro ensina a identificar se um código é <em>CPU-bound</em> (limitado pela taxa de clock e número de ciclos da CPU) ou <em>I/O-bound / Memory-bound</em> (limitado pela velocidade de transferência do barramento de memória, rede ou disco).</li><li><strong>Hierarquia de Caches (L1/L2/L3):</strong> Explica como o acesso aos registradores e caches da CPU é ordens de grandeza mais rápido do que a leitura da memória RAM. A ordenação eficiente de dados na memória garante o aproveitamento do <em>cache line</em> e previne o fenômeno de <em>cache miss</em>.</li><li><strong>Custo dos Objetos em CPython:</strong> Em CPython, qualquer tipo simples (como um inteiro int) não é um número puro, mas uma estrutura em C (PyObject) cheia de metadados, ponteiros e contadores de referência. Esse <em>overhead</em> reduz drasticamente a eficiência do uso do cache da CPU.</li></ul><h4>B. Profiling Rigoroso Antes de Qualquer Otimização</h4><ul><li><strong>Regra de Ouro (Medir, Não Adivinhar):</strong> A intuição humana costuma falhar em identificar onde o código gasta mais tempo. Otimizar sem medir resulta em código complexo sem ganhos reais.</li><li><strong>Medição de Tempo (CPU Profiling):</strong></li></ul><ol><li>cProfile: Mapeia o tempo de execução e a quantidade de chamadas por função, ideal para encontrar os grandes &quot;gargalos&quot; do sistema.</li><li>line_profiler: Executa uma análise linha por linha dentro das funções críticas, revelando exatamente quais laços ou instruções consomem mais ciclos de clock.</li></ol><ul><li><strong>Medição de Memória (Memory Profiling):</strong></li></ul><ol><li>memory_profiler: Monitora o uso e a alocação de RAM linha por linha no decorrer do tempo.</li><li>tracemalloc: Identifica vazamentos de memória (<em>memory leaks</em>) e mapeia quais objetos Python estão ocupando o <em>heap</em>.</li></ol><h4>C. Eficiência de Memória e Estruturas de Dados</h4><ul><li><strong>Uso de </strong><strong>__slots__ em Classes:</strong> Ao definir __slots__ em uma classe Python, descarta-se o dicionário interno dinâmico (__dict__) associado a cada instância. Isso reduz drasticamente a pegada de memória ao instanciar milhões de objetos.</li><li><strong>Vetorização com Arrays Nativos e NumPy:</strong> Substituir listas do Python por <em>arrays</em> do módulo array nativo ou matrizes do NumPy. Essas estruturas armazenam os dados em blocos de memória contíguos em formato C, eliminando o <em>overhead</em> do PyObject e permitindo instruções SIMD (<em>Single Instruction, Multiple Data</em>).</li><li><strong>Uso de Geradores e Iteradores:</strong> Preferir geradores (yield) e iteradores a listas completas quando o processamento for sequencial, garantindo consumo de memória $O(1)$ em vez de $O(N)$.</li></ul><h4>D. Compilação para Código de Máquina Nativo</h4><ul><li><strong>Cython (Compilação Estática):</strong> Traduz o código Python anotado com tipos estáticos (C-types) para código C nativo. Ao eliminar as chamadas dinâmicas do interpretador CPython dentro de laços críticos, atinge-se velocidades idênticas às de C/C++.</li><li><strong>Numba (Compilação JIT — Just-In-Time):</strong> Utiliza a infraestrutura LLVM para compilar funções Python e NumPy puras em código de máquina otimizado durante a execução, apenas decorando a função com @jit(nopython=True).</li><li><strong>PyPy (Interpretador Alternativo):</strong> Substituição direta da máquina virtual CPython pelo PyPy, que conta com um compilador JIT embutido, capaz de acelerar rotinas genéricas sem necessidade de reescrita de código.</li></ul><h4>E. Concorrência e Paralelismo para Contornar o GIL</h4><ul><li><strong>O Impacto do GIL (<em>Global Interpreter Lock</em>):</strong> O livro detalha como o GIL impede que múltiplas threads executem bytecode Python simultaneamente em múltiplos núcleos de CPU.</li><li><strong>Multiprocessing para CPU-Bound:</strong> Utilização do módulo multiprocessing (or ProcessPoolExecutor) para instanciar múltiplos processos CPython independentes, contornando o GIL e distribuindo a carga de trabalho entre todos os núcleos físicos da CPU.</li><li><strong>Threading e AsyncIO para I/O-Bound:</strong> Utilização de threading ou concorrência assíncrona baseada em loop de eventos (asyncio) para manipular dezenas de milhares de requisições de rede ou operações de disco simultâneas, liberando a CPU enquanto espera a resposta dos dispositivos de entrada/saída.</li></ul><h3>2. Otimização sob a Ótica do Código e Datasets (Tiago Rodrigues)</h3><p>A perspectiva de Tiago Rodrigues parte de uma realidade prática enfrentada diariamente por cientistas e engenheiros de dados: <strong>o volume dos dados cresceu exponencialmente, mas a capacidade da memória RAM das máquinas locais continuou limitada</strong>. Para Rodrigues, a solução para a lentidão do Python não é tentar otimizar loops manualmente ou compilar pequenos trechos de funções, mas sim <strong>remodelar a arquitetura do pipeline de dados</strong>, delegando a execução pesada para motores modernos altamente eficientes.</p><p>Alguns ensinamentos fundamentais da obra são:</p><h4>A. Troca de Motores de Processamento (Substituição Inteligente)</h4><ul><li><strong>Adoção do Polars e Apache Arrow:</strong> Substituição do tradicional Pandas pelo <strong>Polars</strong> (escrito em Rust e construído sobre o ecossistema Apache Arrow). O Polars utiliza formatos de memória colunar sem cópia (<em>zero-copy</em>), multithreading nativo por padrão e elimina a sobrecarga da GIL para operações de manipulação de tabelas.</li><li><strong>DuckDB como Motor OLAP Embutido:</strong> Uso do <strong>DuckDB</strong> como um banco de dados analítico vetorial que roda dentro do próprio processo Python. Ele permite executar consultas SQL de altíssima performance diretamente sobre arquivos CSV, Parquet ou tabelas na memória sem necessidade de subir um servidor de banco separado.</li></ul><h4>B. Avaliação Preguiçosa (Lazy Evaluation) e Otimização de Consultas</h4><ul><li><strong>Diferença entre <em>Eager</em> e <em>Lazy Execution</em>:</strong> Enquanto o Pandas processa cada comando imediatamente na memória (<em>Eager</em>), motores modernos como Polars e DuckDB permitem construir um plano lógico de execução (<em>Lazy</em>) que só é processado quando explicitamente solicitado (.collect()).</li><li><strong>Pushdown de Predicados e Projeções:</strong> Ao usar <em>Lazy Evaluation</em>, o motor inspeciona a consulta inteira antes de ler os dados. Se você precisa apenas de 3 colunas e filtrar por um estado específico em um arquivo de 50 GB, o motor faz o <em>Projection Pushdown</em> (lê apenas as 3 colunas necessárias) e o <em>Predicate Pushdown</em> (filtra as linhas na etapa de leitura no disco), evitando carregar gigabytes inúteis na RAM.</li></ul><h4>C. Estratégias Out-of-Core e Batching para Datasets Gigantes</h4><ul><li><strong>Superação do Limite Físico da RAM:</strong> Em vez de tentar carregar um dataset de 100 GB inteiro na memória de uma máquina de 16 GB (o que resulta em erros fatais de <em>Out-Of-Memory</em>), Rodrigues ensina técnicas para processar dados em blocos (<em>chunks</em> ou <em>batches</em>).</li><li><strong>Streaming &amp; Spill-to-Disk:</strong> Motores como DuckDB e Polars conseguem redirecionar os dados temporários excedentes para o SSD (<em>spill-to-disk</em>) quando a RAM atinge a capacidade máxima, garantindo que pipelines de terabytes rodem até o final sem travar a aplicação.</li></ul><h4>D. Formatos de Armazenamento Colunar e Compressão</h4><ul><li><strong>Abandono Definitivo do CSV/JSON:</strong> Arquivos de texto como CSV e JSON são extremamente lentos para parsing, ocupam muito espaço e exigem varredura completa da linha para ler um único campo.</li><li><strong>Adoção de Formatos Colunares Modernos (Parquet, Arrow, Feather):</strong> O livro demonstra ganhos de até <strong>10x a 50x em velocidade de leitura</strong> ao salvar dados em formatos binários colunares como o <strong>Apache Parquet</strong>. O formato colunar permite compressão agressiva por coluna (Snappy, ZSTD), preserva os tipos de dados originais e suporta leitura seletiva instantânea.</li></ul><h4>E. Computação Distribuída de Alto Nível</h4><ul><li><strong>Escalonamento Transparente com Dask e PySpark:</strong> Para volumes de dados que superam a capacidade de processamento de um único nó, Rodrigues explora o uso de frameworks distribuídos. O <strong>Dask</strong> permite estender a API do Pandas/NumPy para executar de forma totalmente transparente entre múltiplos núcleos ou clusters de máquinas locais e em nuvem.</li></ul><h3>3. Os Pontos de Interseção Entre as Abordagens</h3><p>Embora sigam filosofias distintas — uma voltada para a mecânica do interpretador e do hardware, e outra para a eficiência de pipelines de dados — , os livros de Gorelick &amp; Ozsvald e Tiago Rodrigues convergem em quatro grandes áreas temáticas. O ponto de encontro revela que, independentemente da ferramenta escolhida, <strong>a alta performance em Python sempre exige controle rígido de memória, aproveitamento de paralelismo, eliminação da interpretação pura e otimização do fluxo de I/O</strong>.</p><p>Abaixo, detalhamos ponto a ponto como os dois livros abordam e resolvem os mesmos quatro desafios fundamentais:</p><h4>A. Gestão e Otimização de Memória RAM</h4><p>A memória RAM é o gargalo mais comum tanto na execução de código algorítmico quanto no processamento de dados massivos. Ambas as obras tratam o esgotamento da RAM como a principal causa de degradação de desempenho em Python, mas atacam o problema sob primas complementares:</p><h4><strong>Foco em Volume e Limite Físico (Rodrigues):</strong></h4><ul><li><strong>Estratégia Out-of-Core:</strong> Trata a memória como um recurso finito e restrito. O foco é garantir que um dataset de 100 GB consiga ser processado em uma máquina com apenas 16 GB de RAM.</li><li><strong>Mecanismos Práticos:</strong> Ensina a aplicar <em>lazy evaluation</em> (avaliação preguiçosa), processamento por lotes (<em>batching / chunking</em>) e carregamento sob demanda através de motores colunares para garantir que a taxa de uso da RAM permaneça estável durante todo o ciclo de vida do script.</li></ul><h4><strong>Foco em Estrutura e Localidade na Memória (Gorelick &amp; Ozsvald):</strong></h4><ul><li><strong>Redução do Overhead de Estruturas:</strong> Trata a memória sob o ponto de vista da eficiência de acesso e da pegada dos objetos no <em>heap</em>. Demonstra detalhadamente o <em>overhead</em> de memória que o CPython impõe ao criar objetos dinâmicos como listas e dicionários.</li><li><strong>Mecanismos Práticos:</strong> Ensinam a utilizar __slots__ para descartar dicionários de instâncias, trocar listas genéricas por <em>arrays</em> contíguos de baixo nível (NumPy/array) e aplicar ferramentas de <em>profiling</em> (memory_profiler, tracemalloc) para detectar e mitigar <em>memory leaks</em>.</li></ul><h4>B. Paralelismo, Concorrência e Distribuição</h4><p>Tanto para acelerar cálculos matemáticos quanto para agilizar a leitura de múltiplos arquivos, extrair o máximo do hardware exige usar múltiplos núcleos de processamento e contornar a limitação histórica do <em>Global Interpreter Lock</em> (GIL) do CPython:</p><h4><strong>Paralelismo Transparente em Pipelines (Rodrigues):</strong></h4><ul><li><strong>Foco em Orquestração e Motores:</strong> Utiliza o paralelismo de alto nível integrado às próprias ferramentas do ecossistema moderno. O desenvolvedor não escreve código de gerenciamento de threads ou processos; em vez disso, utiliza bibliotecas construídas em Rust/C++ (como Polars) que vetorizam e paralelizam tarefas entre todos os núcleos de CPU automaticamente.</li><li><strong>Escala Vertical e Horizontal:</strong> Explora o uso de frameworks distribuídos como o Dask para fragmentar grafos de execução de DataFrames e distribuí-los entre múltiplos nós de um cluster na nuvem de maneira transparente.</li></ul><h4><strong>Teoria e Prática da Concorrência Geral (Gorelick &amp; Ozsvald):</strong></h4><ul><li><strong>Foco no Controle de Baixo Nível:</strong> Realizam um estudo aprofundado dos diferentes modelos de concorrência disponibilizados pelo Python, fornecendo diretrizes claras sobre qual modelo escolher com base no tipo de gargalo.</li><li><strong>Cenários Distintos:</strong></li></ul><ol><li>Para problemas <strong>CPU-bound</strong> (limitados por cálculo), ensinam a utilizar multiprocessing ou ProcessPoolExecutor para criar processos CPython isolados, contornando o GIL.</li><li>Para problemas <strong>I/O-bound</strong> (limitados por rede e disco), ensinam a implementar threading ou programação assíncrona orientada a eventos via asyncio.</li></ol><h4>C. Compilação e Otimização de Código</h4><p>Ambas as obras concordam que a interpretação pura de bytecode pelo CPython é lenta demais para tarefas intensivas. A solução em ambos os casos é substituir a interpretação dinâmica por execução compilada nativa em C, C++ ou Rust:</p><h4><strong>Delegando a Compilação para Motores de Terceiros (Rodrigues ):</strong></h4><ol><li><strong>Substituição de Ferramental:</strong> O desenvolvedor Python não precisa escrever código compilado manualmente. A otimização ocorre ao adotar ferramentas modernas (Polars, DuckDB, Arrow) cujos núcleos de execução já foram pré-compilados em linguagens de baixíssimo nível (Rust e C++).</li><li><strong>Otimização Automática de Consultas:</strong> O papel do desenvolvedor passa a ser o de construir pipelines e permitir que o compilador interno do motor (JIT ou otimizador SQL) reordene operações matematicamente para máxima eficiência.</li></ol><h4><strong>Compilação Customizada de Algoritmos (</strong>Gorelick &amp; Ozsvald<strong>):</strong></h4><ul><li><strong>Compilação Manual de Funções:</strong> Ensinam o desenvolvedor a identificar laços (<em>loops</em>) lentos através de <em>profiling</em> e compilá-los diretamente para código de máquina nativo.</li><li><strong>Ferramentas de Compilação:</strong> Dedicam capítulos completos a:</li></ul><ol><li><strong>Cython:</strong> Adiciona anotações de tipos C ao código Python para traduzi-lo e compilá-lo em C estático.</li><li><strong>Numba:</strong> Aplica compilação JIT (<em>Just-In-Time</em>) baseada em LLVM diretamente sobre funções numéricas e <em>arrays</em> do NumPy com o simples uso de decoradores (@jit).</li><li><strong>PyPy:</strong> Substitui a máquina virtual CPython pelo interpretador PyPy para obter ganhos imediatos via JIT em código Python legado sem necessidade de refatoração.</li></ol><h4>D. Armazenamento e Formatos de Dados</h4><p>A movimentação de dados entre o disco, a memória e a CPU (I/O) é frequentemente a etapa mais lenta de qualquer aplicação. Ambas as obras reconhecem a urgência de substituir formatos de texto ineficientes por estratégias binárias otimizadas:</p><h4><strong>Formatos Colunares Inteligentes no Disco (Rodrigues):</strong></h4><ul><li><strong>Revolução dos Formatos de Arquivo:</strong> Explora exaustivamente a transição dos formatos orientados a linha (como CSV e JSON) para formatos de armazenamento colunar (Parquet, ORC, Arrow).</li><li><strong>Impacto em Performance:</strong> Demonstra que a combinação de compressão por coluna (Snappy, ZSTD) com leitura seletiva reduz a carga de I/O em até 90%, acelerando brutalmente o tempo total do pipeline antes mesmo dos dados chegarem à CPU.</li></ul><h4><strong>Persistência Rápida e Comunicação Inter-Processos (</strong>Gorelick &amp; Ozsvald<strong>):</strong></h4><ul><li><strong>Persistência e Serialização de Baixo Overhead:</strong> Encaram o armazenamento sob a ótica da serialização eficiente de estruturas de dados vivas na memória para transferência rápida ou persistência temporária.</li><li><strong>Ferramentas Práticas:</strong> Analisam o custo de serialização de bibliotecas como pickle, json, msgpack e protocólos de memória compartilhada, ensinando a minimizar o tempo gasto convertendo objetos Python para bytes durante a comunicação entre múltiplos processos (IPC).</li></ul><h3>4. O Ponto Onde Divergem: Matriz de Comparação e Análise de Escopo</h3><p>Embora converjam nas grandes metas de desempenho, as duas obras divergem fundamentalmente na <strong>natureza do problema</strong>, no <strong>escopo de dados</strong> e na <strong>estratégia de solução</strong>. Compreender esse ponto de divergência é crucial para não aplicar o remédio certo para o problema errado.</p><p>A tabela abaixo sintetiza essa separação antes de detalharmos cada critério ponto a ponto:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/829/1*iRu4U9iohw_SRip5tx5XHg.png" /></figure><h4>1. Natureza do Gargalo Principal (I/O &amp; RAM vs. CPU &amp; Instruções)</h4><ul><li><strong>Rodrigues — Gargalo de Movimentação:</strong> O problema não é o processador ser lento para calcular, mas sim o sistema tentar mover dezenas ou centenas de gigabytes do disco para a RAM de uma só vez. O gargalo reside na banda de transferência de I/O, no <em>parsing</em> lento de arquivos de texto e na alocação excessiva de memória RAM.</li><li><strong>Gorelick &amp; Ozsvald — Gargalo de Processamento:</strong> O problema é o número de ciclos de clock exigidos pela CPU para interpretar cada instrução Python dentro de laços de repetição densos. O gargalo reside no custo de <em>dispatch</em> dinâmico do CPython, na checagem de tipos em tempo de execução e na incapacidade da CPU de vetorizar laços Python puros.</li></ul><h4>2. Escopo e Tamanho dos Dados (Big Data Out-of-Core vs. Fit-In-RAM)</h4><ul><li><strong>Rodrigues — Volume Desproporcional ao Hardware:</strong> O tamanho do dataset ($N$) é significativamente maior do que a capacidade física da memória RAM disponível. A aplicação precisa operar obrigatoriamente no modo <em>Out-of-Core</em>, fragmentando o problema.</li><li><strong>Gorelick &amp; Ozsvald — Alta Complexidade em Dados Ajustados:</strong> Os dados normalmente cabem com folga na memória RAM ($N \ll \text{RAM}$), mas o número de operações matemáticas executadas sobre cada elemento ($O(N²)$, $O(N³)$ ou simulações iterativas) exige velocidade computacional extrema por item.</li></ul><h4>3. Filosofia de Otimização (Troca de Abstrações vs. Reescrita e Compilação)</h4><ul><li><strong>Rodrigues — Otimização Arquitetural via Ecosistema:</strong> Não se gasta tempo reescrevendo código em C/Cython. A otimização ocorre trocando bibliotecas inteiras (ex: trocar Pandas por Polars, ou SQLite por DuckDB) e alterando a estrutura do arquivo em disco (trocar CSV por Parquet). O trabalho do desenvolvedor é ser um orquestrador de motores nativos.</li><li><strong>Gorelick &amp; Ozsvald — Otimização de Código e Recompilação:</strong> Mão na massa no código-fonte. Mede-se cada linha com <em>profilers</em>, refatora-se a lógica de algoritmos, eliminam-se alocações desnecessárias, reescrevem-se loops críticos em Cython/C e configuram-se decoradores JIT do Numba.</li></ul><h4>4. Ecossistema Tecnológico Dominante (Motores Rust/C++ vs. CPython Internals)</h4><ul><li><strong>Rodrigues — A Nova Onda da Engenharia de Dados:</strong> Amparado pela nova safra de ferramentas desenvolvidas em Rust e C++ que encapsulam o Apache Arrow. O desenvolvedor Python atua na camada de interface, usufruindo da segurança e velocidade de linguagens de sistemas sem precisar programar nelas.</li><li><strong>Gorelick &amp; Ozsvald — Ciência da Computação Clássica:</strong> Amparado nos fundamentos do interpretador CPython, estruturas de dados de baixo nível, bibliotecas C legadas e extensões diretas para a linguagem C e LLVM.</li></ul><h3>5. Guia Prático de Decisão: Quando Usar, Quando Evitar e Armadilhas de Otimização</h3><p>Compreender as diferenças teóricas entre as abordagens de Tiago Rodrigues e de Gorelick &amp; Ozsvald é apenas metade da jornada. No ambiente corporativo e acadêmico, o maior desafio é traduzir esses conceitos em decisões arquiteturais corretas, identificando exatamente quando acionar cada alavanca de desempenho e, mais importante, reconhecendo os momentos em que a tentativa de otimização se torna um desserviço ao projeto.</p><p>Saber quando <strong>não</strong> aplicar uma técnica de alta performance é tão vital quanto saber usá-la. Otimizações inadequadas introduzem complexidade desnecessária, elevam o débito técnico, dificultam a manutenção do código e, em cenários extremos, podem até tornar a aplicação mais lenta do que sua versão ingênua original.</p><h4>Quando Adotar a Estratégia de Tiago Rodrigues (Engenharia de Dados &amp; Pipelines)</h4><p>A abordagem arquitetural de Rodrigues deve ser a sua primeira escolha sempre que o gargalo do sistema estiver associado ao <strong>volume de dados, contenção de memória RAM ou sobrecarga de I/O</strong>.</p><ul><li><strong>Gargalos de Ingestão e Leitura:</strong> Se o seu script passa minutos travado logo na etapa inicial de carregamento de dados (como um pd.read_csv()), a causa raiz é o <em>parsing</em> textual e o consumo de disco. Migrar o armazenamento para <strong>Apache Parquet</strong> ou <strong>Feather</strong> e utilizar leitores colunares imediatamente reduz esse tempo para segundos.</li><li><strong>Estouro de Memória RAM (<em>Out-of-Memory</em>):</strong> Quando o volume de dados excede a capacidade física da máquina local — fazendo o sistema operacional apelar para <em>swap</em> de disco ou simplesmente quebrar com um erro de alocação — , a solução não é comprar mais RAM, mas sim reestruturar o pipeline. Aplicar <strong>Lazy Evaluation</strong> com <strong>Polars</strong> ou <strong>DuckDB</strong> permite executar queries analíticas complexas via <em>streaming</em> ou <em>spill-to-disk</em>, processando centenas de gigabytes em computadores convencionais.</li><li><strong>Modernização de Legacy Pipelines em Pandas:</strong> Se você mantém bases legadas com transformações e agregações em Pandas que demoram horas para rodar, a substituição direta pelo Polars oferece ganhos de desempenho de até 20x sem alterar a lógica de negócios, usufruindo de multithreading nativo e vetorização sem a interferência do GIL.</li></ul><h4>Quando Adotar a Estratégia de Gorelick &amp; Ozsvald (Sistemas &amp; Algoritmos de Baixo Nível)</h4><p>A abordagem focada em hardware de Gorelick &amp; Ozsvald torna-se indispensável quando a aplicação lida com <strong>lógica algorítmica pura, laços de repetição densos ou sistemas concorrentes de alta frequência</strong>.</p><ul><li><strong>Lógica Customizada e Loops Matemáticos Inevitáveis:</strong> Se o seu problema exige iterar sobre milhões de elementos executando regras de negócio condicionais que não podem ser expressas em operações matriciais simples do NumPy, o CPython puro falhará. Nesse cenário, compilar as funções críticas com <strong>Numba JIT</strong> (@jit(nopython=True)) ou reescrevê-las em <strong>Cython</strong> aproxima a velocidade de execução à da linguagem C.</li><li><strong>Gargalos de Processamento Concorrente e I/O de Rede:</strong> Quando o sistema precisa lidar com dezenas de milhares de conexões simultâneas de API ou chamadas a serviços externos, a concorrência assíncrona com asyncio e aiohttp libera a CPU para outros trabalhos enquanto aguarda o retorno das redes, maximizando o <em>throughput</em> da aplicação.</li><li><strong>Gargalos de Memória por Proliferação de Objetos:</strong> Ao instanciar milhões de pequenos objetos Python customizados (por exemplo, em simulações de grafos ou sistemas de partículas), o <em>overhead</em> do dicionário interno de cada objeto consome gigabytes. A adoção de __slots__ ou a migração para <em>structs</em> e <em>arrays</em> contíguos elimina os metadados do PyObject, reduzindo a pegada de memória em até 80%.</li></ul><h4>Quando NÃO Otimizar: Cuidados, Armadilhas e Anti-Padrões</h4><p>A tentação de aplicar ferramentas avançadas sem necessidade é uma das maiores armadilhas no desenvolvimento de software. A lista abaixo reúne os cenários em que tentar otimizar produz um efeito oposto ao desejado:</p><ul><li><strong>Otimização Precoce (A Armadilha de Medir sem Profiling):</strong> Tentar otimizar trechos de código com base na intuição é uma das formas mais eficientes de desperdiçar tempo. Se o seu <em>profiler</em> (cProfile ou line_profiler) indicar que 95% do tempo de execução é gasto em uma chamada de rede ou em uma consulta ao banco de dados, compilar rotinas internas em Cython ou reorganizar laços no Python trará ganho zero no tempo total do processo.</li><li><strong>Reescrever Operações Vetorizadas que Já São Nativas:</strong> Tentar reescrever laços de multiplicação de matrizes ou transformações estatísticas usando Cython ou C manual é um erro quando bibliotecas como <strong>NumPy</strong>, <strong>PyTorch</strong> ou <strong>Scikit-Learn</strong> já estão disponíveis. Essas ferramentas executam código nativo em C, Fortran e CUDA altamente otimizado por décadas e ajustado para instruções SIMD da CPU.</li><li><strong>Introduzir Computação Distribuída para Datasets Pequenos:</strong> Adicionar frameworks distribuídos como <strong>Dask</strong> ou <strong>PySpark</strong> para manipular tabelas de poucas centenas de megabytes é um anti-padrão comum. O custo de inicializar o cluster, serializar os dados, gerenciar o Grafo Aclíclico Dirigido (DAG) e transferir pacotes via rede (<em>overhead</em> de coordenação) fará a execução rodar consideravelmente <strong>mais lenta</strong> do que um simples script Python de processo único rodando na memória local.</li><li><strong>Complexidade de Manutenção e Débito Técnico:</strong> Adotar compilação em C via Cython ou gerenciar <em>multiprocessing</em> manualmente aumenta exponencialmente a complexidade da base de código. Exige compiladores na esteira de CI/CD, dificulta a depuração de erros e exige profissionais com conhecimento em linguagens de baixo nível para dar manutenção. Se um ganho de 10% em tempo de execução custar o dobro do tempo de desenvolvimento da equipe, a otimização não se justifica financeiramente.</li></ul><h3>Conclusão</h3><p>Extrair alta performance em Python não significa abandonar a linguagem ou migrar preventivamente todo o ecossistema para C++ ou Rust, mas sim compreender os limites físicos da máquina e saber exatamente qual “alavanca” acionar para cada perfil de gargalo.</p><p>Se o seu desafio diário está na <strong>escala de dados</strong>, no esgotamento da memória RAM e na lentidão de I/O, a arquitetura moderna de motores de alto nível, arquivos colunares e avaliação preguiçosa proposta por Tiago Rodrigues resolverá gargalos históricos de pipelines em questão de minutos.</p><p>Se o seu desafio está na <strong>eficiência computacional pura</strong>, na otimização de laços matemáticos densos ou no controle fino do interpretador, a engenharia de baixo nível, o <em>profiling</em> criterioso e a compilação customizada ensinados por Micha Gorelick e Ian Ozsvald transformarão o Python em uma linguagem de altíssima velocidade operacional.</p><p>A verdadeira mestria do Cientista e Engenheiro de Dados moderno consiste em <strong>não se prender a dogmas</strong> de ferramentas: é saber diagnosticar a origem exata do gargalo e transitar com elegância entre a engenharia de sistemas e a orquestração de dados.</p><h3>Como Citar</h3><p>Se este artigo te ajudou de alguma forma, você pode referenciá-lo como:</p><blockquote><em>Guimarães, Alysson. (Ago 2026). </em>Extraindo o máximo de performance do Python<em>. </em><a href="https://medium.com/data-hackers/p-1f70b1f1a789">https://medium.com/data-hackers/p-1f70b1f1a789</a></blockquote><p>ou</p><pre>@miscellaneous{guimaraes2026dop,<br> title = {Extraindo o máximo de performance do Python},<br> author = {Guimarães, Alysson},<br> journal = {https://medium.com/data-hackers/},<br> year = {2026},<br> month = {Jun},<br> url = {https://k3ybladewielder.medium.com/p-1f70b1f1a789}<br>}</pre><h3>Referências</h3><ul><li><strong>[1] Rodrigues, Tiago.</strong> <em>Python — High performance techniques for large datasets</em>. Manning Publications, 2023.</li><li><strong>[2] Gorelick, Micha; Ozsvald, Ian.</strong> <em>High Performance Python: Practical Performant Programming for Humans</em>. O’Reilly Media.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1f70b1f1a789" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/extraindo-o-m%C3%A1ximo-de-performance-do-python-1f70b1f1a789">Extraindo o máximo de performance do Python</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Programação Orientada a Dados]]></title>
            <link>https://medium.com/data-hackers/programa%C3%A7%C3%A3o-orientada-a-dados-140c3bdd1b0d?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/140c3bdd1b0d</guid>
            <category><![CDATA[data-oriented-programming]]></category>
            <category><![CDATA[dop]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Wed, 08 Jul 2026 12:10:52 GMT</pubDate>
            <atom:updated>2026-07-20T11:12:22.229Z</atom:updated>
            <content:encoded><![CDATA[<p>A <strong>Programação Orientada a Dados</strong>, do inglês <strong>Data-Oriented Programming (DOP) </strong>foi introduzida por <a href="https://www.linkedin.com/in/viebel/">Yehonathan Sharvit</a> no seu <a href="https://amzn.to/3PL7SbH">livro</a> homônimo como uma abordagem que representa os dados com estruturas de dados genéricas imutáveis. Com isso, simplificando o gerenciamento de estado, assim, facilitando a simultaneidade (concorrência de código) e eliminados problemas comuns encontrados na orientação a objetos com um paradigma agnóstico em relação à linguagem.</p><p>No seu livro, ele apresenta conceitos por meio de conversas e diálogos entre personagens em seu livro, também inclui trechos de código e diagramas para ajudar a entender os pontos positivos dessa abordagem.</p><p>Neste artigo, falaremos sobre a complexidade da orientação a objetos, as motivações por trás da criação do DOP e seus princípios.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/0*44OEsl3k4exIfYGl.gif" /><figcaption><a href="https://giphy.com/gifs/universalpictures-super-mario-bros-galaxy-pVOlzNqLIkPX91xQix">Giphy</a> | Reprodução</figcaption></figure><h3>Complexidade da Programação Orientada a Objetos</h3><p>Se assim como eu, você não teve um background de uma graduação em computação (sistemas, ciência, engenharia, etc) em algum momento se deparou um orientação a objetos e inicialmente achou confuso. Mas se você teve esse background ou aprendeu por meio de cursos pela internet, há uma grande chance de ter sido ensinado que o mundo é um reflexo perfeito da Orientação a Objetos (OOP).</p><p>Sob a OOP, aprendemos que um Carro herda de Veículo, que possui um motor e métodos como acelerar.(). Na teoria, essa modelagem parece intuitiva, mas na prática e em <strong>sistemas de larga escala</strong>, essa modelagem muitas vezes se transforma em um pesadelo de manutenção. À medida que os sistemas crescem, a Orientação a Objetos <strong>manifesta uma tendência crônica de aumentar a complexidade do código, tornando-o rígido, difícil de testar e, ironicamente, complexo de entender.</strong></p><p>O núcleo do problema está no princípio fundamental da OOP: <strong>o encapsulamento</strong>. Ensinam que <strong>agrupar dados (atributos) e comportamento (métodos) na mesma entidade </strong>— o objeto — é a chave para a organização. Mas, essa união entre <strong>dado</strong> e o seu <strong>comportamento</strong> cobra um custo altíssimo.</p><p>Quando misturamos código e dados, criamos <strong>entidades com estado mutável compartilhado</strong>. Imagine que um objeto Pedido possui uma lista de Itens e regras para calcular o desconto. Se múltiplos microsserviços ou threads possuem referências a esse mesmo objeto, rastrear <strong>quem, quando e por que</strong> um dado foi alterado torna-se um trabalho de detetive. <strong>O risco de efeitos colaterais indesejados (os famosos side effects) dispara, transformando a depuração em uma roleta russa de bugs intermitentes.</strong></p><p><strong>A promessa original de OOP era aproximar o código da realidade humana. O paradoxo é que ela alcançou o oposto</strong>. Entender a execução de um sistema puramente orientado a objetos <strong>exige um esforço cognitivo massivo por três motivos principais</strong>:</p><ul><li><strong>A Teia de Relações</strong>: Para entender o comportamento de uma única classe, você frequentemente precisa navegar por uma árvore genealógica complexa de herança e dezenas de interfaces de injeção de dependência. <strong>O fluxo de dados não é linear</strong>; ele se espalha por uma teia invisível de objetos interconectados.</li><li><strong>Perda da Visão Geral</strong>: Em vez de focar no que o sistema faz com os dados de entrada para gerar uma saída, o desenvolvedor é forçado a focar em como gerenciar o ciclo de vida e a comunicação de instâncias de classes.</li><li><strong>A Riqueza Artificial</strong>: Modelar o mundo real em hierarquias estritas gera abstrações forçadas. <strong>O mundo real raramente se encaixa perfeitamente em árvores de herança IS-A (É UM)</strong> sem que você precise recorrer a gambiarras de design patterns para contornar a rigidez da própria linguagem.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*hI7ccwuJmKhlxwbH" /><figcaption><a href="https://www.reddit.com/r/ProgrammerHumor/comments/ko5now/do_you_like_oop/">Reddit</a> | Reprodução</figcaption></figure><p><strong>Ao prender dados dentro de métodos, limitamos a flexibilidade</strong>. Os dados perdem sua pureza; eles não são mais apenas “fatos” sobre o sistema, mas sim <strong>reféns da implementação daquela classe específica</strong>. Se você precisar dos mesmos dados estruturados de uma forma ligeiramente diferente para outro caso de uso, a solução padrão de OOP é criar outra classe (um DTO) e um mapeador, adicionando ainda mais código boilerplate e complexidade acidental.</p><p><strong>Precisamos de uma abordagem onde os dados sejam livres, previsíveis e transparentes</strong>. É exatamente nessa rachadura da fundação da Orientação a Objetos que o Data-Oriented Programming (DOP) se apoia, propondo uma separação radical, porém libertadora: <strong>dados de um lado, funções do outro.</strong></p><p>Sob a ótica de um <strong>cientista de dados</strong> (meu caso)<strong>,</strong> e para quem lida com pipelines de dados, <strong>o valor está na imutabilidade e na transparência</strong>: queremos dados puros, previsíveis e livres de efeitos colaterais, processados por funções transparentes (<strong>funções puras</strong>).</p><p>É exatamente aqui que o <strong>Data-Oriented Programming (DOP)</strong> brilha como uma ponte pragmática. <strong>Ao herdar as vantagens da programação funcional</strong>, o DOP trata <strong>dados como fatos estáticos e transformações como fluxos lineares</strong>.</p><p>Essa previsibilidade não é apenas uma conveniência de design, mas encontra respaldo na ciência da computação teórica através do <strong>Isomorfismo de Curry-Howard</strong>. Este princípio estabelece uma <strong>equivalência profunda entre sistemas de tipagem e lógica matemática</strong>: em linguagens tipadas, <strong>um tipo (ou assinatura de função) é um teorema</strong>, enquanto o <strong>código escrito para satisfazê-lo é a prova</strong> de que o teorema é verdadeiro. <strong>Ao desacoplar os dados dos métodos, o DOP nos permite formular e provar esses teoremas sobre o fluxo de dados do sistema com uma clareza que o emaranhado de estados mutáveis da OOP jamais conseguiria oferecer.</strong></p><h3>Por que um novo paradigma</h3><p>A OOP foi consolidada em uma era onde os softwares eram, em sua maioria, monolíticos, síncronos e executados inteiramente na memória de uma única máquina (pense em sistemas desktop dos anos 90, com botões e janelas). Nesse cenário, encapsular o estado dentro de um objeto fazia sentido: o objeto nascia, mudava de estado na memória e morria ali.</p><p>O cenário atual, no entanto, é radicalmente diferente. Vivemos na era dos sistemas distribuídos e orientados a eventos. Hoje, um software passa 90% do seu tempo movendo dados de um lado para o outro: buscando JSON de uma API externa, transformando-o, salvando-o em um banco de dados relacional e postando uma mensagem em um tópico do Kafka.</p><p>Quando tentamos forçar o modelo mental da OOP nesse ecossistema, nos deparamos com três grandes barreiras motivacionais que impulsionaram a criação da DOP:</p><h4>1. O Desastre do Mapeamento (O Efeito Platô)</h4><p>Em sistemas modernos, <strong>o dado pertence ao mundo exterior, não ao programa</strong>. Quando você recebe um JSON de uma API e tenta “forçá-lo” a entrar em uma classe OOP cheia de métodos e encapsulamento, você cria uma fricção artificial. É o que a comunidade chama de <em>impedance mismatch</em> (incompatibilidade de impedância). Para remediar isso, criamos DTOs (Data Transfer Objects), entidades de ORM e conversores. Acabamos escrevendo montanhas de código <em>boilerplate</em> apenas para transformar dados puros em objetos e, logo em seguida, transformá-los de volta em JSON para enviá-los pela rede.</p><h4>2. A Ilusão do Encapsulamento em Sistemas Multithread</h4><p>A OOP protege o estado escondendo-o atrás de métodos privados. Mas em aplicações web modernas que lidam com concorrência e múltiplas threads acessando os mesmos dados simultaneamente, <strong>o estado mutável da OOP se torna perigoso</strong>. Para evitar que duas threads corrompam o mesmo objeto, os desenvolvedores precisam implementar travas complexas (<em>mutexes</em> e <em>locks</em>), o que degrada a performance e gera <em>deadlocks</em>. A DOP nasce da percepção de que <strong>se o dado for imutável, o encapsulamento se torna obsoleto</strong>. Não há nada a proteger se o dado não pode ser modificado.</p><h4>3. Rigidez vs. Dinamismo do Mundo Real</h4><p>Na OOP, a estrutura dos dados precisa ser rigidamente definida em tempo de compilação por meio de classes. Se uma API externa decide adicionar um campo novo ao payload, ou se você precisa extrair apenas três campos de um objeto complexo para gerar um relatório rápido, a OOP te força a criar novas interfaces ou heranças artificiais. A DOP foi motivada pela necessidade de flexibilidade: tratar dados através de estruturas genéricas (como mapas/dicionários e listas) permite que o sistema lide com a volatilidade dos dados do mundo real sem quebrar a cada pequena mudança de contrato.</p><h3>Princípios da Programação Orientada a Dados</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/973/1*E8FtPOU4kMKU1wJihMdkCQ.png" /><figcaption>Fonte: SHARVIT, Yehonathan. <em>Data-Oriented Programming</em>. Manning Publications, 2022.</figcaption></figure><p>Para guiar os desenvolvedores na aplicação prática desse paradigma, Yehonathan Sharvit consolidou o Data-Oriented Programming em <strong>quatro princípios fundamentais</strong>. Longe de serem regras dogmáticas, esses pilares funcionam como diretrizes arquiteturais simples e integradas: o primeiro estabelece a separação radical entre código e dados; o segundo defende a representação desses dados por meio de estruturas genéricas (como mapas e listas); o terceiro introduz a imutabilidade para garantir a segurança do estado; e o quarto separa a validação de dados da sua representação, permitindo checagens flexíveis e contextuais. <strong>Juntos, esses princípios desatam os nós da complexidade acidental da OOP, criando sistemas previsíveis, altamente testáveis e perfeitamente alinhados com o fluxo de informações do mundo real.</strong></p><h4>Princípio 1: Separar código dos dados</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/970/1*2auhU-g0RHkyS_Nil-IJrw.png" /><figcaption>Fonte: SHARVIT, Yehonathan. <em>Data-Oriented Programming</em>. Manning Publications, 2022.</figcaption></figure><p>A primeira ideia da DOP é ​​que podemos diminuir a complexidade de nossos sistemas separando o código dos dados. Quando o código é separado dos dados, nossos sistemas são compostos por duas partes principais que podem ser pensadas separadamente: <strong>entidades de dados e módulos de código</strong>.</p><p>O primeiro pilar do DOP ataca diretamente a fundação da Orientação a Objetos. Enquanto a OOP prega que dados e comportamentos devem viver sob o mesmo teto (a classe), o DOP exige uma separação: <strong>os dados devem ser apenas dados estruturados, e o código deve residir em funções puras e isoladas.</strong></p><p><strong>Na prática, isso significa que suas estruturas de dados não possuem métodos, e suas funções não guardam estado.</strong> Para entender o impacto dessa separação, vamos analisar um cenário clássico de e-commerce: o cálculo de desconto para um cliente VIP.</p><p>Em OOP, tendemos a criar uma classe Author ou Customer que envelopa as propriedades e expõe métodos para manipular ou ler esses dados. Veja como fica:</p><pre>class Customer:<br>    def __init__(self, name: str, is_vip: bool, total_spent: float):<br>        self._name = name<br>        self._is_vip = is_vip<br>        self._total_spent = total_spent<br><br>    # O comportamento está &quot;preso&quot; dentro do objeto de dados<br>    def calculate_discount(self) -&gt; float:<br>        if self._is_vip and self._total_spent &gt; 500:<br>            return 0.15  # 15% de desconto<br>        return 0.05  # 5% de desconto</pre><p>O problema nessa abordagem é que o método calculate_discount está rigidamente acoplado à classe Customer. Se amanhã você precisar calcular o desconto em um contexto de microsserviço que recebe apenas um payload JSON simplificado do banco de dados, você será obrigado a instanciar um objeto Customer artificialmente apenas para poder usar a função. <strong>Os dados viraram reféns do comportamento da classe.</strong></p><p>No DOP, quebramos esse vínculo. Os dados são representados por uma estrutura simples e passiva (como um dicionário nativo ou uma <em>dataclass</em> sem métodos). O comportamento é movido para uma função pura externa.</p><pre>from typing import TypedDict<br><br># 1. Os dados são apenas dados (uma estrutura passiva)<br>class CustomerData(TypedDict):<br>    name: str<br>    is_vip: bool<br>    total_spent: float<br><br># 2. O comportamento é uma função pura e isolada<br>def calculate_discount(customer: CustomerData) -&gt; float:<br>    if customer[&quot;is_vip&quot;] and customer[&quot;total_spent&quot;] &gt; 500:<br>        return 0.15<br>    return 0.05</pre><p>Quando separamos o código dos dados, ganhamos três superpoderes imediatos:</p><ol><li><strong>Testabilidade Isolada:</strong> Para testar a função calculate_discount, você não precisa mockar um objeto complexo, gerenciar construtores ou se preocupar com efeitos colaterais na memória. Você passa um dicionário de entrada e espera um número de saída. <strong>É determinístico.</strong></li><li><strong>Reuso de Código:</strong> A função calculate_discount agora aceita <em>qualquer</em> mapa ou dicionário que contenha as chaves is_vip e total_spent. Se os dados vierem de um banco SQL, de um NoSQL ou de um evento do Kafka, a função funcionará da mesma forma, sem necessidade de conversores de classe complexos.</li><li><strong>Redução do Custo Cognitivo:</strong> O desenvolvedor que está lendo a regra de negócio não precisa entender o ciclo de vida do objeto Customer. <strong>Ele só precisa ler uma função matemática linear: dados entram, cálculo acontece, resultado sai.</strong></li></ol><h4>Princípio 2: Represente dados com estruturas genéricas de dados</h4><p>O segundo princípio do DOP expande a liberdade que demos aos dados no primeiro pilar. Em OOP, quando precisamos representar um conceito do sistema, criamos uma classe específica para ele (uma classe Produto, uma classe CarrinhoDeCompras, etc.). No DOP, a diretriz é oposta: <strong>devemos representar dados através de estruturas genéricas e universais</strong>, como mapas (dicionários), listas (arrays) e conjuntos (<em>sets</em>).</p><p>Em vez de encapsular informações em tipos rígidos criados sob medida, usamos as estruturas que a própria linguagem já fornece nativamente.</p><p>Imagine que o seu sistema precisa lidar com os dados de um livro. Em OOP, você criaria uma classe dedicada:</p><pre>class Book:<br>    def __init__(self, title: str, author: str, isbn: str):<br>        self.title = title<br>        self.author = author<br>        self.isbn = isbn</pre><p>Se você precisar criar uma função genérica para serializar dados, inspecionar chaves ou sincronizar informações com um banco de dados, você terá que escrever um código que <strong>entenda especificamente a classe</strong> Book. <strong>Se o sistema tiver 300 classes diferentes, você precisará de adaptadores ou ferramentas complexas de reflexão (<em>reflection</em>) para lidar com a rigidez de cada uma delas.</strong></p><p><strong>No DOP, o livro é simplesmente um mapa de dados</strong>. Se estivermos usando uma linguagem com tipagem estática ou estrutural (como TypeScript ou o ecossistema de tipagem do Python), usamos definições de formato, mas a estrutura subjacente permanece genérica:</p><pre># O dado é apenas um mapa/dicionário comum<br>book = {<br>    &quot;title&quot;: &quot;Data-Oriented Programming&quot;,<br>    &quot;author&quot;: &quot;Yehonathan Sharvit&quot;,<br>    &quot;isbn&quot;: &quot;9781617298578&quot;<br>}</pre><p>Ao adotar estruturas genéricas, o seu software ganha uma flexibilidade que transforma a arquitetura:</p><ol><li><strong>Polimorfismo Genérico Inter-Sistemas:</strong> Como os dados são apenas mapas e listas, você pode usar as mesmas funções utilitárias (como filtros, mapeadores, ordenadores e serializadores) para qualquer entidade do sistema. Uma função que limpa strings nulas de um dicionário funcionará para um Book, para um Customer ou para qualquer novo payload que surgir no futuro.</li><li><strong>Evolução de Sistemas sem Fricção:</strong> Se a equipe de infraestrutura decidir que a API agora precisa enviar um campo novo chamado published_year no payload do livro, o seu código DOP não quebra. O dicionário simplesmente aceita a nova chave. Em OOP, a ausência desse atributo na definição da classe dispararia erros de desserialização, exigindo que você alterasse a classe, recompilasse e implantasse o código novamente.</li><li><strong>Casamento Perfeito com JSON e Bancos de Dados:</strong> O formato JSON, que domina a web, é fundamentalmente composto por mapas e listas. O DOP elimina a necessidade de camadas complexas de tradução. O dado que chega da rede é exatamente o dado que transita pelas suas funções puras, sem o peso de um ORM ou de mapeadores artificiais.</li></ol><h4>Princípio 3: Dados são imutáveis</h4><p>Se você disser a um desenvolvedor OOP tradicional que os dados do sistema devem ser armazenados em dicionários genéricos e globais, o primeiro sinal de alerta que ele acenderá será: <em>“Mas e se alguma função alterar esses dados por engano em outra parte do sistema?”</em>. Esse medo é totalmente legítimo na OOP, onde o estado é mutável. No DOP, contudo, esse problema é mitigado pelo terceiro princípio: <strong>os dados nunca mudam</strong>.</p><p>Em vez de modificar uma estrutura de dados existente, as funções puras do DOP criam uma <strong>nova cópia</strong> dos dados com as alterações aplicadas. Os dados originais funcionam como fatos históricos invioláveis.</p><p><strong>Em sistemas que compartilham referências de objetos mutáveis, um bug em um módulo pode corromper silenciosamente o comportamento de outro</strong>. Imagine o seguinte cenário:</p><pre># Cenário de mutação perigosa em OOP<br>class Cart:<br>    def __init__(self, items):<br>        self.items = items  # Uma lista mutável<br><br>my_cart = Cart([&quot;Livro DOP&quot;, &quot;Caneca&quot;])<br><br># Uma função de auditoria ou log recebe o carrinho e altera a lista por engano<br>def log_cart_contents(cart):<br>    # Efeito colateral: remove um item permanentemente sem o sistema saber<br>    cart.items.pop() <br><br>log_cart_contents(my_cart)<br>print(my_cart.items)  # Saída: [&#39;Livro DOP&#39;] -&gt; O dado foi corrompido!</pre><p>Em sistemas assíncronos ou multithread, esse tipo de mutação gera condições de corrida (<em>race conditions</em>) estranhas e difíceis de reproduzir em ambiente de teste.</p><p><strong>No DOP, os dados são tratados como registros imutáveis.</strong> Se precisamos atualizar o carrinho de compras,<strong> geramos um novo estado a partir do anterior</strong>. Muitas linguagens possuem suporte nativo a estruturas imutáveis (como as tuplas do Python ou objetos congelados), mas o conceito se aplica mesmo ao usar dicionários comuns, garantindo cópias estruturais superficiais (<em>shallow copies</em>):</p><pre># O dado original é tratado como um fato imutável<br>cart = {<br>    &quot;items&quot;: [&quot;Livro DOP&quot;, &quot;Caneca&quot;],<br>    &quot;total&quot;: 120.00<br>}<br><br># Em vez de modificar o &#39;cart&#39; existente, criamos um NOVO &#39;cart&#39;<br>def add_item(current_cart, new_item, price):<br>    return {<br>        **current_cart,<br>        &quot;items&quot;: current_cart[&quot;items&quot;] + [new_item],<br>        &quot;total&quot;: current_cart[&quot;total&quot;] + price<br>    }<br><br>new_cart = add_item(cart, &quot;Caderno&quot;, 15.00)<br><br>print(cart[&quot;items&quot;])     # Saída: [&#39;Livro DOP&#39;, &#39;Caneca&#39;] (Intacto!)<br>print(new_cart[&quot;items&quot;]) # Saída: [&#39;Livro DOP&#39;, &#39;Caneca&#39;, &#39;Caderno&#39;] (Novo estado)</pre><p>Por que a Imutabilidade é Libertadora?</p><ol><li><strong>Livre de Efeitos Colaterais (Side Effects):</strong> Você pode passar suas estruturas de dados para qualquer função, biblioteca de terceiros ou microsserviço sem medo. Ninguém pode alterar o seu estado local por acidente.</li><li><strong>Concorrência Segura (Thread Safety):</strong> Como os dados não mudam, múltiplas threads ou processos podem ler o mesmo dicionário simultaneamente sem a necessidade de implementar travas complexas (<em>locks</em> ou <em>mutexes</em>), aumentando drasticamente a performance e a escalabilidade do sistema.</li><li><strong>Rastreabilidade de Estado (Time-Travel Debugging):</strong> Como cada transformação gera um novo “print” dos dados, você consegue armazenar ou logar o histórico exato de transição de estados do sistema. Isso torna a depuração de fluxos complexos algo trivial.</li></ol><blockquote><strong><em>Nota de Performance:</em></strong><em> Desenvolvedores costumam se preocupar com o custo de memória ao copiar dados continuamente. Na prática, linguagens puramente funcionais utilizam estruturas de dados persistentes com </em>compartilhamento estrutural<em> (apontando para referências comuns na memória para partes não alteradas), o que torna essas operações extremamente eficientes e baratas.</em></blockquote><h4>Princípio 4: Separe os esquemas de dados da representação de dados</h4><p>Na Orientação a Objetos tradicional, a validação de dados está intrinsecamente ligada à criação do objeto. O construtor da classe atua como um guardião: se você tentar instanciar um objeto Usuario com um e-mail inválido, a classe lança uma exceção e impede a existência do objeto. No DOP, a abordagem é radicalmente diferente: <strong>os dados são aceitos em sua forma pura e flexível, e a validação é executada de forma isolada, apenas quando e onde for necessária.</strong></p><p>Em vez de exigir que os dados sejam válidos para sempre e em qualquer lugar, o DOP entende que a “validade” de um dado é contextual.</p><p>Considere o seguinte cenário em OOP: para cadastrar um usuário no banco de dados, o e-mail e a senha são obrigatórios. Mas para uma tela de “Esqueci minha senha”, você só precisa do e-mail. Se a validação estiver presa no construtor da classe User, você terá problemas:</p><pre>class User:<br>    def __init__(self, email: str, password: str):<br>        if not email or &quot;@&quot; not in email:<br>            raise ValueError(&quot;E-mail inválido&quot;)<br>        if not password or len(password) &lt; 6:<br>            raise ValueError(&quot;Senha muito curta&quot;)<br>        self.email = email<br>        self.password = password<br><br># Na tela de &quot;Esqueci minha senha&quot;, onde não há password disponível:<br># Você é forçado a passar uma string vazia ou um mock para burlar a própria classe.<br>invalid_context_user = User(&quot;usuario@email.com&quot;, &quot;&quot;) # Dispara ValueError!</pre><p>Para resolver isso em OOP, os desenvolvedores criam heranças complexas, múltiplas interfaces ou uma infinidade de DTOs específicos para cada etapa do fluxo.</p><p><strong>No DOP, os dados trafegam livremente pelo sistema como mapas genéricos.</strong> Quando um módulo específico (como o controlador de uma API ou um serviço de persistência) precisa garantir que os dados seguem um formato específico, ele aplica um <strong>esquema de validação externo</strong> (usando ferramentas como JSON Schema, Zod em TypeScript, ou Pydantic/Schematics em Python).</p><pre>from typing import Dict<br><br># 1. OS DADOS SÃO APENAS FATOS (Sem comportamento ou travas rígidas)<br># O payload transita como um dicionário nativo e puramente passivo.<br># Ele não está &quot;completo&quot; para um cadastro, mas isso não impede sua existência no sistema.<br>user_data = {&quot;email&quot;: &quot;alysson@email.com&quot;} <br><br><br># 2. VALIDAÇÃO CONTEXTUAL A (Fluxo de Recuperação de Senha)<br># Para este caso de uso, o dicionário acima é perfeitamente válido, <br># pois precisamos apenas de um e-mail estruturado.<br>def validate_for_password_recovery(data: Dict) -&gt; bool:<br>    return &quot;email&quot; in data and &quot;@&quot; in data[&quot;email&quot;]<br><br><br># 3. VALIDAÇÃO CONTEXTUAL B (Fluxo de Cadastro de Novo Usuário)<br># Para este outro cenário, o mesmo dicionário seria considerado inválido.<br># A regra de integridade pertence ao processo de negócio, não à estrutura do dado.<br>def validate_for_registration(data: Dict) -&gt; bool:<br>    return &quot;email&quot; in data and &quot;password&quot; in data and len(data[&quot;password&quot;]) &gt;= 6<br><br><br># 4. A VALIDAÇÃO ACONTECE &quot;À LA CARTE&quot;<br># Em vez de estourar uma exceção no construtor de uma classe, o sistema interroga<br># o dado baseando-se estritamente no momento atual da aplicação.<br>if validate_for_password_recovery(user_data):<br>    print(&quot;Dados válidos para recuperar senha!&quot;)<br>    <br># Se tentássemos validar para registro, falharia pacificamente (retornando False),<br># permitindo tratar o fluxo sem capturar exceptions complexas de infraestrutura.</pre><p>As Vantagens da Validação Contextual:</p><ol><li><strong>Flexibilidade no Fluxo de Dados:</strong> Um payload de dados pode estar “incompleto” no início de um pipeline (por exemplo, enquanto passa por etapas de enriquecimento de dados) e só se tornar totalmente preenchido no final. O DOP permite que esses dados transitem livremente sem quebrar o sistema no meio do caminho.</li><li><strong>Tratamento de Erros Amigável:</strong> Como a validação não interrompe a criação do dado por meio de exceções abruptas no construtor, você pode validar um formulário inteiro, coletar todos os erros contextuais e devolvê-los de uma só vez para o usuário de forma limpa.</li><li><strong>Múltiplas Visões sobre o Mesmo Dado:</strong> O mesmo dicionário de dados pode ser considerado válido para ser salvo em um log de auditoria, mas inválido para processar um pagamento. Separar a validação permite que cada microsserviço ou módulo defina suas próprias réguas de integridade sem interferir no modelo de dados global.</li></ol><h3>Quando usar e quando <strong>não</strong> usar DOP</h3><p>Depois de entender os quatro princípios, é fácil se empolgar e querer refatorar todo o código da sua empresa amanhã de manhã. Contudo,<strong> o Data-Oriented Programming não é uma bala de prata</strong>. Ele foi desenhado para resolver problemas específicos da engenharia de software moderna. Escolher entre DOP e OOP (ou qualquer outra abordagem) é uma questão de <strong>alinhar o paradigma à natureza do problema que você está resolvendo</strong>.</p><p>Como sempre, devemos analisar os trade-offs, para decidir quando adotar ou não DOP.</p><h4>Quando o DOP brilha (Casos de Uso Ideais)</h4><ul><li><strong>Sistemas Baseados em Informação:</strong> Se a sua aplicação passa a maior parte do tempo movendo, transformando, enriquecendo e persistindo dados (APIs REST/GraphQL, microsserviços na nuvem, sistemas orientados a eventos).</li><li><strong>Pipelines e Ciência de Dados:</strong> Cenários onde o dado precisa passar por transformações lineares, agregações e análises estatísticas, e onde a mutabilidade seria um risco catastrófico para a consistência dos resultados.</li><li><strong>Aplicações com Alta Concorrência:</strong> Ambientes onde múltiplas threads, processos ou workers assíncronos precisam ler as mesmas informações simultaneamente. A imutabilidade do DOP elimina a necessidade de locks complexos.</li><li><strong>Sistemas Voláteis ou de Integração:</strong> Quando você consome dados de múltiplas APIs de terceiros cujos contratos mudam frequentemente. A flexibilidade das estruturas genéricas do DOP absorve essas mudanças sem quebrar o sistema.</li></ul><h4>Quando NÃO usar DOP (Onde OOP ou outros modelos ainda vencem)</h4><ul><li><strong>Sistemas de Simulação e Jogos:</strong> Se você está desenvolvendo uma engine física ou um jogo, onde entidades mudam de estado dezenas de vezes por segundo (como a coordenada x, y de um personagem), o custo computacional de gerar cópias imutáveis pode degradar a taxa de quadros (FPS). A mutabilidade controlada e o encapsulamento de OOP/ECS (Entity Component System) são muito mais eficientes aqui.</li><li><strong>Sistemas Operacionais e Drivers de Dispositivo:</strong> Aplicações de baixíssimo nível que operam coladas ao hardware precisam gerenciar a memória de forma cirúrgica. Mapear registradores e buffers de hardware funciona melhor com estruturas rígidas e ponteiros mutáveis.</li><li><strong>Componentes de Interface Gráfica (UI):</strong> Paradigmas de UI baseados em componentes (como botões, janelas e menus que retêm seu próprio estado de clique e foco) se alinham muito naturalmente com o encapsulamento clássico de OOP.</li><li><strong>Domínios Pequenos e Estáticos:</strong> Se o seu sistema é um CRUD simples, com regras de negócio que nunca mudam e pouca concorrência, o boilerplate de classes OOP clássicas pode ser mais do que suficiente e já está consolidado na sua equipe.</li></ul><h3>Conclusão</h3><p>Ao longo deste artigo, fizemos uma jornada profunda pela forma como construímos software na atualidade. Começamos desmistificando a <strong>Orientação a Objetos</strong>, encarando de frente como o casamento forçado entre código e dados gera uma complexidade acidental crônica, além do alto esforço cognitivo exigido para gerenciar estados mutáveis compartilhados em sistemas de larga escala.</p><p>Vimos que as grandes <strong>motivações</strong> por trás do nascimento do <strong>Data-Oriented Programming (DOP)</strong> vieram da própria evolução da arquitetura de software: a transição para a nuvem, para sistemas distribuídos e orientados a eventos exigia um modelo mais flexível, seguro para concorrência e livre da fricção de mapeamentos artificiais.</p><p>Para resolver essas dores, exploramos os <strong>quatro princípios fundamentais</strong> propostos por Yehonathan Sharvit:</p><ol><li><strong>A separação radical entre código e dados</strong>, libertando as funções para serem puras e os dados para serem apenas fatos.</li><li><strong>O uso de estruturas de dados genéricas</strong>, garantindo que o software flua com a flexibilidade do mundo real.</li><li><strong>A imutabilidade como regra</strong>, erradicando os efeitos colaterais e blindando o sistema contra condições de corrida.</li><li><strong>A validação contextual à la carte</strong>, permitindo que a integridade dos dados seja checada sob demanda, de acordo com o caso de uso.</li></ol><p>Por fim, ponderamos que <strong>o DOP não veio para apagar o passado ou decretar a morte absoluta de OOP, mas sim para oferecer uma alternativa cirúrgica para sistemas cujo oxigênio é a informação</strong>.</p><p><strong>Adotar o Data-Oriented Programming é, acima de tudo, uma mudança de perspectiva</strong>. É parar de tentar forçar a realidade a caber em hierarquias rígidas de classes e passar a enxergar o software como ele realmente é: um fluxo contínuo e previsível de dados sendo transformados. Da próxima vez que você se pegar lutando contra o estado de um objeto ou criando dezenas de DTOs e mapeadores, dê um passo para trás e lembre-se: separe o código dos dados, trate-os como fatos imutáveis e deixe que as funções façam o seu trabalho.</p><h4>Se este artigo te ajudou de alguma forma, você pode referenciá-lo como:</h4><blockquote><em>Guimarães, Alysson. (Jun 2026). Programação Orientada a Dados. </em><a href="https://medium.com/data-hackers/p-140c3bdd1b0d">https://medium.com/data-hackers/p-140c3bdd1b0d</a></blockquote><p>ou</p><pre>@miscellaneous{guimaraes2026dop,<br> title = {Programação Orientada a Dados},<br> author = {Guimarães, Alysson},<br> journal = {https://medium.com/data-hackers/},<br> year = {2026},<br> month = {Jun},<br> url = {https://k3ybladewielder.medium.com/p-140c3bdd1b0d}<br>}</pre><h3>Referências</h3><p>Y. Sharvit, <em>Data-Oriented Programming</em>, 1st ed., Manning Publications, Westborough, MA, 2022.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=140c3bdd1b0d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/programa%C3%A7%C3%A3o-orientada-a-dados-140c3bdd1b0d">Programação Orientada a Dados</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Natural Language Processing (NLP) Com Classificação e Espaços Vetoriais]]></title>
            <link>https://medium.com/data-hackers/natural-language-processing-nlp-com-classifica%C3%A7%C3%A3o-e-espa%C3%A7os-vetoriais-51291c08f29e?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/51291c08f29e</guid>
            <category><![CDATA[text-analytics]]></category>
            <category><![CDATA[natural-language-process]]></category>
            <category><![CDATA[pln]]></category>
            <category><![CDATA[sentiment-analysis]]></category>
            <category><![CDATA[nlp]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Fri, 28 Jun 2024 10:08:08 GMT</pubDate>
            <atom:updated>2024-07-10T12:09:09.188Z</atom:updated>
            <content:encoded><![CDATA[<h3>Natural Language Processing, Classificação e Espaços Vetoriais</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*K8Esd_qfANAkS96u" /><figcaption><strong>Generated with AI</strong> ∙ April 12, 2024 at 3:30 PM</figcaption></figure><h3>1 Introdução</h3><p>Os algoritmos de <strong>Machine Learning (ML)</strong> supervisionados são um tipo de algoritmo que aprende a partir de dados rotulados, ou seja, dados que já possuem um rótulo ou uma classificação pré-definida. Esses algoritmos usam esses dados rotulados para aprender a fazer previsões ou classificações em novos dados.</p><p>Na área de Processamento de Linguagem Natural (NLP), os algoritmos de ML supervisionados são usados para uma variedade de tarefas, como <strong>classificação de texto</strong> (ex. sentimento), <strong>identificação de entidades nomeadas (NER)</strong>, <strong>análise de tópicos</strong>, <strong>tradução automática</strong>, entre outras.</p><p>Um exemplo de como esses algoritmos são usados em NLP é na classificação de sentimentos em textos. Nesse caso, um modelo de ML supervisionado seria <strong>treinado em um conjunto de dados rotulados</strong> que contém textos e suas respectivas classificações de sentimento (por exemplo, positivo, negativo ou neutro). O algoritmo usaria esses dados para <strong>aprender a reconhecer padrões nos textos</strong> e, em seguida, aplicaria esses padrões para <strong>classificar o sentimento em novos textos</strong>. Outro exemplo é na identificação de <strong>entidades nomeadas (NER)</strong>, como já supracitado, que é uma tarefa que envolve a <strong>identificação de nomes de pessoas, locais, organizações e outras entidades em um texto</strong>. Nesse caso, um modelo de ML supervisionado seria treinado em um conjunto de dados rotulados que contém textos e suas respectivas entidades nomeadas. O algoritmo usaria esses dados para aprender a reconhecer os padrões de palavras e contextos que indicam a presença de uma entidade nomeada em um texto, e, em seguida, aplicaria esses padrões para identificar entidades em novos textos.</p><p>Esses algoritmos são uma técnica poderosa para resolver problemas em NLP, permitindo que modelos aprendam com dados rotulados e possam fazer previsões ou classificações em novos dados com base no que foi aprendido.</p><p>Um pipeline básico para a <em>task</em> ou tarefa de análise de sentimento (classificação) geralmente envolve as etapas de:</p><ul><li><strong>Pré-processamento de texto</strong>: Esta etapa envolve a limpeza e preparação dos dados. O texto é normalmente convertido em minúsculas, removidos caracteres especiais, removidos números e pontuações, e realizada a tokenização do texto para obter as palavras individuais.</li><li><strong>Criação de features</strong>: Nesta etapa, são criadas as features que serão usadas pelo modelo de classificação. As features podem incluir a contagem de palavras, a frequência de palavras, o tipo de palavras, o uso de negação, entre outras.</li><li><strong>Treinamento do modelo</strong>: O modelo de classificação é treinado em um conjunto de dados rotulados que contêm exemplos de texto e suas respectivas classificações de sentimento. Existem vários algoritmos de aprendizado de máquina que podem ser usados para treinar um modelo, como Árvores de Decisão, Naive Bayes, Regressão Logística, SVM, Redes Neurais, etc.</li><li><strong>Avaliação do modelo</strong>: Após o treinamento, o modelo é avaliado em um conjunto de dados de teste para verificar sua precisão. É comum dividir o conjunto de dados em conjunto de treinamento, validação e teste.</li><li><strong>Implementação/Deploy</strong>: Por fim, o modelo treinado é usado para classificar o sentimento de novos textos. Novos textos passam pelas mesmas etapas de pré-processamento e criação de features, e o modelo treinado é usado para prever a classificação de sentimento do texto.</li></ul><p>Neste artigo, abordaremos alguns tópicos desse pipeline de tarefas de NLP, como Pré-processamento de texto, Criação de features, Treinamento e avaliação de modelos.</p><p>O artigo está organizado da seguinte forma: A seção 2 descreve como extrair features e realizar o preprocessamento de dados, na seção 3 são introduzidos os modelos de regressão logística e algoritmo naive bayes, e como aplicá-los na tarefa de classificação de texto para análise de sentimento. Na seção 4 são discutidos os Vector Space Models, como eles representam palavras e documentos e como construir matrizes de coocorrencia no formato word by word e word by document, como avaliar a similaridade de vetores com a distância euclidiana e similaridade de cosseno, como manipular palavras nos espaços vetoriais e o algoritmo Principal Component Analysis e como utilizá-lo. Por fim, Na seção 5 é apresentado a conclusão.</p><h3>2 Vocabulary and Feature Extraction</h3><p>Para representar textos de forma numérica, primeiro precisamos construir um vocabulário, com eles poderemos _encodar_ qualquer texto como um array de números. Um Vocabulário é uma lista com palavras únicas, não repetidas.</p><p>Uma forma simples de extrair features do texto é usando o vocabulário, verificando cada palavra do vocabulário que aparece no texto. Caso as palavras no texto que estamos extraindo a feature tenham apareca no vocabulário, atribuímos o valor 1 para ela, e zero para as palavras que do vocabulário que não aparecem no texto. Assim, estamos representando o texto usando <a href="https://k3ybladewielder.medium.com/introdu%C3%A7%C3%A3o-%C3%A0-nlp-4d7d98b9a36a"><strong>one-hot encoding</strong></a> ou representação esparsa. Mas esse método pode ser problemático porquê o número de features é igual ao número de palavras no vocabulário e a grande maioria das features serão zero, aumentando excessivamente o tempo de treino e predição dos modelos.</p><p>Existem diversas técnicas para representar textos como vetores, sendo as mais comuns a Bag of Words (BoW) e a Representação Distribuída de Palavras (Word Embeddings). Vou explicar brevemente cada uma delas:</p><ul><li><strong>Bag of Words (BoW)</strong>: Nessa técnica, o texto é representado como um vetor contendo a contagem de ocorrências de cada palavra presente no texto. Cada palavra é considerada como uma dimensão do vetor. Dessa forma, quanto mais vezes uma palavra aparecer no texto, maior será o valor correspondente na dimensão correspondente no vetor. Por exemplo, se um texto contém as palavras “gato”, “cão” e “casa”, a representação BoW seria um vetor com três dimensões, com valores correspondentes à contagem de ocorrências de cada palavra no texto.</li><li><strong>Word Embeddings</strong>: Essa técnica é baseada em modelos de linguagem neural, que mapeiam cada palavra em um espaço vetorial de alta dimensão, onde palavras semelhantes têm representações próximas. A ideia é que cada palavra seja representada por um vetor de números reais que captura seu significado semântico. Esses vetores podem ser aprendidos a partir de grandes quantidades de textos usando técnicas de aprendizado de máquina, como Word2Vec, GloVe ou FastText. Os vetores resultantes podem ser usados para representar cada palavra em um texto como um vetor numérico. A representação distribuída de palavras pode capturar relações semânticas entre palavras, como sinonímia e antonímia, e pode ser usada para tarefas mais complexas, como análise de sentimento ou classificação de texto.</li></ul><p>Ambas as técnicas são amplamente utilizadas em NLP, dependendo do objetivo e do contexto da tarefa em questão. A escolha da técnica de representação de texto pode influenciar significativamente o desempenho do modelo de aprendizado de máquina, e é importante escolher a técnica mais adequada para a tarefa específica.</p><p>Tanto a técnica de Bag of Words (BoW) quanto a Word Embeddings têm seus <strong>pontos fortes e fracos</strong>, e a escolha de qual usar depende do contexto e do objetivo da tarefa em questão.</p><p>A representação <strong>BoW</strong> pode ser uma escolha adequada para <strong>tarefas simples de classificação de texto</strong>, como classificação de spam ou análise de sentimento, onde a presença ou ausência de palavras específicas pode ser um indicador importante para a classificação. Além disso, a representação <strong>BoW é computacionalmente eficiente e fácil de interpretar</strong>, o que pode ser uma vantagem para <strong>problemas onde a transparência do modelo é importante</strong>. No entanto, a representação <strong>BoW não leva em consideração a ordem das palavras no texto, o que pode limitar sua capacidade de capturar nuances semânticas</strong>.</p><p>Já Word Embedding é mais adequada para <strong>tarefas que envolvem análise semântica</strong>, como tradução automática, classificação de tópicos ou análise de sentimento baseada em frases complexas. A representação Word Embedding <strong>leva em consideração a ordem e o contexto das palavras</strong>, e <strong>pode capturar a similaridade semântica entre palavras que não aparecem juntas com frequência</strong>. Além disso, a representação distribuída de palavras <strong>pode ser usada para inicializar redes neurais em tarefas de aprendizado profundo</strong>, melhorando o desempenho do modelo. No entanto, Word Embedding <strong>pode ser computacionalmente intensiva e requer grandes quantidades de dados de treinamento para obter bons resultados</strong>.</p><h4>2.1 Feature Extraction with Frequencies</h4><p>Numa task de classificação de textos (ex. sentimentos), podemos identificar as palavras positivas e negativas a partir da frequencia de ocorrencia em que elas ocorrem nos textos positivos e negativos. Usando essa contagem, podemos extrair features e usá-las no modelo de classificação, como a <strong>regressão logística</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Bl7whVZQD9C7niDrWwvjMA.png" /></figure><p>A partir da contagem da frequencia das palavras em cada classe, chegamos a essa tabela. Na prática, essa tabela será um dicionário que mapeia a classe da palavra e sua frequência de ocorrência.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aOfoEqy9b-eRed1lvfbyBA.png" /></figure><p>Podemos representar essa tabela de frequencia com um array com 3 features, aumentando a velocidade na implementação, porquê em vez de termos v features, teremos apenas 3 para que o modelo aprenda.</p><p>Aqui, a primeira feature é um bias, depois o somatório das palavras da label positiva e o somatório das palavras da label negativa. Assim, teremos o novo vetor com 3 features.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YeDe0A9oqTwvRoSIefE4zw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/639/1*1xUagFmcRBlVfTsE38BWog.png" /></figure><p>Podemos implementar essa extração de features com frequência de ocorrência de termos negativos ou positivos da seguinte forma:</p><pre>import pandas as pd<br>import nltk<br>from nltk.tokenize import word_tokenize<br>from nltk.probability import FreqDist<br><br>positive_words = df[df[&quot;label&quot;] == 0][&quot;tweet&quot;].to_list() # Lista de palavras positivas<br>positive_words = word_tokenize(&#39; &#39;.join(positive_words))<br><br>negative_words = df[df[&quot;label&quot;] == 1][&quot;tweet&quot;].to_list()# Lista de palavras negativas<br>negative_words = word_tokenize(&#39; &#39;.join(negative_words))<br><br># Função para mapear a frequência das palavras do subconjunto de palavras positivas (label = 0)<br>def positive_words_frequency(tweet):<br>    tokens = word_tokenize(tweet.lower())  # Tokenização e conversão para minúsculas<br>    freq = FreqDist(tokens)<br>    positive_freq = sum([freq[word] for word in positive_words])<br>    return positive_freq<br><br># Função para mapear a frequência das palavras do subconjunto de palavras negativas (label = 1)<br>def negative_words_frequency(tweet):<br>    tokens = word_tokenize(tweet.lower())  # Tokenização e conversão para minúsculas<br>    freq = FreqDist(tokens)<br>    negative_freq = sum([freq[word] for word in negative_words])<br>    return negative_freq<br><br># Aplicar as funções aos tweets e adicionar os resultados ao dataframe<br>df[&#39;bias&#39;] = 1  # Coluna de bias com valor 1<br>df[&#39;pos_freq&#39;] = df[&#39;tweet&#39;].apply(positive_words_frequency)<br>df[&#39;neg_freq&#39;] = df[&#39;tweet&#39;].apply(negative_words_frequency)<br>df</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/440/1*HK2fgMslJby_EadW48IdrA.png" /><figcaption>Output do dataset sintético</figcaption></figure><h4>2.2 Preprocessing</h4><p>O pré-processamento para tarefas de NLP geralmente envolve as etapas de limpeza e preparação dos dados. O texto é normalmente convertido em minúsculas, removidos caracteres especiais, removidos números e pontuações, e realizada a tokenização do texto para obter as palavras individuais.</p><p>Exemplo em python:</p><pre>import nltk<br>from nltk.corpus import stopwords<br>import string<br><br>nltk.download(&#39;stopwords&#39;)<br><br>def text_cleaning(text):<br>    # Extrai as stopwords em português<br>    stopwords_list = stopwords.words(&#39;portuguese&#39;)<br>    <br>    # Remove caracteres especiais<br>    text = text.translate(str.maketrans(&#39;&#39;, &#39;&#39;, string.punctuation))<br>    <br>    # Remove números e minimiza<br>    text = &#39;&#39;.join(word for word in text if not word.isdigit()).lower()<br>    <br>    # Converte para minúsculo e tokeniza as palavras<br>    tokens = nltk.word_tokenize(text.lower())<br>    <br>    # Remove as stopwords<br>    tokens = [word for word in tokens if word not in stopwords_list]<br>    <br>    return tokens<br><br># Exemplo de uso<br>text = &quot;Este é um exemplo de texto que será limpo e tokenizado!&quot;<br>tokens = text_cleaning(text)<br>print(tokens)<br><br># Output<br># [&#39;exemplo&#39;, &#39;texto&#39;, &#39;limpo&#39;, &#39;tokenizado&#39;]</pre><p>Agora, podemos aplicar a extração de features do exemplo acima com o texto tratado.</p><pre># preprocessando o texto<br>df[&quot;ttweet&quot;] = df[&quot;tweet&quot;].apply(text_cleaning)<br>df[&quot;ttweet&quot;] = df[&quot;ttweet&quot;].apply(lambda x: &quot; &quot;.join(x)) # retornando a lista como string<br><br># novo conjunto de palavras<br>positive_words = df[df[&quot;label&quot;] == 0][&quot;ttweet&quot;].to_list() # Lista de palavras positivas<br>positive_words = word_tokenize(&#39; &#39;.join(positive_words))<br><br>negative_words = df[df[&quot;label&quot;] == 1][&quot;ttweet&quot;].to_list()# Lista de palavras negativas<br>negative_words = word_tokenize(&#39; &#39;.join(negative_words))<br><br># Função para mapear a frequência das palavras do subconjunto de palavras positivas (label = 0)<br>def positive_words_frequency(tweet):<br>    tokens = word_tokenize(tweet.lower())  # Tokenização e conversão para minúsculas<br>    freq = FreqDist(tokens)<br>    positive_freq = sum([freq[word] for word in positive_words])<br>    return positive_freq<br><br># Função para mapear a frequência das palavras do subconjunto de palavras negativas (label = 1)<br>def negative_words_frequency(tweet):<br>    tokens = word_tokenize(tweet.lower())  # Tokenização e conversão para minúsculas<br>    freq = FreqDist(tokens)<br>    negative_freq = sum([freq[word] for word in negative_words])<br>    return negative_freq<br><br># Aplicar as funções aos tweets e adicionar os resultados ao dataframe<br>df[&#39;bias&#39;] = 1  # Coluna de bias com valor 1<br>df[&#39;pos_freq&#39;] = df[&#39;ttweet&#39;].apply(positive_words_frequency)<br>df[&#39;neg_freq&#39;] = df[&#39;ttweet&#39;].apply(negative_words_frequency)<br>df.head(20)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/610/1*FEkQkKfCQCfgldQn2_wkGA.png" /><figcaption>Note que, remover as stop words diminuiu drasticamente a frequencia de ocorrencia, já que estamos utilizando somente as palavras que carregam informações</figcaption></figure><p>Mas além disso também podemos fazer <strong>Stemming</strong> e <strong>lematização</strong>. Elas são técnicas de pré-processamento de texto com o objetivo de reduzir as palavras em sua forma base ou raiz, simplificando o processo de análise de texto.</p><p>O <strong>Stemming</strong> é um processo mais simples e rápido de normalização de palavras, que envolve a remoção de sufixos com o objetivo de transformar uma palavra em sua raiz, ou seja, em sua forma básica. Um exemplo de algoritmo de stemming é o Porter Stemmer, que é amplamente utilizado em NLP. O ponto positivo do stemming é sua simplicidade e velocidade de processamento, o que pode ser útil em projetos com grandes volumes de dados. No entanto, o stemming pode produzir algumas palavras raiz que não são facilmente reconhecidas, o que pode ser um problema em alguns casos.</p><p>Já a <strong>lematização</strong> é um processo mais complexo de normalização de palavras, que envolve a análise do contexto da palavra para determinar sua forma básica. A lematização utiliza um dicionário de palavras ou um algoritmo para mapear a palavra para sua forma base. O ponto positivo da lematização é que ela produz palavras que são facilmente reconhecíveis, o que pode ser importante em projetos que exigem maior precisão na análise de texto. No entanto, a lematização é um processo mais lento e computacionalmente mais caro do que o stemming, o que pode ser um problema em projetos com grandes volumes de dados.</p><p>Podemos implementar o stemming da seguinte forma:</p><pre>import nltk<br>from nltk.corpus import stopwords<br>from nltk.stem import SnowballStemmer<br>import string<br><br>def text_cleaning_s(text):<br>    # Extrai as stopwords em português<br>    stopwords_list = stopwords.words(&#39;portuguese&#39;)<br>    <br>    # Remove caracteres especiais<br>    text = text.translate(str.maketrans(&#39;&#39;, &#39;&#39;, string.punctuation))<br>    <br>    # Remove números e minimiza<br>    text = &#39;&#39;.join(word for word in text if not word.isdigit()).lower()<br>    <br>    # Converte para minúsculo e tokeniza as palavras<br>    tokens = nltk.word_tokenize(text.lower())<br>    <br>    # Remove as stopwords<br>    tokens = [word for word in tokens if word not in stopwords_list]<br>    <br>    # Realiza o stemming dos tokens<br>    stemmer = SnowballStemmer(&#39;portuguese&#39;)<br>    stemmed_tokens = [stemmer.stem(token) for token in tokens]<br>    <br>    return stemmed_tokens<br><br># Exemplo de uso<br>text = &quot;Este é um exemplo de texto que será limpo, tokenizado e stemmed!&quot;<br>tokens = text_cleaning_s(text)<br>print(tokens)<br><br># output<br># [&#39;exempl&#39;, &#39;text&#39;, &#39;limp&#39;, &#39;tokeniz&#39;, &#39;stemmed&#39;]</pre><p>Enquanto que para lematização, podemos implementar da seguinte forma, apenas com pequenas alterações:</p><pre>import nltk<br>from nltk.corpus import stopwords<br>from nltk.stem import WordNetLemmatizer<br>import string<br><br>nltk.download(&#39;wordnet&#39;)<br>nltk.download(&#39;omw-1.4&#39;)<br><br>def text_cleaning_l(text):<br>    # Extrai as stopwords em inglês<br>    stopwords_list = stopwords.words(&#39;english&#39;)<br>    <br>    # Remove caracteres especiais<br>    text = text.translate(str.maketrans(&#39;&#39;, &#39;&#39;, string.punctuation))<br>    <br>    # Remove números e minimiza<br>    text = &#39;&#39;.join(word for word in text if not word.isdigit()).lower()<br>    <br>    # Converte para minúsculo e tokeniza as palavras<br>    tokens = nltk.word_tokenize(text.lower())<br>    <br>    # Remove as stopwords<br>    tokens = [word for word in tokens if word not in stopwords_list]<br>    <br>    # Realiza a lematização dos tokens<br>    lemmatizer = WordNetLemmatizer()<br>    lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]<br>    <br>    return lemmatized_tokens</pre><h3>3 Sentiment Analysis</h3><p>Nesta seção falaremos sobre a tarefa de classificação de texto para analisar sentimentos utilizando os modelos Logistic Regression e Naive Bayes.</p><h4>3.1 Sentiment Analysis With Logistic Regression</h4><p>A <strong>regressão logística </strong>ou<strong> logistic regression </strong>é frequentemente utilizada em análise de sentimento porque é um algoritmo de classificação <strong>simples, rápido e eficiente</strong> para problemas de <strong>classificação binária</strong>, ou seja, quando há apenas duas classes, como positivo e negativo. Além disso, a regressão logística é facilmente interpretável, o que significa que é possível entender como o modelo chegou a uma determinada previsão.</p><p>Outra vantagem da regressão logística é que ela <strong>lida bem com problemas de dados desbalanceados</strong>, que é comum em análise de sentimento, onde muitas vezes há mais exemplos de uma classe do que outra. A regressão logística usa uma <strong>função sigmoide</strong> para calcular as probabilidades de cada classe, e essa função é bem adequada para casos de classes desbalanceadas.</p><p>Outro motivo para a popularidade da regressão logística em análise de sentimento é que ela é relativamente <strong>fácil de implementar e ajustar</strong>. É possível utilizar diferentes técnicas de regularização, como a regularização L1 ou L2, para evitar overfitting e melhorar o desempenho do modelo.</p><p>No entanto, é importante ressaltar que a regressão logística pode não ser adequada para problemas de classificação multiclasse, ou seja, quando há mais de duas classes, como em análise de tópicos. Nesses casos, outros algoritmos, como Árvores de Decisão, SVMs ou Redes Neurais, podem ser mais apropriados.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*55BNWSInR1FFbavCzzQKDw.png" /></figure><p>O modelo é treinado usando um conjunto de dados rotulados, onde cada texto é rotulado como tendo um sentimento positivo ou negativo. O objetivo do treinamento é <strong>ajustar os parâmetros do modelo</strong> para que ele possa fazer previsões precisas em novos dados.</p><p>Durante o treinamento, o modelo utiliza a função logística para <strong>calcular a probabilidade de um texto ter um sentimento positivo ou negativo</strong> com base em seus <strong>vetores de características</strong>. A função logística produz um valor entre 0 e 1, que representa a probabilidade de um texto ter um sentimento positivo. Por padrão, se a probabilidade for maior que 0,5, o modelo classifica o texto como tendo um sentimento positivo. Caso contrário, o texto é classificado como tendo um sentimento negativo. Além disso podemos ajustar o limiar de classificação do modelo.</p><p>Após o treinamento, o modelo pode ser usado para fazer previsões em novos dados. <strong>Para cada novo texto, o modelo converte-o em um vetor de características e utiliza a função logística para calcular a probabilidade de ter um sentimento positivo ou negativo</strong>. Em seguida, <strong>classifica o texto como tendo um sentimento positivo ou negativo com base no valor calculado</strong>.</p><p>Note que conforme <strong>𝜃^T x^(i)</strong> se aproxima cada vez mais de -∞, o denominador da função sigmoidal fica cada vez maior e, como resultado, a sigmoidal se aproxima de <strong>0</strong>. Por outro lado, conforme <strong>𝜃^T x^(i)</strong> se aproxima cada vez mais de ∞, o denominador da função sigmoidal se aproxima de 1 e, como resultado, a sigmoidal também se aproxima de <strong>1</strong>.</p><p>Dado um tweet, podemos <strong>transformá-lo em um vetor</strong> e passá-lo pela sua função sigmoidal para obter uma previsão da seguinte forma:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_5x8xzv6eklwdf_W1lX9Zg.png" /></figure><p>Agora, podemos comparar o desempenho do modelo com as duas estratégias de representação numérica de textos: A simples e da função <strong>text_cleaning_l</strong> definida acima, que remove caracteres especiais, stopwords e torna todas as palavras minúsculo.</p><p>Estratégia Simples:</p><pre>from sklearn.linear_model import LogisticRegression<br>from sklearn.model_selection import train_test_split<br>from sklearn.metrics import classification_report<br>from sklearn.metrics import accuracy_score, precision_score<br><br># divisão em treino e teste<br>X = df[[&#39;bias&#39;, &#39;pos_freq&#39;, &#39;neg_freq&#39;]]<br>y = df[&#39;label&#39;]<br>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)<br><br># criação do modelo de regressão logística e treinamento<br>lr = LogisticRegression()<br>lr.fit(X_train, y_train)<br><br># avaliação do modelo nos dados de teste<br>y_pred = lr.predict(X_test)<br><br># avaliando o modelo<br>print(&quot;\n Classification Report: Estratégia Simples \n&quot;, classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/374/1*QIApRkuj7-w05yemqrbZaA.png" /></figure><p>Com o pipeline de pré-processamento + Count Vectorizer:</p><pre>from sklearn.feature_extraction.text import CountVectorizer<br>from sklearn.linear_model import LogisticRegression<br>from sklearn.model_selection import train_test_split<br>from sklearn.metrics import classification_report<br><br># pré-processamento das frases<br>df[&#39;ttweet&#39;] = df[&#39;tweet&#39;].apply(text_cleaning_l)<br><br># divisão em treino e teste<br>X_train, X_test, y_train, y_test = train_test_split(df[&#39;ttweet&#39;], df[&#39;label&#39;], test_size=0.3, random_state=42)<br><br># vetorização das frases com CountVectorizer<br>vectorizer = CountVectorizer()<br>X_train_vec = vectorizer.fit_transform(X_train.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br>X_test_vec = vectorizer.transform(X_test.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br><br># criação do modelo de regressão logística e treinamento<br>lr = LogisticRegression()<br>lr.fit(X_train_vec, y_train)<br><br># avaliação do modelo nos dados de teste<br>y_pred = lr.predict(X_test_vec)<br><br># avaliando o modelo<br>print(&quot;\n Classification Report: Pipeline de Pré-processamento + CountVectorizer \n&quot;, <br>      classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/493/1*BsQbBc8RAV9XBjISpndFFA.png" /></figure><p>Com o pipeline de pré-processamento + TF-IDF:</p><pre>from sklearn.feature_extraction.text import CountVectorizer<br>from sklearn.feature_extraction.text import TfidfVectorizer<br>from sklearn.linear_model import LogisticRegression<br>from sklearn.model_selection import train_test_split<br>from sklearn.metrics import classification_report<br><br># pré-processamento das frases<br>df[&#39;ttweet&#39;] = df[&#39;tweet&#39;].apply(text_cleaning_l)<br><br># divisão em treino e teste<br>X_train, X_test, y_train, y_test = train_test_split(df[&#39;ttweet&#39;], df[&#39;label&#39;], test_size=0.3, random_state=42)<br><br># vetorização das frases com CountVectorizer<br>vectorizer = TfidfVectorizer()<br>X_train_vec = vectorizer.fit_transform(X_train.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br>X_test_vec = vectorizer.transform(X_test.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br><br># criação do modelo de regressão logística e treinamento<br>lr = LogisticRegression()<br>lr.fit(X_train_vec, y_train)<br><br># avaliação do modelo nos dados de teste<br>y_pred = lr.predict(X_test_vec)<br><br># avaliando o modelo<br>print(&quot;\n Classification Report: Pipeline de Pré-processamento +TF-IDF  \n&quot;, <br>      classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/428/1*4SSxx3usRMmCLwe86Zb04Q.png" /></figure><p>Nesse teste exemplo com <strong>fins didáticos</strong>, com um conjunto de dados <strong>sintético</strong> e <strong>pequeno</strong> o processamento simples teve uma melhor performace. Mas num cenário real provavelmente esse não seria o caso. Mas mesmo assim, precisamos partir de um baseline e ir aumentando a complexidade do modelo e criando novas features até que o desempenho do modelo seja adequado para o objetivo da task.</p><h4>3.2 Sentiment Analysis with Naive Bayes</h4><h4>Recaptulando Probabilidade…</h4><p>Antes de abordar o algoritmo Naive bayes, é preciso relembrar alguns conceitos de probabilidade. A <strong>probabilidade</strong> é uma medida numérica que quantifica a incerteza associada a um evento. Em termos simples, é a chance de que algo aconteça. A probabilidade de um evento é sempre um número entre 0 e 1, onde 0 indica impossibilidade absoluta do evento ocorrer e 1 indica certeza absoluta de que o evento ocorrerá.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fxcS2cxd0qugqroHpdtS2Q.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AMbVteTFsnKSwgJOJjRn3Q.png" /></figure><p>Já a <strong>probabilidade condicional</strong> é a probabilidade de um evento ocorrer dado que outro evento já ocorreu. É denotada por $ P(A|B) $, que lê-se como “a probabilidade de A dado B”. A fórmula para calcular a probabilidade condicional é:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/132/1*TmLBwNirMmGHzU4nbUQELw.png" /></figure><p>Onde:<br> — P(A|B) é a probabilidade de A dado B,<br> — P(A ∩ B) é a probabilidade da interseção de A e B,<br> — P(B) é a probabilidade de B.</p><p>Em palavras simples, a probabilidade condicional é a proporção de vezes que o evento A ocorre quando o evento B ocorre.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S-R2bJ__qCxzewcSnfCOnA.png" /></figure><p>Outro conceito importante a ser lembrado é a <strong>Regra de Bayes ou Bayes Rule. </strong>Ela é uma ferramenta fundamental na teoria das probabilidades que permite atualizar as probabilidades de uma hipótese à luz de novas evidências. Formalmente, a regra de Bayes é expressa como:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/159/1*rcwH_hg_y9LwuwNGUJXu8w.png" /></figure><p>Onde:<br> — P(A|B) é a probabilidade de A dado B (posterior),<br> — P(B|A) é a probabilidade de B dado A (likelihood),<br> — P(A) é a probabilidade de A (prior),<br> — P(B) é a probabilidade de B.</p><p>A regra de Bayes nos permite calcular a probabilidade de uma hipótese (A) ser verdadeira dada uma evidência observada (B), usando a probabilidade da evidência dada a hipótese (likelihood), a probabilidade a priori da hipótese e a probabilidade marginal da evidência.A diferença principal entre a probabilidade condicional e a regra de Bayes é que a probabilidade condicional é uma medida da probabilidade de um evento ocorrer dado que outro evento já ocorreu, enquanto a regra de Bayes é uma ferramenta para atualizar a probabilidade de uma hipótese à luz de novas evidências. A regra de Bayes utiliza a probabilidade condicional como um de seus componentes para calcular a probabilidade posterior.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*19H1TX1zhIvXnai7YkIjug.png" /></figure><p>A probabilidade é a chance de um evento ocorrer, a probabilidade condicional é a probabilidade de um evento ocorrer dado que outro evento já ocorreu, e a regra de Bayes é uma ferramenta para atualizar a probabilidade de uma hipótese à luz de novas evidências. Baseado nesses conceitos, foi desenvolvido o algoritmo Naive Bayes.</p><p>O algoritmo Naive Bayes é um método de classificação probabilístico baseado no teorema de Bayes, com uma suposição “ingênua” de independência condicional entre os recursos.</p><p><strong>1. Suposição de Independência Condicional: </strong>A primeira etapa do algoritmo Naive Bayes é a suposição de independência condicional entre os recursos. Isso significa que assumimos que os recursos são independentes entre si, dado o valor da variável de classe. Apesar de ser uma suposição forte e muitas vezes não ser verdadeira na prática, ela simplifica os cálculos e torna o algoritmo computacionalmente eficiente.</p><p><strong>2. Construção do Modelo de Probabilidade: </strong>O próximo passo é construir o modelo de probabilidade. Isso envolve calcular a probabilidade de cada classe e a probabilidade de cada valor do recurso dado cada classe. Em outras palavras, para cada classe, calculamos a probabilidade a priori da classe <strong>P(C_k)</strong> e a probabilidade de cada recurso <strong>P(X_i | C_k)</strong>.</p><p><strong>3. Classificação</strong>: Depois que o modelo de probabilidade é construído, podemos usá-lo para fazer previsões sobre novos exemplos. Dada uma nova instância com valores de recursos <strong>x_1, x_2, …, x_n</strong>, queremos calcular a probabilidade de pertencer a cada classe e, em seguida, atribuir a classe com a maior probabilidade como a classe prevista para a instância. Isso é feito usando o teorema de Bayes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/285/1*YBeocOvQsupxom0t-kQYAQ.png" /></figure><p>Onde:<br> — <strong>P(C_k | x_1, x_2, …, x_n) </strong>é a probabilidade da classe <strong>C_k</strong> dado os valores dos recursos/features<br> — <strong>P(C_k)</strong> é a probabilidade a priori da classe <strong>C_k</strong><br> — <strong>P(x_i | C_k)</strong> é a probabilidade de cada valor do recurso dado a classe <strong>C_k</strong><br> — <strong>P(x_1, x_2, …, x_n)</strong> é a probabilidade dos valores dos recursos.</p><p><strong>4. Estimação de Parâmetros: </strong>Durante a etapa de construção do modelo, precisamos estimar os parâmetros do modelo, ou seja, as probabilidades a priori das classes e as probabilidades condicionais dos recursos para cada classe. Isso geralmente é feito usando técnicas como a frequência relativa de ocorrência dos dados de treinamento.</p><p><strong>5. Suavização de Laplace (Opcional):</strong> Em alguns casos, para evitar probabilidades condicionais iguais a zero para features não observados em uma classe particular, pode ser aplicada a suavização de Laplace, <strong>adicionando uma pequena quantidade aos contadores de frequência de cada valor de recurso para cada classe durante a estimativa dos parâmetros.</strong></p><p>Essas são as etapas principais da função do algoritmo Naive Bayes, desde a suposição de independência condicional até a classificação de novas instâncias usando o teorema de Bayes.</p><p>Para construir um classificador, começaremos primeiro criando probabilidades condicionais, dada a tabela a seguir:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uCxZK1OEAFq8NCD7g2uK0A.png" /></figure><p>Isso nos permite calcular a seguinte tabela de probabilidades:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C6vfN6j9v1ObbWMyuuSbOA.png" /></figure><p>Depois de ter as probabilidades, podemos calcular a pontuação de probabilidade da seguinte forma</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JMweqWMzBs5s1CjQDu3_CA.png" /></figure><p>Uma pontuação maior que 1 indica que a classe é <strong>positiva</strong>, caso contrário é negativa.</p><p>Costumamos calcular a probabilidade de uma palavra dada uma classe da seguinte forma:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/403/1*7hiSWJZHLbxaH_zW6FNfLg.png" /></figure><p>No entanto, se uma palavra não aparecer no treinamento, ela automaticamente recebe uma probabilidade de 0. Para corrigir isso, adicionamos <strong>suavização de laplace</strong> da seguinte forma:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/252/1*s4FusAamj2s-N4Bfg0NLYg.png" /></figure><p>Observe que adicionamos um 1 no numerador e, como há V palavras para normalizar, adicionamos V no denominador.</p><p>Onde:<br>- <strong>N_classe</strong>: frequência de todas as palavras na classe.<br>- <strong>V</strong>: número de palavras únicas no vocabulário.</p><p>Para calcular a <strong>log-verossimilhança (log likelihood)</strong>, precisamos obter as razões e usá-las para calcular uma pontuação que nos permitirá decidir se um tweet é positivo ou negativo. Quanto maior a razão, mais positiva é a palavra:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nJ8h8q94y_4tEcg3BhbR0g.png" /></figure><p>Para fazer inferência, você pode calcular o seguinte:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/253/1*NJurFXxOZ8ciCLFozxbn-A.png" /></figure><p>A expressão acima começa com um logaritmo, <strong>log</strong>, que é aplicado a um produto de probabilidades condicionais. O produto é denotado pelo símbolo Π (pi) e representa o produto de todas as probabilidades condicionais das palavras <strong>w_i</strong> dadas as classes “negativo” e “positivo”. Isso significa que estamos multiplicando a probabilidade de cada palavra <strong>w_i</strong> ocorrer, dado que o tweet é negativo, e dividindo pelo mesmo para tweets positivos.</p><p>A expressão é comparada com o inverso do produto das probabilidades das classes “negativo” e “positivo”. Isso é feito usando o sinal <strong>&gt;</strong>, indicando que queremos que a expressão à esquerda seja maior do que o inverso do produto das probabilidades das classes.</p><p>Em termos de interpretação, isso significa que <strong>estamos comparando a probabilidade conjunta de todas as palavras em um tweet serem negativas (ou positivas) com a probabilidade de um tweet ser classificado como negativo (ou positivo), independente do conteúdo do tweet</strong>. Se a probabilidade conjunta de todas as palavras serem negativas (ou positivas) for maior do que a probabilidade do tweet ser classificado como negativo (ou positivo) independentemente do conteúdo, então a inferência seria que o tweet é mais provavelmente negativo (ou positivo).</p><p>Essa expressão é uma maneira de <strong>inferir a polaridade</strong> (positiva ou negativa) de um tweet com base na probabilidade condicional de cada palavra em relação às classes “negativo” e “positivo”, em comparação com a probabilidade marginal das classes. Se a probabilidade conjunta das palavras sendo negativas (ou positivas) for maior do que a probabilidade do tweet ser classificado como negativo (ou positivo) independentemente do conteúdo, então a inferência seria que o tweet é mais provavelmente negativo (ou positivo).</p><p>Conforme o número de palavras do tweet (m) aumenta, podemos ter problemas numéricos, então introduzimos o logaritmo, que nos dá a seguinte equação:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/395/1*qPdeA7jAZ7cJEYYKSutkfA.png" /></figure><p>Utilizando a propriedade do logaritmo de um produto, podemos reescrever a expressão como a soma dos logaritmos dos fatores dentro do produto. Esta transformação nos permite calcular a log-verossimilhança de forma mais eficiente e robusta, evitando o custo computacional de calcular o logaritmo de um produtório. A expressão agora é uma soma de logaritmos individuais, o que é mais estável numericamente. A nova expressão nos permite calcular a log-verossimilhança somando os logaritmos das razões das probabilidades condicionais de cada palavra em relação às classes “negativo” e “positivo”. Isso nos dá uma medida da probabilidade de observar as palavras em um tweet dado que ele é classificado como negativo, em comparação com a mesma probabilidade para tweets positivos. Essa transformação simplifica o cálculo da log-verossimilhança e reduz a chance de problemas numéricos, tornando a inferência mais eficiente e precisa.</p><p>O primeiro componente é chamado de log prior e o segundo componente é a log-verossimilhança. Introduzimos ainda λ (lambda) como segue:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9RMrWC9Q2prY3JtsaJ9XNg.png" /></figure><p>Ter o dicionário λ ajudará muito ao fazer inferência, uma vez que o computemos, se torna simples fazer a inferência, simplesmente somando os lambdas e comparando com os thresholds de negativo, neutro e positivo.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JDhlkdI_TurYL7uWhGEJrA.png" /></figure><p>O resultado foi <strong>3.3</strong>, sendo &gt; 0, classificaremos o documento como positivo. Se tivessemos um número negativo, classificaríamos como a classe negativa</p><p>Depois desse <strong>(nada)</strong> breve overview sobre probabilidade, podemos fazer os mesmos testes que fizemos com a regressão logística a fim de verificar o desempenho do modelo para cada estratégia de engenharia de features. Vamos seguir a mesma estrutura, testando a estratégia simples e depois a com o preprocessamento do texto aplicando Count Vectorizer e depois TF-IDF. Mas agora vamos utilizar o algoritmo Naive Bayes.</p><p>Implementação com a estratégia simples:</p><pre>import pandas as pd<br>import nltk<br>from nltk.tokenize import word_tokenize<br>from nltk.probability import FreqDist<br>from sklearn.metrics import accuracy_score<br>from sklearn.naive_bayes import MultinomialNB<br>from sklearn.linear_model import LogisticRegression<br><br># divisão em treino e teste<br>X = df[[&#39;pos_freq&#39;, &#39;neg_freq&#39;]]<br>y = df[&#39;label&#39;]<br>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)<br><br># criação do modelo<br>model = MultinomialNB() # Exemplo: usando Naive Bayes<br>model.fit(X_train, y_train)<br><br># avaliação do modelo nos dados de teste<br>y_pred = model.predict(X_test)<br><br># avaliando o modelo<br>print(&quot;\n Classification Report: Estratégia Simples \n&quot;, classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/389/1*htGWmKTtOYVXnlIQ9ZUV8g.png" /></figure><p>Com o pipeline de pré-processamento + Count Vectorizer:</p><pre>from sklearn.feature_extraction.text import CountVectorizer<br>from sklearn.naive_bayes import MultinomialNB<br>from sklearn.model_selection import train_test_split<br>from sklearn.metrics import classification_report<br><br># Divisão em treino e teste<br>X_train, X_test, y_train, y_test = train_test_split(df[&#39;ttweet&#39;], df[&#39;label&#39;], test_size=0.3, random_state=42)<br><br># Pré-processamento<br>X_train_processed = X_train.apply(text_cleaning_l)<br>X_test_processed = X_test.apply(text_cleaning_l)<br><br># Vetorização das frases processadas<br>vectorizer = CountVectorizer()<br>X_train_vectorized = vectorizer.fit_transform(X_train_processed.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br>X_test_vectorized = vectorizer.transform(X_test_processed.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())  # Use o vetorizador treinado<br><br># Criação do modelo Naive Bayes e treinamento<br>nb_model = MultinomialNB()<br>nb_model.fit(X_train_vectorized, y_train)<br><br># Avaliação do modelo nos dados de teste<br>y_pred = nb_model.predict(X_test_vectorized)<br><br># Avaliação do modelo<br>print(&quot;\n Classification Report: Count Vectorizer \n&quot;, classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/387/1*0ffu82LBub7Y25wwEAX1hw.png" /></figure><p>Com o pipeline de pré-processamento + TF-IDF:</p><pre>from sklearn.feature_extraction.text import CountVectorizer<br>from sklearn.naive_bayes import MultinomialNB<br>from sklearn.model_selection import train_test_split<br>from sklearn.metrics import classification_report<br><br># Divisão em treino e teste<br>X_train, X_test, y_train, y_test = train_test_split(df[&#39;ttweet&#39;], df[&#39;label&#39;], test_size=0.3, random_state=42)<br><br># Pré-processamento<br>X_train_processed = X_train.apply(text_cleaning_l)<br>X_test_processed = X_test.apply(text_cleaning_l)<br><br># Vetorização das frases processadas<br>vectorizer = TfidfVectorizer()<br>X_train_vectorized = vectorizer.fit_transform(X_train_processed.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())<br>X_test_vectorized = vectorizer.transform(X_test_processed.apply(lambda tokens: &#39; &#39;.join(tokens)).to_list())  # Use o vetorizador treinado<br><br># Criação do modelo Naive Bayes e treinamento<br>nb_model = MultinomialNB()<br>nb_model.fit(X_train_vectorized, y_train)<br><br># Avaliação do modelo nos dados de teste<br>y_pred = nb_model.predict(X_test_vectorized)<br><br># Avaliação do modelo<br>print(&quot;\n Classification Report: TF-IDF \n&quot;, classification_report(y_test, y_pred))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/387/1*I2ZfYYILnlQFg5pKWXfqNw.png" /></figure><p>Novamente, precisamos identificar quais as métricas importantes e analisar os resultados de acordo com o objetivo do projeto ou do negócio. Com o modelo Naive Bayes, o processamento simples teve uma melhor performace. E em um cenário real provavelmente esse não seria o caso. Mas mesmo assim, precisamos partir de um baseline e ir aumentando a complexidade do modelo e criando novas features até que o desempenho do modelo seja adequado para o objetivo da task.</p><h3>4. Vector Space Models</h3><p>Os Modelos de Espaço Vetorial (VSMs) são uma técnica fundamental em processamento de linguagem natural (NLP) para representar o significado de palavras e documentos em um espaço matemático contínuo. Eles são usados para capturar e representar semanticamente o contexto e a similaridade entre palavras e documentos. Os VSMs representam palavras como <strong>vetores</strong> matemáticos em um <strong>espaço de alta dimensionalidade</strong>. Cada dimensão do vetor <strong>pode representar um aspecto diferente da palavra</strong>, como seu contexto, uso ou significado.</p><p>Essas dimensões são geralmente aprendidas automaticamente a partir de grandes conjuntos de dados textuais usando técnicas como word embeddings. Ou seja, <strong>é codificado uma representação numérica de palavras que captura o significado relativo de forma contextual</strong>. Exemplo: A palavra “gato” pode ser representada por um vetor como [0.2, -0.1, 0.5, …], onde cada valor numérico representa um aspecto diferente da palavra.</p><p>Além de representar palavras, os VSMs também podem representar documentos inteiros. Nesse caso, cada documento é representado como um vetor onde cada dimensão pode representar a frequência ou importância de uma palavra ou conceito dentro desse documento. Por exemplo, um artigo de notícias sobre tecnologia pode ser representado por um vetor onde a dimensão correspondente à palavra “tecnologia” tem um valor alto, enquanto as dimensões correspondentes a palavras irrelevantes têm valores baixos.</p><p>Os VSMs são úteis para <strong>medir a similaridade semântica entre palavras e documentos</strong>. Isso é feito <strong>calculando a proximidade entre os vetores no espaço vetorial</strong>. Palavras ou documentos semanticamente semelhantes estarão mais próximos no espaço vetorial. Os vetores das palavras “cachorro” e “animal” provavelmente estarão mais próximos no espaço vetorial do que os vetores das palavras “cachorro” e “carro”.</p><p>Eles são amplamente utilizados em tarefas de NLP, como <strong>recuperação de informações (information retrieval), classificação de texto, agrupamento de documentos, tradução automática</strong> e muito mais. Eles formam a base para muitas técnicas e algoritmos modernos de NLP. Um sistema de recomendação de filmes pode usar VSMs para encontrar filmes semelhantes com base nas sinopses dos filmes já assistidos pelo usuário.</p><h4>4.1 Word by Word and Word by Doc</h4><p>Construir vetores usando uma <strong>matriz de coocorrência</strong> com um design de <strong>word by word</strong> é uma abordagem para criar <strong>representações vetoriais de palavras com base em sua coocorrência em um corpus de texto</strong>.</p><p>Para construir uma <strong>Matriz de Coocorrência</strong>, precisamos<strong>:</strong></p><ul><li><strong>Definir o Contexto</strong>: Para cada palavra no vocabulário, é necessário definir um contexto em torno dela. Por exemplo, para uma palavra “cachorro”, o contexto pode ser os N tokens à esquerda e à direita da palavra em um texto.</li><li><strong>Contar Coocorrências</strong>: Para cada palavra no vocabulário, conte quantas vezes ela ocorre no contexto de outras palavras. Isso resulta em uma matriz de coocorrência, <strong>onde cada célula (i, j) representa quantas vezes a palavra i ocorre no contexto da palavra j</strong>.</li></ul><p>A <strong>Construção dos Vetores</strong> envolve:</p><ul><li><strong>Normalização</strong>: Antes de construir os vetores, a matriz de coocorrência pode ser normalizada para considerar fatores como a frequência geral das palavras no corpus e a frequência dos próprios contextos.</li><li><strong>Construção dos Vetores</strong>: Cada linha ou coluna na matriz de coocorrência representa um vetor para uma palavra específica. Isso pode ser feito tratando cada linha ou coluna como um vetor.</li></ul><p>Suponha que temos o seguinte texto como nosso corpus:</p><blockquote>O cachorro correu no parque.<br>O gato dormiu no sofá.<br>O cachorro e o gato são amigos.</blockquote><p>Para construir a matriz de coocorrência para um contexto de duas palavras à esquerda e à direita, teríamos algo assim (excluindo palavras de parada como “o”, “e”, etc.):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/354/1*rogYDAxniJX00IJBSXWD1A.png" /></figure><p>Nesta matriz, <strong>cada linha representa uma palavra do vocabulário e cada coluna representa uma palavra do contexto</strong>. A célula (i, j) mostra <strong>quantas vezes a palavra i ocorre no contexto da palavra j</strong>.</p><p>Para construir vetores para cada palavra, normalizamos esta matriz e tratamos cada linha ou coluna como um vetor.</p><p>Uma vez que os vetores tenham sido construídos, <strong>eles podem ser usados para várias tarefas em NLP</strong>, como encontrar palavras semanticamente semelhantes, calcular distâncias entre palavras, etc.</p><p>Essa abordagem é uma maneira <strong>simples e eficaz</strong> de construir representações vetoriais de palavras usando a coocorrência em um corpus de texto. No entanto, <strong>ela pode ser limitada</strong> em capturar nuances semânticas mais complexas, <strong>especialmente em corpora muito grandes ou com vocabulários extensos</strong>.</p><p>Construir vetores usando uma matriz de coocorrência com um design de <strong>word by document</strong> é uma abordagem para criar representações vetoriais de palavras <strong>com base em sua coocorrência com documentos inteiros em um corpus de texto</strong>.</p><p>Novamente<strong>,</strong> para construir uma <strong>Matriz de Coocorrência</strong>, precisamos:</p><ul><li><strong>Definir o Contexto</strong>: Neste caso, o contexto é o documento inteiro em que uma palavra ocorre. Cada documento é tratado como uma “janela” em torno das palavras.</li><li><strong>Contar as Coocorrências</strong>: Para cada palavra no vocabulário, <strong>conte quantas vezes ela ocorre em cada documento</strong>. Isso resulta em uma matriz de coocorrência, onde <strong>cada linha representa uma palavra e cada coluna representa um documento</strong>.</li></ul><p>E para construir os vetores, também precisamos:</p><ul><li><strong>Normalização</strong>: Assim como na abordagem word by word, a matriz de coocorrência pode ser normalizada para considerar fatores como a frequência geral das palavras no corpus e a frequência dos próprios documentos.</li><li><strong>Construção dos Vetores</strong>: Neste caso, cada linha na matriz de coocorrência representa um vetor para uma palavra específica. Cada elemento do vetor pode representar a frequência ou a importância relativa da palavra em cada documento.</li></ul><p>Novamente, suponha que temos o seguinte corpus de três documentos:</p><blockquote>1. Documento 1: “O cachorro correu no parque.”<br>2. Documento 2: “O gato dormiu no sofá.”<br>3. Documento 3: “O cachorro e o gato são amigos.”</blockquote><p>A matriz de coocorrência seria algo como:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/333/1*QshzZIQzyKW_j0j0Wxw_aQ.png" /></figure><p>Para construir vetores para cada palavra, normalizamos esta matriz e tratamos cada linha como um vetor. Uma vez que os vetores tenham sido construídos, eles podem também ser usados para várias tarefas em NLP, como encontrar palavras semanticamente semelhantes, calcular distâncias entre palavras, classificação de texto, entre outros.</p><p>Esta abordagem é útil quando estamos mais interessados nas <strong>relações entre palavras e documentos</strong> do que nas relações entre palavras individuais. Ela pode capturar a essência de como palavras diferentes estão distribuídas em diferentes contextos de documentos.</p><p>No exemplo abaixo, podemos verificar a relação entre palavras e documentos, e temos que as palavras “data” estão mais relacionadas a Economy e ML do que Entertainment. Assim como a palavra “film” está mais relacionada à Entertainment que aos demais documentos. Aqui, a categoria Entertainment pode ser representada como um vetor v = [500, 700], assim como Economy como um vetor v = [6620, 4000] e ML como um vetor v = [9320, 1000]</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-HWP807gfFZmH4Up_858qw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fh5w5kHvVtOtj-KJb8Bu7Q.png" /></figure><p>Para medir o quão similiar os as palavras ou documentos são entre si, podemos utilizar a Distância Euclidiana e a Similaridade de Cosseno.</p><h4>Euclidean Distance</h4><p>A Distância Euclidiana é uma medida de distância entre dois pontos em um espaço euclidiano. É derivada do teorema de Pitágoras e é frequentemente usada em diversas áreas, como geometria, análise de dados, reconhecimento de padrões e aprendizado de máquina. Aqui está uma explicação detalhada:</p><p>A fórmula para calcular a distância euclidiana entre dois pontos <strong>P</strong> e <strong>Q</strong> em um espaço <em>n</em>-dimensional é dada por:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/258/1*HPrYvAvAUpVzWBiJiJ48-A.png" /></figure><p>Onde:</p><ul><li><strong>P = (p_1, p_2, …, p_n)</strong> e <strong>Q = (q_1, q_2, …, q_n)</strong> são os pontos no espaço <em>n</em>-dimensional.</li><li><strong>p_i </strong>e <strong>q_i</strong> são as coordenadas dos pontos <strong>P</strong> e <strong>Q</strong> ao longo da dimensão <strong>i</strong>.</li></ul><p>Essa medida de distância possui as seguintes características:</p><ul><li><strong>Positividade</strong>: A distância euclidiana é sempre não negativa, ou seja, $ EuclideanDistance(P, Q) ≥ 0.</li><li><strong>Identidade de Indiscerníveis</strong>: A distância entre dois pontos é zero se e somente se os pontos são idênticos.</li><li><strong>Simetria</strong>: A distância entre dois pontos <strong>P </strong>e <strong>Q</strong> é a mesma que a distância entre <strong>Q </strong>e <strong>P</strong>.</li><li><strong>Desigualdade Triangular</strong>: A distância de um ponto a outro ponto é sempre menor do que ou igual à soma das distâncias de um ponto a um terceiro ponto intermediário.</li></ul><p>Suponha que tenhamos dois pontos no plano 2D: <strong>P(2, 3) </strong>e <strong>Q(5, 7)</strong>. Para calcular a distância euclidiana entre esses dois pontos, usamos a fórmula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*jF_2UB17SFAGGYypJI59mA.png" /></figure><p>Portanto, a distância euclidiana entre P e Q é 5 unidades.</p><p><strong>A distância euclidiana é amplamente utilizada em algoritmos de agrupamento (como k-means), classificação (como k-NN), redução de dimensionalidade (como PCA), reconhecimento de padrões</strong>, entre outros. É uma métrica fundamental em muitos <strong>problemas de otimização</strong>, onde minimizar ou maximizar a distância entre pontos é um objetivo. Ela é é uma medida intuitiva e útil para quantificar a distância entre pontos em um espaço multidimensional.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*faxknItHZMwpWgEoSj477Q.png" /></figure><p>Podemos generalizar a encontrar a distância entre dois pontos <strong>(x_1, y_1)</strong> e <strong>(x_2, y_2)</strong> para a distância entre um vetor <strong>v</strong> de <strong>n</strong> dimensões da seguinte forma:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/173/1*rr_x9qZKa44aq5Zgwi3aRg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cT-j_CzmNYeYIFGDFBz31Q.png" /></figure><p>Para calcular a distância euclidiana entre dois vetores <em>n</em>-dimensionais em Python, podemos usar a biblioteca NumPy, que fornece funções eficientes para operações matemáticas em vetores e matrizes.</p><pre>import numpy as np<br><br>def euclidean_distance(vector1, vector2):<br>    &quot;&quot;&quot;<br>    Calcula a distância euclidiana entre dois vetores n-dimensionais.<br><br>    Argumentos:<br>    vector1 (np.array): O primeiro vetor.<br>    vector2 (np.array): O segundo vetor.<br><br>    Retorna:<br>    float: A distância euclidiana entre os dois vetores.<br>    &quot;&quot;&quot;<br>    if len(vector1) != len(vector2):<br>        raise ValueError(&quot;Os vetores devem ter o mesmo número de dimensões.&quot;)<br><br>    # Converte as listas em arrays do NumPy para cálculos eficientes<br>    vector1 = np.array(vector1)<br>    vector2 = np.array(vector2)<br><br>    # Calcula a diferença entre os dois vetores<br>    difference = vector2 - vector1<br><br>    # Calcula a soma dos quadrados das diferenças<br>    squared_difference = np.sum(difference ** 2)<br><br>    # Calcula a raiz quadrada da soma dos quadrados<br>    euclidean_distance = np.sqrt(squared_difference)<br><br>    return euclidean_distance<br><br># Exemplo de uso<br>vector1 = [2, 3, 5]<br>vector2 = [5, 7, 1]<br><br>distance = euclidean_distance(vector1, vector2)<br>print(&quot;A distância euclidiana entre os vetores é:&quot;, distance)</pre><p>Ou simplesmente</p><pre>distance = np.linalg.norm(np.array(vector1) - np.array(vector2))</pre><h4>4.3 Cosine Similarity</h4><p>A similaridade do cosseno é uma medida de similaridade entre dois vetores em um espaço vetorial, frequentemente usado em mineração de texto, recuperação de informação e aprendizado de máquina. A medida é baseada no ângulo formado entre os dois vetores no espaço vetorial e não na distância euclidiana entre eles. A similaridade do cosseno entre dois vetores $ \mathbf{A} $ e $ \mathbf{B} $ é dada pela fórmula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/213/1*c5RdjcVSX8K_LEBK3QTYYQ.png" /></figure><p>Onde:<br>- 𝐀⋅𝐁 é o produto interno (ou escalar) entre os vetores 𝐀 e 𝐁.<br>- ‖𝐀‖‖‖ e ‖𝐁‖ são as normas (ou magnitudes) dos vetores 𝐀 e 𝐁, respectivamente.</p><p>Essa medida de similaridade possui as seguintes características:</p><ul><li><strong>Intervalo de Valores</strong>: A similaridade do cosseno varia de -1 a 1, onde 1 indica que os vetores têm a mesma direção, 0 indica que os vetores são ortogonais (semelhantes de forma nula) e -1 indica que os vetores têm direções opostas.</li><li><strong>Independência de Escala</strong>: A similaridade do cosseno é <strong>independente da escala dos vetores</strong>. Isso significa que os vetores podem ser normalizados antes do cálculo sem afetar o resultado.</li><li><strong>Eficiência Computacional</strong>: O cálculo da similaridade do cosseno é computacionalmente eficiente, especialmente para vetores de alta dimensionalidade.</li><li><strong>Apropriado para Texto</strong>: É amplamente utilizado em NLP, pois lida bem com vetores de alta dimensão, como vetores de termos de documentos.</li></ul><p>Suponha que temos dois vetores <em>A</em> = [2, 1] e <em>B</em> = [3, 4]. Para calcular a similaridade do cosseno entre esses dois vetores, usamos a fórmula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/504/1*wc4vILHtBPjXPBL52379Og.png" /></figure><p>Portanto, a similaridade do cosseno entre <em>A </em>e <em>B</em> é 0.4.</p><p>A similaridade do cosseno é amplamente utilizada em tarefas de <strong>recuperação de informação</strong>, onde é usada para encontrar documentos semelhantes com base em vetores de termos de documentos. Também é usado em <strong>sistemas de recomendação</strong> para calcular a similaridade entre perfis de usuários e itens. Além disso, é usado em classificação de texto e agrupamento de documentos. A similaridade do cosseno é uma medida eficaz de similaridade entre vetores que é amplamente utilizada em várias aplicações, especialmente em processamento de linguagem natural e mineração de texto.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GD5pTNGpfT_hmGMk_1HZTg.png" /></figure><p>Se os documentos tiverem <strong>tamanhos diferentes</strong>, a métrica de Euclidean Distance <strong>não é a ideal</strong>. Cosine Similarity <strong>não é enviesada pelo pelo tamanho da diferença entre os vetores</strong>.</p><p>Para calcular a similaridade de cosseno entre dois vetores em Python, você pode usar a biblioteca NumPy para facilitar os cálculos vetoriais</p><pre>import numpy as np<br><br>def cosine_similarity(vector1, vector2):<br>    &quot;&quot;&quot;<br>    Calcula a similaridade de cosseno entre dois vetores.<br><br>    Argumentos:<br>    vector1 (np.array): O primeiro vetor.<br>    vector2 (np.array): O segundo vetor.<br><br>    Retorna:<br>    float: A similaridade de cosseno entre os dois vetores.<br>    &quot;&quot;&quot;<br>    dot_product = np.dot(vector1, vector2)<br>    norm_vector1 = np.linalg.norm(vector1)<br>    norm_vector2 = np.linalg.norm(vector2)<br>    similarity = dot_product / (norm_vector1 * norm_vector2)<br>    return similarity<br><br># Exemplo de uso<br>vector1 = np.array([2, 1])<br>vector2 = np.array([3, 4])<br><br>similarity = cosine_similarity(vector1, vector2)<br>print(&quot;A similaridade de cosseno entre os vetores é:&quot;, similarity)</pre><h4>Manipulating Words in Vector Spaces</h4><p>Podemos manipular os espaços vetoriais para fazer algumas previsões, como prever países e suas capitais, essa é uma aplicação de vetores em aprendizado de máquina e processamento de linguagem natural. Graças ao relacionamento entre os vetores, podemos verificar esse tipo de associação.</p><p>Primeiro, precisaríamos de uma representação vetorial para as palavras envolvidas — neste caso, países e suas capitais. Uma maneira comum de fazer isso é usar embeddings de palavras pré-treinados, como os disponíveis em modelos como Word2Vec, GloVe ou FastText. Esses embeddings atribuem a cada palavra um vetor numérico denso em um espaço vetorial, onde palavras <strong>semanticamente semelhantes</strong> estão próximas umas das outras. Mas podemos usar os embeddings que criamos também.</p><p>Para cada país e capital, precisaríamos de seus respectivos vetores. E, ao usarmos embeddings de palavras pré-treinados, podemos simplesmente pegar os vetores correspondentes às palavras “país” e “capital” e somá-los aos vetores de palavras correspondentes ao país e à capital real. Por exemplo, para prever a capital do Brasil, você somaria o vetor correspondente a “Brasil” ao vetor correspondente a “capital”, obtendo assim um vetor que representa a capital prevista.</p><p>Agora que você tem vetores para países e capitais, você pode usar operações vetoriais para fazer previsões. Por exemplo, para prever a capital de um país, você poderíamos calcular <strong>a diferença entre o vetor do país de interesse e o vetor de outro país conhecido com sua capital conhecida</strong>. Em seguida, você <strong>adiciona esse vetor de diferença ao vetor da capital conhecida para obter a capital prevista</strong>.</p><p>Por exemplo, suponha que você tenha embeddings de palavras onde “Brasil” é representado pelo vetor v_Brasil e “capital” pelo vetor v_capital. Para prever-mos a capital do Brasil, faríamos o seguinte:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/450/1*ivlRsR08wMFMbPigbNnNIA.png" /></figure><p>Outra forma de prever tokens usando as relações vetoriais, é <strong>usar as representações conhecidas</strong>. Sabendo que a capital dos USA é Washington, podemos ver a distância entre os dois vetores e somar ao vetor que representa Russia para descobrir sua capital, Como:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DzmK_rSI13vfK6wKilHbPg.png" /></figure><p>Depois de fazer as previsões, podemos <strong>avaliar a precisão</strong> do modelo usando um conjunto de dados de teste que contenha pares de países e suas capitais reais. Se a precisão não for satisfatória, você pode ajustar os vetores ou usar técnicas mais avançadas de aprendizado de máquina para melhorar o desempenho do modelo.</p><p>Manipular vetores usando aritmética para prever países e suas capitais (ou qualquer relacionamento parecido) envolve representar países e capitais como vetores, realizar operações vetoriais para fazer previsões e avaliar o desempenho do modelo.</p><p>Outro exemplo bem famoso é o do é o do “king — man + woman = queen”, quem ao somar e subtrair os valores correspondentes a eles, acabaríamos ficando próximo no espaço multidimensional do vetor que representa “queen”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*eGYoTR6AXBHFDJUCXARn_w.jpeg" /></figure><h4>4.5 Principal Component Analysis</h4><p>A Análise de Componentes Principais (PCA) é um algoritmo não supevisionado, <strong>uma técnica de redução de dimensionalidade</strong> amplamente utilizada para <strong>simplificar conjuntos de dados complexos</strong>, mantendo as informações mais importantes. Funciona encontrando as direções (ou componentes) de maior variância nos dados e projetando os dados nesses novos eixos, chamados de componentes principais. Ele funciona da seguinte forma:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UVAeqoDQLOM6p8gk-o8bIA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vSsDkt1SEMSyBoexa8b1bw.png" /></figure><ul><li><strong>Centralização dos Dados</strong>: Antes de aplicar o PCA, os dados são geralmente centralizados subtraindo a média de cada variável. Isso garante que o centro dos dados esteja na origem do espaço de características.</li><li><strong>Cálculo da Matriz de Covariância</strong>: Em seguida, é calculada a matriz de covariância dos dados centralizados. A covariância entre duas variáveis mede como elas variam juntas. A matriz de covariância captura as relações lineares entre as variáveis originais.</li><li><strong>Decomposição da Matriz de Covariância</strong>: A seguir, a matriz de covariância é decomposta em seus autovetores e autovalores. Os autovetores representam as direções dos eixos principais (ou componentes principais) dos dados, enquanto os autovalores representam a quantidade de variância explicada por cada componente principal.</li><li><strong>Seleção dos Componentes Principais</strong>: Os autovetores são ordenados de acordo com seus autovalores associados, do maior para o menor. Os autovetores com os maiores autovalores capturam a maior parte da variância nos dados e são selecionados como os componentes principais mais importantes.</li><li><strong>Projeção dos Dados</strong>: Finalmente, os dados são projetados nos novos eixos definidos pelos componentes principais selecionados. Isso reduz a dimensionalidade dos dados, substituindo as variáveis originais por uma combinação linear dos componentes principais.</li></ul><p>Vamos considerar um conjunto de dados bidimensional com duas variáveis, como altura e peso de uma população. O PCA encontrará a direção ao longo da qual a variabilidade dos dados é máxima. Suponha que a direção seja dada pelo autovetor [0.8, 0.6], com um autovalor associado de 10. Isso significa que 80% da variabilidade dos dados está ao longo dessa direção. Os dados podem ser projetados nessa direção, reduzindo-os de duas dimensões para uma dimensão. Podemos usá-lo para:</p><ul><li><strong>Redução de dimensionalidade</strong>: PCA é usado para reduzir a dimensionalidade de dados complexos, preservando o máximo de informação possível.</li><li><strong>Visualização de dados</strong>: PCA pode ser usado para visualizar dados de alta dimensionalidade em um espaço de menor dimensão.</li><li><strong>Pré-processamento de dados</strong>: PCA é frequentemente usado como uma etapa de pré-processamento antes de aplicar algoritmos de aprendizado de máquina, para reduzir o tempo de treinamento e evitar a maldição da dimensionalidade.</li></ul><p>O PCA é uma técnica poderosa e amplamente utilizada para redução de dimensionalidade e análise exploratória de dados, que ajuda a simplificar conjuntos de dados complexos, mantendo as informações mais importantes.</p><p>Sua implementação também é muito simples</p><pre>import numpy as np<br>import matplotlib.pyplot as plt<br>from sklearn.decomposition import PCA<br><br># Passo 2: Gere um conjunto de dados de exemplo com vetores de alta dimensão (d = 10)<br>np.random.seed(42)<br>n_samples = 1000<br>d = 10<br>X = np.random.randn(n_samples, d)  # Gera 1000 vetores de dimensão 10 com distribuição normal<br><br># Passo 3: Aplique o PCA para reduzir a dimensionalidade dos vetores para 2 dimensões<br>pca = PCA(n_components=2)<br>X_2d = pca.fit_transform(X)<br><br># Passo 4: Plote os vetores reduzidos em um gráfico de dispersão para visualização<br>plt.figure(figsize=(8, 6))<br>plt.scatter(X_2d[:, 0], X_2d[:, 1], alpha=0.5)<br>plt.title(&#39;Visualização dos Vetores Reduzidos com PCA em 2 Dimensões&#39;)<br>plt.xlabel(&#39;Componente Principal 1&#39;)<br>plt.ylabel(&#39;Componente Principal 2&#39;)<br>plt.grid(True)<br>plt.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/414/1*47saQ87ShbWP7WYp6ntIgg.png" /></figure><p><strong>PCA é comumente usada para reduzir a dimensão dos seus dados.</strong> Intuitivamente, <strong>o modelo colapsa os dados através dos componentes principais</strong>. Você pode pensar no primeiro componente principal (em um conjunto de dados 2D) como a linha onde há a maior quantidade de variância. Você pode então colapsar os pontos de dados nessa linha. Portanto, você passou de 2D para 1D. Podemos generalizar essa intuição para várias dimensões.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a4i1TP_SDz--osM8W_uwHQ.png" /></figure><p>Com isso, acabamos gerando os <strong>Eigenvectors (autovetores) e Eigenvalues (autovalores)</strong>, que são:</p><ul><li><strong>Eigenvectors</strong>: os vetores resultantes, também conhecidos como características não correlacionadas dos seus dados.</li><li><strong>Eigenvalues</strong>: a quantidade de informação retida por cada nova característica. Você pode pensar nisso como a variância no vetor próprio.</li></ul><p>Podemos seguir os seguintes passos para Calcular a PCA:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SesU8y90lFLJzZ_aO2NegQ.png" /></figure><p>1. Normalizar a média dos seus dados.<br>2. Calcular a matriz de covariância.<br>3. Calcular SVD na sua matriz de covariância. Isso retorna [USV] = svd(Σ) (Sigma). As três matrizes <strong>U, S, V</strong> são desenhadas acima. U é rotulado com os vetores próprios, e S é rotulado com os autovalores.</p><p>Podemos então usar as primeiras <em>n</em> colunas do vetor <em>U</em>, para obter seus novos dados multiplicando <em>XU</em>[:, 0:n].</p><h3>Conclusão</h3><p>Este artigo focou somente no <a href="https://brasileiraspln.com/livro-pln/2a-edicao/parte-introducao/cap-introducao/cap-introducao.html#sec-cap-introducao-sec3"><strong>paradígma estatístico</strong></a><strong>. </strong>Nele, foi abordado como extrair features e realizar o preprocessamento de dados, os modelos de regressão logística e algoritmo naive bayes, e como aplicá-los na tarefa de classificação de texto para análise de sentimento, foi descrito o que são Vector Space Models, como eles representam palavras e documentos e como construir matrizes de coocorrencia no formato word by word e word by document, como avaliar a similaridade de vetores com a distância euclidiana e similaridade de cosseno, como manipular palavras nos espaços vetoriais e o algoritmo Principal Component Analysis e como utilizá-lo.</p><h3>Como Citar</h3><blockquote>Guimarães, Alysson. (Jun 2024). Natural Language Processing (NLP) Com Classificação e Espaços Vetoriais. https://medium.com/data-hackers/. https://medium.com/data-hackers/p-51291c08f29e</blockquote><p>ou</p><pre>@miscellaneous{guimaraes2024nlpvec,<br> title = {Natural Language Processing Com Classificação e Espaços Vetoriais},<br> author = {Guimarães, Alysson},<br> journal = {https://medium.com/data-hackers/},<br> year = {2024},<br> month = {Jun},<br> url = {https://k3ybladewielder.medium.com/p-51291c08f29e}<br>}</pre><h3>Referência</h3><ul><li>Natural Language Processing with Classification and Vector Spaces, disponível em <a href="https://www.coursera.org/learn/classification-vector-spaces-in-nlp">https://www.coursera.org/learn/classification-vector-spaces-in-nlp</a></li></ul><h3>Licença</h3><ul><li>CC BY-SA 2.0 LEGAL CODE. Attribution-ShareAlike 2.0 Generic</li><li>Para detalhes sobre a licença, verifique <a href="https://creativecommons.org/licenses/by-sa/2.0/legalcode">https://creativecommons.org/licenses/by-sa/2.0/legalcode</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=51291c08f29e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/natural-language-processing-nlp-com-classifica%C3%A7%C3%A3o-e-espa%C3%A7os-vetoriais-51291c08f29e">Natural Language Processing (NLP) Com Classificação e Espaços Vetoriais</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Summary ]]></title>
            <link>https://k3ybladewielder.medium.com/summary-6300edb866e7?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/6300edb866e7</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[decision-science]]></category>
            <category><![CDATA[cognitive-science]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Wed, 21 Feb 2024 18:48:11 GMT</pubDate>
            <atom:updated>2024-02-21T18:48:11.710Z</atom:updated>
            <content:encoded><![CDATA[<p>repository of the articles translated to EN-US.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*Q3pRMx_sWr5WPj5d" /></figure><h3>STATISTIC</h3><ul><li>Statistics for Data Science Part 2/8</li><li>Statistics for Data Science Part 1/8</li><li>Introduction to Causal Inference</li></ul><h3>DATA SCIENCE, MACHINE LEARNING AND AI</h3><ul><li><a href="https://cognitivemachine.medium.com/tree-based-supervised-learning-methods-c87c61360d67?source=user_profile---------4----------------------------">Tree-Based Supervised Learning Methods</a></li><li><a href="https://cognitivemachine.medium.com/regression-smoothing-methods-845c6eeb1199?source=user_profile---------5----------------------------">Regression Smoothing Methods</a></li><li><a href="https://cognitivemachine.medium.com/principles-of-linear-regression-4ef44fea7fc8?source=user_profile---------7----------------------------">Principles of Linear Regression</a></li></ul><h3>COGNITIVE SCIENCE</h3><ul><li>Philosophical Approaches to Cognitive Science</li><li>Cognitive Science 🧠</li></ul><h3>DECISION THEORY</h3><ul><li>Introduction to Decision Theory</li></ul><h3>NATURAL LANGUAGE PROCESSING (NLP)</h3><ul><li>Reasoning in Pre-trained Language Models (PLMs)</li><li><a href="https://cognitivemachine.medium.com/competitive-intelligence-with-topic-modelling-9e7f723f1799?source=user_profile---------0----------------------------">Competitive Intelligence with Topic Modeling</a></li><li><a href="https://cognitivemachine.medium.com/topic-modelling-techniques-f1ce0d0c3262?source=user_profile---------1----------------------------">Topic Modeling Techniques</a></li><li><a href="https://cognitivemachine.medium.com/conceitos-de-nlp-8a5c5c2a0b9b?source=user_profile---------2----------------------------">NLP Concepts</a></li><li><a href="https://cognitivemachine.medium.com/introduction-to-nlp-c62d04ea903f?source=user_profile---------3----------------------------">Introduction to NLP</a></li></ul><p>Follow my profile and sign up <a href="https://medium.com/m/signin?actionUrl=%2F_%2Fapi%2Fsubscriptions%2Fnewsletters%2Fec554a52fda0&amp;operation=register&amp;redirect=https%3A%2F%2Fk3ybladewielder.medium.com%2F&amp;newsletterV3=ab4f983cc1ab&amp;newsletterV3Id=ec554a52fda0&amp;user=Alysson+Guimar%C3%A3es&amp;userId=ab4f983cc1ab">here</a> to receive the next posts on the topic, and contribute to the relevance of the post with 👏👏👏 after reading.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6300edb866e7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deep Neural Network com Pytorch]]></title>
            <link>https://medium.com/data-hackers/deep-neural-network-com-pytorch-552abf6bed10?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/552abf6bed10</guid>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[pytorch]]></category>
            <category><![CDATA[dnn]]></category>
            <category><![CDATA[deep-neural-networks]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Tue, 20 Feb 2024 23:38:46 GMT</pubDate>
            <atom:updated>2024-06-07T19:01:36.694Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j17HbNrVgrdO9-vS" /><figcaption><strong>Generated with AI</strong> ∙ February 14, 2024 at 7:15 PM</figcaption></figure><p><strong>PyTorch</strong> é uma biblioteca de código aberto escrita em Python e C++ para computação numérica acelerada por GPU, principalmente usada para aplicações de <strong>deep learning</strong>. Ele fornece as principais funcionalidades necessárias para o desenvolvimento de redes neurais, como suporte multidimensional tensor (matrizes multi-dimensional) e operadores diferenciais automáticos. Esse framework possue diversas vantagens, a citar algumas:</p><ul><li>Facilidade no prototipagem e debugging devido à sua interface dinâmica;</li><li>Suporte nativo para computação paralela através da utilização de GPUs;</li><li>Integração com outras ferramentas populares do ecossistema do Python,como NumPy e SciPy;</li><li>Além disso tem comunidade ativa de desenvolvedores contribuindo com novas features e soluções para problemas específicos.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/855/1*jppeTvG9Yb_5YFnDXJlXdw.png" /></figure><p>O PyTorch foi criado pelo Facebook AI Research Lab (FAIR), mas hoje em dia tem um grande número de colaboradores, incluindo empresas líderes na indústria como NVIDIA, Google, Microsoft e IBM.</p><p>Os modelos de deep learning criados usando o PyTorch podem ser aplicados em uma variedade de tarefas e domínios em diversas indústrias, como:</p><h4>Visão Computacional:</h4><ul><li>Classificação de imagens: Identificação de objetos em fotografias, como animais, veículos, pessoas e outros elementos presentes no conjunto de dados ImageNet.</li><li>Detecção de Objetos: Localização exata de objetos em imagens, geralmente expressa em caixas delimitadoras. Modelos notáveis incluem YOLO (You Only Look Once) e SSD (Single Shot Multibox Detector).</li><li>Segmentação de Imagens: Divisão de imagens em regiões correspondentes a classes distintas, como pixel por pixel classificação.</li><li>Geração de Imagens: Produção de imagens artísticas, hiperrealistas ou fantásticas geradas por meio de redes neuronais condicionadas por entrada textual ou aleatória.</li></ul><h4>Processamento Natural de Linguagem (NLP)</h4><ul><li>Tradução Automática: Conversão de texto escrito em uma língua para outra língua usando modelos como Transformer e Seq2Seq.</li><li>Resposta de Perguntas (QA): Previsão de respostas a perguntas feitas em diversos idiomas.</li><li>Text Summarization: Gerar sumários curtos e concisos de documentos longos preservando informações importantes.</li><li>Chatbots: Construção de assistentes virtuais capazes de compreender e responder consultas de usuários de formas naturais e contextualmente relevantes.</li></ul><h4>Áudio e Processamento de Sinal</h4><ul><li>Reconhecimento de Falas: Interpretação da fala humana convertendo ondas sonoras em transcrições textuais, usado em sistemas de dictâfonos digitais, smartphones e aparelhos auditivos.</li><li>Separação de Fontes Sonoras (Audio Source Separation): Capacidade de separar sons misturados, isolando fontes musicais individuais ou falas de conversas complexas.</li><li>Música Generativa: Criação de músicas originais através de composição e arranjos gerados por máquinas, inspirados em estilos clássicos ou contemporâneos.</li></ul><h4>Jogos e Robótica</h4><ul><li>Reinforcement Learning: Treino de agentes inteligentes que tomam decisões autônomas enfrentando desafios em jogos como Go, poker e StarCraft II.</li><li>Animação e Robótica: Controle de personagens virtuais, manipulação de robôs industriais e assistência a deficientes visuais em ambientes urbanos movimentados.</li></ul><p>Este artigo está dividido em dois grandes blocos, e neles, abordaremos os seguintes tópicos:</p><h4>Fundamentos de Machine Learning com PyTorch</h4><p>— Tensores<br> — Derivada no PyTorch<br> — Gradiente Descendente Estocástico (SGD)<br>— Gradiente Descendente com Momentum<br> — Mini-Batch Gradient Descent<br> — Normalização em Lote<br> — Divisão em Treinamento, Validação e Teste</p><h4>Redes Neurais:</h4><p>— Redes Neurais Rasas<br> — Redes Neurais Profundas<br>— nn.ModuleList()<br> — Funções de Ativação<br> — Backpropagation<br> — O problema do “vanishing gradient”<br> — Dropout<br> — Pesos e Inicialização da Rede Neural</p><h3>Fundamentos de Machine Learning com PyTorch</h3><h4>Tensores</h4><p>Tensores são as estruturas fundamentais que representam dados multi-dimensionais. Eles são semelhantes aos arrays ou matrizes. Eles são os <em>building blocks</em> e a base para construção de e manipulação de modelos de <strong><em>machine learning</em></strong> e <strong><em>deep learning</em></strong>.</p><p>Os tensores são semelhantes aos arrays do NumPy e, muitas vezes, podem ser convertidos de um para o outro. Isso facilita a integração com bibliotecas científicas em Python. Além disso o PyTorch permite a computação em GPUs para acelerar operações, e eles podem ser movidos para uma GPU para aproveitar o poder de processamento paralelo. Ele fornece uma ampla variedade de operações matemáticas que podem ser aplicadas a tensores, isso inclui operações aritméticas, funções trigonométricas, álgebra linear, entre outras.</p><p>Uma característica fundamental do Pytorch é o sistema de <strong>autograd</strong>, que automaticamente calcula gradientes para tensores. Isso é crucial para a otimização de modelos de aprendizado de máquina.</p><p>Podemos criar tensores de várias maneiras, seja inicializando-os com valores específicos, gerando números aleatórios, ou a partir de dados existentes. Eles são úteis para <strong>representar dados ao longo de uma única dimensão</strong>, como séries temporais, uma linha de pixels de uma imagem ou um conjunto de valores.</p><pre>import torch<br><br># Criando um tensor 1D<br>tensor_1d = torch.tensor([1, 2, 3, 4, 5])<br><br># Acessando elementos do tensor<br>print(tensor_1d[0])  # Saída: 1<br>print(tensor_1d[2])  # Saída: 3<br><br># Operações matemáticas em tensores 1D<br>tensor_resultado = tensor_1d * 2<br>print(tensor_resultado)  <br><br># Saída: tensor([2, 4, 6, 8, 10])</pre><p>Neste exemplo, tensor_1d é um tensor 1D contendo os valores de 1 a 5. Podemos acessar elementos individualmente e realizar operações matemáticas diretamente nos tensores. Os tensores 1D são frequentemente usados em problemas onde os dados estão organizados de forma linear, e são a base para a construção de estruturas de dados mais complexas, como matrizes bidimensionais (tensores 2D) e tensores de ordens superiores.</p><p>Tensores bidimensionais em PyTorch são estruturas de dados que representam matrizes, ou seja, arranjos retangulares de elementos organizados em duas dimensões. Eles são comumente utilizados para representar dados tabulares, imagens, ou qualquer outra informação que possa ser organizada em linhas e colunas.</p><pre>import torch<br><br># Criando um tensor bidimensional (matriz)<br>tensor_2d = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])<br><br># Acessando elementos do tensor bidimensional<br>print(tensor_2d[0, 1])  # Saída: 2 (linha 0, coluna 1)<br><br># Operações matemáticas em tensores bidimensionais<br>tensor_resultado = tensor_2d * 2<br>print(tensor_resultado)<br># Saída:<br># tensor([[ 2,  4,  6],<br>#         [ 8, 10, 12],<br>#         [14, 16, 18]])</pre><p>Neste exemplo, tensor_2d é uma matriz 3x3 com elementos de 1 a 9. Eles são geralmente usados em tarefas de processamento de imagens, onde cada elemento da matriz pode representar um pixel. Eles também são essenciais para a construção e treinamento de modelos de aprendizado de máquina, especialmente redes neurais profundas, onde as entradas muitas vezes são representadas por tensores bidimensionais.</p><pre>## Esses são apenas alguns exemplos.<br># Criando dois tensores bidimensionais<br>import torch<br>tensor_a = torch.tensor([[1, 2, 3], [4, 5, 6]])<br>tensor_b = torch.tensor([[7, 8, 9], [10, 11, 12]])<br><br># Soma de tensores<br>soma = tensor_a + tensor_b<br>print(&quot;Soma:&quot;)<br>print(soma)<br><br># Subtração de tensores<br>subtracao = tensor_a - tensor_b<br>print(&quot;\nSubtração:&quot;)<br>print(subtracao)<br><br># Multiplicação de tensores (element-wise)<br>multiplicacao_elementwise = tensor_a * tensor_b<br>print(&quot;\nMultiplicação (element-wise):&quot;)<br>print(multiplicacao_elementwise)<br><br># Multiplicação de tensores (produto matricial)<br>produto_matricial = torch.matmul(tensor_a, tensor_b.T)  # Transpondo tensor_b para alinhar dimensões*<br>print(&quot;\nProduto Matricial:&quot;)<br>print(produto_matricial)<br><br># Operações de redução (somando elementos ao longo das colunas)<br>soma_colunas = torch.sum(tensor_a, dim=0)<br>print(&quot;\nSoma das Colunas:&quot;)<br>print(soma_colunas)<br><br># Operações de broadcast (somando uma constante a cada elemento)<br>tensor_soma_constante = tensor_a + 10<br>print(&quot;\nSoma com Constante:&quot;)<br>print(tensor_soma_constante)<br><br># Saída<br># Soma:<br># tensor([[ 8, 10, 12],<br>#        [14, 16, 18]])<br>#<br># Subtração:<br># tensor([[-6, -6, -6],<br>#       [-6, -6, -6]])<br>#<br># Multiplicação (element-wise):<br># tensor([[ 7, 16, 27],<br>#        [40, 55, 72]])<br>#<br># Produto Matricial:<br># tensor([[ 50,  68],<br>#        [122, 167]])<br>#<br># Soma das Colunas:<br># tensor([5, 7, 9])<br>#<br># Soma com Constante:<br># tensor([[11, 12, 13],<br>#        [14, 15, 16]])</pre><p>Os tensores tridimensionais (tensores 3D) são utilizados para representar dados que possuem uma organização tridimensional, como volumes de imagens ou séries temporais multivariadas.</p><pre># Criando um tensor 3D (volume)<br>tensor_3d = torch.tensor([[[1, 2, 3],[4, 5, 6]],<br>                          [[7, 8, 9], [10, 11, 12]],<br>                          [[13, 14, 15], [16, 17, 18]]])<br><br># Acessando elementos do tensor 3D<br>print(&quot;Acessando um elemento:&quot;)<br>print(tensor_3d[1, 0, 2])  # Saída: 9 (depth 1, linha 0, coluna 2)<br><br># Operações matemáticas em tensores 3D<br>tensor_resultado = tensor_3d * 2<br>print(&quot;\nMultiplicação por 2:&quot;)<br>print(tensor_resultado)<br><br># Operações de redução (somando elementos ao longo das dimensões)<br>soma_dim1 = torch.sum(tensor_3d, dim=1)<br>print(&quot;\nSoma ao Longo da Dimensão 1:&quot;)<br>print(soma_dim1)<br><br># Operações de broadcast (somando uma constante a cada elemento)<br>tensor_soma_constante = tensor_3d + 10<br>print(&quot;\nSoma com Constante:&quot;)<br>print(tensor_soma_constante)<br><br># Saída<br># Acessando um elemento:<br># tensor(9)<br>#<br>#Multiplicação por 2:<br># tensor([[[ 2,  4,  6],<br>#         [ 8, 10, 12]],<br>#<br>#        [[14, 16, 18],<br>#         [20, 22, 24]],<br>#<br>#        [[26, 28, 30],<br>#         [32, 34, 36]]])<br>#<br># Soma ao Longo da Dimensão 1:<br># tensor([[ 5,  7,  9],<br>#        [17, 19, 21],<br>#        [29, 31, 33]])<br>#<br># Soma com Constante:<br># tensor([[[11, 12, 13],<br>#         [14, 15, 16]],<br>#<br>#        [[17, 18, 19],<br>#         [20, 21, 22]],<br>#<br>#        [[23, 24, 25],<br>#         [26, 27, 28]]])</pre><p>Neste exemplo, tensor_3d representa um volume 3D com dimensões 3x2x3. Podemos acessar os elementos individualmente usando índices em cada dimensão, e as operações matemáticas podem ser feitas de forma parecida aos feitos com tensores 2D. Eles são frequentemente encontrados em problemas de visão computacional para representar volumes de dados tridimensionais, como pilhas de imagens ou volumes médicos.</p><h4>Derivada no Pytorch</h4><p>A derivada e a derivada parcial são conceitos fundamentais em cálculo diferencial, e desempenham um papel crucial na compreensão do comportamento de funções em uma ou mais variáveis.</p><p>A derivada de uma função em relação a uma variável representa a taxa de variação instantânea da função em relação a essa variável. Em outras palavras, indica como a função está mudando em um ponto específico do domínio. A notação matemática padrão para a derivada de uma função <em>f</em>(x) em relação a x é <em>f</em>′(x) ou (∂<em>f/</em>∂x). Geometricamente, a derivada no ponto x é equivalente à inclinação da tangente à curva da função nesse ponto. Para entender a intuição, imagine que você está dirigindo em um carro e olhando para o velocímetro. A velocidade do carro está mudando constantemente conforme você dirige. <strong>A derivada de sua posição em relação ao tempo seria como a leitura instantânea do velocímetro</strong>, indicando a <strong>velocidade exata em que você está se movendo naquele momento</strong>. A derivada de uma função em um ponto nos diz <strong>como a função está mudando nesse ponto</strong>. Se a função representa a posição de um objeto ao longo do tempo, a derivada seria a velocidade instantânea.</p><p>Já a <strong>derivada parcial</strong> de uma função de várias variáveis em relação a uma variável específica mede a taxa de variação da função em relação a essa variável, mantendo todas as outras variáveis constantes. A notação para a derivada parcial de uma função <em>f</em>(x,y,…) em relação a uma variável x é ∂<em>f/</em>∂x. Aqui, ∂ é usado para <strong>indicar uma derivada parcial</strong>. Analogamente, a derivada parcial em relação a y seria ∂<em>f/</em>∂y, e assim por diante. Novamente, vamos tentar entender a intuição: Imagine que você está em um terreno acidentado e a temperatura varia em diferentes partes desse terreno. A derivada parcial em relação à temperatura em um determinado ponto seria como <strong>medir o quanto a temperatura muda nesse ponto específico</strong>, mantendo todas as outras variáveis (como a posição) constantes. Em termos mais gerais, a derivada parcial de uma função de várias variáveis em relação a uma delas nos dá a taxa de variação da função em relação a uma variável específica, mantendo as outras constantes. É como analisar como uma feature específica influencia o comportamento da função, enquanto outras feature permanecem inalteradas.</p><p>Ambos os conceitos, derivada e derivada parcial, ajudam a entender como as coisas mudam em relação a uma variável ou a uma mudança em uma única dimensão, seja em trajetórias de carros ou em paisagens de temperatura. Essas ideias são fundamentais para modelagem matemática e são amplamente aplicadas em diversas áreas, desde física até aprendizado de máquina.</p><p>A derivada é uma generalização da ideia de taxa de variação para funções de uma única variável, enquanto a derivada parcial lida com funções de várias variáveis, permitindo-nos entender como a função muda ao longo de cada dimensão independente.</p><p>A fórmula para a derivada parcial em relação a uma variável específica é semelhante à fórmula da derivada para funções de uma variável, considerando todas as outras variáveis como constantes. Essas ferramentas são fundamentais para a análise e otimização de funções em problemas matemáticos e científicos.</p><p>A diferenciação automática é uma feature fundamental em PyTorch que permite calcular automaticamente gradientes em relação às variáveis de interesse. Isso é especialmente útil em aprendizado de máquina, onde <strong>otimizar modelos</strong> envolve ajustar parâmetros para <strong>minimizar ou maximizar uma função de perda</strong>. O mecanismo de diferenciação automática em PyTorch é implementado pelo módulo autograd.</p><pre>import torch<br><br># Variáveis que requerem gradiente<br>x = torch.tensor([2.0], requires_grad=True)<br>y = torch.tensor([3.0], requires_grad=True)<br><br># Operações envolvendo as variáveis<br>z = x**2 + y**3<br><br># Calcular gradientes<br>z.backward()<br><br># Exibir gradientes<br>print(&quot;Gradiente em relação a x:&quot;, x.grad)<br>print(&quot;Gradiente em relação a y:&quot;, y.grad)<br><br># Saída<br># Gradiente em relação a x: tensor([4.])<br># Gradiente em relação a y: tensor([27.])</pre><p>Neste exemplo, x e y são tensores que requerem gradiente (requires_grad=True). A expressão z = x**2 + y**3 é calculada, e em seguida, chamamos z.backward() para calcular automaticamente os gradientes de z em relação a x e y. Os gradientes são então acessados através dos atributos .grad das variáveis.</p><p>Este é um exemplo muito simples, mas em modelos de aprendizado de máquina, esses gradientes são essenciais para a retropropagação (backpropagation) durante o treinamento.</p><pre>import torch<br>import torch.optim as optim<br><br># Dados de entrada e saída desejada<br>x_data = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=False)<br>y_data = torch.tensor([2.0, 4.0, 6.0, 8.0], requires_grad=False)<br><br># Inicializando parâmetros<br>w = torch.tensor([1.0], requires_grad=True)<br>b = torch.tensor([0.0], requires_grad=True)<br><br># Definindo a função linear<br>def modelo_linear(x):<br>    return w * x + b<br><br># Função de perda (erro quadrático médio)<br>def loss_function(y_pred, y_actual):<br>    return torch.mean((y_pred - y_actual)**2)<br><br># Configurando o otimizador<br>optimizer = optim.SGD([w, b], lr=0.01)<br><br># Treinamento do modelo<br>for epoch in range(100):<br>    # Forward pass<br>    y_pred = modelo_linear(x_data)<br><br>    # Cálculo da perda<br>    loss = loss_function(y_pred, y_data)<br><br>    # Retropropagação e otimização<br>    optimizer.zero_grad()  # Zera os gradientes acumulados<br>    loss.backward()        # Calcula gradientes<br>    optimizer.step()        # Atualiza parâmetros<br><br># Exibindo parâmetros finais<br>print(&quot;Parâmetros finais: w =&quot;, w.item(), &quot;, b =&quot;, b.item())<br># Parâmetros finais: w = 1.9231737852096558 , b = 0.22587870061397552</pre><h4>Gradiente Descendente Estocástico</h4><p>O Gradiente Descendente Estocástico (SGD, do inglês Stochastic Gradient Descent) é uma variação do algoritmo de otimização do Gradiente Descendente, frequentemente usado no treinamento de modelos de aprendizado de máquina e redes neurais. A principal diferença entre o Gradiente Descendente Estocástico e o Gradiente Descendente clássico <strong>está na forma como as atualizações dos parâmetros do modelo são realizadas</strong>.</p><p>No <strong>Gradiente Descendente clássico</strong>, a atualização dos parâmetros é realizada utilizando o gradiente médio dos parâmetros sobre o conjunto de dados de treinamento completo. A função de custo é calculada considerando todos os exemplos de treinamento.</p><p>Já no <strong>Gradiente Descendente Estocástico (SGD)</strong>, a atualização dos parâmetros é realizada para <strong>cada exemplo de treinamento</strong> (ou em pequenos lotes (mini lotes)). Ou seja, em vez de calcular o gradiente médio, o gradiente é calculado apenas para um exemplo <strong>aleatório</strong> a cada iteração. Isso acaba introduzindo <strong>estocasticidade (aleatoriedade)</strong> no processo de atualização dos parâmetros. Isso trás vantagens como</p><ul><li><strong>Eficiência Computacional:</strong> Utilizar apenas um exemplo ou mini lote por vez é computacionalmente eficiente, especialmente em conjuntos de dados grandes.</li><li><strong>É Mais Rápido para Convergência:</strong> A aleatoriedade introduzida pode ajudar a escapar de mínimos locais e acelerar a convergência, especialmente em funções de custo não convexas.</li></ul><p>Mas também tras desvantanges como:</p><ul><li><strong>Estocasticidade:</strong> Devido à estocasticidade, o SGD pode levar a uma convergência mais ruidosa. A função de custo pode variar mais ao longo do tempo.</li><li><strong>Taxa de Aprendizado Ajustável:</strong> A escolha apropriada da taxa de aprendizado é crucial. Pode ser necessário ajustar dinamicamente a taxa de aprendizado durante o treinamento.</li></ul><pre>import torch<br>import torch.optim as optim<br><br># Definindo um modelo e uma função de custo<br>model = torch.nn.Linear(2, 1)<br>criterion = torch.nn.MSELoss()<br><br># Definindo um otimizador SGD<br>optimizer = optim.SGD(model.parameters(), lr=0.01)<br><br># Exemplo de treinamento com SGD<br>for epoch in range(num_epochs):<br>    for inputs, labels in data_loader:  # Iterando sobre mini lotes<br>        optimizer.zero_grad()<br>        outputs = model(inputs)<br>        loss = criterion(outputs, labels)<br>        loss.backward()<br>        optimizer.step()</pre><p>Neste exemplo, optim.SGD é usado como otimizador para aplicar o Gradiente Descendente Estocástico durante o treinamento. O loop aninhado sobre mini lotes ilustra o conceito de atualização de parâmetros para cada exemplo ou mini lote.</p><h4>Gradient Descent com Momentum</h4><p>O <strong>Gradiente Descendente com Momentum</strong> é uma variação do algoritmo básico de Gradiente Descendente, projetada para <strong>acelerar a convergência</strong> e lidar melhor com regiões de superfície de perda complicadas. Este método incorpora um conceito chamado “momentum”, inspirado pela física.</p><p>A ideia do momentum é simular o comportamento de uma bola rolando por uma superfície de perda. Quando a bola começa a rolar em uma direção, ela acumula momentum e ganha velocidade. Isso ajuda a superar pequenos obstáculos e acelerar em direção ao mínimo global.</p><p>A atualização dos pesos usando o Gradiente Descendente com Momentum é dada pela seguinte fórmula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/224/1*v3QSTwYlhXogO5o_TE9uVA.png" /></figure><p>onde:</p><ul><li>θ_t são os parâmetros (pesos) no passo de iteração t,</li><li>∇J(θ_t) é o gradiente da função de perda J em relação aos parâmetros θ_t1,</li><li>α é a taxa de aprendizado ou <strong>learning rate</strong>,</li><li>β é o coeficiente de momentum, geralmente entre 0 e 1,</li><li>v_t é o vetor de momentum no passo t.</li></ul><p>A introdução do termo β⋅v_t na atualização dos pesos permite que o momentum acumule ao longo das iterações. Isso é especialmente útil quando o gradiente varia de direção, permitindo que o modelo ganhe impulso em direção ao mínimo global.</p><p>A principal vantagem do Gradiente Descendente com Momentum é sua capacidade de acelerar o treinamento, especialmente em áreas onde o gradiente oscila ou possui muitos mínimos locais. Essa técnica pode ajudar a superar regiões planas na função de perda e escapar de mínimos locais.</p><p>Em PyTorch, você pode implementar o Gradiente Descendente com Momentum usando o otimizador torch.optim.SGD com o argumento momentum.</p><pre>import torch<br>import torch.optim as optim<br><br># Definindo parâmetros<br>learning_rate = 0.01<br>momentum = 0.9<br><br># Criando um otimizador com momentum<br>optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)<br><br># No loop de treinamento, você usaria optimizer.step() para atualizar os pesos<br># e optimizer.zero_grad() para zerar os gradientes antes de uma nova backward pass.</pre><p>A adição de momentum pode ser benéfica em muitos casos, mas é importante ajustar a taxa de aprendizado e o coeficiente de momentum para obter o melhor desempenho em um problema específico.</p><p>O valor típico de 0.9 para o coeficiente de momentum é uma escolha empírica comum que muitas vezes funciona bem em prática. A escolha do valor específico para o coeficiente de momentum pode depender do problema, da arquitetura da rede e da natureza do conjunto de dados. Vários valores, como 0.9, 0.99, ou até mesmo outros, podem ser experimentados para encontrar o melhor desempenho em um determinado contexto.</p><p>O momentum é um<strong> termo de regularização</strong> que ajuda a suavizar as atualizações dos pesos ao longo do tempo. Um valor de 0.9 significa que a atualização dos pesos é uma média ponderada entre a direção atual do gradiente e a direção do termo de momentum anterior. Isso dá ao modelo uma “inércia” para continuar na mesma direção em que estava indo nas iterações anteriores.</p><p>Como regra geral, valores mais altos de momentum (por exemplo, 0.9) podem ajudar a acelerar o treinamento, especialmente em problemas com muitos mínimos locais ou planaltos na função de perda. No entanto, um valor muito alto pode levar a oscilações excessivas e até mesmo prejudicar o desempenho. Em alguns casos, valores menores de momentum podem ser preferíveis.</p><p>A escolha do valor ideal muitas vezes envolve experimentação e ajuste fino. Recomenda-se começar com valores padrão (como 0.9) e ajustar conforme necessário com base no desempenho observado durante o treinamento. Realizar uma busca de hiperparâmetros ou utilizar métodos automatizados de ajuste de hiperparâmetros também pode ajudar a encontrar os melhores valores em uma variedade de contextos.</p><h4>Mini-Batch Gradient Descent</h4><p>O Mini-Batch Gradient Descent é uma variação do Gradiente Descendente em que, em vez de calcular o gradiente e fazer uma atualização de parâmetros usando o <strong>conjunto de dados completo</strong> (Batch Gradient Descent) ou apenas <strong>um exemplo por vez</strong> (Stochastic Gradient Descent), trabalhamos com mini lotes de dados. Cada atualização de parâmetro é realizada com base em um pequeno subconjunto (mini lote) dos dados de treinamento.</p><p>Usar o Gradiente Descendente Estocástico (SGD) com o <strong>DataLoader</strong> em PyTorch é bastante simples. O DataLoader lida com o embaralhamento dos dados e a divisão em mini lotes, enquanto o otimizador SGD é responsável pelas atualizações dos parâmetros do modelo.</p><pre>import torch<br>import torch.nn as nn<br>import torch.optim as optim<br>from torch.utils.data import DataLoader, TensorDataset<br><br># Criando dados fictícios<br>x_train = torch.randn((1000, 3, 64, 64))  # Exemplo de imagens (canais x altura x largura)<br>y_train = torch.randint(0, 2, (1000,))  # Exemplo de rótulos binários<br><br># Criando um conjunto de dados PyTorch<br>dataset = TensorDataset(x_train, y_train)<br><br># Criando um DataLoader<br>batch_size = 32<br>shuffle = True<br>num_workers = 4  # Número de processos de leitura em paralelo<br>data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=num_workers)<br><br># Criando um modelo de exemplo<br>class SimpleModel(nn.Module):<br>    def __init__(self):<br>        super(SimpleModel, self).__init__()<br>        self.fc = nn.Linear(3 * 64 * 64, 1)<br><br>    def forward(self, x):<br>        x = x.view(x.size(0), -1)  # Achatando as imagens para um vetor<br>        return self.fc(x)<br><br># Criando uma instância do modelo<br>modelo = SimpleModel()<br><br># Definindo a função de perda e o otimizador SGD<br>criterio = nn.BCEWithLogitsLoss()  # Para problemas de classificação binária<br>otimizador = optim.SGD(modelo.parameters(), lr=0.01)<br><br># Treinando o modelo usando SGD e DataLoader<br>num_epochs = 10<br><br>for epoch in range(num_epochs):<br>    for inputs, labels in data_loader:<br>        # Forward pass<br>        outputs = modelo(inputs)<br>        loss = criterio(outputs.squeeze(), labels.float())<br><br>        # Backward pass e otimização<br>        otimizador.zero_grad()<br>        loss.backward()<br>        otimizador.step()<br><br>    print(f&#39;Época [{epoch + 1}/{num_epochs}], Perda: {loss.item():.4f}&#39;)<br><br># Saída<br># Época [1/10], Perda: 0.9239<br># Época [2/10], Perda: 0.1510<br># Época [3/10], Perda: 0.1002<br># Época [4/10], Perda: 0.0714<br># Época [5/10], Perda: 0.0521<br># Época [6/10], Perda: 0.0513<br># Época [7/10], Perda: 0.0373<br># Época [8/10], Perda: 0.0273<br># Época [9/10], Perda: 0.0294<br># Época [10/10], Perda: 0.0289</pre><p>Neste exemplo:</p><ul><li>Criamos um DataLoader para o conjunto de dados usando a classe DataLoader.</li><li>Definimos um modelo de exemplo (SimpleModel) para fins explicativos, didáticos.</li><li>Utilizamos a função de perda BCEWithLogitsLoss, que é apropriada para problemas de classificação binária.</li><li>Utilizamos o otimizador SGD para ajustar os parâmetros do modelo.</li><li>Iteramos sobre os mini lotes gerados pelo DataLoader durante o treinamento, realizando o treinamento por várias épocas.</li></ul><p>Este é um exemplo básico para demonstrar como usar o DataLoader em conjunto com o SGD para treinar um modelo. No dia-a-dia, teríamos que adaptar o modelo, a função de perda e outros hiperparâmetros para atender às necessidades específicas do seu problema.</p><h4>Normalização em Lote</h4><p>A Batch Normalization (normalização em lote) é uma técnica de regularização e normalização usada em redes neurais para melhorar a estabilidade e acelerar o treinamento. A ideia principal por trás da Batch Normalization é normalizar as ativações em cada camada, aplicando uma normalização em lote durante o treinamento.</p><p>A normalização em lote é aplicada a um mini lote de exemplos durante o treinamento e consiste em duas etapas principais:</p><ol><li><strong>Normalização:</strong> Normaliza as ativações de cada neurônio para ter média zero e desvio padrão um dentro do mini lote.</li><li><strong>Escalonamento e Deslocamento (Scaling and Shifting):</strong> Introduz dois parâmetros treináveis (gama e beta) para reescalar e deslocar as ativações normalizadas. Isso permite que a rede aprenda a melhor representação dos dados.</li></ol><p>A normalização em lote é aplicada <strong>antes da função de ativação</strong> em cada camada da rede. Isso ajuda a manter as ativações dentro de uma faixa mais estável, o que pode facilitar o treinamento. Além disso, a normalização em lote tem benefícios de regularização, agindo como um mecanismo de suavização do treinamento, reduzindo a dependência em relação à inicialização específica dos pesos e mitigando problemas como o desaparecimento ou explosão de gradientes.</p><p>A fórmula para normalização em lote em uma camada é dada por:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/204/1*IqRLE76wU_bgl9GfKmY4Eg.png" /></figure><p>onde:</p><ul><li>x são as ativações da camada,</li><li>μ é a média das ativações no mini lote,</li><li>σ é o desvio padrão das ativações no mini lote,</li><li>ϒ e β são parâmetros treináveis,</li><li>ε é uma constante pequena para evitar a divisão por zero.</li></ul><p>Em PyTorch, você pode usar a camada torch.nn.BatchNorm1d (ou torch.nn.BatchNorm2d para convoluções) para adicionar Batch Normalization a uma camada da rede.</p><pre>import torch<br>import torch.nn as nn<br><br>class NeuralNetworkWithBatchNorm(nn.Module):<br>    def __init__(self, input_size, hidden_size, output_size):<br>        super(NeuralNetworkWithBatchNorm, self).__init__()<br>        self.fc1 = nn.Linear(input_size, hidden_size)<br>        self.bn1 = nn.BatchNorm1d(hidden_size)  # Adicionando Batch Normalization<br>        self.relu = nn.ReLU()<br>        self.fc2 = nn.Linear(hidden_size, output_size)<br><br>    def forward(self, x):<br>        x = self.fc1(x)<br>        x = self.bn1(x)  # Aplicando Batch Normalization antes da ativação<br>        x = self.relu(x)<br>        x = self.fc2(x)<br>        return x</pre><p>Adicionar Batch Normalization à sua rede pode ajudar na <strong>estabilidade do treinamento</strong>, permitindo taxas de aprendizado mais altas e reduzindo a sensibilidade à inicialização dos pesos. Essa técnica é especialmente útil em <strong>redes mais profundas</strong>.</p><h4>Divisão em Treinamento, Validação e Teste</h4><p>No pipeline de ciencia de dados e comum dividir-mos os dados em um conjunto de treino, validação e teste.</p><p>Abaixo, implementamos um modelo de regressão linear, mas focando na explicação da divisão de treinamento, validação e teste e como utilizar cada um.</p><pre>import torch<br>import torch.nn as nn<br>import torch.optim as optim<br>from torch.utils.data import DataLoader, TensorDataset<br>from sklearn.model_selection import train_test_split<br>import matplotlib.pyplot as plt<br><br># Criando dados fictícios<br>torch.manual_seed(42)<br>X = torch.rand(1000, 1)  # Feature unidimensional<br>y = 2 * X + 1 + 0.1 * torch.rand(1000, 1)  # Regressão linear com ruído<br><br># Dividindo os dados em treino, validação e teste<br>X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42)<br>X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)<br><br># Convertendo para tensores<br>X_train, y_train = torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)<br>X_val, y_val = torch.tensor(X_val, dtype=torch.float32), torch.tensor(y_val, dtype=torch.float32)<br>X_test, y_test = torch.tensor(X_test, dtype=torch.float32), torch.tensor(y_test, dtype=torch.float32)<br><br># Criando conjuntos de dados PyTorch<br>train_dataset = TensorDataset(X_train, y_train)<br>val_dataset = TensorDataset(X_val, y_val)<br>test_dataset = TensorDataset(X_test, y_test)<br><br># Criando DataLoader para treino, validação e teste<br>batch_size = 32<br>train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)<br>val_loader = DataLoader(val_dataset, batch_size=batch_size)<br>test_loader = DataLoader(test_dataset, batch_size=batch_size)</pre><p>Neste primeira etapa, transformamos os dados em tensores utilizando o torch.tensor e, em seguida utilizamos o TensorDataset, para representar os valores como lista de tensores que entrarão como inputs para o modelo. Em seguida, utilizamos o DataLoader para selecionar amostras de cada conjunto de dados, utilizando o batch_size = 32, ou seja, é o número de amostras de dados que serão usadas em cada iteração durante o treinamento do modelo.</p><pre># Definindo um modelo de regressão linear simples<br>class LinearRegressionModel(nn.Module):<br>    def __init__(self):<br>        super(LinearRegressionModel, self).__init__()<br>        self.linear = nn.Linear(1, 1)<br><br>    def forward(self, x):<br>        return self.linear(x)<br><br># Criando uma instância do modelo<br>model = LinearRegressionModel()<br><br># Definindo a função de perda e o otimizador<br>criterion = nn.MSELoss()<br>optimizer = optim.SGD(model.parameters(), lr=0.01)<br><br># Treinando o modelo<br>num_epochs = 500<br>train_losses, val_losses = [], []<br><br>for epoch in range(num_epochs):<br>    model.train()  # Modo de treino<br>    for inputs, labels in train_loader:<br>        optimizer.zero_grad()<br>        outputs = model(inputs)<br>        loss = criterion(outputs, labels)<br>        loss.backward()<br>        optimizer.step()<br><br>    # Calculando a perda no conjunto de treino<br>    model.eval()  # Modo de avaliação<br>    with torch.no_grad():<br>        total_val_loss = 0<br>        for inputs_val, labels_val in val_loader:<br>            outputs_val = model(inputs_val)<br>            val_loss = criterion(outputs_val, labels_val)<br>            total_val_loss += val_loss.item()<br><br>    average_val_loss = total_val_loss / len(val_loader)<br>    val_losses.append(average_val_loss)<br><br>    print(f&#39;Época [{epoch + 1}/{num_epochs}], Perda de Treino: {loss.item():.4f}, Perda de Validação: {average_val_loss:.4f}&#39;)<br><br># Testando o modelo<br>model.eval()  # Modo de avaliação<br>with torch.no_grad():<br>    total_test_loss = 0<br>    for inputs_test, labels_test in test_loader:<br>        outputs_test = model(inputs_test)<br>        test_loss = criterion(outputs_test, labels_test)<br>        total_test_loss += test_loss.item()<br><br>average_test_loss = total_test_loss / len(test_loader)<br>print(f&#39;Perda no Conjunto de Teste: {average_test_loss:.4f}&#39;)<br><br># Visualizando os resultados<br>plt.scatter(X_test, y_test, label=&#39;Dados Reais&#39;)<br>plt.plot(X_test, model(X_test).detach().numpy(), color=&#39;red&#39;, label=&#39;Regressão Linear Ajustada&#39;)<br>plt.xlabel(&#39;X&#39;)<br>plt.ylabel(&#39;y&#39;)<br>plt.legend()<br>plt.show()</pre><p>Aqui, definimos o modelo e o instanciamos. Depois definimos a função de perda (criterion) e o otimizador. O modelo é treinado por um número fixo de épocas (500 neste caso). Para cada época, os dados de treinamento são passados pelo modelo, a perda é calculada usando a função de perda definida e os parâmetros do modelo são atualizados pelo otimizador.</p><p>Durante o treinamento, a perda no conjunto de validação é calculada para monitorar o desempenho do modelo em dados não vistos.</p><p>Após o treinamento, o modelo é testado no conjunto de teste para avaliar seu desempenho em dados completamente novos. A perda no conjunto de teste é calculada usando a função de perda.</p><p>Ao final, os resultados são visualizados plotando os dados reais do conjunto de teste e a linha da regressão linear ajustada pelo modelo. Isso ajuda a visualizar como o modelo se ajusta aos dados de teste.</p><h3>Redes Neurais</h3><p>Um neurônio em deep learning é uma unidade básica de processamento que recebe entradas, realiza uma série de operações matemáticas nelas (como multiplicação por pesos e soma ponderada), aplica uma função de ativação e produz uma saída.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/1*m_e3RIim6-fflmzHeSD3ug.png" /></figure><p>Redes neurais são formadas por interconectar esses neurônios em camadas. <strong>Cada camada recebe entradas das camadas anteriores, processa essas entradas e passa as saídas para as camadas seguintes</strong>. Isso forma uma rede de processamento de informações, capaz de aprender padrões e realizar tarefas complexas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/523/1*CSBHH8cSpKsJdpOiZ5xoqA.png" /></figure><h4>Redes Neurais Rasas</h4><p>Uma Shallow Neural Network (Rede Neural Rasa) refere-se a uma arquitetura de rede neural <strong>com apenas uma única camada oculta</strong> entre a camada de entrada e a camada de saída. Em outras palavras, uma rede neural rasa possui uma estrutura simples, com apenas uma camada intermediária contendo um número limitado de neurônios. A arquitetura básica de uma Shallow Neural Network é composta por três camadas principais:</p><ul><li><strong>Camada de Entrada (Input Layer):</strong> Recebe os dados de entrada e transfere esses dados para a camada oculta.</li><li><strong>Camada Oculta (Hidden Layer):</strong> Realiza transformações nos dados recebidos da camada de entrada. Esta camada contém neurônios que aplicam transformações lineares seguidas de funções de ativação não lineares. A presença desta camada permite que a rede aprenda representações mais complexas dos dados.</li><li><strong>Camada de Saída (Output Layer):</strong> Produz a saída final da rede após processar os dados pela camada oculta. Dependendo da tarefa, a camada de saída pode ter um único neurônio (para problemas de regressão) ou vários neurônios (para problemas de classificação).</li></ul><p>Embora uma Shallow Neural Network seja limitada em termos de capacidade de aprendizado em comparação com redes mais profundas, ela ainda pode ser eficaz para tarefas relativamente simples. Essa arquitetura é frequentemente utilizada em problemas onde os padrões nos dados podem ser aprendidos com uma quantidade menor de parâmetros.</p><ul><li><strong>Menos Suscetíveis a Overfitting:</strong> Redes rasas geralmente têm menos parâmetros do que redes profundas, o que pode torná-las menos propensas a overfitting (ajuste excessivo) em conjuntos de dados menores.</li><li><strong>Treinamento Mais Rápido:</strong> Como há menos camadas e parâmetros para treinar, o treinamento de Shallow Neural Networks pode ser mais rápido em comparação com redes mais profundas.</li><li><strong>Limitações na Representação de Recursos Complexos:</strong> Redes rasas podem ter dificuldades em aprender representações hierárquicas profundas de características complexas presentes nos dados, o que pode limitar seu desempenho em tarefas mais desafiadoras.</li><li><strong>Aplicações:</strong> Shallow Neural Networks podem ser adequadas para problemas mais simples, como classificação binária, regressão linear, ou problemas onde os dados têm padrões que podem ser facilmente aprendidos com uma arquitetura mais simples.</li></ul><p>À medida que a complexidade das tarefas aumenta, é comum recorrer a arquiteturas de redes mais profundas para extrair e aprender representações mais complexas e abstratas dos dados.<br>Redes Neurais Profundas</p><pre># Definindo a Shallow Neural Network<br>class ShallowNeuralNetwork(nn.Module):<br>    def __init__(self, input_size):<br>        super(ShallowNeuralNetwork, self).__init__()<br>        self.linear1 = nn.Linear(input_size, 10) # 10 neuronios na camada oculta<br>        self.relu = nn.ReLU() # funcao de ativacao da camada oculta<br>        self.linear2 = nn.Linear(10, 1) # 1 neuronio na camada de saida<br>        self.sigmoid = nn.Sigmoid() # funcao de ativacao da camada de saida<br><br>    def forward(self, x):<br>        x = self.relu(self.linear1(x))<br>        x = self.sigmoid(self.linear2(x))<br>        return x<br></pre><p>Exemplo completo</p><pre>import torch<br>import torch.nn as nn<br>import torch.optim as optim<br>from sklearn.datasets import make_classification<br>from sklearn.model_selection import train_test_split<br>from sklearn.preprocessing import StandardScaler<br><br># Gerando dados fictícios<br>X, y = make_classification(n_samples=1000, n_features=5, n_classes=2, n_clusters_per_class=1, random_state=42)<br><br># Dividindo os dados em conjuntos de treino e teste<br>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)<br><br># Padronizando os dados (importante para redes neurais)<br>scaler = StandardScaler()<br>X_train = scaler.fit_transform(X_train)<br>X_test = scaler.transform(X_test)<br><br># Convertendo os dados para tensores do PyTorch<br>X_train_tensor = torch.tensor(X_train, dtype=torch.float32)<br>y_train_tensor = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)  # reshape para uma coluna<br>X_test_tensor = torch.tensor(X_test, dtype=torch.float32)<br>y_test_tensor = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)  # reshape para uma coluna<br><br># Definindo a Shallow Neural Network<br>class ShallowNeuralNetwork(nn.Module):<br>    def __init__(self, input_size):<br>        super(ShallowNeuralNetwork, self).__init__()<br>        self.linear1 = nn.Linear(input_size, 10)<br>        self.relu = nn.ReLU()<br>        self.linear2 = nn.Linear(10, 1)<br>        self.sigmoid = nn.Sigmoid()<br><br>    def forward(self, x):<br>        x = self.relu(self.linear1(x))<br>        x = self.sigmoid(self.linear2(x))<br>        return x<br><br># Instanciando o modelo<br>input_size = X_train.shape[1]<br>model = ShallowNeuralNetwork(input_size)<br><br># Definindo a função de perda (Binary Cross Entropy Loss) e o otimizador<br>criterion = nn.BCELoss()<br>optimizer = optim.SGD(model.parameters(), lr=0.01)<br><br># Treinando o modelo<br>num_epochs = 500<br><br>for epoch in range(num_epochs):<br>    # Forward pass<br>    outputs = model(X_train_tensor)<br><br>    # Calculando a perda<br>    loss = criterion(outputs, y_train_tensor)<br><br>    # Backward pass e otimização<br>    optimizer.zero_grad()<br>    loss.backward()<br>    optimizer.step()<br><br>    # Imprimindo a perda a cada 50 épocas<br>    if (epoch + 1) % 50 == 0:<br>        print(f&#39;Época [{epoch + 1}/{num_epochs}], Perda: {loss.item():.4f}&#39;)<br><br># Avaliando o modelo no conjunto de teste<br>model.eval()  # Modo de avaliação<br>with torch.no_grad():<br>    predictions = model(X_test_tensor)<br>    binary_predictions = (predictions &gt; 0.5).float()<br><br># Calculando a acurácia do modelo no conjunto de teste<br>accuracy = (binary_predictions == y_test_tensor).float().mean()<br>print(f&#39;Acurácia do modelo no conjunto de teste: {accuracy.item():.4f}&#39;)<br><br># Saída<br>#Época [50/500], Perda: 0.6457<br>#Época [100/500], Perda: 0.6074<br>#Época [150/500], Perda: 0.5694<br>#Época [200/500], Perda: 0.5321<br>#Época [250/500], Perda: 0.4960<br>#Época [300/500], Perda: 0.4619<br>#Época [350/500], Perda: 0.4305<br>#Época [400/500], Perda: 0.4024<br>#Época [450/500], Perda: 0.3773<br>#Época [500/500], Perda: 0.3552<br>#Acurácia do modelo no conjunto de teste: 0.9150</pre><p>Neste exemplo, criamos uma Shallow Neural Network com uma camada oculta contendo 10 neurônios. A função de ativação ReLU é usada na camada oculta, e a função de ativação Sigmoid é usada na camada de saída para lidar com a tarefa de classificação binária. O modelo é treinado usando o otimizador SGD (Stochastic Gradient Descent) e a função de perda Binary Cross Entropy Loss. O desempenho do modelo é avaliado no conjunto de teste.</p><h4>Redes Neurais Profundas</h4><p>Uma Deep Neural Network (DNN) ou Rede Neural Profunda , é simplesmente uma arquitetura de rede neural artificial que possui <strong>múltiplas camadas entre a entrada e a saída</strong>. Essa abordagem permite que a rede <strong>aprenda representações mais complexas e hierárquicas dos dados</strong>, tornando-a capaz de realizar tarefas mais sofisticadas e de lidar com características abstratas.</p><p>As DNNs são caracterizadas por sua <strong>profundidade</strong>, que é determinada pelo <strong>número de camadas</strong> que compõem a arquitetura. Cada camada da rede contém um conjunto de neurônios (também chamados de unidades ou nodos) que realizam operações de transformação nos dados de entrada. As camadas podem ser classificadas em três tipos principais:</p><ol><li><strong>Camada de Entrada (Input Layer):</strong> Recebe os dados de entrada e transmite-os para a primeira camada oculta.</li><li><strong>Camadas Ocultas (Hidden Layers):</strong> São as camadas intermédias entre a camada de entrada e a camada de saída. Cada neurônio em uma camada oculta realiza transformações nos dados recebidos das camadas anteriores. A existência de múltiplas camadas ocultas permite que a rede aprenda representações cada vez mais complexas.</li><li><strong>Camada de Saída (Output Layer):</strong> Produz a saída final da rede após processar os dados através das camadas ocultas. A arquitetura da camada de saída depende da tarefa que a rede está destinada a realizar (por exemplo, uma única unidade para regressão ou várias unidades para classificação multiclasse).</li></ol><p>O treinamento de DNNs geralmente envolve o uso do algoritmo de <strong>backpropagation</strong>, que consiste na propagação do gradiente durante a fase de treinamento para ajustar os pesos da rede de acordo com um critério de desempenho (como a minimização da função de custo). A capacidade de treinar redes profundas foi impulsionada por avanços em algoritmos de otimização, funções de ativação eficazes e inicializações adequadas de pesos.</p><p>A popularização das DNNs contribuiu significativamente para avanços em campos como visão computacional, processamento de linguagem natural, reconhecimento de voz e muito mais. Exemplos notáveis de arquiteturas de DNNs incluem Redes Neurais Convolucionais (CNNs) para visão computacional e Redes Neurais Recorrentes (RNNs) para processamento de sequências temporais.</p><pre>import torch<br>import torch.nn as nn<br>import torch.optim as optim<br>from torchvision import datasets, transforms<br>from torch.utils.data import DataLoader<br>import matplotlib.pyplot as plt<br><br># Transformações para normalizar as imagens do MNIST<br>transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])<br><br># Carregando os conjuntos de treino e teste do MNIST<br>train_dataset = datasets.MNIST(root=&#39;./data&#39;, train=True, download=True, transform=transform)<br>test_dataset = datasets.MNIST(root=&#39;./data&#39;, train=False, download=True, transform=transform)<br><br># Criando DataLoader para os conjuntos de treino e teste<br>batch_size = 64<br>train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)<br>test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)<br><br># Visualizando uma imagem do MNIST<br>sample_image, sample_label = next(iter(train_loader))<br>plt.imshow(sample_image[0][0], cmap=&#39;gray&#39;)<br>plt.title(f&#39;Label: {sample_label[0]}&#39;)<br>plt.show()<br><br># Definindo a arquitetura da DNN com mais camadas ocultas<br>class DeepNeuralNetwork(nn.Module):<br>    def __init__(self, input_size, hidden_size1, hidden_size2, output_size):<br>        super(DeepNeuralNetwork, self).__init__()<br>        # Camadas ocultas hidden_size1 e hidden_size2<br>        self.fc1 = nn.Linear(input_size, hidden_size1)<br>        self.relu1 = nn.ReLU()<br>        self.fc2 = nn.Linear(hidden_size1, hidden_size2)<br>        self.relu2 = nn.ReLU()<br>        # Camada de saída output_size<br>        self.fc3 = nn.Linear(hidden_size2, output_size)<br>        self.softmax = nn.Softmax(dim=1)<br><br>    def forward(self, x):<br>        # Camadas ocultas<br>        x = self.relu1(self.fc1(x))<br>        x = self.relu2(self.fc2(x))<br>        # Camada de saída<br>        x = self.softmax(self.fc3(x))<br>        return x<br><br># Instanciando o modelo com mais camadas ocultas<br>input_size = 28 * 28  # Tamanho da imagem MNIST<br>hidden_size1 = 128  # Número de neurônios na primeira camada oculta<br>hidden_size2 = 64   # Número de neurônios na segunda camada oculta<br>output_size = 10    # Número de classes no MNIST (dígitos de 0 a 9)<br><br>model = DeepNeuralNetwork(input_size, hidden_size1, hidden_size2, output_size)<br><br># Definindo a função de perda e o otimizador<br>criterion = nn.CrossEntropyLoss()<br>optimizer = optim.Adam(model.parameters(), lr=0.001)<br><br># Treinando o modelo<br>num_epochs = 10<br><br>for epoch in range(num_epochs):<br>    for batch_images, batch_labels in train_loader:<br>        # Achatando as imagens para um vetor<br>        batch_images = batch_images.view(-1, 28 * 28)<br><br>        # Forward pass<br>        outputs = model(batch_images)<br><br>        # Calculando a perda<br>        loss = criterion(outputs, batch_labels)<br><br>        # Backward pass e otimização<br>        optimizer.zero_grad()<br>        loss.backward()<br>        optimizer.step()<br><br>    print(f&#39;Época [{epoch + 1}/{num_epochs}], Perda: {loss.item():.4f}&#39;)<br><br># Avaliando o modelo no conjunto de teste<br>model.eval()  # Modo de avaliação<br>correct = 0<br>total = 0<br><br>with torch.no_grad():<br>    for batch_images, batch_labels in test_loader:<br>        batch_images = batch_images.view(-1, 28 * 28)<br>        outputs = model(batch_images)<br>        _, predicted = torch.max(outputs.data, 1)<br>        total += batch_labels.size(0)<br>        correct += (predicted == batch_labels).sum().item()<br><br>accuracy = correct / total<br>print(f&#39;Acurácia do modelo no conjunto de teste: {accuracy:.4f}&#39;)<br><br># Saída<br>#Época [1/10], Perda: 1.5257<br>#Época [2/10], Perda: 1.4962<br>#Época [3/10], Perda: 1.5844<br>#Época [4/10], Perda: 1.5775<br>#Época [5/10], Perda: 1.5120<br>#Época [6/10], Perda: 1.5854<br>#Época [7/10], Perda: 1.4931<br>#Época [8/10], Perda: 1.5444<br>#Época [9/10], Perda: 1.4860<br>#Época [10/10], Perda: 1.4744<br>#Acurácia do modelo no conjunto de teste: 0.9569</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/415/1*1srW5hzyH9sTndF64G6Vdw.png" /></figure><h4>nn.ModuleList()</h4><p>O nn.ModuleList é uma classe no PyTorch que permite criar uma lista de módulos (camadas) em um modelo. Isso é útil quando você deseja criar uma arquitetura de rede com várias camadas, e o número de camadas pode variar dinamicamente. O nn.ModuleList facilita a gestão de uma sequência de camadas em seu modelo.</p><pre>import torch<br>import torch.nn as nn<br><br>class DynamicNet(nn.Module):<br>    def __init__(self, input_size, hidden_sizes, output_size):<br>        super(DynamicNet, self).__init__()<br><br>        # Lista de camadas ocultas<br>        self.hidden_layers = nn.ModuleList([<br>            nn.Linear(input_size, hidden_sizes[0]),<br>            nn.ReLU()])<br><br>        # Loop para adicionar as demais camadas internas<br>        for i in range(1, len(hidden_sizes)):<br>            self.hidden_layers.extend([<br>                nn.Linear(hidden_sizes[i - 1], hidden_sizes[i]),<br>                nn.ReLU()])<br><br>        # Camada de saída<br>        self.output_layer = nn.Linear(hidden_sizes[-1], output_size)<br><br>    def forward(self, x):<br>        for layer in self.hidden_layers:<br>            x = layer(x)<br>        output = self.output_layer(x)<br>        return output<br><br># Exemplo de uso<br>input_size = 10<br>hidden_sizes = [32, 64, 128]<br>output_size = 1<br><br>model = DynamicNet(input_size, hidden_sizes, output_size)<br>print(model)<br><br># Saída<br>#DynamicNet(<br>#  (hidden_layers): ModuleList(<br>#    (0): Linear(in_features=10, out_features=32, bias=True)<br>#    (1): ReLU()<br>#    (2): Linear(in_features=32, out_features=64, bias=True)<br>#    (3): ReLU()<br>#    (4): Linear(in_features=64, out_features=128, bias=True)<br>#    (5): ReLU()<br>#  )<br>#  (output_layer): Linear(in_features=128, out_features=1, bias=True)<br>#)</pre><p>Ao usar nn.ModuleList, estamos garantindo que ambas as camadas (a camada linear e a função de ativação ReLU) sejam tratadas como parte do modelo. Isso é fundamental para que o PyTorch rastreie e otimize os parâmetros durante o treinamento, além de facilitar a manipulação de várias camadas em uma rede neural dinâmica. Essa abordagem é especialmente útil quando se cria redes com arquiteturas variáveis.</p><pre>for i in range(1, len(hidden_sizes)):<br>    self.hidden_layers.extend([<br>        nn.Linear(hidden_sizes[i - 1], hidden_sizes[i]),<br>        nn.ReLU()<br>    ])</pre><p>Este loop é responsável por criar uma sequência de camadas ocultas na rede neural.</p><ol><li><strong>for i in range(1, len(hidden_sizes)):</strong>: O loop percorre os elementos da lista hidden_sizes a partir do segundo elemento (índice 1). O motivo de começar do segundo elemento é porque estamos criando conexões entre camadas adjacentes.</li><li><strong>nn.Linear(hidden_sizes[i - 1], hidden_sizes[i])</strong>: Aqui, estamos criando uma camada linear (totalmente conectada) que conecta a camada oculta anterior (com hidden_sizes[i - 1] neurônios) à próxima camada oculta (com hidden_sizes[i] neurônios). Isso permite que a rede aprenda representações mais complexas à medida que avança nas camadas ocultas.</li><li><strong>nn.ReLU()</strong>: Em seguida, adicionamos uma camada de ativação ReLU. A função de ativação ReLU (Rectified Linear Unit) introduz não-linearidades na rede, permitindo que ela aprenda padrões mais complexos e não lineares nos dados.</li><li><strong>self.hidden_layers.extend([...])</strong>: Aqui, usamos o método extend de nn.ModuleList para adicionar as camadas criadas ao final da lista de camadas ocultas. O método extend é usado porque queremos adicionar múltiplos módulos de uma vez.</li></ol><p>Ao usar nn.ModuleList, o PyTorch rastreia automaticamente os parâmetros dessas camadas, tornando-os parte do modelo. Isso é crucial para garantir que a retropropagação e a atualização dos pesos ocorram corretamente durante o treinamento.</p><p>Ao criar seu modelo, você pode ajustar o tamanho da lista hidden_sizes para adicionar ou remover camadas ocultas conforme necessário.</p><h4>Funções de Ativação</h4><p>As funções de ativação são elementos-chave em redes neurais, adicionando não-linearidades às saídas dos neurônios. Elas são responsáveis por introduzir complexidade e capacidade de aprendizado não linear aos modelos, permitindo que as redes neurais aproximem funções mais complexas. Algumas das funções de ativação mais comuns:</p><p><strong>Sigmoide:</strong></p><ul><li><strong>Fórmula:</strong> σ(x)= 1 / 1+e ^-x</li><li><strong>Intervalo de Saída:</strong> (0, 1)</li><li><strong>Uso:</strong> Originalmente usada em problemas de classificação binária, mas tem caído em desuso em camadas internas de redes profundas devido ao problema do “vanishing gradient”.</li></ul><p><strong>Tangente Hiperbólica (tanh):</strong></p><ul><li><strong>Fórmula:</strong> tanh⁡(x)= e^x-e^-z / e^x + e^-z</li><li><strong>Intervalo de Saída:</strong> (-1, 1)</li><li><strong>Uso:</strong> Similar à sigmoide, mas com saída variando de -1 a 1. Pode ser usada em camadas internas.</li></ul><p><strong>ReLU (Rectified Linear Unit):</strong></p><ul><li><strong>Fórmula:</strong> ReLU(x)=max(0,x)</li><li><strong>Intervalo de Saída:</strong> [0, +∞)</li><li><strong>Uso:</strong> Muito popular devido à sua simplicidade e eficácia. Ajuda a mitigar o problema do “vanishing gradient”, mas pode levar a problemas de “dying ReLU” quando os neurônios ficam inativos.</li></ul><p><strong>Leaky ReLU:</strong></p><ul><li><strong>Fórmula:</strong> Leaky ReLU(x)=max(αx,x) onde α é uma constante pequena, geralmente próxima de 0.</li><li><strong>Intervalo de Saída:</strong> (-∞, +∞)</li><li><strong>Uso:</strong> Introduz um leve componente linear para evitar o problema de “dying ReLU”.</li></ul><p><strong>Função de Unidade Linear Exponencial (ELU):</strong></p><ul><li><strong>Fórmula:</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/236/1*i_OKm9I8W7cWQlrEKTmohQ.png" /><figcaption>onde α é uma constante positiva</figcaption></figure><ul><li><strong>Intervalo de Saída:</strong> (-∞, +∞)</li><li><strong>Uso:</strong> Similar ao Leaky ReLU, com uma suavização nas regiões negativas.</li></ul><p><strong>Softmax:</strong></p><ul><li><strong>Fórmula:</strong> Softmax(x)i= e^zi/∑_j e^zj para cada i na saída.</li><li><strong>Intervalo de Saída:</strong> [0, 1] e a soma de todas as saídas é 1.</li><li><strong>Uso:</strong> Principalmente <strong>usado na camada de saída para problemas de classificação multiclasse</strong>, onde as saídas representam probabilidades.</li></ul><p>Essas são apenas algumas das muitas funções de ativação disponíveis. A escolha da função de ativação depende do problema específico, da arquitetura da rede e da natureza dos dados. Experimentar diferentes funções de ativação pode ser crucial para alcançar o melhor desempenho em uma tarefa específica. Abaixo sintetizamos algumas das funções de ativação existentes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/735/1*FCCa2UJ6Sl1odUIn95RAew.png" /><figcaption>Tabela de Funções de Ativação</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WPujAbv9aSu0fIrdhiDRJw.png" /><figcaption>Tabela Comparativa</figcaption></figure><ul><li><strong>Não Saturante:</strong> Indica se a função de ativação é não saturante, ou seja, se ela não satura para valores extremos. “Saturar” refere-se ao comportamento da função quando a entrada ultrapassa um determinado limite, resultando em uma saída constante. Isso significa que, à medida que a entrada aumenta, a saída da função de ativação atinge um valor máximo ou mínimo e permanece constante para entradas subsequentes</li><li><strong>Rápido:</strong> Refere-se à rapidez computacional da função de ativação, indicando se é computacionalmente eficiente.</li><li><strong>Binária:</strong> Relaciona-se com o uso da função de ativação em camadas de saída binárias, como em problemas de classificação binária.</li><li><strong>Varia entre -1 e 1:</strong> Indica se a função de ativação produz saídas que variam entre -1 e 1, sendo útil para dados centrados.</li><li><strong>Probabilidades:</strong> Mostra se a função de ativação é comumente usada para converter saídas em probabilidades, frequentemente usado em problemas de classificação.</li><li><strong>Evita Morte Neuronal:</strong> Indica se a função de ativação possui características que ajudam a evitar a “morte” de neurônios durante o treinamento.</li><li><strong>Paramétrico:</strong> Refere-se a funções de ativação que têm parâmetros que são aprendidos durante o treinamento, como o coeficiente em ReLU paramétrico (PReLU).</li><li><strong>Suavização Negativa:</strong> Indica se a função de ativação tem suavização para valores negativos, como no caso da Exponential Linear Unit (ELU).</li><li><strong>Auto-Normalizante:</strong> Refere-se a funções de ativação que têm a capacidade de auto-normalização, como a Scaled Exponential Linear Unit (SELU).</li><li><strong>Suave e Diferenciável:</strong> Indica se a função de ativação é suave e diferenciável, o que é desejável para facilitar o treinamento com otimizadores que requerem derivadas.</li></ul><p>Essas características são importantes ao escolher uma função de ativação, pois diferentes cenários e arquiteturas de rede podem se beneficiar de propriedades específicas de cada função.</p><h4>Backpropagation</h4><p>Backpropagation, que significa propagação para trás, é um algoritmo fundamental no treinamento de redes neurais. Ele é utilizado para ajustar os pesos da rede de forma a minimizar a função de custo durante o processo de treinamento supervisionado. A ideia principal do backpropagation é calcular gradientes em relação aos pesos da rede, permitindo a atualização dos pesos de forma a reduzir o erro da rede. Principais passos do algoritmo de backpropagation:</p><ol><li><strong>Feedforward (Propagação Direta):</strong> Durante a fase de feedforward, a entrada é passada pela rede, camada por camada, até a camada de saída. Cada neurônio realiza uma transformação linear seguida de uma função de ativação.</li><li><strong>Cálculo da Função de Custo:</strong> A saída da rede é comparada com as saídas reais (rótulos) para calcular a função de custo, que representa o quão longe as previsões da rede estão dos rótulos desejados.</li><li><strong>Backpropagation (Propagação para Trás):</strong> O objetivo do backpropagation é calcular os gradientes da função de custo em relação aos pesos da rede. Isso é feito iterativamente, começando pela última camada e indo em direção à primeira. Os gradientes são calculados usando a regra da cadeia.</li><li><strong>Atualização dos Pesos:</strong> Com os gradientes calculados, os pesos da rede são ajustados para minimizar a função de custo. Isso é geralmente feito usando um algoritmo de otimização, como o Gradiente Descendente ou suas variantes.</li><li><strong>Iteração:</strong> Os passos 1 a 4 são repetidos por várias épocas (ou iterações) até que a rede neural atinja um desempenho aceitável no conjunto de treino.</li></ol><p>Os passos acima são os que são codficados no loop de treinamento. É importante observar que o sucesso do backpropagation depende da <strong>diferenciabilidade</strong> das funções de ativação utilizadas na rede, pois a regra da cadeia e o cálculo dos gradientes dependem dessa propriedade. A diferenciabilidade é uma propriedade fundamental de funções matemáticas que expressa a <strong>capacidade de calcular derivadas em cada ponto do seu domínio</strong>. Uma função é considerada diferenciável em um determinado ponto se a sua derivada existe nesse ponto.</p><p>A regra da cadeia é fundamental para o backpropagation. Se <em>C</em> é a função de custo, a é a saída da rede, e <em>w</em> é um dos pesos da rede, então o gradiente <em>dC/dw</em> pode ser calculado como:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/171/1*PNj7b8KcWCaB249DeprvEQ.png" /></figure><p>onde z é a entrada ponderada para um neurônio (z=∑^i_=1(wi⋅xi)+b), <em>a</em> é a saída após a aplicação da função de ativação, e <em>C</em> é a função de custo.</p><p>O backpropagation é uma técnica poderosa que tornou possível treinar redes neurais profundas. Atualmente, frameworks de aprendizado profundo, como PyTorch e TensorFlow, implementam o backpropagation automaticamente, facilitando sua aplicação em uma variedade de problemas.</p><h4>O problema do “vanishing gradient”</h4><p>O problema do “vanishing gradient” (gradiente desaparecendo) é uma questão que pode ocorrer durante o treinamento de redes neurais, especialmente em arquiteturas profundas. Esse problema está relacionado à propagação do gradiente durante a fase de backpropagation, e ocorre quando os gradientes das camadas mais profundas se tornam muito pequenos, aproximando-se de zero. Como resultado, <strong>os pesos dessas camadas são ajustados muito lentamente ou quase não são ajustados durante o treinamento</strong>. Esse problema pode acontecer nos seguintes casos:</p><ol><li><strong>Funções de Ativação Sigmóide e Tangente Hiperbólica:</strong> Funções de ativação como a sigmóide e a tangente hiperbólica têm uma feature em que suas derivadas se aproximam de zero para valores extremos. Quando a propagação do gradiente ocorre através de várias camadas, essas derivadas multiplicadas entre si podem resultar em gradientes muito pequenos nas camadas mais profundas.</li><li><strong>Inicialização inadequada de pesos:</strong> A inicialização inadequada dos pesos da rede também pode contribuir para o problema do vanishing gradient. Se os pesos iniciais forem muito pequenos, os gradientes nas camadas anteriores também serão pequenos, o que pode ser amplificado ao longo das camadas.</li><li><strong>Arquitetura da Rede:</strong> Em arquiteturas muito profundas, os gradientes podem diminuir exponencialmente à medida que se propagam para trás, tornando-se insignificantes nas camadas mais profundas.</li><li><strong>Problema de Longo Prazo (Long-Term Dependency):</strong> O vanishing gradient é particularmente preocupante em modelos que precisam capturar dependências a longo prazo, como em redes recorrentes (RNNs). A propagação do gradiente ao longo de muitos passos temporais pode resultar em gradientes que desaparecem ou explodem.</li></ol><p>Para mitigar o problema do vanishing gradient, várias técnicas podem ser utilizadas, como:</p><ul><li><strong>Escolha de Funções de Ativação:</strong> O uso de funções de ativação que não sofrem tanto com o problema do vanishing gradient, como a ReLU (Rectified Linear Unit), pode ajudar a mitigar esse problema.</li><li><strong>Inicialização Adequada de Pesos:</strong> Estratégias de inicialização de pesos, como a inicialização de He ou Xavier, podem ajudar a evitar que os pesos comecem muito pequenos.</li><li><strong>Normalização de Lote (Batch Normalization):</strong> A normalização de lote pode ajudar a manter ativações e gradientes em uma escala razoável durante o treinamento, o que pode ajudar a prevenir o vanishing gradient.</li><li><strong>Uso de Arquiteturas Específicas:</strong> Arquiteturas projetadas para mitigar o problema do vanishing gradient, como as conexões residuais em ResNets, podem ser utilizadas.</li></ul><p>O vanishing gradient é um desafio significativo em treinamento de redes neurais profundas, e abordá-lo adequadamente é crucial para garantir que a rede possa aprender efetivamente representações complexas dos dados.</p><h4>Dropout</h4><p>Dropout é uma técnica de regularização usada em redes neurais para reduzir o overfitting durante o treinamento. O overfitting ocorre quando um modelo se ajusta demais aos dados de treinamento e tem dificuldade em generalizar para novos dados. O dropout é projetado para evitar que unidades específicas em uma rede neural se tornem excessivamente especializadas, melhorando assim a generalização do modelo.</p><p>A ideia principal do dropout é “eliminar” aleatoriamente unidades (neurônios) durante o treinamento. Durante cada iteração de treinamento, um conjunto aleatório de unidades é “desligado” (ou “dropado”), o que significa que suas contribuições para a propagação para frente e para trás são temporariamente removidas. Isso simula a ideia de treinar diferentes sub-redes em cada iteração, e o modelo precisa aprender a ser robusto mesmo quando partes aleatórias da rede são desativadas.</p><p>A implementação do dropout é bastante simples. Na prática, você adiciona uma camada de dropout após as camadas de ativação em sua rede neural. O dropout é geralmente aplicado apenas durante o treinamento e não durante a inferência ou avaliação.</p><p>No PyTorch, você pode usar a camada nn.Dropout para implementar o dropout em sua rede. Aqui está um exemplo de como adicionar dropout a uma rede em PyTorch:</p><pre>import torch<br>import torch.nn as nn<br><br>class NeuralNetworkWithDropout(nn.Module):<br>    def __init__(self, input_size, hidden_size, output_size, dropout_rate=0.5):<br>        super(NeuralNetworkWithDropout, self).__init__()<br><br>        self.fc1 = nn.Linear(input_size, hidden_size)<br>        self.relu1 = nn.ReLU()<br>        self.dropout = nn.Dropout(p=dropout_rate)  # Adicionando a camada de dropout<br>        self.fc2 = nn.Linear(hidden_size, output_size)<br><br>    def forward(self, x):<br>        x = self.fc1(x)<br>        x = self.relu1(x)<br>        x = self.dropout(x)  # Aplicando o dropout após a ativação<br>        x = self.fc2(x)<br>        return x</pre><p>Neste exemplo, a camada nn.Dropout é adicionada após a primeira camada de ativação (nn.ReLU). O parâmetro p em nn.Dropout representa a probabilidade de desativar uma unidade durante o treinamento. Um valor típico para dropout_rate é 0.5, o que significa que, em média, metade das unidades será desativada durante cada iteração de treinamento.</p><p>Dropout é uma ferramenta eficaz para melhorar a generalização de modelos de redes neurais e é comumente usado em diversas arquiteturas de redes neurais.</p><h4>Pesos e Inicialização da Rede Neural</h4><p>A inicialização adequada dos pesos em uma rede neural é crucial para o sucesso do treinamento do modelo. <strong>Peso inicializado de maneira inadequada pode levar a problemas como convergência lenta</strong>, convergência para mínimos locais ou até mesmo a falta de convergência. Algumas técnicas comuns de inicialização de pesos usadas em redes neurais são:</p><ul><li><strong>Inicialização Aleatória: </strong>Inicializar os pesos com valores aleatórios é uma abordagem comum, onde os pesos são inicializados com números retirados de uma distribuição uniforme ou normal. No entanto, essa abordagem pode levar a problemas, especialmente em redes profundas, onde o gradiente pode diminuir ou explodir durante a retropropagação.</li><li><strong>Inicialização de Xavier/Glorot: </strong>Proposta por Xavier Glorot, esta abordagem ajusta a escala dos pesos com base no número de unidades nas camadas de entrada e saída. Os pesos são inicializados a partir de uma distribuição normal com média zero e variância <strong>2 / número de entradas + número de saídas</strong> ou uma distribuição uniforme entre −<em>a</em> e <em>a</em>, onde <em>a</em> é a <strong>raíz de 6 / número de entradas + numero de saidas</strong>.</li><li><strong>Inicialização de He: </strong>Semelhante à inicialização de Xavier, mas ajusta a escala de acordo com apenas o número de unidades na camada de entrada. Os pesos são inicializados a partir de uma distribuição normal com média zero e variância <strong>2/número de entradas</strong> ou uma distribuição uniforme entre −<em>a</em> e <em>a</em>, onde <em>a</em> é <strong>6/número de entradas</strong>.</li><li><strong>Inicialização de LeCun: </strong>Similar à inicialização de Xavier, mas utiliza uma constante diferente para ajustar a escala dos pesos. Os pesos são inicializados a partir de uma distribuição normal com média zero e variância <strong>1/número de entradas</strong> ou uma distribuição uniforme entre −<em>a</em> e <em>a</em>, onde <em>a</em> é <strong>3/número de entradas</strong>.</li><li><strong>Inicialização de Orthogonal: </strong>Inicializa os pesos de tal forma que a matriz de pesos é ortogonal. Essa abordagem é útil para arquiteturas que envolvem recorrência.</li></ul><p>Em PyTorch, muitas dessas inicializações podem ser facilmente implementadas usando as funções de inicialização fornecidas no módulo torch.nn.init. Por exemplo, você pode usar torch.nn.init.xavier_uniform_ ou torch.nn.init.kaiming_uniform_ para inicializações de Xavier ou He, respectivamente.</p><p>A escolha da inicialização de pesos depende da arquitetura da rede, do tipo de função de ativação utilizada e do contexto específico do problema. Experimentar diferentes técnicas de inicialização pode ser benéfico para encontrar a que melhor se adapta ao seu modelo.</p><pre>import torch<br>import torch.nn as nn<br>import torch.nn.init as init<br><br># Definindo uma rede neural simples com inicialização de Xavier<br>class SimpleNet(nn.Module):<br>    def __init__(self, input_size, hidden_size, output_size):<br>        super(SimpleNet, self).__init__()<br>        self.fc1 = nn.Linear(input_size, hidden_size)<br>        self.relu = nn.ReLU()<br>        self.fc2 = nn.Linear(hidden_size, output_size)<br><br>        # Inicialização de Xavier para as camadas lineares<br>        init.xavier_uniform_(self.fc1.weight)<br>        init.xavier_uniform_(self.fc2.weight)<br><br>    def forward(self, x):<br>        x = self.fc1(x)<br>        x = self.relu(x)<br>        x = self.fc2(x)<br>        return x<br><br># Parâmetros da rede<br>input_size = 10<br>hidden_size = 20<br>output_size = 5<br><br># Instanciando a rede neural<br>model = SimpleNet(input_size, hidden_size, output_size)<br><br># Exibindo a arquitetura da rede e os pesos<br>print(model)<br>print(&quot;\nPesos após a inicialização de Xavier:&quot;)<br>for name, param in model.named_parameters():<br>    if &#39;weight&#39; in name:<br>        print(name, param.data)</pre><p>Além disso podemos inicializar todos os pesos de uma rede sem passar camada por camada utilizando um loop sobre todos os parâmetros da rede.</p><pre>import torch<br>import torch.nn as nn<br>import torch.nn.init as init<br>import torch.optim as optim<br>from torch.utils.data import DataLoader, TensorDataset<br><br># Função para inicializar todos os pesos da rede usando Xavier<br>def initialize_weights(model):<br>    for param in model.parameters():<br>        if len(param.shape) &gt; 1:  # Inicializa apenas os pesos (não bias) das camadas lineares<br>            init.xavier_uniform_(param)<br><br># Definindo uma rede neural simples para regressão<br>class RegressionNet(nn.Module):<br>    def __init__(self, input_size, hidden_size, output_size):<br>        super(RegressionNet, self).__init__()<br>        self.fc1 = nn.Linear(input_size, hidden_size)<br>        self.relu = nn.ReLU()<br>        self.fc2 = nn.Linear(hidden_size, output_size)<br><br>    def forward(self, x):<br>        x = self.fc1(x)<br>        x = self.relu(x)<br>        x = self.fc2(x)<br>        return x<br><br><br># Instanciando a rede neural<br>model = RegressionNet(input_size, hidden_size, output_size)<br><br># Inicializando todos os pesos da rede usando a função definida<br>initialize_weights(model)</pre><p>Neste exemplo, a função initialize_weights percorre todos os parâmetros da rede e inicializa apenas os pesos (não bias) das camadas lineares usando a inicialização de Xavier. A função é chamada antes do treinamento para garantir que todos os pesos estejam inicializados corretamente.</p><h3>Conclusão</h3><p>O Pytorch é uma ferramenta essencial para criação de redes neurais e que possui diversas vantagens, como facilidade de prototipagem, suporte nativo a GPUs, sua integração com outras bibliotecas como numpy e scipy e também sua comunidade ativa. Com ele, podemos resolver problemas de diversas áreas e vários tipos de tarefas, como de visão computacional, NLP, Áudio e Processamento de sinais, Jogos e Robótica. Neste artigo, abordamos desde os fundamentos de machine learning com Pytorch, passando em diversos tópicos desde tensores ao treinamento, e validação de modelos e também em redes neurais rasas e profundas, assim como vários conceitos importantes como Funções de ativação, backpropagation, vanishing gradient e como incializar os pesos da rede.</p><p>Me <strong>siga clicando </strong><a href="https://medium.com/m/signin?actionUrl=%2F_%2Fapi%2Fsubscriptions%2Fnewsletters%2Fec554a52fda0&amp;operation=register&amp;redirect=https%3A%2F%2Fk3ybladewielder.medium.com%2F&amp;newsletterV3=ab4f983cc1ab&amp;newsletterV3Id=ec554a52fda0&amp;user=Alysson+Guimar%C3%A3es&amp;userId=ab4f983cc1ab"><strong>aqui</strong></a><strong> </strong>para acompanhar mais blog posts sobre ciência de dados, machine learning, NLP e demais temas.</p><h3>Como Citar</h3><blockquote>Guimarães, Alysson. (Fev 2024). Deep Neural Network com Pytorch. <a href="https://medium.com/data-hackers/deep-neural-network-com-pytorch-552abf6bed10">https://medium.com/data-hackers/</a>. <a href="https://medium.com/data-hackers/deep-neural-network-com-pytorch-552abf6bed10">https://medium.com/data-hackers/p-552abf6bed10</a></blockquote><p>ou</p><pre>@miscellaneous{guimaraes2024dnnpytorch,<br> title = {Deep Neural Network com Pytorch},<br> author = {Guimarães, Alysson},<br> journal = {https://medium.com/data-hackers/},<br> year = {2024},<br> month = {Fev},<br> url = {https://k3ybladewielder.medium.com/p-88d01ccbf604}<br>}</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=552abf6bed10" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/deep-neural-network-com-pytorch-552abf6bed10">Deep Neural Network com Pytorch</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Raciocínio em Modelos de Linguagem Pré-treinados (PLMs)]]></title>
            <link>https://medium.com/data-hackers/raciocinio-plms-821b188893b8?source=rss-ab4f983cc1ab------2</link>
            <guid isPermaLink="false">https://medium.com/p/821b188893b8</guid>
            <category><![CDATA[language-reasoning]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[nlr]]></category>
            <category><![CDATA[plm]]></category>
            <dc:creator><![CDATA[Alysson Guimarães]]></dc:creator>
            <pubDate>Fri, 19 Jan 2024 01:13:46 GMT</pubDate>
            <atom:updated>2024-06-07T18:58:20.136Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VaHPwnDxNsGlc0l9" /><figcaption><strong>Generated with AI</strong> ∙ January 15, 2024 at 12:16 PM</figcaption></figure><h3>Introdução</h3><p>O advento de aplicações baseadas em modelos de linguagem pré-treinados a partir do lançamento do chatgpt em novembro de 2022, tem atraído um interesse de uma grande parte da população, desde curiosos <em>early adopters</em> de novas tecnologias, grandes empresas e também novos pesquisadores e novas áreas de pesquisa. Quase que diariamente novos modelos com diferente número de parâmetros, e uma infinidade de variações e evoluções de modelos e formas de treinamento são lançados quase que diariamente desde então. Com o crescimento e evolução dos modelos, novos focos de pesquisa foram surgindo, como os evidenciados na detecção das chamadas “habilidades emergentes” de <em>large language models (LLM)</em>, como compreensão de linguagem (natural language understanding), codificação de conhecimentos implícitos nos parâmetros, aprendizado contextual (in-context learning), resolução de tarefas mais complexas e também, <strong>a habilidade de raciocinar</strong>, que será o foco deste <em>post</em>. <strong><em>Natural Language Reasoning (NLR)</em></strong> é o campo de <strong><em>Natural Language Process (NLP)</em></strong> em que se estuda e utiliza o raciocínio em linguagem natural para resolução de tarefas, em que a conclusão é um conhecimento novo. Este <em>post</em> abordará os estudos sobre NLR, as abordagens existentes, técnicas utilizadas para potencializar essa habilidade de raciocinar e como essa habilidade é avaliada.</p><h3>Modelos de Linguagem Pré-treinados</h3><p>Modelos de linguagem pre-treinados (<strong>PLMs</strong>) são baseados na arquitetura transformers e treinados com uma quantidade massiva de textos via aprendizado não supervisionado. Eles podem ser divididos em <strong>encoder-only, decoder-only e encoder-decoder</strong>, como o BERT, GPT e o T5 respectivamente. Além disso, também são <strong>bidirecionais</strong> (encoder-only) ou <strong>causais</strong> (decoder-only e encoder-decoder). Os modelos bidirecionais são utilizados para tasks <strong>discriminativas</strong>, ou seja, classificação entre diferentes categorias ou classes (ex. classificação de frases ou NER). Enquanto que os modelos causais são geralmente usados em tarefas <strong>generativas</strong>.</p><p>Devido à sua forma de treinamento, que resulta na criação de representações numéricas contextuais (pragmáticas) de palavras e frases, e à tendência recente de aumentar consideravelmente o número de parâmetros, alguns estudos revelaram que PLMs suficientemente grandes, como aqueles com mais de 100 bilhões de parâmetros, exibem habilidades emergentes. Estas incluem a capacidade de compreender linguagem (natural langauge understanding), assimilar conhecimentos implícitos em seus parâmetros, <em>in-context learning</em> e habilidades para resolver tarefas mais complexas. Outra habilidade emergente observada foi a capacidade de “<strong>raciocinar</strong>”, demonstrando padrões de raciocínio semelhantes aos descritos na literatura cognitiva humana. No entanto, a razão por trás dessa habilidade emergente <strong>ainda não é completamente compreendida</strong>.</p><p>Além disso, esses modelos demonstraram a capacidade de aprender por meio de poucas demonstrações (few-shot learning). Isso resultou na popularização do in-context learning como um paradigma, no qual previsões podem ser feitas com apenas algumas demonstrações, sem a necessidade de ajuste fino de hiperparâmetros. Diante desse grande potencial, esses modelos têm sido amplamente aplicados em tarefas de raciocínio.</p><h3>Natural Language Reasoning</h3><p>Raciocinar é o processo de pensar sobre algo de maneira lógica e sistemática, utilizando evidências e experiências passadas para chegar a conclusões ou tomar decisões. Esse processo engloba a realização de inferências, a avaliação de argumentos e a formulação de conclusões lógicas com base nas informações disponíveis. Essa atividade desempenha um papel crucial em diversas atividades intelectuais, incluindo a resolução de problemas, a tomada de decisões e o pensamento crítico.</p><p>O estudo do raciocínio é fundamental em diferentes campos, tais como psicologia, filosofia, ciência da computação, ciência cognitiva, entre outros. Ele fornece insights valiosos sobre como os seres humanos processam informações, tomam decisões e enfrentam desafios intelectuais. Compreender o raciocínio é essencial não apenas para o avanço dessas disciplinas acadêmicas, mas também para aplicação prática em diversas áreas da vida.</p><p>O <strong>Natural Language Reasoning (NLR)</strong> ou raciocínio em linguagem natural é um processo que integra diversos conhecimentos para derivar <strong>novas conclusões</strong> sobre o mundo, seja ele realista ou hipotético, utilizando <strong>modelos de linguagem pré-treinados (PLMs)</strong>. Esse processo envolve inferir conclusões a partir de um conjunto de premissas, que consistem em uma ou mais etapas <strong>(inference steps)</strong>, onde as premissas afirmam a veracidade ou falsidade de algo. O conhecimento utilizado nesse processo pode ser proveniente de fontes explícitas e implícitas.</p><p>Ao contrário de outras tarefas, como Question-Answer ou Information Retrieval, <strong>em que a resposta está contida nas informações existentes</strong>, no raciocínio, a <strong>conclusão representa uma nova informação</strong>. Esse aspecto destaca a capacidade dos modelos de linguagem pré-treinados em realizar inferências mais complexas e gerar insights que <strong>não estão diretamente expressos nas premissas</strong>. O Natural Language Reasoning desempenha um papel crucial em diversas aplicações, contribuindo para uma compreensão mais profunda e abrangente das relações e implicações presentes nos dados linguísticos, além de trazer explicabilidade para os outputs e potencializar a interação homem-maquina já que está sendo utilizado linguagem natural, coisa que era difícil ao utilizar linguagem formal ou simbólica.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/732/1*ScKyF_nFGG4D4mhiWFMQ-g.png" /><figcaption>(Yu, et al. 2023)</figcaption></figure><p>O processo de inferência pode ser dividido em (pelo menos) três: <strong>Dedução, indução e abdução</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/848/1*IZrIhv7l52_DL-f3bUFLxQ.png" /><figcaption>(Yu, et al. 2023)</figcaption></figure><ul><li>Uma <strong>inferência dedutiva</strong> é inferir conhecimentos válidos (conclusão) do conhecimento dado (premissas). Ou seja, a conclusão é desenhada baseado na validade das premissas. Se as premissas são verdadeiras, obrigatoriamente a conclusão também o será.</li><li><strong>Inferência indutiva</strong> é inferir um conhecimento provável, que descreve uma regra geral, extrapolada do conhecimento dado. É o tipo de raciocínio em que a conclusão é baeado nas observações ou evidencias. A conclusão é provavelmente verdadeira baseado nas evidências disponíveis.</li><li><strong>Inferência abdutiva</strong> é inferir um conhecimento plausível, como a melhor explicação (causa), para dado conhecimento (fenômeno). É o tipo de raciocínio em que a conclusão é baseada na melhor explicação para um conjunto de observações, é baseado na plausabilidade.</li></ul><p>Além disso, as inferências podem ser <strong>monotônicas</strong> (dedução) e <strong><em>defeasible</em> </strong>(indução e abdução) ou revogável. A primeira busca derivar conclusões válidas, ou seja, suportado pelas premissas, e a segunda busca produzir conclusões plausíveis. Essa diferença entre esses dois tipos acarreta diferentes características, como as relações de inferência entre premissas e conclusões, qualidade da inferência, e o conhecimento requerido, e o <em>reasoning path</em> para chegar na conclusão.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/620/1*LUvS3CA4J0Va_e1EmOf0dA.png" /></figure><p>É importante frisar que <strong>quando falamos de raciocínio em PLMs, não é exatamente o mesmo raciocíonio performado por humanos, e sim uma capacidade de mimetização dele, mas o funcionamento em si não tem nada em comum com o raciocínio humano</strong>, assim como uma rede neural é simplesmente uma representação simplificada e <strong>bioinspirada</strong>, e não é igual e não funciona igual aos neuronios humanos.</p><h3>Abordagens de NLR</h3><p>Existem três abordagens de NLR, o <em>end-to-end reasoning</em>, <em>forward reasoning</em> e <em>backward reasoning.</em> A principal diferença entre cada método está na etapa de raciocínio que é criado.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/845/1*tGA5IzW9Tvo9d4nF7b6u6Q.png" /><figcaption>(Yu, et al. 2023)</figcaption></figure><ul><li><strong>End-to-end reasoning</strong> somente gera o output, sem nenhuma explicação ou conclusão intermediária, <em>reasoning path</em>, ou se foi necessário um ou vários passos para gerar o output. É uma abordagem <em>black box</em>, já que não temos acesso a forma em que o output foi gerado. Para um modelo executar essa abordagem, é preciso agregar multiplos conhecimentos e raciocinar sobre eles.</li><li><strong>Forward reasoning</strong> compõem o conhecimento existente (codificado no modelo) repetidamente para derivar novos conhecimentos até alcançar as respostas. Esse processo gera as etapas de raciocínio, criando confiabilidade e interpretabilidade dos outputs</li><li><strong>Backward reasoning</strong> quebra o problema em vários subproblemas, e os resolve até encontrar a resposta. Assim como o forward reasoning, pode ser usado para criar confiabilidade com as etapas de raciocínio, além de aumentar a performace do output.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/646/1*-VVTa8F8DILplWvVWltIMQ.png" /><figcaption>(Yu, et al. 2023)</figcaption></figure><h3>Técnicas para impulsionar o raciocínio em PLMs</h3><p>Com base em pesquisas que identificaram e demonstraram as capacidades de aprendizado contextual e raciocínio, foram elaboradas técnicas visando expandir o aproveitamento dessas habilidades.</p><p>Diversas estratégias são empregadas para aprimorar e potencializar os resultados do modelo, estimulando as capacidades de raciocínio dos Modelos de Linguagem Pré-treinados (PLMs). Isso inclui o <strong>ajuste fino supervisionado completo (</strong><em>Fully Supervised Finetuning</em>), técnicas de <strong>engenharia de prompts</strong> direcionadas às habilidades de aprendizado contextual, como o <strong>chain-of-thought</strong> e suas variantes, engenharia de justificativas (<strong>rationale engineering</strong>) e decomposição de problemas (problem decomposition).</p><h4>Fully Supervised Finetuning</h4><p>Os modelos podem ser ajustados pare gerarem justificativas, etapas de raciocínio, que levam as respostas certas, levando a aumentar a capacidade de raciocínio dos modelos. Além disso, existem pesquisas focadas em melhorar a capacidade de raciocínio de modelos de linguagem menores através de <strong><em>fully supervised finetuning</em></strong> em datasets. Essa abordagem varia muito foca em modelos menores, treinados em datasets específicos.</p><h4>Chain-of-thougt (CoT)</h4><p>Modelos suficientemente grandes como o GPT-3 demonstraram desempenhos superiores em diversas tasks ao utilizar <em>in-context learning</em>. Para encorajar os modelos a raciocinar em vez de simplesmente responder diretamente (end-to-end reasoning), podemos guiar o modelo para gerar “raciocínio” explícitamente. A abordagem de <strong>chain of thougt (CoT)</strong> faz exatamente isso.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5igrlGJumhmi0y5mA0cJBQ.png" /><figcaption><a href="https://doi.org/10.48550/arXiv.2201.11903">(Wei, et al. 2022)</a></figcaption></figure><p>Essa abordagem envolve dar alguns exemplos de “chain of thought”, com passos intermediários de raciocínio em linguagem natural (NLR) para o modelo. Desse modo, dado uma pergunta, o modelo aprende a gerar justificativas antes da resposta final.</p><h4>Rationale Engineering</h4><p>Rationale Engineering é criar exemplos manualmente de etapas intermediárias de raciocínio e aplicar um greedy decoding em sua geração. O objetivo é aumentar efetivamente ou utilizar o raciocínio dos LLMs. Isso pode ser alcançado através de rationale refinement, que é a criação de exemplos mais efetivos ou rationale exploration e rationale verification, que é a exploração e verificação das justificativas criadas pelos modelos de linguagem</p><h4>Problem Decomposition</h4><p>Problem decomposition busca resolver o problema de tarefas complexas, em que os modelos tem problemas com abordagens de in-context learning. Consiste em quebrar o problema em problema menores, e ao resolver cada subproblema, podemos efetivamente resolver um problema complexo.</p><h4>Métodos Híbridos</h4><p>Os métodos híbridos (propmpt + problem decomposition) foram desenvolvidos para melhorar a performace das capacidades de raciocínio (não somente utilizá-las através de <em>prompts</em>) e fazer um moelhor uso desses modelos para resolução de tarefas complexas. Ou seja, <strong>são feitas modificações nos parâmetros dos modelos</strong>, para ajustálos de forma a melhorar a capacidade de raciocínio. Essa abordagem envolve tanto a <strong>melhoria das capacidades de raciocínio</strong> e o <strong>uso de técnicas de prompt para efetivamente utilizar</strong> essas capacidades.</p><p>Uma abordagem é o <strong>reasoning-enhanced training</strong>, que consiste no pre treino ou <em>finetune</em> do modelo em datasets que incluem “raciocínio”.</p><p>Outra abordagem híbrida é o <strong>Bootstrapping &amp; Self-improving</strong>, que é o auto-aperfeiçoamento das habilidades de raciocínio através de <em>bootstrapping</em>. Nessa abordagem, um LLM é treinado e refinado iterativamente com seu próprio output, como o Self-Taught Reasoner (STaR). O modelo gera as primeiras justificativas com CoT, e depois ele é ajustado nas justificativas que levam a respostas corretas, esse processo pode ser repetido, em que cada iteração resulta numa melhoria do modelo que pode gerar melhores dados de treinamento.</p><h3>Avaliação e Análises do Raciocínio</h3><p>As principais formas de mensurar o raciocínio dos modelos são através da <strong>performace nas tasks</strong> e da <strong>análise do raciocínio</strong>.</p><h4>End Task Performace</h4><p>A análise da performace em tarefas envolve a avaliação de métricas como <strong>acurácia</strong> em benchmarks comuns, como <strong>raciocínio aritmético, commonsense reasoning, racionínio simbólico</strong>, entre outros.</p><p><strong>Raciocínio Aritmético</strong> é a habilidade de entender e aplicar conceitos e princípios matemáticos para resolver problemas que envolvem operações aritméticas. Existem diversos datasets com o objetivo de mensurar essa habilidade como GSM8K, MathQA, SVAMP, ASDIV e AQuA.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/693/0*Ivtg4p3oQqxdNrE5" /><figcaption>Pergunta traduzida e adaptada do Winegrad Challeng Schema (WCS) testada no ChatGPT da task de Commonsense Reasoning</figcaption></figure><p><strong>Commonsense reasoning</strong> é o uso de conhecimento diário, conhecimento comum, e entender e fazer julgamentos e predições sobre novas situações. Esse conhecimento é geralmente entendido como um conhecimento externo sobre o mundo que é presumido que todos os humanos possuem; Por definição, commonsense reasoning requere conhecimento externo, que pode surgir de várias fontes. Algum conhecimento externo é capturado durante o treinamento de forma implícita nos dados utilizados.</p><p><strong>Raciocínio simbólico</strong> é envolve a manipulação de sombolos de acordo com regras formais. É o uso de simbolos abstratos que representam conceitos e relacionamentos, e a manipulação deles de acordo com regras precisas para gerar conclusões e resolver problemas</p><h4>Análise do Raciocínio</h4><p>Apesar do alto desempenho em várias tasks e benchmarks de raciocínio, não fica claro se ele é resultado de um raciocínio real do modelo ou se simplesmente segue heurísticas simples para solucionar as tarefas.</p><p>Em estudos, foi verificado que LLMs podem produzir justificativas inválidas mesmo performando reasoning nas tasks. Isso porquê <strong>a maioria das avaliações existentes foca na acurácia das tasks</strong>, em vez de avaliar diretamente as etapas de raciocínio. Mas já existem pesquisas e desenvolvimentos para suprir esse gap, como o <strong>ROSCOE</strong> e o dataset <strong>PrOntoQA</strong>.</p><p><strong>Esse gap de avaliação de etapas de raciocínio deixa em aberto se os modelos realmente são capazes de raciocinar de forma similar a humanos</strong>, ou se são simplesmente capazes de alcançar um bom desempenho nas tarefas através de outros meios. Por isso, mais pesquisas são necessárias para analisar mais formalmente as habilidades de raciocínio dos LLMs.</p><p>Para aprofundar cada task citada e outras, datasets e estudos feitos, recomendo a leitura das referências citadas.</p><h3>Afinal, os modelos raciocinam ou não ?</h3><p>Apesar das semelhanças, ainda que mínimas, e de algumas evidências, estas não são suficientes para concluir que os Modelos de Linguagem Pré-treinados (LLMs) são capazes de realizar um verdadeiro raciocínio. Não está claro se esses modelos fazem previsões com base no raciocínio ou em heurísticas. Para a maioria dos benchmarks de raciocínio, é possível resolvê-los por meio de programas com heurísticas, alcançando alta performance, sem que consideremos que o programa está efetivamente “raciocinando”.</p><p>Embora os LLMs demonstrem padrões de raciocínio semelhantes aos humanos, isso não implica necessariamente que eles estejam se comportando como humanos; muitas vezes, estão simplesmente imitando a estrutura linguística-lógica-racional. Além disso, os LLMs enfrentam desafios em tarefas que demandam um raciocínio mais complexo. Contudo, mesmo diante dessas limitações, ainda não está claro se os modelos atuais são incapazes de raciocinar de maneira robusta.</p><p>Apesar dos progressos significativos, a capacidade real de raciocínio desses modelos não está clara, sendo uma incógnita se eles a possuem verdadeiramente ou se estão apenas utilizando informações memorizadas por meio de heurísticas. É evidente que são necessárias mais pesquisas para uma compreensão completa das habilidades de raciocínio, aprimorar suas capacidades e determinar o potencial de seu uso em diversas aplicações.</p><h3>Conclusão</h3><p>Neste artigo foi abordado de forma introdutória o Natural Language Reasoning (NLR), assim como os conceitos relacionados a ele, como as abordagens existentes, técnicas utilizadas para potencializar essa habilidade de raciocinar e como essa habilidade é avaliada. Também, falamos brevemente sobre a real ou aparente capacidade de raciocinar, mas é evidente que mais estudos precisam ser feitos, principalmente a criação de novas formas de avaliação de raciocínio, como avaliação dos reasoning paths para compreensão da capacidade de raciocinar.</p><p>Me <strong>siga clicando </strong><a href="https://medium.com/m/signin?actionUrl=%2F_%2Fapi%2Fsubscriptions%2Fnewsletters%2Fec554a52fda0&amp;operation=register&amp;redirect=https%3A%2F%2Fk3ybladewielder.medium.com%2F&amp;newsletterV3=ab4f983cc1ab&amp;newsletterV3Id=ec554a52fda0&amp;user=Alysson+Guimar%C3%A3es&amp;userId=ab4f983cc1ab"><strong>aqui</strong></a><strong> </strong>para acompanhar mais blog posts aprofundando sobre o tema.</p><h3>Como Citar</h3><blockquote>Guimarães, Alysson. (Jan 2024). Raciocínio em Modelos de Linguagem Pré-treinados (PLMs). <a href="https://medium.com/data-hackers/raciocinio-plms-821b188893b8">https://medium.com/data-hackers/</a>. <a href="https://medium.com/data-hackers/raciocinio-plms-821b188893b8">https://medium.com/data-hackers/p-821b188893b8</a></blockquote><p>ou</p><pre>@miscellaneous{guimaraes2024plmsreasoning,<br> title = {Raciocínio em Modelos de Linguagem Pré-treinados (PLMs)},<br> author = {Guimarães, Alysson},<br> journal = {https://medium.com/data-hackers/},<br> year = {2024},<br> month = {Jan},<br> url = {https://k3ybladewielder.medium.com/p-88d01ccbf604}<br>}</pre><h3>Referências</h3><p>Yu, F., Zhang, H., Tiwari, P., &amp; Wang, B. <strong>Natural Language Reasoning, A Survey</strong>. DOI: <a href="https://doi.org/10.48550/arXiv.2303.14725">https://doi.org/10.48550/arXiv.2303.14725</a></p><p>HUANG, Jie; CHANG, Kevin Chen-Chuan. <strong>Towards Reasoning in Large Language Models: A Survey</strong>. Findings of the Association for Computational Linguistics: ACL 2023, pages 1049–1065, Toronto, Canada. Association for Computational Linguistics. DOI: 10.18653/v1/2023.findings-acl.67</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=821b188893b8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-hackers/raciocinio-plms-821b188893b8">Raciocínio em Modelos de Linguagem Pré-treinados (PLMs)</a> was originally published in <a href="https://medium.com/data-hackers">Data Hackers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>