English Русский 中文 Español 日本語 Português
preview
Neuronale Netze leicht gemacht (Teil 15): Datenclustering mit MQL5

Neuronale Netze leicht gemacht (Teil 15): Datenclustering mit MQL5

MetaTrader 5Handelssysteme | 25 Juli 2022, 10:37
246 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Inhaltsverzeichnis

Einführung

Im vorangegangenen Artikel haben wir uns mit der k-means-Clustering-Methode befasst und ihre Implementierung in der Sprache Python untersucht. Der Einsatz der Integration ist jedoch oft mit gewissen Einschränkungen und zusätzlichem Aufwand verbunden. Insbesondere erlaubt der derzeitige Stand der Integration nicht die Verwendung von Daten integrierter Anwendungen wie Indikatoren oder Terminal-Ereignisverarbeitung. Viele klassische Indikatoren sind in verschiedenen Bibliotheken implementiert, aber wenn wir über nutzerdefinierte Indikatoren sprechen, müssen wir ihre Algorithmen in unseren Skripten reproduzieren. Was ist zu tun, wenn es keinen Quellcode des Indikators gibt und wir den Algorithmus seiner Wirkung nicht verstehen? Oder wenn Sie die Clustering-Ergebnisse in anderen MQL5-Programmen verwenden wollen? In solchen Fällen können wir von der Implementierung der Clustering-Methode mit MQL5-Tools profitieren.

1. Grundsätze der Modellbildung

Wir haben bereits die k-means Clustermethode betrachtet, die wie folgt implementiert wird:

  1. Bestimmen von k zufälligen Punkten aus der Trainingsstichprobe als Cluster-Zentren.
  2. Erstelle eine Schleife von Operationen:
    • Bestimme den Abstand von jedem Punkt zu jedem Zentrum.
    • Finde das nächstgelegene Zentrum und weise dieses Clusters einen Punkt zu.
    • Bestimme anhand des arithmetischen Mittels ein neues Zentrum für jedes Cluster.
  3. Wiederhole die Vorgänge in einer Schleife, bis die Cluster-Zentren „stehen bleiben“.

Bevor wir mit dem Schreiben des Methodencodes fortfahren, lassen Sie uns kurz die wichtigsten Punkte unserer Implementierung besprechen.

Die wichtigsten Operationen des zuvor betrachteten Algorithmus sind in einer Schleife implementiert. Zu Beginn des Schleifenkörpers müssen wir den Abstand zwischen jedem Element der Trainingsstichprobe und dem Zentrum jedes Clusters ermitteln. Dieser Vorgang ist für jedes Element der Trainingsstichprobe absolut unabhängig von anderen Elementen. Daher können wir die OpenCL-Technologie zur Implementierung paralleler Berechnungen verwenden. Außerdem sind die Operationen zur Berechnung des Abstands zu den Zentren der verschiedenen Cluster unabhängig voneinander. Daher können wir Operationen in einem zweidimensionalen Aufgabenraum parallelisieren.

Im nächsten Schritt müssen wir feststellen, ob ein Element der Sequenz zu einem bestimmten Cluster gehört. Dieser Vorgang impliziert auch die Unabhängigkeit der Berechnungen für jedes Element der Sequenz. Hier können wir die OpenCL-Technologie für paralleles Rechnen auf einzelne Elemente des Trainingssatzes anwenden.

Am Ende des Schleifenkörpers definieren wir neue Zentren für Cluster. Dazu müssen wir alle Elemente der Trainingsstichprobe in einer Schleife durchlaufen und die arithmetischen Mittelwerte im Zusammenhang mit jedem Element des Vektors, der den Zustand des Systems und jedes Clusters beschreibt, berechnen. Bitte beachten Sie, dass nur die Elemente, die zu diesem Cluster gehören, zur Berechnung des Clusterzentrums verwendet werden. Andere Elemente werden ignoriert. Daher werden die Werte eines jeden Elements nur einmal verwendet. In diesem Fall ist es auch möglich, parallele Rechentechniken in einem zweidimensionalen Raum zu verwenden. Auf einer Achse befinden sich Vektorelemente, die den Zustand des Systems beschreiben, und auf der zweiten Achse die analysierten Cluster.

Nachdem das Clustering der Daten durchgeführt wurde, müssen wir die Verlustfunktion berechnen, um die Leistung des Modells zu bewerten. Wie bereits erwähnt, kann dies durch Berechnung der arithmetischen mittleren Abweichung des Systemzustands vom Zentrum des entsprechenden Clusters erfolgen. Es ist natürlich nicht möglich, die Berechnung des arithmetischen Mittels explizit in Threads zu unterteilen. Diese Aufgabe kann jedoch in zwei Teilaufgaben unterteilt werden. Zunächst berechnen wir die Entfernung zu den jeweiligen Zentren. Diese Aufgabe kann im Rahmen eines einzigen Systemzustands leicht parallelisiert werden. Danach können wir das arithmetische Mittel des resultierenden Abstandsvektors berechnen.

2. Erstellen eines OpenCL-Programms

Wir haben also vier separate Teilaufgaben für die Organisation des parallelen Rechnens. Wie bereits in früheren Artikeln beschrieben, sollten wir für die Implementierung der parallelen Datenverarbeitung mit OpenCL ein separates Programm erstellen, das diese Operation auf der OpenCL-Kontextseite herunterlädt und ausführt. Die ausführbaren Programmkerne werden in einer separaten Datei mit dem Titel „unsupervised.cl“ in der Reihenfolge der oben genannten Aufgaben erstellt.

Beginnen wir mit dieser Arbeit, indem wir den Kernel KmeansCulcDistance schreiben, in dem wir Operationen im Zusammenhang mit der Berechnung der Abstände von Systemzuständen zu den aktuellen Zentren aller Cluster implementieren werden. Dieser Kernel wird in einem zweidimensionalen Aufgabenraum ausgeführt. Eine Dimension wird für die einzelnen Zustände des Systems aus der Trainingsstichprobe verwendet. Die zweite, für die Cluster unseres Modells.

In den Kernel-Eingabeparametern geben wir Zeiger auf drei Datenpuffer und die Größe des Vektors an, der einen Zustand des analysierten Systems beschreibt. Zwei der angegebenen Puffer werden die Quelldaten enthalten. Dies ist die Trainingsstichprobe und die Matrix der Vektoren auf die Clustermittelpunkte. Der dritte Datenpuffer ist der Ergebnis-Tensor.

Im Kernelkörper erhalten wir die Kennungen des aktuellen Operationsthreads in beiden Dimensionen und die Gesamtzahl der Cluster durch die Anzahl der laufenden Threads in der zweiten Dimension. Wir benötigen diese Daten, um den Versatz zu den gewünschten Elementen in allen genannten Tensoren zu bestimmen. Hier bestimmen wir auch die Offsets in den Quelldatentensoren und initialisieren die Variable auf Null, um den Abstand zum Clusterzentrum zu berechnen.

Als Nächstes implementieren wir eine Schleife, deren Anzahl der Iterationen der Größe des Vektors entspricht, der einen Zustand unseres Systems beschreibt. Im Hauptteil dieser Schleife fassen wir die quadrierten Abstände zwischen den Werten der entsprechenden Elemente der Systemzustandsvektoren und dem Clusterzentrum zusammen.

Nach allen Schleifeniterationen müssen wir nur noch den empfangenen Wert in das entsprechende Element des Ergebnispuffers speichern. Um den Abstand zwischen zwei Punkten im Raum zu bestimmen, muss man aus mathematischer Sicht die Quadratwurzel aus dem resultierenden Wert ziehen. Aber in diesem Fall sind wir nicht an der genauen Entfernung zwischen den beiden Punkten interessiert. Wir müssen nur die kleinsten Abstände finden. Um Ressourcen zu sparen, werden wir daher nicht die Quadratwurzel ziehen.

__kernel void KmeansCulcDistance(__global double *data,
                                 __global double *means,
                                 __global double *distance,
                                 int vector_size
                                )
  {
   int m = get_global_id(0);
   int k = get_global_id(1);
   int total_k = get_global_size(1);
   double sum = 0.0;
   int shift_m = m * vector_size;
   int shift_k = k * vector_size;
   for(int i = 0; i < vector_size; i++)
      sum += pow(data[shift_m + i] - means[shift_k + i], 2);
   distance[m * total_k + k] = sum;
  }

Der Code des ersten Kernels ist fertig, und wir können mit der Arbeit am nächsten Unterprozess fortfahren. Gemäß dem Algorithmus unserer Methode müssen wir im nächsten Schritt bestimmen, zu welchem der Cluster jeder Zustand aus der Trainingsstichprobe gehört. Zu diesem Zweck wird ermittelt, welches der Clusterzentren näher am analysierten Zustand liegt. Wir haben die Entfernungen bereits im vorherigen Kernel berechnet. Jetzt müssen wir nur noch die Zahl mit dem niedrigsten Wert ermitteln. Alle Operationen werden im Zusammenhang mit einem einzigen Zustand des Systems durchgeführt.

Um diesen Prozess zu implementieren, erstellen wir einen Kernel KmeansClustering. Wie der vorherige Kernel erhält auch dieser über Parameter Zeiger auf drei Datenpuffer und die Gesamtzahl der Cluster. So seltsam es klingen mag, aber nur einer der drei verfügbaren Puffer - distance - enthält die Originaldaten. Die beiden anderen Puffer enthalten die Ergebnisse der Operationen. In den Cluster-Puffer wird der Index des Clusters geschrieben, zu dem der analysierte Systemzustand gehört.

Der dritte Puffer - flags - wird verwendet, um das Cluster Change Flag im Vergleich zum vorherigen Zustand zu schreiben. Durch die Analyse dieser Flags können wir den Haltepunkt des Modelltrainings definieren. Die Logik hinter diesem Prozess ist ganz einfach. Wenn kein Zustand des Systems seinen Cluster ändert, dann ändern sich folglich auch die Zentren der Cluster nicht. Dies bedeutet, dass eine weitere Fortsetzung der Operationsschleife keinen Sinn macht. Dort wird das Modelltraining beendet.

Kehren wir nun zu unserem Kernel-Algorithmus zurück. Wir werden sie in einem eindimensionalen Aufgabenraum im Zusammenhang mit den analysierten Systemzuständen starten. Im Kernelkörper definieren wir die Ordnungszahl des analysierten Zustands und die entsprechende Verschiebung in den Datenpuffern. Jeder der beiden Ergebnispuffer enthält einen Wert für jeden Zustand. Daher wird die Verschiebung in den angegebenen Puffern gleich der Thread-ID sein. Daher müssen wir nur die Verschiebung im Quelldatenpuffer bestimmen, der die berechneten Abstände zu den Clusterzentren enthält.

Hier werden wir zwei private Variablen vorbereiten. In value schreiben wir den Abstand zum Zentrum. Die Clusternummer wird in die zweite Variable - result- geschrieben. In der Anfangsphase speichern sie die Werte des Clusters mit dem Index „0“.

Dann werden wir in einer Schleife die Entfernungen zu allen Clusterzentren durchlaufen. Da wir den Wert des Clusters „0“ bereits in den Variablen gespeichert haben, lassen Sie uns mit dem nächsten Cluster beginnen.

Im Schleifenkörper überprüfen wir den Abstand zum nächsten Mittelpunkt. Ist er größer oder gleich dem bereits in der Variablen gespeicherten Wert, wird der nächste Cluster geprüft.

Wenn ein näherer Mittelpunkt gefunden wird, überschreiben wir die Werte der privaten Variablen. Darin speichern wir eine kürzere Strecke und die Seriennummer des entsprechenden Clusters.

Nach Abschluss aller Schleifeniterationen wird in der Ergebnisvariablen die Kennung des Clusters gespeichert, das dem analysierten Zustand am nächsten liegt. Es wird auf den aktuellen Stand verwiesen. Bevor jedoch der empfangene Wert im entsprechenden Element des Ergebnispuffers gespeichert wird, muss geprüft werden, ob sich die Clusternummer im Vergleich zur vorherigen Iteration geändert hat. Das Ergebnis des Vergleichs wird im Flag-Puffer gespeichert.

__kernel void KmeansClustering(__global double *distance,
                               __global double *clusters,
                               __global double *flags,
                               int total_k
                              )
  {
   int i = get_global_id(0);
   int shift = i * total_k;
   double value = distance[shift];
   int result = 0;
   for(int k = 1; k < total_k; k++)
     {
      if(value <= distance[shift + k])
         continue;
      value =  distance[shift + k];
      result = k;
     }
   flags[i] = (double)(clusters[i] != (double)result);
   clusters[i] = (double)result;
  }

Am Ende des Clustering-Algorithmus müssen wir die Werte der zentralen Vektoren aller Cluster aktualisieren, die in der Mittelwertmatrix zusammengefasst sind. Um diese Aufgabe zu erfüllen, werden wir einen weiteren Kernel KmeansUpdating erstellen. Wie die oben besprochenen Kernel erhält auch der in den Parametern betrachtete Kernel Zeiger auf drei Datenpuffer und eine Konstante. Zwei Puffer enthalten die Originaldaten und ein Puffer enthält die Ergebnisse. Wie bereits erwähnt, werden wir diesen Kernel in einem zweidimensionalen Aufgabenraum ausführen. Aber anders als der Kernel KmeansCulcDistance werden wir in der ersten Dimension des Aufgabenraums über die Elemente des Vektors iterieren, der einen Systemzustand beschreibt, während wir in der Konstante total_m die Anzahl der Elemente in der Trainingsmenge angeben werden.

Im Kernelkörper werden wir zunächst die Thread-IDs in beiden Dimensionen definieren. Wie zuvor werden wir sie verwenden, um geparste Elemente und Offsets in Datenpuffern zu bestimmen. Hier wird die Länge des Vektors bestimmt, der einen Systemzustand beschreibt, der der Gesamtzahl der laufenden Threads in der ersten Dimension entspricht. Darüber hinaus initialisieren wir zwei private Variablen, in denen wir die Werte der relevanten Elemente der Systemzustandsbeschreibung und deren Anzahl zusammenfassen werden.

Die Summierungsoperationen werden in der Schleife implementiert, die wir erstellen werden; die Anzahl der Iterationen entspricht der Anzahl der Elemente in der Trainingsstichprobe. Vergessen wir nicht, dass wir nur die Elemente zusammenfassen, die zu dem analysierten Cluster gehören. Im Schleifenkörper wird zunächst geprüft, zu welchem Cluster das aktuelle Element gehört. Wenn es nicht mit dem analysierten Element übereinstimmt, gehen wir zum nächsten Element über.

Wenn das Element die Validierung besteht, d.h. zu dem analysierten Cluster gehört, wird der Wert des entsprechenden Elements des Systemzustandsbeschreibungsvektors hinzugefügt und der Zähler um 1 erhöht.

Nach Verlassen der Schleife müssen wir nur noch die akkumulierte Summe durch die Anzahl der summierten Elemente dividieren. Es ist jedoch zu beachten, dass ein kritischer Fehler auftreten kann: die Division durch Null. Angesichts der Organisation des Algorithmus ist eine solche Situation natürlich unwahrscheinlich. Um jedoch die Zuverlässigkeit des Programms zu gewährleisten, werden wir diese Prüfung hinzufügen. Achten Sie darauf, dass, wenn keine Elemente gefunden werden, die zum Cluster gehören, wir den Wert nicht anpassen, sondern ihn unverändert lassen.

__kernel void KmeansUpdating(__global double *data,
                             __global double *clusters,
                             __global double *means,
                             int total_m
                            )
  {
   int i = get_global_id(0);
   int vector_size = get_global_size(0);
   int k = get_global_id(1);
   double sum = 0;
   int count = 0;
   for(int m = 0; m < total_m; m++)
     {
      if(clusters[m] != k)
         continue;
      sum += data[m * vector_size + i];
      count++;
     }
   if(count > 0)
      means[k * vector_size + i] = sum / count;
  }

In diesem Stadium haben wir drei Kernel erstellt, um den k-means-Algorithmus zum Datenclustering zu implementieren. Bevor wir jedoch mit der Erstellung der Objekte des Hauptprogramms fortfahren, müssen wir einen weiteren Kernel für die Berechnung der Verlustfunktion erstellen.

Der Wert der Verlustfunktion wird in zwei Stufen ermittelt. Zunächst wird die Abweichung jedes einzelnen Elements der Trainingsstichprobe vom Zentrum des entsprechenden Clusters ermittelt. Dann berechnen wir die arithmetische mittlere Abweichung für die gesamte Stichprobe. Die Operationen der ersten Stufe können in Threads aufgeteilt werden, um mit OpenCL-Tools parallele Berechnungen durchzuführen. Um diese Funktionalität zu implementieren, erstellen wir den KmeansLoss-Kernel, der als Parameter Zeiger auf vier Puffer und eine Konstante erhält. Drei Puffer enthalten die Quelldaten und ein Puffer wird für die Ergebnisse verwendet.

Wir starten den Kernel in einem eindimensionalen Aufgabenraum, wobei die Anzahl der Threads gleich der Anzahl der Elemente in der Trainingsmenge ist. Im Kernelkörper wird zunächst die Ordnungszahl des analysierten Musters aus der Trainingsmenge bestimmt. Dann bestimmen wir, zu welchem Cluster er gehört. Diesmal werden wir die Abstände zu den Zentren aller Cluster nicht neu berechnen. Stattdessen wird einfach der entsprechende Wert aus dem Clusterpuffer entsprechend der Ordnungszahl des Elements abgerufen. In dem Kernel KmeansClustering, den wir zuvor besprochen haben, wurde die Ordnungszahl des Clusters in diesem Puffer gespeichert.

Nun können wir den Versatz zum Anfang der benötigten Vektoren in den Tensoren der Trainingsstichprobe und der Matrix der Clusterzentren bestimmen.

Nun müssen wir nur noch den Abstand zwischen den beiden Vektoren berechnen. Zu diesem Zweck initialisieren wir eine private Variable, um die Summe der Abweichungen zu akkumulieren, und erstellen eine Schleife über alle Elemente des Vektors, der einen Systemzustand beschreibt. Im Schleifenkörper werden die quadrierten Abweichungen der entsprechenden Elemente der Vektoren summiert.

Nach allen Iterationen der Schleife wird die akkumulierte Summe in das entsprechende Element des Ergebnispuffers des Verlust-Ergebnispuffers verschoben.

__kernel void KmeansLoss(__global double *data,
                         __global double *clusters,
                         __global double *means,
                         __global double *loss,
                         int vector_size
                        )
  {
   int m = get_global_id(0);
   int c = clusters[m];
   int shift_c = c * vector_size;
   int shift_m = m * vector_size;
   double sum = 0;
   for(int i = 0; i < vector_size; i++)
      sum += pow(data[shift_m + i] - means[shift_c + i], 2);
   loss[m] = sum;
  }

Wir haben die Algorithmen für die Konstruktion aller Prozesse auf der OpenCL-Kontextseite betrachtet. Nun können wir zur Organisation der Prozesse auf der Seite des Hauptprogramms übergehen.

3. Vorbereitende Arbeiten für das Hauptprogramm

Auf der Seite des Hauptprogramms werden wir eine neue Klasse CKmeans erstellen. Der Klassencode wird in der Datei kmeans.mqh gespeichert. Bevor wir jedoch direkt zur neuen Klasse übergehen, müssen wir einige vorbereitende Arbeiten durchführen. Um Daten an den OpenCL-Kontext zu übertragen, verwenden wir zunächst die Klasse object, die wir bereits in dieser Artikelserie besprochen haben: CBufferDouble. Wir werden den Code der angegebenen Klasse nicht neu schreiben, sondern einfach die zuvor erstellte Bibliothek einbinden.

#include "..\NeuroNet_DNG\NeuroNet.mqh"

Binden wir jetzt den Code des oben erstellten OpenCL-Programms als Ressource ein.

#resource "unsupervised.cl" as string cl_unsupervised

Als Nächstes erstellen wir benannte Konstanten. Dieses Mal benötigen wir eine Reihe solcher Konstanten. Um die künftige Kompatibilität und Verwendung mit der zuvor erstellten Bibliothek zu gewährleisten, sollten wir sicherstellen, dass die erstellten Konstanten eindeutig sind.

Zunächst benötigen wir eine Konstante, um die neue Klasse zu identifizieren.

#define defUnsupervisedKmeans    0x7901

Zweitens brauchen wir Konstanten, um Kernel und ihre Parameter zu identifizieren. Kernel werden durch eine fortlaufende Nummerierung innerhalb eines OpenCL-Programms identifiziert. Allerdings werden die Parameter innerhalb eines einzelnen Kerns nummeriert. Um die Lesbarkeit des Codes zu verbessern, habe ich beschlossen, die Konstanten nach den Kernels zu gruppieren, zu denen sie gehören.

#define def_k_kmeans_distance    0
#define def_k_kmd_data           0
#define def_k_kmd_means          1
#define def_k_kmd_distance       2
#define def_k_kmd_vector_size    3

