Tensoren für C #. Matrizen, Vektoren, benutzerdefinierter Typ und relativ schnell

Hallo!



Ich brauchte irgendwie Tensoren (Matrixerweiterungen) für meine Projektion. Googelte, fand eine Reihe aller Arten von Bibliotheken, überall und überall, und was benötigt wird - nein. Ich musste den Fünf-Tage-Plan umsetzen und umsetzen, was nötig war. Ein kurzer Hinweis zur Arbeit mit Tensoren und Optimierungstricks.







Was brauchen wir also?



  1. N-dimensionale Matrizen (Tensoren)
  2. Implementierung eines grundlegenden Satzes von Methoden zum Arbeiten mit einem Tensor als Datenstruktur
  3. Implementierung eines grundlegenden Satzes mathematischer Funktionen (für Matrizen und Vektoren)
  4. Generische Typen, dh beliebige. Und benutzerdefinierte Operatoren


Und was wurde schon vor uns geschrieben?
, Towel , :



  1. ,
  2. Transpose — «» , O(V), V — «».
  3. , . , , , . , ( , )


System.Numerics.Tensor . , , , . , , .



MathSharp, NumSharp, Torch.Net, TensorFlow, /ML-, .



Elementspeicherung, Transposition, Subtensor



Die Elemente werden in einem eindimensionalen Array gespeichert. Um ein Element aus einer Reihe von Indizes zu erhalten, multiplizieren wir die Indizes mit bestimmten Koeffizienten und addieren sie. Nehmen wir nämlich an, wir haben einen Tensor [3 x 4 x 5]. Dann müssen wir ein Array von drei Elementen bilden - Blöcke (er selbst hat den Namen erfunden). Dann ist das letzte Element 1, das vorletzte ist 5 und das erste Element ist 5 * 4 = 20. Das heißt, Blöcke = [20, 5, 1]. Wenn Sie beispielsweise über den Index [1, 0, 4] zugreifen, sieht der Index im Array wie folgt aus: 20 * 1 + 5 * 0 + 4 * 1 = 24. Bisher ist alles klar



Transponieren



... das heißt, die Reihenfolge der Achsen ändern. Der dumme und einfache Weg besteht darin, ein neues Array zu erstellen und die Elemente dort in eine neue Reihenfolge zu bringen. Es ist jedoch häufig zweckmäßig, zu transponieren, mit der gewünschten Achsenreihenfolge zu arbeiten und dann die Achsenreihenfolge zurück zu ändern. In diesem Fall können Sie das lineare Array (LM) selbst natürlich nicht ändern. Wenn Sie sich auf bestimmte Indizes beziehen, ändern wir einfach die Reihenfolge.



Betrachten Sie die Funktion:



private int GetFlattenedIndexSilent(int[] indices)
{
    var res = 0;
    for (int i = 0; i < indices.Length; i++)
        res += indices[i] * blocks[i];
    return res + LinOffset;
}


Wie Sie sehen können, wird beim Vertauschen der Blöcke die Sichtbarkeit der Wechselachsen erstellt. Deshalb werden wir folgendes schreiben:



public void Transpose(int axis1, int axis2)
{
    (blocks[axis1], blocks[axis2]) = (blocks[axis2], blocks[axis1]);
    (Shape[axis1], Shape[axis2]) = (Shape[axis2], Shape[axis1]);
}


Wir ändern nur die Anzahl und Länge der Achsen an einigen Stellen.



Subtensor



Der Subtensor eines N-dimensionalen Tensors ist ein M-dimensionaler Tensor (M <N), der Teil des Originals ist. Beispielsweise ist das Nullelement des Formtensors [2 x 3 x 4] der Formtensor [3 x 4]. Wir bekommen es nur durch Verschiebung.



Stellen wir uns vor, wir bekommen einen Subtensor bei Index n . Dann wird das erste Element ist die n * Blöcke [0] + 0 * Blöcke [1] + 0 * Blöcke [2] + ... . Das heißt, die Verschiebung beträgt n * Blöcke [0] . Dementsprechend erinnern wir uns, ohne den ursprünglichen Tensor zu kopieren, an die Verschiebung , erstellen einen neuen Tensor mit einem Link zu unseren Daten, aber mit einer Verschiebung. Außerdem müssen Sie das Element aus Blöcken, nämlich den Elementblöcken [0], wegwerfen, da dies die erste Achse ist und keine Aufrufe erfolgt.

Andere Kompositionsoperationen



Alle anderen folgen bereits daraus.



  1. SetSubtensor leitet die Elemente an den gewünschten Subtensor weiter
  2. Concat erstellt einen neuen Tensor und leitet dort Elemente von zwei weiter (während die Länge der ersten Achse die Summe der Längen der Achsen der beiden Tensoren ist).
  3. Stapel gruppiert eine Anzahl von Tensoren zu einem mit einer zusätzlichen Achse (z. B. Stapel ([3 x 4], [3 x 4]) -> [2 x 3 x 4])
  4. Slice gibt Stack von bestimmten Subtenzern zurück


