web-dev-qa-db-fra.com

Résolution d'équations non linéaires dans python

J'ai 4 équations non linéaires avec trois inconnues X, Y et Z que je veux résoudre. Les équations sont de la forme:

F(m) = X^2 + a(m)Y^2 + b(m)XYcosZ + c(m)XYsinZ

... où a, b et c sont des constantes qui dépendent de chaque valeur de F dans les quatre équations.

Quelle est la meilleure façon de résoudre ce problème?

20
user1171835

Il y a deux façons de faire ça.

  1. Utilisez un solveur non linéaire
  2. Linéariser le problème et le résoudre dans le sens des moindres carrés

Installer

Donc, si je comprends bien votre question, vous connaissez F, a, b et c à 4 points différents, et vous voulez inverser pour les paramètres du modèle X, Y et Z. Nous avons 3 inconnues et 4 points de données observés, donc le problème est surdéterminé. Par conséquent, nous allons résoudre dans le sens des moindres carrés.

Il est plus courant d'utiliser la terminologie opposée dans ce cas, alors retournons votre équation. Au lieu de:

F_i = X^2 + a_i Y^2 + b_i X Y cosZ + c_i X Y sinZ

Écrivons:

F_i = a^2 + X_i b^2 + Y_i a b cos(c) + Z_i a b sin(c)

Où nous connaissons F, X, Y et Z à 4 points différents (par exemple F_0, F_1, ... F_i).

Nous changeons simplement les noms des variables, pas l'équation elle-même. (C'est plus pour ma facilité de réflexion qu'autre chose.)

Solution linéaire

Il est en fait possible de linéariser cette équation. Vous pouvez facilement résoudre pour a^2, b^2, a b cos(c) et a b sin(c). Pour rendre cela un peu plus facile, réétiquetons les choses encore une fois:

d = a^2
e = b^2
f = a b cos(c)
g = a b sin(c)

Maintenant, l'équation est beaucoup plus simple: F_i = d + e X_i + f Y_i + g Z_i. Il est facile de faire une inversion linéaire des moindres carrés pour d, e, f et g. Nous pouvons alors obtenir a, b et c à partir de:

a = sqrt(d)
b = sqrt(e)
c = arctan(g/f)

D'accord, écrivons ceci sous forme de matrice. Nous allons traduire 4 observations de (le code que nous allons écrire prendra un certain nombre d'observations, mais restons concrets pour le moment):

F_i = d + e X_i + f Y_i + g Z_i

Dans:

|F_0|   |1, X_0, Y_0, Z_0|   |d|
|F_1| = |1, X_1, Y_1, Z_1| * |e|
|F_2|   |1, X_2, Y_2, Z_2|   |f|
|F_3|   |1, X_3, Y_3, Z_3|   |g|

Ou: F = G * m (Je suis géophysicien, nous utilisons donc G pour "Green's Functions" et m pour "Model Parameters". Habituellement, nous utilisons d pour "data" au lieu de F, aussi.)

En python, cela se traduirait par:

def invert(f, x, y, z):
    G = np.vstack([np.ones_like(x), x, y, z]).T
    m, _, _, _ = np.linalg.lstsq(G, f)

    d, e, f, g = m
    a = np.sqrt(d)
    b = np.sqrt(e)
    c = np.arctan2(g, f) # Note that `c` will be in radians, not degrees
    return a, b, c

Solution non linéaire

Vous pouvez également résoudre ce problème en utilisant scipy.optimize, Comme l'a suggéré @Joe. La fonction la plus accessible dans scipy.optimize Est scipy.optimize.curve_fit Qui utilise par défaut une méthode Levenberg-Marquardt.

Levenberg-Marquardt est un algorithme d '"escalade" (enfin, il descend, dans ce cas, mais le terme est quand même utilisé). Dans un sens, vous faites une première estimation des paramètres du modèle (tous ceux, par défaut dans scipy.optimize) Et suivez la pente de observed - predicted Dans votre espace de paramètres en descendant vers le bas.

Mise en garde: Choisir la bonne méthode d'inversion non linéaire, deviner initialement et régler les paramètres de la méthode est vraiment un "art sombre". Vous ne l'apprenez qu'en le faisant, et il y a beaucoup de situations où les choses ne fonctionnent pas correctement. Levenberg-Marquardt est une bonne méthode générale si votre espace de paramètres est assez lisse (celui-ci devrait l'être). Il y en a beaucoup d'autres (y compris des algorithmes génétiques, des réseaux de neurones, etc. en plus des méthodes plus courantes comme le recuit simulé) qui sont meilleurs dans d'autres situations. Je ne vais pas me plonger dans cette partie ici.

Il existe un piège courant que certaines boîtes à outils d'optimisation essaient de corriger et que scipy.optimize N'essaie pas de gérer. Si les paramètres de votre modèle ont des magnitudes différentes (par exemple a=1, b=1000, c=1e-8), Vous devrez redimensionner les choses afin qu'elles soient similaires en magnitude. Sinon, les algorithmes d '"escalade" de scipy.optimize (Comme LM) ne calculeront pas avec précision l'estimation du gradient local et donneront des résultats extrêmement inexacts. Pour l'instant, je suppose que a, b et c ont des amplitudes relativement similaires. Sachez également que pratiquement toutes les méthodes non linéaires nécessitent que vous fassiez une supposition initiale et qu'elles y soient sensibles. Je le laisse ci-dessous (il suffit de le passer en tant que kwarg p0 À curve_fit) Car la valeur par défaut a, b, c = 1, 1, 1 Est une estimation assez précise pour a, b, c = 3, 2, 1 .

Avec les mises en garde à l'écart, curve_fit S'attend à recevoir une fonction, un ensemble de points où les observations ont été faites (comme un seul tableau ndim x npoints) Et les valeurs observées.

Donc, si nous écrivons la fonction comme ceci:

def func(x, y, z, a, b, c):
    f = (a**2
         + x * b**2
         + y * a * b * np.cos(c)
         + z * a * b * np.sin(c))
    return f

Nous devons l'encapsuler pour accepter des arguments légèrement différents avant de le passer à curve_fit.

En un mot:

def nonlinear_invert(f, x, y, z):
    def wrapped_func(observation_points, a, b, c):
        x, y, z = observation_points
        return func(x, y, z, a, b, c)

    xdata = np.vstack([x, y, z])
    model, cov = opt.curve_fit(wrapped_func, xdata, f)
    return model

Exemple autonome des deux méthodes:

Pour vous donner une implémentation complète, voici un exemple qui

  1. génère des points distribués aléatoirement pour évaluer la fonction,
  2. évalue la fonction sur ces points (en utilisant des paramètres de modèle définis),
  3. ajoute du bruit aux résultats,
  4. puis inverse les paramètres du modèle en utilisant à la fois les méthodes linéaires et non linéaires décrites ci-dessus.

import numpy as np
import scipy.optimize as opt

def main():
    nobservations = 4
    a, b, c = 3.0, 2.0, 1.0
    f, x, y, z = generate_data(nobservations, a, b, c)

    print 'Linear results (should be {}, {}, {}):'.format(a, b, c)
    print linear_invert(f, x, y, z)

    print 'Non-linear results (should be {}, {}, {}):'.format(a, b, c)
    print nonlinear_invert(f, x, y, z)

def generate_data(nobservations, a, b, c, noise_level=0.01):
    x, y, z = np.random.random((3, nobservations))
    noise = noise_level * np.random.normal(0, noise_level, nobservations)
    f = func(x, y, z, a, b, c) + noise
    return f, x, y, z

def func(x, y, z, a, b, c):
    f = (a**2
         + x * b**2
         + y * a * b * np.cos(c)
         + z * a * b * np.sin(c))
    return f

def linear_invert(f, x, y, z):
    G = np.vstack([np.ones_like(x), x, y, z]).T
    m, _, _, _ = np.linalg.lstsq(G, f)

    d, e, f, g = m
    a = np.sqrt(d)
    b = np.sqrt(e)
    c = np.arctan2(g, f) # Note that `c` will be in radians, not degrees
    return a, b, c

def nonlinear_invert(f, x, y, z):
    # "curve_fit" expects the function to take a slightly different form...
    def wrapped_func(observation_points, a, b, c):
        x, y, z = observation_points
        return func(x, y, z, a, b, c)

    xdata = np.vstack([x, y, z])
    model, cov = opt.curve_fit(wrapped_func, xdata, f)
    return model

main()
50
Joe Kington

Vous voulez probablement utiliser les solveurs non linéaires de scipy, ils sont vraiment faciles: http://docs.scipy.org/doc/scipy/reference/optimize.nonlin.html

2
Joe