Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Perceptron Multicamadas

Open In Colab

Perceptron Multicamadas

O perceptron multicamadas é um modelo que possui um conjunto de perceptrons organizados em múltiplas camadas (conforme o nome).

Em sua forma mais simples, possuímos três camadas: a camada de entrada, a camada oculta, e a camada de saída. A camada de entrada não possui perceptrons. Porém, é por ela que os sinais de entrada serão recebidos. Na camada oculta, podemos ter diversos perceptrons posicionados em paralelo. Por fim, na camada de saída, temos um perceptron (no caso de classificação binária).

É interessante observar que, enquanto os perceptrons da camada oculta recebem os sinais de entrada (multiplicados pelos pesos “sinápticos”), a camada de saída recebe os sinais emitidos pelos perceptrons da camada oculta. Isto é similar a como os dendritos (entradas) de um neurônio recebem sinais dos axônios (saídas) de outro neurônio, formando uma rede neural artificial.

Também é importante ressaltar que não estamos limitados a apenas três camadas. Podemos ter diversas camadas de perceptrons, cada uma recebendo sinais emitidos pela camada anterior e emitindo para a próxima camada.

Vejamos abaixo a implementação do perceptron multicamadas em Python.

import numpy as np
class MultilayerPerceptron:
    def __init__(self, input_size, hidden_size, learning_rate=0.01, epochs=1000):
        self.weights_input_hidden = np.random.randn(input_size, hidden_size)
        self.bias_hidden = np.zeros(hidden_size)
        self.weights_hidden_output = np.random.randn(hidden_size, 1)
        self.bias_output = np.zeros(1)
        self.learning_rate = learning_rate
        self.epochs = epochs

    def _sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def _sigmoid_derivative(self, z):
        # sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
        # Simplificando, com z = sigmoid(x)
        return z * (1 - z)

    def forward(self, X):
        self.hidden_input = np.dot(X, self.weights_input_hidden) + self.bias_hidden
        self.hidden_output = self._sigmoid(self.hidden_input)
        self.final_input = np.dot(self.hidden_output, self.weights_hidden_output) + self.bias_output
        self.final_output = self._sigmoid(self.final_input)
        return self.final_output

    def backward(self, X, y):
        # Calcula o erro e o gradiente na camada de saída
        output_error = y - self.final_output
        output_delta = output_error * self._sigmoid_derivative(self.final_output)
        # Calcula o erro e o gradiente na camada oculta
        hidden_error = output_delta.dot(self.weights_hidden_output.T)
        hidden_delta = hidden_error * self._sigmoid_derivative(self.hidden_output)
        # Ajusta os pesos e viés por meio de gradient descent
        self.weights_hidden_output += self.hidden_output.T.dot(output_delta) * self.learning_rate
        self.bias_output += np.sum(output_delta, axis=0) * self.learning_rate
        self.weights_input_hidden += X.T.dot(hidden_delta) * self.learning_rate
        self.bias_hidden += np.sum(hidden_delta, axis=0) * self.learning_rate

    def fit(self, X, y):
        for _ in range(self.epochs):
            self.forward(X)
            self.backward(X, y)

    def predict(self, X):
        return self.forward(X)

Agora, realizamos o treino do Perceptron Multicamadas no problema não-linear. Da mesma forma que com o perceptron, realizamos o treino no método fit e a previsão no método predict.

Percebe-se que este novo modelo consegue criar uma borda de decisão não-linear, classificando corretamente a maior parte dos exemplos.

import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
def plot_decision_boundary(model, X, y, axis=None):
    xx, yy = np.meshgrid(np.arange(X[:,0].min(), X[:,0].max(), 0.01),
                         np.arange(X[:,1].min(), X[:,1].max(), 0.01))

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    if axis is None:
        plt.contourf(xx, yy, Z, alpha=0.5, cmap='cividis', antialiased=True)
        plt.scatter(X[y==0,0], X[y==0,1], s=10)
        plt.scatter(X[y==1,0], X[y==1,1], s=10)
        plt.xlim(X[:,0].min(), X[:,0].max())
        plt.ylim(X[:,1].min(), X[:,1].max())
        plt.xlabel('x0')
        plt.ylabel('x1')
    else:
        axis.contourf(xx, yy, Z, alpha=0.5, cmap='cividis', antialiased=True)
        axis.scatter(X[y==0,0], X[y==0,1], s=10)
        axis.scatter(X[y==1,0], X[y==1,1], s=10)
        axis.set_xlim(X[:,0].min(), X[:,0].max())
        axis.set_ylim(X[:,1].min(), X[:,1].max())
        axis.set_xlabel('x0')
        axis.set_ylabel('x1')
