Regresión¶

Supongamos que deseamos aproximar una función arbitrariamente $f(x)$ haciendo uso de un perceptrón multicapa. ¿En qué modo afecta el número de neuronas en la capa intermedia a la calidad de la aproximación?

Propongamos como ejemplo la función de una dimensión: \begin{align} f(x) = \frac{x}{2} \left(\text{sen}(20x) - \text{cos}\left(\frac{20}{3}x\right)\right) \end{align}

In [4]:
import numpy as np
In [5]:
def f(x):
    """
    Función objetivo.
    Intentaremos que un perceptrón multicapa la aproxime.
    """
    return x/2 * (np.sin(20*x) - np.cos(20*x/3))
In [6]:
# Rango de valores a estudiar
x = np.arange(-1, 1, 0.02)
y = f(x)
In [7]:
import matplotlib.pyplot as plt
plt.style.use('ggplot')
In [9]:
# Visualización de la función objetivo
plt.plot(x, y)
plt.title('$f(x) = \\frac{x}{2} \\left( sin(20x) - cos(\\frac{20}{3}x) \\right)$')
plt.xlabel('$x$')
plt.ylabel('$y$')
Out[9]:
Text(0, 0.5, '$y$')
In [6]:
# Instalar pyTorch según:
# https://pytorch.org/get-started/locally/
# Elegir versión.
# Ej: Para ejecutar desde aquí versión para cpu descomentar
# CUDA:
#!pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116
# SIN CUDA:
#!pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu
In [7]:
import torch
from torch import nn
In [8]:
# Vector 1D a matriz columna
x_vec = np.reshape(x, (len(x),1))    # Arreglo de numpy
x_vec.dtype
Out[8]:
dtype('float64')
In [9]:
# Matriz de numpy a tensor de pyTorch
X = torch.tensor(x_vec.astype(np.float32))  # Tensor 1D
X.dtype
Out[9]:
torch.float32
In [10]:
# Vector de salidas esperadas
y_vec = np.reshape(y, (len(y),1))
Y = torch.tensor(y_vec.astype(np.float32))  # Tensor 1D
In [11]:
class NeuralNetwork(nn.Module):
    """
    Red neuronal de tres capas.
    Las capas intermedias utilizan una sigmoide como función de activación.
    La capa final sólo calcula la combinación lineal, pues la sigmoide no
    puede salir del rango [0,1], para este problema de regresión no es
    adecuada.
    """
    def __init__(self, s1):
        """
        param s1: Número de neuronas en la capa oculta
        """
        super(NeuralNetwork, self).__init__()
        self.layers_stack = nn.Sequential(
            nn.Linear(1, s1, bias=True),
            nn.Sigmoid(),
            nn.Linear(s1, 1, bias=True),
        )
        
    def forward(self, x):
        """
        Evaluación de la red sobre la entrada x
        usando alimentación hacia adelante para los pesos
        actuales.
        """
        logits = self.layers_stack(x)
        return logits
In [12]:
import math
In [13]:
def train(X, Y, model, learning_rate, num_steps):
    """
    Función que realiza el entrenamiento:
    Intentará reducir la distancia entre los valores que salen
    de la red y lo que deseamos modificando los pesos de la red.
    """
    size = len(X)
    loss_fn = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    #optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
    step_report = 100
    num_reports = int(math.ceil(num_steps/step_report))
    e = np.zeros(num_reports)
    for i in range(num_steps):
        # Predicción y error
        Y_hat = model(X)
        loss = loss_fn(Y_hat, Y)
        
        # Reduciendo el error
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # Cómo vamos
        if i % step_report == 0:
            num_report = int(i/step_report)
            e[num_report] = loss.item()
            #print(f"{num_report} loss: {loss:>7f}")
    #plt.plot(e)
    return Y_hat, e
In [14]:
# Versión interactiva
from ipywidgets import interact_manual
import ipywidgets as widgets
In [17]:
def grafica_entrenamiento(x, e, y_pred):
    """
    Grafica la evolución de la función de error
    y la función que evalúa actualmente la red.
    """
    fig, axes = plt.subplots(1, 2, figsize=(12,6))
    axes[0].plot(np.arange(len(e))*100,e)
    axes[0].set_xlabel('pasos')
    axes[0].set_ylabel('error')
    axes[1].plot(x, y_pred)
    axes[1].set_xlabel('$x$')
    axes[1].set_ylabel('$y$')
In [18]:
learning_rates = {2: 0.025, 5: 0.0015, 50:0.001, 500:0.0005, 5000:0.0001}
num_steps = {2: 4000, 5: 17000, 50: 10000, 500: 10000, 5000: 10000}
model = None
@interact_manual(
    s1 = widgets.SelectionSlider(
        options = [2,5,50,500,5000]
    )
)
def aproxima(s1):
    learning_rate = learning_rates[s1]
    n_steps = num_steps[s1]
    global model
    model = NeuralNetwork(s1)
    y_pred, e = train(X, Y, model, learning_rate, n_steps)
    y_pred = y_pred.detach().numpy()
    
    grafica_entrenamiento(x, e, y_pred)
interactive(children=(SelectionSlider(description='s1', options=(2, 5, 50, 500, 5000), value=2), Button(descri…
In [19]:
@interact_manual(
    learning_rate = widgets.FloatLogSlider(value=0.001,
                                           base=10,
                                           min=-7, # min exponent of base
                                           max=3, # min exponent of base
                                           step=0.2), # exponent step,
    num_steps = widgets.FloatLogSlider(value=1000,
                                           base=10,
                                           min=3, # min exponent of base
                                           max=5, # min exponent of base
                                           step=0.2), # exponent step,
)
def entrena_mas(learning_rate, num_steps):
    n_steps = int(num_steps)
    global model
    y_pred, e = train(X, Y, model, learning_rate, n_steps)
    y_pred = y_pred.detach().numpy()
    
    grafica_entrenamiento(x, e, y_pred)
interactive(children=(FloatLogSlider(value=0.001, description='learning_rate', max=3.0, min=-7.0, step=0.2), F…

@veroarriola

In [ ]: