Neuronale Netze im Trading: Eine intelligente Prognosepipeline (Sparse Mixture of Experts)
Einleitung
Finanzmärkte bilden ein komplexes, chaotisches und hochfrequentes System. Grobe Annäherungen und Durchschnittswerte funktionieren hier nicht. Jede Bar und jede Kursbewegung ist das Ergebnis einer Vielzahl von Faktoren, die von fundamentalen Nachrichten bis hin zum Momentum-Trading reichen. Genau aus diesem Grund erfordert die Arbeit mit Marktzeitreihen einen besonderen Ansatz: einen Ansatz, der sensibel für kleinste Details, robust gegenüber Rauschen und in der Lage ist, Strukturen im Chaos zu erkennen.
Das Time-MoE-Framework bietet genau diese Art von Architektur. Dies ist nicht nur ein für Zeitreihen angepasster Transformer. Es handelt sich um ein in sich geschlossenes System, in dem jeder Zeitschritt der analysierten Historie als eigenständiges Token behandelt wird. Diese Token durchlaufen eine Reihe von Transformationen, behalten dabei jedoch ihre Individualität und ihren zeitlichen Kontext bei. Dieser Ansatz ermöglicht es dem Modell, hochfrequente Daten zu verarbeiten und Muster zu erkennen, die mit herkömmlichen Aggregationsmethoden nicht erkennbar sind.
Die erste Verarbeitungsstufe ist die Embedding-Schicht, in der die Daten nichtlineare Transformationen durchlaufen. Dies hilft dabei, komplexe Zusammenhänge zwischen verschiedenen Features zu erfassen, sei es die Beziehung zwischen Kurs und Handelsvolumen, die Richtung von Indikatoren oder die Stärke der jüngsten Kursbewegung. Die daraus resultierende verborgene Repräsentation dient als Grundlage für die weitere Analyse.
Die Token werden anschließend an eine Reihe von Transformer-Blöcken weitergeleitet. Hier blickt das Modell zurück und bildet auf der Grundlage gesammelter Erfahrungen eine Repräsentation des aktuellen Moments. Dies wird durch einen Attention-Mechanismus erreicht, bei dem jedes Token mit vorherigen Token verglichen wird und die Bedeutung bestimmter Elemente nicht manuell, sondern vom Modell selbst während des Trainings ermittelt wird. Dadurch lassen sich sowohl kurzfristiges Momentum als auch langfristige Trends berücksichtigen.
Time-MoE legt besonderen Wert auf Robustheit gegenüber Rauschen. Zu diesem Zweck umfasst die Architektur Normalisierungsmechanismen, die dazu beitragen, zufällige Ausreißer zu glätten und aussagekräftige Signale zu verstärken. Infolgedessen wird die Attention nicht auf einzelne Anomalien fokussiert, sondern gleichmäßiger und sinnvoller verteilt, was insbesondere bei Volatilität und Marktrauschen wichtig ist.
Der wesentliche Unterschied zwischen Time-MoE und klassischen Transformern besteht in der Verwendung einer spärlichen Expertenmischung (Mixture-of-Experts – MoE). In jedem Block wählt der Router nur eine Teilmenge der verfügbaren Experten aus, die tatsächlich an der Verarbeitung des aktuellen Tokens beteiligt sind. Diese Lösung reduziert die Rechenlast erheblich und ermöglicht eine Skalierung des Modells, ohne dass der Ressourcenbedarf exponentiell ansteigt. Ferner umfasst Time-MoE einen gemeinsam genutzten Experten, der stets aktiv ist und dem Modell Robustheit gegenüber fehlerhaften Routing-Entscheidungen verleiht.
Die abschließende Phase ist die Prognose. Hier erstellt das Modell gleichzeitig mehrere Prognosen für verschiedene Zeithorizonte. Dieser Ansatz ermöglicht es, sowohl kurzfristige Signale als auch langfristige Trends gleichzeitig zu berücksichtigen, wodurch dem Trader oder dem Analysesystem eine breite Palette an Szenarien zur Verfügung steht. Während des Trainings lernt das Modell gleichzeitig über alle Zeithorizonte hinweg, wodurch es flexibler wird und sich besser an veränderte Marktbedingungen anpassen kann.
Somit ist Time-MoE:
- ein detailsensitives Modell, das mit Token pro Zeitschritt arbeitet;
- robust gegenüber Rauschen und Ausreißern durch Attention-Normalisierung;
- eine skalierbare Architektur mit spärlicher Nutzung von Experten;
- ein universeller Mechanismus für die Multi-Horizon-Prognose.
Nachfolgend ist die Visualisierung des Time-MoE-Frameworks durch den Autor dargestellt.

