[{"data":1,"prerenderedAt":1035},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fneural-networks\u002Fbackprop-qualquer-profundidade":3,"post-pt-neural-networks-backprop-qualquer-profundidade":4},"\u002Fen\u002Fplaylists\u002Fneural-networks\u002Fbackprop-qualquer-profundidade",{"id":5,"title":6,"body":7,"cover":1019,"date":1020,"description":1021,"extension":1022,"meta":1023,"navigation":89,"order":111,"path":1024,"playlist":1025,"seo":1026,"status":1027,"stem":1028,"tags":1029,"__hash__":1034},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fbackprop-qualquer-profundidade.md","Generalizando pra Qualquer Profundidade: o Mesmo Loop Resolve Tudo",{"type":8,"value":9,"toc":1007},"minimark",[10,32,37,316,371,378,428,455,495,501,576,636,678,682,694,714,729,735,753,760,764,796,821,824,836,855,859,904,908,930,971,1003],[11,12,13,18,19,23,24,27,28,31],"p",{},[14,15,17],"a",{"href":16},"\u002Fplaylists\u002Fneural-networks\u002Fcamada-escondida-tambem-aprende","No post passado",", o pulo do gato apareceu escrito à mão, pra exatamente uma camada escondida: calcular o erro da saída, repartir ele entre os dois neurônios escondidos, multiplicar pela folga de cada um. Funciona, mas só serve pra essa arquitetura específica. Essa aula pega esse mesmo pulo do gato e reescreve ele como um laço, um ",[20,21,22],"code",{},"for"," que roda uma vez por camada, pra frente no ",[20,25,26],{},"forward"," e de trás pra frente no ",[20,29,30],{},"backward",". O resultado funciona pra zero camadas escondidas, pra uma, pra duas, pra quantas você quiser, sem trocar uma linha de código.",[33,34,36],"h2",{"id":35},"de-duas-variáveis-fixas-pra-duas-listas","De duas variáveis fixas pra duas listas",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-python shiki shiki-themes github-light github-dark","class MultiLayer(BaseEstimator, ClassifierMixin):\n  def __init__(self, n_hidden=[2], max_iter=20000, learning_rate=0.001):\n    self.max_iter = max_iter\n    self.learning_rate = learning_rate\n    self.n_hidden = n_hidden\n    self.activation = np.tanh\n\n  def forward(self, X):\n    self.A = []\n    self.Z = []\n    AUX = X.copy()\n    for W in self.Ws:\n      self.A.append(include_bias(AUX))\n      self.Z.append(self.A[-1] @ W)\n      AUX = self.activation(self.Z[-1])\n    return AUX\n\n  def backward(self, y, y_pred):\n    grads = []\n    output_delta = y_pred - y\n    output_grad = self.A[-1].T @ output_delta\n    grads.append(output_grad)\n    for i in range(len(self.Ws)-1, 0, -1):\n      tanh_grad = (1 - np.tanh(self.Z[i-1])**2)\n      input_delta = output_delta @ self.Ws[i][1:].T * tanh_grad\n      grad = self.A[i-1].T @ input_delta\n      grads.insert(0, grad)\n      output_delta = input_delta.copy()\n    for i in range(len(self.Ws)):\n      self.Ws[i] -= self.learning_rate * grads[i]\n    return self\n\n  def fit(self, X, y):\n    self.Ws = []\n    previous_output = X.shape[1]\n    for n in self.n_hidden:\n      self.Ws.append(np.random.randn(previous_output+1, n))\n      previous_output = n\n    if len(y.shape) == 1:\n      y = y.reshape(-1,1)\n    self.Ws.append(np.random.randn(previous_output+1, y.shape[1]))\n    for _ in range(self.max_iter):\n      logits = self.forward(X)\n      self.backward(y, logits)\n    return self\n","python","",[20,45,46,54,60,66,72,78,84,91,97,103,109,115,121,127,133,139,145,150,156,162,168,174,180,186,192,198,204,210,216,222,228,234,239,245,251,257,263,269,275,281,287,293,299,305,311],{"__ignoreMap":43},[47,48,51],"span",{"class":49,"line":50},"line",1,[47,52,53],{},"class MultiLayer(BaseEstimator, ClassifierMixin):\n",[47,55,57],{"class":49,"line":56},2,[47,58,59],{},"  def __init__(self, n_hidden=[2], max_iter=20000, learning_rate=0.001):\n",[47,61,63],{"class":49,"line":62},3,[47,64,65],{},"    self.max_iter = max_iter\n",[47,67,69],{"class":49,"line":68},4,[47,70,71],{},"    self.learning_rate = learning_rate\n",[47,73,75],{"class":49,"line":74},5,[47,76,77],{},"    self.n_hidden = n_hidden\n",[47,79,81],{"class":49,"line":80},6,[47,82,83],{},"    self.activation = np.tanh\n",[47,85,87],{"class":49,"line":86},7,[47,88,90],{"emptyLinePlaceholder":89},true,"\n",[47,92,94],{"class":49,"line":93},8,[47,95,96],{},"  def forward(self, X):\n",[47,98,100],{"class":49,"line":99},9,[47,101,102],{},"    self.A = []\n",[47,104,106],{"class":49,"line":105},10,[47,107,108],{},"    self.Z = []\n",[47,110,112],{"class":49,"line":111},11,[47,113,114],{},"    AUX = X.copy()\n",[47,116,118],{"class":49,"line":117},12,[47,119,120],{},"    for W in self.Ws:\n",[47,122,124],{"class":49,"line":123},13,[47,125,126],{},"      self.A.append(include_bias(AUX))\n",[47,128,130],{"class":49,"line":129},14,[47,131,132],{},"      self.Z.append(self.A[-1] @ W)\n",[47,134,136],{"class":49,"line":135},15,[47,137,138],{},"      AUX = self.activation(self.Z[-1])\n",[47,140,142],{"class":49,"line":141},16,[47,143,144],{},"    return AUX\n",[47,146,148],{"class":49,"line":147},17,[47,149,90],{"emptyLinePlaceholder":89},[47,151,153],{"class":49,"line":152},18,[47,154,155],{},"  def backward(self, y, y_pred):\n",[47,157,159],{"class":49,"line":158},19,[47,160,161],{},"    grads = []\n",[47,163,165],{"class":49,"line":164},20,[47,166,167],{},"    output_delta = y_pred - y\n",[47,169,171],{"class":49,"line":170},21,[47,172,173],{},"    output_grad = self.A[-1].T @ output_delta\n",[47,175,177],{"class":49,"line":176},22,[47,178,179],{},"    grads.append(output_grad)\n",[47,181,183],{"class":49,"line":182},23,[47,184,185],{},"    for i in range(len(self.Ws)-1, 0, -1):\n",[47,187,189],{"class":49,"line":188},24,[47,190,191],{},"      tanh_grad = (1 - np.tanh(self.Z[i-1])**2)\n",[47,193,195],{"class":49,"line":194},25,[47,196,197],{},"      input_delta = output_delta @ self.Ws[i][1:].T * tanh_grad\n",[47,199,201],{"class":49,"line":200},26,[47,202,203],{},"      grad = self.A[i-1].T @ input_delta\n",[47,205,207],{"class":49,"line":206},27,[47,208,209],{},"      grads.insert(0, grad)\n",[47,211,213],{"class":49,"line":212},28,[47,214,215],{},"      output_delta = input_delta.copy()\n",[47,217,219],{"class":49,"line":218},29,[47,220,221],{},"    for i in range(len(self.Ws)):\n",[47,223,225],{"class":49,"line":224},30,[47,226,227],{},"      self.Ws[i] -= self.learning_rate * grads[i]\n",[47,229,231],{"class":49,"line":230},31,[47,232,233],{},"    return self\n",[47,235,237],{"class":49,"line":236},32,[47,238,90],{"emptyLinePlaceholder":89},[47,240,242],{"class":49,"line":241},33,[47,243,244],{},"  def fit(self, X, y):\n",[47,246,248],{"class":49,"line":247},34,[47,249,250],{},"    self.Ws = []\n",[47,252,254],{"class":49,"line":253},35,[47,255,256],{},"    previous_output = X.shape[1]\n",[47,258,260],{"class":49,"line":259},36,[47,261,262],{},"    for n in self.n_hidden:\n",[47,264,266],{"class":49,"line":265},37,[47,267,268],{},"      self.Ws.append(np.random.randn(previous_output+1, n))\n",[47,270,272],{"class":49,"line":271},38,[47,273,274],{},"      previous_output = n\n",[47,276,278],{"class":49,"line":277},39,[47,279,280],{},"    if len(y.shape) == 1:\n",[47,282,284],{"class":49,"line":283},40,[47,285,286],{},"      y = y.reshape(-1,1)\n",[47,288,290],{"class":49,"line":289},41,[47,291,292],{},"    self.Ws.append(np.random.randn(previous_output+1, y.shape[1]))\n",[47,294,296],{"class":49,"line":295},42,[47,297,298],{},"    for _ in range(self.max_iter):\n",[47,300,302],{"class":49,"line":301},43,[47,303,304],{},"      logits = self.forward(X)\n",[47,306,308],{"class":49,"line":307},44,[47,309,310],{},"      self.backward(y, logits)\n",[47,312,314],{"class":49,"line":313},45,[47,315,233],{},[11,317,318,319,322,323,326,327,330,331,334,335,338,339,343,344,347,348,351,352,355,356,359,360,363,364,366,367,370],{},"Repara no ",[20,320,321],{},"__init__",": ",[20,324,325],{},"ihw"," e ",[20,328,329],{},"how"," ",[14,332,333],{"href":16},"do post passado"," viraram uma coisa só, ",[20,336,337],{},"self.Ws",", uma ",[340,341,342],"strong",{},"lista"," de matrizes, uma por camada (todas as escondidas mais a de saída). ",[20,345,346],{},"n_hidden"," também virou lista: ",[20,349,350],{},"[2]"," significa uma camada escondida com 2 neurônios, ",[20,353,354],{},"[10, 10]"," significa duas camadas de 10, ",[20,357,358],{},"[]"," significa nenhuma camada escondida. O ",[20,361,362],{},"fit"," monta essa lista percorrendo ",[20,365,346],{}," e empilhando uma matriz de pesos pra cada tamanho, sempre lembrando o ",[20,368,369],{},"previous_output"," pra saber quantas linhas a próxima matriz precisa ter, e no fim adiciona mais uma matriz pra camada de saída.",[372,373,375,377],"h3",{"id":374},"forward-o-mesmo-cálculo-guardado-camada-por-camada",[20,376,26],{},": o mesmo cálculo, guardado camada por camada",[38,379,381],{"className":40,"code":380,"language":42,"meta":43,"style":43},"def forward(self, X):\n  self.A = []\n  self.Z = []\n  AUX = X.copy()\n  for W in self.Ws:\n    self.A.append(include_bias(AUX))\n    self.Z.append(self.A[-1] @ W)\n    AUX = self.activation(self.Z[-1])\n  return AUX\n",[20,382,383,388,393,398,403,408,413,418,423],{"__ignoreMap":43},[47,384,385],{"class":49,"line":50},[47,386,387],{},"def forward(self, X):\n",[47,389,390],{"class":49,"line":56},[47,391,392],{},"  self.A = []\n",[47,394,395],{"class":49,"line":62},[47,396,397],{},"  self.Z = []\n",[47,399,400],{"class":49,"line":68},[47,401,402],{},"  AUX = X.copy()\n",[47,404,405],{"class":49,"line":74},[47,406,407],{},"  for W in self.Ws:\n",[47,409,410],{"class":49,"line":80},[47,411,412],{},"    self.A.append(include_bias(AUX))\n",[47,414,415],{"class":49,"line":86},[47,416,417],{},"    self.Z.append(self.A[-1] @ W)\n",[47,419,420],{"class":49,"line":93},[47,421,422],{},"    AUX = self.activation(self.Z[-1])\n",[47,424,425],{"class":49,"line":99},[47,426,427],{},"  return AUX\n",[11,429,430,433,434,437,438,326,441,444,445,448,449,451,452,454],{},[20,431,432],{},"AUX"," começa como o dado bruto e vai virando, a cada volta do laço, a saída da camada anterior já ativada, exatamente como ",[20,435,436],{},"H"," virava a entrada da camada de saída no post passado, só que agora pode acontecer de novo, e de novo, uma vez por camada. A diferença que importa: ",[20,439,440],{},"self.A",[20,442,443],{},"self.Z"," guardam uma cópia de ",[340,446,447],{},"cada"," entrada-com-bias e ",[340,450,447],{}," pré-ativação ao longo do caminho, não só da última. Isso não é por acaso, é exatamente o que o ",[20,453,30],{}," vai precisar pra repartir o erro por todas as camadas, não só pela última.",[456,457,458],"blockquote",{},[11,459,460,461,464,465,468,469,472,473,476,477,480,481,484,485,487,488,491,492,494],{},"Um detalhe honesto: ",[20,462,463],{},"AUX = self.activation(self.Z[-1])"," roda em ",[340,466,467],{},"toda"," camada dentro do laço, inclusive a última. Isso significa que a saída final também passa por ",[20,470,471],{},"tanh"," agora, diferente do post passado, onde a camada de saída ficava sem ativação nenhuma (",[20,474,475],{},"logits = self.Hb @ self.how",", puro linear). Não muda o sinal da predição (",[20,478,479],{},"sign(tanh(z))"," é sempre igual a ",[20,482,483],{},"sign(z)",", porque ",[20,486,471],{}," é crescente e cruza o zero exatamente onde ",[20,489,490],{},"z"," cruza), mas é uma simplificação que o professor manteve tanto ao generalizar quanto no cálculo do gradiente do ",[20,493,30],{}," logo abaixo.",[372,496,498,500],{"id":497},"backward-o-pulo-do-gato-repetido-de-trás-pra-frente",[20,499,30],{},": o pulo do gato, repetido de trás pra frente",[38,502,504],{"className":40,"code":503,"language":42,"meta":43,"style":43},"def backward(self, y, y_pred):\n  grads = []\n  output_delta = y_pred - y\n  output_grad = self.A[-1].T @ output_delta\n  grads.append(output_grad)\n  for i in range(len(self.Ws)-1, 0, -1):\n    tanh_grad = (1 - np.tanh(self.Z[i-1])**2)\n    input_delta = output_delta @ self.Ws[i][1:].T * tanh_grad\n    grad = self.A[i-1].T @ input_delta\n    grads.insert(0, grad)\n    output_delta = input_delta.copy()\n  for i in range(len(self.Ws)):\n    self.Ws[i] -= self.learning_rate * grads[i]\n  return self\n",[20,505,506,511,516,521,526,531,536,541,546,551,556,561,566,571],{"__ignoreMap":43},[47,507,508],{"class":49,"line":50},[47,509,510],{},"def backward(self, y, y_pred):\n",[47,512,513],{"class":49,"line":56},[47,514,515],{},"  grads = []\n",[47,517,518],{"class":49,"line":62},[47,519,520],{},"  output_delta = y_pred - y\n",[47,522,523],{"class":49,"line":68},[47,524,525],{},"  output_grad = self.A[-1].T @ output_delta\n",[47,527,528],{"class":49,"line":74},[47,529,530],{},"  grads.append(output_grad)\n",[47,532,533],{"class":49,"line":80},[47,534,535],{},"  for i in range(len(self.Ws)-1, 0, -1):\n",[47,537,538],{"class":49,"line":86},[47,539,540],{},"    tanh_grad = (1 - np.tanh(self.Z[i-1])**2)\n",[47,542,543],{"class":49,"line":93},[47,544,545],{},"    input_delta = output_delta @ self.Ws[i][1:].T * tanh_grad\n",[47,547,548],{"class":49,"line":99},[47,549,550],{},"    grad = self.A[i-1].T @ input_delta\n",[47,552,553],{"class":49,"line":105},[47,554,555],{},"    grads.insert(0, grad)\n",[47,557,558],{"class":49,"line":111},[47,559,560],{},"    output_delta = input_delta.copy()\n",[47,562,563],{"class":49,"line":117},[47,564,565],{},"  for i in range(len(self.Ws)):\n",[47,567,568],{"class":49,"line":123},[47,569,570],{},"    self.Ws[i] -= self.learning_rate * grads[i]\n",[47,572,573],{"class":49,"line":129},[47,574,575],{},"  return self\n",[11,577,578,579,326,582,585,586,326,589,592,593,596,597,600,601,604,605,608,609,612,613,616,617,620,621,624,625,628,629,632,633,635],{},"Duas coisas mudam de cara em relação ao post passado, e nenhuma delas é matemática nova. Primeiro, o sinal virou: era ",[20,580,581],{},"erro = y - y_pred",[20,583,584],{},"self.how += taxa * grad",", agora é ",[20,587,588],{},"output_delta = y_pred - y",[20,590,591],{},"self.Ws[i] -= taxa * grads[i]",". É a mesma conta multiplicada por ",[20,594,595],{},"-1"," dos dois lados, ir na direção que ",[340,598,599],{},"diminui"," o erro (",[20,602,603],{},"pred - y",") é a mesma coisa que ir na direção que ",[340,606,607],{},"aumenta"," o acerto (",[20,610,611],{},"y - pred","), só reescrita ao contrário. Segundo, e essa é a generalização de verdade: o bloco que era só três linhas fixas virou um laço, ",[20,614,615],{},"for i in range(len(self.Ws)-1, 0, -1)",", que começa na penúltima camada e anda pra trás até a primeira, repetindo, camada por camada, exatamente ",[14,618,619],{"href":16},"o mesmo pulo do gato do post passado",": pega o ",[20,622,623],{},"output_delta"," que chegou da camada seguinte, reparte ele através da transposta dos pesos dessa camada (",[20,626,627],{},"self.Ws[i][1:].T",", sem o bias), multiplica pela derivada da tanh (quanta folga essa camada ainda tinha), e o resultado (",[20,630,631],{},"input_delta",") vira o ",[20,634,623],{}," da próxima volta do laço, uma camada mais pra trás. Cada volta do laço faz, pra uma camada, exatamente o que o post passado fez, à mão, pra uma única camada escondida.",[11,637,638,639,641,642,645,646,649,650,653,654,657,658,661,662,664,665,669,670,673,674,677],{},"E repara no caso mais simples de todos: se ",[20,640,337],{}," tem só ",[340,643,644],{},"uma"," matriz (",[20,647,648],{},"n_hidden=[]",", sem camada escondida nenhuma), o laço ",[20,651,652],{},"range(0, 0, -1)"," não roda nem uma vez. ",[20,655,656],{},"grads"," fica só com o ",[20,659,660],{},"output_grad"," calculado antes do laço, e o ",[20,663,362],{}," treina essa única matriz de pesos direto em cima do dado bruto. Isso não é um caso especial tratado à parte no código, é o mesmo laço, só que vazio: a arquitetura mais simples de todas ",[14,666,668],{"href":667},"\u002Fplaylists\u002Fneural-networks\u002Fmcculloch-pitts-perceptron","(um perceptron sem camada escondida)"," é só o caso ",[20,671,672],{},"len(self.Ws) == 1"," dessa fórmula geral, sem precisar de nenhum ",[20,675,676],{},"if"," a mais.",[33,679,681],{"id":680},"testando-o-caso-vazio-or-e-and-sem-camada-escondida","Testando o caso vazio: OR e AND sem camada escondida",[11,683,684,685,688,689,693],{},"O notebook confirma isso na prática, treinando ",[20,686,687],{},"MultiLayer(n_hidden=[])"," em cima de OR e de AND, os dois ",[14,690,692],{"href":691},"\u002Fplaylists\u002Fneural-networks\u002Fxor-o-limite-do-perceptron","linearmente separáveis",", diferente do XOR:",[38,695,697],{"className":40,"code":696,"language":42,"meta":43,"style":43},"X, y = createLogicalDataset(func= lambda a, b: bool(a) or bool(b))\nmodel = MultiLayer(n_hidden=[])\nmodel.fit(X, y)\n",[20,698,699,704,709],{"__ignoreMap":43},[47,700,701],{"class":49,"line":50},[47,702,703],{},"X, y = createLogicalDataset(func= lambda a, b: bool(a) or bool(b))\n",[47,705,706],{"class":49,"line":56},[47,707,708],{},"model = MultiLayer(n_hidden=[])\n",[47,710,711],{"class":49,"line":62},[47,712,713],{},"model.fit(X, y)\n",[456,715,716],{},[11,717,718,721,722,725,726,728],{},[340,719,720],{},"Saída:"," acurácia ",[340,723,724],{},"1.0"," (OR) e acurácia ",[340,727,724],{}," (AND), cada um com sua própria fronteira linear.",[11,730,731,732,734],{},"Reproduzi o caso do OR eu mesmo, pra você ver o ",[20,733,648],{}," treinando ao vivo:",[736,737],"mlp-trainer",{":classes":738,":learning-rate":739,":points":740,":steps-per-click":741,":x-max":742,":x-min":743,":y-max":742,":y-min":743,"accuracy-readout-label":744,"architecture-hidden":43,"architecture-labels":745,"negative-label":746,"positive-label":747,"reset-label":748,"step-label":749,"steps-readout-label":750,"x-label":751,"y-label":752},"[-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]","0.02","[[-0.0179, -0.0148], [0.1669, -0.1294], [0.0857, -0.1256], [0.2101, 0.1203], [0.2356, 0.0496], [0.1398, 0.0714], [0.0262, 0.1145], [-0.0257, 0.0645], [0.1697, 0.1058], [0.059, 0.3189], [0.9058, 0.0413], [0.916, 0.0477], [0.8615, 0.065], [1.1025, 0.1023], [0.9176, 0.1674], [1.1017, 0.0776], [0.8376, 0.3475], [0.754, 0.0633], [0.9667, 0.3284], [0.805, -0.1428], [0.1662, 0.8478], [0.0613, 0.9461], [0.1224, 0.9291], [0.0569, 1.0289], [0.2505, 0.9032], [0.0548, 0.9733], [0.1478, 0.796], [0.054, 1.0049], [0.0904, 0.8671], [0.1212, 0.8966], [0.8998, 0.7069], [1.0749, 0.9199], [0.8123, 0.998], [0.9309, 0.9018], [1.0048, 1.1261], [0.9554, 1.0585], [1.1157, 0.7979], [0.8477, 0.9776], [0.7292, 0.8733], [1.0293, 1.0043]]","300","1.5","-0.5","acurácia no dataset","sem camada escondida","falso (OR)","verdadeiro (OR)","Reiniciar (novos pesos aleatórios)","Treinar mais passos","passos de treino","x0","x1",[11,754,755,756,759],{},"Uma reta só, treinada pela mesma regra delta ",[14,757,758],{"href":667},"desde o primeiro post da playlist",", resolve o OR sem precisar de nenhuma camada extra. A generalização não trocou a ferramenta, só deixou claro que \"sem camada escondida\" é um ponto na mesma reta de opções, não uma exceção.",[33,761,763],{"id":762},"testando-o-caso-difícil-quando-uma-reta-não-basta","Testando o caso difícil: quando uma reta não basta",[11,765,766,767,770,771,770,774,777,778,770,781,770,784,787,788,791,792,795],{},"Fui um passo além do notebook aqui: gerei um dataset bem mais complicado que os quatro cantos do XOR, dez blobs espalhados no plano, cada um rotulado pela paridade do índice do seu centro (blobs ",[20,768,769],{},"0",", ",[20,772,773],{},"2",[20,775,776],{},"4","... numa classe, ",[20,779,780],{},"1",[20,782,783],{},"3",[20,785,786],{},"5","... na outra), a mesma receita do ",[20,789,790],{},"make_hard_dataset"," da aula, só que fixei o sorteio pra poder comparar arquiteturas diferentes no ",[340,793,794],{},"mesmo"," dataset:",[38,797,799],{"className":40,"code":798,"language":42,"meta":43,"style":43},"def make_hard_dataset(n=100, centers=10, cluster_std=0.5):\n  X, y = make_blobs(n_samples=n, centers=centers, cluster_std=cluster_std)\n  y = np.where(y%2 == 0, 1, -1)\n  return X, y\n",[20,800,801,806,811,816],{"__ignoreMap":43},[47,802,803],{"class":49,"line":50},[47,804,805],{},"def make_hard_dataset(n=100, centers=10, cluster_std=0.5):\n",[47,807,808],{"class":49,"line":56},[47,809,810],{},"  X, y = make_blobs(n_samples=n, centers=centers, cluster_std=cluster_std)\n",[47,812,813],{"class":49,"line":62},[47,814,815],{},"  y = np.where(y%2 == 0, 1, -1)\n",[47,817,818],{"class":49,"line":68},[47,819,820],{},"  return X, y\n",[11,822,823],{},"Testa você mesmo, trocando de arquitetura no mesmo dado (a cada troca, os pesos são sorteados de novo e o treino recomeça do zero):",[736,825],{":classes":826,":learning-rate":827,":points":828,":steps-per-click":829,":x-max":830,":x-min":831,":y-max":786,":y-min":831,"accuracy-readout-label":744,"architecture-hidden":832,"architecture-labels":833,"negative-label":834,"positive-label":835,"reset-label":748,"step-label":749,"steps-readout-label":750,"x-label":751,"y-label":752},"[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, 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, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1]","0.01","[[-0.332, 0.553], [3.218, -0.301], [0.197, 1.046], [-1.972, 0.205], [0.145, 2.691], [-3.96, -1.656], [-4.007, 2.481], [1.161, -3.553], [5.51, 3.696], [1.651, 0.258], [-0.524, 0.811], [3.344, -0.085], [0.112, 0.197], [-2.238, -0.306], [1.976, 2.087], [-2.907, -1.569], [-2.794, 2.179], [2.049, -3.722], [4.492, 4.032], [1.301, 0.53], [0.004, 0.714], [4.211, -0.459], [0.337, 0.954], [-2.379, 0.204], [1.131, 2.041], [-3.757, -1.768], [-2.975, 3.236], [2.039, -3.133], [4.249, 4.069], [1.322, 1.306], [0.507, 0.316], [3.043, 0.71], [0.182, 1.185], [-2.083, -0.468], [2.257, 3.228], [-3.162, -1.194], [-3.207, 2.038], [1.494, -3.473], [4.418, 4.075], [1.26, 1.597], [-0.76, 0.876], [3.946, -0.416], [-0.835, 0.247], [-2.401, 0.364], [1.814, 1.838], [-3.249, -1.246], [-3.349, 1.295], [1.953, -2.504], [4.362, 3.261], [1.024, 1.031], [0.865, 0.789], [3.447, 0.509], [0.019, 1.631], [-2.863, -0.137], [1.4, 3.052], [-2.923, -1.419], [-4.181, 2.124], [1.245, -3.927], [4.153, 3.552], [1.624, 0.961], [-0.546, 0.539], [3.604, 0.159], [-0.179, 0.584], [-3.202, 0.902], [1.766, 2.251], [-3.489, -2.188], [-3.185, 2.583], [2.641, -3.537], [3.471, 2.498], [1.935, 1.015], [-1.185, 0.57], [2.586, 1.47], [0.619, 0.986], [-2.608, -0.97], [0.977, 1.563], [-2.952, -2.639], [-3.724, 3.09], [2.372, -4.251], [3.093, 4.16], [1.657, 0.427], [-0.727, 0.133], [2.795, -0.61], [0.685, 1.045], [-1.492, 0.057], [0.972, 1.849], [-3.312, -2.227], [-4.159, 2.828], [2.27, -3.242], [4.526, 3.846], [0.871, 0.963], [-0.567, -0.076], [2.544, -1.011], [0.057, 0.775], [-2.757, 0.814], [1.129, 2.631], [-2.641, -1.98], [-4.255, 2.856], [1.206, -3.005], [4.029, 4.223], [1.612, -0.12]]","1500","6","-5",";10;10,10","sem camada escondida;1 camada de 10;2 camadas de 10","classe -1","classe +1",[11,837,838,839,842,843,846,847,850,851,854],{},"Sem camada escondida, o treino empaca em torno de ",[340,840,841],{},"81%"," de acurácia, não importa quantas vezes você clique em treinar mais: dez blobs espalhados alternando classe não cabem atrás de uma única reta, não importa quão bem ajustada ela seja. Troca pra ",[340,844,845],{},"1 camada de 10"," e, depois de alguns cliques, a acurácia sobe até ",[340,848,849],{},"100%",": dez direções combinadas conseguem contornar cada blob individualmente. ",[340,852,853],{},"2 camadas de 10"," chegam no mesmo 100%, só que geralmente levando um pouco mais de cliques pra estabilizar, mais uma camada de composição não ajudou nesse problema específico (ele já tinha \"espaço\" suficiente pra ser resolvido com uma camada só), mas também não atrapalhou, o mesmo laço genérico dá conta dos dois jeitos igual.",[33,856,858],{"id":857},"fechando","Fechando",[860,861,862,876],"table",{},[863,864,865],"thead",{},[866,867,868,873],"tr",{},[869,870,872],"th",{"align":871},"left","O que eu já sabia",[869,874,875],{"align":871},"O que essa aula assentou",[877,878,879,888,896],"tbody",{},[866,880,881,885],{},[882,883,884],"td",{"align":871},"O pulo do gato reparte o erro através de UMA camada escondida",[882,886,887],{"align":871},"O mesmo pulo do gato, num laço, reparte o erro através de QUALQUER número de camadas, uma de cada vez",[866,889,890,893],{},[882,891,892],{"align":871},"Sem camada escondida, o modelo é um perceptron\u002FAdaline",[882,894,895],{"align":871},"Isso não é um caso à parte no código, é só o laço rodando zero vezes",[866,897,898,901],{},[882,899,900],{"align":871},"Uma reta resolve problemas linearmente separáveis (OR, AND)",[882,902,903],{"align":871},"Um dataset com dez blobs alternados não é linearmente separável, e nenhuma quantidade de treino resolve isso sem profundidade",[33,905,907],{"id":906},"aplicação-prática","Aplicação Prática",[11,909,910,913,914,917,918,921,922,925,926,929],{},[14,911,912],{"href":16},"Na aula passada",", variei a ",[340,915,916],{},"largura"," de uma única camada escondida no dataset de duas luas e vi a acurácia subir rápido e depois estabilizar. Essa aula generalizou pra profundidade, então a pergunta natural agora é a inversa: será que empilhar uma ",[340,919,920],{},"segunda"," camada escondida ajuda nesse mesmo dataset? Treinei ",[20,923,924],{},"MultiLayer"," no mesmo dataset de duas luas (mesmos 300 pontos, mesma divisão 70\u002F30, ",[20,927,928],{},"3000"," passos, 10 sementes por arquitetura), comparando zero, uma e duas camadas escondidas de 20 neurônios cada:",[860,931,932,943],{},[863,933,934],{},[866,935,936,939],{},[869,937,938],{"align":871},"Arquitetura",[869,940,942],{"align":941},"right","Acurácia de teste (média de 10 sementes)",[877,944,945,953,963],{},[866,946,947,950],{},[882,948,949],{"align":871},"Sem camada escondida",[882,951,952],{"align":941},"0.889",[866,954,955,958],{},[882,956,957],{"align":871},"1 camada de 20",[882,959,960],{"align":941},[340,961,962],{},"0.967",[866,964,965,968],{},[882,966,967],{"align":871},"2 camadas de 20",[882,969,970],{"align":941},"0.937",[11,972,973,974,976,977,980,981,770,983,986,987,989,990,322,993,995,996,998,999,1002],{},"Sem camada escondida, o modelo já chega em ",[20,975,952],{},", mais alto do que eu esperava: ",[14,978,979],{"href":691},"diferente do XOR",", as duas luas não são um caso tão hostil pra uma reta só, elas ficam deslocadas uma da outra, então uma diagonal bem colocada já acerta a maioria dos pontos, só erra nas bordas onde as duas formas se encaixam. Uma camada de 20 sobe pra ",[20,982,962],{},[14,984,985],{"href":16},"o mesmo número que vi na aula passada",", a curva consegue se dobrar em volta de cada lua de verdade. Mas empilhar uma ",[340,988,920],{}," camada de 20 não ajuda, ",[340,991,992],{},"piora",[20,994,970],{},", abaixo até da rede de uma camada só. Isso não é erro de digitação nem falta de sorte no sorteio (dá pra reproduzir com sementes diferentes): o erro precisa atravessar duas derivadas de ",[20,997,471],{}," encadeadas pra chegar na primeira camada, em vez de uma só, e com o mesmo número fixo de passos de treino esse caminho mais longo nem sempre termina de convergir. Empilhar camada não é grátis: cada camada a mais é mais um degrau pro gradiente descer até lá atrás, e se o treino não rodar tempo suficiente pra compensar, profundidade extra pode ",[340,1000,1001],{},"atrapalhar"," em vez de ajudar. No dataset de blobs ali em cima, com um problema genuinamente mais difícil, a segunda camada não trouxe prejuízo porque tinha motivo real pra existir; aqui, onde uma camada já dava conta, ela só tornou o treino mais lento de convergir.",[1004,1005,1006],"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":43,"searchDepth":56,"depth":56,"links":1008},[1009,1015,1016,1017,1018],{"id":35,"depth":56,"text":36,"children":1010},[1011,1013],{"id":374,"depth":62,"text":1012},"forward: o mesmo cálculo, guardado camada por camada",{"id":497,"depth":62,"text":1014},"backward: o pulo do gato, repetido de trás pra frente",{"id":680,"depth":56,"text":681},{"id":762,"depth":56,"text":763},{"id":857,"depth":56,"text":858},{"id":906,"depth":56,"text":907},null,"2026-09-02","Aula 5c: o pulo do gato do post passado resolvia só uma camada escondida. Aqui ele vira um laço que funciona pra zero, uma, duas ou qualquer número de camadas, e um dataset bem mais difícil mostra por que, às vezes, uma camada só não é suficiente.","md",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fbackprop-qualquer-profundidade","neural-networks",{"title":6,"description":1021},"published","pt\u002Fplaylists\u002Fneural-networks\u002Fbackprop-qualquer-profundidade",[1030,1031,1032,1033],"mlp","backpropagation","profundidade","generalização","MtRiUo25vHNaIkl2LOsKc_LPXjlOmXnXAy3tXlO12N8",1788965098947]