Une fonction à boutons, et un moyen de les tourner
Un réseau de neurones n'est qu'une fonction avec beaucoup de nombres ajustables, les poids. Une couche multiplie les entrées par une matrice de poids et ajoute un biais ; une non-linéarité comme ReLU (max(x, 0)) est appliquée ; le résultat alimente la couche suivante. Empilées ainsi, ces fonctions peuvent approcher des relations très compliquées. Entraîner signifie choisir les poids pour que les prédictions collent à des cibles connues : mesurez l'écart avec une perte (pour prévoir un nombre, l'erreur quadratique moyenne), calculez comment la perte varie avec chaque poids, et poussez chaque poids un peu dans la direction qui la réduit. Cette poussée, répétée des milliers de fois, est la descente de gradient.
# 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 reluLa descente de gradient sur le modèle le plus simple
L'entraînement d'un réseau utilise la rétropropagation pour obtenir les gradients, mais l'idée est déjà complète pour un modèle linéaire y ~ x @ w + b. L'erreur quadratique moyenne a pour gradient 2/n * x.T @ err par rapport à w et 2 * mean(err) par rapport à b, où err = prédiction - cible. Partez de zéros, soustrayez le taux d'apprentissage fois le gradient, recommencez. Un taux d'apprentissage trop grand dépasse la cible et diverge ; trop petit, il rampe. Standardisez d'abord les variables (moyenne nulle, variance unité) pour qu'un seul taux convienne à toutes. Des bibliothèques comme PyTorch calculent les gradients automatiquement, mais elles font exactement cette boucle.
# 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 foundPourquoi prévoir des rendements est difficile pour un réseau
Un réseau peut ajuster presque n'importe quoi, et c'est le danger avec les données financières : les rendements sont surtout du bruit, si bien qu'un modèle flexible mémorise le bruit de la période d'entraînement et échoue sur la suivante. Le surapprentissage se voit à une erreur d'entraînement bien inférieure à l'erreur sur des données jamais vues. Les défenses sont celles des sujets précédents : comparer à une référence simple (un modèle linéaire, ou prédire zéro) ; n'évaluer que sur des données postérieures avec la validation walk-forward, sans jamais mélanger le temps ; peu de variables et un petit réseau ; ajouter de la régularisation (décroissance des poids, dropout) et un arrêt précoce ; et moyenner plusieurs réseaux lancés de graines différentes. Si le réseau ne bat pas nettement la référence linéaire hors échantillon, utilisez le modèle linéaire.
# 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- Une couche est
relu(x @ w + b); un réseau empile des couches, et la perte (l'erreur quadratique moyenne pour une prévision) mesure l'écart des prédictions aux cibles. - L'entraînement est une descente de gradient : pour le modèle linéaire
w -= lr * 2/n * x.T @ erretb -= lr * 2 * mean(err); mettez les variables à l'échelle, réglez le taux d'apprentissage. - Les rendements sont surtout du bruit : comparez à une référence linéaire, validez en walk-forward, utilisez de petits réseaux régularisés, et arrêtez quand le gain hors échantillon disparaît.
À suivre : l'apprentissage par renforcement pour le trading, apprendre une politique à partir de récompenses plutôt que de réponses étiquetées.
Lab : ce qui est noté
- Éteindre les négatifs avec
relu25 pts - Faire avancer le réseau avec
mlp_forward25 pts - Mesurer l'erreur avec
mse_loss25 pts - Apprendre en descendant avec
train_linear25 pts