Alle von mir definierten Kompositionsoperationen finden Sie hier .



Mathematische Operationen



Hier ist schon alles einfach.



1) Punktweise Operationen (dh für zwei Tensoren werden Operationen an einem Paar entsprechender Elemente ausgeführt (dh mit denselben Koordinaten)). Die Implementierung ist hier (eine Erklärung, warum solch ein hässlicher Code unten ist).



2) Operationen an Matrizen. Produkt, Inversion und andere einfache Operationen erfordern meines Erachtens keine Erklärung. Obwohl es eine Geschichte über die Determinante zu erzählen gibt.



Die Geschichte des Determinanten
, . — , , O(N!). 3x3 ( ).



, . -: float , int .



, , . , .



, . , , , , . . SafeDivisionWrapper .



: . , . SafeDivision ( , ).



3) Operationen an Vetora (Punkt- und Kreuzprodukt).



Optimierung



Vorlagen?


In C # sind keine Vorlagen vorhanden. Daher müssen Sie Krücken verwenden. Einige Leute haben sich eine dynamische Kompilierung in einen Delegaten ausgedacht, zum Beispiel implementiert sie die Summe zweier Typen.



Ich wollte jedoch eine benutzerdefinierte, also startete ich eine Schnittstelle, von der der Benutzer die Struktur erben muss. In diesem Fall wird das Grundelement im linearen Array selbst gespeichert, und die Additions-, Differenz- und andere Funktionen werden als bezeichnet



var c = default(TWrapper).Addition(a, b);


Welches ist vor Ihrer Methode inliniert. Ein Beispiel für die Implementierung einer solchen Struktur .



Indizierung


Obwohl es logisch erscheint, Parameter im Indexer zu verwenden, ist dies ungefähr so:



public T this[params int[] indices]


Tatsächlich erstellt jeder Aufruf ein Array, sodass Sie viele Überladungen erstellen müssen . Das gleiche passiert mit anderen Funktionen, die mit Indizes arbeiten.



Ausnahmen


Ich habe auch alle Ausnahmen und Überprüfungen in den Block #if ALLOW_EXCEPTIONS verschoben, falls Sie ihn definitiv schnell benötigen und es definitiv keine Probleme mit Indizes gibt. Die Leistung steigt leicht an.



Tatsächlich ist dies nicht nur eine Mikrooptimierung, die in Bezug auf die Sicherheit viel kostet. Beispielsweise wird eine Abfrage an Ihren Tensor gesendet, in der Sie aus eigenen Gründen bereits die Richtigkeit der Daten überprüft haben. Warum brauchen Sie dann noch einen Scheck? Und sie sind nicht frei, besonders wenn wir selbst unnötige arithmetische Operationen mit ganzen Zahlen speichern.



Multithreading


Danke Billy, es stellte sich als sehr einfach heraus und wurde über Parallel.For implementiert.



Obwohl Multithreading kein Allheilmittel ist und sorgfältig aktiviert werden muss. Ich habe einen Benchmark für die punktweise Addition von Tensoren auf dem i7-7700HQ durchgeführt:







Dabei zeigt die Y-Achse die Zeit (in Mikrosekunden) an, um eine einzelne Operation mit zwei ganzzahligen Tensoren einer bestimmten Größe (Größe der X-Achse) auszuführen.



Das heißt, es gibt eine bestimmte Schwelle, ab der Multithreading sinnvoll ist. Um nicht nachdenken zu müssen, habe ich das Threading.Auto- Flag erstellt und die Konstanten dumm hartcodiert, beginnend mit einem Volume, das dem Multithreading entspricht (gibt es eine intelligentere automatische Methode?).



Gleichzeitig ist die Bibliothek immer noch nicht schneller als Spielmatrizen oder noch mehr diejenigen, die auf CUDA berechnet werden. Und warum? Diese wurden bereits geschrieben, aber unsere Hauptsache ist ein benutzerdefinierter Typ.



So ist das



Hier ist eine kurze Notiz, danke fürs Lesen. Der Link zum Github des Projekts ist hier . Hauptnutzer ist die AngouriMath-Bibliothek für symbolische Algebra .



Ein wenig über unsere Tensoren in AngouriMath
, AM- Entity, . ,



var t = MathS.Matrices.Matrix(3, 3,
              "A", "B", "C",   //      ,     "A * 3",  "sqrt(sin(x) + 5)"
              "D", "E", "F",
              "G", "H", "J");
Console.WriteLine(t.Determinant().Simplify());






A * (E * J - F * H) + C * (D * H - E * G) - B * (D * J - F * G)








All Articles