Wie am besten Zwischenergebnisse zwischen Methodenaufrufen speichern



  • Hi,

    Ich weiß nicht, ob der Titel 100% passt. Das Problem ist etwa skompliziert.

    Also, ich habe eine Basisklasse mit 2 Methoden:

    struct Base{
    
    virtual T foo(T x)const = 0;
    virtual T bar(T x, T weights)const = 0;
    };
    

    Die Methoden werden wie folgt verwendet:

    Base* b;
    WeightGenerator w;
    T r=foo(x);
    T weights = weightGenerator(r);
    T endresult = b->bar(x,weights);
    

    bar kann erst aufgerufen werden, nachdem die Gewichte bekannt sind.

    Nun ist es so, dass dieser Methodendreiklang echt rechenaufwendig sein kann, aber durch einen mathematischen Zufall sehr oft Zwischenergebnisse von foo in bar wiederverwendet werden können. Dabei hat aber jede von Base abgeleitete Klasse andere Zwischenergebnisse. Momentan lösen wir das Problem mit einer variable in jeder abgeleiteten Klasse:

    struct Derived:public Base{
    private:
        mutable T2 m_intermediate;
    public:
    T foo(T x)const{
        m_intermediate = foobar(x);//dauert total lange zu berechnen :)
        return foobar2(m_intermediate);
    }
    virtual T bar(T x, T weights)const{
        return weights*x*m_intermediate; //total fix
    }
    };
    

    Leider hat das nun den Nachteil, dass wir das nicht mehr parallelisieren können und wir an unserer eigenen const-correctness Regelung vorbeihacken.

    Aber wie löst man das besser? Jeder Typ kann völlig andere Daten schicken, vektordaten, matrixdaten, skalare...

    wie würdet ihr das angehen? boost::variant oder boost::any als zusätzlicher Parameter? irgendwas anderes?



  • otze schrieb:

    Nun ist es so, dass dieser Methodendreiklang echt rechenaufwendig sein kann, aber durch einen mathematischen Zufall sehr oft Zwischenergebnisse von foo in bar wiederverwendet werden können.

    Warum kein Vierklang?

    Base* b;
    WeightGenerator w;
    T2 tmp(foobar(x));
    T r=foo(tmp);
    T weights = weightGenerator(r);
    T endresult = b->bar(x,tmp,weights);
    

    Ansonsten halt alles in einem:

    T endresult=b->arpeggio(x, weights)
    


  • Ahh ein Musiker. Das Problem ist, dass bei einem Vierklang immer Spannungstöne dabei sind. Und diese würde ich gerne vermeiden. Zumal ich den exakten Typ des Tons gar nicht kenne, er kann dur, moll oder sogar aus einem ganz anderen Modus kommen. Man könnte sagen, das ist der Kern des Problems.

    Ausserdem bin ich verwirrt. Bislang bin ich davon ausgegangen, dass ich das als arpeggio implementiert hätte. Dein Vorschlag sieht mir eher nach einem Akkord aus.

    Ein Akkord ist aber nicht möglich, da ich Akkordvariationen brauche, zum Beispiel kann ich den dritten Ton weglassen. Ausserdem ist der zweite Ton sehr variabel und unter umständen komplett austauschbar.



  • Ich geb mal ein wenig Hintergrundinformationen, vielleicht ist das bislang zu abstrakt:

    Ich arbeite an einer Maschinenlernbibliotek und im kern stehen dabei die Mdoelle : Lineare Funktionen, Neuronale Netzwerke, Faltungsnetzwerke usw.

    Wichtig für uns ist es dann für ein Model f(x) auch den Gradienten f'(x) bezüglich der Parameter des Modells berechnen zu können. Für die meisten Modelle könnte man beides parallel berechnen. Aber es gibt einen sehr viel schnelleren Weg für den Normalfall, dass die Ableitung des Modell in der Kettenregel verwendet wird: g'(f(x))*f'(x). Die Implementation der Kettenregel direkt gibt bei einigen Modellen einen speed up um Faktor der Ausgabedimensionalität von f(x). Wenn man Bildverarbeitung macht, ist das also schon relevant. Leider braucht man dafür zwingend die Ableitung der verwendeten Fehlerfunktion und kann beides nicht mehr parallel berechnen.

    Das heißt, Base sieht so aus:

    struct BaseModel{
    
    virtual T f(T x)const = 0;
    virtual T chainRule(T x, T gDerivative)const = 0;
    };
    

    Die Fehlerfunktion Teil des Modells oder des Aufrufs zu machen kommt nicht in Frage, weil das Kettenregel Konzept noch allgemeiner als das Konzept der Fehlerfunktion ist - das kommt am Ende überall in unterschiedlichen Variationen vor.

    Der Nachteil der Technik ist, dass man nicht mehr so einfach Zwischenergebnisse wiederverwenden kann. Wir lösen das bislang indem wir sagen: f muss vor chainRule aufgerufen werden und es ist dem Modell erlaubt in f Zwischenergebnisse zu speichern und in chainRule wieder zu verwenden. Ist natürlich höchst suboptimal. Und bei bestimmten Fehlerfunktionen führt das dazu, dass wir f doppelt evaluieren müssen, um alle benötigten Zwischenergebnisse da zu haben...

    Was ich mir nun überlegt hatte war, irgendwie sowas zu machen:

    struct BaseModel{
    
    virtual T f(T x, Intermediate& intermediateResults)const = 0;
    virtual T chainRule(T x, T gDerivative,const Intermediate& intermediateResults)const = 0;
    };
    

    Aber was sollte der Typ von Intermediate sein? jedes Netzwerk hat seine eigenen Zwischenergebnisse: Neuronale Netze einen Vektor, Kernel-Methoden eine Reihe von Kernel-Zwischenergebnissen (also ein vektor von Intermediate), seriell geschaltete Modelle die Zwischenergebnisse beider Modelle usw.

    Hätte ich eine Templateumgebung, wüsste ich was ich machen würde, einfach einen Typedef für den zwischenspeichertyp definieren und verwenden. Aber bei einer abstrakten Basisklasse? Etwas viel intelligenteres als boost::any fällt mir da auch nicht ein. Und das hat zur Folge, dass wir quais überall testen müssen, dass das was in any drin ist natürlich der korrekte typ ist...



  • hola otze

    koenntest du vielleicht ein ganz einfaches beispiel deines problems aus einem anderen (einfachen) bereich zeigen ? ich versteh naemlich nicht genau was du da meinst mit den zwischenergebnissen.
    ist so ein zwischenergebnis einfach nur ein wert (double, int ...) ?

    Meep Meep



  • ist so ein zwischenergebnis einfach nur ein wert (double, int ...) ?

    etwas mehr. eher std::vector<double> oder noch komplexer. Einfach ein beliebiges Ergebnis das man auch in der Ableitung wieder verwenden kann (und das teuer zu berechnen ist). Ein Beispiel waere, wenn die Eingabe ein vector<double> ist, dass das Zwischenergebnis dann wieder ein vector<double> ist, der so berechnet wird:

    virtual std::vector<double> f(std::vector<double>const & x)const{
    std::vector<double> intermediate(x.size());
    for(std::size_t i = 0; i != x.size(); ++i){
        intermediate[i]=std::exp(x[i]);
    }
    //...
    return intermediate;
    }
    

    wenn die Terme in der Ableitung gebraucht werden, moechte man die (sehr sehr vielen) exponential berechnungen nicht weg werfen. Aber eigentlich ist der Typ voellig beliebig.



  • Also da Du mit Interfaces arbeitest, ist die Anzahl der möglichen Implementierungen ja praktisch begrenzt. Ich würde da mit variant arbeiten.
    Allerdings ist mir immer noch nicht klar, was Du genau willst. Du ruft irgendeine komplexe Funktion auf, deren Ergebnis als Basis für n weitere Berechnungen dienen soll. Wie stellt Du Dir denn idealerweise (wenn es keine Typprobleme gäbe) den Teil vor, der dafür veantwortlich ist, das Ergebnis an die weiteren Funktionen zu geben?



  • Zu KNNs:
    - in Matrixform schreiben,
    - Transferfunktion so waehlen, dass Ableitung schon implizit gegeben
    - Parallelisierung ueber Matrixoperationen

    In diesem Zusammenhang empfehle ich immer http://www.amazon.de/Theorie-neuronalen-Netze-systematische-Einführung/dp/3540563539 , sollte in den meisten Uni-Bibliotheken verfuegbar sein.

    Ob es bei deinem Problem hilft, k.A. da ich es nicht verstehe.



  • wie waere es, wenn du dir eine liste (umgangssprachlich, nicht std::list) von zwischenergebnissen machst, wo du immer mal nachgucken kannst ob mit deinen parametern schon mal ein zwischenergebnis gespeichert worden war ? dann kannst es dir aus deiner liste eifach rausholen und damit weiterrechnen.
    z.b. ne map. da nen hashwert der relevanten parameter + zwischenergebnistype als key generieren. dann gibts auch keine probleme mit den Typen, falls es unterschiedliche zwischenergebnistypen geben sollte

    Meep Meep


Anmelden zum Antworten