[{"data":1,"prerenderedAt":722},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta":3,"post-pt-neural-networks-adaline-regra-delta":4},"\u002Fen\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta",{"id":5,"title":6,"body":7,"cover":707,"date":708,"description":709,"extension":710,"meta":711,"navigation":79,"order":64,"path":712,"playlist":713,"seo":714,"status":715,"stem":716,"tags":717,"__hash__":721},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta.md","Adaline: Treinar na Reta, Classificar no Sinal",{"type":8,"value":9,"toc":699},"minimark",[10,27,32,98,132,148,155,213,237,251,283,287,301,312,315,319,366,386,402,406,441,461,475,482,526,538,542,587,591,603,618,641,684,695],[11,12,13,14,18,19,22,23,26],"p",{},"Aula 2e e 2f, e o nome do modelo muda pra ",[15,16,17],"strong",{},"Adaline"," (ADAptive LInear NEuron), de Bernard Widrow e Ted Hoff, 1960, só dois anos depois do Rosenblatt. A ideia central deles é sutil, mas separa duas coisas que até aqui eu tratava como uma só: ",[15,20,21],{},"o que o modelo otimiza durante o treino"," e ",[15,24,25],{},"o que ele calcula na hora de prever",".",[28,29,31],"h2",{"id":30},"pré-ativação-e-pós-ativação-a-distinção-que-o-adaline-introduz","Pré-ativação e pós-ativação: a distinção que o Adaline introduz",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"language-python shiki shiki-themes github-light github-dark","class AdalinePseudoInverse(BaseEstimator, ClassifierMixin):\n  def fit(self, X, y):\n    X = include_bias(X)\n    self.w_ = np.linalg.pinv(X) @ y\n    return self\n\n  def predict(self, X):\n    X = include_bias(X)\n    return X @ self.w_\n","python","",[40,41,42,50,56,62,68,74,81,87,92],"code",{"__ignoreMap":38},[43,44,47],"span",{"class":45,"line":46},"line",1,[43,48,49],{},"class AdalinePseudoInverse(BaseEstimator, ClassifierMixin):\n",[43,51,53],{"class":45,"line":52},2,[43,54,55],{},"  def fit(self, X, y):\n",[43,57,59],{"class":45,"line":58},3,[43,60,61],{},"    X = include_bias(X)\n",[43,63,65],{"class":45,"line":64},4,[43,66,67],{},"    self.w_ = np.linalg.pinv(X) @ y\n",[43,69,71],{"class":45,"line":70},5,[43,72,73],{},"    return self\n",[43,75,77],{"class":45,"line":76},6,[43,78,80],{"emptyLinePlaceholder":79},true,"\n",[43,82,84],{"class":45,"line":83},7,[43,85,86],{},"  def predict(self, X):\n",[43,88,90],{"class":45,"line":89},8,[43,91,61],{},[43,93,95],{"class":45,"line":94},9,[43,96,97],{},"    return X @ self.w_\n",[11,99,100,101,104,105,110,111,114,115,118,119,122,123,125,126,128,129,26],{},"Repara: isso é ",[15,102,103],{},"exatamente"," ",[106,107,109],"a",{"href":108},"\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal","a equação normal do post passado",", sem mudar uma linha, só que agora ",[40,112,113],{},"y"," é ",[40,116,117],{},"-1"," ou ",[40,120,121],{},"+1"," em vez de contínuo. O professor está tratando classificação como se fosse regressão: ajustando a reta pra chegar o mais perto possível de ",[40,124,117],{}," nos pontos de uma classe e ",[40,127,121],{}," na outra, sem nunca aplicar ",[40,130,131],{},"sign()",[133,134,135],"blockquote",{},[11,136,137,140,141,144,145,26],{},[15,138,139],{},"Saída:"," RMSE ",[40,142,143],{},"0.4148",", pesos ",[40,146,147],{},"[-1.76, 1.01, 2.74]",[11,149,150,151,154],{},"RMSE faz sentido aqui porque ",[40,152,153],{},"predict"," devolve um número contínuo, não uma classe. Só que pra classificar de verdade, falta o último passo:",[33,156,158],{"className":35,"code":157,"language":37,"meta":38,"style":38},"class AdalinePseudoInverse(BaseEstimator, ClassifierMixin):\n  def fit(self, X, y):\n    X = include_bias(X)\n    self.w_ = np.linalg.pinv(X) @ y\n    return self\n\n  def pre_activation(self, X):\n    X = include_bias(X)\n    return X @ self.w_\n\n  def predict(self, X):\n    return np.sign(self.pre_activation(X))\n",[40,159,160,164,168,172,176,180,184,189,193,197,202,207],{"__ignoreMap":38},[43,161,162],{"class":45,"line":46},[43,163,49],{},[43,165,166],{"class":45,"line":52},[43,167,55],{},[43,169,170],{"class":45,"line":58},[43,171,61],{},[43,173,174],{"class":45,"line":64},[43,175,67],{},[43,177,178],{"class":45,"line":70},[43,179,73],{},[43,181,182],{"class":45,"line":76},[43,183,80],{"emptyLinePlaceholder":79},[43,185,186],{"class":45,"line":83},[43,187,188],{},"  def pre_activation(self, X):\n",[43,190,191],{"class":45,"line":89},[43,192,61],{},[43,194,195],{"class":45,"line":94},[43,196,97],{},[43,198,200],{"class":45,"line":199},10,[43,201,80],{"emptyLinePlaceholder":79},[43,203,205],{"class":45,"line":204},11,[43,206,86],{},[43,208,210],{"class":45,"line":209},12,[43,211,212],{},"    return np.sign(self.pre_activation(X))\n",[11,214,215,216,219,220,222,223,225,226,22,229,232,233,236],{},"Agora tem dois métodos: ",[40,217,218],{},"pre_activation"," (o valor contínuo, antes de qualquer limiar) e ",[40,221,153],{}," (aplica ",[40,224,131],{}," em cima). O Aggarwal chama exatamente essas duas coisas de ",[15,227,228],{},"valor de pré-ativação",[15,230,231],{},"valor de pós-ativação"," (capítulo 1): tudo que um neurônio calcula acontece em duas etapas, primeiro a soma ponderada, depois a função de ativação em cima dela. O Adaline treina em cima da pré-ativação (ela é contínua, então dá pra medir \"quão longe\" cada previsão ficou do alvo) e só aplica a ativação (",[40,234,235],{},"sign",") na hora de decidir a classe final.",[133,238,239],{},[11,240,241,243,244,247,248,250],{},[15,242,139],{}," acurácia ",[40,245,246],{},"1.0",", os mesmos pesos ",[40,249,147],{}," de antes (é a mesma conta, só que agora avaliada por acurácia em vez de RMSE).",[11,252,253,254,257,258,262,263,267,268,271,272,274,275,278,279,282],{},"Isso é a ",[15,255,256],{},"regra delta",", e ",[106,259,261],{"href":260},"\u002Fplaylists\u002Fpattern-recognition\u002Flinear-regression-estimator","eu já vi ela antes, com esse nome exato",": \"atualizar o peso proporcionalmente ao erro vezes a entrada\" é a assinatura do Widrow-Hoff. A diferença pro perceptron do Rosenblatt (que eu vi ",[106,264,266],{"href":265},"\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron","duas aulas atrás",") é justamente essa: o perceptron mede o erro ",[15,269,270],{},"depois"," de aplicar ",[40,273,131],{}," (erro em ",[40,276,277],{},"{-2,0,+2}","), o Adaline mede o erro ",[15,280,281],{},"antes",", na pré-ativação contínua. Isso parece um detalhe pequeno, mas muda tudo: erro contínuo dá um gradiente de verdade, suave, que aponta pra melhor direção mesmo quando a previsão já está do lado certo mas ainda meio \"insegura\". Já o erro binário do perceptron só liga quando classifica errado, sem noção de \"quão errado\".",[28,284,286],{"id":285},"interativo-mexendo-na-pré-ativação-e-vendo-o-rmse-e-a-acurácia-mudarem","Interativo: mexendo na pré-ativação e vendo o RMSE (e a acurácia) mudarem",[11,288,289,290,293,294,22,297,300],{},"Em vez de treinar automaticamente, arrasta os sliders de ",[40,291,292],{},"w0",", ",[40,295,296],{},"w1",[40,298,299],{},"bias"," na mão e observa duas leituras ao mesmo tempo: o RMSE (contínuo, muda suavemente a cada arrastão) e a acurácia (discreta, só pula quando um ponto atravessa a fronteira de decisão).",[302,303],"adaline-explorer",{":classes":304,":points":305,":x-max":306,":x-min":307,":y-max":306,":y-min":307,"negative-label":308,"positive-label":309,"x-label":310,"y-label":311},"[-1, 1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1]","[[0.0856, 0.2368], [0.8013, 0.5822], [0.0941, 0.4331], [0.4791, 0.1597], [0.7346, 0.1137], [0.3912, 0.5167], [0.4306, 0.5868], [0.7378, 0.9563], [0.2842, 0.6485], [0.6962, 0.2927], [0.0015, 0.9735], [0.2984, 0.314], [0.8917, 0.5852], [0.4713, 0.7733], [0.0303, 0.707], [0.3742, 0.0909], [0.6605, 0.9315], [0.2072, 0.6301], [0.2982, 0.7418], [0.7222, 0.2187]]","1","0","classe -1","classe +1","x0","x1",[11,313,314],{},"Repara que o RMSE quase sempre continua mudando um pouquinho mesmo depois que a acurácia já bateu 100%: dá pra empurrar a fronteira mais fundo pro meio do vazio entre as duas classes (RMSE cai mais) sem ganhar nem perder nenhum ponto (acurácia parada). É exatamente essa diferença que separa \"achar uma reta que separa\" (o que o perceptron faz) de \"achar a reta que separa com folga\" (o que dá pra melhorar otimizando RMSE em vez de só contar erro).",[28,316,318],{"id":317},"aula-2f-a-mesma-conta-só-que-iterando-e-o-notebook-chama-isso-de-sgd","Aula 2f: a mesma conta, só que iterando (e o notebook chama isso de \"SGD\")",[33,320,322],{"className":35,"code":321,"language":37,"meta":38,"style":38},"class Adaline(BaseEstimator, ClassifierMixin):\n  def fit(self, X, y):\n    X = include_bias(X)\n    self.w_ = np.zeros(X.shape[1])\n    for _ in range(self.max_iter):\n      y_pred = X @ self.w_\n      error = y - y_pred\n      self.w_ += self.learning_rate * error @ X\n    return self\n",[40,323,324,329,333,337,342,347,352,357,362],{"__ignoreMap":38},[43,325,326],{"class":45,"line":46},[43,327,328],{},"class Adaline(BaseEstimator, ClassifierMixin):\n",[43,330,331],{"class":45,"line":52},[43,332,55],{},[43,334,335],{"class":45,"line":58},[43,336,61],{},[43,338,339],{"class":45,"line":64},[43,340,341],{},"    self.w_ = np.zeros(X.shape[1])\n",[43,343,344],{"class":45,"line":70},[43,345,346],{},"    for _ in range(self.max_iter):\n",[43,348,349],{"class":45,"line":76},[43,350,351],{},"      y_pred = X @ self.w_\n",[43,353,354],{"class":45,"line":83},[43,355,356],{},"      error = y - y_pred\n",[43,358,359],{"class":45,"line":89},[43,360,361],{},"      self.w_ += self.learning_rate * error @ X\n",[43,363,364],{"class":45,"line":94},[43,365,73],{},[11,367,368,369,372,373,376,377,385],{},"Um detalhe honesto sobre o nome do notebook (",[40,370,371],{},"aula02f adaline with SGD","): o código mostrado aqui é ",[15,374,375],{},"gradiente em lote"," (",[106,378,380,381,384],{"href":379},"\u002Fplaylists\u002Fneural-networks\u002Fperceptron-com-bias","o mesmo ",[40,382,383],{},"X.T @ error"," de sempre","), calculando o erro em cima do dataset inteiro a cada iteração, não SGD de verdade (que atualizaria um exemplo por vez, em ordem embaralhada). É um jeito comum de falar informalmente (\"é tipo gradiente descendente, então chamo de SGD\"), mas vale registrar a diferença técnica, já que os nomes têm significado preciso.",[133,387,388],{},[11,389,390,243,392,394,395,398,399,26],{},[15,391,139],{},[40,393,246],{}," no treino, pesos ",[40,396,397],{},"[-2.92, 3.32, 2.63]",". Testando em 1000 pontos novos: acurácia ",[15,400,401],{},"0.953",[28,403,405],{"id":404},"o-dataset-ruim-o-que-ele-realmente-prova","O dataset \"ruim\": o que ele realmente prova",[33,407,409],{"className":35,"code":408,"language":37,"meta":38,"style":38},"X_bad = np.concatenate((X,np.ones_like(X)))\ny_bad = np.concatenate((y,np.ones_like(y)))\nX_bad = np.concatenate((X_bad,np.ones_like(X)))\ny_bad = np.concatenate((y_bad,np.ones_like(y)))\nclf_bad = Adaline()\nclf_bad.fit(X_bad, y_bad)\n",[40,410,411,416,421,426,431,436],{"__ignoreMap":38},[43,412,413],{"class":45,"line":46},[43,414,415],{},"X_bad = np.concatenate((X,np.ones_like(X)))\n",[43,417,418],{"class":45,"line":52},[43,419,420],{},"y_bad = np.concatenate((y,np.ones_like(y)))\n",[43,422,423],{"class":45,"line":58},[43,424,425],{},"X_bad = np.concatenate((X_bad,np.ones_like(X)))\n",[43,427,428],{"class":45,"line":64},[43,429,430],{},"y_bad = np.concatenate((y_bad,np.ones_like(y)))\n",[43,432,433],{"class":45,"line":70},[43,434,435],{},"clf_bad = Adaline()\n",[43,437,438],{"class":45,"line":76},[43,439,440],{},"clf_bad.fit(X_bad, y_bad)\n",[11,442,443,444,447,448,451,452,454,455,457,458,460],{},"O professor concatena o dataset original com ",[15,445,446],{},"dois blocos extras"," de pontos artificiais: todo mundo em ",[40,449,450],{},"(1,1)",", todo mundo rotulado ",[40,453,121],{},". Isso não é ruído, é um enviesamento deliberado, empurrando o treino a \"acreditar\" que a região perto de ",[40,456,450],{}," é ainda mais fortemente classe ",[40,459,121],{}," do que ela realmente é.",[133,462,463],{},[11,464,465,243,467,470,471,474],{},[15,466,139],{},[40,468,469],{},"0.967"," no treino (sobre o dataset enviesado), mas só ",[15,472,473],{},"0.811"," nos mesmos 1000 pontos de teste limpos de antes.",[11,476,477,478,481],{},"Caiu de 0.953 pra 0.811. Antes de escrever esse post, eu tinha a hipótese de que essa célula ia mostrar a equação normal (pseudo-inversa) quebrando nesse dataset problemático, e o SGD aguentando melhor. Reproduzi o experimento eu mesmo, comparando pseudo-inversa contra o gradiente em lote ",[15,479,480],{},"no mesmo dataset enviesado",", e a hipótese não se sustentou:",[483,484,485,503],"table",{},[486,487,488],"thead",{},[489,490,491,496,500],"tr",{},[492,493,495],"th",{"align":494},"left","Método",[492,497,499],{"align":498},"right","Acurácia treino (enviesado)",[492,501,502],{"align":498},"Acurácia teste (limpo)",[504,505,506,517],"tbody",{},[489,507,508,512,514],{},[509,510,511],"td",{"align":494},"Pseudo-inversa",[509,513,469],{"align":498},[509,515,516],{"align":498},"0.783",[489,518,519,522,524],{},[509,520,521],{"align":494},"Gradiente em lote",[509,523,469],{"align":498},[509,525,516],{"align":498},[11,527,528,529,532,533,537],{},"Os dois caem exatamente igual, com pesos praticamente idênticos entre si. A lição de verdade dessa célula não é sobre qual algoritmo de treino é mais robusto, é sobre ",[15,530,531],{},"qualidade do dado de treino",": enviesar a distribuição do treino (mesmo sem adicionar \"ruído\" no sentido de erro aleatório) desloca a fronteira aprendida pra um lugar que não representa mais a distribuição real, e isso prejudica os dois métodos igualmente, porque os dois estão resolvendo o mesmíssimo problema de otimização por baixo dos panos. ",[106,534,536],{"href":535},"\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud","Isso ecoa uma lição que já vi na outra playlist",": treino com distribuição diferente do mundo real é um problema de dado, não de algoritmo.",[28,539,541],{"id":540},"fechando","Fechando",[483,543,544,554],{},[486,545,546],{},[489,547,548,551],{},[492,549,550],{"align":494},"O que eu já sabia",[492,552,553],{"align":494},"O que essa aula assentou",[504,555,556,567,579],{},[489,557,558,561],{},[509,559,560],{"align":494},"Perceptron classifica e atualiza pelo erro binário",[509,562,563,564,566],{"align":494},"Adaline separa pré-ativação (contínua, usada no treino) de pós-ativação (",[40,565,235],{},", usada só na hora de decidir a classe)",[489,568,569,572],{},[509,570,571],{"align":494},"Regra delta já apareceu na outra playlist",[509,573,574,575,578],{"align":494},"Adaline ",[15,576,577],{},"é"," a regra delta aplicada à classificação, treinando como se fosse regressão nos rótulos ±1",[489,580,581,584],{},[509,582,583],{"align":494},"Achar uma reta que separa parece suficiente",[509,585,586],{"align":494},"Otimizar RMSE (não só contar erro) continua achando fronteira melhor mesmo depois da acurácia bater 100%",[28,588,590],{"id":589},"aplicação-prática","Aplicação Prática",[11,592,593,594,598,599,602],{},"Reproduzi pseudo-inversa vs. gradiente em lote no Iris (",[595,596,597],"em",{},"setosa"," vs. ",[595,600,601],{},"versicolor","), a essa altura já um velho conhecido dessa playlist, mas dessa vez treinando como Adaline, no alvo contínuo ±1, não como perceptron.",[33,604,606],{"className":35,"code":605,"language":37,"meta":38,"style":38},"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)\nw_pinv = np.linalg.pinv(include_bias(X_train)) @ y_train\n",[40,607,608,613],{"__ignoreMap":38},[43,609,610],{"class":45,"line":46},[43,611,612],{},"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)\n",[43,614,615],{"class":45,"line":52},[43,616,617],{},"w_pinv = np.linalg.pinv(include_bias(X_train)) @ y_train\n",[11,619,620,621,624,625,628,629,632,633,636,637,640],{},"Na primeira tentativa, usei o mesmo ",[40,622,623],{},"learning_rate=0.01"," do notebook original pro gradiente em lote, e ele ",[15,626,627],{},"divergiu"," (peso virou ",[40,630,631],{},"NaN",") direto no dado cru do Iris. Nada de surpresa nessa altura: ",[106,634,635],{"href":108},"é a mesma lição de escala do post da equação normal",", só reencontrada de novo, dessa vez precisando de uma taxa bem menor (",[40,638,639],{},"0.001",") pra não explodir.",[483,642,643,655],{},[486,644,645],{},[489,646,647,649,652],{},[492,648,495],{"align":494},[492,650,651],{"align":498},"RMSE (treino)",[492,653,654],{"align":498},"Acurácia (teste)",[504,656,657,669],{},[489,658,659,662,665],{},[509,660,661],{"align":494},"Pseudo-inversa (Adaline)",[509,663,664],{"align":498},"0.2414",[509,666,667],{"align":498},[15,668,246],{},[489,670,671,678,680],{},[509,672,673,674,677],{"align":494},"Gradiente em lote, ",[40,675,676],{},"learning_rate=0.001"," (Adaline)",[509,679,664],{"align":498},[509,681,682],{"align":498},[15,683,246],{},[11,685,686,687,690,691,694],{},"Com a taxa ajustada, mesmo resultado dos dois métodos de novo, RMSE idêntico até a quarta casa decimal, e as duas acurácias batendo 100%, ",[106,688,689],{"href":379},"a mesma margem generosa do Iris que já favoreceu o perceptron com bias",". A diferença real entre os métodos, nesse dataset fácil, continua sendo só velocidade de convergência (e sensibilidade à escolha de ",[40,692,693],{},"learning_rate","), não qualidade da solução final.",[696,697,698],"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":38,"searchDepth":52,"depth":52,"links":700},[701,702,703,704,705,706],{"id":30,"depth":52,"text":31},{"id":285,"depth":52,"text":286},{"id":317,"depth":52,"text":318},{"id":404,"depth":52,"text":405},{"id":540,"depth":52,"text":541},{"id":589,"depth":52,"text":590},null,"2026-08-20","Aula 2e e 2f: o professor treina o Adaline ajustando uma reta contínua nos rótulos ±1 (em vez de ajustar direto no sinal), e eu conecto isso com a regra delta que já apareceu na outra playlist. No fim, um dataset artificialmente enviesado revela que o problema nunca foi o algoritmo de treino.","md",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta","neural-networks",{"title":6,"description":709},"published","pt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta",[718,719,720],"adaline","regra-delta","widrow-hoff","cbghfvPwR6a9QLRUcV5MuER7WPdR2fYLxW2Wn5ttVUs",1787338982218]