[{"data":1,"prerenderedAt":606},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse":3,"post-pt-neural-networks-multiclasse":4},"\u002Fen\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse",{"id":5,"title":6,"body":7,"cover":592,"date":593,"description":594,"extension":595,"meta":596,"navigation":316,"order":63,"path":597,"playlist":598,"seo":599,"status":600,"stem":601,"tags":602,"__hash__":605},"posts\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse.md","Multiclasse: Quando um Peso Vira uma Matriz",{"type":8,"value":9,"toc":582},"minimark",[10,14,19,73,82,96,100,125,136,181,185,210,232,252,260,271,354,388,397,401,408,420,423,427,482,486,497,506,516,562,578],[11,12,13],"p",{},"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.",[15,16,18],"h2",{"id":17},"o-dataset-quatro-blobs-um-em-cada-canto","O dataset: quatro blobs, um em cada canto",[20,21,26],"pre",{"className":22,"code":23,"language":24,"meta":25,"style":25},"language-python shiki shiki-themes github-light github-dark","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","python","",[27,28,29,37,43,49,55,61,67],"code",{"__ignoreMap":25},[30,31,34],"span",{"class":32,"line":33},"line",1,[30,35,36],{},"def createMulticlassDataset(n=40):\n",[30,38,40],{"class":32,"line":39},2,[30,41,42],{},"  X, y = make_blobs(n_samples=n,\n",[30,44,46],{"class":32,"line":45},3,[30,47,48],{},"                    centers=[[0.2,0.2], [0.8, 0.2], [0.2, 0.8], [0.8, 0.8]],\n",[30,50,52],{"class":32,"line":51},4,[30,53,54],{},"                    n_features=2,\n",[30,56,58],{"class":32,"line":57},5,[30,59,60],{},"                    cluster_std=0.05,\n",[30,62,64],{"class":32,"line":63},6,[30,65,66],{},"                    center_box=(0,1))\n",[30,68,70],{"class":32,"line":69},7,[30,71,72],{},"  return X, y\n",[20,74,76],{"className":22,"code":75,"language":24,"meta":25,"style":25},"set(y_train)\n",[27,77,78],{"__ignoreMap":25},[30,79,80],{"class":32,"line":33},[30,81,75],{},[83,84,85],"blockquote",{},[11,86,87,91,92,95],{},[88,89,90],"strong",{},"Saída:"," ",[27,93,94],{},"{0, 1, 2, 3}",". Quatro classes, um número inteiro cada.",[15,97,99],{"id":98},"primeira-tentativa-reusar-o-que-já-existe-e-falhar-de-propósito","Primeira tentativa: reusar o que já existe (e falhar de propósito)",[20,101,103],{"className":22,"code":102,"language":24,"meta":25,"style":25},"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",[27,104,105,110,115,120],{"__ignoreMap":25},[30,106,107],{"class":32,"line":33},[30,108,109],{},"model = NeuralNetwork()  # o mesmo de sempre, sign(X @ w_)\n",[30,111,112],{"class":32,"line":39},[30,113,114],{},"model.fit(X, y)\n",[30,116,117],{"class":32,"line":45},[30,118,119],{},"y_pred = model.predict(X)\n",[30,121,122],{"class":32,"line":51},[30,123,124],{},"print(f\"Accuracy: {accuracy_score(y, y_pred)}\")\n",[83,126,127],{},[11,128,129,131,132,135],{},[88,130,90],{}," acurácia ",[88,133,134],{},"0.25",".",[11,137,138,139,141,142,145,146,149,150,153,154,157,158,161,162,161,165,153,168,171,172,161,174,161,176,153,178,180],{},"Vale entender por que exatamente ",[27,140,134],{},", 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 ",[27,143,144],{},"NeuralNetwork"," de sempre usa ",[27,147,148],{},"sign(X @ w_)",", que só devolve ",[27,151,152],{},"-1"," ou ",[27,155,156],{},"+1",", dois valores possíveis, nunca ",[27,159,160],{},"0",", ",[27,163,164],{},"1",[27,166,167],{},"2",[27,169,170],{},"3",". Comparar isso contra um rótulo que pode ser ",[27,173,160],{},[27,175,164],{},[27,177,167],{},[27,179,170],{}," é 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.",[15,182,184],{"id":183},"one-hot-cada-classe-vira-sua-própria-coluna","One-hot: cada classe vira sua própria coluna",[20,186,188],{"className":22,"code":187,"language":24,"meta":25,"style":25},"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",[27,189,190,195,200,205],{"__ignoreMap":25},[30,191,192],{"class":32,"line":33},[30,193,194],{},"y_hot = np.zeros((y_train.shape[0], len(set(y_train))), dtype=int)\n",[30,196,197],{"class":32,"line":39},[30,198,199],{},"for i, label in enumerate(list(set(y_train))):\n",[30,201,202],{"class":32,"line":45},[30,203,204],{},"  idxs = np.where(y_train == label)[0]\n",[30,206,207],{"class":32,"line":51},[30,208,209],{},"  y_hot[idxs, i] = 1\n",[11,211,212,213,161,215,161,217,153,219,221,222,224,225,227,228,231],{},"Em vez de um rótulo escalar (",[27,214,160],{},[27,216,164],{},[27,218,167],{},[27,220,170],{},"), cada exemplo vira uma linha com um ",[27,223,164],{}," na coluna da sua classe e ",[27,226,160],{}," nas outras. O professor confirma que isso bate exatamente com o ",[27,229,230],{},"LabelBinarizer"," do scikit-learn:",[20,233,235],{"className":22,"code":234,"language":24,"meta":25,"style":25},"from sklearn.preprocessing import LabelBinarizer\nlb = LabelBinarizer()\ny_hot = lb.fit_transform(y_train)\n",[27,236,237,242,247],{"__ignoreMap":25},[30,238,239],{"class":32,"line":33},[30,240,241],{},"from sklearn.preprocessing import LabelBinarizer\n",[30,243,244],{"class":32,"line":39},[30,245,246],{},"lb = LabelBinarizer()\n",[30,248,249],{"class":32,"line":45},[30,250,251],{},"y_hot = lb.fit_transform(y_train)\n",[11,253,254,255,135],{},"Mesma matriz, duas implementações, ",[256,257,259],"a",{"href":258},"\u002Fplaylists\u002Fneural-networks\u002Fregressao-e-equacao-normal","o mesmo tipo de conferência \"bate com a ferramenta profissional\" que já apareceu antes nessa playlist",[15,261,263,264,267,268],{"id":262},"a-correção-peso-vira-matriz-sign-vira-argmax","A correção: peso vira matriz, ",[27,265,266],{},"sign"," vira ",[27,269,270],{},"argmax",[20,272,274],{"className":22,"code":273,"language":24,"meta":25,"style":25},"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",[27,275,276,281,286,291,296,301,306,311,318,324,330,336,342,348],{"__ignoreMap":25},[30,277,278],{"class":32,"line":33},[30,279,280],{},"class SGD(TrainingAlgorithm):\n",[30,282,283],{"class":32,"line":39},[30,284,285],{},"  def get_w(self, X, y):\n",[30,287,288],{"class":32,"line":45},[30,289,290],{},"    self.w_ = np.random.random(size=(X.shape[1], y.shape[1]))\n",[30,292,293],{"class":32,"line":51},[30,294,295],{},"    for _ in range(self.max_iter):\n",[30,297,298],{"class":32,"line":57},[30,299,300],{},"      y_pred = X @ self.w_\n",[30,302,303],{"class":32,"line":63},[30,304,305],{},"      self.w_ += self.learning_rate * self.cost_function.get_gradient(X, y, y_pred)\n",[30,307,308],{"class":32,"line":69},[30,309,310],{},"    return self.w_\n",[30,312,314],{"class":32,"line":313},8,[30,315,317],{"emptyLinePlaceholder":316},true,"\n",[30,319,321],{"class":32,"line":320},9,[30,322,323],{},"class NeuralNetwork(BaseEstimator, ClassifierMixin):\n",[30,325,327],{"class":32,"line":326},10,[30,328,329],{},"  def predict(self, X):\n",[30,331,333],{"class":32,"line":332},11,[30,334,335],{},"    X = include_bias(X)\n",[30,337,339],{"class":32,"line":338},12,[30,340,341],{},"    logits = X @ self.w_\n",[30,343,345],{"class":32,"line":344},13,[30,346,347],{},"    idxs = np.argmax(logits, axis=1)\n",[30,349,351],{"class":32,"line":350},14,[30,352,353],{},"    return np.array([self.labels[idx] for idx in idxs])\n",[11,355,356,357,360,361,364,365,368,369,372,373,376,377,380,381,383,384,387],{},"A mudança que resolve tudo: ",[27,358,359],{},"self.w_"," deixa de ser um vetor (",[27,362,363],{},"(features,)",") e vira uma ",[88,366,367],{},"matriz"," (",[27,370,371],{},"(features, classes)","), uma coluna de pesos por classe. ",[27,374,375],{},"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 ",[27,378,379],{},"sign()"," por ",[27,382,270],{},": 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 ",[27,385,386],{},"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.",[83,389,390],{},[11,391,392,131,394,135],{},[88,393,90],{},[88,395,396],{},"1.0",[15,398,400],{"id":399},"interativo-as-quatro-regiões-de-decisão","Interativo: as quatro regiões de decisão",[11,402,403,404,407],{},"Reconstruí o mesmo dataset (",[27,405,406],{},"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.",[409,410],"multiclass-region-chart",{":classes":411,":points":412,":weights":413,":x-max":164,":x-min":160,":y-max":164,":y-min":160,"class0-label":414,"class1-label":415,"class2-label":416,"class3-label":417,"x-label":418,"y-label":419},"[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","x0","x1",[11,421,422],{},"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.",[15,424,426],{"id":425},"fechando","Fechando",[428,429,430,444],"table",{},[431,432,433],"thead",{},[434,435,436,441],"tr",{},[437,438,440],"th",{"align":439},"left","O que eu já sabia",[437,442,443],{"align":439},"O que essa aula assentou",[445,446,447,461,469],"tbody",{},[434,448,449,455],{},[450,451,452,454],"td",{"align":439},[27,453,379],{}," classifica em duas classes",[450,456,457,458,460],{"align":439},"Com mais de duas classes, ",[27,459,379],{}," estruturalmente não tem como funcionar, sobra só 2 saídas possíveis pra N classes",[434,462,463,466],{},[450,464,465],{"align":439},"One-hot encoding transforma rótulo categórico em vetor",[450,467,468],{"align":439},"Isso não é só conveniência de formato, é o que permite o peso virar matriz (uma coluna por classe)",[434,470,471,476],{},[450,472,473,475],{"align":439},[27,474,270],{}," escolhe o maior valor",[450,477,478,479,481],{"align":439},"É a generalização direta de ",[27,480,379],{}," (que é basicamente \"argmax entre 2 opções: positivo ou negativo\") pra qualquer número de classes",[15,483,485],{"id":484},"aplicação-prática","Aplicação Prática",[11,487,488,489,492,493,496],{},"Testei a mesma ideia (one-hot + matriz de peso + argmax) no Wine (",[27,490,491],{},"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, ",[256,494,495],{"href":258},"o mesmo problema já visto antes nessa playlist",", então normalizei antes de treinar.",[20,498,500],{"className":22,"code":499,"language":24,"meta":25,"style":25},"X_train_s = StandardScaler().fit_transform(X_train)\n",[27,501,502],{"__ignoreMap":25},[30,503,504],{"class":32,"line":33},[30,505,499],{},[11,507,508,509,512,513,135],{},"Mesmo normalizado, ",[27,510,511],{},"learning_rate=0.01"," (o padrão do notebook) ainda divergiu com 13 variáveis, e precisei cair pra ",[27,514,515],{},"0.001",[428,517,518,532],{},[431,519,520],{},[434,521,522,525,529],{},[437,523,524],{"align":439},"Abordagem",[437,526,528],{"align":527},"right","Acurácia treino",[437,530,531],{"align":527},"Acurácia teste",[445,533,534,548],{},[434,535,536,542,545],{},[450,537,538,539,541],{"align":439},"Ingênua (",[27,540,266],{},", rótulo escalar)",[450,543,544],{"align":527},"0.403",[450,546,547],{"align":527},"-",[434,549,550,555,557],{},[450,551,552,553],{"align":439},"One-hot + matriz de peso + ",[27,554,270],{},[450,556,396],{"align":527},[450,558,559],{"align":527},[88,560,561],{},"0.9815",[11,563,564,565,153,567,569,570,572,573,153,575,577],{},"A versão ingênua nem chega nos 3 valores de classe possíveis (só consegue prever ",[27,566,152],{},[27,568,156],{},", nunca a classe ",[27,571,167],{},"), 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 ",[27,574,152],{},[27,576,164],{},". 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.",[579,580,581],"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":25,"searchDepth":39,"depth":39,"links":583},[584,585,586,587,589,590,591],{"id":17,"depth":39,"text":18},{"id":98,"depth":39,"text":99},{"id":183,"depth":39,"text":184},{"id":262,"depth":39,"text":588},"A correção: peso vira matriz, sign vira argmax",{"id":399,"depth":39,"text":400},{"id":425,"depth":39,"text":426},{"id":484,"depth":39,"text":485},null,"2026-08-20","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.","md",{},"\u002Fpt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse","neural-networks",{"title":6,"description":594},"published","pt\u002Fplaylists\u002Fneural-networks\u002Fmulticlasse",[603,604,270],"multiclasse","one-hot-encoding","e62TbMaQwvX5luUffwTAIOE2j38Dibh3K09F-R-G58E",1787338984058]