
Ensemble-Methoden zur Verbesserung numerischer Vorhersagen in MQL5
Einführung
Beim maschinellen Lernen entstehen oft mehrere Vorhersagemodelle unterschiedlicher Qualität. Praktiker bewerten in der Regel diese Modelle und wählen das beste Modell für reale Anwendungen aus. In diesem Artikel wird jedoch ein alternativer Ansatz untersucht: die Wiederverwendung scheinbar minderwertiger Modelle durch Kombination ihrer Ergebnisse, um die Gesamtvorhersageleistung zu verbessern. Wir werden verschiedene Techniken zur Kombination von Vorhersagen untersuchen und ihre Umsetzung in reinem MQL5 demonstrieren. Schließlich werden wir diese Methoden vergleichen und ihre Eignung für verschiedene Szenarien diskutieren.
Um das Konzept der Kombination von Modellvorhersagen zu formalisieren, führen wir einige wichtige Begriffe ein. Betrachten wir einen Trainingssatz, der aus K Datenpunkten besteht, die jeweils als Paar (xi,yi) dargestellt werden, wobei xi ein Prädiktorenvektor und yi die entsprechende skalare Antwortvariable ist, die wir vorhersagen wollen. Angenommen, wir haben N trainierte Modelle, von denen jedes in der Lage ist, Vorhersagen zu treffen. Wenn ein Prädiktor x vorliegt, erstellt Modell n eine Vorhersage, die als f_n(x) bezeichnet wird. Unser Ziel ist es, eine Konsensfunktion f(x) zu konstruieren, die diese N individuellen Vorhersagen effektiv kombiniert und so eine genauere Gesamtvorhersage als jedes einzelne Modell liefert.
Diese Konsensfunktion, die oft als Ensemble- oder Metamodell bezeichnet wird, hat das Potenzial, die Leistung der einzelnen Modelle zu übertreffen. Im Rahmen dieser Untersuchung werden wir uns mit verschiedenen Techniken zur Erstellung effektiver Ensemble-Modelle befassen und ihre praktische Umsetzung und Leistung in MQL5 bewerten.
Ensembles auf der Grundlage gemittelter Vorhersagen
Eine der einfachsten Techniken zur Kombination numerischer Vorhersagen ist die einfache Mittelwertbildung. Durch die Berechnung des Mittelwerts mehrerer Vorhersagen können wir oft eine genauere und solidere Schätzung erzielen, als wenn wir uns auf ein einzelnes Modell verlassen. Dieser Ansatz ist sowohl rechnerisch effizient als auch einfach zu implementieren, was ihn zu einer praktischen Wahl für eine Vielzahl von Anwendungen macht. Die Einfachheit des arithmetischen Mittels ist seine größte Stärke. Im Gegensatz zu komplexeren Ensemble-Methoden, die die Schätzung mehrerer Parameter erfordern, ist die Mittelwertbildung von Natur aus resistent gegen Überanpassung. Eine Überanpassung liegt vor, wenn ein Modell zu sehr an die spezifischen Merkmale der Trainingsdaten gebunden ist, wodurch seine Fähigkeit zur Verallgemeinerung auf ungesehene Daten beeinträchtigt wird.
Durch den vollständigen Verzicht auf eine Parameterschätzung umgeht der einfache Mittelwert dieses Problem und gewährleistet auch bei verrauschten oder kleinen Datensätzen eine konsistente Leistung. Im Gegensatz dazu sind andere Ensemble-Techniken, wie wir später noch untersuchen werden, oft mit Parameterabstimmung und -optimierung verbunden, was zu einer gewissen Anfälligkeit für eine Überanpassung führen kann. Die Mittelwertbildung mag zwar nicht so ausgefeilt sein wie die fortgeschrittenen Ensemble-Methoden, aber ihre Zuverlässigkeit und Nutzzerfreundlichkeit machen sie zu einem unverzichtbaren Werkzeug für das Ensemble-Lernen.
Ein grundlegendes mathematisches Prinzip, das in der Cauchy-Schwarz-Ungleichung verwurzelt ist, bildet die theoretische Grundlage für die Funktion, die ein Ensemble von gemittelten Vorhersagen definiert. Diese Ungleichung besagt, dass das Quadrat der Summe von N Zahlen immer kleiner oder gleich dem N-fachen der Summe ihrer Quadrate ist.
Betrachten wir nun einen Vektor von Prädiktoren x, die zur Vorhersage einer abhängigen Variable y verwendet werden. Setzt man a in die Ungleichung mit den Fehlern ein, die ein Modell macht, das y aus x vorhersagt, so ist a_n = f_n(x) - y. Wenn die Summanden auf der linken Seite dieser Gleichung aufgeteilt werden, indem man annimmt, dass f(x) der Durchschnitt der Vorhersagen ist. Durch Ausklammern von N und Ersetzen des ganz rechten Terms der Gleichung durch die linke Seite der von Cauchy abgeleiteten Ungleichung und anschließendes Dividieren beider Seiten durch N^2 erhalten wir die grundlegende Gleichung, die die Mittelwertbildung als Ensemble-Methode untermauert:
Die Summen auf der rechten Seite der obigen Gleichung stellen die quadrierten Fehler der einzelnen Modelle dar. Summiert man diese quadrierten Fehler und teilt sie durch die Anzahl der Komponentenmodelle, erhält man den mittleren quadratischen Fehler (MSE) der einzelnen Modelle. Die linke Seite der Gleichung stellt den quadrierten Fehler des Konsensmodells dar, der sich aus dem Mittelwert der einzelnen Vorhersagen ergibt.
Mathematisch gesehen besagt diese Ungleichung, dass für eine beliebige Menge von Prädiktoren und Zielen der quadratische Fehler der mittleren Vorhersage niemals den mittleren quadratischen Fehler der einzelnen Vorhersagen übersteigen wird. Gleichheit ist nur dann gegeben, wenn die Vorhersagefehler aller Einzelmodelle identisch sind.
Natürlich ist dieser Vorteil nicht ohne Einschränkungen. Die Wirksamkeit der Mittelwertbildung hängt wesentlich von der Art der Komponentenmodelle ab. Wenn alle Modelle eine ähnliche Vorhersagekraft haben, ist die Durchschnittsbildung ihrer Vorhersagen oft ein vernünftiger und effektiver Ansatz. Es kann jedoch zu Problemen kommen, wenn die Vorhersagekraft der einzelnen Modelle sehr unterschiedlich ist. In solchen Fällen kann die Mittelwertbildung die Beiträge der stärkeren Modelle verwässern, während die schwächeren Modelle überbetont werden, was die Gesamtvorhersageleistung des Ensembles verringern kann.
Der Code, der die Mittelwertbildung für Ensembles implementiert, ist in der Klasse CAvg gekapselt, die in ensemble.mqh definiert ist. Diese Klasse, wie auch alle anderen Klassen, die Ensemble-Methoden implementieren, sind darauf angewiesen, dass der Nutzzer eine Sammlung von trainierten Modellen bereitstellt. Diese Modelle müssen sich an die IModel-Schnittstelle halten, die wie folgt definiert ist:
//+------------------------------------------------------------------+ //| IModel interface defining methods for manipulation of learning | //| algorithms | //+------------------------------------------------------------------+ interface IModel { //train a model bool train(matrix &predictors,matrix&targets); //make a prediction with a trained model double forecast(vector &predictors); };
Die IModel-Schnittstelle spezifiziert zwei Methoden:
- train(): Diese Methode enthält die Logik für das Training eines Modells.
- forecast(): Diese Methode definiert die Vorgänge zur Erstellung von Prognosen auf der Grundlage neuer Eingabedaten.
//+------------------------------------------------------------------+ //| Compute the simple average of the predictions | //+------------------------------------------------------------------+ class CAvg { public: CAvg(void) ; ~CAvg(void) ; double predict(vector &inputs, IModel* &models[]) ; } ; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CAvg::CAvg(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CAvg::~CAvg(void) { } //+------------------------------------------------------------------+ //| Make a prediction by using consensus from multiple models | //+------------------------------------------------------------------+ double CAvg::predict(vector &inputs, IModel* &models[]) { double output = 0.0 ; for(uint imodel=0 ; imodel<models.Size() ; imodel++) { output +=models[imodel].forecast(inputs) ; } output /= double(models.Size()) ; return output; }
Die Klasse CAvg enthält eine Methode predict(), die mit einem Vektor von Eingabedaten und einem Array von vortrainierten Komponentenmodellen aufgerufen wird. Diese Methode gibt einen skalaren Wert zurück, der die Konsensprognose darstellt. Bei der Klasse CAvg wird die Konsensvorhersage als mittlere Vorhersage berechnet, die aus der bereitgestellten Reihe von Modellen abgeleitet wird. Durch die Einhaltung dieses Konzepts gewährleistet die Klasse CAvg Flexibilität und Modularität, sodass die Nutzzer verschiedene Modelltypen nahtlos in ihre Ensemble-Methoden integrieren können.
Unbeschränkte lineare Kombinationen von Vorhersagemodellen
Wenn eine Reihe von Modellen mit sehr unterschiedlichen Vorhersagequalitäten zur Verfügung steht, kann eine einfache lineare Regression als Ensemble-Methode eingesetzt werden. Die Idee besteht darin, die Konsensprognose als gewichtete Summe der Prognosen der einzelnen Modelle zu berechnen, einschließlich eines konstanten Terms, um etwaige Verzerrungen zu berücksichtigen.
Diese Ensemble-Methode ist in der Klasse CLinReg implementiert. Der Konstruktor, der Destruktor und die Methode predict() haben die gleichen Signaturen wie die der zuvor beschriebenen Klasse CAvg.
//+------------------------------------------------------------------+ //| Compute the linear regression of the predictions | //+------------------------------------------------------------------+ class CLinReg { public: CLinReg(void) ; ~CLinReg() ; bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); double predict(vector &inputs, IModel* &models[]) ; private: OLS *m_linreg ; // The linear regression object } ; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLinReg::CLinReg(void) { m_linreg = new OLS(); } //+------------------------------------------------------------------+ //| Fit the consensus model from saved models | //+------------------------------------------------------------------+ bool CLinReg::fit(matrix &train_vars,vector &train_targets,IModel* &models[]) { matrix independent(train_vars.Rows(),models.Size()+1); for(ulong i=0 ; i<independent.Rows() ; i++) // Build the design matrix { independent[i][models.Size()] = 1.0; vector ins = train_vars.Row(i); for(uint imodel=0 ; imodel<models.Size() ; imodel++) independent[i][imodel] = models[imodel].forecast(ins) ; } return m_linreg.Fit(train_targets,independent); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLinReg::~CLinReg(void) { if(CheckPointer(m_linreg)==POINTER_DYNAMIC) delete m_linreg ; } //+------------------------------------------------------------------+ //| Predict | //+------------------------------------------------------------------+ double CLinReg::predict(vector &inputs, IModel* &models[]) { vector args = vector::Zeros(models.Size()); for(uint i = 0; i<models.Size(); i++) args[i] = models[i].forecast(inputs); return m_linreg.Predict(args); }
Die Klasse CLinReg führt jedoch eine fit()-Methode ein, mit der die Operationen für das Training des Konsensmodells festgelegt werden.
Die Methode fit() nimmt als Eingabe:
- Matrix der Prädiktoren.
- Vektorielle Ziele.
- Eine Reihe von Komponentenmodellen.
In fit() wird eine Instanz der OLS-Klasse verwendet, um das Konsensregressionsmodell darzustellen. Die Matrixvariable independent dient als Entwurfsmatrix, die aus den quadrierten Fehlern der Vorhersagen der einzelnen Komponentenmodelle, ergänzt um einen konstanten Term (eine Spalte mit Einsen), gebildet wird. Wenn die Methode predict() von CLinReg aufgerufen wird, gibt sie das Ergebnis der Verwendung der Vorhersagefehler der Komponentenmodelle als Input für das Konsensregressionsmodell zurück.
Die Kombination von Modellen als gewichtete Summe von Komponentenvorhersagen funktioniert gut in bestimmten, seltenen Szenarien. Dieser Ansatz wird jedoch in der Praxis oft übersehen, und zwar aus zwei Hauptgründen:
- Risiko der Überanpassung: Die Gewichte im Konsensmodell sind Parameter, die optimiert werden müssen. Wenn das Ensemble viele Komponentenmodelle umfasst, kann der Optimierungsprozess zu einer erheblichen Überanpassung führen, was die Fähigkeit des Modells zur Verallgemeinerung auf ungesehene Daten verringert.
- Kollinearität: Wenn zwei oder mehr Modelle ähnliche Vorhersagen liefern, kann die Kollinearität zu einer Instabilität der Gewichtsschätzungen führen. Dieses Problem entsteht, weil die Gewichte für Modelle mit ähnlicher Leistung sich zu einer Konstante summieren können, wobei sich diese Modelle nur für die Fälle ähnlich verhalten, die während des Trainings auftreten.
Diese Annahme ist jedoch in der Praxis oft nicht haltbar. Wenn ein Fall auftritt, der außerhalb der Stichprobe liegt, können Modelle, die zuvor ähnliche Vorhersagen gemacht haben, unterschiedlich reagieren, was dazu führen kann, dass das Konsensmodell extreme und unzuverlässige Ergebnisse liefert.
Eingeschränkte lineare Kombinationen von verzerrten Modellen
Die Verwendung einer einfachen Regression als Grundlage für die Kombination mehrerer Vorhersagemodelle kann manchmal zu einem instabilen Modell mit extremen Gewichten führen. Dieses Problem tritt in der Regel auf, wenn die Regressionskoeffizienten entgegengesetzte Vorzeichen haben, was erforderlich ist, um die Werte auszugleichen, damit die Daten gut passen. So kann beispielsweise ein Koeffizient eines korrelierten Modellpaares nur dann einen großen positiven Wert annehmen, wenn sein Gegenstück einen kleinen negativen Wert annimmt. Um diese Extremwerte zu vermeiden, können wir die Regressionskoeffizienten einschränken, um extreme negative Werte zu vermeiden. Dieser Ansatz reduziert auch die Anzahl der Freiheitsgrade im Optimierungsprozess, wodurch das Modell stabiler und weniger anfällig für eine Überanpassung wird.
Diese Ensemble-Methode ist in der Klasse Cbiased implementiert. Es enthält die bereits erwähnten Methoden fit()- und predict(), die in anderen Ensemble-Implementierungen zu finden sind.
//+------------------------------------------------------------------+ //|Compute the optimal linear combination of the predictions | //|subject to the constraints that the weights are all nonnegative. | //|A constant term is also included. | //|This is appropriate for biased predictors | //+------------------------------------------------------------------+ class Cbiased:public PowellsMethod { public: Cbiased(void) ; ~Cbiased() ; bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); double predict(vector &inputs,IModel* &models[]) ; private: vector m_coefs ; // Computed coefficients here int biased_ncases ; int biased_nvars ; matrix biased_x ; vector biased_y ; virtual double func(vector &p,int n=0); } ; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ Cbiased::Cbiased(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ Cbiased::~Cbiased(void) { } //+------------------------------------------------------------------+ //| Function to be optimized | //+------------------------------------------------------------------+ double Cbiased::func(vector &p,int n = 0) { double err, pred,diff, penalty ; // Compute criterion err = 0.0 ; for(int i=0 ; i<biased_ncases ; i++) { pred = p[p.Size()-1] ; // Will cumulate prediction for(int j=0 ; j<biased_nvars ; j++) // For all model outputs pred += biased_x[i][j] * p[j] ; // Weight them per call diff = pred - biased_y[i] ; // Predicted minus true err += diff * diff ; // Cumulate squared error } penalty = 0.0 ; for(int j=0 ; j<biased_nvars ; j++) { if(p[j] < 0.0) penalty -= 1.e30 * p[j] ; } return err + penalty ; } //+------------------------------------------------------------------+ //| Fit the consensus model | //+------------------------------------------------------------------+ bool Cbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { biased_ncases = int(train_vars.Rows()); biased_nvars = int(models.Size()); biased_x = matrix::Zeros(biased_ncases,biased_nvars); biased_y = train_targets; m_coefs = vector::Zeros(biased_nvars+1); for(int i = 0; i<biased_ncases; i++) { vector ins = train_vars.Row(i); for(int j = 0; j<biased_nvars; j++) biased_x[i][j] = models[j].forecast(ins); } m_coefs.Fill(1.0/double(biased_nvars)); m_coefs[m_coefs.Size()-1] = 0.0; int iters = Optimize(m_coefs,int(m_coefs.Size())); double sum = m_coefs.Sum(); m_coefs/=sum; return true; } //+------------------------------------------------------------------+ //| Make prediction with consensus model | //+------------------------------------------------------------------+ double Cbiased::predict(vector &inputs,IModel* &models[]) { double output=0.0; for(uint imodel=0 ; imodel<models.Size() ; imodel++) { output += m_coefs[imodel] * models[imodel].forecast(inputs); } return output; }
Der entscheidende Unterschied bei Cbiased liegt jedoch darin, wie die Gewichte optimiert werden.
Die Optimierung der Gewichte erfolgt nach der Powell-Methode zur Funktionsminimierung. Aus diesem Grund ist die Klasse Cbiased ein Nachkomme der Klasse PowellsMethod. Die Kriteriumsfunktion ist in der Methode func() implementiert, die durch die Trainingsdaten iteriert und die quadrierten Fehler unter Verwendung der angegebenen Gewichte akkumuliert. Für jede Stichprobe im Datensatz:
- Die Vorhersagen aus den Komponentenmodellen werden nach den aktuellen Gewichten und einem konstanten Term gewichtet.
- Die quadrierten Differenzen zwischen den Vorhersagen und den Zielwerten werden addiert.
Die Kriteriumsfunktion endet mit der Prüfung, ob eine der Versuchsgewichte negativ ist. Sind die Gewichte negativ, wird eine Strafe verhängt. Die Funktion gibt den Gesamtfehler zuzüglich eines eventuellen Abzugs für die negativen Gewichte zurück. Diese Art von Ensemble-Methode ist am besten geeignet, wenn bekannt ist, dass einige Komponentenmodelle verzerrt sind. Verzerrungen in diesem Zusammenhang beziehen sich auf Modelle, die im Vergleich zu den entsprechenden Zielwerten durchgängig entweder zu hohe oder zu niedrige Vorhersagen liefern, wobei häufig eine auffällige Tendenz zu beobachten ist. Durch die Beschränkung der Gewichte reduziert Cbiased den Einfluss voreingenommener Modelle, was zu einer ausgewogeneren und genaueren Ensemblevorhersage führt. Im nächsten Abschnitt stellen wir eine Methode vor, die sich für Modellsätze eignet, die wenig bis keine Verzerrungen aufweisen, wobei der Schwerpunkt auf der Aggregation von Vorhersagen aus Modellen mit vergleichbarer Leistung liegt.
Eingeschränkte Kombinationen von unverzerrten Modellen
Wenn bekannt ist, dass ein Satz von Komponentenmodellen keine signifikante Verzerrung in ihren Vorhersagen aufweist, besteht keine Notwendigkeit, einen konstanten Term in das Konsensmodell aufzunehmen. Die Entfernung des konstanten Terms trägt dazu bei, die Tendenz zur Überanpassung des Modells der Daten zu verringern. Darüber hinaus stellt dieser Ansatz sicher, dass die Gewichte der Modelle niemals negativ sind, wie dies bei früheren Methoden der Fall war. Darüber hinaus gibt es eine zusätzliche Bedingung für die Gewichte: Sie müssen die Summe eins ergeben. Diese Einschränkung bietet zwei wesentliche Vorteile:
- Sicherstellung eines unvoreingenommenen Konsensmodells: Solange die Komponentenmodelle einigermaßen unvoreingenommen sind, stellt die Forderung, dass die Summe der Gewichte eins ergibt, sicher, dass auch das Konsensmodell unvoreingenommen bleibt.
- Interpolation zwischen Vorhersagen: Die Bedingung von „Summe-ergibt-Eins“ garantiert, dass die Konsensvorhersage eine Interpolation zwischen den Vorhersagen der Komponentenmodelle ist. Dadurch wird sichergestellt, dass die endgültige Vorhersage nicht drastisch von den einzelnen Vorhersagen abweicht, sodass extreme Ergebnisse, die durch extreme Gewichtungen entstehen könnten, vermieden werden.
Dies wird durch die folgende Gleichung veranschaulicht:
Der Code für diese Ensemble-Methode ist weitgehend identisch mit den vorherigen Implementierungen. Der Hauptunterschied zur Klasse CUnbiased liegt in der zu minimierenden Funktion.
//+------------------------------------------------------------------+ //|Compute the optimal linear combination of the predictions | //|subject to the constraints that the weights are all nonnegative | //|and they sum to one. This is appropriate for unbiased predictors.| //+------------------------------------------------------------------+ class CUnbiased:public PowellsMethod { public: CUnbiased(void) ; ~CUnbiased() ; bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); double predict(vector &inputs,IModel* &models[]) ; private: vector m_coefs ; // Computed coefficients here int unbiased_ncases ; int unbiased_nvars ; matrix unbiased_x ; vector unbiased_y ; virtual double func(vector &p,int n=0); } ; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CUnbiased::CUnbiased(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CUnbiased::~CUnbiased(void) { } //+------------------------------------------------------------------+ //| Function to be optimized | //+------------------------------------------------------------------+ double CUnbiased::func(vector &p,int n = 0) { double sum, err, pred,diff, penalty ; // Normalize weights to sum to one sum = p.Sum() ; if(sum < 1.e-60) // Should almost never happen sum = 1.e-60 ; // But be prepared to avoid division by zero vector unbiased_work = p / sum ; // Compute criterion err = 0.0 ; for(int i=0 ; i<unbiased_ncases ; i++) { pred = 0.0 ; // Will cumulate prediction for(int j=0 ; j<unbiased_nvars ; j++) // For all model outputs pred += unbiased_x[i][j] * unbiased_work[j] ; // Weight them per call diff = pred - unbiased_y[i] ; // Predicted minus true err += diff * diff ; // Cumulate squared error } penalty = 0.0 ; for(int j=0 ; j<unbiased_nvars ; j++) { if(p[j] < 0.0) penalty -= 1.e30 * p[j] ; } return err + penalty ; } //+------------------------------------------------------------------+ //| Fit the consensus model | //+------------------------------------------------------------------+ bool CUnbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { unbiased_ncases = int(train_vars.Rows()); unbiased_nvars = int(models.Size()); unbiased_x = matrix::Zeros(unbiased_ncases,unbiased_nvars); unbiased_y = train_targets; m_coefs = vector::Zeros(unbiased_nvars); for(int i = 0; i<unbiased_ncases; i++) { vector ins = train_vars.Row(i); for(int j = 0; j<unbiased_nvars; j++) unbiased_x[i][j] = models[j].forecast(ins); } m_coefs.Fill(1.0/double(unbiased_nvars)); int iters = Optimize(m_coefs); double sum = m_coefs.Sum(); m_coefs/=sum; return true; } //+------------------------------------------------------------------+ //| Make prediction with consensus model | //+------------------------------------------------------------------+ double CUnbiased::predict(vector &inputs,IModel* &models[]) { double output=0.0; for(uint imodel=0 ; imodel<models.Size() ; imodel++) { output += m_coefs[imodel] * models[imodel].forecast(inputs); } return output; }
Diese Funktion berücksichtigt die zuvor besprochenen zusätzlichen Einschränkungen, insbesondere die Nicht-Negativität der Gewichte und die Anforderung, dass sie sich zu Eins summieren.
Varianz-gewichtete Kombinationen von Vorhersagemodellen
Eine andere Methode zur Kombination von Vorhersagen aus Komponentenmodellen basiert auf einer optimalen Gewichtung, die durch die Vorhersagegenauigkeit der einzelnen Modelle bestimmt wird. Bei dieser Technik werden Modellen mit größeren Vorhersagefehlern kleinere Gewichte zugewiesen und Modellen mit kleineren Vorhersagefehlern größere Gewichte. Diese Methode ist besonders effektiv, wenn die Qualität der Komponentenmodelle stark schwankt. Wenn die Modelle jedoch stark korrelieren, ist diese Technik möglicherweise nicht ideal, und eine andere Ensemble-Methode sollte in Betracht gezogen werden.
Die Idee hinter der Gewichtung nach der Modellqualität beruht auf der Theorie, dass bei unvoreingenommenen und unkorrelierten Modellen die Zuweisung von Gewichten, die umgekehrt proportional zu den Fehlern der Modelle sind, den erwarteten quadratischen Fehler minimiert.
Zu diesem Zweck wird die relative Gewichtung für jedes Modell auf der Grundlage des Kehrwerts seines Fehlers berechnet, und anschließend werden die Gewichte so skaliert, dass sie in der Summe eins ergeben.
Das Ensemble der varianzgewichteten Modelle ist in der Klasse CWeighted implementiert. In der Methode fit(), für jede Trainingsstichprobe:
- Die Vorhersage der einzelnen Komponentenmodelle wird berechnet.
- Und der quadratische Fehler jeder Vorhersage wird akkumuliert.
//+------------------------------------------------------------------+ //| Compute the variance-weighted average of the predictions | //+------------------------------------------------------------------+ class CWeighted { public: CWeighted(void) ; ~CWeighted() ; bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); double predict(vector &inputs,IModel* &models[]) ; private: vector m_coefs ; // Computed coefficients here }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CWeighted::CWeighted(void) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CWeighted::~CWeighted(void) { } //+------------------------------------------------------------------+ //| Fit a consensus model | //+------------------------------------------------------------------+ bool CWeighted::fit(matrix &train_vars,vector &train_targets,IModel* &models[]) { m_coefs = vector::Zeros(models.Size()); m_coefs.Fill(1.e-60); double diff = 0.0; for(ulong i = 0; i<train_vars.Rows(); i++) { vector ins = train_vars.Row(i); for(ulong j = 0; j<m_coefs.Size(); j++) { diff = models[j].forecast(ins) - train_targets[i]; m_coefs[j] += (diff*diff); } } m_coefs=1.0/m_coefs; m_coefs/=m_coefs.Sum(); return true; } //+------------------------------------------------------------------+ //| Make a prediction with the consensus model | //+------------------------------------------------------------------+ double CWeighted::predict(vector &inputs,IModel* &models[]) { double output = 0.0; for(uint i = 0; i<models.Size(); i++) output+=m_coefs[i]*models[i].forecast(inputs); return output; }
Sobald dies für alle Trainingsbeispiele geschehen ist, wird der Gesamtfehler für jedes Modell verwendet, um sein Gewicht zu berechnen. Diese Gewichte werden dann addiert und skaliert, um sicherzustellen, dass die Gesamtsumme der Gewichte eins beträgt. Dieser Ansatz stellt sicher, dass Modelle mit geringeren Fehlern mehr Einfluss auf die endgültige Ensemble-Vorhersage haben, was die Vorhersage verbessern kann, insbesondere in Szenarien, in denen die Modelle unterschiedliche Grade der Vorhersagegenauigkeit aufweisen.
Interpolierte Kombinationen auf der Grundlage allgemeiner neuronaler Regressionsnetze
Die bisher besprochenen Ensemble-Methoden funktionieren gut, wenn das Konsensmodell mit sauberen Daten trainiert wird. Wenn die Trainingsdaten jedoch verrauscht sind, kann das Modell unter einer schlechten Generalisierung leiden. Eine wirksame Regressionsmethode zur Lösung dieses Problems ist das Allgemeine Neuronale Regressionsnetzwerk (GRNN). Der herausragende Vorteil von GRNN gegenüber der traditionellen Regression ist die geringere Anfälligkeit für Überanpassung. Dies liegt daran, dass die Parameter von GRNNs im Vergleich zu traditionellen Regressionstechniken relativ geringe Auswirkungen auf das Modell haben. Dieser Gewinn an Verallgemeinerung geht zwar auf Kosten einer gewissen Genauigkeit, aber GRNNs können komplexe, nicht lineare Beziehungen modellieren und sind damit ein nützliches Werkzeug, wenn die Daten solche Merkmale aufweisen.
GRNNs erstellen Vorhersagen, die Interpolationen zwischen den Zielwerten in den Trainingsdaten sind. Die Interpolation wird durch ein Gewicht bestimmt, das festlegt, wie sich ein Fall außerhalb der Stichprobe von den bekannten Fällen innerhalb der Stichprobe unterscheidet. Je ähnlicher die Proben sind, desto höher ist das zugewiesene relative Gewicht. Obwohl ein GRNN aufgrund der Interpolation unbekannter Stichproben in den bekannten Stichprobenraum als eine Glättungsoperation beschrieben werden kann, basieren seine theoretischen Grundlagen auf der Statistik.
Bei einem Datensatz, der Vorhersagen von Komponentenmodellen und ihren entsprechenden Zielen enthält, ist die Konsensvorhersage eines GRNN der minimale erwartete quadratische Fehler, der durch die bedingte Erwartung gegeben ist.
Da die gemeinsame Dichte der Trainingsdaten in der Regel nicht bekannt ist, können wir die Formel für die bedingte Erwartung nicht direkt verwenden. Stattdessen verlassen wir uns auf Schätzungen der gemeinsamen Dichten, die aus den Trainingsdaten abgeleitet werden, was zu der unten dargestellten Form des GRNN führt.
Bevor wir den Code für die GRNN-basierte Ensemble-Methode vorstellen, müssen wir zunächst die Implementierung des GRNN diskutieren. Der Code für das GRNN ist in grnn.mqh definiert, das die Definition der Klasse CGrnn enthält.
//+------------------------------------------------------------------+ //| General regression neural network | //+------------------------------------------------------------------+ class CGrnn { public: CGrnn(void); CGrnn(int num_outer, int num_inner, double start_std); ~CGrnn(void); bool fit(matrix &predictors,matrix &targets); vector predict(vector &predictors); //double get_mse(void); private: bool train(void); double execute(void); ulong m_inputs,m_outputs; int m_inner,m_outer; double m_start_std; ulong m_rows,m_cols; bool m_trained; vector m_sigma; matrix m_targets,m_preds; };
Die GRNN-Implementierung für Regressionsaufgaben umfasst mehrere Schlüsselkomponenten. Der Konstruktor initialisiert Parameter wie die Anzahl der inneren und äußeren Iterationen und die Anfangsstandardabweichung für die Sigma-Gewichte.
//+------------------------------------------------------------------+ //| Default constructor | //+------------------------------------------------------------------+ CGrnn::CGrnn(void) { m_inner = 100; m_outer = 10; m_start_std = 3.0; } //+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CGrnn::CGrnn(int num_outer,int num_inner,double start_std) { m_inner = num_inner; m_outer = num_outer; m_start_std = start_std; }
Die Fit-Methode speichert die Trainingsdaten, einschließlich der Eingabeprädiktoren und Zielwerte, und initialisiert die Sigma-Gewichte. Anschließend wird das GRNN-Modell durch iterative Optimierung der Sigma-Gewichte mithilfe eines Simulated Annealing-Ansatzes trainiert. Während des Trainings werden die Sigma-Gewichte gestört, und der Kreuzvalidierungsfehler für die gestörten Gewichte wird berechnet. Die Störung wird auf der Grundlage des Fehlers und eines Temperaturparameters angenommen oder abgelehnt, wobei die Temperatur schrittweise reduziert wird, um die Suche zu fokussieren.
//+------------------------------------------------------------------+ //| Fit data to a model | //+------------------------------------------------------------------+ bool CGrnn::fit(matrix &predictors,matrix &targets) { m_targets = targets; m_preds = predictors; m_trained = false; m_rows = m_preds.Rows(); m_cols = m_preds.Cols(); m_sigma = vector::Zeros(m_preds.Cols()); if(m_targets.Rows() != m_preds.Rows()) { Print(__FUNCTION__, " invalid inputs "); return false; } m_trained = train(); return m_trained; }
Die Vorhersagemethode berechnet den Abstand zwischen dem Eingangsvektor und jedem Trainingsdatenpunkt, gewichtet die Trainingsdatenpunkte auf der Grundlage ihres Abstands zum Eingang und berechnet die vorhergesagte Ausgabe als gewichteten Durchschnitt der Zielwerte der Trainingsdatenpunkte. Die Sigma-Gewichte bestimmen den Einfluss der einzelnen Trainingsdatenpunkte auf die Vorhersage.
//+------------------------------------------------------------------+ //| Make a prediction with a trained model | //+------------------------------------------------------------------+ vector CGrnn::predict(vector &predictors) { if(!m_trained) { Print(__FUNCTION__, " no trained model available for predictions "); return vector::Zeros(1); } if(predictors.Size() != m_cols) { Print(__FUNCTION__, " invalid inputs "); return vector::Zeros(1); } vector output = vector::Zeros(m_targets.Cols()); double diff,dist,psum=0.0; for(ulong i = 0; i<m_rows; i++) { dist = 0.0; for(ulong j = 0; j<m_cols; j++) { diff = predictors[j] - m_preds[i][j]; diff/= m_sigma[j]; dist += (diff*diff); } dist = exp(-dist); if(dist< EPS1) dist = EPS1; for(ulong k = 0; k<m_targets.Cols(); k++) output[k] += dist * m_targets[i][k]; psum += dist; } output/=psum; return output; }
Die Kreuzvalidierung wird verwendet, um die Leistung des Modells zu bewerten und die Sigma-Gewichte zu optimieren, während das simulierte Glühen als meta-heuristischer Optimierungsalgorithmus zur Ermittlung der optimalen Sigma-Gewichte dient. Letztlich führt GRNN eine kernelbasierte Interpolation durch, bei der die Vorhersage eine gewichtete Interpolation zwischen den Trainingsdatenpunkten ist.
Das auf dem GRNN basierende Ensemble ist in der Klasse CGenReg implementiert.
//+------------------------------------------------------------------+ //| Compute the General Regression of the predictions | //+------------------------------------------------------------------+ class CGenReg { public: CGenReg(void) ; ~CGenReg(void) ; bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); double predict(vector &inputs,IModel* &models[]) ; private: CGrnn *grnn ; // The GRNN object vector m_work ; // Work vector nmodels long vector m_targs; matrix m_vars; } ;
Die Klasse CGenReg verwendet ein CGrnn-Objekt, um komplexe Beziehungen zwischen den Vorhersagen der einzelnen Modelle und den tatsächlichen Zielwerten zu modellieren. Bei der Methode fit werden zunächst die Trainingsdaten gespeichert, einschließlich der Zielwerte (train_targets) und der Eingabevariablen (train_vars). Anschließend werden die einzelnen Vorhersagen jedes Modells gesammelt, wobei eine Matrix (preds) erstellt wird, bei der jede Zeile eine Trainingsstichprobe darstellt und jede Spalte die Vorhersage des entsprechenden Modells im Satz enthält. Das Objekt CGrnn wird unter Verwendung der Matrix der individuellen Vorhersagen (preds) als Eingabe und der tatsächlichen Zielwerte (targ) als Ausgabe trainiert.
//+------------------------------------------------------------------+ //| Fit consensus model | //+------------------------------------------------------------------+ bool CGenReg::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { m_targs = train_targets; m_vars = train_vars; m_work = vector::Zeros(models.Size()); matrix targ = matrix::Zeros(train_targets.Size(),1); if(!targ.Col(train_targets,0)) { Print(__FUNCSIG__, " error adding column ", GetLastError()); return false; } matrix preds(m_vars.Rows(),models.Size()); for(ulong i = 0; i<m_vars.Rows(); i++) { vector ins = m_vars.Row(i); for(uint j = 0; j< models.Size(); j++) { preds[i][j] = models[j].forecast(ins); } } return grnn.fit(preds,targ); }
Bei der Predict-Methode sammelt die Klasse die Vorhersagen der einzelnen Modelle für eine neue Eingabe (inputs) und speichert sie in einem Arbeitsvektor (m_work). Das trainierte CGrnn wird dann zur Vorhersage der endgültigen Ausgabe auf der Grundlage dieser einzelnen Vorhersagen verwendet. Die Methode gibt das erste Element des vorhergesagten Ausgangsvektors als endgültige Vorhersage zurück.
//+------------------------------------------------------------------+ //| Make a prediction | //+------------------------------------------------------------------+ double CGenReg::predict(vector &inputs,IModel* &models[]) { vector output; for(uint i = 0; i<models.Size(); i++) m_work[i] = models[i].forecast(inputs); output = grnn.predict(m_work); return output[0]; }
Schlussfolgerung: Ein Vergleich von Ensemble-Methoden
Es wurden verschiedene Ensemble-Methoden vorgestellt, deren Stärken und Schwächen kurz erörtert wurden. Zum Abschluss dieses Artikels werden wir untersuchen, wie diese Methoden bei der Anwendung auf reale Daten abschneiden. Dieser Vergleich ist als MetaTrader 5-Skript mit dem Namen Ensemble_Demo.mq5 implementiert.
Das Skript erzeugt mehrere synthetische Gruppen von Datensätzen. Die erste Gruppe besteht aus Datensätzen, die zum Trainieren von Benchmark-Modellen verwendet werden. Modelle, die mit solchen Daten trainiert wurden, werden als gute Modelle bezeichnet, und die Daten selbst gelten als sauber. Eine zweite Gruppe von Datensätzen wird erstellt, um schlechte Modelle zu trainieren, die den guten Modellen, die mit den sauberen Daten trainiert wurden, unterlegen sind.
Die letzte Gruppe von Datensätzen wird verwendet, um Modelle zu trainieren, die als voreingenommen gelten. Diese Modelle sind im Vergleich zu den bereits erwähnten guten Modellen verzerrt. Ein Teildatensatz aus jeder Gruppe wird kombiniert, um verrauschte Daten zu simulieren.
Mit dem Skript kann der Nutzzer festlegen, wie viele gute, schlechte und voreingenommene Modelle trainiert werden sollen. Der Nutzzer hat auch die Kontrolle über die Anzahl der Stichproben, aus denen die Trainingsdaten bestehen, und kann so beurteilen, wie sich die Stichprobengröße auf die Leistung der Ensemble-Methoden auswirkt. Schließlich können die Nutzzer wählen, ob sie ein Ensemble-Modell mit sauberen Daten trainieren wollen, indem sie den Parameter TrainCombinedModelsOnCleanData auf true setzen, oder ob sie es mit verrauschten Daten trainieren wollen, indem sie ihn auf false setzen.
Die Modelle sind vorwärtsgerichtete neuronale Netze, die durch die Klasse FFNN in mlffnn.mqh implementiert werden.
//+------------------------------------------------------------------+ //| Class for a basic feed-forward neural network | //+------------------------------------------------------------------+ class FFNN { protected: bool m_trained; // flag noting if neural net successfully trained matrix m_weights[]; // layer weights matrix m_outputs[]; // hidden layer outputs matrix m_result; // training result uint m_epochs; // number of epochs ulong m_num_inputs; // number of input variables for nn ulong m_layers; // number of layers of neural net ulong m_hidden_layers; // number of hidden layers ulong m_hidden_layer_size[]; // node config for layers double m_learn_rate; // learning rate ENUM_ACTIVATION_FUNCTION m_act_fn; // activation function //+------------------------------------------------------------------+ //| Initialize the neural network structure | //+------------------------------------------------------------------+ virtual bool create(void) { if(m_layers - m_hidden_layers != 1) { Print(__FUNCTION__," Network structure misconfiguration "); return false; } for(ulong i = 0; i<m_layers; i++) { if(i==0) { if(!m_weights[i].Init(m_num_inputs+1,m_hidden_layer_size[i])) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } } else if(i == m_layers-1) { if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } } else { if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,m_hidden_layer_size[i])) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } } } return true; } //+------------------------------------------------------------------+ //| Calculate output from all layers | //+------------------------------------------------------------------+ virtual matrix calculate(matrix &data) { if(data.Cols() != m_weights[0].Rows()-1) { Print(__FUNCTION__," input data not compatible with network structure "); return matrix::Zeros(0,0); } matrix temp = data; for(ulong i = 0; i<m_hidden_layers; i++) { if(!temp.Resize(temp.Rows(), m_weights[i].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[i].Rows() - 1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); matrix::Zeros(0,0); } m_outputs[i]=temp.MatMul(m_weights[i]); if(!m_outputs[i].Activation(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return matrix::Zeros(0,0); } } if(!temp.Resize(temp.Rows(), m_weights[m_hidden_layers].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[m_hidden_layers].Rows() - 1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return matrix::Zeros(0,0); } return temp.MatMul(m_weights[m_hidden_layers]); } //+------------------------------------------------------------------+ //| Backpropagation method | //+------------------------------------------------------------------+ virtual bool backprop(matrix &data, matrix& targets, matrix &result) { if(targets.Rows() != result.Rows() || targets.Cols() != result.Cols()) { Print(__FUNCTION__," invalid function parameters "); return false; } matrix loss = (targets - result) * 2; matrix gradient = loss.MatMul(m_weights[m_hidden_layers].Transpose()); matrix temp; for(long i = long(m_hidden_layers-1); i>-1; i--) { if(!m_outputs[i].Activation(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } if(!temp.Resize(temp.Rows(), m_weights[i+1].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[i+1].Rows() - 1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } m_weights[i+1] = m_weights[i+1] + temp.Transpose().MatMul(loss) * m_learn_rate; if(!m_outputs[i].Derivative(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } if(!gradient.Resize(gradient.Rows(), gradient.Cols() - 1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } loss = gradient * temp; gradient = (i>0)?loss.MatMul(m_weights[i].Transpose()):gradient; } temp = data; if(!temp.Resize(temp.Rows(), m_weights[0].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[0].Rows() - 1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); return false; } m_weights[0] = m_weights[0] + temp.Transpose().MatMul(loss) * m_learn_rate; return true; } public: //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ FFNN(ulong &layersizes[], ulong num_layers = 3) { m_trained = false; m_layers = num_layers; m_hidden_layers = m_layers - 1; ArrayCopy(m_hidden_layer_size,layersizes,0,0,int(m_hidden_layers)); ArrayResize(m_weights,int(m_layers)); ArrayResize(m_outputs,int(m_hidden_layers)); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ ~FFNN(void) { } //+------------------------------------------------------------------+ //| Neural net training method | //+------------------------------------------------------------------+ bool fit(matrix &data, matrix &targets,double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs) { m_learn_rate = learning_rate; m_act_fn = act_fn; m_epochs = num_epochs; m_num_inputs = data.Cols(); m_trained = false; if(!create()) return false; for(uint ep = 0; ep < m_epochs; ep++) { m_result = calculate(data); if(!backprop(data, targets,m_result)) return m_trained; } m_trained = true; return m_trained; } //+------------------------------------------------------------------+ //| Predict method | //+------------------------------------------------------------------+ matrix predict(matrix &data) { if(m_trained) return calculate(data); else return matrix::Zeros(0,0); } }; //+------------------------------------------------------------------+
Die FFNN-Klasse definiert ein mehrschichtiges Perzeptron (MLP), eine Art künstliches neuronales Netz, das für überwachte Lernaufgaben verwendet wird. Es enthält mehrere Eigenschaften wie z. B.:
- m_trained, ein boolesches Flag, das angibt, ob das Netz erfolgreich trainiert wurde;
- m_weights, ein Array von Matrizen, die die Gewichte zwischen den einzelnen Schichten speichern;
- m_outputs, ein Array von Matrizen, die die Ausgaben jeder versteckten Schicht enthalten;
- m_result, eine Matrix, die die endgültige Netzausgabe nach dem Training enthält;
- m_epochs, die Anzahl der Trainingsepochen (Iterationen);
- m_num_inputs, die Anzahl der Eingangsvariablen für das Netz;
- m_layers, die Gesamtzahl der Schichten des Netzes, einschließlich der Eingabe- und Ausgabeschichten;
- m_hidden_layers, die Anzahl der versteckten Schichten im Netz;
- m_hidden_layer_size, ein Array, das die Anzahl der Knoten in jeder versteckten Schicht definiert;
- m_learn_rate, die Lernrate, die für Gewichtsaktualisierungen während des Trainings verwendet wird;
- m_act_fn, die in den versteckten Schichten verwendete Aktivierungsfunktion.
Die Klasse enthält sowohl private als auch öffentliche Methoden. Private Methoden wie z.B.:
- create, die die Netzstruktur initialisiert, indem sie Speicher für Gewichtsmatrizen und die Ausgänge der verborgenen Schicht auf der Grundlage der angegebenen Konfiguration zuweist;
- calculate, die die Eingabedaten durch das Netz weiterleitet und dabei Gewichte und Aktivierungsfunktionen anwendet, um die Ausgabe zu berechnen;
- backprop, die den Backpropagation-Algorithmus implementiert und die Gewichte auf der Grundlage des Fehlers zwischen den vorhergesagten und tatsächlichen Ausgaben anpasst.
Zu den öffentlichen Methoden gehören:
- FFNN (Konstruktor), die das Netz mit der angegebenen Anzahl von Schichten und der Größe der verborgenen Schichten initialisiert;
- ~FFNN (Destruktor), die die für das Netz zugewiesenen Ressourcen freigibt;
- fit, die das Netz auf einem bestimmten Datensatz trainiert und die Gewichte durch Backpropagation über die angegebene Anzahl von Epochen anpasst;
- predict, die das trainierte Netz verwendet, um Vorhersagen für neue Eingabedaten zu generieren und somit eine Vorwärtspropagation durchzuführen.
In dem Skript implementiert die Klasse CMlfn die Schnittstelle IModel auf der Grundlage einer Instanz von FFNN. Es folgt eine kurze Erläuterung der Ausführung des Skripts in verschiedenen Konfigurationen.
//+------------------------------------------------------------------+ //| Ensemble_Demo.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #include<mlffnn.mqh> #include<ensemble.mqh> #include<np.mqh> //--- input parameters input int NumGoodModels=3; input int NumBiasedModels=7; input int NumBadModels=5; input int NumSamples=20; input int NumAttempts=1; input double VarParam=3.0;//variance parameter input bool TrainCombinedModelsOnCleanData = true; //+------------------------------------------------------------------+ //| Clean up dynamic array pointers | //+------------------------------------------------------------------+ void cleanup(IModel* &array[]) { for(uint i = 0; i<array.Size(); i++) if(CheckPointer(array[i])==POINTER_DYNAMIC) delete array[i]; } //+------------------------------------------------------------------+ //| IModel implementation of Multilayered iterative algo of GMDH | //+------------------------------------------------------------------+ class CMlfn:public IModel { private: FFNN *m_mlfn; double m_learningrate; ENUM_ACTIVATION_FUNCTION m_actfun; uint m_epochs; ulong m_layer[3]; public: CMlfn(); ~CMlfn(void); void setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs); bool train(matrix &predictors,matrix&targets); double forecast(vector &predictors); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMlfn::CMlfn(void) { m_learningrate=0.01; m_actfun=AF_SOFTMAX; m_epochs= 100; m_layer[0] = 2; m_layer[1] = 2; m_layer[2] = 1; m_mlfn = new FFNN(m_layer); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CMlfn::~CMlfn(void) { if(CheckPointer(m_mlfn) == POINTER_DYNAMIC) delete m_mlfn; } //+------------------------------------------------------------------+ //| Set other hyperparameters of the model | //+------------------------------------------------------------------+ void CMlfn::setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs) { m_learningrate=learning_rate; m_actfun=act_fn; m_epochs= num_epochs; } //+------------------------------------------------------------------+ //| Fit a model to the data | //+------------------------------------------------------------------+ bool CMlfn::train(matrix &predictors,matrix &targets) { return m_mlfn.fit(predictors,targets,m_learningrate,m_actfun,m_epochs); } //+------------------------------------------------------------------+ //| Make a prediction with the trained model | //+------------------------------------------------------------------+ double CMlfn::forecast(vector &predictors) { matrix preds(1,predictors.Size()); if(!preds.Row(predictors,0)) { Print(__FUNCTION__, " error inserting row ", GetLastError()); return EMPTY_VALUE; } matrix out = m_mlfn.predict(preds); return out[0][0]; } //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- if(NumSamples<1 || NumAttempts<1 || VarParam<0.0 || NumBadModels<1 || NumGoodModels<1 || NumBiasedModels<1) { Print(" Invalid User inputs "); return; } int ndone, divisor; double diff, std, temp; double computed_err_average ; double computed_err_unconstrained ; double computed_err_unbiased ; double computed_err_biased ; double computed_err_weighted ; double computed_err_bagged ; double computed_err_genreg ; CAvg average; CLinReg unconstrained; CUnbiased unbiased; Cbiased biased; CWeighted weighted; CGenReg genreg; vector computed_err_raw = vector::Zeros(NumBadModels+NumGoodModels+NumBiasedModels); std = sqrt(VarParam); divisor = 1; IModel* puremodels[]; matrix xgood[],xbad[],xbiased[],test[10]; if(ArrayResize(puremodels,NumBadModels+NumGoodModels+NumBiasedModels)<0 || ArrayResize(xgood,NumBadModels+NumGoodModels+NumBiasedModels)<0 || ArrayResize(xbad,NumBadModels+NumGoodModels+NumBiasedModels)<0 || ArrayResize(xbiased,NumBadModels+NumGoodModels+NumBiasedModels)<0) { Print(" failed puremodels array resize ", GetLastError()); return; } for(uint i = 0; i<puremodels.Size(); i++) puremodels[i] = new CMlfn(); for(uint i = 0; i<xgood.Size(); i++) xgood[i] = matrix::Zeros(NumSamples,3); for(uint i = 0; i<xbad.Size(); i++) xbad[i] = matrix::Zeros(NumSamples,3); for(uint i = 0; i<xbiased.Size(); i++) xbiased[i] = matrix::Zeros(NumSamples,3); for(uint i = 0; i<test.Size(); i++) test[i] = matrix::Zeros(NumSamples,3); computed_err_average = 0.0 ; computed_err_unconstrained = 0.0 ; computed_err_unbiased = 0.0 ; computed_err_biased = 0.0 ; computed_err_weighted = 0.0 ; computed_err_bagged = 0.0 ; computed_err_genreg = 0.0 ; vector t,v; matrix d; ndone = 1; for(uint i = 0; i<xgood.Size(); i++) { xgood[i].Random(0.0,1.0); if(!xgood[i].Col(sin(xgood[i].Col(0)) - pow(xgood[i].Col(1),2.0) + std*xgood[i].Col(2),2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); return; } } matrix xb(xgood[0].Rows(),1); for(uint i = 0; i<xbad.Size(); i++) { xbad[i] = xgood[0]; xb.Random(0.0,1.0); if(!xbad[i].Col(xb.Col(0),2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); return; } } for(uint i = 0; i<xbiased.Size(); i++) { xbiased[i] = xgood[0]; if(!xbiased[i].Col(xgood[0].Col(2)+1.0,2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); return; } } for(uint i = 0; i<test.Size(); i++) { test[i].Random(0.0,1.0); if(!test[i].Col(sin(test[i].Col(0)) - pow(test[i].Col(1),2.0) + std * test[i].Col(2),2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); return; } } for(uint imodel=0; imodel<puremodels.Size(); imodel++) { if(imodel < xgood.Size()) { t=xgood[imodel].Col(2); d=np::sliceMatrixCols(xgood[imodel],0,2); } else if(imodel >= xgood.Size() && imodel<(xgood.Size()+xbiased.Size())) { t=xbiased[imodel-xgood.Size()].Col(2); d=np::sliceMatrixCols(xbiased[imodel-xgood.Size()],0,2); } else { t=xbad[imodel - (xgood.Size()+xbiased.Size())].Col(2); d=np::sliceMatrixCols(xbad[imodel - (xgood.Size()+xbiased.Size())],0,2); } matrix tt(t.Size(),1); if(!tt.Col(t,0) || !puremodels[imodel].train(d,tt)) { Print(" failed column insertion ", GetLastError()); cleanup(puremodels); return; } temp = 0.0; for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = puremodels[imodel].forecast(v) - t[2]; temp += diff*diff; } } computed_err_raw[imodel] += temp/double(test.Size()*NumSamples); } //average matrix tdata; if(TrainCombinedModelsOnCleanData) tdata = xgood[0]; else { tdata = matrix::Zeros(NumSamples*3,3); if(!np::matrixCopyRows(tdata,xgood[0],0,NumSamples) || !np::matrixCopyRows(tdata,xbad[0],NumSamples,NumSamples*2) || !np::matrixCopyRows(tdata,xbiased[0],NumSamples*2)) { Print(" failed to create noisy dataset"); cleanup(puremodels); return; } } temp = 0.0; for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = average.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_average += temp/double(test.Size()*NumSamples); //unconstrained temp = 0.0; t = tdata.Col(2); d = np::sliceMatrixCols(tdata,0,2); if(!unconstrained.fit(d,t,puremodels)) { Print(" failed to fit unconstrained model "); cleanup(puremodels); } for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = unconstrained.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_unconstrained += temp/double(test.Size()*NumSamples); //unbiased temp = 0.0; t = tdata.Col(2); d = np::sliceMatrixCols(tdata,0,2); if(!unbiased.fit(d,t,puremodels)) { Print(" failed to fit unbiased model "); cleanup(puremodels); } for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = unbiased.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_unbiased += temp/double(test.Size()*NumSamples); //biased temp = 0.0; t = tdata.Col(2); d = np::sliceMatrixCols(tdata,0,2); if(!biased.fit(d,t,puremodels)) { Print(" failed to fit biased model "); cleanup(puremodels); } for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = biased.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_biased += temp/double(test.Size()*NumSamples); //weighted temp = 0.0; t = tdata.Col(2); d = np::sliceMatrixCols(tdata,0,2); if(!weighted.fit(d,t,puremodels)) { Print(" failed to fit weighted model "); cleanup(puremodels); } for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = weighted.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_weighted += temp/double(test.Size()*NumSamples); //gendreg temp = 0.0; t = tdata.Col(2); d = np::sliceMatrixCols(tdata,0,2); if(!genreg.fit(d,t,puremodels)) { Print(" failed to fit generalized regression model "); cleanup(puremodels); } for(uint i = 0; i<test.Size(); i++) { for(int j = 0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,0,2); diff = genreg.predict(v,puremodels) - t[2]; temp += diff*diff; } } computed_err_genreg += temp/double(test.Size()*NumSamples); temp = 0.0; PrintFormat("\n\n\nRandom DataSet%5d Raw errors:", ndone); for(uint imodel = 0; imodel<puremodels.Size() ; imodel++) { PrintFormat(" %.8lf", computed_err_raw[imodel] / ndone) ; temp += computed_err_raw[imodel] / ndone ; } PrintFormat("\n Mean raw error = %8.8lf", temp / double(puremodels.Size())) ; PrintFormat("\n Average error = %8.8lf", computed_err_average / ndone) ; PrintFormat("\n Unconstrained error = %8.8lf", computed_err_unconstrained / ndone) ; PrintFormat("\n Unbiased error = %8.8lf", computed_err_unbiased / ndone) ; PrintFormat("\n Biased error = %8.8lf", computed_err_biased / ndone) ; PrintFormat("\n Weighted error = %8.8lf", computed_err_weighted / ndone) ; PrintFormat("\n GenReg error = %8.8lf", computed_err_genreg / ndone) ; cleanup(puremodels); } //+------------------------------------------------------------------+
Wenn Sie das Skript mit den Standardeinstellungen ausführen, erhalten Sie die folgende Ausgabe.
MR 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) Random DataSet 1 Raw errors: KI 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.38602529 HP 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.36430552 CK 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.36703202 OS 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.51205057 EJ 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.57791798 HE 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.66825953 FL 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.65051234 QD 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.57403745 EO 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.71593174 PF 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.62444495 NQ 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.77552594 KI 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.75079339 MP 0 15:56:41.914 Ensemble_Demo (BTCUSD,D1) 0.78851743 CK 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) 0.52343272 OR 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) 0.70166082 EK 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) RE 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Mean raw error = 0.59869651 QL 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) DE 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Average error = 0.55224337 ML 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) QF 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Unconstrained error = 10.21673109 KL 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) RI 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Unbiased error = 0.55224337 GL 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) PH 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Biased error = 0.48431477 CL 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) HH 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) Weighted error = 0.51507522 OM 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) LK 0 15:56:41.915 Ensemble_Demo (BTCUSD,D1) GenReg error = 0.33761372 KM 0 15:57:11.108 Ensemble_Demo (BTCUSD,D1) GG 0 15:57:11.108 Ensemble_Demo (BTCUSD,D1) CQ 0 15:57:11.108 Ensemble_Demo (BTCUSD,D1)
Wenn alle Skriptparameter wie im vorherigen Durchlauf beibehalten werden, außer dass wir uns diesmal dafür entscheiden, das Konsensmodell auf verrauschten Daten zu trainieren. Wir haben das folgende Ergebnis beobachtet.
NL 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) Random DataSet 1 Raw errors: OS 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.72840629 GJ 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.63345953 PE 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.68442450 JL 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.91936106 OD 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.75230667 LO 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.88366446 PF 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.78226316 CQ 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.87140196 II 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.58672356 KP 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 1.09990815 MK 0 15:59:51.502 Ensemble_Demo (BTCUSD,D1) 0.92548778 OR 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) 1.03795716 GJ 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) 0.80684429 GE 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) 1.24041209 GL 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) 0.92169606 NF 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) CS 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Mean raw error = 0.85828778 RF 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) DS 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Average error = 0.83433599 FF 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) FP 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Unconstrained error = 23416285121251567120416768.00000000 DS 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) JR 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Unbiased error = 0.83433599 HS 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) PP 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Biased error = 0.74321307 LD 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) GQ 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) Weighted error = 0.83213118 PD 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) FR 0 15:59:51.503 Ensemble_Demo (BTCUSD,D1) GenReg error = 0.78697882
Die wichtigsten Erkenntnisse aus diesen Ergebnissen sind, dass Ensemble-Methoden im Allgemeinen besser abschneiden als einzelne Modelle, da die Kombination mehrerer Modelle in der Regel bessere Ergebnisse liefert als ein einzelnes Modell. Es gibt jedoch keine allgemeingültige beste Methode, denn jede hat ihre Stärken und Schwächen, und die optimale Wahl hängt von dem jeweiligen Datensatz und dem jeweiligen Problem ab.
Unbeschränkte Regression ist zwar potenziell leistungsstark, aber sehr anfällig für Überanpassung, insbesondere bei verrauschten oder kleinen Datensätzen. Andererseits zeichnet sich GRNN bei der Verarbeitung kleiner, verrauschter Datensätze durch eine wirksame Glättung der Daten aus, auch wenn es bei größeren, sauberen Datensätzen etwas an Anpassungsfähigkeit einbüßt.
Lineare Regressionsmethoden können ebenfalls effektiv sein, aber es besteht die Gefahr der Überanpassung, insbesondere bei verrauschten oder kleinen Datensätzen. Einfache Mittelwertbildung und Varianzgewichtung sind im Allgemeinen robust und können eine gute Wahl sein, wenn der Datensatz verrauscht ist oder die optimale Methode unsicher ist. Zusammenfassend lässt sich sagen, dass die Wahl der Ensemble-Methode auf der Grundlage der spezifischen Merkmale des Datensatzes sorgfältig abgewogen werden sollte, und dass es oft von Vorteil ist, mit verschiedenen Methoden zu experimentieren und ihre Leistung anhand eines Validierungssatzes zu bewerten, um eine fundierte Entscheidung zu treffen. Der gesamte Code, auf den im Text verwiesen wird, ist beigefügt.
Datei Name | Beschreibung |
---|---|
MQL5/include/mlffnn.mqh | Enthält die Definition der Klasse FFNN, die ein einfaches mehrschichtiges Peceptron implementiert |
MQL5/include/grnn.mqh | Definiert die Klasse CGrnn, die ein verallgemeinertes neuronales Regressionsnetz implementiert, das simuliertes Glühen verwendet |
MQL5/include/OLS.mqh | Definiert die OLS-Klasse, die die gewöhnliche kleinste Quadrate-Regression kapselt |
MQL5/include/ensemble.mqh | Enthält die Definition von sechs Ensemble-Methoden, die als die Klassen CAvg, CLinReg, Cbiased, CUnbiased, CWeighted und CGenReg implementiert sind |
MQL5/include/np.mqh | Enthält Variationsmatrix und vektorielle Nutzenfunktionen |
MQL5/scripts/Ensemble_Demo.mq5 | Dieses Skript demonstriert die in ensemble.mqh definierten Ensemble-Klassen |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/16630





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.