X, y = make_moons(n_samples=200, noise=0.1, random_state=42)
y = np.expand_dims(y, axis=1)

model = MultilayerPerceptron(input_size=X.shape[1], hidden_size=3, epochs=10_000)
model.fit(X, y)
y_pred = model.predict(X)

plot_decision_boundary(model, X, y[:,0])
plt.show()
<Figure size 640x480 with 1 Axes>

Desbravando do Multilayer Perceptron

Vamos entender, então, como o Perceptron Multicamadas consegue resolver o problema proposto. Para isto, vamos analisar cada neurônio e entender um pouco sobre a função de ativação.

Analisando a Camada Oculta

Para entender o que ocorre dentro deste modelo, vamos criar uma classe para facilitar o acesso à sua camada oculta.

class MLProbe():
    def __init__(self, model, probe_position=0):
        self.model = model
        self.probe_position = probe_position

    def predict(self, X):
        self.model.forward(X)
        return model.hidden_output[:,self.probe_position]

Assim, vamos analisar separadamente a saída de cada um dos três perceptrons da camada oculta.

fig, axs = plt.subplots(1, 3, figsize=(18,4))
for i in range(3):
    probed_model = MLProbe(model, i)
    axs[i].set_title(f'Neuronio Oculto {i+1}')
    plot_decision_boundary(probed_model, X, y[:,0], axis=axs[i])
<Figure size 1800x400 with 3 Axes>

Conseguimos verificar que cada perceptron da camada oculta cria uma borda de classificação linear, cada uma em uma região diferente. Ao combinarmos estas bordas de decisão no perceptron da camada de saída, temos a saída com borda não-linear.

Função de Ativação

Um ponto interessante que verificamos nas saídas dos perceptrons multicamadas é que a borda de classificação apresenta um variação gradual entre uma classe e outra. Isto ocorre porque, ao invés de utilizarmos a função de ativação do perceptron original, utilizamos a função sigmóide, definida abaixo.

δ(x)=11+ex\delta(x) = \frac{1}{1+e^{-x}}

No gráfico a seguir conseguimos verificar o formato desta função, que parece um “S”. Para valores muito baixos de xx, o valor de saída da função é próximo a zero. Porém, a medida que xx se aproxima de 0, a saída começa a aumentar até chegar em 0.5. Após isto, enquanto o valor de xx aumenta, a saída da função aumenta até se aproximar do valor de 1.0.

De certa forma, esta função é parecida com a função de ativação do perceptron. Porém, esta função tem como característica ser contínua e derivável, o que permite o treinamento do perceptron multicamadas.

x = np.linspace(-10,10,100)
y = 1/(1+np.exp(-x))

plt.plot(x, y)
plt.grid()
plt.xlabel('x')
plt.ylabel('sigmoid(x)')
plt.show()
<Figure size 640x480 with 1 Axes>

Otimização

Por fim, ocorre a etapa de otimização da rede neural, cujo propósito é ajustar os pesos do modelo de forma a minimizar o erro de predição. Isso é viabilizado pelo algoritmo de backpropagation, introduzido em 1986, um marco decisivo na história da inteligência artificial. A partir dos gradientes calculados por backpropagation, os parâmetros de redes neurais com múltiplas camadas podem ser atualizados iterativamente por meio do método de descida do gradiente, reduzindo progressivamente o erro do modelo.

Exercícios

  1. Modifique a chamada do Perceptron Multicamadas para utilizar apenas um perceptron na camada oculta. Depois, utilize apenas dois perceptrons. Foi possível resolver o problema não-linear? Discuta a resposta e justifique.

  2. Reproduza o código do Multilayer Perceptron utilizando Pytorch.

Referências

  • Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. nature, 323(6088), 533-536.