Implementation für das bestimmen einer Tangente in einem Punkt



  • Guten Abend,

    ich grübel gerade zur späten Stund, wie ich die Tangente einer Funktion in einem Punkt berechnen kann.
    Nimmt man beispielsweise eine Sinusfunktion und möchte für einen bestimmten Punkt die Tangente bestimmen, muss man zunächst die Ableitung dieser Funktion bestimmen. Dann setzt man den x-Wert in die Ableitung ein und erhält den Anstieg der Tangente, die die Sinusfunktion im entsprechenden Punkt schneidet.

    Soweit so gut ...

    Wo ich jetzt ins stolpern komme, ist bei der Implementierung.
    Wie kann ich die Berechnung des Anstiegs für eine beliebige Funktion realisieren.
    Heißt, das ich die Funktion nicht fest im Code definieren möchte, sondern die Tangenten soll dynamisch für unterschiedliche Funktionen berechnet werden.

    Das einzige Hilfsmittel was ich zur Verfügung habe ist die eigentliche Funktion, deren Funktionswerte und die dazugehörigen Funktionsargumente.

    Wäre super, wenn mir da jemand einen Denkanstoß geben könnte.

    Viele Grüße,
    Saul



  • Ich nehme an, dass du die Tangente numerisch und nicht analytisch berechnen moechtest.

    Wenn du eine Funktion ff gegeben hast, dann kannst du die Steigung in einem Punkt xx mit dem zentralen Differenzenquotient berechnen:
    f(x)=f(x+h)f(xh)2hf'(x) = \frac{f(x+h) - f(x-h)}{2h}
    Dabei sollte hh klein aber nicht allzu klein gewaehlt werden. Ein zu kleines h ist numerisch schlecht. Wenn ich mich richtig erinnere, dann ist h=109h = 10^{-9} fuer double allgemein eine nicht allzu schlechte Wahl.



  • Das blöde bei der numerischen Differentiation ist, dass man so ein h wählen muss. Ist es zu groß, ist der Diskretisierungsfehler zu groß. Ist es zu klein, wird das ganze "instabil" (numerische Auslöschung) und die Genauigkeit geht wieder in den Keller.

    Alternativ kann man "automatische Differentiation" (automatic differentiation) verwenden. Automatische Differentiation ist nicht zu verwechseln mit symbolischer Differentiation. Symbolosche Differentiation liefert einem eine Formel für die Ableitung. Automatische Differentiation berechnet die Ableitung in einem Punkt, nicht als Formel. Die Unterstützung von solchen Dinge ist ganz gut in C++, da man sich relativ leicht Typen bauen kann, die sich wie double verhalten, aber das nötige Extra mitschleppen, so dass man hinterher an den Wert einer Ableitung kommt. Man muss das sogar nicht mal selbst implementieren. Da gibt's genug Source Code hier und da.

    Aber hier mal das Prinzip von automatischer Differentiation per Vorwärtspropagierung bezüglich einer Variablen:

    struct vpad
    {
      double wert, ableitung;
    
      vpad(double w=0, double a=0)
      : wert(w), ableitung(a)
      {}
    
      vpad& operator+=(vpad const& x)
      {
        wert      += x.wert;
        ableitung += x.ableitung;
        return *this;
      }
    
      vpad& operator-=(vpad const& x)
      {
        wert      -= x.wert;
        ableitung -= x.ableitung;
        return *this;
      }
    
      vpad& operator*=(vpad const& x)
      {
        ableitung = wert * x.ableitung + ableitung * x.wert; // Produktregel
        wert = wert * x.wert;
        return *this;
      }
    
      friend vpad operator+(vpad a, vpad const& b) { return a+=b; }
      friend vpad operator-(vpad a, vpad const& b) { return a-=b; }
      friend vpad operator*(vpad a, vpad const& b) { return a*=b; }
    };
    
    #include <iostream>
    
    int main()
    {
      using namespace std;
      vpad x (3, 1); // x = 3, Ableitung von x nach x ist 1
      vpad y = (x-2)*(x-5);
      cout << "f (x) = (x-2)*(x-5)" << endl;
      cout << "f (3) = " << y.wert << endl;
      cout << "f'(3) = " << y.ableitung << endl;
    }
    

    Das heißt, man braucht nur bei der Funktion, deren Ableitung man in einem Punkt berechnen will, double durch vpad ersetzen und bekommt dafür die Ableitung geschenkt. Am besten schreibt man sie als Template auf. Dann kann man sich aussuchen, ob man das Template mit T=double oder T=vpad instantiieren will.

    Die frei verfügbaren Bibliotheken sind da natürlich mächtiger und können Ableitung von mehreren Variablen und höherer Ordnung berechnen.

    Division und die Anwendung von Funktionen (sin, cos, sqrt, exp, ...) ließe sich in diesem Beispiel auch noch einbauen. Aber das Prinzip ist hoffentlich klar.



  • Hi Leute,
    danke für eure Antworten.

    Die von icarus2 gepostete Variante ist echt einfach und schnell berechenbar.
    Ist zwar etwas ungenau, könnte aber für meine Anforderungen ausreichen. Muss ich mal testen.

    Soweit ich das verstanden habe, gibt es wohl keine 100%ig genaue Bestimmung des Anstiegs? Scheint irgendwie immer nur als Approximation realisierbar zu sein oder sehe ich das falsch?

    Den Ansatz von krümelkacker verstehe ich leider nicht so ganz.
    Wikipedia hat mir nicht weitergeholfen. Steige nicht hinter die Grundidee der automatischen Differentiation.
    Anscheinend trifft da diese Regel zu

    wikipedia schrieb:

    1. Stelle die Berechnungsvorschrift als Berechnungsbaum, d. h. als arithmetisches Netzwerk, dar und erweitere diesen unter Verwendung der Kettenregel zu einem Berechnungsbaum für Funktionswert und Ableitung

    Aber was genau steckt dahinter?



  • Saul schrieb:

    Soweit ich das verstanden habe, gibt es wohl keine 100%ig genaue Bestimmung des Anstiegs?

    Scheint irgendwie immer nur als Approximation realisierbar zu sein oder sehe ich das falsch?

    100%ig genau ist so gut wie nichts, wenn du mit Fließkommazahlen rechnest; denn du schleppst ständig Rundungsfehler mit dir rum. Selbst wenn du eine Formel für die Ableitung hättest, könntest du die Ableitung damit nicht "100%ig genau" ausrechnen, weil Fließkommazahlen nunmal eine endliche Genauigkeit haben.

    Bei der numerischen Differentiation kommt allerdings ein sogenannter Diskretisierungsfehler dazu. Außerdem verschlimmern sich die Rundungsfehler bei dieser Methode, je kleiner der "Schritt" h ist -- aufgrund des numerischen Auslöschungseffekts gibt.

    Automatische Differentiation hingegen liefert dir ein Ergebnis mit einer Genauigkeit, die man praktisch mit "handelsüblichen Fließkommazahlen" nicht mehr übertreffen kann, weil man die Rundungsfehler bei der Fließkomma-Arithmetik nicht loswird.

    Saul schrieb:

    Den Ansatz von krümelkacker verstehe ich leider nicht so ganz.
    Wikipedia hat mir nicht weitergeholfen. Steige nicht hinter die Grundidee der automatischen Differentiation.
    Anscheinend trifft da diese Regel zu

    wikipedia schrieb:

    1. Stelle die Berechnungsvorschrift als Berechnungsbaum, d. h. als arithmetisches Netzwerk, dar und erweitere diesen unter Verwendung der Kettenregel zu einem Berechnungsbaum für Funktionswert und Ableitung

    Aber was genau steckt dahinter?

    Es gibt mehrere Ansätze, wie man das berechnen kann. Das, was ich zeigte, nennt sich Vorwärtspropagierung. Die Idee der Vorwärtspropagierung ist, dass man für alle Zwischenergebnisse zusätzlich den Wert der Ableitung automatisch mitschleppt. Deswegen habe ich auch einen eigenen Typen definiert, der zwei double-Elemente enthält. Wenn du so etwas statt double in deinen Berechnungen unter Berücksichtigung der Rechenregeln für die Ableitung verwendest, hast du am Ende automatisch zwei Ergebnisse, wobei der zweite der Wert der Ableitung des ersten Wertes nach deiner Variablen ist.

    Der Code von mir ist nur als kleine Demo gedacht, um zu zeigen, wie das Prinzip funktioniert. Ich sage nicht, dass du das mit allem drum und dran neu erfinden musst. Such mal nach quelloffenen C++ Bibliotheken, die so etwas implementieren. C++ bietet sich hier wegen der Operator- und Funktions-Überladung prima an. Die Doku einer solchen Bibliothek verrät dir sicher mehr dazu.

    Wenn dir das zu hoch ist und dir numerische Differenzierung genau genug ist, dann nimmste einfach die numerische Differenzierung. Da kann man auch den Schaden noch etwas begrenzen: Statt

    double x = 2;
    double h = 0.0001;
    double ableitung = (f(x+h)-f(x-h))/(2*h);
    

    sollte man

    double x = 2;
    double h = 0.0001;
    double xm = x-h;
    double xp = x+h;
    double ableitung = (f(xp)-f(xm))/(xp-xm);
    

    schreiben, da x+h und x-h ja nur mit Rundungsfehlern berechnet werden können und f eben nicht bei x-h und x+h sondern bei x-h+fehler1=xm und x+h+fehler2=xp ausgewertet wird. Der entsprechende Differenzenkoeffizient hat im Nenner nicht 2h sondern xp-xm stehen. Allerdings bleibt das Problem mit den Rundungsfehlern bestehen. Ist h zu klein, beißt einen die Auslöschung in den Ars*h.

    Von numerischer Differenzierung halte ich persönlich gar nichts, weil es schwierig ist, ein "gutes h" zu wählen.



  • icarus2 schrieb:

    Wenn ich mich richtig erinnere, dann ist h=109h = 10^{-9} fuer double allgemein eine nicht allzu schlechte Wahl.

    Man kann keine solche Empfehlung für h aussprechen, weil die optimale Wahl von h nicht nur von der Maschinengenauigkeit sondern auch stark von der Funktion abhängt, die differenziert werden soll.



  • krümelkacker schrieb:

    Aber hier mal das Prinzip von automatischer Differentiation per Vorwärtspropagierung bezüglich einer Variablen:

    das ist genial und einfach - bzw. einfach genial 👍
    Danke krümelkacker, wieder was gelernt.

    krümelkacker schrieb:

    Von numerischer Differenzierung halte ich persönlich gar nichts, weil es schwierig ist, ein "gutes h" zu wählen.

    .. so wie einst mein Regeltechikprof sagte: "Die schlechteste Messung ist besser als die beste Differenzierung"

    Saul schrieb:

    Den Ansatz von krümelkacker verstehe ich leider nicht so ganz.
    Wikipedia hat mir nicht weitergeholfen. Steige nicht hinter die Grundidee der automatischen Differentiation.

    Wikipedia scheint hier mal wieder von Experten für Experten geschrieben worden zu sein.

    Ich versuche es mal an einem Beispiel:
    Mal angenommen Du hast die Funktion f(x) =(x-3)*x und diese willst Du ableiten.
    In der Schule lernt man: f(x) =(x-3)*x=x^2-3x -> f'(x)=2x-3. f'(x=5) wäre also =2*5-3=7 .. soweit klar!?

    Bei der ADvp (automatischen Differenzierung per Vorwärtspropagierung) macht man nun folgendes - man stellt die Funktion 'f(x) =(x-3)*x' um nach
    f(x) =g(x)*h(x) mit g(x)=e(x)-3 und h(x)=x und e(x)=x
    Jetzt einfach nach der Produktregel ableiten
    f'(x) =g(x)*h'(x) + g'(x)*h(x) mit g'(x)=e'(x) und e'(x) = 1, da e(x)=x ist.

    Das alles macht man jetzt für einen bestimmten Wert von x (z.B.=5) und das ist genau das, was die Klasse vpad von krümelkacker macht. Jedes Objekt dieser Klasse repräsentiert einen Funktionswert und den Wert der Ableitung an der gleichen Stelle.
    e(x) = x -> e(5) = 5; e'(5)=1 entspricht vpad(5,1)
    h(x) = x -> h(5) = 5; h'(5)=1 dito.
    g(5)=e(5)-3 = 2, g'(5)=e'(5)=1 entspricht vpad(5,1)-3 -> vpad(2,1) // die Ableitung (hier =1) bleibt unverändert

    einsetzen ergibt
    f'(5) =2*1 + 1*5 = 7 .. passt! entsprach

    vpad(2,1)/*g(5)*/ * vpad(5,1)/*h(5)*/
    

    .. siehe Code in Zeile 25

    :xmas2: Werner



  • Werner Salomon schrieb:

    krümelkacker schrieb:

    Aber hier mal das Prinzip von automatischer Differentiation per Vorwärtspropagierung bezüglich einer Variablen:

    das ist genial und einfach - bzw. einfach genial 👍
    Danke krümelkacker, wieder was gelernt.

    Gerne. 🙂 Ich find' AD auch genial. Es scheint nur nicht besonders bekannt zu sein.

    Mal ein Beispiel zur Kettenregel für trigonometrische Funktionen:

    vpad unaereOperation(vpad const& t, double neuerWert, double aeussereAbleitung)
    { return vpad(neuerWert,t.ableitung*aeussereAbleitung); }
    //                         innere  * äußere Ableitung
    
    vpad sin(vpad const& x)
    { return unaereOperation(x,std::sin(x.wert), std::cos(x.wert)); }
    
    vpad cos(vpad const& x)
    { return unaereOperation(x,std::cos(x.wert),-std::sin(x.wert)); }
    

    (ungetestet)


Anmelden zum Antworten