Redes Neurais MLP para Classificação de imagens#
Visão Computacional | Prof. Dr. Denis Mayr Lima Martins
Objetivos de Aprendizagem#
Compreender a estrutura e funcionamento do perceptron clássico, incluindo sua relação com a função logística.
Analisar o Multi‑Layer Perceptron (MLP) como extensão do perceptron, destacando camadas ocultas, funções de ativação não‑lineares e topologia geral.
Descrever o algoritmo de treinamento baseado em back‑propagation e a otimização de pesos via gradiente descendente.
Compreender estratégias de validação cruzada (k‑fold) para avaliação robusta de modelos.
Discutir os conceitos de viés e variância, suas implicações na generalização e estratégias de mitigação.
Entender métodos de regularização (L1/L2, dropout) e suas aplicações práticas em redes neurais.
Neurônio Artificial#
Warren McCulloch e Walter Pitts. Criadores do primeiro modelo de neurônio artificial (1943). Fonte: History of Information.
|
Neurônio Artificial. Fonte: Tahseen Mulla.
|
Perceptron#
|
Visão Geral do Perceptron. Image Source: Sebastian Raschka.
|
Perceptron Learning Rule#
Dado um exemplo \((\mathbf{x},y)\), onde \(y\in\{0,1\}\) é o rótulo verdadeiro, calcula‑se a predição \(\hat{y}=f(z)\).
Se a predição estiver incorreta (\(e=y-\hat{y}\neq 0\)), os pesos e viés são ajustados na direção que reduz o erro.
\(\mathbf{w}\leftarrow\mathbf{w}+\eta\,e\,\mathbf{x}\) e \(b\leftarrow b+\eta\,e\), onde \(\eta>0\) é a taxa de aprendizado.
Inicializa w ← 0, b ← 0
para cada época até convergência:
para cada (x, d) em D:
z ← w·x + b
ŷ ← (z ≥ 0) ? 1 : 0 // passo unitário
se ŷ ≠ d então // erro de classificação
w ← w + η(d - ŷ)x
b ← b + η(d - ŷ)
fim se
fim para
fim para
|
Frank Rosenblatt em 1960 conectando o Mark 1 Perceptron. Fonte: Perceptron Demo.
|
Perceptron em Pytorch: Demo#
# Preparação dos plots
import matplotlib as mpl
import matplotlib.pyplot as plt
# Set global matplotlib parameters
mpl.rcParams['lines.linewidth'] = 1.2
mpl.rcParams['lines.markersize'] = 6
mpl.rcParams['font.size'] = 12
# Remove plot edges
mpl.rcParams['axes.spines.right'] = False
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['axes.grid'] = True
mpl.rcParams['axes.grid.axis'] = 'y'
mpl.rcParams['grid.alpha'] = 0.3
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.2.5 as it may crash. To support both 1.x and 2.x
versions of NumPy, modules must be compiled with NumPy 2.0.
Some module may need to rebuild instead e.g. with 'pybind11>=2.12'.
If you are a user of the module, the easiest solution will be to
downgrade to 'numpy<2' or try to upgrade the affected module.
We expect that some modules will need time to support NumPy 2.
Traceback (most recent call last): File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel_launcher.py", line 17, in <module>
app.launch_new_instance()
File "/opt/anaconda3/lib/python3.11/site-packages/traitlets/config/application.py", line 992, in launch_instance
app.start()
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelapp.py", line 701, in start
self.io_loop.start()
File "/opt/anaconda3/lib/python3.11/site-packages/tornado/platform/asyncio.py", line 195, in start
self.asyncio_loop.run_forever()
File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
self._run_once()
File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
handle._run()
File "/opt/anaconda3/lib/python3.11/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 534, in dispatch_queue
await self.process_one()
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 523, in process_one
await dispatch(*args)
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 429, in dispatch_shell
await result
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 767, in execute_request
reply_content = await reply_content
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/ipkernel.py", line 429, in do_execute
res = shell.run_cell(
File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/zmqshell.py", line 549, in run_cell
return super().run_cell(*args, **kwargs)
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3051, in run_cell
result = self._run_cell(
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3106, in _run_cell
result = runner(coro)
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/async_helpers.py", line 129, in _pseudo_sync_runner
coro.send(None)
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3311, in run_cell_async
has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3493, in run_ast_nodes
if await self.run_code(code, result, async_=asy):
File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3553, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "/var/folders/p7/p37cm2fj10xgjrjj5rzdm66c0000gn/T/ipykernel_59249/40178957.py", line 2, in <module>
import matplotlib as mpl
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py", line 161, in <module>
from . import _api, _version, cbook, _docstring, rcsetup
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py", line 27, in <module>
from matplotlib.colors import Colormap, is_color_like
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py", line 57, in <module>
from matplotlib import _api, _cm, cbook, scale
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py", line 22, in <module>
from matplotlib.ticker import (
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py", line 143, in <module>
from matplotlib import transforms as mtransforms
File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py", line 49, in <module>
from matplotlib._path import (
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
AttributeError: _ARRAY_API not found
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
Cell In[1], line 2
1 # Preparação dos plots
----> 2 import matplotlib as mpl
3 import matplotlib.pyplot as plt
5 # Set global matplotlib parameters
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py:161
157 from packaging.version import parse as parse_version
159 # cbook must import matplotlib only within function
160 # definitions, so it is safe to import from it here.
--> 161 from . import _api, _version, cbook, _docstring, rcsetup
162 from matplotlib.cbook import sanitize_sequence
163 from matplotlib._api import MatplotlibDeprecationWarning
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py:27
25 from matplotlib import _api, cbook
26 from matplotlib.cbook import ls_mapper
---> 27 from matplotlib.colors import Colormap, is_color_like
28 from matplotlib._fontconfig_pattern import parse_fontconfig_pattern
29 from matplotlib._enums import JoinStyle, CapStyle
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py:57
55 import matplotlib as mpl
56 import numpy as np
---> 57 from matplotlib import _api, _cm, cbook, scale
58 from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
61 class _ColorMapping(dict):
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py:22
20 import matplotlib as mpl
21 from matplotlib import _api, _docstring
---> 22 from matplotlib.ticker import (
23 NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
24 NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
25 SymmetricalLogLocator, AsinhLocator, LogitLocator)
26 from matplotlib.transforms import Transform, IdentityTransform
29 class ScaleBase:
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py:143
141 import matplotlib as mpl
142 from matplotlib import _api, cbook
--> 143 from matplotlib import transforms as mtransforms
145 _log = logging.getLogger(__name__)
147 __all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
148 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
149 'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
(...)
155 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
156 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator')
File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py:49
46 from numpy.linalg import inv
48 from matplotlib import _api
---> 49 from matplotlib._path import (
50 affine_transform, count_bboxes_overlapping_bbox, update_path_extents)
51 from .path import Path
53 DEBUG = False
ImportError: numpy.core.multiarray failed to import
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import torch
X, y = make_classification(
n_samples=100,
n_classes=2,
n_features=2,
n_clusters_per_class=1,
n_informative=2,
n_redundant=0,
class_sep=1.2,
random_state=123)
X_train, X_test, y_train, y_test = \
train_test_split(X, y, test_size=0.3, random_state=123)
std_scaler = StandardScaler()
X_train = std_scaler.fit_transform(X_train)
X_test = std_scaler.transform(X_test)
fig, ax = plt.subplots(1, 2, sharex=True, figsize=(7, 3))
ax[0].scatter(
X_train[y_train==0, 0],
X_train[y_train==0, 1],
label='class 0',
marker='o')
ax[0].scatter(
X_train[y_train==1, 0],
X_train[y_train==1, 1],
label='class 1',
marker='s')
ax[0].set_title("Dados de Treinamento")
ax[1].scatter(
X_test[y_test==0, 0],
X_test[y_test==0, 1],
label='class 0',
marker='o')
ax[1].scatter(
X_test[y_test==1, 0],
X_test[y_test==1, 1],
label='class 1',
marker='s')
ax[1].set_title("Dados de Teste")
ax[1].legend(loc='lower right', fontsize=8)
plt.show()
Perceptron em Pytorch#

def set_device(on_gpu=True):
has_mps = torch.backends.mps.is_available()
has_cuda = torch.cuda.is_available()
return "mps" if (has_mps and on_gpu) \
else "cuda" if (has_cuda and on_gpu) \
else "cpu"
device = set_device(on_gpu=True)
class Perceptron():
def __init__(self, num_features):
self.num_features = num_features
self.weights = torch.zeros(
num_features, 1, dtype=torch.float32, device=device)
self.bias = torch.zeros(1, dtype=torch.float32, device=device)
self.ones = torch.ones(1, device=device)
self.zeros = torch.zeros(1, device=device)
def forward(self, x):
linear = torch.mm(x, self.weights) + self.bias
predictions = torch.where(linear > 0., self.ones, self.zeros)
return predictions
def backward(self, x, y):
predictions = self.forward(x)
errors = y - predictions
return errors
def train(self, x, y, epochs):
for _ in range(epochs):
for i in range(y.shape[0]):
errors = self.backward(
x[i].reshape(1, self.num_features), y[i]).reshape(-1)
self.weights += (errors * x[i]).reshape(self.num_features, 1)
self.bias += errors
def evaluate(self, x, y):
predictions = self.forward(x).reshape(-1)
accuracy = torch.sum(predictions == y).float() / y.shape[0]
return accuracy
Treinando o Modelo#
ppn = Perceptron(num_features=2)
X_train_tensor = torch.tensor(
X_train,
dtype=torch.float32,
device=device)
y_train_tensor = torch.tensor(
y_train,
dtype=torch.float32,
device=device)
ppn.train(X_train_tensor, y_train_tensor, epochs=5)
print('Model parameters:')
print('\tWeights:', ppn.weights.tolist())
print('\tBias: ', ppn.bias.tolist())
Model parameters:
Weights: [[-1.0376710891723633], [-1.455593466758728]]
Bias: [0.0]
Avaliando o Modelo nos dados de Teste#
X_test_tensor = torch.tensor(
X_test,
dtype=torch.float32,
device=device)
y_test_tensor = torch.tensor(
y_test,
dtype=torch.float32,
device=device)
test_acc = ppn.evaluate(X_test_tensor, y_test_tensor)
print(f'Test set accuracy: {(test_acc*100):.2f}%')
Test set accuracy: 96.67%
Visualizando a Fronteira de Decisão#

w, b = ppn.weights.cpu(), ppn.bias.cpu()
x0_min = -2
x1_min = ( (-(w[0] * x0_min) - b[0])
/ w[1] )
x0_max = 2
x1_max = ( (-(w[0] * x0_max) - b[0])
/ w[1] )
fig, ax = plt.subplots(1, 2, sharex=True, figsize=(7, 3))
ax[0].plot([x0_min, x0_max], [x1_min, x1_max])
ax[1].plot([x0_min, x0_max], [x1_min, x1_max])
ax[0].scatter(X_train[y_train==0, 0], X_train[y_train==0, 1],
label='class 0', marker='o')
ax[0].scatter(X_train[y_train==1, 0], X_train[y_train==1, 1],
label='class 1', marker='s')
ax[0].set_title("Dados de Treinamento")
ax[1].scatter(X_test[y_test==0, 0], X_test[y_test==0, 1],
label='class 0', marker='o')
ax[1].scatter(X_test[y_test==1, 0], X_test[y_test==1, 1],
label='class 1', marker='s')
ax[1].set_title(f"Dados de Teste (Acc.: {test_acc*100:.2f}%)")
plt.tight_layout()
plt.show()
/Users/denismartins/.local/share/virtualenvs/hypernn-G_MaMKSj/lib/python3.11/site-packages/numpy/core/shape_base.py:65: FutureWarning: The input object of type 'Tensor' is an array-like implementing one of the corresponding protocols (`__array__`, `__array_interface__` or `__array_struct__`); but not a sequence (or 0-D). In the future, this object will be coerced as if it was first converted using `np.array(obj)`. To retain the old behaviour, you have to either modify the type 'Tensor', or assign to an empty array created with `np.empty(correct_shape, dtype=object)`.
ary = asanyarray(ary)
/Users/denismartins/.local/share/virtualenvs/hypernn-G_MaMKSj/lib/python3.11/site-packages/numpy/core/shape_base.py:65: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
ary = asanyarray(ary)
Perceptron é um modelo flexível#
Adaline: Adaptive Linear Neuron#
|
Adaline. Image Source: Sebastian Raschka.
|
Gradiente Descendente#
Atualização de parâmetros do modelo utilizando informação do gradiente de uma função de custo (loss) \(\mathcal{L}(w,b)\): \(\nabla(\mathcal{L}(w,b))\).
Ou seja, dar um passo na direção oposta do gradiente. $\(\Delta w = - \eta \times \nabla_w(\mathcal{L}(w,b))\)\( \)\(\Delta b = - \eta \times \nabla_b(\mathcal{L}(w,b))\)$
\(\eta\): learning rate (tamanho do passo) Gradiente da função de custo com respeito a \(w_j\) e a \(b\):
Gradiente Descendente (cont.)#
Podemos escrever a atualização de weights e bias como:
\(\Delta w_j = - \eta (\frac{\partial \mathcal{L}}{\partial w_j})\)
\(\Delta b = - \eta (\frac{\partial \mathcal{L}}{\partial b)}\)
Usamos um algoritmo para computar gradientes com base no conjunto completo de dados de treinamento e atualizar os parâmetros do modelo. Essa atualização se dá realizando um pequeno passo na direção oposta do gradiente da loss \(\Delta \mathcal{L}(w,b)\).
Gradiente Descendente#
Limitação do Perceptron#
Mas e quanto à porta XOR?
Perceptron: Limitações#
|
Limitação do Perceptron. Image Source: Sebastian Raschka.
|
Funções de Ativação (Não-lineares)#
|
Funções de Ativação. Fonte: Prazan NH.
|
Redes Neurais: Empilhando Perceptrons#
MLP: Demo Visual#
Redes Neurais: Empilhando Perceptrons#
Multilayer Perceptron (MLP)
Noção de camada (layer): operações parametrizadas sobre tensores.
Conceito de arquitetura: Camada de entrada, várias camadas escondidas, uma cada de saída. $\(h^{(l)} = \sigma((\mathbf{W}^{(l)})^{T}\mathbf{x}^{(l)}+b^{(l)})\)$
MLP: Treinamento#
O procedimento de treinamento do MLP pode ser resumido em três etapas simples:
A partir da camada de entrada, propagamos adiante (“feed‑forward”) os padrões dos dados de treinamento através da rede para gerar uma saída.
Com base na saída da rede, calculamos a perda (loss) que queremos minimizar usando uma função de perda que descreveremos mais adiante.
Propagamos a loss para trás (back‑propagation), determinamos sua derivada em relação a cada peso e viés da rede, e atualizamos o modelo.
Por fim, após repetirmos essas três etapas por múltiplas épocas e aprendermos os parâmetros de peso e viés do MLP, usamos a propagação adiante para calcular a saída da rede e aplicamos uma função de limiar (threshold) para obter as classes previstas.
MLP: Feed-forward#
Vamos analisar passo a passo a propagação adiante para gerar uma saída a partir dos padrões presentes nos dados de treinamento. Como cada unidade da camada oculta está conectada a todas as unidades das camadas de entrada, primeiro calculamos a unidade de ativação da camada oculta \(a_1^{(h)}\) da seguinte forma:
Aqui, \(z_1^{(h)}\) é a entrada líquida (net input) e \(\sigma(.)\) é a função de ativação, que deve ser diferenciável para permitir o aprendizado dos pesos que conectam os neurônios por meio de uma abordagem baseada em gradiente. Para resolver problemas complexos, como classificação de imagens, precisamos de funções de ativação não lineares no nosso modelo MLP; um exemplo comum é a função sigmoide (logística):
A função sigmoide é uma curva em forma de S que mapeia a entrada líquida \(z\) para uma distribuição logística no intervalo de 0 a 1, cruzando o eixo \(y\) quando \(z = 0\).
MLP: Feed-Forward (cont.)#
Escrevemos a ativação em uma forma mais compacta e vetorizada (para evitar loops): $\( z^{(h)} = x^{(\text{in})}W^{(h)T} + b^{(h)} \)$
Aqui, \(x^{(\text{in})}\) é o nosso vetor de características de dimensão \(1\times m\).
\(W^{(h)}\) é uma matriz de pesos de dimensão \(d\times m\), onde \(d\) representa o número de unidades na camada oculta; consequentemente, a matriz transposta \(W^{(h)T}\) tem dimensão \(m\times d\).
O vetor de viés \(b^{(h)}\) contém \(d\) unidades de bias (uma para cada nó oculto).
Após a multiplicação matricial‑vetorial, obtemos o vetor de entrada líquida \(\;z^{(h)}\;\) de dimensão \(1\times d\), que será usado para calcular a ativação \(a^{(h)} \in \mathbb{R}^{1\times d}\).
MLP: Feed-Forward (cont.)#
Podemos generalizar este cálculo para os \(n\) exemplos do conjunto de treinamento:
Neste caso, \(X^{(\text{in})}\) passa a ser uma matriz \(n\times m\); a multiplicação matricial resulta em uma matriz de entrada líquida \(\;Z^{(h)}\;\) de dimensão \(n\times d\). Por fim, aplicamos a função de ativação \(\sigma(\cdot)\) a cada elemento da matriz de entrada líquida para obter a matriz de ativação \(n\times d\) na camada seguinte (aqui, a camada de saída):
De maneira análoga, podemos escrever a ativação da camada de saída em forma vetorizada para múltiplos exemplos:
Aqui multiplicamos a transposta da matriz \(t\times d\) \(W^{(\text{out})}\) (onde \(t\) é o número de unidades de saída) pela matriz \(n\times d\) \(A^{(h)}\), e somamos o vetor de viés de dimensão \(t\), \(b^{(\text{out})}\), obtendo a matriz \(Z^{(\text{out})}\) de dimensão \(n\times t\). (As linhas desta matriz representam as saídas para cada exemplo.)
Por fim, aplicamos a função sigmoide para obter o valor contínuo da saída do nosso modelo:
Componentes de uma Rede Neural#
Arquitetura: Camadas, parâmetros, neurônios e conexões
Função de ativação
Função de custo/perda (loss)
Otimizador
MLP em PyTorch#
Vamos criar uma MLP simples para classificar dados do problema XOR.
A rede recebe 2 entradas, possui 5 neurônios na camada escondida e 1 neurônio na camada de saída.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import torch
from torch import nn
import torch.optim as optim
def make_xor_dataset(n_per_blob=50, stddev=0.3, random_state=0):
# Source: https://colab.research.google.com/github/tufts-ml-courses/cs135-25s-assignments/blob/main/labs/day11-NeuralNets.ipynb#scrollTo=gqa0ujlzw_fo
random_state = np.random.RandomState(random_state)
cov_22 = np.square(stddev) * np.eye(2)
x_00 = random_state.multivariate_normal([-1, -1], cov_22, size=n_per_blob)
x_01 = random_state.multivariate_normal([-1, +1], cov_22, size=n_per_blob)
x_10 = random_state.multivariate_normal([+1, -1], cov_22, size=n_per_blob)
x_11 = random_state.multivariate_normal([+1, +1], cov_22, size=n_per_blob)
N = n_per_blob * 4
x_N2 = np.vstack([x_00, x_11, x_01, x_10])
assert x_N2.shape == (N, 2)
y_N = np.hstack([np.ones(N//2), np.zeros(N//2)]).astype(np.int32)
assert y_N.shape == (N,)
# Shuffle the order
perm_ids = random_state.permutation(N)
x_N2 = x_N2[perm_ids].copy()
y_N = y_N[perm_ids].copy()
return x_N2, y_N
X, y = make_xor_dataset(n_per_blob=1000)
X = torch.from_numpy(X).type(torch.float)
y = torch.from_numpy(y).type(torch.float)
dataset = pd.DataFrame(X, columns=["x1", "x2"])
dataset["y"] = y
dataset.head(5)
| x1 | x2 | y | |
|---|---|---|---|
| 0 | -1.018523 | -1.134051 | 1.0 |
| 1 | 0.988928 | -0.629781 | 0.0 |
| 2 | 1.263018 | -1.451490 | 0.0 |
| 3 | -1.129139 | -0.655186 | 1.0 |
| 4 | 1.053061 | -1.202237 | 0.0 |
plt.figure(figsize=(4,4))
sns.scatterplot(data=dataset, x="x1", y="x2", hue="y")
plt.legend(bbox_to_anchor=(1.0, 0.5))
sns.despine()
# Split data into train and test sets
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,
y,
test_size=0.2,
random_state=42)
len(X_train), len(X_test), len(y_train), len(y_test)
(3200, 800, 3200, 800)
class LinearMLP(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=2, out_features=5)
self.layer_2 = nn.Linear(in_features=5, out_features=1)
def forward(self, x):
hidden = self.layer_1(x)
logits = self.layer_2(hidden)
return logits
model = LinearMLP()
num_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Num. Parâmetros no modelo:", num_parameters)
model
Num. Parâmetros no modelo: 21
LinearMLP(
(layer_1): Linear(in_features=2, out_features=5, bias=True)
(layer_2): Linear(in_features=5, out_features=1, bias=True)
)
y_logits = model(X_train)[:5]
y_logits
tensor([[ 0.7329],
[ 0.0526],
[-0.1272],
[-0.0342],
[ 0.7385]], grad_fn=<SliceBackward0>)
# Sigmoid para transformar logits em probabilidade
y_pred_probas = torch.sigmoid(y_logits)
y_pred_probas
tensor([[0.6754],
[0.5131],
[0.4682],
[0.4915],
[0.6767]], grad_fn=<SigmoidBackward0>)
# Round para transformar probas em rótulos (labels)
y_hat = torch.round(y_pred_probas)
y_hat
tensor([[1.],
[1.],
[0.],
[0.],
[1.]], grad_fn=<RoundBackward0>)
# Verificando se o modelo acertou
print(torch.eq(y_hat.squeeze(), y[:5].squeeze()))
tensor([ True, False, True, False, False])
def accuracy_fn(y_true, y_pred):
correct = torch.eq(y_true, y_pred).sum().item()
acc = (correct / len(y_pred)) * 100
return acc
from tqdm.notebook import trange
def train_model(epochs, X, y, model):
loss_function = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.03)
ep_loss = [0.]*epochs
with trange(epochs, desc="Training", leave=False) as tepoch:
for epoch in range(epochs):
model.train()
y_logits = model(X).squeeze() # Remove dimensão extra
y_probas = torch.sigmoid(y_logits) # Logits -> Probas
y_hat = torch.round(y_probas) # Probas -> Labels
loss = loss_function(y_probas, y)
ep_loss[epoch] = loss.item()
acc = accuracy_fn(y, y_hat)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
tepoch.set_postfix(loss=loss.item(), train_acc=acc)
tepoch.update(1)
epoch_ticks = [i+1 for i in range(epochs)]
plt.figure(figsize=(4,3))
sns.lineplot(x=epoch_ticks, y=ep_loss)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title("Comportamento da Loss")
sns.despine()
plt.show()
torch.manual_seed(42)
EPOCHS = 100
train_model(EPOCHS, X_train, y_train, model)
from sklearn.metrics import classification_report
model.eval()
with torch.inference_mode():
y_pred = torch.round(torch.sigmoid(model(X_test).squeeze()))
y_pred = y_pred.detach().numpy()
print(classification_report(y_test, y_pred))
precision recall f1-score support
0.0 0.49 0.49 0.49 403
1.0 0.48 0.48 0.48 397
accuracy 0.48 800
macro avg 0.48 0.48 0.48 800
weighted avg 0.48 0.48 0.48 800
# Modelo Não-Linear
class NonLinearMLP(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=2, out_features=5)
self.layer_2 = nn.Linear(in_features=5, out_features=1)
self.relu = nn.ReLU()
def forward(self, x):
z_1 = self.layer_1(x)
h_1 = self.relu(z_1)
z_2 = self.layer_2(h_1)
return z_2
model = NonLinearMLP()
num_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Num. Parâmetros no modelo:", num_parameters)
model
Num. Parâmetros no modelo: 21
NonLinearMLP(
(layer_1): Linear(in_features=2, out_features=5, bias=True)
(layer_2): Linear(in_features=5, out_features=1, bias=True)
(relu): ReLU()
)
train_model(EPOCHS, X_train, y_train, model)
model.eval()
with torch.inference_mode():
y_pred = torch.round(torch.sigmoid(model(X_test).squeeze()))
y_pred = y_pred.detach().numpy()
print(classification_report(y_test, y_pred))
precision recall f1-score support
0.0 0.65 1.00 0.79 403
1.0 0.99 0.47 0.63 397
accuracy 0.73 800
macro avg 0.82 0.73 0.71 800
weighted avg 0.82 0.73 0.71 800
MLP para o MNIST dataset#
Camada de Entrada da MLP para o MNIST dataset. Dígitos são imagens de 28x28 pixels (ou seja, 784 dimensões). Fonte: ML4A.
MLP MNIST: Tutorial Visual (YouTube)#
from IPython.display import YouTubeVideo
YouTubeVideo("aircAruvnKk", width=600, height=350)
MLP e MNIST em Pytorch#
from torchvision import datasets
import torchvision.transforms as transforms
import torch.utils.data as data
# Número de processos para o dataloader
NUM_WORKERS = 0
# Quantas amostras (imagens) por batch
BATCH_SIZE = 128
# Converte dados em tensores
transform = transforms.ToTensor()
# Carrega dados de treino e teste
train_data = datasets.MNIST(root='data', train=True,
download=True, transform=transform)
test_data = datasets.MNIST(root='data', train=False,
download=True, transform=transform)
# Cria dataset de validação
VALIDATION_SIZE = 0.1
n_train_examples = int(len(train_data) * VALIDATION_SIZE)
n_valid_examples = len(train_data) - n_train_examples
train_data, valid_data = data.random_split(
train_data, [n_train_examples, n_valid_examples])
# Data Loaders
train_loader = torch.utils.data.DataLoader(train_data, shuffle=True,
batch_size=BATCH_SIZE, num_workers=NUM_WORKERS)
valid_loader = torch.utils.data.DataLoader(valid_data,
batch_size=BATCH_SIZE, num_workers=NUM_WORKERS)
test_loader = torch.utils.data.DataLoader(test_data,
batch_size=BATCH_SIZE, num_workers=NUM_WORKERS)
Arquitetura da Rede Neural#
import torch.nn as nn
import torch.nn.functional as F
class MLPNet(nn.Module):
def __init__(self):
super(MLPNet, self).__init__()
self.flatten = nn.Flatten()
# input layer
self.fc1 = nn.Linear(28 * 28, 64)
# linear layer (n_hidden -> hidden_2)
self.fc2 = nn.Linear(64, 32)
# linear layer (n_hidden -> 10)
self.fc3 = nn.Linear(32, 10)
def forward(self, x):
x = self.flatten(x)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
Função de Custo/Loss para Classificação#
Cross‑Entropy Loss: \(\mathcal{L}(\mathbf{y},\hat{\mathbf{y}})= -\sum_{k=1}^{K} y_k\,\log(\hat y_k)\)
Intuição
Mede a divergência entre o vetor de rótulos reais \(y\) (ou \(y_k\)) e a distribuição predita \(\hat{y}\).
Penaliza fortemente previsões que atribuem baixa probabilidade ao verdadeiro rótulo.
Propriedades importantes
Não‑negatividade: \(\mathcal{L} \ge 0\); zero apenas quando \(\hat y = y\).
Derivada simples: facilita a implementação do algoritmo de backpropagation.
Quando usar: Binária ou multiclasse quando as saídas são interpretadas como probabilidades (softmax ou sigmoid).
import torch.optim as optim
EPOCHS = 10
device = set_device(on_gpu=True)
model = MLPNet().to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.03)
num_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Num. Parâmetros no modelo:", num_parameters)
Num. Parâmetros no modelo: 52650
Loop de Treinamento#
from IPython.display import YouTubeVideo
YouTubeVideo("Nutpusq_AFw", width=600, height=350)
def correct(output, target):
predicted_digits = output.argmax(1) # Posição com maior valor da saída
correct_ones = (predicted_digits == target).type(torch.float) # 1.0 se acertou, 0.0 caso contrário
return correct_ones.sum().item() # Conta o número de acertos
def train_model_mini_batch(data_loader, model, loss_function, optimizer):
model.train()
num_batches = len(data_loader)
num_items = len(data_loader.dataset)
total_loss = 0
total_correct = 0
for data, target in data_loader:
# Copy data and targets to GPU
data = data.to(device)
target = target.to(device)
# Do a forward pass
output = model(data)
# Calculate the loss
loss = loss_function(output, target)
total_loss += loss
# Count number of correct digits
total_correct += correct(output, target)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss = total_loss/num_batches
accuracy = total_correct/num_items
print(f"{train_loss:7f}\t|\t {accuracy:.2%}")
%%time
print("Epoch\t|\tAvg. Loss\t|\tAccuracy")
for epoch in range(EPOCHS):
print(f"{epoch+1}\t|\t", end=" ")
train_model_mini_batch(train_loader, model, loss_function, optimizer)
Epoch | Avg. Loss | Accuracy
1 | 2.295317 | 10.82%
2 | 2.246656 | 23.77%
3 | 2.155340 | 43.27%
4 | 1.960779 | 53.75%
5 | 1.612798 | 62.15%
6 | 1.236662 | 69.22%
7 | 0.978333 | 74.67%
8 | 0.813973 | 79.18%
9 | 0.702173 | 81.78%
10 | 0.622700 | 83.23%
CPU times: user 3.18 s, sys: 415 ms, total: 3.59 s
Wall time: 5.79 s
Verificando desempenho no dataset de validação#
from tqdm.notebook import trange
def train_model_mini_batch_with_validation(epochs, train_loader, valid_loader, model, loss_function, optimizer):
tr_loss = [0.]*epochs
val_loss = [0.]*epochs
with trange(epochs, desc="Training", leave=False) as tepoch:
for epoch in range(epochs):
model.train()
train_total_loss = 0.0
total_correct = 0.0
for data, target in train_loader:
# Copia dados para o device (ex.: GPU)
data = data.to(device)
target = target.to(device)
# Forward pass
output = model(data)
# Calcula loss
loss = loss_function(output, target)
train_total_loss += loss.item()
# Conta acertos
total_correct += correct(output, target)
# Backward pass: Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
avg_tr_loss = train_total_loss / len(train_loader)
tr_loss[epoch] = avg_tr_loss
training_accuracy = total_correct / len(train_loader.dataset) * 100
# Validation phase
model.eval() # Set the model to evaluation mode
val_total_loss = 0.0
val_total_correct = 0
with torch.no_grad():
for data, target in valid_loader:
data = data.to(device)
target = target.to(device)
# Forward pass
output = model(data)
# Calcula loss
loss = loss_function(output, target)
val_total_loss += loss.item()
# Conta acertos
val_total_correct += correct(output, target)
avg_val_loss = val_total_loss / len(valid_loader)
val_loss[epoch] = avg_val_loss
validation_accuracy = val_total_correct / len(valid_loader.dataset) * 100
tepoch.set_postfix(train_loss=avg_tr_loss, train_acc=training_accuracy, val_loss=avg_val_loss, val_acc=validation_accuracy)
tepoch.update(1)
epoch_ticks = [i+1 for i in range(epochs)]
plt.figure(figsize=(4,3))
sns.lineplot(x=epoch_ticks, y=tr_loss, marker="o", label="Train. Loss" )
sns.lineplot(x=epoch_ticks, y=val_loss, marker="^", linestyle="--", label="Valid. Loss")
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title("Comportamento da Loss")
plt.legend(loc='lower left')
sns.despine()
plt.show()
train_model_mini_batch_with_validation(EPOCHS, train_loader, valid_loader, model, loss_function, optimizer)
Testando o Modelo#
Avaliar se o modelo generaliza bem para dados que não foram vistos pela rede durante o treinamento (unseen data).
Importante: usar
model.eval()para configurar o modelo em evaluation mode.
def get_predictions(model, iterator, device):
model.eval()
images, labels, probs = [], [], []
with torch.no_grad():
for (x, y) in iterator:
x = x.view(-1, 28*28).to(device)
y = y.to(device)
y_pred = model(x)
y_prob = F.softmax(y_pred, dim=-1)
images.append(x.cpu())
labels.append(y.cpu())
probs.append(y_prob.cpu())
images = torch.cat(images, dim=0)
labels = torch.cat(labels, dim=0)
probs = torch.cat(probs, dim=0)
return images, labels, probs
Função de Ativação na Camada de Saída#
Camada de Saída com Softmax: Cabeça de Classificação (Classification Head)
Propósito principal: Converter vetores de logits (saída da última camada escondida) \(z \in \mathbb{R}^{K}\) em uma distribuição de probabilidade sobre \(K\) classes.
Definição matemática: \(\sigma(z)_k = \frac{\exp(z_k)}{\sum_{j=1}^{K} \exp(z_j)}, \qquad k=1,\dots ,K\)
Cada componente fica no intervalo \((0,1)\) e a soma totaliza \(1\).
Derivada simples: \( \frac{\partial \sigma(z)_i}{\partial z_j} = \sigma(z)_i (\delta_{ij} - \sigma(z)_j)\), onde \(\delta_{ij}\) é a função indicadora.
Junto com a loss Cross‑Entropy, forma o padrão ouro “Softmax + Cross‑Entropy”.
Função Softmax para classificação. Fonte: Abhisek Jana.
images, labels, probs = get_predictions(model, test_loader, device)
pred_labels = torch.argmax(probs, 1)
import sklearn.metrics as mtr
import seaborn as sns
fig = plt.figure(figsize=(7, 5))
ax = fig.add_subplot(1, 1, 1)
cm = mtr.confusion_matrix(labels, pred_labels)
sns.heatmap(
cm, annot=True, fmt='d', cmap='bone_r', cbar=False,
square=True, linewidths=3, linecolor="w", ax=ax)
plt.show()
pred_labels.shape
torch.Size([10000])
import numpy as np
dataiter = iter(test_loader)
images, labels = next(dataiter)
images = images.view(-1, 28*28).to(device)
labels = labels.to(device)
output = model(images)
_, preds = torch.max(output, 1)
images = images.cpu()
fig = plt.figure(figsize=(3, 3))
for idx in np.arange(20):
ax = fig.add_subplot(4, int(20/4), idx+1, xticks=[], yticks=[])
ax.imshow(images[idx].view(28,28), cmap='gray')
ax.set_title("{} ({})".format(str(preds[idx].item()), str(labels[idx].item())),
color=("green" if preds[idx]==labels[idx] else "red"))
plt.tight_layout()
plt.show()
Salvando o modelo#
torch.save(model.state_dict(), "mlp-mnist-model.pth")
Carregando o modelo#
model = MLPNet()
model.load_state_dict(torch.load("mlp-mnist-model.pth"))
model.eval();
Resumo#
|
Leitura Recomendada: Capítulo 6.
|