Una función con mandos y una forma de girarlos
Una red neuronal es solo una función con muchos números ajustables, los pesos. Una capa multiplica las entradas por una matriz de pesos y suma un sesgo; se aplica una no linealidad como ReLU (max(x, 0)); el resultado alimenta la capa siguiente. Apiladas así, estas funciones pueden aproximar relaciones muy complicadas. Entrenar significa elegir los pesos para que las predicciones coincidan con objetivos conocidos: mide el desajuste con una pérdida (para pronosticar un número, el error cuadrático medio), calcula cómo cambia la pérdida con cada peso y empuja cada peso un poco en la dirección que la reduce. Ese empujón, repetido miles de veces, es el descenso de gradiente.
# A one-hidden-layer network: relu(x @ w1 + b1) @ w2 + b2
import numpy as np
def relu(x):
return np.maximum(x, 0.0)
def mlp_forward(x, w1, b1, w2, b2):
return relu(x @ w1 + b1) @ w2 + b2
x = np.array([[1.0, 2.0], [0.5, -1.0]]) # two samples, two features
w1 = np.array([[1.0, -1.0], [0.5, 1.0]]) # two hidden units
b1 = np.array([0.0, 0.5])
w2 = np.array([1.0, 2.0])
print(relu(np.array([-1.5, 0.0, 2.0]))) # [0. 0. 2.]
print(mlp_forward(x, w1, b1, w2, 0.1)) # [5.1 0.1] a prediction for each sample; the second sample's hidden values are switched off by the reluEl descenso de gradiente en el modelo más simple
Entrenar una red usa retropropagación para obtener los gradientes, pero la idea ya está completa en un modelo lineal y ~ x @ w + b. El error cuadrático medio tiene gradiente 2/n * x.T @ err respecto a w y 2 * mean(err) respecto a b, donde err = predicción - objetivo. Parte de ceros, resta la tasa de aprendizaje por el gradiente y repite. Una tasa de aprendizaje demasiado grande se pasa y diverge; demasiado pequeña, se arrastra. Estandariza primero las variables (media cero, varianza unidad) para que una sola tasa sirva a todas. Bibliotecas como PyTorch calculan los gradientes automáticamente, pero hacen exactamente este bucle.
# Gradient descent on a line: start at zero and walk downhill on the mean squared error
import numpy as np
def mse_loss(pred, target):
return float(np.mean((pred - target) ** 2))
def train_linear(x, y, lr, epochs):
n, d = x.shape
w, b = np.zeros(d), 0.0
for _ in range(epochs):
err = x @ w + b - y
w = w - lr * 2 / n * x.T @ err
b = b - lr * 2 * float(np.mean(err))
return w, float(b)
x = np.array([[0.0], [1.0], [2.0], [3.0]])
y = np.array([1.0, 3.0, 5.0, 7.0]) # exactly y = 2x + 1
for epochs in (0, 10, 2000):
w, b = train_linear(x, y, 0.05, epochs)
print(epochs, np.round(w, 3), round(b, 3), round(mse_loss(x @ w + b, y), 6))
# 0 [0.] 0.0 21.0 the zero start
# 10 [2.009] 0.958 0.00092 already close
# 2000 [2.] 1.0 0.0 the line y = 2x + 1 is foundPor qué pronosticar rendimientos es difícil para una red
Una red puede ajustar casi cualquier cosa, y ese es el peligro con datos financieros: los rendimientos son sobre todo ruido, así que un modelo flexible memoriza el ruido del periodo de entrenamiento y falla en el siguiente. El sobreajuste se ve como un error de entrenamiento muy inferior al error en datos que no ha visto. Las defensas son las de los temas anteriores: comparar con una referencia simple (un modelo lineal, o predecir cero); evaluar solo con datos posteriores usando validación walk-forward, sin barajar nunca el tiempo; pocas variables y una red pequeña; añadir regularización (decaimiento de pesos, dropout) y parada temprana; y promediar varias redes iniciadas con semillas distintas. Si la red no supera claramente a la referencia lineal fuera de muestra, usa el modelo lineal.
# Overfitting in miniature: a flexible fit to pure noise looks great in-sample and useless afterwards
import numpy as np
rng = np.random.default_rng(5)
x_train, x_test = rng.normal(size=(30, 12)), rng.normal(size=(1000, 12)) # 12 features that mean nothing
y_train, y_test = rng.normal(0, 0.01, size=30), rng.normal(0, 0.01, size=1000) # next-day returns: pure noise
w = np.linalg.lstsq(x_train, y_train, rcond=None)[0] # a model with 12 free weights fitted to 30 points
train_mse = np.mean((x_train @ w - y_train) ** 2)
test_mse = np.mean((x_test @ w - y_test) ** 2)
baseline_mse = np.mean(y_test ** 2) # predict zero
print(train_mse < baseline_mse, test_mse > baseline_mse) # True True better in-sample, worse than zero out of sample# The learning rate matters: a small one settles, one that is too large overshoots and the error blows up
import numpy as np
x = np.array([[1.0], [2.0], [3.0]])
y = np.array([2.0, 4.0, 6.0])
def final_errors(lr, epochs):
w, b, errors = np.zeros(1), 0.0, []
for _ in range(epochs):
err = x @ w + b - y
errors.append(float(np.mean(err ** 2)))
w = w - lr * 2 / len(y) * x.T @ err
b = b - lr * 2 * float(np.mean(err))
return errors
small, large = final_errors(0.05, 30), final_errors(0.5, 30)
print(small[-1] < small[0]) # True the error shrinks
print(large[-1] > 1000 * large[0]) # True the error explodes: the steps are too big- Una capa es
relu(x @ w + b); una red apila capas y la pérdida (el error cuadrático medio en un pronóstico) mide cuánto se alejan las predicciones de los objetivos. - Entrenar es descenso de gradiente: en el modelo lineal
w -= lr * 2/n * x.T @ erryb -= lr * 2 * mean(err); escala las variables y ajusta la tasa de aprendizaje. - Los rendimientos son sobre todo ruido: compara con una referencia lineal, valida en walk-forward, usa redes pequeñas con regularización y para cuando desaparezca la ganancia fuera de muestra.
A continuación: aprendizaje por refuerzo para trading, aprender una política a partir de recompensas en vez de respuestas etiquetadas.
Lab: qué se califica
- Apagar los negativos con
relu25 pts - Ejecutar la red hacia delante con
mlp_forward25 pts - Medir el error con
mse_loss25 pts - Aprender descendiendo con
train_linear25 pts