#define def_k_kmeans_clustering  1
#define def_k_kmc_distance       0
#define def_k_kmc_clusters       1
#define def_k_kmc_flags          2
#define def_k_kmc_total_k        3

#define def_k_kmeans_updates     2
#define def_k_kmu_data           0
#define def_k_kmu_clusters       1
#define def_k_kmu_means          2
#define def_k_kmu_total_m        3

#define def_k_kmeans_loss        3
#define def_k_kml_data           0
#define def_k_kml_clusters       1
#define def_k_kml_means          2
#define def_k_kml_loss           3
#define def_k_kml_vector_size    4

Nachdem wir die benannten Konstanten erstellt haben, gehen wir zum nächsten Schritt der vorbereitenden Arbeiten über. Als wir die Implementierung von Multithreading-Berechnung in überwachten Lernmodellen besprachen, initialisierten wir ein Objekt für die Arbeit mit dem OpenCL-Kontext im Konstruktor der Dispatch-Klasse des neuronalen Netzwerks. In diesem Artikel verwenden wir die Clusterklasse CKmeans ohne weitere Modelle. Nun, wir könnten die Initialisierungsfunktion der COpenCLMy Objektinstanz in unsere neue Klasse CKmeans verschieben. Das Clustering könnte jedoch eines Tages als Teil anderer, komplexerer Modelle verwendet werden. Dies würde den Rahmen dieses Artikels sprengen, aber wir werden in weiteren Artikeln dieser Reihe darauf zurückkommen. Auf jeden Fall sollten wir diese Möglichkeit vorsehen. Daher habe ich beschlossen, eine separate Funktion zu erstellen, um eine Instanz der Objektklasse COpenCLMy zu initialisieren.

Schauen Sie sich den Algorithmus der Funktion OpenCLCreate an. Er ist so aufgebaut, dass er den Test des OpenCL-Programms als Parameter erhält und einen Zeiger auf eine Instanz eines initialisierten Objekts zurückgibt. Im Hauptteil der Funktion wird zunächst eine neue Instanz der Klasse COpenCLMy erstellt. Wir überprüfen sofort das Ergebnis des Vorgangs zur Erstellung eines neuen Objekts.

COpenCLMy *OpenCLCreate(string programm)
  {
   COpenCL *result = new COpenCLMy();
   if(CheckPointer(result) == POINTER_INVALID)
      return NULL;

Dann rufen wir die neue Objektinitialisierungsmethode auf und übergeben ihr eine String-Variable mit dem OpenCL-Programmtext in den Parametern. Erneut überprüfen wir das Ergebnis der Operation. Wenn die Operation zu einem Fehler führt, löschen wir das oben erstellte Objekt und beenden die Methode, wobei wir einen leeren Zeiger zurückgeben.

   if(!result.Initialize(programm, true))
     {
      delete result;
      return NULL;
     }

Nach erfolgreicher Initialisierung des Programms fahren wir mit der Erstellung von Kerneln im Kontext von OpenCL fort. Zunächst geben wir die Anzahl der zu erstellenden Kernel an und erstellen dann alle zuvor beschriebenen Kernel nacheinander. Vergessen wir nicht, den Prozess zu kontrollieren und das Ergebnis jedes Vorgangs zu überprüfen.

Der folgende Code zeigt ein Beispiel für die Initialisierung nur eines Kernels. Die Übrigen werden auf die gleiche Weise initialisiert. Der vollständige Code aller Methoden und Funktionen findet sich im Anhang.

   if(!result.SetKernelsCount(4))
     {
      delete result;
      return NULL;
     }
//---
   if(!result.KernelCreate(def_k_kmeans_distance, "KmeansCulcDistance"))
     {
      delete result;
      return NULL;
     }
//---
...........
//---
   return result;
  }

Nachdem alle Kernel erfolgreich erstellt wurden, beenden Sie die Methode, indem Sie einen Zeiger auf die erstellte Objektinstanz zurückgeben.

Damit sind die vorbereitenden Arbeiten abgeschlossen, und wir können direkt mit der Arbeit an einer neuen Datenclusterklasse fortfahren.


4. Konstruktion einer Organisationsklasse für den k-means-Algorithmus

Beginnen wir mit der Arbeit an der neuen Datenclusterklasse CKmeans und diskutieren wir ihren Inhalt. Welche Funktionen sollte sie haben? Welche Methoden und Variablen benötigen wir, um diese Funktion auszuführen? Alle Variablen werden in dem geschützten Block implementiert.

Für die Speicherung von Modell-Hyperparametern werden separate Variablen benötigt: die Anzahl der zu erstellenden Cluster (m_iClusters) und die Größe des Beschreibungsvektors eines einzelnen Systemzustands (m_iVectorSize).

Um die Qualität des trainierten Modells zu bewerten, wird die Verlustfunktion berechnet, deren Wert in der Variablen m_dLoss gespeichert wird.

Um den Zustand des Modells (trainiert oder nicht) zu verstehen, benötigen wir außerdem das Flag m_bTrained.

Ich denke, diese Liste reicht aus, um die gewünschte Funktionalität zu implementieren. Als Nächstes werden die verwendeten Objekte deklariert. Hier deklarieren wir eine Instanz der Klasse, um mit dem OpenCL-Kontext (c_OpenCL) zu arbeiten. Wir brauchen auch Datenpuffer, um Informationen zu speichern und mit dem OpenCL-Kontext auszutauschen. Wir werden ihre Namen mit denen übereinstimmen, die wir bei der Entwicklung des OpenCL-Programms verwendet haben:

  • c_aDistance;
  • c_aMeans;
  • c_aClasters;
  • c_aFlags;
  • c_aLoss.

Nachdem wir die Variablen deklariert haben, gehen wir zu den Klassenmethoden über. Wir werden hier nichts verstecken, und daher werden alle Methoden öffentlich sein.

Natürlich beginnen wir mit dem Konstruktor und dem Destruktor der Klasse. Im Konstruktor erstellen wir Instanzen der von uns verwendeten Objekte und setzen die anfänglichen Variablenwerte.

void CKmeans::CKmeans(void)   :  m_iClusters(2),
                                 m_iVectorSize(1),
                                 m_dLoss(-1),
                                 m_bTrained(false)
  {
   c_aMeans = new CBufferDouble();
   if(CheckPointer(c_aMeans) != POINTER_INVALID)
      c_aMeans.BufferInit(m_iClusters * m_iVectorSize, 0);
   c_OpenCL = NULL;
  }

Im Destruktor der Klasse wird der Speicher geleert und alle in der Klasse erstellten Objekte gelöscht.

void CKmeans::~CKmeans(void)
  {
   if(CheckPointer(c_aMeans) == POINTER_DYNAMIC)
      delete c_aMeans;
   if(CheckPointer(c_aDistance) == POINTER_DYNAMIC)
      delete c_aDistance;
   if(CheckPointer(c_aClasters) == POINTER_DYNAMIC)
      delete c_aClasters;
   if(CheckPointer(c_aFlags) == POINTER_DYNAMIC)
      delete c_aFlags;
   if(CheckPointer(c_aLoss) == POINTER_DYNAMIC)
      delete c_aLoss;
  }

