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.