Qu

Machine learning

Curve fitting

nonlinear least-squares curve fitting and general multivariate minimization.

catalog/qu_curve_fit.qu

`curve_fit` wraps Levenberg-Marquardt around a user-defined model function `f(x, params)`; `minimize` runs L-BFGS on any scalar objective of a parameter vector. Both return a Model rather than a bare vector, since each call's outputs (fitted params, cost/value, convergence flag) are one related result, not independent ones -- Qu's usual named-return convention.

What it exercises

How it works

Fit a damped-sinusoid model to noisy data

function damped_sine(x, p)
    return p[0] * exp(-x / p[1]) * sin(2*pi*p[2]*x + p[3])
end function

N = 80
x = linspace(0, 4, N)
true_params = [2.0, 1.5, 1.2, 0.3]
y_clean = damped_sine(x, true_params)
y = y_clean + 0.08 * randn(N)

p0 = [1.0, 1.0, 1.0, 0.0]
fit = curve_fit("damped_sine", x, y, p0, max_iter=300)
print("converged={fit.converged}, cost={fit.cost:.4f}")
print("true params:   {true_params}")
print("fitted params: {fit.params}")

General minimization: find the minimum of a bowl-shaped objective

function rosenbrock(p)
    a = 1 - p[0]
    b = p[1] - p[0] * p[0]
    return a * a + 100 * b * b
end function

result = minimize("rosenbrock", [-1.2, 1.0], max_iter=500)
print("minimize: converged={result.converged}, value={result.value:.6f}, params={result.params}")

Plot data vs the fitted curve

xf = linspace(0, 4, 300)
yf = damped_sine(xf, fit.params)

plot(x, y, label="noisy data", color="gray")
plot(xf, yf, label="curve_fit", color="blue")
xlabel("x")
ylabel("y")
title("Levenberg-Marquardt fit of a damped sinusoid")
legend()
show plot

Results

printed
converged=true, cost=0.5539
true params:   [2, 1.5, 1.2, 0.3]
fitted params: [2.044156, 1.447219, 1.201374, 0.288305]
minimize: converged=true, value=0.000000, params=[1, 1]
-1.5 -1.0 -0.5 0 0.5 1.0 1.5 2.0 0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 Levenberg-Marquardt fit of a damped sinusoid x y noisy data curve_fit
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_curve_fit.qu

# qu_curve_fit.qu -- nonlinear least-squares curve fitting and general

# multivariate minimization.

#

# `curve_fit` wraps Levenberg-Marquardt around a user-defined model function

# `f(x, params)`; `minimize` runs L-BFGS on any scalar objective of a

# parameter vector. Both return a Model rather than a bare vector, since

# each call's outputs (fitted params, cost/value, convergence flag) are one

# related result, not independent ones -- Qu's usual named-return convention.

#

# Constructs exercised (all real, verified-working builtins):

#   curve_fit(model_fn, xdata, ydata, p0, max_iter=, tol=) -- Levenberg-Marquardt

#   minimize(f, x0, max_iter=, tol=)                        -- L-BFGS

#   randn                                                   -- synthetic measurement noise


backend auto

#%% Fit a damped-sinusoid model to noisy data

function damped_sine(x, p)
    return p[0] * exp(-x / p[1]) * sin(2*pi*p[2]*x + p[3])
end function

N = 80
x = linspace(0, 4, N)
true_params = [2.0, 1.5, 1.2, 0.3]
y_clean = damped_sine(x, true_params)
y = y_clean + 0.08 * randn(N)

p0 = [1.0, 1.0, 1.0, 0.0]
fit = curve_fit("damped_sine", x, y, p0, max_iter=300)
print("converged={fit.converged}, cost={fit.cost:.4f}")
print("true params:   {true_params}")
print("fitted params: {fit.params}")

#%% General minimization: find the minimum of a bowl-shaped objective

function rosenbrock(p)
    a = 1 - p[0]
    b = p[1] - p[0] * p[0]
    return a * a + 100 * b * b
end function

result = minimize("rosenbrock", [-1.2, 1.0], max_iter=500)
print("minimize: converged={result.converged}, value={result.value:.6f}, params={result.params}")

#%% Plot data vs the fitted curve

xf = linspace(0, 4, 300)
yf = damped_sine(xf, fit.params)

plot(x, y, label="noisy data", color="gray")
plot(xf, yf, label="curve_fit", color="blue")
xlabel("x")
ylabel("y")
title("Levenberg-Marquardt fit of a damped sinusoid")
legend()
show plot