Als Nächstes erstellen wir unsere Klasseninitialisierungsmethode, der wir als Parameter einen Zeiger auf das Objekt der Operationen mit dem OpenCL-Kontext und den Modell-Hyperparametern übergeben. Im Methodenrumpf erstellen wir zunächst einen kleinen Block von Steuerelementen, in dem wir die in den Parametern empfangenen Daten überprüfen.

Danach speichern wir die erhaltenen Hyperparameter in den entsprechenden Variablen und initialisieren den Puffer der Matrix der mittleren Clustervektoren mit Nullwerten. Vergessen wir nicht, das Ergebnis der Pufferinitialisierungsoperationen zu überprüfen.

bool CKmeans::Init(COpenCLMy *context, int clusters, int vector_size)
  {
   if(CheckPointer(context) == POINTER_INVALID || clusters < 2 || vector_size < 1)
      return false;
//---
   c_OpenCL = context;
   m_iClusters = clusters;
   m_iVectorSize = vector_size;
   if(CheckPointer(c_aMeans) == POINTER_INVALID)
     {
      c_aMeans = new CBufferDouble();
      if(CheckPointer(c_aMeans) == POINTER_INVALID)
         return false;
     }
   c_aMeans.BufferFree();
   if(!c_aMeans.BufferInit(m_iClusters * m_iVectorSize, 0))
      return false;
   m_bTrained = false;
   m_dLoss = -1;
//---
   return true;
  }

Nach der Initialisierung müssen wir das Modell trainieren. Wir implementieren diese Funktionalität in der Methode Study. In den Methodenparametern übergeben wir die Trainingsstichprobe und das Initialisierungsflag der Matrix der Clusterzentren. Durch die Verwendung des Flags wird die Möglichkeit geschaffen, die Matrixinitialisierung zu deaktivieren, wenn das Training eines vollständig oder teilweise trainierten Modells, das aus einer Datei geladen wurde, fortgesetzt wird.

Der Block von Steuerelementen ist im Körper der Methode implementiert. Überprüfen wir zunächst die Gültigkeit der Objektzeiger, die wir in den Parametern des Trainingsbeispiels und des OpenCL-Kontexts erhalten haben.

Überprüfen wir dann die Verfügbarkeit von Daten in der Ausbildungsstichprobe. Außerdem stellen wir sicher, dass ihre Anzahl ein Vielfaches der Größe des Beschreibungsvektors eines einzelnen Systemzustands ist, der bei der Initialisierung angegeben wurde.

Weiters ist zu prüfen, ob die Anzahl der Elemente in der Trainingsstichprobe mindestens 10-mal größer ist als die Anzahl der Cluster.

bool CKmeans::Study(CBufferDouble *data, bool init_means = true)
  {
   if(CheckPointer(data) == POINTER_INVALID || CheckPointer(c_OpenCL) == POINTER_INVALID)
      return false;
//---
   int total = data.Total();
   if(total <= 0 || m_iClusters < 2 || (total % m_iVectorSize) != 0)
      return false;
//---
   int rows = total / m_iVectorSize;
   if(rows <= (10 * m_iClusters))
      return false;

Der nächste Schritt besteht darin, die Matrix der Clusterzentren zu initialisieren. Bevor wir die Matrix initialisieren, überprüfen wir natürlich den Status des Initialisierungsflags, das wir in den Methodenparametern erhalten haben.

Die Matrix wird mit zufällig aus der Trainingsstichprobe ausgewählten Vektoren initialisiert. Hier müssen wir einen Algorithmus entwickeln, der verhindert, dass mehrere Cluster mit demselben Systemzustand initialisiert werden. Zu diesem Zweck wird ein Array von Flags erstellt, dessen Anzahl der Anzahl der Systemzustände im Trainingssatz entspricht. In der Anfangsphase initialisieren wir dieses Array mit falschen Werten. Als Nächstes implementieren wir eine Schleife, deren Anzahl der Iterationen der Anzahl der Cluster im Modell entspricht. Im Schleifenkörper wird nach dem Zufallsprinzip eine Zahl innerhalb der Größe der Trainingsstichprobe erzeugt und das Flag am erhaltenen Index überprüft. Wenn dieser Systemzustand bereits einen Cluster initialisiert hat, werden wir die Iterationszählerstände dekrementieren und mit der nächsten Iteration der Schleife fortfahren.

Wenn das ausgewählte Element noch nicht an der Cluster-Initialisierung teilgenommen hat, bestimmen wir den Offset in der Trainingsstichprobe zum Beginn des gegebenen Systemzustands in der Trainingsstichprobe und die Matrix der zentralen Vektoren. Danach implementieren wir eine verschachtelte Schleife zum Kopieren von Daten. Bevor wir zur nächsten Iteration der Schleife übergehen, ändern wir das Flag mit dem verarbeiteten Index.

   bool flags[];
   if(ArrayResize(flags, rows) <= 0 || !ArrayInitialize(flags, false))
      return false;
//---
   for(int i = 0; (i < m_iClusters && init_means); i++)
     {
      Comment(StringFormat("Cluster initialization %d of %d", i, m_iClusters));
      int row = (int)((double)MathRand() * MathRand() / MathPow(32767, 2) * (rows - 1));
      if(flags[row])
        {
         i--;
         continue;
        }
      int start = row * m_iVectorSize;
      int start_c = i * m_iVectorSize;
      for(int c = 0; c < m_iVectorSize; c++)
        {
         if(!c_aMeans.Update(start_c + c, data.At(start + c)))
            return false;
        }
      flags[row] = true;
     }

Nach der Initialisierung der Matrix der Zentren werden die Zeiger validiert und gegebenenfalls neue Instanzen von Pufferobjekten erstellt, um die Abstandsmatrix (c_aDistance), den Clusteridentifikationsvektor für jeden Zustand des Systems (c_aClusters) und den Vektor der Clusteränderungsflags für einzelne Systemzustände (c_aFlags) zu schreiben. Denken Sie daran, die Ausführung von Vorgängen zu kontrollieren.

   if(CheckPointer(c_aDistance) == POINTER_INVALID)
     {
      c_aDistance = new CBufferDouble();
      if(CheckPointer(c_aDistance) == POINTER_INVALID)
         return false;
     }
   c_aDistance.BufferFree();
   if(!c_aDistance.BufferInit(rows * m_iClusters, 0))
      return false;

   if(CheckPointer(c_aClasters) == POINTER_INVALID)
     {
      c_aClasters = new CBufferDouble();
      if(CheckPointer(c_aClasters) == POINTER_INVALID)
         return false;
     }
   c_aClasters.BufferFree();
   if(!c_aClasters.BufferInit(rows, 0))
      return false;

   if(CheckPointer(c_aFlags) == POINTER_INVALID)
     {
      c_aFlags = new CBufferDouble();
      if(CheckPointer(c_aFlags) == POINTER_INVALID)
         return false;
     }
   c_aFlags.BufferFree();
   if(!c_aFlags.BufferInit(rows, 0))
      return false;

Schließlich werden wir Puffer im OpenCL-Kontext erstellen.

   if(!data.BufferCreate(c_OpenCL) ||
      !c_aMeans.BufferCreate(c_OpenCL) ||
      !c_aDistance.BufferCreate(c_OpenCL) ||
      !c_aClasters.BufferCreate(c_OpenCL) ||
      !c_aFlags.BufferCreate(c_OpenCL))
      return false;

Damit ist die Vorbereitungsphase abgeschlossen. Nun können wir mit der Implementierung von Schleifenoperationen fortfahren, die sich direkt auf den Modellbildungsprozess beziehen. Wie bereits erwähnt, sind die wichtigsten Meilensteine des Algorithmus wie folgt:

  • Bestimmung der Abstände von jedem Element der Trainingsstichprobe zu jedem Clusterzentrum
  • Verteilung der Systemzustände nach Clustern (nach Mindestabstand)
  • Aktualisierung der Clusterzentren

Sehen Sie sich diese Stufen des Algorithmus an. Wir haben bereits mehrere Kernels im OpenCL-Programm erstellt, um jede Stufe auszuführen. Daher müssen wir nun einen Schleifenaufruf der entsprechenden Kernel implementieren.

Wir implementieren eine Trainingsschleife und rufen im Schleifenkörper zunächst den Kernel zur Berechnung der Abstände zu den Clusterzentren auf. Wir haben bereits alle notwendigen Puffer in den Speicher des OpenCL-Kontexts geladen. Daher können wir sofort zur Bestimmung der Kernelparameter übergehen. Hier geben wir Zeiger auf die von uns verwendeten Datenpuffer und die Größe des Vektors an, der einen Systemzustand beschreibt. Vergessen wir nicht, dass wir zur Angabe eines bestimmten Parameters ein Konstantenpaar „Kernel-Identifikator - Parameter-Identifikator“ verwenden

   int count = 0;
   do
     {
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_data, data.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_means, c_aMeans.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_distance, c_aDistance.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgument(def_k_kmeans_distance, def_k_kmd_vector_size, m_iVectorSize))
         return false;

Als Nächstes müssen wir die Dimension des Aufgabenraums und den Versatz in jedem dieser Räume angeben. Wir wollten diesen Kernel in einem zweidimensionalen Aufgabenraum ausführen. Erstellen wir zwei statische Arrays, deren Anzahl der Elemente dem Aufgabenbereich entspricht:

  • global_work_size - zur Angabe der Dimension des Aufgabenraums
  • global_work_offset - zur Angabe des Offsets in jeder Dimension

In ihnen wird die Nullpunktverschiebung in beiden Dimensionen angegeben. Die Größe der ersten Dimension ist gleich der Anzahl der einzelnen Zustände des Systems in der Trainingsmenge. Die Größe der zweiten Dimension ist gleich der Anzahl der Cluster in unserem Modell.

      uint global_work_offset[2] = {0, 0};
      uint global_work_size[2];
      global_work_size[0] = rows;
      global_work_size[1] = m_iClusters;

Danach müssen wir nur noch den Kernel zur Ausführung bringen und die Ergebnisse der Operationen ablesen.

      if(!c_OpenCL.Execute(def_k_kmeans_distance, 2, global_work_offset, global_work_size))
         return false;
      if(!c_aDistance.BufferRead())
         return false;

In ähnlicher Weise nennen wir den zweiten Kernel - bestimmend, ob die Systemzustände zu bestimmten Clustern gehören. Beachten Sie, dass dieser Kernel in einem eindimensionalen Aufgabenraum gestartet wird. Daher benötigen wir andere Arrays, um die Dimension und den Versatz anzugeben.

      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_clustering, def_k_kmc_flags, c_aFlags.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_clustering, def_k_kmc_clusters, c_aClasters.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_clustering, def_k_kmc_distance, c_aDistance.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgument(def_k_kmeans_clustering, def_k_kmc_total_k, m_iClusters))
         return false;
      uint global_work_offset1[1] = {0};
      uint global_work_size1[1];
      global_work_size1[0] = rows;
      if(!c_OpenCL.Execute(def_k_kmeans_clustering, 1, global_work_offset1, global_work_size1))
         return false;
      if(!c_aFlags.BufferRead())
         return false;

