[{"data":1,"prerenderedAt":5022},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fneural-networks":3,"playlist-pt-neural-networks":4,"playlist-posts-pt-neural-networks":89},null,{"id":5,"title":6,"body":7,"cover":3,"description":79,"extension":80,"meta":81,"navigation":82,"order":83,"path":84,"seo":85,"status":86,"stem":87,"__hash__":88},"playlists\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Findex.md","Redes Neurais",{"type":8,"value":9,"toc":75},"minimark",[10,25,43,59],[11,12,13,14,18,19,24],"p",{},"Segunda playlist com o ",[15,16,17],"strong",{},"Dr. Francisco Boldt"," (a primeira foi ",[20,21,23],"a",{"href":22},"\u002Fplaylists\u002Fpattern-recognition","Reconhecimento de Padrões","), agora na disciplina de Redes Neurais. Mesmo estilo de sempre: código na mão, ao vivo, na aula, sem slide de fórmula pronta. Se você já leu a playlist anterior, sabe exatamente o que esperar.",[11,26,27,28,32,33,37,38,42],{},"O livro de referência muda: aqui eu uso ",[29,30,31],"em",{},"Neural Networks and Deep Learning: A Textbook",", do Charu Aggarwal (2018), pra fazer o papel que o Bishop fazia na playlist passada, encher de fundamento o que o notebook só mostra em código. Boa parte do assunto também conecta direto com o que eu já vi antes: ",[20,34,36],{"href":35},"\u002Fplaylists\u002Fpattern-recognition\u002Flinear-regression-estimator","regra delta e regressão linear já apareceram lá no Reconhecimento de Padrões",", e ",[20,39,41],{"href":40},"\u002Fplaylists\u002Fmachine-learning-specialization\u002Floss-functions","funções de custo já apareceram na especialização do Andrew Ng",", então sempre que der eu vou puxar esses fios em vez de reensinar do zero.",[11,44,45,46,52,53,58],{},"Os notebooks vêm do repositório da matéria, ",[20,47,51],{"href":48,"rel":49},"https:\u002F\u002Fgithub.com\u002Fpablobelmiro\u002Faulasann",[50],"nofollow","pablobelmiro\u002Faulasann",", um fork do repositório original do próprio Dr. Boldt, ",[20,54,57],{"href":55,"rel":56},"https:\u002F\u002Fgithub.com\u002Ffboldt\u002Faulasann",[50],"fboldt\u002Faulasann",", onde ele publica o código de cada aula. Mesma convenção da playlist anterior: quem \"faz\" o código é sempre ele, o professor; a explicação de fundamento é minha.",[11,60,61,62,65,66,69,70,74],{},"Um detalhe importante dessa vez: essa disciplina está sendo dada ",[15,63,64],{},"agora",", ao vivo, e o repositório só tem o começo do curso até este momento (perceptron, Adaline, funções de custo, e um gancho bem no limite do que um único neurônio consegue resolver). Diferente da playlist de Reconhecimento de Padrões, que eu comecei só depois que a matéria inteira já tinha acabado, essa aqui é uma ",[15,67,68],{},"playlist viva",": cresce toda vez que o professor publica uma aula nova, e eu volto pra continuar. Começamos pelo começo de tudo: ",[20,71,73],{"href":72},"\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron","o neurônio mais simples que existe",".",{"title":76,"searchDepth":77,"depth":77,"links":78},"",2,[],"Minhas anotações da disciplina de Redes Neurais, aula por aula, com o livro do Aggarwal como base teórica. Playlist viva, crescendo junto com o curso.","md",{},true,3,"\u002Fpt\u002Fplaylists\u002Fneural-networks",{"title":6,"description":79},"published","pt\u002Fplaylists\u002Fneural-networks\u002Findex","E7qSDm0PDSKcEZo03_C5IczMHfp2NxtLtMpDImcPtBc",[90,762,1474,2148,2775,4128,4647],{"id":91,"title":92,"body":93,"cover":3,"date":750,"description":751,"extension":80,"meta":752,"navigation":82,"order":136,"path":753,"playlist":754,"seo":755,"status":86,"stem":756,"tags":757,"__hash__":761},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron.md","Perceptron: o Primeiro Neurônio que Aprende Sozinho",{"type":8,"value":94,"toc":741},[95,98,103,112,119,123,168,201,226,245,259,263,382,397,440,447,451,470,487,494,514,527,542,552,556,567,578,592,596,641,645,659,689,692,734,737],[11,96,97],{},"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.",[99,100,102],"h2",{"id":101},"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,104,105,106,111],{},"Rede neural não nasceu como código, nasceu como pergunta de biofísica. Em 1943, Warren McCulloch e Walter Pitts publicaram ",[20,107,110],{"href":108,"rel":109},"https:\u002F\u002Fwww.cs.cmu.edu\u002F~epxing\u002FClass\u002F10715\u002Freading\u002FMcCulloch.and.Pitts.pdf",[50],"\"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,113,114,115,118],{},"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 ",[15,116,117],{},"Perceptron Learning Algorithm"," (PLA), do jeito mais cru possível, sem bias ainda (isso fica pra próxima aula).",[99,120,122],{"id":121},"o-dataset-dois-grupos-separáveis-por-uma-reta","O dataset: dois grupos separáveis por uma reta",[124,125,129],"pre",{"className":126,"code":127,"language":128,"meta":76,"style":76},"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",[130,131,132,140,145,150,156,162],"code",{"__ignoreMap":76},[133,134,137],"span",{"class":135,"line":136},"line",1,[133,138,139],{},"def createDataset(n=20):\n",[133,141,142],{"class":135,"line":77},[133,143,144],{},"  X = np.random.rand(n,2)\n",[133,146,147],{"class":135,"line":83},[133,148,149],{},"  coefs = np.array([1, -1])\n",[133,151,153],{"class":135,"line":152},4,[133,154,155],{},"  labels = X @ coefs\n",[133,157,159],{"class":135,"line":158},5,[133,160,161],{},"  y = np.array(labels>0, dtype=int)*2-1\n",[133,163,165],{"class":135,"line":164},6,[133,166,167],{},"  return X, y\n",[11,169,170,171,174,175,178,179,181,182,184,185,188,189,192,193,196,197,200],{},"O professor gera pontos aleatórios em 2D e rotula cada um pelo sinal de ",[130,172,173],{},"X @ coefs",", ou seja, o produto escalar entre o ponto e o vetor ",[130,176,177],{},"[1, -1]",". Geometricamente, ",[130,180,173],{}," é positivo de um lado da reta que passa pela origem e é perpendicular a ",[130,183,177],{},", e negativo do outro lado, então o rótulo ",[130,186,187],{},"y"," (",[130,190,191],{},"-1"," ou ",[130,194,195],{},"+1",") já nasce ",[15,198,199],{},"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.",[124,202,204],{"className":126,"code":203,"language":128,"meta":76,"style":76},"def plotHyperplan(vector):\n  xs = np.array([0,1])\n  ys = -(vector[0]*xs)\u002Fvector[1]\n  plt.plot(xs, ys)\n",[130,205,206,211,216,221],{"__ignoreMap":76},[133,207,208],{"class":135,"line":136},[133,209,210],{},"def plotHyperplan(vector):\n",[133,212,213],{"class":135,"line":77},[133,214,215],{},"  xs = np.array([0,1])\n",[133,217,218],{"class":135,"line":83},[133,219,220],{},"  ys = -(vector[0]*xs)\u002Fvector[1]\n",[133,222,223],{"class":135,"line":152},[133,224,225],{},"  plt.plot(xs, ys)\n",[11,227,228,229,232,233,236,237,240,241,244],{},"Essa função desenha a reta onde ",[130,230,231],{},"vector[0]*x + vector[1]*y = 0",", a fronteira de decisão de um vetor de pesos ",[130,234,235],{},"w"," qualquer. É a mesma equação de sempre, ",[130,238,239],{},"w · x = 0"," define um hiperplano, só que aqui sem bias, então o hiperplano é obrigado a passar pela origem ",[130,242,243],{},"(0,0)",". Guarda esse detalhe, ele vai importar daqui a pouco.",[11,246,247,248,251,252,254,255,258],{},"O professor até testa um ",[130,249,250],{},"DummyClassifier"," com pesos fixos ",[130,253,177],{},", os mesmos que geraram o dataset, e claro que acerta 100%: é o gabarito jogado direto na resposta. A pergunta de verdade é: dá pra ",[15,256,257],{},"aprender"," esses pesos só olhando pros exemplos, sem eu entregar a resposta pronta?",[99,260,262],{"id":261},"o-algoritmo-ajustar-o-peso-na-direção-do-erro","O algoritmo: ajustar o peso na direção do erro",[124,264,266],{"className":126,"code":265,"language":128,"meta":76,"style":76},"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",[130,267,268,273,278,283,288,293,298,304,310,316,322,328,334,340,346,352,358,364,370,376],{"__ignoreMap":76},[133,269,270],{"class":135,"line":136},[133,271,272],{},"class PLA(BaseEstimator, ClassifierMixin):\n",[133,274,275],{"class":135,"line":77},[133,276,277],{},"  def __init__(self, max_iter=10):\n",[133,279,280],{"class":135,"line":83},[133,281,282],{},"    self.max_iter = max_iter\n",[133,284,285],{"class":135,"line":152},[133,286,287],{"emptyLinePlaceholder":82},"\n",[133,289,290],{"class":135,"line":158},[133,291,292],{},"  def fit(self, X, y):\n",[133,294,295],{"class":135,"line":164},[133,296,297],{},"    self.w_ = np.random.rand(X.shape[1])\n",[133,299,301],{"class":135,"line":300},7,[133,302,303],{},"    for _ in range(self.max_iter):\n",[133,305,307],{"class":135,"line":306},8,[133,308,309],{},"      cost = 0\n",[133,311,313],{"class":135,"line":312},9,[133,314,315],{},"      idx = np.arange(X.shape[0])\n",[133,317,319],{"class":135,"line":318},10,[133,320,321],{},"      np.random.shuffle(idx)\n",[133,323,325],{"class":135,"line":324},11,[133,326,327],{},"      for i in idx:\n",[133,329,331],{"class":135,"line":330},12,[133,332,333],{},"        logits = X[i] @ self.w_\n",[133,335,337],{"class":135,"line":336},13,[133,338,339],{},"        y_pred = np.sign(logits)\n",[133,341,343],{"class":135,"line":342},14,[133,344,345],{},"        error = y[i] - y_pred\n",[133,347,349],{"class":135,"line":348},15,[133,350,351],{},"        if error != 0:\n",[133,353,355],{"class":135,"line":354},16,[133,356,357],{},"          cost += error**2\n",[133,359,361],{"class":135,"line":360},17,[133,362,363],{},"          self.w_ += error*X[i]\n",[133,365,367],{"class":135,"line":366},18,[133,368,369],{},"        if cost == 0:\n",[133,371,373],{"class":135,"line":372},19,[133,374,375],{},"          break\n",[133,377,379],{"class":135,"line":378},20,[133,380,381],{},"    return self\n",[11,383,384,385,388,389,392,393,396],{},"O ",[130,386,387],{},"w_"," começa aleatório. Depois, ponto por ponto, em ordem embaralhada: calcula a previsão (",[130,390,391],{},"sign(w · x)","), compara com o rótulo verdadeiro, e se errou, atualiza ",[130,394,395],{},"w_ += error * x",". Essa é a regra de aprendizado inteira, e vale entender por que ela funciona, não só decorar a fórmula.",[11,398,399,400,402,403,406,407,409,410,413,414,416,417,420,421,424,425,427,428,431,432,435,436,439],{},"Pensa geometricamente: ",[130,401,235],{}," é 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 ",[15,404,405],{},"de verdade"," classe +1 mas foi classificado como -1, isso significa que ",[130,408,235],{}," está apontando \"longe demais\" desse ponto. Somar ",[130,411,412],{},"error * x"," a ",[130,415,235],{}," (aqui ",[130,418,419],{},"error = +2",", já que ",[130,422,423],{},"y_pred"," e ",[130,426,187],{}," estão em ",[130,429,430],{},"{-1,+1}",") empurra o vetor de pesos ",[15,433,434],{},"na direção desse ponto",", deixando ",[130,437,438],{},"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,441,442,443,446],{},"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 ",[15,444,445],{},"sempre converge"," pra uma solução com erro zero, em um número finito de passos. Não é \"geralmente funciona\", é uma garantia matemática.",[99,448,450],{"id":449},"um-bug-real-escondido-na-condição-de-parada","Um bug real, escondido na condição de parada",[11,452,453,454,457,458,461,462,465,466,469],{},"Só que reparar de perto no ",[130,455,456],{},"if cost == 0: break"," revela um problema. Essa checagem está ",[15,459,460],{},"dentro"," do laço que percorre os pontos, não depois dele. Isso significa: assim que ",[130,463,464],{},"cost"," (que só cresce quando há erro) estiver em zero, o laço ",[15,467,468],{},"para imediatamente",", mesmo que ainda faltem pontos pra checar naquela época.",[11,471,472,473,475,476,479,480,482,483,486],{},"O problema é que, logo no início de cada época, ",[130,474,464],{}," já começa em zero. Então se o ",[15,477,478],{},"primeiro ponto sorteado"," naquela época por acaso já está classificado certo, ",[130,481,464],{}," continua zero, e o ",[130,484,485],{},"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,488,489,490,493],{},"Rodei esse código exato, byte a byte, com uma semente fixa (",[130,491,492],{},"np.random.seed(10)","), pra ver o estrago na prática:",[495,496,497],"blockquote",{},[11,498,499,502,503,505,506,509,510,513],{},[15,500,501],{},"Saída:"," contando quantos pontos (de 20) cada época realmente processou antes do ",[130,504,485],{},": ",[130,507,508],{},"[1, 1, 1, 1, 1, 1, 1, 1, 20, 20]",". Só as duas últimas épocas conferiram o dataset inteiro. Acurácia final: ",[15,511,512],{},"0.6",", bem longe da separação perfeita que o Rosenblatt garante.",[11,515,516,517,519,520,523,524,526],{},"Movendo só o ",[130,518,456],{}," pra fora do laço interno (checar zero erros ",[15,521,522],{},"depois"," de passar por todos os 20 pontos, não a cada um), a mesma semente, os mesmos dados, o mesmo ",[130,525,235],{}," inicial:",[495,528,529],{},[11,530,531,533,534,537,538,541],{},[15,532,501],{}," ",[130,535,536],{},"[20, 20]",", duas épocas completas, e pronto: convergiu com acurácia ",[15,539,540],{},"1.0",", exatamente o que o teorema promete pra dado linearmente separável.",[11,543,544,545,547,548,551],{},"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 ",[130,546,485],{}," foi posicionado faz ele parar assim que ",[15,549,550],{},"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.",[99,553,555],{"id":554},"interativo-treinando-o-perceptron-ponto-por-ponto","Interativo: treinando o perceptron ponto por ponto",[11,557,558,559,562,563,566],{},"Reconstruí o mesmo dataset (a semente ",[130,560,561],{},"10"," de cima, os mesmos 20 pontos) num componente que roda a versão ",[15,564,565],{},"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.",[568,569],"perceptron-explorer",{":classes":570,":points":571,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"negative-label":574,"positive-label":575,"x-label":576,"y-label":577},"[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,579,580,581,583,584,587,588,591],{},"Repara que, sem bias, a fronteira é sempre uma reta passando pela origem ",[130,582,243],{},", ela só pode ",[15,585,586],{},"girar",", nunca ",[15,589,590],{},"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.",[99,593,595],{"id":594},"fechando","Fechando",[597,598,599,613],"table",{},[600,601,602],"thead",{},[603,604,605,610],"tr",{},[606,607,609],"th",{"align":608},"left","O que eu já sabia",[606,611,612],{"align":608},"O que essa aula assentou",[614,615,616,625,633],"tbody",{},[603,617,618,622],{},[619,620,621],"td",{"align":608},"Rede neural é sobre \"aprender pesos\"",[619,623,624],{"align":608},"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",[603,626,627,630],{},[619,628,629],{"align":608},"Atualizar peso \"na direção do erro\" é intuitivo",[619,631,632],{"align":608},"Isso tem nome (critério do perceptron) e uma garantia matemática de convergência pra dado linearmente separável",[603,634,635,638],{},[619,636,637],{"align":608},"Código do professor é a referência de verdade",[619,639,640],{"align":608},"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",[99,642,644],{"id":643},"aplicação-prática","Aplicação Prática",[11,646,647,648,650,651,654,655,658],{},"Testei o mesmo PLA sem bias (código corrigido, sem o bug do ",[130,649,485],{},") num dataset real: Iris, as duas classes mais fáceis de separar (",[29,652,653],{},"setosa"," vs. ",[29,656,657],{},"versicolor","), usando comprimento e largura da pétala como as duas variáveis.",[124,660,662],{"className":126,"code":661,"language":128,"meta":76,"style":76},"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",[130,663,664,669,674,679,684],{"__ignoreMap":76},[133,665,666],{"class":135,"line":136},[133,667,668],{},"from sklearn.datasets import load_iris\n",[133,670,671],{"class":135,"line":77},[133,672,673],{},"iris = load_iris()\n",[133,675,676],{"class":135,"line":83},[133,677,678],{},"mask = iris.target \u003C 2\n",[133,680,681],{"class":135,"line":152},[133,682,683],{},"X = iris.data[mask][:, [2, 3]]  # comprimento e largura da pétala\n",[133,685,686],{"class":135,"line":158},[133,687,688],{},"y = np.where(iris.target[mask] == 0, -1, 1)\n",[11,690,691],{},"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.",[597,693,694,709],{},[600,695,696],{},[603,697,698,701,705],{},[606,699,700],{"align":608},"Versão",[606,702,704],{"align":703},"center","Convergiu em (máx. 50 épocas)",[606,706,708],{"align":707},"right","Acurácia final",[614,710,711,722],{},[603,712,713,716,719],{},[619,714,715],{"align":608},"PLA sem bias",[619,717,718],{"align":703},"nunca convergiu",[619,720,721],{"align":707},"0.84 a 0.93 (variando a semente)",[603,723,724,727,730],{},[619,725,726],{"align":608},"PLA com bias",[619,728,729],{"align":703},"2 épocas",[619,731,732],{"align":707},[15,733,540],{},[11,735,736],{},"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.",[738,739,740],"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":76,"searchDepth":77,"depth":77,"links":742},[743,744,745,746,747,748,749],{"id":101,"depth":77,"text":102},{"id":121,"depth":77,"text":122},{"id":261,"depth":77,"text":262},{"id":449,"depth":77,"text":450},{"id":554,"depth":77,"text":555},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"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.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron","neural-networks",{"title":92,"description":751},"pt\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron",[758,759,760],"perceptron","mcculloch-pitts","rosenblatt","iUSXZah74pR9ZYkVjOWTICZgEnqAVF1Tf6zU8epgbV0",{"id":763,"title":764,"body":765,"cover":3,"date":750,"description":1465,"extension":80,"meta":1466,"navigation":82,"order":77,"path":1467,"playlist":754,"seo":1468,"status":86,"stem":1469,"tags":1470,"__hash__":1473},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fperceptron-com-bias.md","Bias e Vetorização: Destravando o Perceptron",{"type":8,"value":766,"toc":1455},[767,770,774,810,831,838,852,857,861,960,999,1006,1021,1035,1039,1047,1052,1056,1137,1183,1198,1213,1217,1220,1230,1241,1244,1315,1318,1322,1329,1333,1335,1374,1376,1384,1399,1446,1453],[11,768,769],{},"Aula 2a e 2b, e as duas resolvem um problema cada: a primeira destrava onde a fronteira de decisão pode ficar, a segunda destrava como o peso é atualizado.",[99,771,773],{"id":772},"o-mesmo-limite-da-aula-passada-confirmado-de-novo","O mesmo limite da aula passada, confirmado de novo",[124,775,777],{"className":126,"code":776,"language":128,"meta":76,"style":76},"def createDataset(n=20):\n  X = np.random.rand(n,2)\n  coefs = np.array([1, 1])\n  intercept = 1\n  labels = X @ coefs - intercept\n  y = np.array(labels>0, dtype=int)*2-1\n  return X, y\n",[130,778,779,783,787,792,797,802,806],{"__ignoreMap":76},[133,780,781],{"class":135,"line":136},[133,782,139],{},[133,784,785],{"class":135,"line":77},[133,786,144],{},[133,788,789],{"class":135,"line":83},[133,790,791],{},"  coefs = np.array([1, 1])\n",[133,793,794],{"class":135,"line":152},[133,795,796],{},"  intercept = 1\n",[133,798,799],{"class":135,"line":158},[133,800,801],{},"  labels = X @ coefs - intercept\n",[133,803,804],{"class":135,"line":164},[133,805,161],{},[133,807,808],{"class":135,"line":300},[133,809,167],{},[11,811,812,813,816,817,820,821,824,825,828,829,74],{},"Repara na diferença sutil pro dataset da aula passada: agora tem um ",[130,814,815],{},"intercept = 1"," subtraído antes de aplicar o sinal. Isso desloca a reta separadora verdadeira pra ",[130,818,819],{},"x + y = 1",", que ",[15,822,823],{},"não passa pela origem",". ",[20,826,827],{"href":72},"Isso é exatamente o cenário que eu simulei artificialmente lá no post anterior"," usando o Iris: uma fronteira que não passa por ",[130,830,243],{},[11,832,833,834,837],{},"O professor roda o mesmo ",[130,835,836],{},"PLA"," sem bias da aula passada, sem mudar uma linha, direto nesse dataset novo:",[495,839,840],{},[11,841,842,844,845,848,849,74],{},[15,843,501],{}," acurácia ",[15,846,847],{},"0.7",", pesos ",[130,850,851],{},"[2.71, 0.20]",[11,853,854,855,74],{},"Confirma, com o código de verdade dele (não só com a minha simulação no Iris), o mesmo limite: sem bias, a fronteira só pode girar em torno da origem, e não existe jeito de girar uma reta pela origem até ela coincidir com ",[130,856,819],{},[99,858,860],{"id":859},"adicionando-bias-a-fronteira-ganha-liberdade-de-deslizar","Adicionando bias: a fronteira ganha liberdade de deslizar",[124,862,864],{"className":126,"code":863,"language":128,"meta":76,"style":76},"class PLA(BaseEstimator, ClassifierMixin):\n  def __init__(self, max_iter=1000):\n    self.max_iter = max_iter\n\n  def fit(self, X, y):\n    self.w_ = np.random.rand(X.shape[1])\n    self.b_ = np.random.rand()\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_ + self.b_\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          self.b_ += error\n        if cost == 0:\n          break\n    return self\n",[130,865,866,870,875,879,883,887,891,896,900,904,908,912,916,921,925,929,933,937,941,946,950,955],{"__ignoreMap":76},[133,867,868],{"class":135,"line":136},[133,869,272],{},[133,871,872],{"class":135,"line":77},[133,873,874],{},"  def __init__(self, max_iter=1000):\n",[133,876,877],{"class":135,"line":83},[133,878,282],{},[133,880,881],{"class":135,"line":152},[133,882,287],{"emptyLinePlaceholder":82},[133,884,885],{"class":135,"line":158},[133,886,292],{},[133,888,889],{"class":135,"line":164},[133,890,297],{},[133,892,893],{"class":135,"line":300},[133,894,895],{},"    self.b_ = np.random.rand()\n",[133,897,898],{"class":135,"line":306},[133,899,303],{},[133,901,902],{"class":135,"line":312},[133,903,309],{},[133,905,906],{"class":135,"line":318},[133,907,315],{},[133,909,910],{"class":135,"line":324},[133,911,321],{},[133,913,914],{"class":135,"line":330},[133,915,327],{},[133,917,918],{"class":135,"line":336},[133,919,920],{},"        logits = X[i] @ self.w_ + self.b_\n",[133,922,923],{"class":135,"line":342},[133,924,339],{},[133,926,927],{"class":135,"line":348},[133,928,345],{},[133,930,931],{"class":135,"line":354},[133,932,351],{},[133,934,935],{"class":135,"line":360},[133,936,357],{},[133,938,939],{"class":135,"line":366},[133,940,363],{},[133,942,943],{"class":135,"line":372},[133,944,945],{},"          self.b_ += error\n",[133,947,948],{"class":135,"line":378},[133,949,369],{},[133,951,953],{"class":135,"line":952},21,[133,954,375],{},[133,956,958],{"class":135,"line":957},22,[133,959,381],{},[11,961,962,963,966,967,970,971,974,975,977,978,980,981,984,985,987,988,991,992,995,996,74],{},"A mudança é pequena no código mas grande no que ela destrava: agora ",[130,964,965],{},"logits = X[i] @ w_ + b_",", e o ",[130,968,969],{},"b_"," é atualizado junto, ",[130,972,973],{},"self.b_ += error",". O jeito mais direto de entender por que essa é a atualização certa: pensa no bias como o peso de uma variável de entrada extra que vale sempre ",[130,976,572],{},". Se ",[130,979,387],{}," já é atualizado por ",[130,982,983],{},"error * x[i]"," pra cada variável real, o \"peso\" dessa variável fantasma de valor ",[130,986,572],{}," seria atualizado por ",[130,989,990],{},"error * 1",", ou seja, ",[130,993,994],{},"error"," sozinho. É o mesmo truque de sempre (adicionar uma coluna de 1s), só que aqui ele aparece explícito como uma variável separada em vez de escondido dentro de ",[130,997,998],{},"X",[11,1000,1001,1002,1005],{},"Geometricamente, o bias desloca o hiperplano sem girar ele: agora ",[130,1003,1004],{},"w · x + b = 0"," pode estar em qualquer lugar do plano, não só cruzando a origem.",[495,1007,1008],{},[11,1009,1010,844,1012,848,1014,1017,1018,74],{},[15,1011,501],{},[15,1013,540],{},[130,1015,1016],{},"[5.10, 3.28]",", bias ",[130,1019,1020],{},"-3.98",[11,1022,1023,1024,1027,1028,1031,1032,1034],{},"Converge. Repara que o professor também aumentou ",[130,1025,1026],{},"max_iter"," de 10 pra 1000 nessa versão, ",[20,1029,1030],{"href":72},"o mesmo bug de parada da aula passada"," ainda mora dentro desse ",[130,1033,456],{},", só que com 1000 épocas de chance em vez de 10, a probabilidade de nunca conseguir uma passada inteira limpa fica bem menor. Não é um conserto do bug, é só dar espaço suficiente pra sorte compensar.",[99,1036,1038],{"id":1037},"interativo-perceptron-com-bias-ponto-por-ponto","Interativo: perceptron com bias, ponto por ponto",[11,1040,1041,1042,1044,1045,74],{},"Mesmo dataset de verdade do notebook (os 20 pontos exatos que o professor rodou), agora com bias ligado. Clica em \"Processar próximo ponto\" e repara como a fronteira, dessa vez, consegue ",[15,1043,590],{}," pra longe da origem até encaixar em ",[130,1046,819],{},[568,1048],{":classes":1049,":points":1050,":update-bias":1051,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"negative-label":574,"positive-label":575,"x-label":576,"y-label":577},"[1, 1, 1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, 1, -1, -1, 1, 1, 1]","[[0.78019343, 0.84822159], [0.69216878, 0.44056631], [0.60614199, 0.66202198], [0.47672573, 0.54204385], [0.7461111, 0.01893793], [0.6065733, 0.435086], [0.75295568, 0.47766191], [0.07180149, 0.08440745], [0.64647152, 0.46276951], [0.95252713, 0.83099628], [0.74947845, 0.82567423], [0.26043976, 0.72253777], [0.45985729, 0.86798639], [0.21107237, 0.04054585], [0.87159312, 0.76289216], [0.11142677, 0.30233227], [0.10352009, 0.82903887], [0.60640317, 0.65858996], [0.8663473, 0.184016], [0.91813453, 0.47377763]]","true",[99,1053,1055],{"id":1054},"vetorizando-de-laço-ponto-a-ponto-pra-uma-conta-só","Vetorizando: de laço ponto a ponto pra uma conta só",[124,1057,1059],{"className":126,"code":1058,"language":128,"meta":76,"style":76},"class Perceptron(BaseEstimator, ClassifierMixin):\n  def __init__(self, max_iter=1000):\n    self.max_iter = max_iter\n\n  def fit(self, X, y):\n    self.w_ = np.random.rand(X.shape[1])\n    self.b_ = np.random.rand()\n    for _ in range(self.max_iter):\n      cost = 0\n      y_pred = self.predict(X)\n      error = y - y_pred\n      self.w_ += np.dot(X.T, error)\n      self.b_ += np.sum(error)\n      cost = np.sum(error**2)\n      if cost == 0:\n        break\n    return self\n",[130,1060,1061,1066,1070,1074,1078,1082,1086,1090,1094,1098,1103,1108,1113,1118,1123,1128,1133],{"__ignoreMap":76},[133,1062,1063],{"class":135,"line":136},[133,1064,1065],{},"class Perceptron(BaseEstimator, ClassifierMixin):\n",[133,1067,1068],{"class":135,"line":77},[133,1069,874],{},[133,1071,1072],{"class":135,"line":83},[133,1073,282],{},[133,1075,1076],{"class":135,"line":152},[133,1077,287],{"emptyLinePlaceholder":82},[133,1079,1080],{"class":135,"line":158},[133,1081,292],{},[133,1083,1084],{"class":135,"line":164},[133,1085,297],{},[133,1087,1088],{"class":135,"line":300},[133,1089,895],{},[133,1091,1092],{"class":135,"line":306},[133,1093,303],{},[133,1095,1096],{"class":135,"line":312},[133,1097,309],{},[133,1099,1100],{"class":135,"line":318},[133,1101,1102],{},"      y_pred = self.predict(X)\n",[133,1104,1105],{"class":135,"line":324},[133,1106,1107],{},"      error = y - y_pred\n",[133,1109,1110],{"class":135,"line":330},[133,1111,1112],{},"      self.w_ += np.dot(X.T, error)\n",[133,1114,1115],{"class":135,"line":336},[133,1116,1117],{},"      self.b_ += np.sum(error)\n",[133,1119,1120],{"class":135,"line":342},[133,1121,1122],{},"      cost = np.sum(error**2)\n",[133,1124,1125],{"class":135,"line":348},[133,1126,1127],{},"      if cost == 0:\n",[133,1129,1130],{"class":135,"line":354},[133,1131,1132],{},"        break\n",[133,1134,1135],{"class":135,"line":360},[133,1136,381],{},[11,1138,1139,1140,1143,1144,188,1147,1150,1151,188,1153,1156,1157,1160,1161,1164,1165,424,1169,1173,1174,1176,1177,1179,1180,1182],{},"Essa versão joga fora o laço ",[130,1141,1142],{},"for i in idx"," inteiro. Em vez de olhar um ponto por vez, ela: prevê ",[15,1145,1146],{},"todo mundo de uma vez",[130,1148,1149],{},"y_pred = self.predict(X)","), calcula o erro de ",[15,1152,1146],{},[130,1154,1155],{},"error = y - y_pred","), e faz ",[15,1158,1159],{},"uma única atualização"," somando a contribuição de cada ponto errado (",[130,1162,1163],{},"X.T @ error",", o mesmo produto matriz-vetor que já apareceu ",[20,1166,1168],{"href":1167},"\u002Fplaylists\u002Fpattern-recognition\u002Fnormal-equation","na equação normal",[20,1170,1172],{"href":1171},"\u002Fplaylists\u002Fmachine-learning-specialization\u002Fw2-lab01-numpy-vectorization","na especialização do Andrew Ng","). Isso também mata o bug da parada prematura de vez: como ",[130,1175,464],{}," só é calculado ",[15,1178,522],{}," de já ter processado o dataset inteiro naquela iteração, não tem como o ",[130,1181,485],{}," disparar cedo demais.",[11,1184,1185,1186,1189,1190,1193,1194,1197],{},"Essa é a mesma distinção entre ",[15,1187,1188],{},"gradiente descendente em lote"," (usa o dataset inteiro por atualização) e ",[15,1191,1192],{},"gradiente descendente estocástico"," (atualiza a cada exemplo) ",[20,1195,1196],{"href":1171},"que eu já vi na outra playlist",", só que aplicada aqui na regra de aprendizado do perceptron em vez de numa regressão.",[495,1199,1200],{},[11,1201,1202,844,1205,848,1207,1017,1210,74],{},[15,1203,1204],{},"Saída (mesmo dataset de 20 pontos):",[15,1206,540],{},[130,1208,1209],{},"[24.75, 16.55]",[130,1211,1212],{},"-19.04",[99,1214,1216],{"id":1215},"o-detalhe-que-passa-batido-100-no-treino-não-é-100-garantido-em-dado-novo","O detalhe que passa batido: 100% no treino não é 100% garantido em dado novo",[11,1218,1219],{},"O professor testa esse mesmo modelo vetorizado num conjunto de teste bem maior, 1000 pontos novos gerados pela mesma regra:",[495,1221,1222],{},[11,1223,1224,844,1226,1229],{},[15,1225,501],{},[15,1227,1228],{},"0.897"," em 1000 pontos de teste.",[11,1231,1232,1233,1236,1237,1240],{},"Caiu de 1.0 pra 0.897. Isso não é sinal de erro no código, é uma propriedade do próprio perceptron que o Aggarwal comenta no capítulo 1: a regra de atualização do perceptron só garante achar ",[15,1234,1235],{},"alguma"," reta que separa os pontos de treino, não necessariamente a ",[15,1238,1239],{},"melhor"," reta (a de maior margem até os pontos de cada classe). Com só 20 pontos de treino, várias retas diferentes conseguem separar todos eles perfeitamente, mas cada uma dessas retas erra uma fatia diferente de pontos novos que caem perto da fronteira real. O Aggarwal chama isso de \"critério do perceptron\", em contraste com a SVM (Support Vector Machine), que resolve exatamente esse problema maximizando a margem de propósito.",[11,1242,1243],{},"Rodei essa mesma comparação (treino pequeno, teste de 1000 pontos) 5 vezes, tanto na versão ponto a ponto quanto na vetorizada, pra ver se isso é specific de uma versão ou das duas:",[597,1245,1246,1259],{},[600,1247,1248],{},[603,1249,1250,1253,1256],{},[606,1251,1252],{"align":703},"Rodada",[606,1254,1255],{"align":707},"Ponto a ponto (treino \u002F teste)",[606,1257,1258],{"align":707},"Vetorizada (treino \u002F teste)",[614,1260,1261,1271,1282,1293,1304],{},[603,1262,1263,1265,1268],{},[619,1264,572],{"align":703},[619,1266,1267],{"align":707},"1.0 \u002F 0.894",[619,1269,1270],{"align":707},"1.0 \u002F 0.937",[603,1272,1273,1276,1279],{},[619,1274,1275],{"align":703},"2",[619,1277,1278],{"align":707},"1.0 \u002F 0.963",[619,1280,1281],{"align":707},"1.0 \u002F 0.881",[603,1283,1284,1287,1290],{},[619,1285,1286],{"align":703},"3",[619,1288,1289],{"align":707},"1.0 \u002F 0.967",[619,1291,1292],{"align":707},"1.0 \u002F 0.960",[603,1294,1295,1298,1301],{},[619,1296,1297],{"align":703},"4",[619,1299,1300],{"align":707},"1.0 \u002F 0.986",[619,1302,1303],{"align":707},"1.0 \u002F 0.978",[603,1305,1306,1309,1312],{},[619,1307,1308],{"align":703},"5",[619,1310,1311],{"align":707},"1.0 \u002F 0.948",[619,1313,1314],{"align":707},"1.0 \u002F 0.980",[11,1316,1317],{},"As duas sempre acertam 100% no treino, e as duas variam bastante no teste (de 0.88 a 0.99), sem um padrão claro de qual jeito de atualizar é mais confiável. Isso confirma que a instabilidade não vem de \"atualizar ponto a ponto\" versus \"atualizar tudo de uma vez\", vem do próprio critério do perceptron: qualquer separador serve, não necessariamente o mais seguro.",[99,1319,1321],{"id":1320},"interativo-a-versão-vetorizada-um-passo-uma-época-inteira","Interativo: a versão vetorizada, um passo = uma época inteira",[11,1323,1324,1325,1328],{},"Mesmo dataset, mesmo componente, só trocando o modo: agora cada clique processa o dataset ",[15,1326,1327],{},"inteiro"," de uma vez, em vez de ponto por ponto.",[568,1330],{":classes":1049,":points":1050,":update-bias":1051,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"negative-label":574,"positive-label":575,"x-label":576,"y-label":577,"mode":1331,"step-label":1332},"batch","Processar próxima iteração",[99,1334,595],{"id":594},[597,1336,1337,1345],{},[600,1338,1339],{},[603,1340,1341,1343],{},[606,1342,609],{"align":608},[606,1344,612],{"align":608},[614,1346,1347,1355,1366],{},[603,1348,1349,1352],{},[619,1350,1351],{"align":608},"Fronteira sem bias sempre passa pela origem",[619,1353,1354],{"align":608},"Adicionar bias é o mesmo truque de sempre (variável fantasma de valor 1), e destrava a fronteira pra deslizar pra qualquer lugar",[603,1356,1357,1360],{},[619,1358,1359],{"align":608},"Gradiente em lote vs. estocástico é distinção de regressão",[619,1361,1362,1363,1365],{"align":608},"A mesma distinção existe pro perceptron: laço ponto a ponto é a versão \"estocástica\", ",[130,1364,1163],{}," de uma vez é a versão \"em lote\"",[603,1367,1368,1371],{},[619,1369,1370],{"align":608},"Acurácia 1.0 no treino soa como \"terminei\"",[619,1372,1373],{"align":608},"Não garante generalização: o perceptron só acha alguma reta separadora, não a mais segura, e isso é limite do critério, não bug de implementação",[99,1375,644],{"id":643},[11,1377,1378,1379,654,1381,1383],{},"Repeti a comparação ponto a ponto vs. vetorizada no Iris (",[29,1380,653],{},[29,1382,657],{},", comprimento e largura de pétala), agora com bias e com uma divisão treino\u002Fteste de verdade (70\u002F30).",[124,1385,1387],{"className":126,"code":1386,"language":128,"meta":76,"style":76},"from sklearn.model_selection import train_test_split\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)\n",[130,1388,1389,1394],{"__ignoreMap":76},[133,1390,1391],{"class":135,"line":136},[133,1392,1393],{},"from sklearn.model_selection import train_test_split\n",[133,1395,1396],{"class":135,"line":77},[133,1397,1398],{},"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)\n",[597,1400,1401,1416],{},[600,1402,1403],{},[603,1404,1405,1407,1410,1413],{},[606,1406,700],{"align":608},[606,1408,1409],{"align":703},"Épocas até convergir",[606,1411,1412],{"align":707},"Acurácia treino",[606,1414,1415],{"align":707},"Acurácia teste",[614,1417,1418,1432],{},[603,1419,1420,1423,1426,1428],{},[619,1421,1422],{"align":608},"Ponto a ponto",[619,1424,1425],{"align":703},"2 a 3",[619,1427,540],{"align":707},[619,1429,1430],{"align":707},[15,1431,540],{},[603,1433,1434,1437,1440,1442],{},[619,1435,1436],{"align":608},"Vetorizada",[619,1438,1439],{"align":703},"7",[619,1441,540],{"align":707},[619,1443,1444],{"align":707},[15,1445,540],{},[11,1447,1448,1449,1452],{},"Dessa vez as duas bateram 100% no teste também, nas 3 sementes que testei. Faz sentido: diferente do dataset sintético de cima (só 20 pontos espalhados meio soltos), o Iris tem uma margem bem generosa entre as duas classes, então qualquer reta separadora razoável já acerta os pontos novos. A diferença que sobrou foi a velocidade: a versão ponto a ponto convergiu em 2 a 3 épocas, a vetorizada precisou de 7. Faz sentido com o que expliquei mais acima: numa única época, a versão ponto a ponto pode fazer até 20 atualizações de peso (uma por ponto errado encontrado), enquanto a vetorizada faz só ",[15,1450,1451],{},"uma"," atualização por época, então ela naturalmente precisa de mais passadas pelo dataset pra acumular a mesma quantidade de ajuste.",[738,1454,740],{},{"title":76,"searchDepth":77,"depth":77,"links":1456},[1457,1458,1459,1460,1461,1462,1463,1464],{"id":772,"depth":77,"text":773},{"id":859,"depth":77,"text":860},{"id":1037,"depth":77,"text":1038},{"id":1054,"depth":77,"text":1055},{"id":1215,"depth":77,"text":1216},{"id":1320,"depth":77,"text":1321},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"Aula 2a e 2b: o professor adiciona bias ao perceptron (resolvendo exatamente o limite que fechou a aula passada) e depois vetoriza a atualização de peso inteira, trocando o laço ponto a ponto por uma única conta em cima de todo o dataset.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fperceptron-com-bias",{"title":764,"description":1465},"pt\u002Fplaylists\u002Fneural-networks\u002Fperceptron-com-bias",[758,1471,1472],"bias","vetorizacao","ceWL1MfgzSBc5aha6J-W2nsuMG9TqHSQoOCbITdZApY",{"id":1475,"title":1476,"body":1477,"cover":3,"date":750,"description":2138,"extension":80,"meta":2139,"navigation":82,"order":83,"path":2140,"playlist":754,"seo":2141,"status":86,"stem":2142,"tags":2143,"__hash__":2147},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal.md","Da Classificação pra Regressão, e a Solução em Uma Linha",{"type":8,"value":1478,"toc":2130},[1479,1486,1492,1532,1547,1609,1751,1774,1792,1796,1855,1880,1915,1931,1950,1954,1967,1978,1980,2032,2034,2050,2059,2074,2091,2122,2128],[11,1480,1481,1482,1485],{},"Aula 2c e 2d. A estrutura de código muda muito pouco, mas o significado muda bastante: sai o ",[130,1483,1484],{},"sign()",", entra a previsão contínua, e no fim uma conta fechada substitui centenas de iterações.",[99,1487,1489,1490],{"id":1488},"trocando-classificação-por-regressão-só-tira-o-sign","Trocando classificação por regressão: só tira o ",[130,1491,1484],{},[124,1493,1495],{"className":126,"code":1494,"language":128,"meta":76,"style":76},"def createRegressionDataset(n=20):\n  X = np.random.rand(n,1)\n  coef = 0.7\n  intercept = 0.2\n  noise = np.random.randn(n,1) * 0.1\n  y = X * coef + intercept + noise\n  return X, y.reshape(-1)\n",[130,1496,1497,1502,1507,1512,1517,1522,1527],{"__ignoreMap":76},[133,1498,1499],{"class":135,"line":136},[133,1500,1501],{},"def createRegressionDataset(n=20):\n",[133,1503,1504],{"class":135,"line":77},[133,1505,1506],{},"  X = np.random.rand(n,1)\n",[133,1508,1509],{"class":135,"line":83},[133,1510,1511],{},"  coef = 0.7\n",[133,1513,1514],{"class":135,"line":152},[133,1515,1516],{},"  intercept = 0.2\n",[133,1518,1519],{"class":135,"line":158},[133,1520,1521],{},"  noise = np.random.randn(n,1) * 0.1\n",[133,1523,1524],{"class":135,"line":164},[133,1525,1526],{},"  y = X * coef + intercept + noise\n",[133,1528,1529],{"class":135,"line":300},[133,1530,1531],{},"  return X, y.reshape(-1)\n",[11,1533,1534,1535,1537,1538,192,1540,1542,1543,1546],{},"Diferente dos datasets anteriores, ",[130,1536,187],{}," aqui não é mais ",[130,1539,191],{},[130,1541,195],{},", é um número contínuo, ",[130,1544,1545],{},"0.7 * x + 0.2"," mais um ruído gaussiano pequeno. Não tem classe pra acertar, tem uma reta pra encontrar.",[124,1548,1550],{"className":126,"code":1549,"language":128,"meta":76,"style":76},"class LinearRegression(BaseEstimator, ClassifierMixin):\n  def fit(self, X, y):\n    self.w_ = np.random.rand(X.shape[1])\n    self.b_ = np.random.rand()\n    for _ in range(self.max_iter):\n      y_pred = self.predict(X)\n      error = y - y_pred\n      self.w_ += np.dot(X.T, error) * self.learning_rate\n      self.b_ += np.sum(error) * self.learning_rate\n    return self\n\n  def predict(self, X):\n    return X @ self.w_ + self.b_\n",[130,1551,1552,1557,1561,1565,1569,1573,1577,1581,1586,1591,1595,1599,1604],{"__ignoreMap":76},[133,1553,1554],{"class":135,"line":136},[133,1555,1556],{},"class LinearRegression(BaseEstimator, ClassifierMixin):\n",[133,1558,1559],{"class":135,"line":77},[133,1560,292],{},[133,1562,1563],{"class":135,"line":83},[133,1564,297],{},[133,1566,1567],{"class":135,"line":152},[133,1568,895],{},[133,1570,1571],{"class":135,"line":158},[133,1572,303],{},[133,1574,1575],{"class":135,"line":164},[133,1576,1102],{},[133,1578,1579],{"class":135,"line":300},[133,1580,1107],{},[133,1582,1583],{"class":135,"line":306},[133,1584,1585],{},"      self.w_ += np.dot(X.T, error) * self.learning_rate\n",[133,1587,1588],{"class":135,"line":312},[133,1589,1590],{},"      self.b_ += np.sum(error) * self.learning_rate\n",[133,1592,1593],{"class":135,"line":318},[133,1594,381],{},[133,1596,1597],{"class":135,"line":324},[133,1598,287],{"emptyLinePlaceholder":82},[133,1600,1601],{"class":135,"line":330},[133,1602,1603],{},"  def predict(self, X):\n",[133,1605,1606],{"class":135,"line":336},[133,1607,1608],{},"    return X @ self.w_ + self.b_\n",[11,1610,1611,1612,1615,1616,1620,1621,1623,1624,1627,1628,1631,1632,1634,1635,1638,1639,1735,1736,1739,1740,1742,1743,1746,1747,74],{},"Repara como isso é ",[15,1613,1614],{},"quase idêntico"," ao perceptron vetorizado ",[20,1617,1619],{"href":1618},"\u002Fplaylists\u002Fneural-networks\u002Fperceptron-com-bias","do post passado",": mesmo ",[130,1622,1163],{}," pra atualizar peso, mesmo ",[130,1625,1626],{},"sum(error)"," pra atualizar bias. As duas diferenças são pequenas no código, mas mudam o significado inteiro: primeiro, ",[130,1629,1630],{},"predict"," não passa mais por ",[130,1633,1484],{},", a previsão fica contínua (ativação linear, ",[20,1636,1637],{"href":72},"a mesma ideia que o Aggarwal descreve"," como a função de ativação mais simples que existe, ",[133,1640,1643,1683],{"className":1641},[1642],"katex",[133,1644,1647],{"className":1645},[1646],"katex-mathml",[1648,1649,1651],"math",{"xmlns":1650},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[1652,1653,1654,1678],"semantics",{},[1655,1656,1657,1662,1667,1670,1673,1676],"mrow",{},[1658,1659,1661],"mi",{"mathvariant":1660},"normal","Φ",[1663,1664,1666],"mo",{"stretchy":1665},"false","(",[1658,1668,1669],{},"v",[1663,1671,1672],{"stretchy":1665},")",[1663,1674,1675],{},"=",[1658,1677,1669],{},[1679,1680,1682],"annotation",{"encoding":1681},"application\u002Fx-tex","\\Phi(v) = v",[133,1684,1687,1725],{"className":1685,"ariaHidden":1051},[1686],"katex-html",[133,1688,1691,1696,1700,1704,1709,1713,1718,1722],{"className":1689},[1690],"base",[133,1692],{"className":1693,"style":1695},[1694],"strut","height:1em;vertical-align:-0.25em;",[133,1697,1661],{"className":1698},[1699],"mord",[133,1701,1666],{"className":1702},[1703],"mopen",[133,1705,1669],{"className":1706,"style":1708},[1699,1707],"mathnormal","margin-right:0.0359em;",[133,1710,1672],{"className":1711},[1712],"mclose",[133,1714],{"className":1715,"style":1717},[1716],"mspace","margin-right:0.2778em;",[133,1719,1675],{"className":1720},[1721],"mrel",[133,1723],{"className":1724,"style":1717},[1716],[133,1726,1728,1732],{"className":1727},[1690],[133,1729],{"className":1730,"style":1731},[1694],"height:0.4306em;",[133,1733,1669],{"className":1734,"style":1708},[1699,1707],"). Segundo, apareceu um ",[130,1737,1738],{},"learning_rate"," multiplicando a atualização. No perceptron essa taxa de aprendizado nem existia, era implicitamente ",[130,1741,572],{},". O Aggarwal chama isso de peculiaridade interessante do perceptron, dá pra fixar a taxa em 1 porque ela só reescala o peso, não muda a direção do ajuste. Aqui, com erro contínuo em vez de erro ",[130,1744,1745],{},"{-2,0,+2}",", a magnitude do ajuste pode ficar grande ou pequena demais dependendo da escala do erro, e por isso a taxa de aprendizado explícita vira necessária pra controlar o tamanho do passo, ",[20,1748,1750],{"href":1749},"\u002Fplaylists\u002Fmachine-learning-specialization\u002Fw2-lab03-feature-scaling","o mesmo assunto que já rendeu um post inteiro na outra playlist",[495,1752,1753],{},[11,1754,1755,1757,1758,848,1761,1017,1764,1767,1768,424,1770,1773],{},[15,1756,501],{}," RMSE ",[130,1759,1760],{},"0.0948",[130,1762,1763],{},"[0.706]",[130,1765,1766],{},"0.204",". Bem perto do gerador de verdade (",[130,1769,847],{},[130,1771,1772],{},"0.2","), a diferença é só o ruído que foi embutido de propósito no dataset.",[11,1775,1776,1777,1780,1781,1784,1785,424,1788,1791],{},"Uma observação honesta: a célula seguinte do notebook original chama ",[130,1778,1779],{},"createDataset"," (não ",[130,1782,1783],{},"createRegressionDataset","), ",[130,1786,1787],{},"accuracy_score",[130,1789,1790],{},"plotHyperplan",", nomes que não existem nesse notebook, só no da aula passada. Isso só rodou porque o Colab do professor estava com a sessão anterior ainda na memória (variável reaproveitada de aula pra aula). Rodando esse notebook do zero, essa célula quebraria. Não reproduzo esse pedaço aqui, porque ele não testa de fato o modelo de regressão que acabou de ser treinado.",[99,1793,1795],{"id":1794},"a-equação-normal-a-mesma-pergunta-resolvida-sem-iterar","A equação normal: a mesma pergunta, resolvida sem iterar",[124,1797,1799],{"className":126,"code":1798,"language":128,"meta":76,"style":76},"def include_bias(X):\n  return np.hstack((np.ones((X.shape[0],1)), X))\n\nclass NormalEquation(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",[130,1800,1801,1806,1811,1815,1820,1824,1829,1834,1838,1842,1846,1850],{"__ignoreMap":76},[133,1802,1803],{"class":135,"line":136},[133,1804,1805],{},"def include_bias(X):\n",[133,1807,1808],{"class":135,"line":77},[133,1809,1810],{},"  return np.hstack((np.ones((X.shape[0],1)), X))\n",[133,1812,1813],{"class":135,"line":83},[133,1814,287],{"emptyLinePlaceholder":82},[133,1816,1817],{"class":135,"line":152},[133,1818,1819],{},"class NormalEquation(BaseEstimator, ClassifierMixin):\n",[133,1821,1822],{"class":135,"line":158},[133,1823,292],{},[133,1825,1826],{"class":135,"line":164},[133,1827,1828],{},"    X = include_bias(X)\n",[133,1830,1831],{"class":135,"line":300},[133,1832,1833],{},"    self.w_ = np.linalg.pinv(X) @ y\n",[133,1835,1836],{"class":135,"line":306},[133,1837,381],{},[133,1839,1840],{"class":135,"line":312},[133,1841,287],{"emptyLinePlaceholder":82},[133,1843,1844],{"class":135,"line":318},[133,1845,1603],{},[133,1847,1848],{"class":135,"line":324},[133,1849,1828],{},[133,1851,1852],{"class":135,"line":330},[133,1853,1854],{},"    return X @ self.w_\n",[11,1856,1857,1858,1861,1862,1864,1865,1867,1868,1871,1872,1874,1875,1877,1878,74],{},"Duas coisas novas aqui. A primeira é o ",[130,1859,1860],{},"include_bias",": gruda uma coluna de ",[130,1863,572],{},"s na frente de ",[130,1866,998],{},". Isso é literalmente o truque que eu descrevi em palavras ",[20,1869,1870],{"href":1618},"no post passado",", o bias como peso de uma variável fantasma que vale sempre ",[130,1873,572],{},", só que agora escrito como código de verdade em vez de um ",[130,1876,969],{}," separado. Com essa coluna extra, o bias vira só mais um peso normal dentro de ",[130,1879,387],{},[11,1881,1882,1883,1886,1887,1889,1890,1893,1894,1897,1898,188,1901,1904,1905,1907,1908,1911,1912,1914],{},"A segunda é o ",[130,1884,1885],{},"NormalEquation"," em si: nada de laço, nada de ",[130,1888,1738],{},", só ",[130,1891,1892],{},"np.linalg.pinv(X) @ y",". Essa é a solução fechada ",[20,1895,1896],{"href":1167},"que eu já explorei com detalhe lá no Reconhecimento de Padrões",", a mesma pergunta (\"qual reta minimiza o erro quadrático\") resolvida direto, numa única conta, em vez de descida passo a passo. A diferença técnica aqui é o método: lá eu implementei via eliminação de Gauss-Jordan com pivotamento. O professor usa a ",[15,1899,1900],{},"pseudo-inversa",[130,1902,1903],{},"pinv","), que resolve por decomposição SVD por baixo dos panos. A vantagem da pseudo-inversa é ela nunca travar: mesmo se ",[130,1906,998],{}," tiver colunas redundantes (linearmente dependentes) e a matriz ",[130,1909,1910],{},"X^T X"," não puder ser invertida da forma tradicional, ",[130,1913,1903],{}," ainda devolve uma resposta válida (a de menor norma entre as infinitas soluções possíveis). Eliminação de Gauss-Jordan pura, nesse mesmo caso, simplesmente quebra.",[495,1916,1917],{},[11,1918,1919,1757,1921,848,1924,1927,1928,1930],{},[15,1920,501],{},[130,1922,1923],{},"0.12364226682065012",[130,1925,1926],{},"[0.25402951 0.61056989]"," (bias e coeficiente, nessa ordem, por causa do ",[130,1929,1860],{},").",[11,1932,1933,1934,1937,1938,1941,1942,1945,1946,1949],{},"E o gradiente descendente da célula anterior, no mesmo dataset, chegou em RMSE ",[130,1935,1936],{},"0.12364226680246916",", pesos praticamente idênticos. Duas contas completamente diferentes (uma iterativa, uma fechada) convergindo pro mesmíssimo lugar, até a sétima casa decimal. Reproduzi isso eu mesmo com um dataset seedado (",[130,1939,1940],{},"np.random.seed(7)",") pra confirmar que não foi coincidência de uma rodada só: GD deu pesos ",[130,1943,1944],{},"[0.6478, bias 0.2041]",", equação normal deu ",[130,1947,1948],{},"[bias 0.2041, coef 0.6478]",", RMSE idêntico até a nona casa decimal nos dois.",[99,1951,1953],{"id":1952},"interativo-ache-a-reta-você-mesmo","Interativo: ache a reta você mesmo",[11,1955,1956,1957,1960,1961,424,1963,1966],{},"Antes de ver a máquina resolver, tenta resolver na mão. Esse é o mesmo dataset seedado de cima (20 pontos reais gerados por ",[130,1958,1959],{},"coef=0.7, intercept=0.2"," mais ruído), mexe nos sliders de ",[130,1962,235],{},[130,1964,1965],{},"b"," e observa o erro total mudando ao vivo.",[1968,1969],"model-playground",{":b-max":572,":b-min":1970,":b-step":1971,":initial-b":573,":initial-w":573,":w-max":1972,":w-min":573,":w-step":1971,":x-train":1973,":y-train":1974,"dataLabel":1975,"prediction-label":1976,"x-label":1977,"y-label":187},"-0.5","0.05","1.5","[0.0763, 0.7799, 0.4384, 0.7235, 0.978, 0.5385, 0.5011, 0.0721, 0.2684, 0.4999, 0.6792, 0.8037, 0.3809, 0.0659, 0.2881, 0.9096, 0.2134, 0.4521, 0.9312, 0.0249]","[0.3089, 0.7583, 0.5343, 0.5538, 1.0497, 0.5924, 0.5121, 0.4533, 0.3834, 0.4048, 0.6349, 0.5338, 0.5716, 0.2045, 0.3274, 0.944, 0.1843, 0.57, 0.6454, 0.1512]","pontos de treino","reta ajustada","x",[99,1979,595],{"id":594},[597,1981,1982,1990],{},[600,1983,1984],{},[603,1985,1986,1988],{},[606,1987,609],{"align":608},[606,1989,612],{"align":608},[614,1991,1992,2005,2019],{},[603,1993,1994,1999],{},[619,1995,1996,1997],{"align":608},"Perceptron classifica com ",[130,1998,1484],{},[619,2000,2001,2002,2004],{"align":608},"Tirar o ",[130,2003,1484],{}," da mesma estrutura de código transforma classificação em regressão, quase sem mexer em mais nada",[603,2006,2007,2010],{},[619,2008,2009],{"align":608},"Bias é peso de uma variável fantasma de valor 1",[619,2011,2012,2013,2015,2016,2018],{"align":608},"Isso vira código explícito com ",[130,2014,1860],{},", em vez de um ",[130,2017,969],{}," separado",[603,2020,2021,2024],{},[619,2022,2023],{"align":608},"Equação normal já resolvi via Gauss-Jordan",[619,2025,2026,2028,2029,2031],{"align":608},[130,2027,1903],{}," (pseudo-inversa via SVD) resolve a mesma conta e ainda funciona quando ",[130,2030,1910],{}," não é invertível",[99,2033,644],{"id":643},[11,2035,2036,2037,2041,2042,2045,2046,2049],{},"Testei gradiente descendente contra a equação normal no dataset real de 50 casas ",[20,2038,2040],{"href":2039},"\u002Fplaylists\u002Fmachine-learning-specialization\u002Flab02-model-representation","que já apareceu na outra playlist",", usando só ",[130,2043,2044],{},"square_feet"," pra prever ",[130,2047,2048],{},"price",", sem normalizar nada primeiro.",[124,2051,2053],{"className":126,"code":2052,"language":128,"meta":76,"style":76},"w, b = fit_gd(X, y, max_iter=1000, learning_rate=0.01)  # sem normalizar\n",[130,2054,2055],{"__ignoreMap":76},[133,2056,2057],{"class":135,"line":136},[133,2058,2052],{},[495,2060,2061],{},[11,2062,2063,2065,2066,2069,2070,2073],{},[15,2064,501],{}," o peso vira ",[130,2067,2068],{},"-inf"," já na iteração 71. O gradiente descendente ",[15,2071,2072],{},"diverge"," completamente.",[11,2075,2076,2077,2080,2081,2083,2084,2086,2087,2090],{},"Esperado: ",[20,2078,2079],{"href":1749},"é a mesma lição da feature scaling na outra playlist",", só que reencontrada aqui dentro do contexto de rede neural. ",[130,2082,2044],{}," vive na casa das centenas e ",[130,2085,2048],{}," na casa das centenas de milhares, então o gradiente é enorme e o passo de ",[130,2088,2089],{},"0.01"," explode.",[597,2092,2093,2103],{},[600,2094,2095],{},[603,2096,2097,2100],{},[606,2098,2099],{"align":608},"Abordagem",[606,2101,2102],{"align":707},"RMSE",[614,2104,2105,2113],{},[603,2106,2107,2110],{},[619,2108,2109],{"align":608},"Gradiente descendente, dado normalizado",[619,2111,2112],{"align":707},"101878.42",[603,2114,2115,2118],{},[619,2116,2117],{"align":608},"Equação normal, dado cru (sem normalizar)",[619,2119,2120],{"align":707},[15,2121,2112],{},[11,2123,2124,2125,2127],{},"Normalizando ",[130,2126,2044],{}," antes de rodar gradiente descendente, ele converge e chega exatamente no mesmo RMSE que a equação normal encontra direto no dado cru, sem precisar normalizar nada. Faz sentido: a equação normal resolve o sistema linear de uma vez só, então a escala das variáveis afeta só a estabilidade numérica da conta, não se ela converge ou não (diferente do gradiente descendente, que literalmente pode divergir se o passo for grande demais pra escala do dado).",[738,2129,740],{},{"title":76,"searchDepth":77,"depth":77,"links":2131},[2132,2134,2135,2136,2137],{"id":1488,"depth":77,"text":2133},"Trocando classificação por regressão: só tira o sign()",{"id":1794,"depth":77,"text":1795},{"id":1952,"depth":77,"text":1953},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"Aula 2c e 2d: o professor troca o sinal binário pela reta contínua (regressão linear) e depois substitui gradiente descendente inteiro por uma única conta fechada, a equação normal via pseudo-inversa.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal",{"title":1476,"description":2138},"pt\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal",[2144,2145,2146],"regressao-linear","equacao-normal","gradiente-descendente","WaS0edSiFTL2UX4OXXs55rOfEizPZpaLDDENEz5LVik",{"id":2149,"title":2150,"body":2151,"cover":3,"date":750,"description":2765,"extension":80,"meta":2766,"navigation":82,"order":152,"path":2767,"playlist":754,"seo":2768,"status":86,"stem":2769,"tags":2770,"__hash__":2774},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta.md","Adaline: Treinar na Reta, Classificar no Sinal",{"type":8,"value":2152,"toc":2757},[2153,2166,2170,2212,2239,2251,2257,2312,2336,2348,2375,2379,2392,2397,2400,2404,2449,2466,2482,2486,2521,2541,2555,2562,2598,2610,2612,2655,2657,2665,2679,2702,2745,2755],[11,2154,2155,2156,2159,2160,424,2163,74],{},"Aula 2e e 2f, e o nome do modelo muda pra ",[15,2157,2158],{},"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,2161,2162],{},"o que o modelo otimiza durante o treino",[15,2164,2165],{},"o que ele calcula na hora de prever",[99,2167,2169],{"id":2168},"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",[124,2171,2173],{"className":126,"code":2172,"language":128,"meta":76,"style":76},"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",[130,2174,2175,2180,2184,2188,2192,2196,2200,2204,2208],{"__ignoreMap":76},[133,2176,2177],{"class":135,"line":136},[133,2178,2179],{},"class AdalinePseudoInverse(BaseEstimator, ClassifierMixin):\n",[133,2181,2182],{"class":135,"line":77},[133,2183,292],{},[133,2185,2186],{"class":135,"line":83},[133,2187,1828],{},[133,2189,2190],{"class":135,"line":152},[133,2191,1833],{},[133,2193,2194],{"class":135,"line":158},[133,2195,381],{},[133,2197,2198],{"class":135,"line":164},[133,2199,287],{"emptyLinePlaceholder":82},[133,2201,2202],{"class":135,"line":300},[133,2203,1603],{},[133,2205,2206],{"class":135,"line":306},[133,2207,1828],{},[133,2209,2210],{"class":135,"line":312},[133,2211,1854],{},[11,2213,2214,2215,533,2218,2222,2223,2225,2226,192,2228,2230,2231,2233,2234,2236,2237,74],{},"Repara: isso é ",[15,2216,2217],{},"exatamente",[20,2219,2221],{"href":2220},"\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal","a equação normal do post passado",", sem mudar uma linha, só que agora ",[130,2224,187],{}," é ",[130,2227,191],{},[130,2229,195],{}," 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 ",[130,2232,191],{}," nos pontos de uma classe e ",[130,2235,195],{}," na outra, sem nunca aplicar ",[130,2238,1484],{},[495,2240,2241],{},[11,2242,2243,1757,2245,848,2248,74],{},[15,2244,501],{},[130,2246,2247],{},"0.4148",[130,2249,2250],{},"[-1.76, 1.01, 2.74]",[11,2252,2253,2254,2256],{},"RMSE faz sentido aqui porque ",[130,2255,1630],{}," devolve um número contínuo, não uma classe. Só que pra classificar de verdade, falta o último passo:",[124,2258,2260],{"className":126,"code":2259,"language":128,"meta":76,"style":76},"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",[130,2261,2262,2266,2270,2274,2278,2282,2286,2291,2295,2299,2303,2307],{"__ignoreMap":76},[133,2263,2264],{"class":135,"line":136},[133,2265,2179],{},[133,2267,2268],{"class":135,"line":77},[133,2269,292],{},[133,2271,2272],{"class":135,"line":83},[133,2273,1828],{},[133,2275,2276],{"class":135,"line":152},[133,2277,1833],{},[133,2279,2280],{"class":135,"line":158},[133,2281,381],{},[133,2283,2284],{"class":135,"line":164},[133,2285,287],{"emptyLinePlaceholder":82},[133,2287,2288],{"class":135,"line":300},[133,2289,2290],{},"  def pre_activation(self, X):\n",[133,2292,2293],{"class":135,"line":306},[133,2294,1828],{},[133,2296,2297],{"class":135,"line":312},[133,2298,1854],{},[133,2300,2301],{"class":135,"line":318},[133,2302,287],{"emptyLinePlaceholder":82},[133,2304,2305],{"class":135,"line":324},[133,2306,1603],{},[133,2308,2309],{"class":135,"line":330},[133,2310,2311],{},"    return np.sign(self.pre_activation(X))\n",[11,2313,2314,2315,2318,2319,2321,2322,2324,2325,424,2328,2331,2332,2335],{},"Agora tem dois métodos: ",[130,2316,2317],{},"pre_activation"," (o valor contínuo, antes de qualquer limiar) e ",[130,2320,1630],{}," (aplica ",[130,2323,1484],{}," em cima). O Aggarwal chama exatamente essas duas coisas de ",[15,2326,2327],{},"valor de pré-ativação",[15,2329,2330],{},"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 (",[130,2333,2334],{},"sign",") na hora de decidir a classe final.",[495,2337,2338],{},[11,2339,2340,844,2342,2344,2345,2347],{},[15,2341,501],{},[130,2343,540],{},", os mesmos pesos ",[130,2346,2250],{}," de antes (é a mesma conta, só que agora avaliada por acurácia em vez de RMSE).",[11,2349,2350,2351,37,2354,2357,2358,2361,2362,2364,2365,2367,2368,2370,2371,2374],{},"Isso é a ",[15,2352,2353],{},"regra delta",[20,2355,2356],{"href":35},"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 ",[20,2359,2360],{"href":72},"duas aulas atrás",") é justamente essa: o perceptron mede o erro ",[15,2363,522],{}," de aplicar ",[130,2366,1484],{}," (erro em ",[130,2369,1745],{},"), o Adaline mede o erro ",[15,2372,2373],{},"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\".",[99,2376,2378],{"id":2377},"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,2380,2381,2382,2385,2386,424,2389,2391],{},"Em vez de treinar automaticamente, arrasta os sliders de ",[130,2383,2384],{},"w0",", ",[130,2387,2388],{},"w1",[130,2390,1471],{}," 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).",[2393,2394],"adaline-explorer",{":classes":2395,":points":2396,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"negative-label":574,"positive-label":575,"x-label":576,"y-label":577},"[-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]]",[11,2398,2399],{},"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).",[99,2401,2403],{"id":2402},"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\")",[124,2405,2407],{"className":126,"code":2406,"language":128,"meta":76,"style":76},"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",[130,2408,2409,2414,2418,2422,2427,2431,2436,2440,2445],{"__ignoreMap":76},[133,2410,2411],{"class":135,"line":136},[133,2412,2413],{},"class Adaline(BaseEstimator, ClassifierMixin):\n",[133,2415,2416],{"class":135,"line":77},[133,2417,292],{},[133,2419,2420],{"class":135,"line":83},[133,2421,1828],{},[133,2423,2424],{"class":135,"line":152},[133,2425,2426],{},"    self.w_ = np.zeros(X.shape[1])\n",[133,2428,2429],{"class":135,"line":158},[133,2430,303],{},[133,2432,2433],{"class":135,"line":164},[133,2434,2435],{},"      y_pred = X @ self.w_\n",[133,2437,2438],{"class":135,"line":300},[133,2439,1107],{},[133,2441,2442],{"class":135,"line":306},[133,2443,2444],{},"      self.w_ += self.learning_rate * error @ X\n",[133,2446,2447],{"class":135,"line":312},[133,2448,381],{},[11,2450,2451,2452,2455,2456,188,2459,2465],{},"Um detalhe honesto sobre o nome do notebook (",[130,2453,2454],{},"aula02f adaline with SGD","): o código mostrado aqui é ",[15,2457,2458],{},"gradiente em lote",[20,2460,2461,2462,2464],{"href":1618},"o mesmo ",[130,2463,1163],{}," 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.",[495,2467,2468],{},[11,2469,2470,844,2472,2474,2475,2478,2479,74],{},[15,2471,501],{},[130,2473,540],{}," no treino, pesos ",[130,2476,2477],{},"[-2.92, 3.32, 2.63]",". Testando em 1000 pontos novos: acurácia ",[15,2480,2481],{},"0.953",[99,2483,2485],{"id":2484},"o-dataset-ruim-o-que-ele-realmente-prova","O dataset \"ruim\": o que ele realmente prova",[124,2487,2489],{"className":126,"code":2488,"language":128,"meta":76,"style":76},"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",[130,2490,2491,2496,2501,2506,2511,2516],{"__ignoreMap":76},[133,2492,2493],{"class":135,"line":136},[133,2494,2495],{},"X_bad = np.concatenate((X,np.ones_like(X)))\n",[133,2497,2498],{"class":135,"line":77},[133,2499,2500],{},"y_bad = np.concatenate((y,np.ones_like(y)))\n",[133,2502,2503],{"class":135,"line":83},[133,2504,2505],{},"X_bad = np.concatenate((X_bad,np.ones_like(X)))\n",[133,2507,2508],{"class":135,"line":152},[133,2509,2510],{},"y_bad = np.concatenate((y_bad,np.ones_like(y)))\n",[133,2512,2513],{"class":135,"line":158},[133,2514,2515],{},"clf_bad = Adaline()\n",[133,2517,2518],{"class":135,"line":164},[133,2519,2520],{},"clf_bad.fit(X_bad, y_bad)\n",[11,2522,2523,2524,2527,2528,2531,2532,2534,2535,2537,2538,2540],{},"O professor concatena o dataset original com ",[15,2525,2526],{},"dois blocos extras"," de pontos artificiais: todo mundo em ",[130,2529,2530],{},"(1,1)",", todo mundo rotulado ",[130,2533,195],{},". Isso não é ruído, é um enviesamento deliberado, empurrando o treino a \"acreditar\" que a região perto de ",[130,2536,2530],{}," é ainda mais fortemente classe ",[130,2539,195],{}," do que ela realmente é.",[495,2542,2543],{},[11,2544,2545,844,2547,2550,2551,2554],{},[15,2546,501],{},[130,2548,2549],{},"0.967"," no treino (sobre o dataset enviesado), mas só ",[15,2552,2553],{},"0.811"," nos mesmos 1000 pontos de teste limpos de antes.",[11,2556,2557,2558,2561],{},"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,2559,2560],{},"no mesmo dataset enviesado",", e a hipótese não se sustentou:",[597,2563,2564,2577],{},[600,2565,2566],{},[603,2567,2568,2571,2574],{},[606,2569,2570],{"align":608},"Método",[606,2572,2573],{"align":707},"Acurácia treino (enviesado)",[606,2575,2576],{"align":707},"Acurácia teste (limpo)",[614,2578,2579,2589],{},[603,2580,2581,2584,2586],{},[619,2582,2583],{"align":608},"Pseudo-inversa",[619,2585,2549],{"align":707},[619,2587,2588],{"align":707},"0.783",[603,2590,2591,2594,2596],{},[619,2592,2593],{"align":608},"Gradiente em lote",[619,2595,2549],{"align":707},[619,2597,2588],{"align":707},[11,2599,2600,2601,2604,2605,2609],{},"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,2602,2603],{},"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. ",[20,2606,2608],{"href":2607},"\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.",[99,2611,595],{"id":594},[597,2613,2614,2622],{},[600,2615,2616],{},[603,2617,2618,2620],{},[606,2619,609],{"align":608},[606,2621,612],{"align":608},[614,2623,2624,2635,2647],{},[603,2625,2626,2629],{},[619,2627,2628],{"align":608},"Perceptron classifica e atualiza pelo erro binário",[619,2630,2631,2632,2634],{"align":608},"Adaline separa pré-ativação (contínua, usada no treino) de pós-ativação (",[130,2633,2334],{},", usada só na hora de decidir a classe)",[603,2636,2637,2640],{},[619,2638,2639],{"align":608},"Regra delta já apareceu na outra playlist",[619,2641,2642,2643,2646],{"align":608},"Adaline ",[15,2644,2645],{},"é"," a regra delta aplicada à classificação, treinando como se fosse regressão nos rótulos ±1",[603,2648,2649,2652],{},[619,2650,2651],{"align":608},"Achar uma reta que separa parece suficiente",[619,2653,2654],{"align":608},"Otimizar RMSE (não só contar erro) continua achando fronteira melhor mesmo depois da acurácia bater 100%",[99,2656,644],{"id":643},[11,2658,2659,2660,654,2662,2664],{},"Reproduzi pseudo-inversa vs. gradiente em lote no Iris (",[29,2661,653],{},[29,2663,657],{},"), a essa altura já um velho conhecido dessa playlist, mas dessa vez treinando como Adaline, no alvo contínuo ±1, não como perceptron.",[124,2666,2668],{"className":126,"code":2667,"language":128,"meta":76,"style":76},"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",[130,2669,2670,2674],{"__ignoreMap":76},[133,2671,2672],{"class":135,"line":136},[133,2673,1398],{},[133,2675,2676],{"class":135,"line":77},[133,2677,2678],{},"w_pinv = np.linalg.pinv(include_bias(X_train)) @ y_train\n",[11,2680,2681,2682,2685,2686,2689,2690,2693,2694,2697,2698,2701],{},"Na primeira tentativa, usei o mesmo ",[130,2683,2684],{},"learning_rate=0.01"," do notebook original pro gradiente em lote, e ele ",[15,2687,2688],{},"divergiu"," (peso virou ",[130,2691,2692],{},"NaN",") direto no dado cru do Iris. Nada de surpresa nessa altura: ",[20,2695,2696],{"href":2220},"é a mesma lição de escala do post da equação normal",", só reencontrada de novo, dessa vez precisando de uma taxa bem menor (",[130,2699,2700],{},"0.001",") pra não explodir.",[597,2703,2704,2716],{},[600,2705,2706],{},[603,2707,2708,2710,2713],{},[606,2709,2570],{"align":608},[606,2711,2712],{"align":707},"RMSE (treino)",[606,2714,2715],{"align":707},"Acurácia (teste)",[614,2717,2718,2730],{},[603,2719,2720,2723,2726],{},[619,2721,2722],{"align":608},"Pseudo-inversa (Adaline)",[619,2724,2725],{"align":707},"0.2414",[619,2727,2728],{"align":707},[15,2729,540],{},[603,2731,2732,2739,2741],{},[619,2733,2734,2735,2738],{"align":608},"Gradiente em lote, ",[130,2736,2737],{},"learning_rate=0.001"," (Adaline)",[619,2740,2725],{"align":707},[619,2742,2743],{"align":707},[15,2744,540],{},[11,2746,2747,2748,2751,2752,2754],{},"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%, ",[20,2749,2750],{"href":1618},"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 ",[130,2753,1738],{},"), não qualidade da solução final.",[738,2756,740],{},{"title":76,"searchDepth":77,"depth":77,"links":2758},[2759,2760,2761,2762,2763,2764],{"id":2168,"depth":77,"text":2169},{"id":2377,"depth":77,"text":2378},{"id":2402,"depth":77,"text":2403},{"id":2484,"depth":77,"text":2485},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"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.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta",{"title":2150,"description":2765},"pt\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta",[2771,2772,2773],"adaline","regra-delta","widrow-hoff","cbghfvPwR6a9QLRUcV5MuER7WPdR2fYLxW2Wn5ttVUs",{"id":2776,"title":2777,"body":2778,"cover":3,"date":750,"description":4118,"extension":80,"meta":4119,"navigation":82,"order":158,"path":4120,"playlist":754,"seo":4121,"status":86,"stem":4122,"tags":4123,"__hash__":4127},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Ffuncoes-de-custo.md","Trocando a Função de Custo Como Quem Troca de Roupa",{"type":8,"value":2779,"toc":4110},[2780,2787,2791,2891,2923,2949,2957,2961,3034,3065,3069,3105,3313,3349,3362,3383,3437,3468,3484,3762,3800,3811,3825,3829,3929,3938,3955,3957,4009,4011,4025,4099,4108],[11,2781,2782,2783,2786],{},"Aula 3a e 3b. Até aqui eu vi 4 algoritmos diferentes (perceptron, perceptron vetorizado, Adaline via pseudo-inversa, Adaline via gradiente) meio que como coisas separadas. Essa aula mostra que eles são, na verdade, a ",[15,2784,2785],{},"mesma receita",", só trocando um ingrediente: a função de custo.",[99,2788,2790],{"id":2789},"primeiro-o-algoritmo-de-treino-vira-plugável","Primeiro, o algoritmo de treino vira plugável",[124,2792,2794],{"className":126,"code":2793,"language":128,"meta":76,"style":76},"class TrainingAlgorithm(ABC):\n  @abstractmethod\n  def get_w(self, X, y):\n    pass\n\nclass PseudoInverse(TrainingAlgorithm):\n  def get_w(self, X, y):\n    return np.linalg.pinv(X) @ y\n\nclass NeuralNetwork(BaseEstimator, ClassifierMixin):\n  def __init__(self, training_algorithm=PseudoInverse()):\n    self.training_algorithm = training_algorithm\n\n  def fit(self, X, y):\n    X = include_bias(X)\n    self.w_ = self.training_algorithm.get_w(X, y)\n    return self\n\n  def predict(self, X):\n    X = include_bias(X)\n    return np.sign(X @ self.w_)\n",[130,2795,2796,2801,2806,2811,2816,2820,2825,2829,2834,2838,2843,2848,2853,2857,2861,2865,2870,2874,2878,2882,2886],{"__ignoreMap":76},[133,2797,2798],{"class":135,"line":136},[133,2799,2800],{},"class TrainingAlgorithm(ABC):\n",[133,2802,2803],{"class":135,"line":77},[133,2804,2805],{},"  @abstractmethod\n",[133,2807,2808],{"class":135,"line":83},[133,2809,2810],{},"  def get_w(self, X, y):\n",[133,2812,2813],{"class":135,"line":152},[133,2814,2815],{},"    pass\n",[133,2817,2818],{"class":135,"line":158},[133,2819,287],{"emptyLinePlaceholder":82},[133,2821,2822],{"class":135,"line":164},[133,2823,2824],{},"class PseudoInverse(TrainingAlgorithm):\n",[133,2826,2827],{"class":135,"line":300},[133,2828,2810],{},[133,2830,2831],{"class":135,"line":306},[133,2832,2833],{},"    return np.linalg.pinv(X) @ y\n",[133,2835,2836],{"class":135,"line":312},[133,2837,287],{"emptyLinePlaceholder":82},[133,2839,2840],{"class":135,"line":318},[133,2841,2842],{},"class NeuralNetwork(BaseEstimator, ClassifierMixin):\n",[133,2844,2845],{"class":135,"line":324},[133,2846,2847],{},"  def __init__(self, training_algorithm=PseudoInverse()):\n",[133,2849,2850],{"class":135,"line":330},[133,2851,2852],{},"    self.training_algorithm = training_algorithm\n",[133,2854,2855],{"class":135,"line":336},[133,2856,287],{"emptyLinePlaceholder":82},[133,2858,2859],{"class":135,"line":342},[133,2860,292],{},[133,2862,2863],{"class":135,"line":348},[133,2864,1828],{},[133,2866,2867],{"class":135,"line":354},[133,2868,2869],{},"    self.w_ = self.training_algorithm.get_w(X, y)\n",[133,2871,2872],{"class":135,"line":360},[133,2873,381],{},[133,2875,2876],{"class":135,"line":366},[133,2877,287],{"emptyLinePlaceholder":82},[133,2879,2880],{"class":135,"line":372},[133,2881,1603],{},[133,2883,2884],{"class":135,"line":378},[133,2885,1828],{},[133,2887,2888],{"class":135,"line":952},[133,2889,2890],{},"    return np.sign(X @ self.w_)\n",[11,2892,2893,2894,505,2897,2900,2901,2904,2905,2908,2909,2912,2913,2916,2917,2920,2921,74],{},"Isso é o padrão de projeto ",[15,2895,2896],{},"Strategy",[130,2898,2899],{},"NeuralNetwork"," não sabe mais ",[15,2902,2903],{},"como"," os pesos são calculados, só que existe um objeto ",[130,2906,2907],{},"training_algorithm"," com um método ",[130,2910,2911],{},"get_w",". Trocar ",[130,2914,2915],{},"PseudoInverse()"," por ",[130,2918,2919],{},"SGD()"," no construtor troca o algoritmo de treino inteiro, sem tocar em ",[130,2922,2899],{},[495,2924,2925],{},[11,2926,2927,844,2934,848,2937,2940,2941,844,2946,2948],{},[15,2928,2929,2930,2933],{},"Saída (",[130,2931,2932],{},"PseudoInverse","):",[130,2935,2936],{},"0.95",[130,2938,2939],{},"[-1.905, 2.656, 1.049]",".\n",[15,2942,2929,2943,2933],{},[130,2944,2945],{},"SGD",[130,2947,2936],{},", pesos praticamente idênticos.",[11,2950,2951,2952,2956],{},"Confirma de novo, agora com o código organizado de um jeito mais limpo, o que ",[20,2953,2955],{"href":2954},"\u002Fplaylists\u002Fneural-networks\u002Fadaline-regra-delta","já vi nos dois posts passados",": pseudo-inversa e gradiente descendente resolvem o mesmíssimo problema.",[99,2958,2960],{"id":2959},"agora-a-função-de-custo-também-vira-plugável","Agora a função de custo também vira plugável",[124,2962,2964],{"className":126,"code":2963,"language":128,"meta":76,"style":76},"class CostFunction(ABC):\n  @abstractstaticmethod\n  def get_cost(y, y_pred):\n    pass\n  @abstractstaticmethod\n  def get_gradient(X, y, y_pred):\n    pass\n\nclass WidrowHoff(CostFunction):\n  @staticmethod\n  def get_cost(y, y_pred):\n    return np.mean((y-y_pred)**2)\n  @staticmethod\n  def get_gradient(X, y, y_pred):\n    return X.T @ (y-y_pred)\n",[130,2965,2966,2971,2976,2981,2985,2989,2994,2998,3002,3007,3012,3016,3021,3025,3029],{"__ignoreMap":76},[133,2967,2968],{"class":135,"line":136},[133,2969,2970],{},"class CostFunction(ABC):\n",[133,2972,2973],{"class":135,"line":77},[133,2974,2975],{},"  @abstractstaticmethod\n",[133,2977,2978],{"class":135,"line":83},[133,2979,2980],{},"  def get_cost(y, y_pred):\n",[133,2982,2983],{"class":135,"line":152},[133,2984,2815],{},[133,2986,2987],{"class":135,"line":158},[133,2988,2975],{},[133,2990,2991],{"class":135,"line":164},[133,2992,2993],{},"  def get_gradient(X, y, y_pred):\n",[133,2995,2996],{"class":135,"line":300},[133,2997,2815],{},[133,2999,3000],{"class":135,"line":306},[133,3001,287],{"emptyLinePlaceholder":82},[133,3003,3004],{"class":135,"line":312},[133,3005,3006],{},"class WidrowHoff(CostFunction):\n",[133,3008,3009],{"class":135,"line":318},[133,3010,3011],{},"  @staticmethod\n",[133,3013,3014],{"class":135,"line":324},[133,3015,2980],{},[133,3017,3018],{"class":135,"line":330},[133,3019,3020],{},"    return np.mean((y-y_pred)**2)\n",[133,3022,3023],{"class":135,"line":336},[133,3024,3011],{},[133,3026,3027],{"class":135,"line":342},[133,3028,2993],{},[133,3030,3031],{"class":135,"line":348},[133,3032,3033],{},"    return X.T @ (y-y_pred)\n",[11,3035,384,3036,3038,3039,3042,3043,3046,3047,2225,3050,3053,3054,3057,3058,3060,3061,3064],{},[130,3037,2945],{}," agora recebe um ",[130,3040,3041],{},"cost_function"," também, e usa ",[130,3044,3045],{},"self.cost_function.get_gradient(...)"," em vez de calcular o gradiente na mão. ",[130,3048,3049],{},"WidrowHoff",[20,3051,3052],{"href":2954},"exatamente a regra delta que eu vi na aula passada",": erro contínuo (",[130,3055,3056],{},"y - y_pred",", sem ",[130,3059,1484],{},") vezes a entrada. Trocar a função de custo aqui é trocar ",[15,3062,3063],{},"o que \"erro\" significa",", sem tocar no laço de treino.",[99,3066,3068],{"id":3067},"cada-função-de-custo-recupera-um-algoritmo-diferente","Cada função de custo recupera um algoritmo diferente",[124,3070,3072],{"className":126,"code":3071,"language":128,"meta":76,"style":76},"class SmoothedSurrogate(CostFunction):\n  @staticmethod\n  def get_cost(y, y_pred):\n    return np.sum(np.maximum(np.zeros(y.shape), -y * y_pred))\n  @staticmethod\n  def get_gradient(X, y, y_pred):\n    return X.T @ (y - np.sign(y_pred))\n",[130,3073,3074,3079,3083,3087,3092,3096,3100],{"__ignoreMap":76},[133,3075,3076],{"class":135,"line":136},[133,3077,3078],{},"class SmoothedSurrogate(CostFunction):\n",[133,3080,3081],{"class":135,"line":77},[133,3082,3011],{},[133,3084,3085],{"class":135,"line":83},[133,3086,2980],{},[133,3088,3089],{"class":135,"line":152},[133,3090,3091],{},"    return np.sum(np.maximum(np.zeros(y.shape), -y * y_pred))\n",[133,3093,3094],{"class":135,"line":158},[133,3095,3011],{},[133,3097,3098],{"class":135,"line":164},[133,3099,2993],{},[133,3101,3102],{"class":135,"line":300},[133,3103,3104],{},"    return X.T @ (y - np.sign(y_pred))\n",[11,3106,3107,3108,3111,3112,3114,3115,3118,3119,3122,3123,505,3126,3306,3307,3309,3310,3312],{},"Repara no ",[130,3109,3110],{},"np.sign(y_pred)"," dentro do gradiente: isso volta a medir erro ",[15,3113,522],{}," do limiar, exatamente como ",[20,3116,3117],{"href":72},"o perceptron do Rosenblatt",". O nome ",[130,3120,3121],{},"SmoothedSurrogate"," bate com o que o Aggarwal chama de ",[15,3124,3125],{},"critério do perceptron",[133,3127,3129,3178],{"className":3128},[1642],[133,3130,3132],{"className":3131},[1646],[1648,3133,3134],{"xmlns":1650},[1652,3135,3136,3175],{},[1655,3137,3138,3141,3143,3146,3149,3151,3154,3157,3160,3162,3165,3173],{},[1658,3139,3140],{},"L",[1663,3142,1675],{},[1658,3144,3145],{},"max",[1663,3147,3148],{},"⁡",[1663,3150,1666],{"stretchy":1665},[3152,3153,573],"mn",{},[1663,3155,3156],{"separator":1051},",",[1663,3158,3159],{},"−",[1658,3161,187],{},[1663,3163,3164],{},"⋅",[3166,3167,3168,3170],"mover",{"accent":1051},[1658,3169,187],{},[1663,3171,3172],{},"^",[1663,3174,1672],{"stretchy":1665},[1679,3176,3177],{"encoding":1681},"L = \\max(0, -y \\cdot \\hat{y})",[133,3179,3181,3200,3241],{"className":3180,"ariaHidden":1051},[1686],[133,3182,3184,3188,3191,3194,3197],{"className":3183},[1690],[133,3185],{"className":3186,"style":3187},[1694],"height:0.6833em;",[133,3189,3140],{"className":3190},[1699,1707],[133,3192],{"className":3193,"style":1717},[1716],[133,3195,1675],{"className":3196},[1721],[133,3198],{"className":3199,"style":1717},[1716],[133,3201,3203,3206,3210,3213,3216,3220,3224,3227,3230,3234,3238],{"className":3202},[1690],[133,3204],{"className":3205,"style":1695},[1694],[133,3207,3145],{"className":3208},[3209],"mop",[133,3211,1666],{"className":3212},[1703],[133,3214,573],{"className":3215},[1699],[133,3217,3156],{"className":3218},[3219],"mpunct",[133,3221],{"className":3222,"style":3223},[1716],"margin-right:0.1667em;",[133,3225,3159],{"className":3226},[1699],[133,3228,187],{"className":3229,"style":1708},[1699,1707],[133,3231],{"className":3232,"style":3233},[1716],"margin-right:0.2222em;",[133,3235,3164],{"className":3236},[3237],"mbin",[133,3239],{"className":3240,"style":3233},[1716],[133,3242,3244,3247,3303],{"className":3243},[1690],[133,3245],{"className":3246,"style":1695},[1694],[133,3248,3251],{"className":3249},[1699,3250],"accent",[133,3252,3256,3294],{"className":3253},[3254,3255],"vlist-t","vlist-t2",[133,3257,3260,3289],{"className":3258},[3259],"vlist-r",[133,3261,3265,3276],{"className":3262,"style":3264},[3263],"vlist","height:0.6944em;",[133,3266,3268,3273],{"style":3267},"top:-3em;",[133,3269],{"className":3270,"style":3272},[3271],"pstrut","height:3em;",[133,3274,187],{"className":3275,"style":1708},[1699,1707],[133,3277,3278,3281],{"style":3267},[133,3279],{"className":3280,"style":3272},[3271],[133,3282,3286],{"className":3283,"style":3285},[3284],"accent-body","left:-0.1944em;",[133,3287,3172],{"className":3288},[1699],[133,3290,3293],{"className":3291},[3292],"vlist-s","​",[133,3295,3297],{"className":3296},[3259],[133,3298,3301],{"className":3299,"style":3300},[3263],"height:0.1944em;",[133,3302],{},[133,3304,1672],{"className":3305},[1712],", zero quando o ponto já está do lado certo, crescendo linearmente quando está errado. Não é coincidência a acurácia bater ",[130,3308,540],{},": essa função de custo, plugada nesse framework genérico, ",[15,3311,2645],{}," o perceptron original de novo, só que expresso na linguagem de \"função de custo\" em vez de \"regra de atualização\".",[124,3314,3316],{"className":126,"code":3315,"language":128,"meta":76,"style":76},"class LogLikehood(CostFunction):\n  @staticmethod\n  def get_cost(y, y_pred):\n    return np.sum(np.maximum(np.zeros(y.shape), 1 - y * y_pred))\n  @staticmethod\n  def get_gradient(X, y, y_pred):\n    return X.T @ (y - expit(y_pred))\n",[130,3317,3318,3323,3327,3331,3336,3340,3344],{"__ignoreMap":76},[133,3319,3320],{"class":135,"line":136},[133,3321,3322],{},"class LogLikehood(CostFunction):\n",[133,3324,3325],{"class":135,"line":77},[133,3326,3011],{},[133,3328,3329],{"class":135,"line":83},[133,3330,2980],{},[133,3332,3333],{"class":135,"line":152},[133,3334,3335],{},"    return np.sum(np.maximum(np.zeros(y.shape), 1 - y * y_pred))\n",[133,3337,3338],{"class":135,"line":158},[133,3339,3011],{},[133,3341,3342],{"class":135,"line":164},[133,3343,2993],{},[133,3345,3346],{"class":135,"line":300},[133,3347,3348],{},"    return X.T @ (y - expit(y_pred))\n",[495,3350,3351],{},[11,3352,3353,844,3355,848,3358,3361],{},[15,3354,501],{},[15,3356,3357],{},"0.65",[130,3359,3360],{},"[-60.78, 27.44, -24.79]",". Bem pior que tudo que eu vi até aqui, e os pesos ficaram enormes.",[11,3363,3364,3365,3368,3369,3372,3373,3376,3377,3379,3380,3382],{},"Duas coisas erradas aqui, e vale separar. Primeiro, um detalhe que não afeta o resultado: o ",[130,3366,3367],{},"get_cost"," dessa classe usa a fórmula do hinge loss (",[130,3370,3371],{},"max(0, 1 - y·ŷ)","), não uma fórmula de log-verossimilhança de verdade. Isso não quebra nada na prática porque ",[130,3374,3375],{},"get_gradient"," é a única coisa que o ",[130,3378,2945],{}," chama, ",[130,3381,3367],{}," nunca é usado durante o treino, sobrou como resíduo de copiar e colar de outra célula.",[11,3384,3385,3386,3389,3390,424,3392,3394,3395,2225,3397,192,3399,3401,3402,3405,3406,533,3409,3412,3413,3415,3416,3419,3420,3422,3423,3425,3426,3429,3430,3433,3434,3436],{},"O segundo problema é de verdade, e explica a acurácia ruim: ",[130,3387,3388],{},"expit"," (a função sigmoide) devolve valores só entre ",[130,3391,573],{},[130,3393,572],{},", mas o rótulo ",[130,3396,187],{},[130,3398,191],{},[130,3400,195],{},". Pra classe ",[130,3403,3404],{},"y=-1",", o erro ",[130,3407,3408],{},"y - expit(y_pred)",[15,3410,3411],{},"nunca"," consegue chegar perto de zero, porque ",[130,3414,3388],{}," nunca fica negativo: mesmo com a previsão infinitamente confiante do lado certo, ",[130,3417,3418],{},"-1 - expit(y_pred)"," fica travado perto de ",[130,3421,191],{},", nunca de ",[130,3424,573],{},". Conferi isso na mão: ",[130,3427,3428],{},"expit(-1000) = 0.0",", então ",[130,3431,3432],{},"-1 - expit(-1000) = -1.0"," cravado, não ",[130,3435,573],{},". O gradiente pra metade dos pontos nunca some, então o treino nunca sossega, e os pesos ficam crescendo tentando compensar um erro que é estruturalmente impossível de zerar.",[124,3438,3440],{"className":126,"code":3439,"language":128,"meta":76,"style":76},"class LogLikehood(CostFunction):\n  @staticmethod\n  def get_gradient(X, y, y_pred):\n    return X.T @ (y - tanh(y_pred))\n\nmodel = NeuralNetwork(training_algorithm=SGD(max_iter=10000, cost_function=LogLikehood()))\n",[130,3441,3442,3446,3450,3454,3459,3463],{"__ignoreMap":76},[133,3443,3444],{"class":135,"line":136},[133,3445,3322],{},[133,3447,3448],{"class":135,"line":77},[133,3449,3011],{},[133,3451,3452],{"class":135,"line":83},[133,3453,2993],{},[133,3455,3456],{"class":135,"line":152},[133,3457,3458],{},"    return X.T @ (y - tanh(y_pred))\n",[133,3460,3461],{"class":135,"line":158},[133,3462,287],{"emptyLinePlaceholder":82},[133,3464,3465],{"class":135,"line":164},[133,3466,3467],{},"model = NeuralNetwork(training_algorithm=SGD(max_iter=10000, cost_function=LogLikehood()))\n",[495,3469,3470],{},[11,3471,3472,844,3474,848,3476,3479,3480,3483],{},[15,3473,501],{},[15,3475,540],{},[130,3477,3478],{},"[-9.84, 12.40, 8.37]"," (com ",[130,3481,3482],{},"max_iter=10000",", dez vezes mais iterações).",[11,3485,3486,3487,2916,3489,3492,3493,3495,3496,424,3498,3500,3501,3429,3504,3507,3508,3647,3648,3700,3701,3758,3759,74],{},"Trocando ",[130,3488,3388],{},[130,3490,3491],{},"tanh"," (mesma classe redefinida, o Python deixa isso rodando ao vivo numa sessão de notebook), o problema some. Faz sentido: ",[130,3494,3491],{}," varia entre ",[130,3497,191],{},[130,3499,195],{},", exatamente a faixa dos rótulos. Conferi de novo na mão: ",[130,3502,3503],{},"tanh(-1000) = -1.0",[130,3505,3506],{},"-1 - tanh(-1000) = 0.0",", o erro consegue mesmo chegar a zero dessa vez. O Aggarwal cita essa relação direto no capítulo 1: ",[133,3509,3511,3555],{"className":3510},[1642],[133,3512,3514],{"className":3513},[1646],[1648,3515,3516],{"xmlns":1650},[1652,3517,3518,3552],{},[1655,3519,3520,3522,3524,3526,3528,3530,3532,3534,3536,3540,3542,3544,3546,3548,3550],{},[1658,3521,3491],{},[1663,3523,3148],{},[1663,3525,1666],{"stretchy":1665},[1658,3527,1669],{},[1663,3529,1672],{"stretchy":1665},[1663,3531,1675],{},[3152,3533,1275],{},[1663,3535,3164],{},[3537,3538,3539],"mtext",{},"sigmoide",[1663,3541,1666],{"stretchy":1665},[3152,3543,1275],{},[1658,3545,1669],{},[1663,3547,1672],{"stretchy":1665},[1663,3549,3159],{},[3152,3551,572],{},[1679,3553,3554],{"encoding":1681},"\\tanh(v) = 2 \\cdot \\text{sigmoide}(2v) - 1",[133,3556,3558,3585,3604,3638],{"className":3557,"ariaHidden":1051},[1686],[133,3559,3561,3564,3567,3570,3573,3576,3579,3582],{"className":3560},[1690],[133,3562],{"className":3563,"style":1695},[1694],[133,3565,3491],{"className":3566},[3209],[133,3568,1666],{"className":3569},[1703],[133,3571,1669],{"className":3572,"style":1708},[1699,1707],[133,3574,1672],{"className":3575},[1712],[133,3577],{"className":3578,"style":1717},[1716],[133,3580,1675],{"className":3581},[1721],[133,3583],{"className":3584,"style":1717},[1716],[133,3586,3588,3592,3595,3598,3601],{"className":3587},[1690],[133,3589],{"className":3590,"style":3591},[1694],"height:0.6444em;",[133,3593,1275],{"className":3594},[1699],[133,3596],{"className":3597,"style":3233},[1716],[133,3599,3164],{"className":3600},[3237],[133,3602],{"className":3603,"style":3233},[1716],[133,3605,3607,3610,3617,3620,3623,3626,3629,3632,3635],{"className":3606},[1690],[133,3608],{"className":3609,"style":1695},[1694],[133,3611,3614],{"className":3612},[1699,3613],"text",[133,3615,3539],{"className":3616},[1699],[133,3618,1666],{"className":3619},[1703],[133,3621,1275],{"className":3622},[1699],[133,3624,1669],{"className":3625,"style":1708},[1699,1707],[133,3627,1672],{"className":3628},[1712],[133,3630],{"className":3631,"style":3233},[1716],[133,3633,3159],{"className":3634},[3237],[133,3636],{"className":3637,"style":3233},[1716],[133,3639,3641,3644],{"className":3640},[1690],[133,3642],{"className":3643,"style":3591},[1694],[133,3645,572],{"className":3646},[1699],", e é exatamente esse deslocamento de faixa, de ",[133,3649,3651,3673],{"className":3650},[1642],[133,3652,3654],{"className":3653},[1646],[1648,3655,3656],{"xmlns":1650},[1652,3657,3658,3670],{},[1655,3659,3660,3662,3664,3666,3668],{},[1663,3661,1666],{"stretchy":1665},[3152,3663,573],{},[1663,3665,3156],{"separator":1051},[3152,3667,572],{},[1663,3669,1672],{"stretchy":1665},[1679,3671,3672],{"encoding":1681},"(0,1)",[133,3674,3676],{"className":3675,"ariaHidden":1051},[1686],[133,3677,3679,3682,3685,3688,3691,3694,3697],{"className":3678},[1690],[133,3680],{"className":3681,"style":1695},[1694],[133,3683,1666],{"className":3684},[1703],[133,3686,573],{"className":3687},[1699],[133,3689,3156],{"className":3690},[3219],[133,3692],{"className":3693,"style":3223},[1716],[133,3695,572],{"className":3696},[1699],[133,3698,1672],{"className":3699},[1712]," pra ",[133,3702,3704,3728],{"className":3703},[1642],[133,3705,3707],{"className":3706},[1646],[1648,3708,3709],{"xmlns":1650},[1652,3710,3711,3725],{},[1655,3712,3713,3715,3717,3719,3721,3723],{},[1663,3714,1666],{"stretchy":1665},[1663,3716,3159],{},[3152,3718,572],{},[1663,3720,3156],{"separator":1051},[3152,3722,572],{},[1663,3724,1672],{"stretchy":1665},[1679,3726,3727],{"encoding":1681},"(-1,1)",[133,3729,3731],{"className":3730,"ariaHidden":1051},[1686],[133,3732,3734,3737,3740,3743,3746,3749,3752,3755],{"className":3733},[1690],[133,3735],{"className":3736,"style":1695},[1694],[133,3738,1666],{"className":3739},[1703],[133,3741,3159],{"className":3742},[1699],[133,3744,572],{"className":3745},[1699],[133,3747,3156],{"className":3748},[3219],[133,3750],{"className":3751,"style":3223},[1716],[133,3753,572],{"className":3754},[1699],[133,3756,1672],{"className":3757},[1712],", que resolve o descompasso com rótulo ",[130,3760,3761],{},"±1",[124,3763,3765],{"className":126,"code":3764,"language":128,"meta":76,"style":76},"class HingeLoss(CostFunction):\n  @staticmethod\n  def get_gradient(X, y, y_pred):\n    marginal_errors = (y * y_pred) \u003C 1\n    marginal_ys = np.copy(y)\n    marginal_ys[~marginal_errors] = 0\n    return X.T @ marginal_ys\n",[130,3766,3767,3772,3776,3780,3785,3790,3795],{"__ignoreMap":76},[133,3768,3769],{"class":135,"line":136},[133,3770,3771],{},"class HingeLoss(CostFunction):\n",[133,3773,3774],{"class":135,"line":77},[133,3775,3011],{},[133,3777,3778],{"class":135,"line":83},[133,3779,2993],{},[133,3781,3782],{"class":135,"line":152},[133,3783,3784],{},"    marginal_errors = (y * y_pred) \u003C 1\n",[133,3786,3787],{"class":135,"line":158},[133,3788,3789],{},"    marginal_ys = np.copy(y)\n",[133,3791,3792],{"class":135,"line":164},[133,3793,3794],{},"    marginal_ys[~marginal_errors] = 0\n",[133,3796,3797],{"class":135,"line":300},[133,3798,3799],{},"    return X.T @ marginal_ys\n",[495,3801,3802],{},[11,3803,3804,844,3806,848,3808,74],{},[15,3805,501],{},[15,3807,540],{},[130,3809,3810],{},"[-8.70, 11.59, 7.71]",[11,3812,3107,3813,3816,3817,3820,3821,3824],{},[130,3814,3815],{},"marginal_ys[~marginal_errors] = 0",": pontos que já estão bem classificados, com folga (",[130,3818,3819],{},"y · ŷ ≥ 1","), são zerados e ",[15,3822,3823],{},"não contribuem nada"," pro gradiente. Só os pontos dentro da margem (ou errados) participam da atualização. Essa é literalmente a ideia central da SVM: só os pontos perto da fronteira (os \"vetores de suporte\") importam pra decidir onde ela fica. O resto do dataset é ignorado depois que já está bem separado.",[99,3826,3828],{"id":3827},"as-quatro-curvas-lado-a-lado","As quatro curvas, lado a lado",[11,3830,3831,3832,3928],{},"O professor tem uma imagem de referência guardada no notebook comparando as quatro curvas de penalidade em função da \"margem\" (",[133,3833,3835,3857],{"className":3834},[1642],[133,3836,3838],{"className":3837},[1646],[1648,3839,3840],{"xmlns":1650},[1652,3841,3842,3854],{},[1655,3843,3844,3846,3848],{},[1658,3845,187],{},[1663,3847,3164],{},[3166,3849,3850,3852],{"accent":1051},[1658,3851,187],{},[1663,3853,3172],{},[1679,3855,3856],{"encoding":1681},"y \\cdot \\hat{y}",[133,3858,3860,3879],{"className":3859,"ariaHidden":1051},[1686],[133,3861,3863,3867,3870,3873,3876],{"className":3862},[1690],[133,3864],{"className":3865,"style":3866},[1694],"height:0.6389em;vertical-align:-0.1944em;",[133,3868,187],{"className":3869,"style":1708},[1699,1707],[133,3871],{"className":3872,"style":3233},[1716],[133,3874,3164],{"className":3875},[3237],[133,3877],{"className":3878,"style":3233},[1716],[133,3880,3882,3886],{"className":3881},[1690],[133,3883],{"className":3884,"style":3885},[1694],"height:0.8889em;vertical-align:-0.1944em;",[133,3887,3889],{"className":3888},[1699,3250],[133,3890,3892,3920],{"className":3891},[3254,3255],[133,3893,3895,3917],{"className":3894},[3259],[133,3896,3898,3906],{"className":3897,"style":3264},[3263],[133,3899,3900,3903],{"style":3267},[133,3901],{"className":3902,"style":3272},[3271],[133,3904,187],{"className":3905,"style":1708},[1699,1707],[133,3907,3908,3911],{"style":3267},[133,3909],{"className":3910,"style":3272},[3271],[133,3912,3914],{"className":3913,"style":3285},[3284],[133,3915,3172],{"className":3916},[1699],[133,3918,3293],{"className":3919},[3292],[133,3921,3923],{"className":3922},[3259],[133,3924,3926],{"className":3925,"style":3300},[3263],[133,3927],{},": positivo e grande é acerto confiante, negativo é erro). Recriei a mesma ideia aqui, interativa:",[3930,3931],"margin-loss-chart",{"hinge-label":3932,"logistic-label":3933,"perceptron-label":3934,"widrow-hoff-label":3935,"x-label":3936,"y-label":3937},"Hinge (SVM)","Logística","Perceptron","Widrow-Hoff","margem (y · ŷ)","penalidade",[11,3939,3940,3941,3944,3945,3948,3949,3951,3952,3954],{},"Passa o mouse em cima de qualquer ponto do eixo x e compara as quatro. Repara nos formatos: Widrow-Hoff é uma parábola, penaliza até ponto que já acertou com folga (margem ",[130,3942,3943],{},"> 1","), porque ela não sabe que \"acerto é acerto\", só sabe medir distância até o alvo contínuo. Perceptron e Hinge são os dois únicos que ",[15,3946,3947],{},"zeram"," de vez quando o ponto está bem classificado (perceptron zera assim que passa de ",[130,3950,573],{},", hinge exige passar de ",[130,3953,572],{},", com folga). Logística nunca zera de verdade, só se aproxima de zero, o que é o preço de ela devolver uma probabilidade suave em vez de uma decisão binária.",[99,3956,595],{"id":594},[597,3958,3959,3967],{},[600,3960,3961],{},[603,3962,3963,3965],{},[606,3964,609],{"align":608},[606,3966,612],{"align":608},[614,3968,3969,3987,4001],{},[603,3970,3971,3974],{},[619,3972,3973],{"align":608},"Perceptron, Adaline, gradiente batch pareciam algoritmos separados",[619,3975,3976,3977,3979,3980,3979,3983,3986],{"align":608},"São o mesmo framework (",[130,3978,2899],{}," + ",[130,3981,3982],{},"TrainingAlgorithm",[130,3984,3985],{},"CostFunction","), só trocando qual função de custo plugar",[603,3988,3989,3992],{},[619,3990,3991],{"align":608},"Sigmoide devolve probabilidade entre 0 e 1",[619,3993,3994,3995,3997,3998,4000],{"align":608},"Usar sigmoide direto contra rótulo ",[130,3996,3761],{}," trava o gradiente, porque a faixa de saída não bate com a faixa do alvo. ",[130,3999,3491],{}," resolve isso",[603,4002,4003,4006],{},[619,4004,4005],{"align":608},"SVM usa \"vetores de suporte\"",[619,4007,4008],{"align":608},"Isso não é jargão vazio: o gradiente do hinge loss literalmente zera a contribuição de todo ponto que não é um vetor de suporte",[99,4010,644],{"id":643},[11,4012,4013,4014,4016,4017,654,4019,4021,4022,4024],{},"Rodei as quatro funções de custo (Widrow-Hoff, critério do perceptron, hinge, e a versão log-verossimilhança com ",[130,4015,3491],{},") no Iris (",[29,4018,653],{},[29,4020,657],{},"), mais a versão com ",[130,4023,3388],{}," de propósito, pra confirmar que o problema da sigmoide não é exclusivo do dataset sintético de brinquedo.",[597,4026,4027,4038],{},[600,4028,4029],{},[603,4030,4031,4034,4036],{},[606,4032,4033],{"align":608},"Função de custo",[606,4035,1412],{"align":707},[606,4037,1415],{"align":707},[614,4039,4040,4050,4060,4071,4084],{},[603,4041,4042,4044,4046],{},[619,4043,3935],{"align":608},[619,4045,540],{"align":707},[619,4047,4048],{"align":707},[15,4049,540],{},[603,4051,4052,4054,4056],{},[619,4053,3934],{"align":608},[619,4055,540],{"align":707},[619,4057,4058],{"align":707},[15,4059,540],{},[603,4061,4062,4065,4067],{},[619,4063,4064],{"align":608},"Hinge",[619,4066,540],{"align":707},[619,4068,4069],{"align":707},[15,4070,540],{},[603,4072,4073,4078,4080],{},[619,4074,4075,4076,1672],{"align":608},"Log-verossimilhança (",[130,4077,3491],{},[619,4079,540],{"align":707},[619,4081,4082],{"align":707},[15,4083,540],{},[603,4085,4086,4091,4094],{},[619,4087,4075,4088,4090],{"align":608},[130,4089,3388],{},", sigmoide)",[619,4092,4093],{"align":707},"0.843",[619,4095,4096],{"align":707},[15,4097,4098],{},"0.933",[11,4100,4101,4102,4104,4105,4107],{},"Quatro das cinco batem 100% (o Iris tem margem generosa o bastante pra qualquer uma delas achar uma fronteira perfeita), e a sigmoide de novo fica pra trás, dessa vez em dado real, não só no dataset sintético do notebook. Confirma que não foi coincidência de uma rodada: o descompasso de faixa entre ",[130,4103,3388],{}," e rótulo ",[130,4106,3761],{}," prejudica a convergência de verdade, em qualquer dataset onde eu tentei.",[738,4109,740],{},{"title":76,"searchDepth":77,"depth":77,"links":4111},[4112,4113,4114,4115,4116,4117],{"id":2789,"depth":77,"text":2790},{"id":2959,"depth":77,"text":2960},{"id":3067,"depth":77,"text":3068},{"id":3827,"depth":77,"text":3828},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"Aula 3a e 3b: o professor generaliza o treino pra aceitar qualquer função de custo plugável, e cada escolha (Widrow-Hoff, critério do perceptron, log-verossimilhança, hinge) recupera um algoritmo diferente dessa playlist. No meio do caminho, achei uma incompatibilidade real entre função de ativação e codificação de rótulo.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Ffuncoes-de-custo",{"title":2777,"description":4118},"pt\u002Fplaylists\u002Fneural-networks\u002Ffuncoes-de-custo",[4124,4125,4126],"funcoes-de-custo","hinge-loss","regressao-logistica","vrlcf9sVSWCNt1PSqoQuZ_AyK-_pQcbYMuR7_tC7G-c",{"id":4129,"title":4130,"body":4131,"cover":3,"date":750,"description":4638,"extension":80,"meta":4639,"navigation":82,"order":164,"path":4640,"playlist":754,"seo":4641,"status":86,"stem":4642,"tags":4643,"__hash__":4646},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse.md","Multiclasse: Quando um Peso Vira uma Matriz",{"type":8,"value":4132,"toc":4628},[4133,4136,4140,4179,4188,4198,4202,4227,4236,4272,4276,4301,4323,4343,4349,4359,4427,4458,4466,4470,4477,4487,4490,4492,4538,4540,4551,4560,4568,4610,4626],[11,4134,4135],{},"Aula 3c, e o professor troca o dataset de 2 classes por um de 4. A primeira tentativa é honestamente reusar tudo que já existe sem mudar nada, só pra mostrar, na prática, exatamente onde isso quebra.",[99,4137,4139],{"id":4138},"o-dataset-quatro-blobs-um-em-cada-canto","O dataset: quatro blobs, um em cada canto",[124,4141,4143],{"className":126,"code":4142,"language":128,"meta":76,"style":76},"def createMulticlassDataset(n=40):\n  X, y = make_blobs(n_samples=n,\n                    centers=[[0.2,0.2], [0.8, 0.2], [0.2, 0.8], [0.8, 0.8]],\n                    n_features=2,\n                    cluster_std=0.05,\n                    center_box=(0,1))\n  return X, y\n",[130,4144,4145,4150,4155,4160,4165,4170,4175],{"__ignoreMap":76},[133,4146,4147],{"class":135,"line":136},[133,4148,4149],{},"def createMulticlassDataset(n=40):\n",[133,4151,4152],{"class":135,"line":77},[133,4153,4154],{},"  X, y = make_blobs(n_samples=n,\n",[133,4156,4157],{"class":135,"line":83},[133,4158,4159],{},"                    centers=[[0.2,0.2], [0.8, 0.2], [0.2, 0.8], [0.8, 0.8]],\n",[133,4161,4162],{"class":135,"line":152},[133,4163,4164],{},"                    n_features=2,\n",[133,4166,4167],{"class":135,"line":158},[133,4168,4169],{},"                    cluster_std=0.05,\n",[133,4171,4172],{"class":135,"line":164},[133,4173,4174],{},"                    center_box=(0,1))\n",[133,4176,4177],{"class":135,"line":300},[133,4178,167],{},[124,4180,4182],{"className":126,"code":4181,"language":128,"meta":76,"style":76},"set(y_train)\n",[130,4183,4184],{"__ignoreMap":76},[133,4185,4186],{"class":135,"line":136},[133,4187,4181],{},[495,4189,4190],{},[11,4191,4192,533,4194,4197],{},[15,4193,501],{},[130,4195,4196],{},"{0, 1, 2, 3}",". Quatro classes, um número inteiro cada.",[99,4199,4201],{"id":4200},"primeira-tentativa-reusar-o-que-já-existe-e-falhar-de-propósito","Primeira tentativa: reusar o que já existe (e falhar de propósito)",[124,4203,4205],{"className":126,"code":4204,"language":128,"meta":76,"style":76},"model = NeuralNetwork()  # o mesmo de sempre, sign(X @ w_)\nmodel.fit(X, y)\ny_pred = model.predict(X)\nprint(f\"Accuracy: {accuracy_score(y, y_pred)}\")\n",[130,4206,4207,4212,4217,4222],{"__ignoreMap":76},[133,4208,4209],{"class":135,"line":136},[133,4210,4211],{},"model = NeuralNetwork()  # o mesmo de sempre, sign(X @ w_)\n",[133,4213,4214],{"class":135,"line":77},[133,4215,4216],{},"model.fit(X, y)\n",[133,4218,4219],{"class":135,"line":83},[133,4220,4221],{},"y_pred = model.predict(X)\n",[133,4223,4224],{"class":135,"line":152},[133,4225,4226],{},"print(f\"Accuracy: {accuracy_score(y, y_pred)}\")\n",[495,4228,4229],{},[11,4230,4231,844,4233,74],{},[15,4232,501],{},[15,4234,4235],{},"0.25",[11,4237,4238,4239,4241,4242,4244,4245,4248,4249,192,4251,4253,4254,2385,4256,2385,4258,192,4260,4262,4263,2385,4265,2385,4267,192,4269,4271],{},"Vale entender por que exatamente ",[130,4240,4235],{},", não é um número aleatório qualquer: com 4 classes bem balanceadas, chutar sempre a mesma coisa acerta em média 1 em cada 4, ou seja, 25%. O ",[130,4243,2899],{}," de sempre usa ",[130,4246,4247],{},"sign(X @ w_)",", que só devolve ",[130,4250,191],{},[130,4252,195],{},", dois valores possíveis, nunca ",[130,4255,573],{},[130,4257,572],{},[130,4259,1275],{},[130,4261,1286],{},". Comparar isso contra um rótulo que pode ser ",[130,4264,573],{},[130,4266,572],{},[130,4268,1275],{},[130,4270,1286],{}," é comparar coisas de naturezas diferentes. O modelo não está \"quase acertando\", ele literalmente não consegue expressar 3 das 4 respostas possíveis. A acurácia de 0.25 é, na prática, o mesmo nível de um chute cego.",[99,4273,4275],{"id":4274},"one-hot-cada-classe-vira-sua-própria-coluna","One-hot: cada classe vira sua própria coluna",[124,4277,4279],{"className":126,"code":4278,"language":128,"meta":76,"style":76},"y_hot = np.zeros((y_train.shape[0], len(set(y_train))), dtype=int)\nfor i, label in enumerate(list(set(y_train))):\n  idxs = np.where(y_train == label)[0]\n  y_hot[idxs, i] = 1\n",[130,4280,4281,4286,4291,4296],{"__ignoreMap":76},[133,4282,4283],{"class":135,"line":136},[133,4284,4285],{},"y_hot = np.zeros((y_train.shape[0], len(set(y_train))), dtype=int)\n",[133,4287,4288],{"class":135,"line":77},[133,4289,4290],{},"for i, label in enumerate(list(set(y_train))):\n",[133,4292,4293],{"class":135,"line":83},[133,4294,4295],{},"  idxs = np.where(y_train == label)[0]\n",[133,4297,4298],{"class":135,"line":152},[133,4299,4300],{},"  y_hot[idxs, i] = 1\n",[11,4302,4303,4304,2385,4306,2385,4308,192,4310,4312,4313,4315,4316,4318,4319,4322],{},"Em vez de um rótulo escalar (",[130,4305,573],{},[130,4307,572],{},[130,4309,1275],{},[130,4311,1286],{},"), cada exemplo vira uma linha com um ",[130,4314,572],{}," na coluna da sua classe e ",[130,4317,573],{}," nas outras. O professor confirma que isso bate exatamente com o ",[130,4320,4321],{},"LabelBinarizer"," do scikit-learn:",[124,4324,4326],{"className":126,"code":4325,"language":128,"meta":76,"style":76},"from sklearn.preprocessing import LabelBinarizer\nlb = LabelBinarizer()\ny_hot = lb.fit_transform(y_train)\n",[130,4327,4328,4333,4338],{"__ignoreMap":76},[133,4329,4330],{"class":135,"line":136},[133,4331,4332],{},"from sklearn.preprocessing import LabelBinarizer\n",[133,4334,4335],{"class":135,"line":77},[133,4336,4337],{},"lb = LabelBinarizer()\n",[133,4339,4340],{"class":135,"line":83},[133,4341,4342],{},"y_hot = lb.fit_transform(y_train)\n",[11,4344,4345,4346,74],{},"Mesma matriz, duas implementações, ",[20,4347,4348],{"href":2220},"o mesmo tipo de conferência \"bate com a ferramenta profissional\" que já apareceu antes nessa playlist",[99,4350,4352,4353,4355,4356],{"id":4351},"a-correção-peso-vira-matriz-sign-vira-argmax","A correção: peso vira matriz, ",[130,4354,2334],{}," vira ",[130,4357,4358],{},"argmax",[124,4360,4362],{"className":126,"code":4361,"language":128,"meta":76,"style":76},"class SGD(TrainingAlgorithm):\n  def get_w(self, X, y):\n    self.w_ = np.random.random(size=(X.shape[1], y.shape[1]))\n    for _ in range(self.max_iter):\n      y_pred = X @ self.w_\n      self.w_ += self.learning_rate * self.cost_function.get_gradient(X, y, y_pred)\n    return self.w_\n\nclass NeuralNetwork(BaseEstimator, ClassifierMixin):\n  def predict(self, X):\n    X = include_bias(X)\n    logits = X @ self.w_\n    idxs = np.argmax(logits, axis=1)\n    return np.array([self.labels[idx] for idx in idxs])\n",[130,4363,4364,4369,4373,4378,4382,4386,4391,4396,4400,4404,4408,4412,4417,4422],{"__ignoreMap":76},[133,4365,4366],{"class":135,"line":136},[133,4367,4368],{},"class SGD(TrainingAlgorithm):\n",[133,4370,4371],{"class":135,"line":77},[133,4372,2810],{},[133,4374,4375],{"class":135,"line":83},[133,4376,4377],{},"    self.w_ = np.random.random(size=(X.shape[1], y.shape[1]))\n",[133,4379,4380],{"class":135,"line":152},[133,4381,303],{},[133,4383,4384],{"class":135,"line":158},[133,4385,2435],{},[133,4387,4388],{"class":135,"line":164},[133,4389,4390],{},"      self.w_ += self.learning_rate * self.cost_function.get_gradient(X, y, y_pred)\n",[133,4392,4393],{"class":135,"line":300},[133,4394,4395],{},"    return self.w_\n",[133,4397,4398],{"class":135,"line":306},[133,4399,287],{"emptyLinePlaceholder":82},[133,4401,4402],{"class":135,"line":312},[133,4403,2842],{},[133,4405,4406],{"class":135,"line":318},[133,4407,1603],{},[133,4409,4410],{"class":135,"line":324},[133,4411,1828],{},[133,4413,4414],{"class":135,"line":330},[133,4415,4416],{},"    logits = X @ self.w_\n",[133,4418,4419],{"class":135,"line":336},[133,4420,4421],{},"    idxs = np.argmax(logits, axis=1)\n",[133,4423,4424],{"class":135,"line":342},[133,4425,4426],{},"    return np.array([self.labels[idx] for idx in idxs])\n",[11,4428,4429,4430,4433,4434,4437,4438,188,4441,4444,4445,4448,4449,2916,4451,4453,4454,4457],{},"A mudança que resolve tudo: ",[130,4431,4432],{},"self.w_"," deixa de ser um vetor (",[130,4435,4436],{},"(features,)",") e vira uma ",[15,4439,4440],{},"matriz",[130,4442,4443],{},"(features, classes)","), uma coluna de pesos por classe. ",[130,4446,4447],{},"X @ self.w_"," agora devolve, pra cada ponto, 4 números (um \"quão confiante\" por classe), não mais 1 só. E a previsão final troca ",[130,4450,1484],{},[130,4452,4358],{},": em vez de perguntar \"positivo ou negativo?\", pergunta \"qual das 4 colunas teve o maior valor?\". Isso é exatamente a arquitetura de camada de saída múltipla que o Aggarwal descreve pra classificação categórica: um peso por classe, e a decisão final é qual delas \"venceu\". A única peça que falta pra virar a versão completa dele (com ",[130,4455,4456],{},"softmax",", transformando os 4 números em probabilidades que somam 1) é a normalização. Aqui o modelo só compara os números crus, sem transformar em probabilidade, mas o vencedor do argmax já não muda.",[495,4459,4460],{},[11,4461,4462,844,4464,74],{},[15,4463,501],{},[15,4465,540],{},[99,4467,4469],{"id":4468},"interativo-as-quatro-regiões-de-decisão","Interativo: as quatro regiões de decisão",[11,4471,4472,4473,4476],{},"Reconstruí o mesmo dataset (",[130,4474,4475],{},"make_blobs",", mesmos 4 centros) e treinei a versão com matriz de peso. Cada cor de fundo é a região onde aquela classe vence o argmax.",[4478,4479],"multiclass-region-chart",{":classes":4480,":points":4481,":weights":4482,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"class0-label":4483,"class1-label":4484,"class2-label":4485,"class3-label":4486,"x-label":576,"y-label":577},"[1, 1, 3, 2, 0, 1, 2, 1, 2, 0, 0, 0, 0, 1, 2, 1, 3, 3, 0, 3, 1, 2, 3, 3, 1, 3, 3, 2, 3, 0, 2, 0, 1, 1, 2, 0, 2, 2, 0, 3]","[[0.6968, 0.1669], [0.7174, 0.2268], [0.785, 0.741], [0.2956, 0.8119], [0.2277, 0.2062], [0.7398, 0.2731], [0.1953, 0.8597], [0.8825, 0.2077], [0.242, 0.791], [0.2, 0.1123], [0.1687, 0.1914], [0.2845, 0.1767], [0.1606, 0.2001], [0.8525, 0.1792], [0.2051, 0.8126], [0.7629, 0.2536], [0.795, 0.885], [0.8166, 0.8368], [0.1879, 0.1273], [0.8749, 0.7859], [0.7797, 0.0856], [0.1146, 0.7098], [0.7403, 0.7475], [0.7904, 0.7111], [0.8883, 0.1835], [0.7816, 0.7047], [0.7808, 0.7555], [0.1283, 0.8251], [0.8054, 0.8719], [0.2016, 0.2204], [0.2135, 0.7738], [0.2253, 0.1869], [0.7806, 0.3015], [0.7977, 0.1275], [0.1934, 0.7845], [0.2509, 0.23], [0.2284, 0.7624], [0.2192, 0.9124], [0.2137, 0.1237], [0.8752, 0.7894]]","[[1.0602, -0.8069, -0.8239], [0.2232, 0.812, -0.7759], [0.2644, -0.8451, 0.8351], [-0.5478, 0.84, 0.7647]]","Classe 0","Classe 1","Classe 2","Classe 3",[11,4488,4489],{},"Repara nas quatro fronteiras se encontrando perto do meio do gráfico, dividindo o plano em quatro fatias, uma por classe. Cada blob cai limpinho dentro da cor certa.",[99,4491,595],{"id":594},[597,4493,4494,4502],{},[600,4495,4496],{},[603,4497,4498,4500],{},[606,4499,609],{"align":608},[606,4501,612],{"align":608},[614,4503,4504,4517,4525],{},[603,4505,4506,4511],{},[619,4507,4508,4510],{"align":608},[130,4509,1484],{}," classifica em duas classes",[619,4512,4513,4514,4516],{"align":608},"Com mais de duas classes, ",[130,4515,1484],{}," estruturalmente não tem como funcionar, sobra só 2 saídas possíveis pra N classes",[603,4518,4519,4522],{},[619,4520,4521],{"align":608},"One-hot encoding transforma rótulo categórico em vetor",[619,4523,4524],{"align":608},"Isso não é só conveniência de formato, é o que permite o peso virar matriz (uma coluna por classe)",[603,4526,4527,4532],{},[619,4528,4529,4531],{"align":608},[130,4530,4358],{}," escolhe o maior valor",[619,4533,4534,4535,4537],{"align":608},"É a generalização direta de ",[130,4536,1484],{}," (que é basicamente \"argmax entre 2 opções: positivo ou negativo\") pra qualquer número de classes",[99,4539,644],{"id":643},[11,4541,4542,4543,4546,4547,4550],{},"Testei a mesma ideia (one-hot + matriz de peso + argmax) no Wine (",[130,4544,4545],{},"load_wine",", 3 castas de uva, 13 variáveis químicas), com um detalhe a mais: as variáveis aqui têm escalas bem diferentes entre si, ",[20,4548,4549],{"href":2220},"o mesmo problema já visto antes nessa playlist",", então normalizei antes de treinar.",[124,4552,4554],{"className":126,"code":4553,"language":128,"meta":76,"style":76},"X_train_s = StandardScaler().fit_transform(X_train)\n",[130,4555,4556],{"__ignoreMap":76},[133,4557,4558],{"class":135,"line":136},[133,4559,4553],{},[11,4561,4562,4563,4565,4566,74],{},"Mesmo normalizado, ",[130,4564,2684],{}," (o padrão do notebook) ainda divergiu com 13 variáveis, e precisei cair pra ",[130,4567,2700],{},[597,4569,4570,4580],{},[600,4571,4572],{},[603,4573,4574,4576,4578],{},[606,4575,2099],{"align":608},[606,4577,1412],{"align":707},[606,4579,1415],{"align":707},[614,4581,4582,4596],{},[603,4583,4584,4590,4593],{},[619,4585,4586,4587,4589],{"align":608},"Ingênua (",[130,4588,2334],{},", rótulo escalar)",[619,4591,4592],{"align":707},"0.403",[619,4594,4595],{"align":707},"-",[603,4597,4598,4603,4605],{},[619,4599,4600,4601],{"align":608},"One-hot + matriz de peso + ",[130,4602,4358],{},[619,4604,540],{"align":707},[619,4606,4607],{"align":707},[15,4608,4609],{},"0.9815",[11,4611,4612,4613,192,4615,4617,4618,4620,4621,192,4623,4625],{},"A versão ingênua nem chega nos 3 valores de classe possíveis (só consegue prever ",[130,4614,191],{},[130,4616,195],{},", nunca a classe ",[130,4619,1275],{},"), então mesmo o número de 0.403 já é enganoso, ele conta como \"acerto\" qualquer caso em que o rótulo por coincidência já era ",[130,4622,191],{},[130,4624,572],{},". A versão com matriz de peso acerta praticamente tudo, treino e teste, confirmando que o mesmo truque que funcionou no dataset sintético de 4 blobs generaliza pra um problema real de classificação multiclasse.",[738,4627,740],{},{"title":76,"searchDepth":77,"depth":77,"links":4629},[4630,4631,4632,4633,4635,4636,4637],{"id":4138,"depth":77,"text":4139},{"id":4200,"depth":77,"text":4201},{"id":4274,"depth":77,"text":4275},{"id":4351,"depth":77,"text":4634},"A correção: peso vira matriz, sign vira argmax",{"id":4468,"depth":77,"text":4469},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"Aula 3c: reusar o classificador binário direto num problema de 4 classes trava exatamente na acurácia de chute aleatório, 25%. A correção é generalizar peso de vetor pra matriz, um conjunto de pesos por classe, e trocar sign() por argmax.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse",{"title":4130,"description":4638},"pt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse",[4644,4645,4358],"multiclasse","one-hot-encoding","e62TbMaQwvX5luUffwTAIOE2j38Dibh3K09F-R-G58E",{"id":4648,"title":4649,"body":4650,"cover":3,"date":750,"description":5012,"extension":80,"meta":5013,"navigation":82,"order":300,"path":5014,"playlist":754,"seo":5015,"status":86,"stem":5016,"tags":5017,"__hash__":5021},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fxor-o-limite-do-perceptron.md","XOR: Onde um Neurônio Só Bate a Cabeça na Parede",{"type":8,"value":4651,"toc":5003},[4652,4655,4659,4698,4723,4727,4746,4758,4776,4788,4799,4803,4821,4834,4846,4863,4867,4873,4878,4881,4885,4892,4899,4901,4937,4939,4947,4962,4998,5001],[11,4653,4654],{},"Aula 3d, e é a última que existe no repositório até hoje. Antes de fechar essa leva de posts, o professor deixa um gancho que é quase poético: o mesmo perceptron que resolveu tudo até aqui, sem esforço, trava duro num problema de 4 pontos.",[99,4656,4658],{"id":4657},"o-dataset-portas-lógicas-geometricamente","O dataset: portas lógicas, geometricamente",[124,4660,4662],{"className":126,"code":4661,"language":128,"meta":76,"style":76},"def createLogicalDataset(n=40, func=lambda a, b: bool(a) or bool(b)):\n  X, y = make_blobs(n_samples=n,\n                    centers=[[0.2,0.2], [0.8, 0.2], [0.2, 0.8], [0.8, 0.8]],\n                    n_features=2,\n                    cluster_std=0.05,\n                    center_box=(0,1))\n  y = np.array([func(a>0.5, b>0.5) for a, b in X], dtype=int)\n  return X, y\n",[130,4663,4664,4669,4673,4677,4681,4685,4689,4694],{"__ignoreMap":76},[133,4665,4666],{"class":135,"line":136},[133,4667,4668],{},"def createLogicalDataset(n=40, func=lambda a, b: bool(a) or bool(b)):\n",[133,4670,4671],{"class":135,"line":77},[133,4672,4154],{},[133,4674,4675],{"class":135,"line":83},[133,4676,4159],{},[133,4678,4679],{"class":135,"line":152},[133,4680,4164],{},[133,4682,4683],{"class":135,"line":158},[133,4684,4169],{},[133,4686,4687],{"class":135,"line":164},[133,4688,4174],{},[133,4690,4691],{"class":135,"line":300},[133,4692,4693],{},"  y = np.array([func(a>0.5, b>0.5) for a, b in X], dtype=int)\n",[133,4695,4696],{"class":135,"line":306},[133,4697,167],{},[11,4699,4700,4704,4705,2385,4708,2385,4711,4714,4715,4718,4719,4722],{},[20,4701,4703],{"href":4702},"\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse","O mesmo dataset de 4 blobs do post anterior",", só que agora o rótulo vem de aplicar uma função lógica (",[130,4706,4707],{},"or",[130,4709,4710],{},"and",[130,4712,4713],{},"!=",") em cima de \"esse ponto está à direita de 0.5?\" e \"esse ponto está acima de 0.5?\". Cada blob vira, na prática, um valor de porta lógica: canto inferior-esquerdo é ",[130,4716,4717],{},"(falso, falso)",", inferior-direito é ",[130,4720,4721],{},"(verdadeiro, falso)",", e assim por diante.",[99,4724,4726],{"id":4725},"or-e-and-sem-drama","OR e AND: sem drama",[124,4728,4730],{"className":126,"code":4729,"language":128,"meta":76,"style":76},"X, y = createLogicalDataset(func=lambda a, b: bool(a) or bool(b))\nmodel = NeuralNetwork()\nmodel.fit(X, y)\n",[130,4731,4732,4737,4742],{"__ignoreMap":76},[133,4733,4734],{"class":135,"line":136},[133,4735,4736],{},"X, y = createLogicalDataset(func=lambda a, b: bool(a) or bool(b))\n",[133,4738,4739],{"class":135,"line":77},[133,4740,4741],{},"model = NeuralNetwork()\n",[133,4743,4744],{"class":135,"line":83},[133,4745,4216],{},[495,4747,4748],{},[11,4749,4750,844,4753,848,4755,74],{},[15,4751,4752],{},"Saída (OR):",[15,4754,540],{},[130,4756,4757],{},"[-1.14, 1.72, 1.57]",[124,4759,4761],{"className":126,"code":4760,"language":128,"meta":76,"style":76},"X, y = createLogicalDataset(func=lambda a, b: bool(a) and bool(b))\nmodel = NeuralNetwork()\nmodel.fit(X, y)\n",[130,4762,4763,4768,4772],{"__ignoreMap":76},[133,4764,4765],{"class":135,"line":136},[133,4766,4767],{},"X, y = createLogicalDataset(func=lambda a, b: bool(a) and bool(b))\n",[133,4769,4770],{"class":135,"line":77},[133,4771,4741],{},[133,4773,4774],{"class":135,"line":83},[133,4775,4216],{},[495,4777,4778],{},[11,4779,4780,844,4783,848,4785,74],{},[15,4781,4782],{},"Saída (AND):",[15,4784,540],{},[130,4786,4787],{},"[-2.16, 1.55, 1.79]",[11,4789,4790,4791,4794,4795,4798],{},"As duas portas são linearmente separáveis: em OR, uma única reta separa o blob inferior-esquerdo (o único ",[130,4792,4793],{},"falso",") dos outros três. Em AND, a mesma coisa, só que isolando o blob superior-direito (o único ",[130,4796,4797],{},"verdadeiro","). Nada que esse perceptron não tenha feito o post inteiro até aqui.",[99,4800,4802],{"id":4801},"xor-a-mesma-receita-o-mesmo-código-e-trava","XOR: a mesma receita, o mesmo código, e trava",[124,4804,4806],{"className":126,"code":4805,"language":128,"meta":76,"style":76},"X, y = createLogicalDataset(func=lambda a, b: bool(a) != bool(b))\nmodel = NeuralNetwork()\nmodel.fit(X, y)\n",[130,4807,4808,4813,4817],{"__ignoreMap":76},[133,4809,4810],{"class":135,"line":136},[133,4811,4812],{},"X, y = createLogicalDataset(func=lambda a, b: bool(a) != bool(b))\n",[133,4814,4815],{"class":135,"line":77},[133,4816,4741],{},[133,4818,4819],{"class":135,"line":83},[133,4820,4216],{},[495,4822,4823],{},[11,4824,4825,844,4828,848,4831,74],{},[15,4826,4827],{},"Saída (XOR):",[15,4829,4830],{},"0.5",[130,4832,4833],{},"[0.006, -0.011, -0.002]",[11,4835,4836,4837,4839,4840,4842,4843,1930],{},"Acurácia de 0.5 com 2 classes é exatamente o nível de jogar uma moeda. E repara nos pesos: praticamente zero nos três, o modelo essencialmente desistiu, encolhendo o vetor de peso até quase nada em vez de convergir pra qualquer coisa útil. Reproduzi isso eu mesmo, com dataset seedado, e bati na mesma parede: OR e AND em ",[130,4838,540],{},", XOR travado em ",[130,4841,4830],{},", pesos igualmente murchos (",[130,4844,4845],{},"[-0.025, -0.066, 0.118]",[11,4847,4848,4849,4851,4852,4855,4856,4858,4859,4862],{},"O motivo é geométrico, e dá pra ver sem fórmula nenhuma: XOR marca como ",[130,4850,4797],{}," os dois cantos ",[15,4853,4854],{},"opostos"," na diagonal (inferior-direito e superior-esquerdo) e como ",[130,4857,4793],{}," os outros dois cantos, também opostos entre si (inferior-esquerdo e superior-direito). Não existe reta nenhuma que separe \"os dois cantos de uma diagonal\" dos \"dois cantos da outra diagonal\": qualquer reta que eu desenhar corta uma das diagonais ao meio, misturando as duas classes dos dois lados. XOR ",[15,4860,4861],{},"não é linearmente separável",", ponto final, e nenhuma quantidade de treino vai mudar isso, porque o problema não está no algoritmo de aprendizado, está no que um único neurônio, com uma única fronteira reta, consegue representar.",[99,4864,4866],{"id":4865},"interativo-assiste-o-perceptron-nunca-convergir","Interativo: assiste o perceptron nunca convergir",[11,4868,4869,4870,4872],{},"Mesmo dataset seedado de cima, agora no XOR. Clica em \"Processar próximo ponto\" várias vezes: repara que, diferente de todo componente anterior nessa playlist, esse aqui ",[15,4871,3411],{}," mostra a mensagem de convergência. Sempre vai sobrar pelo menos um ponto do lado errado, não importa quanto eu clique.",[568,4874],{":classes":4875,":points":4481,":update-bias":1051,":x-max":572,":x-min":573,":y-max":572,":y-min":573,"negative-label":4876,"positive-label":4877,"x-label":576,"y-label":577},"[1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, -1, -1]","falso (XOR)","verdadeiro (XOR)",[11,4879,4880],{},"Fica à vontade pra clicar bastante. A fronteira vai girar e deslizar sem parar, tentando achar um lugar que não existe.",[99,4882,4884],{"id":4883},"o-que-isso-significa-e-o-que-vem-depois","O que isso significa (e o que vem depois)",[11,4886,4887,4888,4891],{},"Esse resultado não é uma curiosidade isolada, é historicamente ",[15,4889,4890],{},"o"," limite que definiu os primeiros anos de rede neural: um único neurônio, com uma fronteira de decisão linear, tem um teto de expressividade, e XOR fica acima desse teto. A saída, e é aqui que essa playlist ainda não chegou, é empilhar neurônios: um MLP (rede de múltiplas camadas) consegue resolver XOR combinando duas fronteiras lineares numa camada escondida antes de decidir a saída final. Guardei dois papers exatamente pra esse momento, na pasta de referência dessa playlist: a tese de doutorado de Paul Werbos (1974), a derivação mais antiga que se conhece do algoritmo de retropropagação (backpropagation), e o paper de Rumelhart, Hinton e Williams (Nature, 1986), o que de fato popularizou esse algoritmo e destravou o treino prático de MLPs.",[11,4893,4894,4895,4898],{},"O repositório do professor ainda não tem esse notebook. As 3 aulas que cobri até aqui (perceptron, Adaline, funções de custo, multiclasse) são tudo que existe publicado até o momento, e o gancho do XOR é exatamente onde a matéria dele também está agora. ",[20,4896,4897],{"href":72},"Como já falei lá no primeiro post dessa playlist",", essa é uma playlist viva: assim que o professor publicar a aula de MLP e retropropagação, volto aqui pra continuar de onde parei, com Werbos e Rumelhart-Hinton-Williams já esperando pra entrar em cena.",[99,4900,595],{"id":594},[597,4902,4903,4911],{},[600,4904,4905],{},[603,4906,4907,4909],{},[606,4908,609],{"align":608},[606,4910,612],{"align":608},[614,4912,4913,4921,4929],{},[603,4914,4915,4918],{},[619,4916,4917],{"align":608},"Perceptron separa qualquer coisa dada tempo suficiente",[619,4919,4920],{"align":608},"Só separa o que é linearmente separável: o XOR nunca converge, não importa quanto eu treine",[603,4922,4923,4926],{},[619,4924,4925],{"align":608},"Pesos crescem quando o modelo está errado",[619,4927,4928],{"align":608},"Em XOR, os pesos encolhem até quase zero, o modelo desiste em vez de continuar tentando algo que não existe",[603,4930,4931,4934],{},[619,4932,4933],{"align":608},"Rede neural é \"só\" um neurônio com pesos",[619,4935,4936],{"align":608},"Um neurônio sozinho tem teto de expressividade: empilhar camadas (MLP) é o que existe pra furar esse teto",[99,4938,644],{"id":643},[11,4940,4941,4942,4946],{},"Não tem um dataset real clássico de \"XOR\" pra buscar (é uma função lógica, não um fenômeno do mundo), então a aplicação prática aqui é mostrar que a mesma barreira de separabilidade linear aparece em dado real, não só em portas lógicas de brinquedo. Uso o dataset de duas luas ",[20,4943,4945],{"href":4944},"\u002Fplaylists\u002Fpattern-recognition\u002Fdbscan-semi-supervised","que já apareceu no Reconhecimento de Padrões",", sabidamente não-linearmente-separável.",[124,4948,4950],{"className":126,"code":4949,"language":128,"meta":76,"style":76},"from sklearn.datasets import make_moons\nX, y = make_moons(n_samples=300, noise=0.05, random_state=42)\n",[130,4951,4952,4957],{"__ignoreMap":76},[133,4953,4954],{"class":135,"line":136},[133,4955,4956],{},"from sklearn.datasets import make_moons\n",[133,4958,4959],{"class":135,"line":77},[133,4960,4961],{},"X, y = make_moons(n_samples=300, noise=0.05, random_state=42)\n",[597,4963,4964,4974],{},[600,4965,4966],{},[603,4967,4968,4971],{},[606,4969,4970],{"align":608},"Modelo",[606,4972,4973],{"align":707},"Acurácia",[614,4975,4976,4984],{},[603,4977,4978,4981],{},[619,4979,4980],{"align":608},"Perceptron (fronteira linear)",[619,4982,4983],{"align":707},"0.867",[603,4985,4986,4994],{},[619,4987,4988,4989,4993],{"align":608},"KNN (K=5, ",[20,4990,4992],{"href":4991},"\u002Fplaylists\u002Fpattern-recognition\u002Fknn-classifier","já visto no Reconhecimento de Padrões"," como contraste)",[619,4995,4996],{"align":707},[15,4997,540],{},[11,4999,5000],{},"O perceptron não trava tão feio quanto no XOR (0.867 não é 0.5, porque as duas luas têm uma separação aproximadamente linear na maior parte do espaço, só a curvatura nas pontas engana uma reta), mas fica bem atrás do KNN, que não tem esse teto porque não depende de uma fronteira reta única. É o mesmo limite estrutural do XOR, só que numa versão mais suave: qualquer problema onde a fronteira verdadeira não é uma reta vai expor essa limitação, cedo ou tarde.",[738,5002,740],{},{"title":76,"searchDepth":77,"depth":77,"links":5004},[5005,5006,5007,5008,5009,5010,5011],{"id":4657,"depth":77,"text":4658},{"id":4725,"depth":77,"text":4726},{"id":4801,"depth":77,"text":4802},{"id":4865,"depth":77,"text":4866},{"id":4883,"depth":77,"text":4884},{"id":594,"depth":77,"text":595},{"id":643,"depth":77,"text":644},"Aula 3d, a última que o professor publicou até agora: o mesmo perceptron que aprende OR e AND sem esforço trava em XOR, preso em 50% de acurácia, pesos praticamente zerados. Fecho essa primeira leva da playlist exatamente onde a matéria de verdade também está: no limiar antes do MLP.",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fxor-o-limite-do-perceptron",{"title":4649,"description":5012},"pt\u002Fplaylists\u002Fneural-networks\u002Fxor-o-limite-do-perceptron",[5018,5019,5020],"xor","separabilidade-linear","mlp","EMbbL6qKtxsH-Yy2xR0WPOX-m5CgV0CYWOpfAuc2COE",1787338982559]