A pergunta
Dois agentes precisam ir do ponto A ao ponto B num plano. Cada um só sabe fazer uma coisa: olhar para B, escolher uma direção e andar um trecho reto. Os dois erram a mira. A diferença entre eles é o tamanho do passo e o tamanho do erro.
O primeiro é efetivo: mira melhor, passo curto. O segundo é eficiente no sentido em que a palavra costuma ser usada quando se fala de produtividade com IA: cobre quase 4x mais terreno por passo, ao custo de errar mais a direção. A pergunta é qual dos dois chega, e a resposta não é a que a intuição sugere.
Efetivo
L = 0,47 · σ = 22°
Também erra feio a mira, mas anda pouco por passo, então o erro não tem espaço para virar desvio. Chega em 23 passos.
Eficiente
L = 1,80 · σ = 35°
Cobre quase 4x mais terreno por passo e erra 1,6x mais a direção. Chega perto de B em 6 passos e depois orbita.
Adaptativo
Lk = c · dk · σ = 35°
A mesma mira torta do eficiente. Só encolhe o passo conforme se aproxima. Chega em 8 passos na mediana.
O modelo
O agente opera em malha fechada: a cada passo ele re-mira em B. Isso é importante, porque o caso de mirar uma única vez e andar em linha reta é trivial e não tem graça. Com re-mira, um passo é isto:
Chamando de dk a distância até B, dá para escrever o que acontece com essa distância depois de um passo. O termo negativo aproxima, o termo positivo afasta:
É aqui que a coisa fica interessante. Existe uma distância em que os dois termos se anulam e o progresso esperado zera. Igualando 2Ldkc = L²:
Esse d* é um piso. Não importa quantos passos você dê: em média, você não desce abaixo dele. O agente fica orbitando um anel de raio d* em volta do alvo. E note o que a fórmula diz: o piso é proporcional ao tamanho do passo. Dobrar o passo dobra a distância mínima em que você consegue parar.
Rodando os números dos dois perfis com o alvo tendo raio ρ = 0,35:
| Perfil | L | σ | c = e-σ²/2 | d* = L / 2c | Entra no alvo? |
|---|---|---|---|---|---|
| Efetivo | 0,47 | 22° | 0,9289 | 0,253 | sim, com folga |
| Eficiente | 1,80 | 35° | 0,8298 | 1,085 | não, para nunca |
1,085 é três vezes o raio do alvo. O agente eficiente não erra o alvo por azar. Ele erra por construção: o passo dele é grande demais para o próprio erro de mira, e existe um anel em volta de B onde ele fica preso.
Os números medidos
A fórmula é bonita, mas eu queria saber se ela descrevia mesmo a simulação. Rodei 3.000 execuções de cada perfil, com sementes diferentes, orçamento generoso de 400 passos:
| Perfil | Chegou | Mediana | Média | p90 | Pior caso | Caminho andado |
|---|---|---|---|---|---|---|
| Efetivo | 100% | 23 | 22,9 | 24 | 26 | 10,8 |
| Eficiente | 100% | 28 | 37,9 | 84 | 294 | 50,4 |
| Adaptativo | 100% | 8 | 8,4 | 10 | 15 | 14,2 |
O agente eficiente chega. Só que não por convergência: por sorteio. Ele orbita o anel e, de vez em quando, um passo aleatório o joga dentro do alvo. Por isso a mediana (28) é bem menor que a média (37,9), e por isso existe uma cauda que vai até 294 passos. Enquanto isso ele andou 50,4 unidades para percorrer um trajeto de 10,0. O efetivo andou 10,8.
Compare as caudas, que é onde mora a diferença prática. O efetivo vai de 23 passos na mediana a 26 no pior caso das 3.000 execuções: três passos de dispersão. O eficiente vai de 28 a 294, e o p90 dele é três vezes a própria mediana. Os dois erram a mira; só um deles converte esse erro em imprevisibilidade de prazo.
Vale reparar num momento no meio do trajeto: no passo 20 os dois estão praticamente empatados, o efetivo a 1,33 de B e o eficiente a 1,04. Se o vídeo terminasse ali, o placar seria de empate técnico. Dali em diante um fecha e o outro não consegue mais, e é essa segunda metade que decide.
A média medida da distância do eficiente ao alvo, depois de estabilizar, foi 1,085. A fórmula previa 1,085. Foi o momento em que decidi que o vídeo valia a pena.
O desfecho
Se o problema é o passo ser grande demais perto do alvo, a correção é óbvia depois que você vê: faça o passo encolher junto com a distância. Pondo Lk = α·dk na recursão:
Agora o piso some: a distância contrai por um fator constante a cada passo, o que é convergência geométrica. E o α ótimo é exatamente c, o fator de pontaria. Ou seja: o tamanho de passo que você pode se dar ao luxo de usar é definido pela sua precisão. Quem mira bem pode andar muito. Quem mira mal não pode, e insistir no passo grande é o que produz a órbita.
O agente adaptativo do vídeo tem a mesma mira péssima de 35° do eficiente. A única mudança é encolher o passo perto de B. Ele chega em 8 passos, contra 23 do preciso e 38 do eficiente. É o argumento inteiro: eficiência não substitui pontaria, ela é consequência dela.
Como fiz com o Manim
Manim é uma biblioteca Python: você declara objetos geométricos (mobjects) e anima transformações neles dentro de um método construct(). O render sai em mp4 via ffmpeg. A versão que usei é a Manim Community v0.20.1, que é o fork mantido pela comunidade e o que se deve usar hoje (o repositório pessoal do Grant, o manimgl, é otimizado para o fluxo dele, não para terceiros).
1. Instalação
Ambiente virtual dentro da pasta do projeto, para a biblioteca e o cache de render nunca encostarem no repositório do site. O Manim precisa de cairo e pango (renderização vetorial e de texto) e de ffmpeg. Para fórmulas com MathTex ele precisa de uma instalação LaTeX, porque as equações são compiladas de verdade em LaTeX e convertidas em SVG por dvisvgm.
# dependências de sistema (Arch; nos demais, os pacotes equivalentes)
sudo pacman -S --needed cairo pango ffmpeg \
texlive-basic texlive-latexextra texlive-fontsrecommended texlive-binextra
# a lib fica num venv local, fora do versionamento
python -m venv manim/.venv
manim/.venv/bin/python -m pip install manim
Em Python 3.14 não havia wheels prontos para pycairo e manimpango, então o pip compilou os dois do código-fonte. Funcionou sem intervenção porque os headers de cairo e pango já estavam instalados. Se faltarem, a mensagem de erro é sobre cairo.h não encontrado, e não sobre Python.
2. Separar o modelo da cena
Esta foi a decisão que mais economizou tempo. Toda a matemática vive em sim.py, que só depende de numpy. A cena importa dele. Assim eu consigo rodar 3.000 simulações em segundos, sem tocar no Manim, e o render nunca vira o gargalo da exploração.
manim/ ├── sim.py o modelo: perfis, um passo, a recursão. Só numpy. ├── efetividade.py a cena Manim. Só encenação. ├── seed_search.py escolhe a semente da execução que vai ao ar. ├── diagnostico.py os números de 3.000 execuções. ├── sweep.py varredura de parâmetros. ├── requirements.txt ├── .venv/ gitignore ├── media/ gitignore: cache de render, centenas de MB └── out/ gitignore: frames de inspeção
@dataclass
class Perfil:
nome: str
passo: float
sigma_graus: float
adaptativo: bool = False
@property
def c(self) -> float:
"""E[cos eps] para erro gaussiano: o 'fator de pontaria', em (0, 1]."""
return math.exp(-(self.sigma ** 2) / 2)
@property
def raio_orbita(self) -> float:
"""d* = L / (2c). Distância abaixo da qual o agente não desce."""
return self.passo / (2 * self.c)
3. Escolher a semente sem trapacear
O modelo é aleatório, mas o vídeo mostra uma execução só. Existe uma tentação óbvia aqui: rodar até achar a semente em que o agente eficiente se dá mal de forma espetacular. Isso seria fabricar o resultado.
O que fiz foi diferente. Defini critérios de legibilidade (o eficiente precisa liderar de forma visível no começo, o efetivo precisa chegar perto da mediana medida) e depois contei quantas sementes passavam. Foram 1.081 de 2.999, ou seja 36,0%. A execução que está no vídeo é o caso comum, não a exceção. Esse número está impresso na saída do script justamente para poder ser cobrado.
Os dois agentes usam a mesma semente. Isso é de propósito: mesma sequência de azar, estratégias diferentes. É um experimento controlado, não duas corridas separadas.
4. O truque que faz a animação fluir
A forma ingênua de animar 26 passos é chamar self.play() 26 vezes. O resultado é picotado, porque cada chamada tem começo e fim próprios, e o render fica lento. A forma certa no Manim é um ValueTracker: um número animável que serve de relógio, mais updaters que redesenham os objetos em função dele. Aí toda a corrida cabe em uma chamada só, com velocidade constante.
Cada passo é desenhado como uma seta, não como um trecho de linha. É o que o modelo diz que ele é: um vetor, com magnitude e ângulo. E tem um efeito colateral ótimo no terceiro ato, onde as setas visivelmente encolhem conforme o agente adaptativo se aproxima do alvo.
class Agente:
"""Um vetor por passo, revelados em sequência por um ValueTracker."""
def __init__(self, pontos, cor, espessura, raio, ponta):
self.pontos = pontos
self.t = ValueTracker(0.0)
self.opacidade = ValueTracker(1.0) # escurecer sem brigar com o updater
# Setas dos passos concluídos. A geometria NUNCA é mexida: só a opacidade.
self.setas = VGroup(*[
Arrow(em_3d(a), em_3d(b), **self._estilo)
for a, b in pairwise(pontos)
])
self.setas.set_opacity(0)
self.setas.add_updater(self._atualizar_setas)
# A seta do passo em curso é um objeto à parte, refeito a cada quadro.
self.corrente = VMobject()
self.corrente.add_updater(self._atualizar_corrente)
def _atualizar_setas(self, grupo):
concluidos = int(np.floor(self.t.get_value()))
alfa = self.opacidade.get_value()
for i, seta in enumerate(grupo):
seta.set_opacity(alfa if i < concluidos else 0.0)
def _atualizar_corrente(self, m):
t = self.t.get_value(); k = int(np.floor(t)); fracao = t - k
# Abaixo de 5% do passo a seta é degenerada e o Manim não a orienta.
if k >= len(self.pontos) - 1 or fracao <= 0.05:
m.set_opacity(0); return
fim = self.pontos[k] + fracao * (self.pontos[k + 1] - self.pontos[k])
m.become(Arrow(em_3d(self.pontos[k]), em_3d(fim), **self._estilo))
m.set_opacity(self.opacidade.get_value())
# a corrida inteira, os dois agentes em lockstep, numa chamada:
self.play(
efetivo.t.animate.set_value(24),
eficiente.t.animate.set_value(24),
run_time=6.2,
rate_func=linear, # sem ease: passo tem que ter cadência constante
)
Três detalhes que só aparecem quando você erra. O rate_func=linear é obrigatório: o padrão do Manim é smooth, que acelera no meio e desacelera nas pontas, e isso destrói a leitura de cadência de passos. Os updaters precisam ser removidos com clear_updaters() antes do FadeOut, senão o objeto continua sendo redesenhado enquanto tenta desaparecer. E o motivo de a seta em curso ser um objeto separado está no diário de erros abaixo.
A opacidade também tem que passar por um ValueTracker. Como o updater reescreve a opacidade a cada quadro, um .animate.set_opacity() por fora seria sobrescrito na hora. O updater lê o tracker e multiplica, então escurecer o agente eficiente durante a chegada do efetivo vira eficiente.opacidade.animate.set_value(0.35).
5. Paleta
Preto e branco puro, como o Manim é por padrão. Sem cor nenhuma, os dois agentes precisam se distinguir por outra coisa: o efetivo são setas brancas finas (2,4 px), o eficiente são setas cinzas grossas (3,8 px). A espessura carrega o significado, já que passo grande é literalmente traço mais pesado. E a ordem de adição na cena define o empilhamento, então o branco entra depois do cinza para não sumir no emaranhado.
A razão da ponta precisa ser diferente entre os dois. O Manim aceita max_tip_length_to_length_ratio, que dimensiona a cabeça em função do comprimento da seta. As setas do eficiente são quase 4x mais longas, então com a mesma razão a cabeça delas virava um borrão perto de B: 0,16 para o eficiente contra 0,30 para o efetivo.
6. Render e iteração
O ciclo que funcionou: renderizar rápido e feio, extrair quadros com ffmpeg, olhar, corrigir. Nunca abri o vídeo inteiro durante a iteração, só grades de frames em pontos escolhidos da linha do tempo.
# iteração: 480p a 15 fps, uns 20 segundos de render
.venv/bin/manim -ql --disable_caching efetividade.py Efetividade
# inspeção: quadros em pontos-chave, mais rápido que assistir
for t in 4.5 21 30 38; do
ffmpeg -ss $t -i media/videos/efetividade/480p15/Efetividade.mp4 \
-frames:v 1 -y out/frames/f$t.png
done
# final: 1080p a 60 fps, cerca de um minuto em 16 núcleos
.venv/bin/manim -qh efetividade.py Efetividade
# compressão para a web: 2,6 MB viram 1,7 MB, com faststart
ffmpeg -i media/videos/efetividade/1080p60/Efetividade.mp4 \
-c:v libx264 -preset slow -crf 21 -pix_fmt yuv420p \
-movflags +faststart -an assets/video/efetividade-eficiencia.mp4
O --disable_caching importa durante a iteração. O Manim guarda trechos já renderizados e reaproveita, o que é ótimo em produção e péssimo quando você está mexendo em updaters, porque ele às vezes serve um trecho velho e você fica caçando um bug que já corrigiu.
O -movflags +faststart move o índice do mp4 para o começo do arquivo. Sem isso o navegador precisa baixar o vídeo quase inteiro antes de começar a tocar.
O que deu errado
A parte que os tutoriais não mostram. Em ordem cronológica:
- O modelo estava fácil demais para o agente eficiente Com o alvo em raio 0,45, ele acabava caindo dentro por sorteio em 90% das execuções. Eu já tinha um seed search filtrando os 10% em que ele falhava, o que teria sido escolher o resultado a dedo. Varri o espaço de parâmetros procurando um regime em que a tese valesse no caso típico e não achei nenhum: mesmo no melhor ponto ele ainda chegava em 37% das vezes. A correção não foi mexer nos parâmetros, foi trocar a pergunta. Medir "quantos passos ele leva e quanto caminho ele anda" em vez de "ele chega ou não" produziu números muito mais fortes (37,9 passos de média contra 21, cauda de 294, 5x o caminho andado) e honestos.
- Um agente sem erro nenhum, em três diagnósticos Com passo 0,50 e trajeto de 10,0, o efetivo chegava em exatamente 20 passos nas 3.000 execuções: mediana, p90 e pior caso, todos 20. Parece resultado fraudado. Primeiro diagnóstico: a culpa é 10,0 ÷ 0,5 dar inteiro redondo. Troquei o passo para 0,47, incomensurável com o trajeto, e a variância continuou zero, agora em 21. Segundo: a causa não era aritmética. Com erro de 5°, cada passo entrega cos(5°) = 99,6% do avanço pretendido, e ao longo de 21 passos o ruído nunca soma um passo inteiro. O agente é determinístico na prática, e publiquei isso como resultado.
- O sintoma que os números não mostravam Vendo o vídeo pronto, o agente efetivo não parece ter erro nenhum: a trajetória dele é uma reta. Isso apaga a premissa inteira, que é os dois erram, um erra menos. Um espectador vê um agente perfeito contra um bêbado, e não dois agentes imperfeitos com estratégias diferentes de passo. O reflexo foi trocar a semente, e ele estava errado: com σ = 5° o sistema é determinístico, então nenhuma semente produz um resultado diferente. A medição de 3.000 execuções já provava isso, eu é que não tinha ligado uma coisa à outra. A alavanca era o σ, não a semente. Subi o erro do efetivo para 22° e conferi renderizando os candidatos lado a lado, porque "a trajetória parece torta?" é pergunta visual e não se responde por tabela. Com 22° o caminho zigzagueia de forma óbvia, a chegada ganha dispersão real (23 na mediana, 26 no pior caso) e o d* = 0,253 continua cabendo com folga no alvo de 0,35. Efeito colateral: a tese ficou mais forte, porque agora os dois erram feio e o que os separa é quanto espaço o passo dá para o erro virar desvio.
-
A fonte padrão saiu serifada
O
Text()do Manim usa a fonte padrão do sistema via Pango, que aqui resolvia para uma serifada. Ficou conflitando com a Computer Modern doMathTex, que é serifada de propósito. Fixeifont="Noto Sans"em todo texto de interface e deixei a serifada só para as equações, que é onde ela pertence. - A linha branca sumia no emaranhado cinza A chegada do agente efetivo, que é o clímax do segundo ato, acontecia bem no meio da bagunça de trajetórias do eficiente. Duas correções: inverter a ordem de adição na cena, para o branco ficar por cima, e acrescentar um halo circular permanente no ponto de chegada.
-
Colisões de layout que só aparecem no quadro
A legenda do terceiro ato entrava por cima do anel tracejado. Depois de mover a fórmula para a esquerda, esqueci de mover a fórmula que a substitui no
Transform, então ela pulava de volta para o centro e colidia de novo. Achei os dois olhando frames extraídos, não lendo código. -
A ponta da seta descolando do marcador
Ao trocar os traços por setas, reaproveitei o mesmo objeto
Arrowpara o passo em curso e para o passo concluído, chamandoput_start_and_end_on()a cada quadro para fazer a seta crescer. O resultado é que a ponta ia ficando desalinhada do ponto de chegada: esse método reescala a cabeça em função do novo comprimento, e o erro acumula ao longo de dezenas de chamadas no mesmo objeto. A correção foi separar as responsabilidades. As setas dos passos concluídos são construídas uma vez e nunca têm a geometria tocada, só a opacidade; a seta em curso é umVMobjectà parte que fazbecome()de umaArrownova a cada quadro. Custa uma construção de objeto por quadro e elimina o acúmulo. -
Um ponto órfão no cartão final
O ponto A ficou de fora da lista do
FadeOutfinal e permaneceu aceso atrás da frase de encerramento. Só notei porque um frame do fade mostrou tudo escurecendo menos ele.
Reproduzir
A pasta manim/ do repositório deste site tem os cinco arquivos Python e um requirements.txt. O venv, o cache de render e os frames de inspeção estão no gitignore, então clonar e rodar custa uma instalação:
cd manim python -m venv .venv .venv/bin/python -m pip install -r requirements.txt .venv/bin/python sim.py # os pisos teóricos de cada perfil .venv/bin/python diagnostico.py # os números de 3.000 execuções .venv/bin/python seed_search.py # a semente e a fração de casos típicos .venv/bin/manim -qh efetividade.py Efetividade
Ressalvas
O d* é o ponto fixo da recursão do valor esperado de d², não a média da distribuição estacionária da distância. Para o agente eficiente os dois coincidiram até a terceira casa (1,085 previsto contra 1,085 medido), mas isso não é garantido em geral: quando o passo é grande em relação à distância, a dinâmica vira um vaivém em torno do alvo cuja média pode ficar acima do ponto fixo.
A distância só é conferida no fim de cada passo. Um passo longo pode passar raspando em B no meio do caminho e não contar. Isso é deliberado: a metáfora aqui é a de decisões discretas, em que o que importa é onde você para, não por onde você passou.
O erro de mira é gaussiano e independente entre passos. Erro correlacionado, que é o caso realista de um viés sistemático, só piora o quadro do agente de passo grande.
Referências
- Manim Community. Manim: a community-maintained Python library for creating mathematical animations. manim.community Fork mantido pela comunidade, versão 0.20.1 usada aqui. É o que se deve instalar via pip.
- Sanderson, G. 3Blue1Brown. 3blue1brown.com Autor original do Manim e do estilo visual que este vídeo imita.
- Robbins, H., & Monro, S. (1951). A Stochastic Approximation Method. Annals of Mathematical Statistics, 22(3), 400-407. As condições clássicas sobre o tamanho do passo. O passo adaptativo do terceiro ato é um caso particular.
- Bottou, L., Curtis, F. E., & Nocedal, J. (2018). Optimization Methods for Large-Scale Machine Learning. SIAM Review, 60(2), 223-311. Formaliza por que passo constante estaciona num raio em torno do ótimo em vez de convergir. É o mesmo fenômeno do d* deste estudo.
- Drucker, P. F. (1967). The Effective Executive. Harper & Row. Origem da distinção entre fazer as coisas certas e fazer as coisas do jeito certo.