Quant500 Academy

4. Una red en NumPy y otra con Scikit-Learn

Nivel 11: Redes Neuronales para Trading

Implementar una red desde cero al menos una vez es un ejercicio extremadamente valioso. Te obliga a entender cómo funciona por dentro, y eso es esencial para diseñar modelos efectivos después.

Un guion de entrenamiento típico tiene siempre los mismos pasos: definir la red, inicializar los parámetros, recorrer las entradas, propagar hacia delante para obtener la salida, calcular la pérdida con la salida y la etiqueta, retropropagar el gradiente y actualizar los parámetros con descenso de gradiente.

Aquí tienes una red de tres capas completa, sin ninguna librería de aprendizaje. Las entradas son cuatro señales binarias por día (por ejemplo: precio por encima de la media de 50, RSI por encima de 50, volumen por encima de su media, y vela alcista) y la etiqueta es si el cierre de mañana sube.

import numpy as np

# Cada fila es un dia; cada columna una senal binaria
X = np.array([[1, 0, 1, 0],
              [1, 0, 1, 1],
              [0, 1, 0, 1]])

# Etiqueta: 1 si manana sube
y = np.array([[1], [1], [0]])

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# OJO: recibe la SALIDA ya activada, no la preactivacion
def derivatives_sigmoid(x):
    return x * (1 - x)

epoch = 500        # iteraciones de entrenamiento
lr = 0.5           # tasa de aprendizaje
inputlayer_neurons = X.shape[1]
hiddenlayer_neurons = 5
output_neurons = 1

wh = np.random.uniform(size=(inputlayer_neurons, hiddenlayer_neurons))
bh = np.random.uniform(size=(1, hiddenlayer_neurons))
wout = np.random.uniform(size=(hiddenlayer_neurons, output_neurons))
bout = np.random.uniform(size=(1, output_neurons))

for i in range(epoch):
    # --- Pasada hacia delante ---
    hidden_layer_input = np.dot(X, wh) + bh
    hiddenlayer_activations = sigmoid(hidden_layer_input)
    output_layer_input = np.dot(hiddenlayer_activations, wout) + bout
    output = sigmoid(output_layer_input)

    # --- Pasada hacia atras ---
    E = y - output
    slope_output_layer = derivatives_sigmoid(output)
    slope_hidden_layer = derivatives_sigmoid(hiddenlayer_activations)
    d_output = E * slope_output_layer
    Error_at_hidden_layer = d_output.dot(wout.T)   # antes de tocar wout
    d_hiddenlayer = Error_at_hidden_layer * slope_hidden_layer

    # --- Actualizacion ---
    wout += hiddenlayer_activations.T.dot(d_output) * lr
    bout += np.sum(d_output, axis=0, keepdims=True) * lr
    wh += X.T.dot(d_hiddenlayer) * lr
    bh += np.sum(d_hiddenlayer, axis=0, keepdims=True) * lr

print(output)

Fíjate en tres detalles. Primero: la derivada de la sigmoide se calcula como la salida por (uno menos la salida), lo que significa que hay que pasarle el valor ya activado, no la preactivación; si le pasas hidden_layer_input en vez de hiddenlayer_activations el gradiente es basura y la red no aprende. Segundo: Error_at_hidden_layer se calcula antes de actualizar wout, porque necesita los pesos con los que se hizo la pasada hacia delante; invertir ese orden es un error clásico y silencioso. Tercero: los pesos se suman en lugar de restarse porque E está definido como real menos predicho, así que el signo ya está incorporado.

Con Scikit-Learn. En producción no escribes esto a mano. Scikit-Learn trae perceptrones multicapa listos para usar, con MLPRegressor para regresión y MLPClassifier para clasificación.

from sklearn.neural_network import MLPRegressor
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-1.5, 1.5, 0.01).reshape(-1, 1)
y = np.sin(2 * np.pi * x).ravel()

nn = MLPRegressor(
    hidden_layer_sizes=(4,), activation='tanh', solver='lbfgs', alpha=0.001,
    learning_rate='constant', learning_rate_init=0.1, max_iter=5000,
    shuffle=True, random_state=9, tol=0.0001, momentum=0.9,
    nesterovs_momentum=True, early_stopping=False, validation_fraction=0.1)

nn.fit(x, y)
test_x = np.arange(-1.5, 1.5, 0.05).reshape(-1, 1)
test_y = nn.predict(test_x)

plt.scatter(x, y, s=1, c='b', marker='s', label='real')
plt.scatter(test_x, test_y, s=10, c='r', marker='o', label='prediccion NN')
plt.legend()
plt.show()

Con solo cuatro unidades ocultas y activación tangente hiperbólica, la red reproduce una onda seno completa: eso es precisamente lo que una regresión lineal jamás podría hacer. El parámetro alpha es la regularización L2 y es tu primera línea de defensa contra el sobreajuste; el solver lbfgs converge bien en conjuntos pequeños, mientras que adam es la opción razonable cuando tienes muchos datos.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.