Heute werden wir die zuvor begonnene Arbeit fortsetzen und uns auf ein Schlüsselelement des Time-MoE-Frameworks konzentrieren – die spärlich aktivierte Expertenmischung (Sparse Mixture of Experts). Im vorigen Teil haben wir Schritt für Schritt die Grundlage des Modells geschaffen und dabei Token sowie verborgene Repräsentationen mit SwiGLU-Embeddings gebildet; nun ist es an der Zeit, uns dem zentralen Architekturelement zuzuwenden, von dem die Effizienz und Skalierbarkeit des gesamten Systems maßgeblich abhängen.
In diesem Artikel werden wir im Detail untersuchen, wie die Expertengruppe arbeitet und wie die Berechnungen verteilt werden. Wir werden nicht nur einen theoretischen Rahmen beschreiben, sondern uns anschließend mit der konkreten Umsetzung eines spärlichen MoE mithilfe von MQL5 befassen, wobei der Schwerpunkt auf praktischen Aspekten liegt.
Entwicklung der Architektur
Bevor wir uns mit der eigentlichen Umsetzung der spärlichen Expertenmischung befassen, sollten wir einen Moment innehalten und darüber nachdenken. Wie bisher halten wir an dem Konzept fest, dass alle Experten parallel arbeiten – ein Ansatz, der sich perfekt in das Paradigma des massiv parallelen Rechnens einfügt. Daher werden wir den Großteil der Rechenlast ohne zu zögern in den OpenCL-Kontext verlagern. Dies wirft jedoch eine wichtige Frage auf: Wie genau sollten wir die spärliche Aktivierung von Experten organisieren, ohne dabei die Effizienz und Trainierbarkeit des Modells zu beeinträchtigen?
Lassen Sie uns zunächst einen wichtigen Punkt wiederholen. In ihrer ursprünglichen Veröffentlichung schlugen die Autoren von Time-MoE eine Expertenarchitektur vor, die sich erheblich vom klassischen FeedForward-Block unterscheidet, den wir aus der Transformer-Architektur kennen. Konkret wurde die erste Schicht in den Experten durch eine SwiGLU-Transformation ersetzt. Diese Änderung ist voll und ganz gerechtfertigt: Wie wir bereits gesehen haben, bietet SwiGLU dem Modell mehr Flexibilität bei der Verarbeitung von Features und bildet nichtlineare Beziehungen besser ab. Glücklicherweise verfügen wir bereits über eine Implementierung dieser Schicht – im vorherigen Artikel haben wir die Komponente CNeuronSwiGLUOCL entwickelt. Nun können wir sie zuversichtlich als erste Stufe unserer Expertenmischung verwenden und die Anzahl der Ausgabefilter entsprechend der Anzahl der parallelen Teilmodelle skalieren.
Jeder Filter verfügt über eigene trainierbare Parameter. Im Grunde handelt es sich hierbei um ein eigenständiges Expertenmodell. Wenn wir die Ausgaben nach der Anzahl der Experten gruppieren, erhalten wir die gewünschte Struktur: eine Eingabe – eine Gruppe unabhängiger Experten, die parallel arbeiten. In der zweiten Verarbeitungsstufe können wir eine Multi-Window-Faltung anwenden, die wir bereits aus unseren früheren Arbeiten kennen. Diese Schicht hilft dabei, die Informationen innerhalb jedes Experten zu konsolidieren und für die Aggregation vorzubereiten.
Bisher scheint alles ziemlich logisch zu sein. Es gibt jedoch einen wichtigen Vorbehalt: Dem beschriebenen Algorithmus fehlt das wesentliche Element – die Sparsity, die spärliche Aktivierung. Ja, wir können die Ausgaben aller Experten mit der Aktivierungsmaske multiplizieren und so das richtige Ergebnis erhalten. Dennoch rechnen alle Experten weiter, auch wenn ihr Beitrag lediglich durch die Maske unterdrückt wird. Dieser Ansatz ist bei kleinen Modellen akzeptabel, wird jedoch mit zunehmender Größe des Systems ineffizient: Eine steigende Anzahl von Experten, höhere Dimensionen und eine tiefere Netzarchitektur – all diese Faktoren erhöhen den Rechenaufwand erheblich.
Damit kommen wir zur entscheidenden Frage: An welcher Stelle die Sparse-Aktivierung eingeführt werden sollte?
Auf den ersten Blick könnte man meinen, die beste Lösung wäre, nicht genutzte Experten auf allen Ebenen zu deaktivieren – schließlich lässt sich dadurch die Rechenlast drastisch reduzieren. Allerdings spielt hier ein weitaus subtilerer und wichtigerer Aspekt eine Rolle. Der springende Punkt ist, dass die Grundidee hinter MoE darin besteht, dass verschiedene Experten unterschiedliche Teilaufgaben erlernen. Gleichzeitig entwickelt jeder von ihnen seine eigene Spezialisierung. Aber wer entscheidet, welche Experten in einer bestimmten Situation zum Einsatz kommen? Die Antwort lautet: ein Router, der anhand der Eingabedaten eine Teilmenge von Modellen zur Aktivierung auswählt.
Das Problem entsteht, wenn sich der Router zu früh ausschließlich auf eine kleine Gruppe von Experten konzentriert und beginnt, diese wahllos einzusetzen – unabhängig vom Kontext. Ein solches Modell mag in der Anfangsphase zwar gute Ergebnisse liefern, verliert jedoch seine Anpassungsfähigkeit, da es niemals alternative Wege ausprobiert. In solchen Fällen haben nicht eingesetzte Experten schlichtweg keine Gelegenheit, ihre Leistungsfähigkeit in anderen Szenarien unter Beweis zu stellen. Dadurch entsteht eine Art lokaler Komfortzone: Der Router kennt nur seine Favoriten und bleibt, ohne andere auszuprobieren, in demselben Muster gefangen. Dies führt zu einer Verringerung der Vielfalt des Modells und zu einer Verschlechterung seiner Generalisierungsfähigkeit.
Unser Ziel ist es daher, den Router so zu trainieren, dass er nicht einfach nur Experten auswählt, sondern seine Auswahlstrategie an die Eigenschaften der Eingabedaten anpasst. Wir müssen Bedingungen schaffen, unter denen das Modell das Verhalten anderer Experten exploriert, auch wenn diese anfangs weniger effektiv sind. Nur so wird es lernen können, Aufgaben flexibler zuzuweisen und dadurch die Gesamtgenauigkeit sowie die Widerstandsfähigkeit gegenüber Marktveränderungen zu verbessern.
Um ein Gleichgewicht zwischen Recheneffizienz und Trainingsabdeckung zu finden, wurde beschlossen, die spärliche Nutzung von Experten nur in der zweiten Schicht des MoE-Blocks zu implementieren und diese Schicht mit dem Ergebnisaggregationsprozess zu kombinieren. Dieser Ansatz vereinfacht die Architektur, verringert die Rechenlast und bewahrt gleichzeitig die erforderliche Flexibilität für das Training des Routers.
Der zentrale Aspekt dieses Entwurfs ist der Mechanismus zur Propagation des Fehlergradienten. Wir lehnen es bewusst ab, ungenutzte Experten direkt zu trainieren – schließlich besteht der Sinn von MoE gerade darin, verschiedene Experten auf unterschiedliche Aufgaben zu spezialisieren. Um jedoch zu verhindern, dass sich das Modell auf denselben Satz von Experten festfährt, routen wir den Gradienten zum Router und geben ihm damit ein Signal, dass die aktuelle Auswahl möglicherweise unwirksam ist. Dadurch kann der Routing-Algorithmus selbst trainiert werden: Beim nächsten Mal kann er eine andere Gruppe von Experten aktivieren, falls die aktuelle Gruppe eine schlechte Prognose geliefert hat.
Selbst wenn also in einem bestimmten Fall nur zwei Experten aktiviert wurden, lernt der Router, dass auch andere hätten ausgewählt werden können. Dies ist nicht nur ein technischer Kniff, sondern ein leistungsstarkes Anpassungswerkzeug, das es dem Modell ermöglicht, schrittweise den Zusammenhang zwischen den Merkmalen der Eingabedaten und der optimalen Konfiguration der aktiven Experten zu erlernen.
Faltung über maskierte Fenster
Die wichtigsten Ansätze wurden festgelegt, und nun krempeln wir die Ärmel hoch und gehen von der Theorie zur Praxis über. In der ersten Implementierungsphase müssen wir eine Schlüsselkomponente entwickeln – das MoE-Objekt der zweiten Schicht, das für die spärliche Aktivierung der Experten und die Aggregation ihrer Ergebnisse zuständig ist. Diese Komponente wird als zentrales Bindeglied im Modell dienen: Daten aller Experten werden durch sie hindurchgeleitet, doch bei jedem einzelnen Durchlauf werden nur einige davon aktiviert.
Wir haben die Auswahl der aktiven Experten in ein separates Modul verlagert – den Router. Seine Aufgabe besteht darin, die Eingabedaten zu analysieren und eine Aktivierungsmaske zu erzeugen, die festlegt, welche Experten für jedes Token herangezogen werden sollen. Diese Maske wird zusammen mit dem Haupttensor der zu analysierenden Features an dieses Objekt übergeben und bestimmt so die Konfiguration der aktiven Berechnungen im aktuellen Schritt.
Eine wichtige Annahme: Alle Experten verfügen über dieselbe Architektur und liefern Ergebnisse mit einer festen Dimension. Zu jedem gegebenen Zeitpunkt ist jedoch nur eine begrenzte Anzahl von Modellen aktiv – in der Regel deutlich weniger als die Dimensionalität des Ausgaberaums. Die übrigen Experten bleiben inaktiv; sie nehmen nicht an den Berechnungen teil und verbrauchen keine Ressourcen.
Dieser Ansatz bietet zwei wesentliche Vorteile:
- Eine erhebliche Verringerung der Rechenlast – von entscheidender Bedeutung bei der Skalierung von Modellen und der Erhöhung der Anzahl der Experten.
- Erhöhte Spezialisierung: Jeder Experte kann sich auf seinen eigenen engen Teilaufgaben-Subraum konzentrieren. Dadurch bildet das Modell echtes Expertenwissen aus, das auf die beteiligten Experten verteilt ist.
Wir haben den Großteil der Berechnungen in den OpenCL-Kontext verlagert, wodurch wir die parallele Verarbeitung auf GPUs und anderen kompatiblen Geräten optimal nutzen können. Unser Fokus liegt auf dem Vorwärtsdurchlauf-Kernel der zweiten MoE-Schicht, der die spärliche Aktivierung ausgewählter Experten implementiert und deren Ergebnisse aggregiert.
In den Kernel-Parametern erhalten wir die Gewichte aller Experten, die Eingabedaten, die Aktivierungsmaske sowie verschiedene Parameter, die die Fensterstruktur und die Dimensionen definieren.
__kernel void FeedForwardMaskMultWinConv(__global const float *matrix_w, __global const float *matrix_i, __global const float *masks, __global float *matrix_o, const int inputs, const int window_in, const int windows_total, const int activation ) { const size_t u = get_global_id(0); const size_t w = get_global_id(1); const size_t v = get_global_id(2); const size_t units = get_global_size(0); const size_t window_out = get_global_size(1); const size_t variables = get_global_size(2);
Jeder Rechenthread entspricht einem bestimmten Element des Ergebnistensors – anhand von Sequenzposition, Token-Element und Variable. Diese elementweise Adressierung ermöglicht ein hohes Maß an Parallelisierung.
Es ist wichtig zu betonen, dass wir im Vorfeld eine Annahme getroffen haben: Die Anzahl der aktiven Experten in jedem Vorwärtsdurchlauf ist deutlich geringer als die Token-Dimensionalität des Ergebnistensors. Dies ist ein entscheidender Punkt, der die Logik der Aufgabenverteilung innerhalb des Kernels bestimmt. Wir unterteilen den Aufgabenraum nach Token-Elementen und durchlaufen die aktiven Experten in einer Schleife innerhalb des Kernels selbst. Dieser Ansatz ermöglicht eine effiziente Nutzung der Rechenressourcen bei gleichzeitiger Wahrung der spärlichen Aktivierung.
Im Kernel selbst identifizieren wir den Rechenthread im Aufgabenraum und ermitteln den Offset in den Datenpuffern für die zu analysierenden Elemente.
const int shift_in = u * window_in * windows_total; const int shift_in_var = v * units * window_in * windows_total; const int shift_out = (u + v * units) * window_out + w; const int shift_mask = (u + v * units) * windows_total; const int shift_weight = (v * window_out * windows_total + w) * (window_in + 1); const int step_weight = window_out * (window_in + 1);
Als Nächstes richten wir eine Schleifenstruktur ein. Die äußere Schleife durchläuft alle Experten und überprüft deren Status anhand der Aktivierungsmaske. Ist ein Experte in diesem Fenster nicht aktiv, wird sein Beitrag übersprungen, wodurch unnötige Berechnungen vermieden und Ressourcen geschont werden. Bei aktiven Experten wird eine gewichtete Summe über die Eingabedaten berechnet, wobei ein Bias hinzugefügt wird. Die Endwerte werden akkumuliert und anschließend durch eine Aktivierungsfunktion geleitet.
float sum = 0; for(int w_in = 0; w_in < windows_total; w_in++) { float m = IsNaNOrInf(masks[shift_mask + w_in], 0); if(m < FLT_EPSILON) continue; const int shift_in_loc = shift_in + w_in * window_in; const int shift_weight_loc = shift_weight + w_in * step_weight; for(int i = 0; i < window_in; i++) if((shift_in_loc + i) < (inputs / variables)) sum += IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc + i], 0) * matrix_w[shift_weight_loc + i] * m; sum += matrix_w[shift_weight_loc + window_in] * m; }
Bitte beachten Sie, dass wir während des Aggregationsprozesses nicht einfach die Ergebnisse der aktiven Experten addieren, sondern jedes Ergebnis mit dem entsprechenden Maskenwert multiplizieren. Bei der binären Maskierung, bei der die Maske ausschließlich Nullen und Einsen enthält, bringt dies keinen zusätzlichen Nutzen. Dieser Ansatz lässt uns jedoch eine wichtige Option offen: die Umsetzung einer gewichteten Aggregation. Wenn die Maske nicht nur Flags, sondern auch tatsächliche Gewichtungskoeffizienten enthält, können wir den Beitrag jedes Experten zum Ergebnis steuern – bis hin zur Soft-Selektion und zum probabilistischen Routing.
Die resultierenden Werte werden durch eine Aktivierungsfunktion geleitet und im Ergebnispuffer gespeichert.
matrix_o[shift_out] = Activation(sum, activation);
}
Nach der Durchführung des Vorwärtsdurchlaufs – bei dem aktive Experten ihre Feature-Teilräume verarbeiten und die Ergebnisse anhand einer Maske gewichtet werden – gehen wir zur zweiten wichtigen Phase über: der Backpropagation des Fehlergradienten. In dieser Phase müssen wir den Fehler sorgfältig nicht nur zu den Eingabedaten jedes aktiven Modells, sondern auch zur Aktivierungsmaske selbst propagieren, damit diese während des Trainings angepasst werden kann.
Um diese Aufgabe zu lösen, haben wir einen OpenCL-Kernel namens CalcHiddenGradientMaskMultWinConv entwickelt, der all dies im Rahmen des Paradigmas der parallelen Berechnung übernimmt.
__kernel void CalcHiddenGradientMaskMultWinConv(__global const float *matrix_w, __global const float *matrix_i, __global float *matrix_ig, __global const float *matrix_og, __global const float *masks, __global float *masks_g, const int outputs, const int window_in, const int window_out, const int activation ) { const size_t u = get_global_id(0); const size_t w_in = get_global_id(1); const size_t v = get_global_id(2); const size_t units = get_global_size(0); const size_t windows_total = get_global_size(1); const size_t variables = get_global_size(2);
Der Kernel benötigt Gewichte, Eingabedaten, Fehlergradienten auf Ausgabeebene, eine Maske sowie Puffer zum Schreiben der Gradienten der Eingabedaten und der Maske. Jedes Work-Item im OpenCL-Kontext ist für eine separate Kombination aus einem Token, einem Experten und einer Variablen zuständig. Im Kernel-Code identifizieren wir dann sofort das Work-Item entlang aller Dimensionen des Aufgabenraums. Als Nächstes ermitteln wir die Offsets in den Datenpuffern.
const int shift_in = (u + v * units) * window_in * windows_total + w_in * window_in; const int shift_out = u * window_out; const int shift_out_var = v * units * window_out; const int shift_mask = (u + v * units) * windows_total + w_in; const int shift_weight = (v * window_out * windows_total + w_in * window_out) * (window_in + 1);
Im ersten Schritt propagieren wir den Fehlergradienten bis hinunter auf die Ebene der Eingabedaten. Hier beginnt der Prozess mit der Überprüfung der Maske: War der entsprechende Experte für das jeweilige Token inaktiv, werden einfach Nullwerte in den Gradientenpuffer der Eingabedaten geschrieben, und es werden keine Rechenressourcen verschwendet. Hat der Experte jedoch an der Berechnung mitgewirkt, beginnt die Gradientenpropagation.
const float m = IsNaNOrInf(masks[shift_mask], 0); for(int i = 0; i < window_in; i++) { float sum = 0; if(m >= FLT_EPSILON) { for(int out = 0; out < window_out; out++) { if((shift_out + out) >= (outputs / variables)) continue; sum += IsNaNOrInf(matrix_og[shift_out_var + shift_out + out] * matrix_w[shift_weight + out * (window_in + 1) + i] * m, 0); sum += IsNaNOrInf(matrix_w[shift_weight + out * (window_in + 1) + window_in] * m, 0); } } matrix_ig[shift_in + i] = Deactivation(sum, matrix_i[shift_in + i], activation); }
Zunächst iteriert der Kernel über alle Ausgabekanäle und berechnet den Beitrag jedes Elements der Eingabedaten zum Gesamtfehler. Die resultierenden Werte werden mit der Ableitung der Aktivierungsfunktion der Eingabedatenschicht angepasst und im entsprechenden Element des globalen Datenpuffers gespeichert.
Als Nächstes wird der zweite Schritt durchgeführt – die Propagation des Fehlergradienten zur Maske. Hier aggregieren wir die Beiträge aller Kanäle auf der Ebene der Ergebnisse dieses Experten, berücksichtigen dabei den Einfluss des aktuellen Experten auf das Ergebnis und erzeugen ein Feedback-Signal, das angibt, wie nützlich sich die Auswahl dieses Experten erwiesen hat.
float sum = 0; for(int out = 0; out < window_out; out++) { int shift_weight_loc = out * (window_in + 1) + shift_weight; float temp = matrix_w[shift_weight_loc + window_in]; for(int i = 0; i < window_in; i++) temp += IsNaNOrInf(matrix_i[shift_in + i], 0) * matrix_w[shift_weight_loc + i]; sum += IsNaNOrInf(temp * matrix_og[shift_out_var + shift_out + out], 0); } masks_g[shift_mask] = IsNaNOrInf(sum, 0); }
Selbst wenn die Maske binär ist, hilft dieser Wert dem Router, in Zukunft fundiertere Entscheidungen zu treffen, und kann bei Bedarf als Gewichtungswert für eine gewichtete Aktivierung interpretiert werden.
Dank dieses Mechanismus erhalten wir eine wirklich trainierbare Architektur, in der jede Routing-Entscheidung über Gradienten angepasst werden kann und ruhende Experten aktiviert werden können, wenn sie benötigt werden. Dadurch kann die spärlich aktivierte Expertenmischung effizient, flexibel und wirklich intelligent arbeiten.
Der nächste entscheidende Schritt besteht darin, die Modellparameter zu aktualisieren. An dieser Stelle lernt das Modell, indem es seine Gewichte auf der Grundlage des empfangenen Fehlersignals anpasst. Zur Umsetzung dieses Prozesses verwenden wir einen separaten OpenCL-Kernel namens UpdateWeightsMaskMultWinConvAdam, der auf die Besonderheiten einer spärlichen Expertenmischung zugeschnitten ist und die Optimierung mithilfe der Adam-Methode unterstützt.
Wie zuvor verlagern wir den Großteil der Berechnungen in den OpenCL-Kontext, da jedes am Training beteiligte Gewicht unabhängig verarbeitet werden kann.
__kernel void UpdateWeightsMaskMultWinConvAdam(__global float *matrix_w, __global const float *matrix_og, __global const float *matrix_i, __global const float *masks, __global float *matrix_m, __global float *matrix_v, const int windows_total, const int inputs, const int outputs, const float l, const float b1, const float b2 ) { const size_t id_in = get_global_id(0); const size_t id_out = get_global_id(1); const size_t id_v = get_global_id(2); const size_t window_in = get_global_size(0) / windows_total - 1; const size_t window_out = get_global_size(1); const size_t variables = get_global_size(2);
Die Arbeitsweise des Kernels ist entlang von drei Koordinaten organisiert: id_in entspricht der Eingabedimension und der Position innerhalb des Fensters, id_out entspricht dem Index des Ausgabefilters, und id_v entspricht der aktuellen Variablen im Batch. Diese dreidimensionale Struktur deckt alle Parameter der Experten vollständig ab und ermöglicht deren parallele Verarbeitung.
An dieser Stelle ist es wichtig zu beachten, dass wir bewusst von einer strukturellen Annahme ausgehen: Alle Experten arbeiten mit identischen Analysefenstern, und die Ergebnistensoren haben dieselbe Form. Das bedeutet, dass sich das gesamte System auf eine zweidimensionale Matrix reduzieren lässt: Eine Achse steht für die Anzahl der Filter, die andere für die über alle Experten hinweg zusammengefasste Gesamtzahl der Elemente in den Analysefenstern. Diese Vereinfachung ermöglicht es, Gewichte im Speicher effizient und kompakt zu adressieren, was insbesondere bei der Aktualisierung großer Parametermengen von entscheidender Bedeutung ist.
Im Kernel identifizieren wir zunächst jeden Rechenthread über alle Dimensionen des Aufgabenraums hinweg. Als Nächstes ermitteln wir den Offset in allen Datenpuffern für die zu analysierenden Elemente.
const int w_id = id_in / (window_in + 1); const int shift_in = id_in - w_id; const int step_in = window_in * windows_total; const int units = outputs / window_out; const int shift_in_var = id_v * inputs; const int shift_out_var = id_v * outputs; const int shift_mask_var = id_v * units * windows_total; const int shift_weight = ((id_v * windows_total + w_id) * window_out + id_out) * (window_in + 1) + id_in % (window_in + 1); const bool bias = (id_in % (window_in + 1) == window_in);
Als Nächstes durchlaufen wir nacheinander alle Token (Einheiten), mit denen die Experten gearbeitet haben. Für jedes dieser Fragmente überprüfen wir mithilfe einer Maske, ob der entsprechende Experte aktiv war.
float grad = 0; for(int u = 0; u < units; u++) { const int shift_in_loc = shift_in + u * step_in; if(shift_in < inputs) continue; float m = IsNaNOrInf(masks[shift_mask_var + u * windows_total + w_id], 0); if(m < FLT_EPSILON) continue; float inp = (bias ? 1 : IsNaNOrInf(matrix_i[shift_in_var + shift_in_loc], 0)); grad += IsNaNOrInf(inp * m * matrix_og[shift_out_var + u * window_out + id_out], 0); }
Befand sich ein Experte in einem bestimmten Schritt im Ruhemodus (die Maske liegt nahe bei Null), wenden wir keine Ressourcen für die Berechnung des Fehlergradienten auf und fahren mit dem nächsten Token fort. Dies ist eine der wichtigsten Eigenschaften des Sparse Learning.
Bei aktiven Experten berechnen wir den Fehlergradienten: Die Eingabedaten werden mit dem Fehlerwert auf der Ebene des Ausgabetensors (matrix_og) und mit der Maske multipliziert.
float mt = IsNaNOrInf(clamp(b1 * matrix_m[shift_weight] + (1 - b1) * grad, -1.0e5f, 1.0e5f), 0); float vt = IsNaNOrInf(clamp(b2 * matrix_v[shift_weight] + (1 - b2) * pow(grad, 2), 1.0e-6f, 1.0e6f), 1.0e-6f); float weight = clamp(matrix_w[shift_weight] + IsNaNOrInf(l * mt / sqrt(vt), 0), -MAX_WEIGHT, MAX_WEIGHT);
Als Nächstes führen wir einen Adam-Optimierungsschritt durch: Wir aktualisieren die Momente erster Ordnung (mt) und zweiter Ordnung (vt), normalisieren den Gradienten und passen den Gewichtswert an. Alle Aktualisierungen durchlaufen die Funktion Clamp, die die Gewichtswerte auf den zulässigen Bereich begrenzt.
Wir speichern die resultierenden Werte in globalen Puffern.
matrix_w[shift_weight] = weight; matrix_m[shift_weight] = mt; matrix_v[shift_weight] = vt; }
Somit wird jedes Gewicht streng entsprechend seinem Beitrag zum Endergebnis aktualisiert – und zwar nur dann, wenn es an den eigentlichen Berechnungen beteiligt war. Diese Art der Organisation gewährleistet eine hohe Recheneffizienz und ermöglicht zudem, hochspezialisierte Experten zu trainieren, die nicht nur in ungeeigneten Momenten im Ruhemodus bleiben, sondern auch aktiv lernen, wenn ihr Wissen wirklich benötigt wird.
Nachdem die Rechenlogik auf der OpenCL-Seite nun vollständig implementiert ist, gehen wir zum nächsten Schritt über – der Organisation des gesamten Prozesses innerhalb des Hauptprogramms. Hier wird das Objekt erstellt und konfiguriert, das für den Aufruf der entsprechenden Kernel und die Arbeit mit Masken zuständig ist. Diese Aufgabe übernimmt die spezialisierte Klasse CNeuronMaskMultiWinConv, die von CNeuronConvOCL abgeleitet ist. Die Struktur des neuen Objekts ist unten dargestellt.
class CNeuronMaskMultiWinConv : public CNeuronConvOCL { protected: uint iWindowsTotal; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *second)override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; public: CNeuronMaskMultiWinConv(void) {}; ~CNeuronMaskMultiWinConv(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint windows_total, uint window_out, uint units_count, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMaskMultiWinConv; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; };
Es bildet die Verbindung zwischen dem Modell und der Low-Level-Rechenpipeline. Seine Aufgabe besteht darin, die Daten ordnungsgemäß aufzubereiten, sie an den OpenCL-Kontext zu übergeben, den erforderlichen Kernel zu starten und die Ergebnisse abzurufen.
In der Struktur des Objekts CNeuronMaskMultiWinConv verzichten wir bewusst darauf, neue interne Komponenten zu deklarieren. Alles Erforderliche wird bereits von der übergeordneten Klasse CNeuronConvOCL bereitgestellt, und das reicht für die Organisation der Berechnungen auf der OpenCL-Seite völlig aus. Dieser Ansatz sorgt für eine übersichtliche Architektur ohne Redundanzen und gewährleistet gleichzeitig, dass alle Ressourcen zentral verwaltet werden.
Die Schicht wird in der überschriebenen Init-Methode initialisiert, wobei wir lediglich die Schlüsselparameter anpassen, ohne die Gesamtlogik zu beeinflussen.
bool CNeuronMaskMultiWinConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint windows_total, uint window_out, uint units_count, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch) { uint win = window * windows_total + MathMax(windows_total, 1) - 1; if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, win, win, window_out, units_count, variables, optimization_type, batch)) return false; iWindowsTotal = windows_total; iWindow = window; //--- return true; }
Konkret legen wir den Wert von iWindowsTotal fest, der die Anzahl der parallel arbeitenden Experten bestimmt. Dieser Wert wird bei der Erzeugung von Offsets und der Berechnung von Adressen innerhalb des Kernels verwendet.
Der Schwerpunkt der Initialisierungsmethode liegt auf der korrekten Berechnung der Breite des zu verarbeitenden Fensters. Da jeder Experte mit einem eigenen Fenster fester Länge arbeitet, fassen wir diese zu einem einzigen Eingaberaum zusammen. Dadurch ergibt sich die Größe des kombinierten Analysefensters über den Quelldaten. Diese Berechnung stellt sicher, dass die Fenstergröße auch in Grenzfällen nicht auf null sinkt.
Anschließend delegieren wir die Ausführung an die gleichnamige Methode der übergeordneten Klasse, übergeben die aktualisierten Parameter und kehren dann aus der Methode zurück.
In den Methoden für den Vorwärts- und Rückwärtsdurchlauf dieser Schicht ist keine eigenständige Rechenlogik implementiert; stattdessen bedient die Schicht lediglich die oben beschriebenen OpenCL-Kernels. Jede dieser Methoden ist dafür zuständig, Argumente vorzubereiten, Parameter zu übergeben und den entsprechenden Kernel zur Ausführung in die Warteschlange einzureihen.
Das Vorgehen ist in diesem Fall Standard: Wir übergeben Zeiger auf die Datenpuffer, einschließlich Masken, sowie weitere Parameter und rufen dann die OpenCL-Funktion mit den erforderlichen Arbeitsdimensionen auf. Ich denke, Sie sind mit diesem Verfahren bereits recht gut vertraut. Daher ist eine eingehende Analyse der einzelnen Methoden an dieser Stelle nicht erforderlich, und wir empfehlen, diese Methoden im Rahmen des Selbststudiums zu behandeln. Der vollständige Quellcode für die Klasse CNeuronMaskMultiWinConv und alle ihre Methoden ist im Anhang enthalten.
Sparse Mixture of Experts
Der nächste wichtige Schritt in unserer Arbeit besteht darin, ein Modul für eine spärlich aktivierte Expertenmischung aufzubauen. In unserer Implementierung ist die Architektur als Klasse CNeuronTimeMoESparseExperts definiert, die von der Basisklasse CNeuronBaseOCL erbt. Das Objekt umfasst alle wesentlichen Komponenten, die für den Start und die Koordination des Betriebs der spezialisierten und gemeinsam genutzten Experten erforderlich sind.
class CNeuronTimeMoESparseExperts : public CNeuronBaseOCL { protected: CNeuronSwiGLUOCL cExpertsIn; CNeuronSwiGLUOCL cSharedIn; CNeuronMaskMultiWinConv cExpertsOut; CNeuronConvOCL cSharedOut; CNeuronTopKGates cMasks; CNeuronConvOCL cSharedGates; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTimeMoESparseExperts(void) {}; ~CNeuronTimeMoESparseExperts(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint variables, uint experts, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTimeMoESparseExperts; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void TrainMode(bool flag) override; };
In der Struktur der Klasse CNeuronTimeMoESparseExperts lassen sich mehrere interne Objekte identifizieren, von denen jedes eine genau definierte Funktion innerhalb des Mechanismus der spärlichen Expertenmischung erfüllt. Wir werden uns nach und nach mit ihrer inneren Logik vertraut machen, während wir die Algorithmen entwickeln, die die Funktionsweise dieser Methoden beschreiben. An dieser Stelle ist es wichtig, auf folgende architektonische Entscheidung hinzuweisen: Alle internen Objekte werden statisch deklariert, ohne dynamische Speicherzuweisung. Dieser Ansatz vereinfacht nicht nur die Ressourcenverwaltung, sondern ermöglicht es auch, den Konstruktor und den Destruktor der Klasse leer zu lassen. Alle Initialisierungsvorgänge werden in der Methode Init implementiert.
bool CNeuronTimeMoESparseExperts::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint variables, uint experts, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) return false; SetActivationFunction(None);
Die Methodenparameter stellen eine Reihe von Schlüsselkonstanten bereit, mit denen wir die Architektur des zu erstellenden Objekts eindeutig definieren können. Hier wird alles festgelegt: die Größen der Eingabe- und Ausgabetoken, die Gesamtzahl der Experten und die durch die Top-K-Maske ausgewählte Anzahl, die Art der verwendeten Optimierung sowie die Anzahl der Variablen und Rechenblöcke.
Einige dieser Parameter werden an die gleichnamige Methode in der übergeordneten Klasse übergeben. Dort wurden bereits die grundlegende Validierung der Architektureinstellungen und die Initialisierung globaler Schnittstellen, einschließlich des OpenCL-Kontexts, implementiert. Auf diese Weise schaffen wir eine klare hierarchische Struktur, in der jede Ebene für ihren eigenen Zuständigkeitsbereich verantwortlich ist und alle Elemente der Architektur zu einem einzigen, zusammenhängenden Ganzen verschmelzen.
Nachdem die Initialisierung auf der Ebene der übergeordneten Klasse erfolgreich abgeschlossen wurde, fahren wir mit der Konfiguration der internen Komponenten fort – hier beginnt die Struktur der Expertenmischung selbst Gestalt anzunehmen.
Das erste Objekt in dieser Kette ist cExpertsIn, das die SwiGLU-Funktionalität implementiert. Es dient als erste Stufe der Eingabedatenverarbeitung – eine Art Vorfilter, der nützliche Signale verstärkt und ein Embedding erzeugt, das an jeden Experten gesendet wird. Die Anzahl der Filter in dieser Schicht skaliert proportional zur Anzahl der Experten, sodass jeder von ihnen eine ausreichend aussagekräftige Repräsentation der Aufgabe erhält.
int index = 0; if(!cExpertsIn.Init(0, index, OpenCL, window, window, window_out * experts, units_count, variables, optimization, iBatch)) return false; index++; if(!cSharedIn.Init(0, index, OpenCL, window, window, window_out, units_count, variables, optimization, iBatch)) return false;
Auf dieselbe Weise initialisieren wir die Komponente cSharedIn, die als erste Verarbeitungsschicht für den gemeinsam genutzten Experten dient. Im Gegensatz zu spezialisierten Experten ist es hier nicht erforderlich, die Dimensionalität der Filter zu skalieren – im Gegenteil, sie ist auf einem Niveau festgelegt, das dem eines einzelnen Experten entspricht. Das liegt daran, dass der gemeinsam genutzte Experte den gesamten Aufgabenraum abdeckt und seine Rolle nicht darin besteht, mit den anderen zu konkurrieren, sondern eine universelle Perspektive zu bieten – eine Art Baseline für die Entscheidungsfindung.
Der Einsatz von SwiGLU in dieser Funktion sorgt für eine nichtlineare Selektivität und verstärkt so die Unterscheidung zwischen Token, die für einen bestimmten Experten potenziell relevant sind.
Als Nächstes gehen wir dazu über, die zweite Verarbeitungsschicht sowohl für die spezialisierten Experten als auch für den gemeinsam genutzten Experten zu bilden. Hier folgt die Architektur strikt der zuvor festgelegten Logik: Jeder Block erfüllt seine spezifische Funktion innerhalb des gesamten Mechanismus der spärlichen Expertenmischung.
Für die Mischung spezialisierter Experten verwenden wir das zuvor erstellte Modul CNeuronMaskMultiWinConv, das die Faltung mit maskierten Fenstern implementiert. Diese Komponente filtert die analysierten Features anhand der individuellen Maske jedes Experten und ermöglicht so eine gezielte Aktivierung sowie eine klare Aufgabenteilung unter den Experten.
index++; if(!cExpertsOut.Init(0, index, OpenCL, window_out, experts, window, units_count, variables, optimization, iBatch)) return false; cExpertsOut.SetActivationFunction(None); index++; if(!cSharedOut.Init(0, index, OpenCL, window_out, window_out, window, units_count, variables, optimization, iBatch)) return false; cSharedOut.SetActivationFunction(None);
Für den gemeinsam genutzten Experten verwenden wir wiederum die Standard-Faltungsschicht CNeuronConvOCL.
Als Objekt, das die Maske der aktiven Experten erstellt, verwenden wir das CNeuronTopKGates-Modul, das wir bereits aus dem DUET-Framework kennen. Diese Komponente spielt eine zentrale Rolle im Sparsifizierungsmechanismus: Sie analysiert die Eingabe-Token und wählt eine streng begrenzte Anzahl der relevantesten Experten aus.
Mit anderen Worten: CNeuronTopKGates erzeugt eine Aktivierungsmaske nach dem Top-K-Prinzip, wobei irrelevante Kanäle vollständig auf Null gesetzt werden. Dieser Ansatz reduziert den Rechenaufwand erheblich, ohne dabei die hohe Ausdruckskraft des Modells zu beeinträchtigen. Die Anzahl der aktiven Experten (topK) wird durch einen Parameter festgelegt und lässt sich problemlos an die Besonderheiten einer bestimmten Aufgabe anpassen.
index++; if(!cMasks.Init(0, index, OpenCL, window, units_count, experts, topK, optimization, iBatch)) return false;
Es ist wichtig zu verstehen, dass das CNeuronTopKGates-Objekt nicht einfach nur eine binäre Maske zurückgibt. Es erzeugt außerdem eine normalisierte Wahrscheinlichkeitsverteilung über die ausgewählten Kanäle. Dadurch lässt sich der Beitrag jedes Experten zum Endergebnis flexibel anpassen. Aktive Experten tragen mit unterschiedlicher Konfidenz zu den Berechnungen bei, und diese Wahrscheinlichkeiten spiegeln sich explizit in den Gewichten der endgültigen Maske wider.
Dank dieses Ansatzes gewinnt die Architektur nicht nur an Kompaktheit und Recheneffizienz, sondern auch an Robustheit gegenüber Overfitting. Jeder Experte wird ausschließlich dann eingesetzt, wenn es angebracht ist, und nicht willkürlich – was besonders bei der Arbeit mit verrauschten oder multimodalen Zeitreihen von Bedeutung ist.
Die Struktur des Objekts wird durch das Modul für die Gates des gemeinsam genutzten Experten – cSharedGates – vervollständigt. Sein Zweck besteht darin, zu bestimmen, inwieweit der gemeinsam genutzte Experte zur endgültigen Ausgabe des Modells beiträgt. Dazu verwenden wir eine Standard-Faltungsschicht, die so konfiguriert ist, dass sie räumlich-zeitliche Muster extrahiert. Im Gegensatz zu der für einzelne Experten verwendeten Maske kommt hier eine Sigmoid-Aktivierungsfunktion zum Einsatz, die kontinuierliche Werte im Bereich von 0 bis 1 liefert.
index++; if(!cSharedGates.Init(0, index, OpenCL, window, window, window, units_count, variables, optimization, iBatch)) return false; cSharedGates.SetActivationFunction(SIGMOID); //--- return true; }
Dieser Ansatz führt nicht zu einer binären Ein/Aus-Entscheidung, sondern ermöglicht eine flexible Skalierung des Beitrags des gemeinsam genutzten Experten je nach aktuellem Kontext. Die Sigmoid-Funktion wirkt wie ein flexibler Dämpfer: Ist das Vertrauen in das allgemeine Muster hoch, nähert sich der Wert eins an; ist das Signal schwach, unterdrückt die entsprechende Maske die Ausgabe. All dies ermöglicht eine präzise Steuerung der Wechselwirkung zwischen den lokalisierten und den generalisierenden Komponenten der Architektur.
Nachdem alle Iterationen erfolgreich abgeschlossen wurden, geben wir den resultierenden booleschen Status an das aufrufende Programm zurück und verlassen die Methode.
Anschließend gehen wir zur nächsten wichtigen Phase über – der Organisation des Vorwärtsdurchlaufs in der Methode feedForward. An dieser Stelle wird die Funktionsweise des Moduls der spärlichen Expertenmischung in vollem Umfang deutlich.
bool CNeuronTimeMoESparseExperts::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cExpertsIn.FeedForward(NeuronOCL)) return false;
Zunächst führen wir einen Vorwärtsdurchlauf durch die cExpertsIn-Schicht durch, die das zu analysierende Signal in den Expertenraum transformiert, indem sie es entsprechend der Anzahl der spezialisierten Verarbeitungspfade skaliert. Parallel dazu wird die Schicht cSharedIn aufgerufen; diese Schicht stellt die erste Verarbeitungsstufe eines vielseitigeren gemeinsam genutzten Experten dar.
if(!cSharedIn.FeedForward(NeuronOCL)) return false;
Als Nächstes werden zwei Routing-Zweige aktiviert. Die Schicht cSharedGates berechnet die Beteiligungsmaske des gemeinsam genutzten Experten: Sie verwendet eine Sigmoid-Aktivierungsfunktion, um eine abgestufte Relevanzmaske zu erzeugen. Das cMasks-Objekt wiederum ist ein spezialisiertes Modul, das die relevantesten Experten auswählt, um eine diskrete Top-K-Maske zu bilden. Es schließt nicht nur inaktive Kanäle aus, sondern liefert auch eine normalisierte Verteilung der Gewichte über die ausgewählten Pfade.
if(!cSharedGates.FeedForward(NeuronOCL)) return false; if(!cMasks.FeedForward(NeuronOCL)) return false;
In dieser Phase beginnt die Ergebniserzeugung. Das Modul cExpertsOut wird verwendet, um eine Faltung über den maskierten Expertenblock durchzuführen: Jeder aktive Filter verfügt über eigene Gewichte, und die gesamte Verarbeitung erfolgt auf der Grundlage der zuvor erstellten Maske.
if(!cExpertsOut.FeedForward(cExpertsIn.AsObject(), cMasks.getOutput())) return false;
Für den gemeinsam genutzten Experten wird mithilfe von cSharedOut eine Standard-Faltung durchgeführt; anschließend wird die Ausgabe entsprechend einer zuvor von cSharedGates berechneten Relevanzmaske weiter skaliert. Die Skalierung erfolgt durch elementweise Multiplikation.
if(!cSharedOut.FeedForward(cSharedIn.AsObject())) return false; if(!ElementMult(cSharedOut.getOutput(), cSharedGates.getOutput(), cSharedOut.getPrevOutput())) return false;
Nachdem die Ausgabe der spärlichen Expertenmischung und die durch die Relevanzmaske skalierte Ausgabe des gemeinsam genutzten Experten vorliegen, werden die beiden Informationsströme zusammengeführt. In dieser Phase werden die Daten summiert und in einen Zwischendatenpuffer geschrieben.
const int window = (int)cSharedGates.GetWindow(); if(!SumAndNormilize(cExpertsOut.getOutput(), cSharedOut.getPrevOutput(), PrevOutput, window, false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(NeuronOCL.getOutput(), PrevOutput, Output, window, true, 0, 0, 0, 1)) return false; //--- return true; }
Dies ist jedoch nur der erste Teil der letzten Phase des Vorwärtsdurchlaufs. Anschließend werden Residualverbindungen – also die gespeicherten Ausgaben der vorherigen Schicht der neuronalen Architektur – zu den erhaltenen Werten addiert. Diese Technik bewahrt wichtige Informationen aus dem Eingangssignal und verbessert die Konvergenz des Modells durch Stabilisierung der Gradienten.
Nachdem alle Komponenten kombiniert wurden, werden die Ergebnisse innerhalb jedes Tokens (entlang des Analysefensters) normalisiert. Durch diesen Vorgang werden die Daten auf einen einheitlichen Maßstab gebracht, wodurch die korrekte Interpretation des Ausgabetensors gewährleistet wird. Das Ergebnis wird im globalen Schnittstellenpuffer für Ergebnisse gespeichert.
Wie Sie sehen können, weist der Algorithmus für den Vorwärtsdurchlauf in unserem Modul eine stark verzweigte Struktur auf. Die Eingabedaten werden gleichzeitig fünf verschiedenen Informationsströmen zugeführt, was die Flexibilität und Anpassungsfähigkeit des Modells erheblich erhöht. Eine solche Mehrkanalarchitektur bringt jedoch während der Backpropagation des Fehlers gewisse Herausforderungen mit sich.
In der Methode calcInputGradients werden die Fehlergradienten sorgfältig auf alle internen Komponenten verteilt, entsprechend ihrem Beitrag zum Endergebnis. Das ähnelt meisterhaftem Dirigieren, bei dem jedes Instrument in vollkommener Harmonie mit dem Orchester spielt.
bool CNeuronTimeMoESparseExperts::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
Zunächst werden die Verfahren zur Verteilung des Fehlergradienten auf die einzelnen Informationsströme aktiviert:
- für die Ausgabe der Schicht der spärlichen Expertenmischung (cExpertsOut), wobei die Ableitungen der Aktivierungsfunktionen auf die Gradienten angewendet werden;
- für den gemeinsam genutzten Experten und die Gates (cSharedOut und cSharedGates), wobei die Wechselwirkung zwischen Ausgangswerten und Masken berücksichtigt wird und Gradientenprodukte unter Berücksichtigung der Aktivierungsfunktionen angewendet werden.
if(!DeActivation(cExpertsOut.getOutput(), cExpertsOut.getGradient(), Gradient, cExpertsOut.Activation())) return false; if(!ElementMultGrad(cSharedOut.getOutput(), cSharedOut.getGradient(), cSharedGates.getOutput(), cSharedGates.getGradient(), Gradient, cSharedOut.Activation(), cSharedGates.Activation())) return false;
Darauf folgt ein rekursiver Aufruf zur Berechnung der verborgenen Gradienten in den internen Objekten – dies ermöglicht es, den Beitrag jeder Komponente Schritt für Schritt aufzuschlüsseln, beginnend bei den ersten Expertenschichten (cExpertsIn, cSharedIn) bis hinunter zur Ebene der Rohdaten, wodurch eine gründliche und korrekte Berechnung gewährleistet wird.
if(!cExpertsIn.calcHiddenGradients(cExpertsOut.AsObject(), cMasks.getOutput(), cMasks.getGradient(), (ENUM_ACTIVATION)cMasks.Activation())) return false; if(!cSharedIn.calcHiddenGradients(cSharedOut.AsObject())) return false; //--- if(!NeuronOCL.calcHiddenGradients(cExpertsIn.AsObject())) return false;
Besonderes Augenmerk wird auf die Akkumulation von Gradienten in Zwischenpuffern gelegt. Hier werden die Gradienten über die Token im Fenster summiert, wodurch eine korrekte Ausrichtung der Signale der verschiedenen am Prozess beteiligten Komponenten gewährleistet wird.
if(!DeActivation(NeuronOCL.getOutput(), PrevOutput, Gradient, NeuronOCL.Activation())) return false; const int window = (int)cSharedGates.GetWindow(); if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1)) return false; if(!NeuronOCL.calcHiddenGradients(cSharedIn.AsObject())) return false; if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1)) return false; if(!NeuronOCL.calcHiddenGradients(cMasks.AsObject())) return false; if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), PrevOutput, window, false, 0, 0, 0, 1)) return false; if(!NeuronOCL.calcHiddenGradients(cSharedGates.AsObject())) return false; if(!SumAndNormilize(PrevOutput, NeuronOCL.getGradient(), NeuronOCL.getGradient(), window, false, 0, 0, 0, 1)) return false; //--- return true; }
Letztendlich balanciert die Methode calcInputGradients den komplexen Informationsfluss sorgfältig aus und gewährleistet ein effizientes Training des gesamten Moduls der spärlichen Expertenmischung, wodurch diese Architektur nicht nur leistungsstark, sondern auch robust gegenüber Fehlern und Overfitting ist.
Die Methode zur Aktualisierung der Modellparameter folgt dem klassischen Ansatz: Die Steuerung wird an interne Komponenten delegiert. Jede davon implementiert auf der Grundlage der ermittelten Gradienten eine eigene Strategie zur Anpassung der Gewichte. Im Rumpf der Methode updateInputWeights werden die entsprechenden Prozeduren einfach nacheinander aufgerufen. Um Wiederholungen zu vermeiden, empfehlen wir Ihnen daher, diesen Abschnitt selbstständig durchzuarbeiten. Der vollständige Quellcode für die Klasse CNeuronTimeMoESparseExperts, einschließlich der Implementierungen aller Methoden, ist im Anhang verfügbar.
Fazit
In diesem Artikel haben wir die Implementierung des Moduls für maskierte Faltung sowie die Architektur des Blocks der spärlichen Expertenmischung eingehend untersucht, die für Aufgaben der Zeitreihenverarbeitung in der OpenCL-Umgebung angepasst wurden. Es wurden die wichtigsten Aspekte der Konfiguration geräteseitiger Berechnungen und der Organisation der Interaktion mit dem Hauptprogramm untersucht. Besonderes Augenmerk wurde auf die korrekte Verteilung des Gradienten im Zusammenhang mit einem verzweigten Informationsfluss gelegt.
Im nächsten Teil werden wir die Architektur weiterentwickeln und uns auf die Erstellung und das Training von Modellen konzentrieren, die diese Struktur als Teil komplexerer neuronaler Systeme nutzen.
Literaturverzeichnis
- Time-MoE: Foundation Models für Zeitreihen im Milliardenmaßstab mit Mixture-of-Experts
- Weitere Artikel dieser Reihe
In diesem Artikel verwendete Programme
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | Study.mq5 | Expert Advisor | Expert Advisor für das Offline-Modelltraining |
| 2 | StudyOnline.mq5 | Expert Advisor | Expert Advisor für Online-Modelltraining |
| 3 | Test.mq5 | Expert Advisor | Expert Advisor für Modelltests |
| 4 | Trajectory.mqh | Klassenbibliothek | Struktur zur Beschreibung des Systemzustands und der Modellarchitektur |
| 5 | NeuroNet.mqh | Klassenbibliothek | Klassenbibliothek zum Erstellen neuronaler Netze |
| 6 | NeuroNet.cl | Bibliothek | Codebibliothek für das OpenCL-Programm |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/18527
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Neuronale Netze im Trading: Adaptive periodische Segmentierung (Fazit)
Neuronale Netze im Handel: Adaptive periodische Segmentierung (LightGTS)
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Neuronale Netze im Trading: Adaptive periodische Segmentierung (Token-Generierung)
- 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.