← Voltar pra playlist

Multiclasse: Quando um Peso Vira uma Matriz

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.

O dataset: quatro blobs, um em cada canto

def createMulticlassDataset(n=40):
  X, y = make_blobs(n_samples=n,
                    centers=[[0.2,0.2], [0.8, 0.2], [0.2, 0.8], [0.8, 0.8]],
                    n_features=2,
                    cluster_std=0.05,
                    center_box=(0,1))
  return X, y
set(y_train)

Saída: {0, 1, 2, 3}. Quatro classes, um número inteiro cada.

Primeira tentativa: reusar o que já existe (e falhar de propósito)

model = NeuralNetwork()  # o mesmo de sempre, sign(X @ w_)
model.fit(X, y)
y_pred = model.predict(X)
print(f"Accuracy: {accuracy_score(y, y_pred)}")

Saída: acurácia 0.25.

Vale entender por que exatamente 0.25, 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 NeuralNetwork de sempre usa sign(X @ w_), que só devolve -1 ou +1, dois valores possíveis, nunca 0, 1, 2 ou 3. Comparar isso contra um rótulo que pode ser 0, 1, 2 ou 3 é 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.

One-hot: cada classe vira sua própria coluna

y_hot = np.zeros((y_train.shape[0], len(set(y_train))), dtype=int)
for i, label in enumerate(list(set(y_train))):
  idxs = np.where(y_train == label)[0]
  y_hot[idxs, i] = 1

Em vez de um rótulo escalar (0, 1, 2 ou 3), cada exemplo vira uma linha com um 1 na coluna da sua classe e 0 nas outras. O professor confirma que isso bate exatamente com o LabelBinarizer do scikit-learn:

from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
y_hot = lb.fit_transform(y_train)

Mesma matriz, duas implementações, o mesmo tipo de conferência "bate com a ferramenta profissional" que já apareceu antes nessa playlist.

A correção: peso vira matriz, sign vira argmax

class SGD(TrainingAlgorithm):
  def get_w(self, X, y):
    self.w_ = np.random.random(size=(X.shape[1], y.shape[1]))
    for _ in range(self.max_iter):
      y_pred = X @ self.w_
      self.w_ += self.learning_rate * self.cost_function.get_gradient(X, y, y_pred)
    return self.w_

class NeuralNetwork(BaseEstimator, ClassifierMixin):
  def predict(self, X):
    X = include_bias(X)
    logits = X @ self.w_
    idxs = np.argmax(logits, axis=1)
    return np.array([self.labels[idx] for idx in idxs])

A mudança que resolve tudo: self.w_ deixa de ser um vetor ((features,)) e vira uma matriz ((features, classes)), uma coluna de pesos por classe. 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 sign() por argmax: 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 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.

Saída: acurácia 1.0.

Interativo: as quatro regiões de decisão

Reconstruí o mesmo dataset (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.

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.

Fechando

O que eu já sabiaO que essa aula assentou
sign() classifica em duas classesCom mais de duas classes, sign() estruturalmente não tem como funcionar, sobra só 2 saídas possíveis pra N classes
One-hot encoding transforma rótulo categórico em vetorIsso não é só conveniência de formato, é o que permite o peso virar matriz (uma coluna por classe)
argmax escolhe o maior valorÉ a generalização direta de sign() (que é basicamente "argmax entre 2 opções: positivo ou negativo") pra qualquer número de classes

Aplicação Prática

Testei a mesma ideia (one-hot + matriz de peso + argmax) no Wine (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, o mesmo problema já visto antes nessa playlist, então normalizei antes de treinar.

X_train_s = StandardScaler().fit_transform(X_train)

Mesmo normalizado, learning_rate=0.01 (o padrão do notebook) ainda divergiu com 13 variáveis, e precisei cair pra 0.001.

AbordagemAcurácia treinoAcurácia teste
Ingênua (sign, rótulo escalar)0.403-
One-hot + matriz de peso + argmax1.00.9815

A versão ingênua nem chega nos 3 valores de classe possíveis (só consegue prever -1 ou +1, nunca a classe 2), 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 -1 ou 1. 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.