Redes neurais artificiais são o motor por trás do reconhecimento de imagens, tradução automática, geração de texto e dezenas de outras aplicações que pareciam impossíveis há uma década. O TensorFlow é, ao lado do PyTorch, uma das duas grandes plataformas de deep learning, e o Keras é a API de alto nível que vem com ele — permite construir e treinar redes neurais complexas com poucas linhas de código. Desde o Keras 3, a mesma API roda também sobre JAX e PyTorch. Neste artigo construímos redes do zero, entendemos seus componentes e as aplicamos em problemas reais.
Instalação
pip install tensorflow tensorboard # o TensorBoard não vem mais junto
pip install pydot # só para keras.utils.plot_model,
# que exige também o Graphviz do sistema
# O TensorFlow 2.21 ainda não tem versão estável para o Python 3.14.
# GPU NVIDIA: só no Linux ou no WSL2 — pip install "tensorflow[and-cuda]"
# Mac com Apple Silicon: o pacote tensorflow já roda nativo;
# para usar a GPU, acrescente tensorflow-metal
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models, callbacks
print(f"TensorFlow: {tf.__version__}")
print(f"GPU disponível: {len(tf.config.list_physical_devices('GPU')) > 0}")
O Neurônio Artificial
Um neurônio artificial recebe entradas, aplica pesos, soma e passa por uma função de ativação:
Entradas Pesos Soma + Bias Ativação Saída
x₁ ──── w₁ ─┐
x₂ ──── w₂ ──┤──► Σ(xᵢwᵢ) + b ──► f(z) ──► ŷ
x₃ ──── w₃ ─┘
import numpy as np
# Simulando um neurônio manualmente
def neuronio(entradas, pesos, bias, ativacao="relu"):
z = np.dot(entradas, pesos) + bias
if ativacao == "relu":
return max(0, z)
elif ativacao == "sigmoid":
return 1 / (1 + np.exp(-z))
elif ativacao == "tanh":
return np.tanh(z)
return z
entradas = np.array([0.5, 0.3, 0.8])
pesos = np.array([0.4, 0.7, 0.2])
bias = 0.1
saida = neuronio(entradas, pesos, bias, "relu")
print(f"Saída do neurônio: {saida:.4f}")
Funções de Ativação
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 200)
funcoes = {
"ReLU": np.maximum(0, x),
"Sigmoid": 1 / (1 + np.exp(-x)),
"Tanh": np.tanh(x),
"LeakyReLU": np.where(x > 0, x, 0.1 * x),
}
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for ax, (nome, y) in zip(axes, funcoes.items()):
ax.plot(x, y, color="#2E75B6", linewidth=2)
ax.axhline(0, color="gray", linewidth=0.5)
ax.axvline(0, color="gray", linewidth=0.5)
ax.set_title(nome)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("funcoes_ativacao.png", dpi=120)
plt.show()
Primeira Rede Neural: Classificação Binária
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# Dataset — previsão de aprovação
np.random.seed(42)
n = 1000
X = np.column_stack([
np.random.uniform(1, 10, n), # horas_estudo
np.random.randint(0, 20, n), # faltas
np.random.uniform(3, 10, n), # nota_anterior
])
y = ((0.5 * X[:, 0] + 0.3 * X[:, 2] - 0.1 * X[:, 1] +
np.random.normal(0, 0.5, n)) >= 5).astype(int)
# Divisão e normalização
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# Construindo a rede
def construir_modelo(input_dim: int) -> keras.Model:
modelo = keras.Sequential([
# Camada de entrada
layers.Input(shape=(input_dim,)),
# Camadas ocultas
layers.Dense(64, activation="relu"),
layers.BatchNormalization(),
layers.Dropout(0.3),
layers.Dense(32, activation="relu"),
layers.BatchNormalization(),
layers.Dropout(0.2),
layers.Dense(16, activation="relu"),
# Camada de saída — 1 neurônio, sigmoid para binário
layers.Dense(1, activation="sigmoid")
])
modelo.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss="binary_crossentropy",
metrics=["accuracy", keras.metrics.AUC(name="auc")]
)
return modelo
modelo = construir_modelo(X_train.shape[1])
modelo.summary()
Treinamento e Callbacks
# Callbacks — comportamentos durante o treinamento
lista_callbacks = [
# Para o treinamento se não melhorar
callbacks.EarlyStopping(
monitor="val_loss",
patience=15,
restore_best_weights=True,
verbose=1
),
# Reduz learning rate quando estagna
callbacks.ReduceLROnPlateau(
monitor="val_loss",
factor=0.5,
patience=7,
min_lr=1e-6,
verbose=1
),
# Salva o melhor modelo
callbacks.ModelCheckpoint(
filepath="melhor_modelo.keras",
monitor="val_auc",
save_best_only=True,
mode="max",
verbose=1
),
# Logs para TensorBoard
callbacks.TensorBoard(log_dir="./logs", histogram_freq=1),
]
# Treinamento
historico = modelo.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_split=0.2,
callbacks=lista_callbacks,
verbose=1
)
# Avaliação no conjunto de teste
loss, acc, auc = modelo.evaluate(X_test, y_test, verbose=0)
print(f"\nResultados no Teste:")
print(f" Loss: {loss:.4f}")
print(f" Acurácia: {acc:.4f}")
print(f" AUC: {auc:.4f}")
Visualizando o Treinamento
import matplotlib.pyplot as plt
def plotar_historico(historico):
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
metricas = [
("loss", "Loss", "Loss de Treino vs Validação"),
("accuracy", "Acurácia", "Acurácia de Treino vs Validação"),
("auc", "AUC", "AUC de Treino vs Validação"),
]
for ax, (metrica, ylabel, titulo) in zip(axes, metricas):
ax.plot(historico.history[metrica],
label="Treino", color="#2E75B6")
ax.plot(historico.history[f"val_{metrica}"],
label="Validação", color="#C00000", linestyle="--")
ax.set_title(titulo)
ax.set_xlabel("Época")
ax.set_ylabel(ylabel)
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("historico_treinamento.png", dpi=120)
plt.show()
plotar_historico(historico)
Regressão com Redes Neurais
# Prevendo nota_final (valor contínuo)
y_reg = 0.4 * X[:, 0] + 0.3 * X[:, 2] - 0.1 * X[:, 1] + np.random.normal(0, 0.3, n)
y_reg = np.clip(y_reg, 0, 10)
X_tr, X_te, y_tr, y_te = train_test_split(X, y_reg, test_size=0.2, random_state=42)
# Scaler próprio: refazer o fit no da classificação o deixaria
# ajustado a outra divisão dos dados
scaler_reg = StandardScaler()
X_tr = scaler_reg.fit_transform(X_tr)
X_te = scaler_reg.transform(X_te)
def modelo_regressao(input_dim: int) -> keras.Model:
modelo = keras.Sequential([
layers.Input(shape=(input_dim,)),
layers.Dense(128, activation="relu"),
layers.Dropout(0.2),
layers.Dense(64, activation="relu"),
layers.Dropout(0.2),
layers.Dense(32, activation="relu"),
layers.Dense(1) # sem ativação — saída linear
])
modelo.compile(
optimizer="adam",
loss="mse",
metrics=["mae", keras.metrics.RootMeanSquaredError(name="rmse")]
)
return modelo
modelo_reg = modelo_regressao(3)
historico_reg = modelo_reg.fit(
X_tr, y_tr,
epochs=100,
batch_size=32,
validation_split=0.2,
callbacks=[callbacks.EarlyStopping(patience=10, restore_best_weights=True)],
verbose=0
)
mse, mae, rmse = modelo_reg.evaluate(X_te, y_te, verbose=0)
print(f"MSE: {mse:.4f}")
print(f"MAE: {mae:.4f}")
print(f"RMSE: {rmse:.4f}")
# Visualizando predições
y_pred_reg = modelo_reg.predict(X_te, verbose=0).flatten()
plt.figure(figsize=(8, 5))
plt.scatter(y_te, y_pred_reg, alpha=0.4, color="#2E75B6")
plt.plot([0, 10], [0, 10], "r--", label="Ideal")
plt.xlabel("Nota Real")
plt.ylabel("Nota Prevista")
plt.title("Real vs Previsto")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("regressao_nn.png", dpi=120)
plt.show()
API Funcional: Redes Complexas
A API Sequencial é simples, mas a API Funcional permite arquiteturas complexas — múltiplas entradas, saídas ou conexões residuais:
from tensorflow.keras import Input, Model
def construir_rede_funcional():
# Múltiplas entradas
entrada_academica = Input(shape=(3,), name="academica") # horas, faltas, nota
entrada_pessoal = Input(shape=(2,), name="pessoal") # idade, renda_familiar
# Ramo acadêmico
x1 = layers.Dense(32, activation="relu")(entrada_academica)
x1 = layers.Dropout(0.2)(x1)
x1 = layers.Dense(16, activation="relu")(x1)
# Ramo pessoal
x2 = layers.Dense(16, activation="relu")(entrada_pessoal)
x2 = layers.Dense(8, activation="relu")(x2)
# Combinando os ramos
combinado = layers.Concatenate()([x1, x2])
x = layers.Dense(32, activation="relu")(combinado)
x = layers.Dropout(0.3)(x)
# Múltiplas saídas
saida_aprovado = layers.Dense(1, activation="sigmoid", name="aprovado")(x)
saida_nota = layers.Dense(1, activation="linear", name="nota")(x)
modelo = Model(
inputs=[entrada_academica, entrada_pessoal],
outputs=[saida_aprovado, saida_nota]
)
modelo.compile(
optimizer="adam",
loss={
"aprovado": "binary_crossentropy",
"nota": "mse"
},
loss_weights={"aprovado": 1.0, "nota": 0.5},
metrics={
"aprovado": ["accuracy"],
"nota": ["mae"]
}
)
return modelo
modelo_funcional = construir_rede_funcional()
keras.utils.plot_model(
modelo_funcional,
to_file="arquitetura.png",
show_shapes=True,
dpi=80
)
modelo_funcional.summary()
Regularização: Evitando Overfitting
from tensorflow.keras import regularizers
def modelo_regularizado(input_dim: int) -> keras.Model:
modelo = keras.Sequential([
layers.Input(shape=(input_dim,)),
# L2 regularization — penaliza pesos grandes
layers.Dense(128, activation="relu",
kernel_regularizer=regularizers.l2(0.001)),
layers.BatchNormalization(),
layers.Dropout(0.4),
# L1 regularization — incentiva esparsidade
layers.Dense(64, activation="relu",
kernel_regularizer=regularizers.l1(0.001)),
layers.Dropout(0.3),
# Combinação L1 + L2 (Elastic Net)
layers.Dense(32, activation="relu",
kernel_regularizer=regularizers.l1_l2(l1=0.001, l2=0.001)),
layers.Dense(1, activation="sigmoid")
])
# Sem compile(), o fit() falha: "You must call `compile()`"
modelo.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss="binary_crossentropy",
metrics=["accuracy", keras.metrics.AUC(name="auc")]
)
return modelo
# Comparando modelos com e sem regularização
historicos = {}
for nome, model in [
("Sem regularização", construir_modelo(3)),
("Com regularização", modelo_regularizado(3)),
]:
h = model.fit(
X_train, y_train,
epochs=50,
batch_size=32,
validation_split=0.2,
callbacks=[callbacks.EarlyStopping(patience=10, restore_best_weights=True)],
verbose=0
)
historicos[nome] = h
loss, acc, auc = model.evaluate(X_test, y_test, verbose=0)
print(f"{nome:25} | Acc: {acc:.4f} | AUC: {auc:.4f}")
Transfer Learning
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
def modelo_transfer_learning(num_classes: int) -> keras.Model:
"""
Usa MobileNetV2 pré-treinado no ImageNet como extrator de features.
Útil quando o dataset próprio é pequeno.
"""
# Base pré-treinada — congela os pesos
base = MobileNetV2(
weights="imagenet",
include_top=False, # remove a camada de classificação
input_shape=(224, 224, 3)
)
base.trainable = False # congela
# Adiciona cabeçalho de classificação customizado
modelo = keras.Sequential([
base,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation="relu"),
layers.Dropout(0.3),
layers.Dense(num_classes, activation="softmax")
])
modelo.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss="categorical_crossentropy",
metrics=["accuracy"]
)
return modelo
# Fine-tuning — descongela as últimas camadas da base
def fine_tuning(modelo_base: keras.Model, num_layers_descongelar: int = 20):
base = modelo_base.layers[0]
base.trainable = True
# Congela tudo exceto as últimas N camadas
for layer in base.layers[:-num_layers_descongelar]:
layer.trainable = False
modelo_base.compile(
optimizer=keras.optimizers.Adam(1e-5), # lr menor para fine-tuning
loss="categorical_crossentropy",
metrics=["accuracy"]
)
return modelo_base
Salvando e Carregando Modelos
# Formato recomendado — .keras (nativo Keras 3)
modelo.save("modelo_completo.keras")
modelo_carregado = keras.models.load_model("modelo_completo.keras")
# Apenas os pesos
modelo.save_weights("pesos.weights.h5")
modelo_novo = construir_modelo(3)
modelo_novo.load_weights("pesos.weights.h5")
# Exportando para TensorFlow SavedModel (para TensorFlow Serving)
modelo.export("saved_model/")
# Convertendo para TensorFlow Lite (mobile e edge devices)
converter = tf.lite.TFLiteConverter.from_keras_model(modelo)
tflite_model = converter.convert()
with open("modelo.tflite", "wb") as f:
f.write(tflite_model)
print("Modelos salvos com sucesso.")
Exemplo Completo: Pipeline Completo de Deep Learning
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, callbacks
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt
class PipelineDeepLearning:
"""Pipeline completo de classificação com rede neural."""
def __init__(self, nome: str = "modelo"):
self.nome = nome
self.scaler = StandardScaler()
self.modelo = None
self.historico = None
def preparar_dados(self, X, y, test_size=0.2, val_size=0.2):
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=test_size,
random_state=42, stratify=y
)
X_tr = self.scaler.fit_transform(X_tr)
X_te = self.scaler.transform(X_te)
self._X_test = X_te
self._y_test = y_te
return X_tr, X_te, y_tr, y_te
def construir(self, input_dim: int, camadas: list = None):
camadas = camadas or [64, 32, 16]
entradas = layers.Input(shape=(input_dim,))
x = entradas
for unidades in camadas:
x = layers.Dense(unidades, activation="relu")(x)
x = layers.BatchNormalization()(x)
x = layers.Dropout(0.3)(x)
saida = layers.Dense(1, activation="sigmoid")(x)
self.modelo = keras.Model(entradas, saida, name=self.nome)
self.modelo.compile(
optimizer=keras.optimizers.Adam(0.001),
loss="binary_crossentropy",
metrics=["accuracy", keras.metrics.AUC(name="auc")]
)
return self
def treinar(self, X_train, y_train, epochs=100, batch_size=32):
self.historico = self.modelo.fit(
X_train, y_train,
epochs=epochs,
batch_size=batch_size,
validation_split=0.2,
callbacks=[
callbacks.EarlyStopping(patience=15, restore_best_weights=True),
callbacks.ReduceLROnPlateau(patience=7, factor=0.5, verbose=0)
],
verbose=0
)
epocas_treinadas = len(self.historico.history["loss"])
print(f"Treinado por {epocas_treinadas} épocas.")
return self
def avaliar(self):
y_pred_prob = self.modelo.predict(self._X_test, verbose=0).flatten()
y_pred = (y_pred_prob >= 0.5).astype(int)
loss, acc, auc = self.modelo.evaluate(self._X_test, self._y_test, verbose=0)
print(f"\n{'='*40}")
print(f"Modelo: {self.nome}")
print(f" Loss: {loss:.4f}")
print(f" Acurácia: {acc:.4f}")
print(f" AUC: {auc:.4f}")
print(classification_report(
self._y_test, y_pred,
target_names=["Reprovado", "Aprovado"]
))
def salvar(self):
self.modelo.save(f"{self.nome}.keras")
import joblib
joblib.dump(self.scaler, f"{self.nome}_scaler.pkl")
print(f"Pipeline salvo: {self.nome}.keras")
def prever(self, X_novo):
X_proc = self.scaler.transform(X_novo)
prob = self.modelo.predict(X_proc, verbose=0).flatten()
return prob
# Usando o pipeline
np.random.seed(42)
n = 2000
X = np.column_stack([
np.random.uniform(1, 10, n),
np.random.randint(0, 20, n),
np.random.uniform(3, 10, n),
np.random.uniform(0, 5, n),
])
y = ((0.5*X[:,0] + 0.3*X[:,2] - 0.1*X[:,1] + 0.2*X[:,3]
+ np.random.normal(0, 0.5, n)) >= 5).astype(int)
pipeline = PipelineDeepLearning("aprovacao_nn")
X_tr, X_te, y_tr, y_te = pipeline.preparar_dados(X, y)
pipeline.construir(input_dim=4, camadas=[128, 64, 32])
pipeline.treinar(X_tr, y_tr, epochs=150)
pipeline.avaliar()
pipeline.salvar()
# Prevendo novos alunos
novos = np.array([
[8.0, 2, 9.0, 4.0], # perfil forte
[2.0, 18, 4.0, 0.5], # perfil fraco
])
probs = pipeline.prever(novos)
for i, p in enumerate(probs, 1):
print(f"Aluno {i}: {p:.1%} de probabilidade de aprovação")
Uma rede neural densa é uma sequência de multiplicações de matriz com uma função não linear entre elas, e o Keras reduz a construção dela a uma lista de camadas: Dense para aprender combinações das entradas, BatchNormalization e Dropout para estabilizar e regularizar, uma saída sigmoid para classificação binária ou linear para regressão. Os callbacks cuidam do resto do treino — parar quando a validação para de melhorar, reduzir a taxa de aprendizado, guardar o melhor modelo —, a API funcional abre espaço para várias entradas e saídas, e o transfer learning aproveita redes treinadas em milhões de imagens. As métricas são as mesmas de Machine Learning com scikit-learn, e o próximo passo é aplicar redes a texto, onde a vantagem delas sobre os modelos clássicos é grande de verdade.
Em dados tabulares pequenos, porém, a rede raramente é a melhor escolha. No exemplo deste artigo, uma regressão logística com 4 parâmetros teve AUC de 0,978, e a rede com 3.265 teve 0,975: a regra por trás dos dados era linear, e a rede só a reaprendeu com mais custo. O trabalho de verdade está em volta do modelo — scaler salvo junto com ele, validação separada do teste, clareza sobre qual critério escolheu "o melhor modelo" — e em conferir a instalação, que muda de uma versão do TensorFlow para a seguinte.
Fontes e leituras recomendadas
- TensorFlow — documentação oficial — https://www.tensorflow.org/api_docs/python/tf
- Keras — guia do desenvolvedor — https://keras.io/guides/
- TensorFlow Playground — visualização interativa — https://playground.tensorflow.org/
- Transfer Learning com Keras — https://keras.io/guides/transfer_learning/
- GÉRON, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras and TensorFlow. 3. ed. O'Reilly Media, 2022. Cap. 10–16 — redes neurais do básico ao avançado.
- CHOLLET, François. Deep Learning with Python. 2. ed. Manning, 2021. — escrito pelo criador do Keras, referência definitiva sobre deep learning prático.
- GOODFELLOW, Ian; BENGIO, Yoshua; COURVILLE, Aaron. Deep Learning. MIT Press, 2016. — fundamentos teóricos do deep learning, disponível em https://www.deeplearningbook.org/
Exercícios
Exercício 1
Numa máquina nova, com pip install tensorflow recém-feito, o script de treino do artigo morre antes da primeira época com TBNotInstalledError: TensorBoard is not installed. No notebook de um colega, com uma instalação antiga, o mesmo código roda. Qual é a causa, e o que isso ensina sobre arquivos de dependências?
Ver resposta
✓ Resposta: O callback callbacks.TensorBoard escreve os logs com as funções do pacote tensorboard, e o TensorFlow deixou de instalá-lo como dependência: medido no 2.21.0, a lista de requisitos do pacote tem keras, numpy, h5py e outros, mas não tensorboard. Na instalação antiga do colega ele veio junto e ficou. O erro surge dentro do fit, quando o callback tenta gravar o primeiro escalar, e derruba o treino inteiro por causa de uma ferramenta de visualização. A correção é instalar os dois, pip install tensorflow tensorboard, como o artigo passou a mostrar. A lição é declarar no requirements.txt tudo o que o código importa ou usa, com versão, em vez de depender do que outro pacote traz de carona: dependências transitivas mudam sem aviso, e "funciona na minha máquina" costuma ser uma delas.
Exercício 2
Depois do treino com os callbacks do artigo, uma analista avalia o modelo que ficou na memória e depois carrega melhor_modelo.keras do disco para comparar. As métricas não batem. Os dois não deveriam ser "o melhor modelo"?
Ver resposta
✓ Resposta: São o melhor por critérios diferentes. O EarlyStopping monitora val_loss e, com restore_best_weights=True, devolve à memória os pesos da época de menor perda na validação; o ModelCheckpoint monitora val_auc com mode="max" e grava no disco a época de maior AUC. Medido numa execução do artigo: o treino parou na época 45, o EarlyStopping restaurou a época 30, e o último arquivo gravado foi o da época 9, quando a AUC de validação chegou a 0,970 e não subiu mais. Perda e AUC medem coisas diferentes: a AUC só olha a ordem das previsões, e a perda também pune probabilidades mal calibradas, então podem ter o ótimo em épocas distantes. Nenhum dos dois está errado, mas o projeto precisa escolher um critério e usá-lo nos dois callbacks, ou vai para produção um modelo diferente do que foi avaliado.
Exercício 3
Um colega copia a função modelo_regularizado da primeira versão do artigo, que terminava em return keras.Sequential([...]), e o laço de comparação morre na segunda volta com ValueError: You must call compile() before using the model. A primeira volta, com construir_modelo, funcionou. Explique o erro e por que ele só aparece na segunda volta.
Ver resposta
✓ Resposta: Um modelo Keras recém-montado tem arquitetura e pesos, mas não sabe como aprender: é o compile() que define o otimizador, a função de perda e as métricas, e sem eles o fit() não tem o que minimizar. A construir_modelo chama compile antes de devolver o modelo; a modelo_regularizado original só montava a sequência de camadas e a devolvia. O laço cria os dois modelos antes de começar, mas só chama fit dentro dele, então o primeiro treina normalmente — medido, acurácia de 0,91 — e o erro só surge quando chega a vez do segundo. A correção do artigo compila dentro da função, com o mesmo otimizador, perda e métricas do modelo sem regularização. Esse último ponto importa: comparar dois modelos compilados de formas diferentes mediria a diferença de configuração, não a da regularização.
Exercício 4
O gerente de um projeto pergunta por que a equipe gastou uma semana numa rede de três camadas ocultas para prever aprovação, se a AUC final foi 0,975. Um estagiário treina uma regressão logística em cinco minutos e obtém 0,978 nos mesmos dados. A rede foi um erro?
Ver resposta
✓ Resposta: Para esses dados, sim, e o motivo está na forma deles. A aprovação foi gerada por uma soma ponderada de horas, nota anterior e faltas comparada a um limiar — exatamente a forma que uma regressão logística representa. Medido na mesma divisão treino/teste do artigo: a regressão logística, com 4 parâmetros, fez AUC de 0,978 e acurácia de 0,900; a rede, com 3.265 parâmetros, fez 0,975 e 0,905. A diferença está dentro do ruído, e o modelo simples treina em milissegundos, tem coeficientes que se leem e é fácil de pôr em produção. Redes neurais ganham quando há estrutura que os modelos lineares não captam: pixels de uma imagem, palavras em sequência, interações complexas entre muitas variáveis. Em tabelas pequenas, a linha de base simples vem antes, e a rede precisa superá-la com folga para justificar o custo.
Exercício 5
Num notebook, uma aluna treina o classificador, depois a rede de regressão, e por fim usa o scaler para preparar dados de novos alunos para o classificador. As probabilidades saem estranhas, embora os dois modelos tenham ido bem no teste. Na primeira versão do artigo, o bloco de regressão fazia X_tr = scaler.fit_transform(X_tr). Onde está o problema?
Ver resposta
✓ Resposta: O StandardScaler guarda a média e o desvio padrão de cada coluna no fit, e o transform usa esses números. Quando o bloco de regressão chamou fit_transform no mesmo objeto, ele sobrescreveu as estatísticas da classificação pelas de outra divisão dos dados — e o classificador foi treinado com entradas padronizadas pelas estatísticas antigas. Os testes de cada modelo foram feitos logo depois do seu próprio fit, então os dois pareceram bem; o estrago só aparece quando dados novos passam pelo scaler depois da troca. Aqui, com os mesmos dados divididos de outro jeito, o deslocamento é pequeno — e é justamente por isso que o defeito passa despercebido até o dia em que o segundo modelo usa outro conjunto, e o classificador passa a receber entradas deslocadas sem erro algum. O artigo passou a usar um scaler_reg próprio. A regra geral é um pré-processador por modelo, salvo junto com ele — no scikit-learn, dentro do mesmo Pipeline; no pipeline de deep learning do artigo, gravado com joblib ao lado do .keras.