1vtleffl2ewkxrx1u0negcw.png

En primer lugar, necesitamos datos sintéticos con los que trabajar. Los datos deberían exhibir alguna dependencia no lineal. Definámoslo así:

Imagen por autor.

En Python tendrá la siguiente forma:

np.random.seed(42)
X = np.random.normal(1, 4.5, 10000)
y = np.piecewise(X, [X < -2,(X >= -2) & (X < 2), X >= 2], [lambda X: 2*X + 5, lambda X: 7.3*np.sin(X), lambda X: -0.03*X**3 + 2]) + np.random.normal(0, 1, X.shape)

Después de la visualización:

Imagen por autor.

Como estamos visualizando un espacio 3D, nuestra red neuronal solo tendrá 2 pesos. Esto significa que la RNA estará formada por una única neurona oculta. Implementar esto en PyTorch es bastante intuitivo:

class ANN(nn.Module):
def __init__(self, input_size, N, output_size):
super().__init__()
self.net = nn.Sequential()
self.net.add_module(name='Layer_1', module=nn.Linear(input_size, N, bias=False))
self.net.add_module(name='Tanh',module=nn.Tanh())
self.net.add_module(name='Layer_2',module=nn.Linear(N, output_size, bias=False))

def forward(self, x):
return self.net(x)

¡Importante! No olvides desactivar los sesgos en tus capas, de lo contrario terminarás teniendo x2 más parámetros.

Imagen por autor.

Para construir la superficie de error, primero necesitamos crear una cuadrícula de valores posibles para W1 y W2. Luego, para cada combinación de pesos, actualizaremos los parámetros de la red y calcularemos el error:

W1, W2 = np.arange(-2, 2, 0.05), np.arange(-2, 2, 0.05)
LOSS = np.zeros((len(W1), len(W2)))
for i, w1 in enumerate(W1):
model.net._modules['Layer_1'].weight.data = torch.tensor([[w1]], dtype=torch.float32)

for j, w2 in enumerate(W2):
model.net._modules['Layer_2'].weight.data = torch.tensor([[w2]], dtype=torch.float32)

model.eval()
total_loss = 0
with torch.no_grad():
for x, y in test_loader:
preds = model(x.reshape(-1, 1))
total_loss += loss(preds, y).item()

LOSS[i, j] = total_loss / len(test_loader)

Puede que lleve algún tiempo. Si hace que la resolución de esta cuadrícula sea demasiado gruesa (es decir, el tamaño del paso entre posibles valores de peso), es posible que se pierdan los mínimos y máximos locales. ¿Recuerda que a menudo está previsto que la tasa de aprendizaje disminuya con el tiempo? Cuando hacemos esto, el cambio absoluto en los valores de peso puede ser tan pequeño como 1e-3 o menos. ¡Una cuadrícula con un paso de 0,5 simplemente no capturará estos finos detalles de la superficie de error!

En este punto, no nos importa en absoluto la calidad del modelo entrenado. Sin embargo, queremos prestar atención a la tasa de aprendizaje, así que mantengámosla entre 1e-1 y 1e-2. Simplemente recopilaremos los valores de peso y los errores durante el proceso de entrenamiento y los almacenaremos en listas separadas:

model = ANN(1,1,1)
epochs = 25
lr = 1e-2

optimizer = optim.SGD(model.parameters(),lr =lr)

model.net._modules['Layer_1'].weight.data = torch.tensor([[-1]], dtype=torch.float32)
model.net._modules['Layer_2'].weight.data = torch.tensor([[-1]], dtype=torch.float32)

errors, weights_1, weights_2 = [], [], []

model.eval()
with torch.no_grad():
total_loss = 0
for x, y in test_loader:
preds = model(x.reshape(-1,1))
error = loss(preds, y)
total_loss += error.item()
weights_1.append(model.net._modules['Layer_1'].weight.data.item())
weights_2.append(model.net._modules['Layer_2'].weight.data.item())
errors.append(total_loss / len(test_loader))

for epoch in tqdm(range(epochs)):
model.train()

for x, y in train_loader:
pred = model(x.reshape(-1,1))
error = loss(pred, y)
optimizer.zero_grad()
error.backward()
optimizer.step()

model.eval()
test_preds, true = [], []
with torch.no_grad():
total_loss = 0
for x, y in test_loader:
preds = model(x.reshape(-1,1))
error = loss(preds, y)
test_preds.append(preds)
true.append(y)

total_loss += error.item()
weights_1.append(model.net._modules['Layer_1'].weight.data.item())
weights_2.append(model.net._modules['Layer_2'].weight.data.item())
errors.append(total_loss / len(test_loader))

Imagen por autor.

Finalmente, podemos visualizar los datos que hemos recopilado usando plotly. La trama tendrá dos escenas: superficie y trayectoria SGD. Una de las formas de hacer la primera parte es crear una figura con un argumento. superficie. Después de eso, le daremos un poco de estilo actualizando un diseño.

La segunda parte es tan simple como es: solo usa dispersión3d función y especificar los tres ejes.

import plotly.graph_objects as go
import plotly.io as pio

plotly_template = pio.templates["plotly_dark"]
fig = go.Figure(data=[go.Surface(z=LOSS, x=W1, y=W2)])

fig.update_layout(
title='Loss Surface',
scene=dict(
xaxis_title='w1',
yaxis_title='w2',
zaxis_title='Loss',
aspectmode='manual',
aspectratio=dict(x=1, y=1, z=0.5),
xaxis=dict(showgrid=False),
yaxis=dict(showgrid=False),
zaxis=dict(showgrid=False),
),
width=800,
height=800
)

fig.add_trace(go.Scatter3d(x=weights_2, y=weights_1, z=errors,
mode='lines+markers',
line=dict(color='red', width=2),
marker=dict(size=4, color='yellow') ))
fig.show()

Ejecutarlo en Google Colab o localmente en Jupyter Notebook le permitirá investigar la superficie del error más de cerca. Honestamente, pasé mucho tiempo mirando esta figura 🙂

Imagen por autor.

Me encantaría verte en la superficie, así que siéntete libre de compartirlo en los comentarios. ¡Creo firmemente que cuanto más imperfecta es la superficie, más interesante es investigarla!

=============================================

Todas mis publicaciones en Medium son gratuitas y de acceso abierto, ¡por eso te agradecería mucho que me siguieras aquí!

PD: Soy un apasionado de la ciencia (geo)datos, el aprendizaje automático/inteligencia artificial y el cambio climático. Entonces, si quieres trabajar juntos en algún proyecto, por favor contáctame en LinkedIn y echa un vistazo mi sitio web!

🛰️Sigue para más🛰️