kramann.info
© Guido Kramann

Login: Passwort:










kramann.info
© Guido Kramann

Login: Passwort:




Implementierung des evolutionären Optimierers als für einen Mirkocontroller geeignete Klasse

  • Nach den Vorarbeiten in dem vorangegangenen Unterkapitel, soll nun der evolutionäre Optimierer, der im Hauptprogramm des Beispiels in Regelungssysteme, Kapitel 8.6 "Umsetzung 3" umgesetzt wurde, nun als in Mikrocontrollern verwendbare Klasse (Evoopt) realisiert werden.
  • Um die Klasse zu testen, wird jedoch zunächst wieder das Beispiel aus Regelungssysteme, Kapitel 8.6 "Umsetzung 3" verwendet.
  • Damit die Klasse allgemein einsetzbar ist, darf in ihr nicht definiert sein, wie der Fehler berechnet wird.
  • Dies wird in einer weiteren speziellen Klasse vom Typ "Paramtest" festgelegt, von der ein Objekt in dem Optimierer registriert wird.
  • "Paramtest" wird zunächst als abstrakte Klasse definiert, mit der virtuellen Methode "berechneFehler(...)".
  • Die abgeleitete Klasse, in der dann die Methode "berechneFehler(...)" implementiert wird, heißt "ParamtestModell".
  • Insgesamt handelt es sich im folgenden also lediglich um eine Umstrukturierung der vorangegangenen Umsetzung, mit dem Ziel, den Optimierer auch für andere Anwendungen herausschälen zu können.
evoopt.zip - Objektorientierte Umsetzung des Evolutionären Optimierer, hier zur Optimierung der Geschwindigkeitsregelung.
Aufgabe
  • Verwenden Sie die Klassen aus evoopt, um die Parameter der Lenkung zu optimieren.

Heranführung an die Aufgabe: Wo müssen Anpassungen vorgenommen werden?

  • Das Hauptprogramm heißt Testlauf.cpp - Dieses muß neu kompiliert werden. Hier muß aber nichts geändert werden.
  • Zwei Dinge müssen angepasst werden:
  1. Die Funktion "berechneFehler(..)" in der Klasse "ParamtestModell" muß angepaßt werden.
  2. Die zu optimierenden Parameter müssen in dem Objekt param registriert werden. Dies geschieht im Kontruktor der Klasse "ParamtestModell"
  • Folgende Konstanten sollten zumindest überprüft werden:
  • PANZ, Klasse Param, s. "#define PANZ 3". Hier wird definiert, wieviele zu optimierende Parameter es gibt.
  • ANZAHL_DNA, Klasse Evoopt, s. "#define ANZAHL_DNA 100". Anzahl der Individuen innerhalb einer Generation
  • ANZAHL_BESTE, Klasse Evoopt, s. "#define ANZAHL_BESTE 10". Anzahl der Besten einer Generation, die in die nächste Generation übernommen werden.
  • MUTATIONSRATE, Klasse Evoopt, s. "#define MUTATIONSRATE 500 //0..1000". Hiermit wird festgelegt, wie groß der Anteil an spontanen Parameterveränderungen ist (Mutationsrate) .

Programmhierarchie

  • Zum besseren Verständnis des Programms soll dessen Struktur näher besschrieben werden.
  • Neben dem Hauptprogramm "Testlauf" mit der main-Methode, besteht es aus folgenden Klassen:
  1. Evoopt
  2. ParamtestModell
  3. Paramtest
  4. Param
  5. IntegratorRuKu
  6. Integrator
  7. ModellRegelkreis
  8. Modell
  • Auf der untersten Ebene findet die Simulation des Regelkreises statt.
  • Das Modell des Regelkreises ist in der Klasse "ModellRegelkreis" formuliert.
  • Ein Objekt dieser Klasse wird in einem Integrator-Objekt registriert.
  • Dieser kann nun mit Hilfe der Methode "rechteSeite(..)" aus dem Modellobjekt, die numerische Integration durchführen:
  • "anzahl" enthält die Anzahl der Zustandsgrößen und wird beim Aufruf des Konstruktors initialisert.
  • "anzahl" wird auch vom Integrator ausgelesen, um die richtige Anzahl an Gleichungen zu integrieren.
Simulationsebene des Optimierungsprogramms

Bild 0-1: Simulationsebene des Optimierungsprogramms

  • Zur Berechnung des Fehlers bei einem bestimmten Parametersatz, wird die Simulationsebene benutzt und ein Objekt vom Typ param, um die Wandlung zwischen double- und Integer-Repräsentation zu bewerkstelligen.
