[{"data":1,"prerenderedAt":689},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron":3,"post-pt-neural-networks-mcculloch-pitts-perceptron":4},"\u002Fen\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron",{"id":5,"title":6,"body":7,"cover":674,"date":675,"description":676,"extension":677,"meta":678,"navigation":209,"order":56,"path":679,"playlist":680,"seo":681,"status":682,"stem":683,"tags":684,"__hash__":688},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron.md","Perceptron: o Primeiro Neurônio que Aprende Sozinho",{"type":8,"value":9,"toc":665},"minimark",[10,14,19,30,38,42,90,123,148,167,181,185,305,320,363,370,374,393,410,417,437,450,465,475,479,490,501,515,519,564,568,583,613,616,658,661],[11,12,13],"p",{},"Primeira aula da playlist viva, e ela começa exatamente onde qualquer curso de redes neurais deveria começar: no neurônio mais simples que existe.",[15,16,18],"h2",{"id":17},"antes-do-código-dois-papers-e-15-anos-de-distância","Antes do código: dois papers e 15 anos de distância",[11,20,21,22,29],{},"Rede neural não nasceu como código, nasceu como pergunta de biofísica. Em 1943, Warren McCulloch e Walter Pitts publicaram ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fwww.cs.cmu.edu\u002F~epxing\u002FClass\u002F10715\u002Freading\u002FMcCulloch.and.Pitts.pdf",[27],"nofollow","\"A Logical Calculus of the Ideas Immanent in Nervous Activity\"",", propondo um modelo matemático bem enxuto pra um neurônio biológico: soma os sinais de entrada, e dispara um sinal binário (tudo ou nada) se essa soma passar de um limiar. Sem aprendizado nenhum ainda, é só um circuito lógico fixo, cada \"neurônio\" resolve uma função lógica que alguém já decidiu de antemão.",[11,31,32,33,37],{},"O salto que faltava veio 15 anos depois, com Frank Rosenblatt em 1958: e se, em vez de alguém escolher os pesos à mão, o próprio neurônio aprendesse os pesos certos olhando pra exemplos? Esse é o Perceptron, e é exatamente o que a aula 1a do professor implementa: o ",[34,35,36],"strong",{},"Perceptron Learning Algorithm"," (PLA), do jeito mais cru possível, sem bias ainda (isso fica pra próxima aula).",[15,39,41],{"id":40},"o-dataset-dois-grupos-separáveis-por-uma-reta","O dataset: dois grupos separáveis por uma reta",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-python shiki shiki-themes github-light github-dark","def createDataset(n=20):\n  X = np.random.rand(n,2)\n  coefs = np.array([1, -1])\n  labels = X @ coefs\n  y = np.array(labels>0, dtype=int)*2-1\n  return X, y\n","python","",[50,51,52,60,66,72,78,84],"code",{"__ignoreMap":48},[53,54,57],"span",{"class":55,"line":56},"line",1,[53,58,59],{},"def createDataset(n=20):\n",[53,61,63],{"class":55,"line":62},2,[53,64,65],{},"  X = np.random.rand(n,2)\n",[53,67,69],{"class":55,"line":68},3,[53,70,71],{},"  coefs = np.array([1, -1])\n",[53,73,75],{"class":55,"line":74},4,[53,76,77],{},"  labels = X @ coefs\n",[53,79,81],{"class":55,"line":80},5,[53,82,83],{},"  y = np.array(labels>0, dtype=int)*2-1\n",[53,85,87],{"class":55,"line":86},6,[53,88,89],{},"  return X, y\n",[11,91,92,93,96,97,100,101,103,104,106,107,110,111,114,115,118,119,122],{},"O professor gera pontos aleatórios em 2D e rotula cada um pelo sinal de ",[50,94,95],{},"X @ coefs",", ou seja, o produto escalar entre o ponto e o vetor ",[50,98,99],{},"[1, -1]",". Geometricamente, ",[50,102,95],{}," é positivo de um lado da reta que passa pela origem e é perpendicular a ",[50,105,99],{},", e negativo do outro lado, então o rótulo ",[50,108,109],{},"y"," (",[50,112,113],{},"-1"," ou ",[50,116,117],{},"+1",") já nasce ",[34,120,121],{},"linearmente separável por construção",": existe uma reta reta que separa as duas classes perfeitamente, porque foi exatamente essa reta que gerou os rótulos.",[43,124,126],{"className":45,"code":125,"language":47,"meta":48,"style":48},"def plotHyperplan(vector):\n  xs = np.array([0,1])\n  ys = -(vector[0]*xs)\u002Fvector[1]\n  plt.plot(xs, ys)\n",[50,127,128,133,138,143],{"__ignoreMap":48},[53,129,130],{"class":55,"line":56},[53,131,132],{},"def plotHyperplan(vector):\n",[53,134,135],{"class":55,"line":62},[53,136,137],{},"  xs = np.array([0,1])\n",[53,139,140],{"class":55,"line":68},[53,141,142],{},"  ys = -(vector[0]*xs)\u002Fvector[1]\n",[53,144,145],{"class":55,"line":74},[53,146,147],{},"  plt.plot(xs, ys)\n",[11,149,150,151,154,155,158,159,162,163,166],{},"Essa função desenha a reta onde ",[50,152,153],{},"vector[0]*x + vector[1]*y = 0",", a fronteira de decisão de um vetor de pesos ",[50,156,157],{},"w"," qualquer. É a mesma equação de sempre, ",[50,160,161],{},"w · x = 0"," define um hiperplano, só que aqui sem bias, então o hiperplano é obrigado a passar pela origem ",[50,164,165],{},"(0,0)",". Guarda esse detalhe, ele vai importar daqui a pouco.",[11,168,169,170,173,174,176,177,180],{},"O professor até testa um ",[50,171,172],{},"DummyClassifier"," com pesos fixos ",[50,175,99],{},", os mesmos que geraram o dataset, e claro que acerta 100%: é o gabarito jogado direto na resposta. A pergunta de verdade é: dá pra ",[34,178,179],{},"aprender"," esses pesos só olhando pros exemplos, sem eu entregar a resposta pronta?",[15,182,184],{"id":183},"o-algoritmo-ajustar-o-peso-na-direção-do-erro","O algoritmo: ajustar o peso na direção do erro",[43,186,188],{"className":45,"code":187,"language":47,"meta":48,"style":48},"class PLA(BaseEstimator, ClassifierMixin):\n  def __init__(self, max_iter=10):\n    self.max_iter = max_iter\n\n  def fit(self, X, y):\n    self.w_ = np.random.rand(X.shape[1])\n    for _ in range(self.max_iter):\n      cost = 0\n      idx = np.arange(X.shape[0])\n      np.random.shuffle(idx)\n      for i in idx:\n        logits = X[i] @ self.w_\n        y_pred = np.sign(logits)\n        error = y[i] - y_pred\n        if error != 0:\n          cost += error**2\n          self.w_ += error*X[i]\n        if cost == 0:\n          break\n    return self\n",[50,189,190,195,200,205,211,216,221,227,233,239,245,251,257,263,269,275,281,287,293,299],{"__ignoreMap":48},[53,191,192],{"class":55,"line":56},[53,193,194],{},"class PLA(BaseEstimator, ClassifierMixin):\n",[53,196,197],{"class":55,"line":62},[53,198,199],{},"  def __init__(self, max_iter=10):\n",[53,201,202],{"class":55,"line":68},[53,203,204],{},"    self.max_iter = max_iter\n",[53,206,207],{"class":55,"line":74},[53,208,210],{"emptyLinePlaceholder":209},true,"\n",[53,212,213],{"class":55,"line":80},[53,214,215],{},"  def fit(self, X, y):\n",[53,217,218],{"class":55,"line":86},[53,219,220],{},"    self.w_ = np.random.rand(X.shape[1])\n",[53,222,224],{"class":55,"line":223},7,[53,225,226],{},"    for _ in range(self.max_iter):\n",[53,228,230],{"class":55,"line":229},8,[53,231,232],{},"      cost = 0\n",[53,234,236],{"class":55,"line":235},9,[53,237,238],{},"      idx = np.arange(X.shape[0])\n",[53,240,242],{"class":55,"line":241},10,[53,243,244],{},"      np.random.shuffle(idx)\n",[53,246,248],{"class":55,"line":247},11,[53,249,250],{},"      for i in idx:\n",[53,252,254],{"class":55,"line":253},12,[53,255,256],{},"        logits = X[i] @ self.w_\n",[53,258,260],{"class":55,"line":259},13,[53,261,262],{},"        y_pred = np.sign(logits)\n",[53,264,266],{"class":55,"line":265},14,[53,267,268],{},"        error = y[i] - y_pred\n",[53,270,272],{"class":55,"line":271},15,[53,273,274],{},"        if error != 0:\n",[53,276,278],{"class":55,"line":277},16,[53,279,280],{},"          cost += error**2\n",[53,282,284],{"class":55,"line":283},17,[53,285,286],{},"          self.w_ += error*X[i]\n",[53,288,290],{"class":55,"line":289},18,[53,291,292],{},"        if cost == 0:\n",[53,294,296],{"class":55,"line":295},19,[53,297,298],{},"          break\n",[53,300,302],{"class":55,"line":301},20,[53,303,304],{},"    return self\n",[11,306,307,308,311,312,315,316,319],{},"O ",[50,309,310],{},"w_"," começa aleatório. Depois, ponto por ponto, em ordem embaralhada: calcula a previsão (",[50,313,314],{},"sign(w · x)","), compara com o rótulo verdadeiro, e se errou, atualiza ",[50,317,318],{},"w_ += error * x",". Essa é a regra de aprendizado inteira, e vale entender por que ela funciona, não só decorar a fórmula.",[11,321,322,323,325,326,329,330,332,333,336,337,339,340,343,344,347,348,350,351,354,355,358,359,362],{},"Pensa geometricamente: ",[50,324,157],{}," é o vetor normal ao hiperplano de decisão, ele aponta pro lado que o modelo considera \"classe +1\". Se o modelo errou um ponto que era ",[34,327,328],{},"de verdade"," classe +1 mas foi classificado como -1, isso significa que ",[50,331,157],{}," está apontando \"longe demais\" desse ponto. Somar ",[50,334,335],{},"error * x"," a ",[50,338,157],{}," (aqui ",[50,341,342],{},"error = +2",", já que ",[50,345,346],{},"y_pred"," e ",[50,349,109],{}," estão em ",[50,352,353],{},"{-1,+1}",") empurra o vetor de pesos ",[34,356,357],{},"na direção desse ponto",", deixando ",[50,360,361],{},"w · x"," um pouco mais positivo da próxima vez. O oposto acontece quando o erro é pro outro lado. É um ajuste local e barato: cada erro move a fronteira de decisão um pouquinho na direção que teria acertado aquele ponto específico, sem precisar calcular gradiente nenhum de verdade (isso é justamente o que o Aggarwal chama de \"critério do perceptron\", capítulo 1: uma regra de atualização heurística que se parece muito com gradiente descendente, mas que foi desenhada direto em cima do erro de classificação, antes de alguém formalizar qual função de custo suave ela estaria otimizando por baixo dos panos).",[11,364,365,366,369],{},"E o Rosenblatt provou, lá em 1958, algo forte: se os dados são mesmo linearmente separáveis (como são aqui, por construção), o PLA ",[34,367,368],{},"sempre converge"," pra uma solução com erro zero, em um número finito de passos. Não é \"geralmente funciona\", é uma garantia matemática.",[15,371,373],{"id":372},"um-bug-real-escondido-na-condição-de-parada","Um bug real, escondido na condição de parada",[11,375,376,377,380,381,384,385,388,389,392],{},"Só que reparar de perto no ",[50,378,379],{},"if cost == 0: break"," revela um problema. Essa checagem está ",[34,382,383],{},"dentro"," do laço que percorre os pontos, não depois dele. Isso significa: assim que ",[50,386,387],{},"cost"," (que só cresce quando há erro) estiver em zero, o laço ",[34,390,391],{},"para imediatamente",", mesmo que ainda faltem pontos pra checar naquela época.",[11,394,395,396,398,399,402,403,405,406,409],{},"O problema é que, logo no início de cada época, ",[50,397,387],{}," já começa em zero. Então se o ",[34,400,401],{},"primeiro ponto sorteado"," naquela época por acaso já está classificado certo, ",[50,404,387],{}," continua zero, e o ",[50,407,408],{},"break"," dispara ali mesmo, sem checar os outros 19 pontos. A época termina achando que \"está tudo certo\", quando na verdade só um ponto foi conferido.",[11,411,412,413,416],{},"Rodei esse código exato, byte a byte, com uma semente fixa (",[50,414,415],{},"np.random.seed(10)","), pra ver o estrago na prática:",[418,419,420],"blockquote",{},[11,421,422,425,426,428,429,432,433,436],{},[34,423,424],{},"Saída:"," contando quantos pontos (de 20) cada época realmente processou antes do ",[50,427,408],{},": ",[50,430,431],{},"[1, 1, 1, 1, 1, 1, 1, 1, 20, 20]",". Só as duas últimas épocas conferiram o dataset inteiro. Acurácia final: ",[34,434,435],{},"0.6",", bem longe da separação perfeita que o Rosenblatt garante.",[11,438,439,440,442,443,446,447,449],{},"Movendo só o ",[50,441,379],{}," pra fora do laço interno (checar zero erros ",[34,444,445],{},"depois"," de passar por todos os 20 pontos, não a cada um), a mesma semente, os mesmos dados, o mesmo ",[50,448,157],{}," inicial:",[418,451,452],{},[11,453,454,456,457,460,461,464],{},[34,455,424],{}," ",[50,458,459],{},"[20, 20]",", duas épocas completas, e pronto: convergiu com acurácia ",[34,462,463],{},"1.0",", exatamente o que o teorema promete pra dado linearmente separável.",[11,466,467,468,470,471,474],{},"Achado real, não hipotético: a intenção óbvia do código é \"para quando não tiver mais erro nesta época\", mas o jeito como o ",[50,469,408],{}," foi posicionado faz ele parar assim que ",[34,472,473],{},"um único ponto favorável"," aparece, mesmo cercado de erro em algum lugar mais à frente na fila. Com sorte (como em boa parte das sementes que testei), isso não muda o resultado final porque outras épocas compensam. Mas com essa semente específica, o algoritmo declara sucesso prematuramente, oito vezes seguidas, e nunca chega na solução perfeita que deveria.",[15,476,478],{"id":477},"interativo-treinando-o-perceptron-ponto-por-ponto","Interativo: treinando o perceptron ponto por ponto",[11,480,481,482,485,486,489],{},"Reconstruí o mesmo dataset (a semente ",[50,483,484],{},"10"," de cima, os mesmos 20 pontos) num componente que roda a versão ",[34,487,488],{},"correta"," do algoritmo, um ponto por vez. Clica em \"Processar próximo ponto\" e repara: cada vez que um ponto colorido cai do lado errado da região de fundo, isso é um erro, e o próximo clique empurra a fronteira em direção a ele.",[491,492],"perceptron-explorer",{":classes":493,":points":494,":x-max":495,":x-min":496,":y-max":495,":y-min":496,"negative-label":497,"positive-label":498,"x-label":499,"y-label":500},"[1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1]","[[0.7713, 0.0208], [0.6336, 0.7488], [0.4985, 0.2248], [0.1981, 0.7605], [0.1691, 0.0883], [0.6854, 0.9534], [0.0039, 0.5122], [0.8126, 0.6125], [0.7218, 0.2919], [0.9178, 0.7146], [0.5425, 0.1422], [0.3733, 0.6741], [0.4418, 0.434], [0.6178, 0.5131], [0.6504, 0.601], [0.8052, 0.5216], [0.9086, 0.3192], [0.0905, 0.3007], [0.114, 0.8287], [0.0469, 0.6263]]","1","0","classe -1","classe +1","x0","x1",[11,502,503,504,506,507,510,511,514],{},"Repara que, sem bias, a fronteira é sempre uma reta passando pela origem ",[50,505,165],{},", ela só pode ",[34,508,509],{},"girar",", nunca ",[34,512,513],{},"deslizar",". Nesse dataset funciona porque os dados foram desenhados em volta da origem de propósito. Mas e se a nuvem de pontos estivesse toda deslocada, longe da origem? Chego nisso na Aplicação Prática.",[15,516,518],{"id":517},"fechando","Fechando",[520,521,522,536],"table",{},[523,524,525],"thead",{},[526,527,528,533],"tr",{},[529,530,532],"th",{"align":531},"left","O que eu já sabia",[529,534,535],{"align":531},"O que essa aula assentou",[537,538,539,548,556],"tbody",{},[526,540,541,545],{},[542,543,544],"td",{"align":531},"Rede neural é sobre \"aprender pesos\"",[542,546,547],{"align":531},"O McCulloch-Pitts vem antes disso: primeiro alguém teve que propor que um neurônio pudesse ser modelado matematicamente, aprendizado veio 15 anos depois com Rosenblatt",[526,549,550,553],{},[542,551,552],{"align":531},"Atualizar peso \"na direção do erro\" é intuitivo",[542,554,555],{"align":531},"Isso tem nome (critério do perceptron) e uma garantia matemática de convergência pra dado linearmente separável",[526,557,558,561],{},[542,559,560],{"align":531},"Código do professor é a referência de verdade",[542,562,563],{"align":531},"Mesmo código de referência pode ter um bug sutil escondido numa condição de parada, e vale a pena testar em vez de confiar de olhos fechados",[15,565,567],{"id":566},"aplicação-prática","Aplicação Prática",[11,569,570,571,573,574,578,579,582],{},"Testei o mesmo PLA sem bias (código corrigido, sem o bug do ",[50,572,408],{},") num dataset real: Iris, as duas classes mais fáceis de separar (",[575,576,577],"em",{},"setosa"," vs. ",[575,580,581],{},"versicolor","), usando comprimento e largura da pétala como as duas variáveis.",[43,584,586],{"className":45,"code":585,"language":47,"meta":48,"style":48},"from sklearn.datasets import load_iris\niris = load_iris()\nmask = iris.target \u003C 2\nX = iris.data[mask][:, [2, 3]]  # comprimento e largura da pétala\ny = np.where(iris.target[mask] == 0, -1, 1)\n",[50,587,588,593,598,603,608],{"__ignoreMap":48},[53,589,590],{"class":55,"line":56},[53,591,592],{},"from sklearn.datasets import load_iris\n",[53,594,595],{"class":55,"line":62},[53,596,597],{},"iris = load_iris()\n",[53,599,600],{"class":55,"line":68},[53,601,602],{},"mask = iris.target \u003C 2\n",[53,604,605],{"class":55,"line":74},[53,606,607],{},"X = iris.data[mask][:, [2, 3]]  # comprimento e largura da pétala\n",[53,609,610],{"class":55,"line":80},[53,611,612],{},"y = np.where(iris.target[mask] == 0, -1, 1)\n",[11,614,615],{},"Essas duas classes são genuinamente linearmente separáveis (é um dos exemplos mais citados de \"separável de verdade\" em todo material introdutório de ML). Só que comprimento e largura de pétala nunca são negativos, então a nuvem de pontos inteira vive longe da origem, bem diferente do dataset sintético de cima.",[520,617,618,633],{},[523,619,620],{},[526,621,622,625,629],{},[529,623,624],{"align":531},"Versão",[529,626,628],{"align":627},"center","Convergiu em (máx. 50 épocas)",[529,630,632],{"align":631},"right","Acurácia final",[537,634,635,646],{},[526,636,637,640,643],{},[542,638,639],{"align":531},"PLA sem bias",[542,641,642],{"align":627},"nunca convergiu",[542,644,645],{"align":631},"0.84 a 0.93 (variando a semente)",[526,647,648,651,654],{},[542,649,650],{"align":531},"PLA com bias",[542,652,653],{"align":627},"2 épocas",[542,655,656],{"align":631},[34,657,463],{},[11,659,660],{},"Sem bias, o PLA nunca declara convergência dentro do limite de 50 épocas, porque a reta que separaria de verdade as duas classes não passa pela origem, e sem bias essa reta está simplesmente fora do alcance do modelo. Rodei 5 sementes diferentes e nenhuma passou de 93% de acurácia. Adicionando um único parâmetro (o bias, que desloca o hiperplano em vez de só girá-lo em torno da origem), o mesmo algoritmo converge em só 2 épocas com acurácia perfeita. É o exato limite que a próxima aula ataca de frente.",[662,663,664],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":48,"searchDepth":62,"depth":62,"links":666},[667,668,669,670,671,672,673],{"id":17,"depth":62,"text":18},{"id":40,"depth":62,"text":41},{"id":183,"depth":62,"text":184},{"id":372,"depth":62,"text":373},{"id":477,"depth":62,"text":478},{"id":517,"depth":62,"text":518},{"id":566,"depth":62,"text":567},null,"2026-08-20","Aula 1a: o professor implementa o Perceptron Learning Algorithm do zero, sem bias. Eu conto a história de dois papers que vêm antes dele e encontro um bug real escondido na condição de parada do algoritmo.","md",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron","neural-networks",{"title":6,"description":676},"published","pt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron",[685,686,687],"perceptron","mcculloch-pitts","rosenblatt","iUSXZah74pR9ZYkVjOWTICZgEnqAVF1Tf6zU8epgbV0",1787338982218]