Bitte beachten Sie, dass wir, nachdem der Kernel in die Warteschlange zur Ausführung gestellt wurde, nur die Daten des Flag-Puffers lesen. Zu diesem Zeitpunkt reichen diese Daten aus, um das Ende der Modellschulung zu bestimmen. Das Laden von Zwischendaten von Cluster-Indizes ist nicht sinnvoll, sondern verursacht zusätzliche Aufwand. Daher wird sie in diesem Stadium nicht verwendet. 

Nachdem alle Elemente der Trainingsstichprobe nach Clustern verteilt wurden, prüfen wir, ob es eine Umverteilung der Elemente nach Clustern gab. Dazu überprüfen wir den Höchstwert des Datenpuffers der Flags. Wie Sie sich erinnern, haben wir im entsprechenden Code des Kernels den Flag-Puffer mit dem booleschen Ergebnis des Vergleichs der Cluster-IDs aus der vorherigen Iteration und der neu zugewiesenen ID gefüllt. Bei Gleichheit wurde 0 in den Puffer geschrieben. Wenn sich der Cluster geändert hat, haben wir 1 geschrieben. Wir interessieren uns nicht für die genaue Anzahl der Elemente, die den Cluster verändert haben. Es genügt zu wissen, dass es solche Elemente gibt. Deshalb prüfen wir den Maximalwert. Wenn sie gleich 0 ist, d.h. keines der Elemente den Cluster gewechselt hat, betrachten wir das Training des Modells alsabgeschlossen. Wir lesen den Cluster-Identifikationspuffer für jedes Element der Sequenz und verlassen die Schleife.

      m_bTrained = (c_aFlags.Maximum() == 0);
      if(m_bTrained)
        {
         if(!c_aClasters.BufferRead())
            return false;
         break;
        }

Wenn der Lernprozess noch nicht abgeschlossen ist, fahren wir mit dem Aufruf des dritten Kerns fort, der die zentralen Vektoren der Cluster aktualisiert. Dieser Kernel wird auch in einem zweidimensionalen Aufgabenraum arbeiten. Daher werden wir die beim Aufruf des ersten Kernels erstellten Arrays verwenden. Wir werden nur die Größe der ersten Dimension ändern.

      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_updates, def_k_kmu_data, data.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_updates, def_k_kmu_means, c_aMeans.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_updates, def_k_kmu_clusters, c_aClasters.GetIndex()))
         return false;
      if(!c_OpenCL.SetArgument(def_k_kmeans_updates, def_k_kmu_total_m, rows))
         return false;
      global_work_size[0] = m_iVectorSize;
      if(!c_OpenCL.Execute(def_k_kmeans_updates, 2, global_work_offset, global_work_size))
         return false;
      if(!c_aMeans.BufferRead())
         return false;
      count++;
      Comment(StringFormat("Study iterations %d", count));
     }
   while(!m_bTrained && !IsStopped());

Nach der Ausführung des Kernels wird zur visuellen Kontrolle des Trainingsprozesses die Anzahl der abgeschlossenen Trainingsiterationen in das Kommentarfeld des Diagramms gedruckt und mit der nächsten Iteration der Schleife fortgefahren.