ParamtestModell - Ein Objekt dieses Typs liefert bei Übergabe eines Parametersatzes, den zugehörigen zu minimierenden Fehler.

Bild 0-2: ParamtestModell - Ein Objekt dieses Typs liefert bei Übergabe eines Parametersatzes, den zugehörigen zu minimierenden Fehler.

class ParamtestModell : public Paramtest
{
    private:
        ModellRegelkreis  modell;
        IntegratorRuKu    integrator;
    public:
        ParamtestModell()
        {
            integrator.registriereModell(&modell);
            //Die Modellparameter werden in param registriert, indem deren Zeiger 
            //in param.p gespeichert werden.
            //Ausserdem: Angabe des Definitionsbereiches jedes Parameters.
            param.registriere(0,&(modell.P),0.0,1000.0);
            param.registriere(1,&(modell.I),50.0,650.0);
            param.registriere(2,&(modell.D),-100.0,100.0);
        }
        double berechneFehler(uint16_t* psatz_akt) //liefert -1 wenn ungültige Parameterkombination
        {
            //double-Werte aus Integer-Repräsentation gewinnen:    
            for(int k=0;k<PANZ;k++)
            {
               param.u[k] = psatz_akt[k];                                  
               param.aktualisiereDouble(k);
            }                              
            //Simulation zur Bestimmung des aktuellen Fehlers durchführen:
            double t=0.0;
            double dt=0.001;
            double* yneu;
            double yalt[3] = {0.0,0.0,0.0};
            double fehler = 0.0;
            for(int i=0;i<2000;i++)
            {
                yneu = integrator.zeitschritt(yalt,dt);
                yalt[0] = yneu[0];
                yalt[1] = yneu[1];
                yalt[2] = yneu[2];
                t=t+dt;
                if(modell.pwm >1023.0)
                    return -1.0;
                fehler+= fabs(modell.e);
                if(fehler > 5.10839e+223)
                    return fehler;
            }
            return fehler;
        }
};

Code 0-1: Klasse ParamtestModell in der Datei ParamtestModell.h

  • Der evolutionäre Optimierungsvorgang findet in der Methode "optimieren()" in einem Objekt vom Typ Evoopt statt.
  • Hierzu benötigt das Objekt vom Typ Evoopt ein Objekt vom Typ ParamtestModell, um den aktuellen Fehler berechnen zu können.
  • Das ParamtestModell-Objekt wird in dem Evoopt-Objekt registriert.
Das Evoopt-Objekt benutzt ein ParamtestModell-Objekt.

Bild 0-3: Das Evoopt-Objekt benutzt ein ParamtestModell-Objekt.

