1. Optimization, the heart of machine learning
Level 10: Machine Learning Applied to Trading
Machine learning has been attracting interest since the nineteen-sixties, but plenty of people are afraid to get started, mostly because it looks as though it demands a deep command of linear algebra and calculus. The aim of this level is to take that fear away and show you that, with relatively little effort, you can build practical models that solve real problems. That said, let us be clear from the outset rather than sell smoke: building genuinely accurate models is still a complex and demanding job.
Machine learning rests on three pillars:
- Linear algebra: data are matrices, and models are, deep down, operations on those matrices.
- Probability analysis: noise, uncertainty and the ability to generalize are all reasoned about in probabilistic terms.
- Numerical optimization: the engine that fits the model to the data.
Of the three, one deserves a sentence of its own: optimization is at the heart of almost every machine learning algorithm. When we say that a model learns, what is actually happening is that a numerical procedure is searching for the parameter values that minimize an error function. In regression we minimize the sum of squared errors; in classification, the cross-entropy; in Markowitz-style portfolio optimization, we minimize variance subject to a target return. The model and the objective function change, but the conceptual machinery is identical: define an error, work out which direction makes it decrease, and move that way.
That moving is gradient descent: you compute the derivative of the error function with respect to each parameter and take a small step in the direction opposite to the gradient. We are going to write it out by hand, with no ML libraries at all, on a minimal financial problem: predicting tomorrow's return from today's return.
import numpy as np
rng = np.random.default_rng(42)
# Synthetic series of daily returns with a pinch of autocorrelation
r_today = rng.normal(0, 0.01, 2000)
r_tomorrow = 0.15 * r_today + rng.normal(0, 0.01, 2000)
# We rescale the input variable: optimization is VERY sensitive to scale
x = (r_today - r_today.mean()) / r_today.std()
y = r_tomorrow
w, b = 0.0, 0.0 # parameters to be learned
lr = 0.1 # learning rate
for epoch in range(500):
pred = w * x + b
error = pred - y
grad_w = 2 * np.mean(error * x)
grad_b = 2 * np.mean(error)
w -= lr * grad_w
b -= lr * grad_b
if epoch % 100 == 0:
print(epoch, 'MSE =', float(np.mean(error ** 2)))
print('learned w:', round(w, 6), '| learned b:', round(b, 8))Pay attention to the scaling detail. If you train directly on returns whose standard deviation is 0.01, the curvature of the error function with respect to w is minuscule and the algorithm takes an eternity to move, while b converges almost immediately. That is not a failure of the model: it is a failure of the optimization. Problems of that kind, invisible if all you ever do is call a library function, are precisely why it is worth understanding what happens underneath.