Beachten Sie, dass wir während des gesamten Modelltrainings den Speicher des OpenCL-Kontextes nicht gelöscht und die Daten nicht erneut in den Kontext kopiert haben. Denn solche Maßnahmen würden auch Ressourcen erfordern. Um die Effizienz der Ressourcennutzung zu erhöhen und die Gesamtdauer der Modellschulung zu verkürzen, haben wir diesen Aufwand eliminiert. Dieser Ansatz ist jedoch nur möglich, wenn der Kontextspeicher ausreicht, um alle Daten zu speichern. Wenn nicht, müssen wir die Verwendung des Kontextspeichers überdenken, alte Daten entladen und neue Daten laden, bevor jeder Kernel ausgeführt wird.

Nach Abschluss des Trainingsprozesses und vor dem Beenden der Methode löschen wir jedoch den Kontextspeicher und einige der Puffer.

   data.BufferFree();
   c_aDistance.BufferFree();
   c_aFlags.BufferFree();
//---
   return true;
  }

Das Modelltraining ist kein Selbstzweck. Wir trainieren das Modell, um die Trainingsergebnisse zu nutzen und sie auf neue Daten anzuwenden. Um diese Funktionalität zu implementieren, erstellen wir die Methode Clustering. Sein Algorithmus ist eine etwas verkürzte Version der oben beschriebenen Lernmethode, bei der wir die Lernschleife und den dritten Kernel ausgenommen haben. Nur die ersten 2 Kernel werden einmal aufgerufen. Sie können den Code im Anhang selbst studieren.

Die nächste Methode, die wir uns ansehen werden, ist die Methode zur Berechnung des Wertes der Verlustfunktion — getloss. Um beim Modelltraining Ressourcen zu sparen, haben wir die Werte der Verlustfunktion nicht berechnet. Daher erhält die Methode in den Parametern einen Zeiger auf die Datenprobe, für die der Fehler berechnet werden soll. Aber wenn wir früher, am Anfang der Methode, einen Block von Steuerelementen implementiert haben, rufen wir jetzt stattdessen die Clustermethode auf. Und vergessen wir natürlich nicht, das Ergebnis der Methodenausführung zu überprüfen.

double CKmeans::GetLoss(CBufferDouble *data)
  {
   if(!Clustering(data))
      return -1;

Dieser Ansatz ermöglicht es uns, 2 Aufgaben gleichzeitig mit einer Aktion zu lösen. Die erste Aufgabe ist das Clustering der neuen Stichprobe selbst. Um die Abweichungen berechnen zu können, müssen wir wissen, zu welchen Clustern die Stichprobenelemente gehören.

Zweitens enthält die Clustering-Methode bereits alle erforderlichen Steuerelemente, sodass wir sie nicht wiederholen müssen.

Als Nächstes zählen wir die Anzahl der Systemzustände in der Stichprobe und initialisieren den Puffer, um Abweichungen zu ermitteln.

   int total = data.Total();
   int rows = total / m_iVectorSize;
//---
   if(CheckPointer(c_aLoss) == POINTER_INVALID)
     {
      c_aLoss = new CBufferDouble();
      if(CheckPointer(c_aLoss) == POINTER_INVALID)
         return -1;
     }
   if(!c_aLoss.BufferInit(rows, 0))
      return -1;

Dann übertragen wir die Ausgangsdaten in den Kontextspeicher. Vergessen wir nicht, dass wir keine Puffer mit Durchschnittswerten und Cluster-IDs an den Kontextspeicher übergeben. Dies liegt daran, dass sie bereits im OpenCL-Kontextspeicher vorhanden sind. Wir haben sie nach dem Clustering der Daten nicht gelöscht, sodass wir in diesem Stadium einige Ressourcen sparen können.

   if(!data.BufferCreate(c_OpenCL) ||
      !c_aLoss.BufferCreate(c_OpenCL))
      return -1;

Anschließend rufen wir den entsprechenden Kernel auf. Der Ablauf des Kernelaufrufs ist völlig identisch mit den oben beschriebenen Beispielen. Wir sollten uns also nicht damit aufhalten. Der vollständige Code aller Methoden und Funktionen findet sich im Anhang.

In diesem Kernel haben wir jedoch die Abweichung jedes einzelnen Zustands ermittelt. Nun müssen wir die mittlere Abweichung bestimmen. Dazu erstellen wir eine Schleife, in der wir einfach alle Werte des Puffers zusammenzählen. Anschließend wird das Ergebnis durch die Gesamtzahl der Elemente in der analysierten Probe geteilt.

   m_dLoss = 0;
   for(int i = 0; i < rows; i++)
      m_dLoss += c_aLoss.At(i);
   m_dLoss /= rows;

Am Ende der Methode wird der Kontextspeicher gelöscht und der resultierende Wert zurückgegeben.

   data.BufferFree();
   c_aLoss.BufferFree();
   return m_dLoss;
  }

Inzwischen haben wir die gesamte Funktionalität geschaffen, die für das Modelltraining und das anschließende Clustering der Daten erforderlich ist. Wir wissen jedoch, dass die Ausbildung eines Modells ein ressourcenintensiver Prozess ist, der nicht vor jedem Start der praktischen Modellnutzung wiederholt werden kann. Daher sollten wir die Speicherung des Modells in einer Datei und die Möglichkeit, seine volle Funktionsfähigkeit aus der Datei wiederherzustellen, hinzufügen. Diese Funktionen werden über die Methoden Save und Load implementiert. Im Rahmen dieser Artikelserie haben wir bereits mehrfach ähnliche Methoden erstellt, da sie in jeder Klasse verwendet werden. Der entsprechende Code ist im Anhang zu finden. Wenn Sie Fragen haben, schreiben Sie sie bitte in die Kommentare zu diesem Artikel.

Die endgültige Struktur unseres Kurses wird wie folgt aussehen. Der vollständige Code aller Methoden und Klassen ist in der Anlage unten zu finden.

class CKmeans  : public CObject
  {
protected:
   int               m_iClusters;
   int               m_iVectorSize;
   double            m_dLoss;
   bool              m_bTrained;

   COpenCLMy         *c_OpenCL;       
   //---
   CBufferDouble     *c_aDistance;
   CBufferDouble     *c_aMeans;
   CBufferDouble     *c_aClasters;
   CBufferDouble     *c_aFlags;
   CBufferDouble     *c_aLoss;

public:
                     CKmeans(void);
                    ~CKmeans(void);
   //---
   bool              SetOpenCL(COpenCLMy *context);
   bool              Init(COpenCLMy *context, int clusters, int vector_size);
   bool              Study(CBufferDouble *data, bool init_means = true);
   bool              Clustering(CBufferDouble *data);
   double            GetLoss(CBufferDouble *data);
   //---
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   //---
   virtual int       Type(void)  { return defUnsupervisedKmeans; }
  };

5. Tests