#define ANZAHL_DNA 100   //Anzahl der Individuen innerhalb einer Generation
#define ANZAHL_BESTE 10  //Anzahl der Besten einer Generation, die in die nächste Generation übernommen werden.
#define MUTATIONSRATE 500 //0..1000
class Evoopt
{
    private:
        Paramtest* paramtest;
        double fehler[ANZAHL_DNA];              //Merker für die Fehler einer Generation
        int bestindex[ANZAHL_BESTE];           //Merker für die Indices der ANZAHL_BESTE Besten einer Generation
        double bestfehler[ANZAHL_BESTE];        //Merker für den Fehler bei den Besten.
        uint16_t psatz[ANZAHL_DNA][PANZ];       //Merker der Parametersätze einer Generation
        uint16_t psatzbest[ANZAHL_BESTE][PANZ]; //Merker der Parametersätze der Besten einer Generation
    public:
        Evoopt()
        {
            srand(0); //Die Berechnung von Pseudozufallszahlen muß auf dem Mikrocontroller womöglich anders erfolgen.
        }
        void registriereParamtest(Paramtest* paramtest)
        {
            this->paramtest = paramtest;
        }
        int zufall(int von, int bis)
        {
            int x = rand();
            if(x<0)
                x=-x;
            return x%(bis-von+1)+von;        
        }
        void mutieren(uint16_t* p)
        {
            if(zufall(0,10)>5)              //Hier Mutationsniveau einstellen
                return; 
            int index = zufall(0,PANZ-1);  //Auswahl eines der Parameter
            int stelle = zufall(0,16);     //Zufälliges Bit bei uint16_t
            if( (p[index] & (1<<stelle)) > 0)
            {
                p[index] = p[index] & ~(1<<stelle);
            }     
            else
            {
                p[index] = p[index] | (1<<stelle);
            }
        }
        void rekombinieren(uint16_t* best1, uint16_t* best2, uint16_t* satz)
        {
            for(int k=0;k<PANZ;k++)
            {     
                satz[k]=0;        
                for(int i=0;i<16;i++)
                {                   
                    if(zufall(0,100)>50)
                    {
                        satz[k] |= (best1[k] & (1<<i));
                    }
                    else
                    {
                        satz[k] |= (best2[k] & (1<<i));                           
                    }            
                }     
            }    
        }
        void kontrollausgabe()
        {
                //Vorbereitung der Kontrollausgabe
                for(int k=0;k<PANZ;k++)
                {
                    paramtest->param.u[k] = psatz[bestindex[0]][k];
                    paramtest->param.aktualisiereDouble(k);            
                }
                //Kontrollausgabe
                cout<<"Kleinster Fehler = "<<bestfehler[0]<<endl;
                for(int k=0;k<PANZ;k++)
                    cout<<"Param Nr."<<k<<" = "<<*(paramtest->param.p[k])<<endl;                    
        }
        void optimieren(int schritte=100)
        {
            //1. Fehler bei allen Parametersätzen bestimmen
            for(int i=0;i<ANZAHL_DNA;i++)
            {
                for(int k=0;k<PANZ;k++)            
                    psatz[i][k] = paramtest->param.u[k];
                mutieren(psatz[i]);
                fehler[i] = paramtest->berechneFehler(psatz[i]);
            }
            //Mehrere Generationen durchrechnen.
            for(int schritt=0;schritt<schritte;schritt++)
            {
                //ANZAHL_BESTE Beste finden:
                for(int i=0;i<ANZAHL_BESTE;i++)
                {
                    bestfehler[i] = fehler[0];
                    bestindex[i] = 0;
                }    
                for(int i=0;i<ANZAHL_DNA;i++)
                {
                    if( (fehler[i]<=bestfehler[0] && fehler[i]>0.0)
                       || bestfehler[0]<0.0
                    )
                    {
                        for(int p=ANZAHL_BESTE-1;p>=1;p--)
                        {
                            bestfehler[p] = bestfehler[p-1];
                            bestindex[p] = bestindex[p-1];
                        }                               
                        bestfehler[0]=fehler[i];
                        bestindex[0]=i;                
                    }            
                }                     
                kontrollausgabe();
                //2. Beste merken
                for(int i=0;i<10;i++)
                    for(int k=0;k<PANZ;k++)            
                        psatzbest[i][k] = psatz[bestindex[i]][k];            
                //3. rekombinieren
                for(int i=ANZAHL_BESTE;i<ANZAHL_DNA;i++)
                {
                    rekombinieren(psatzbest[zufall(0,ANZAHL_BESTE-1)],psatzbest[zufall(0,ANZAHL_BESTE-1)],psatz[i]);
                    if(zufall(0,999)<MUTATIONSRATE)
                        mutieren(psatz[i]);            
                }    
                //4. Beste zurückschreiben
                for(int i=0;i<ANZAHL_BESTE;i++)
                {
                    for(int k=0;k<PANZ;k++)            
                        psatz[i][k]=psatzbest[i][k];            
                }
               //5. Fehler bei allen Parametersätzen bestimmen
                for(int i=0;i<ANZAHL_DNA;i++)
                    fehler[i] = paramtest->berechneFehler(psatz[i]);
            }    
        }
};

Code 0-2: Quelltext der Klasse Evoopt in der Datei Evoopt.h

  • Schließlich muß der Optimierungsvorgang konfiguriert und ausgelöst werden.
  • Dies geschieht im Haptprogramm "Testlauf.cpp":
#include<iostream>
#include<fstream>
#include<math.h>
#include<stdlib.h>
#include<stdint.h> //für uint16_t
using namespace std;
#include "Modell.h"
#include "Integrator.h"
#include "ModellRegelkreis.h"
#include "IntegratorRuKu.h"
#include "Param.h"
#include "Paramtest.h"
#include "ParamtestModell.h"
#include "Evoopt.h"
//Param           param;
ParamtestModell paramtest;
Evoopt          evoopt;
int main()
{
    evoopt.registriereParamtest(¶mtest);    
    evoopt.optimieren(10); //10 Optimierungsschritte
    cout<<endl<<"Bisher bester Parametersatz:"<<endl;
    for(int k=0;k<PANZ;k++)
       cout<<"Param Nr."<<k<<" = "<<*(paramtest.param.p[k])<<endl;                        
}    

Code 0-3: Quellcode der Datei Testlauf.cpp