Und jetzt kommen wir zum Höhepunkt des Prozesses. Wir haben eine neue Daten-Clustering-Klasse erstellt. Lassen Sie uns nun seinen praktischen Wert bewerten. Wir werden das Modell trainieren. Zu diesem Zweck erstellen wir einen Expert Advisor mit dem Namen kmeans.mq5“. Der gesamte EA-Code ist im Anhang zu finden.

Die externen Parameter des EA entsprechen denen, die wir zuvor verwendet haben. Der einzige Unterschied besteht darin, dass die Trainingszeit der EA auf 15 Jahre verlängert wird. Dies ist der Vorteil des unüberwachten Lernens: Wir können eine große Menge an unmarkierten Daten verwenden. Ich habe die Anzahl der Modellcluster nicht in die Parameter aufgenommen, da der Lernprozess in einer Schleife mit einer ziemlich großen Anzahl von Clustern durchgeführt wird. Um die optimale Anzahl von Clustern zu finden, haben wir mehrere Optionen zwischen 50 und 1000 Clustern durchgespielt. Der Schritt betrug 50 Cluster. Dies sind genau die Clustering-Parameter, die wir im vorherigen Artikel beim Testen des Python-Skripts verwendet haben. Die Testparameter entsprechen denen, die wir in früheren Experimenten verwendet haben:

  • Symbol: EURUSD;
  • Zeitrahmen H1.

Als Ergebnis des Trainings erhielten wir einen Graphen, der die Abhängigkeit der Verlustfunktion von der Anzahl der Cluster zeigt. Sie ist unten dargestellt: 

Diagramm der Abhängigkeit der Werte der Verlustfunktion von der Anzahl der Cluster

Wie Sie aus dem Diagramm ersehen können, erwies sich der Bruch als ziemlich ausgedehnt - im Bereich von 100 bis 500 Clustern. Insgesamt analysierte das Modell mehr als 92 Tausend Systemzustände. Die Form des Diagramms ist völlig identisch mit dem vom Python-Skript im vorherigen Artikel erstellten Diagramm. Dies bestätigt indirekt, dass die von uns erstellte Klasse korrekt funktioniert.

Schlussfolgerungen

In diesem Artikel haben wir eine neue Klasse CKmeans erstellt, um eine der gängigsten k-means-Clustermethoden zu implementieren. Es ist uns sogar gelungen, das Modell mit einer unterschiedlichen Anzahl von Clustern zu trainieren. Während der Tests gelang es dem Modell, etwa 500 Muster zu erkennen. Ein ähnliches Ergebnis wurde durch ähnliche Tests in Python erzielt. Das bedeutet, dass wir den Methodenalgorithmus korrekt kopiert haben. Im nächsten Artikel werden wir mögliche Methoden zur praktischen Nutzung von Clustering-Ergebnissen diskutieren.


Referenzen

  1. Neuronale Netze leicht gemacht
  2. Neuronale Netze leicht gemacht (Teil 2): Netzschulung und -prüfung
  3. Neuronale Netze leicht gemacht (Teil 3): Convolutional Neurale Netzwerke
  4. Neuronale Netze leicht gemacht (Teil 4): Rekurrente Netzwerke
  5. Neuronale Netze leicht gemacht (Teil 5): Multithreading-Berechnungen in OpenCL
  6. Neuronale Netze leicht gemacht (Teil 6): Experimentieren mit der Lernrate des neuronalen Netzes
  7. Neuronale Netze leicht gemacht (Teil 7): Adaptive Optimierungsverfahren
  8. Neuronale Netze leicht gemacht (Teil 8): Attention-Mechanismen
  9. Neuronale Netze leicht gemacht (Teil 9): Dokumentieren der Arbeit
  10. Neuronale Netze leicht gemacht (Teil 10): Multi-Head Aufmerksamkeit
  11. Neuronale Netze leicht gemacht (Teil 11): Ein Blick auf GPT
  12. Neuronale Netze leicht gemacht (Teil 12): Dropout
  13. Neuronale Netze leicht gemacht (Teil 13): Batch-Normalisierung
  14. Neuronale Netze leicht gemacht (Teil 14): Datenclustering

Programme, die im diesem Artikel verwendet werden

# Name Typ Beschreibung
1 kmeans.mq5 Expert Advisor   Expert Advisor zum Trainieren des Modells 
2 kmeans.mqh  Klassenbibliothek Bibliothek für die Organisation der k-means-Methode 
3 unsupervised.cl Bibliothek
OpenCL-Programmcode-Bibliothek zur Implementierung der k-means-Methode
4 NeuroNet.mqh Klassenbibliothek Klassenbibliothek zur Erstellung eines neuronalen Netzes
5 NeuroNet.cl Bibliothek OpenCL-Programmcode-Bibliothek


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/10947

Beigefügte Dateien |
MQL5.zip (63.7 KB)
DoEasy. Steuerung (Teil 7): Steuerung der Text Label DoEasy. Steuerung (Teil 7): Steuerung der Text Label
In diesem Artikel werde ich die Klasse des WinForms Steuerungsobjekts der Text Label erstellen. Ein solches Objekt kann seinen Container an beliebiger Stelle positionieren, während seine eigene Funktionalität die Funktionalität des MS Visual Studio-Text Label kopiert. Wir werden in der Lage sein, Schriftparameter für einen angezeigten Text festzulegen.
Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 15): Zugang zu Daten im Internet (I) Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 15): Zugang zu Daten im Internet (I)
Wie kann man über den MetaTrader 5 auf Online-Daten zugreifen? Es gibt viele Webseiten und Orte im Internet, die eine riesige Menge an Informationen bieten. Sie müssen nur wissen, wo Sie suchen und wie Sie diese Informationen am besten nutzen können.
Lernen Sie, wie man ein Handelssystem mit der Standardabweichung entwirft Lernen Sie, wie man ein Handelssystem mit der Standardabweichung entwirft
Hier ist ein neuer Artikel in unserer Serie darüber, wie man ein Handelssystem mit den beliebtesten technischen Indikatoren in MetaTrader 5 Handelsplattform zu entwerfen. Lernen Sie, wie man ein Handelssystem mit Hilfe des Indikators der Standardabweichung entwickelt.
Techniken des MQL5-Assistenten, die Sie kennen sollten (Teil 02): Kohonen-Karten Techniken des MQL5-Assistenten, die Sie kennen sollten (Teil 02): Kohonen-Karten
Der Händler von heute ist ein Philomath, der fast immer (entweder bewusst oder unbewusst...) nach neuen Ideen sucht, sie ausprobiert, sich entscheidet, sie zu modifizieren oder zu verwerfen; ein explorativer Prozess, der einiges an Sorgfalt kosten sollte. Dies legt eindeutig einen hohen Stellenwert auf die Zeit des Händlers und die Notwendigkeit, Fehler zu vermeiden. Diese Artikelserie wird vorschlagen, dass der MQL5-Assistent eine Hauptstütze für Händler sein sollte. Warum? Denn der Händler spart nicht nur Zeit, indem er seine neuen Ideen mit dem MQL5-Assistenten zusammenstellt, und reduziert Fehler durch doppelte Codierung erheblich. Er ist letztendlich so eingestellt, dass er seine Energie auf die wenigen kritischen Bereiche seiner Handelsphilosophie konzentriert.