J'avais en quelque sorte besoin de tenseurs (expansions de matrice) pour ma projection. Googlé, trouvé un certain nombre de toutes sortes de bibliothèques, partout dans la brousse, et ce dont vous avez besoin - non. J'ai dû mettre en œuvre le plan de cinq jours et mettre en œuvre ce qui était nécessaire. Une brève note sur l'utilisation des tenseurs et des astuces d'optimisation.
Alors, de quoi avons-nous besoin?
- Matrices à N dimensions (tenseurs)
- Implémentation d'un ensemble de méthodes de base pour travailler avec un tenseur comme structure de données
- Implémentation d'un ensemble de fonctions mathématiques de base (pour les matrices et les vecteurs)
- Types génériques, c'est-à-dire tout. Et opérateurs personnalisés
Et qu'est-ce qui a déjà été écrit avant nous?
, Towel , :
System.Numerics.Tensor . , , , . , , .
MathSharp, NumSharp, Torch.Net, TensorFlow, /ML-, .
- ,
- Transpose — «» , O(V), V — «».
, . , , , ., ( , )
System.Numerics.Tensor . , , , . , , .
MathSharp, NumSharp, Torch.Net, TensorFlow, /ML-, .
Stockage d'éléments, transposition, sous-capteur
Les éléments seront stockés dans un tableau unidimensionnel. Pour obtenir un élément d'un ensemble d'indices, nous multiplierons les indices par certains coefficients et ajouterons. À savoir, supposons que nous ayons un tenseur [3 x 4 x 5]. Ensuite, nous devons former un tableau de trois éléments - des blocs (il a lui-même trouvé le nom). Alors le dernier élément est 1, l'avant-dernier est 5 et le premier élément est 5 * 4 = 20. Autrement dit, blocs = [20, 5, 1]. Par exemple, lors de l'accès par index [1, 0, 4], l'index du tableau ressemblera à 20 * 1 + 5 * 0 + 4 * 1 = 24. Jusqu'à présent, tout est clair
Transposer
... c'est-à-dire changer l'ordre des axes. Le moyen stupide et simple est de créer un nouveau tableau et de mettre les éléments dans un nouvel ordre. Mais il est souvent pratique de transposer, de travailler avec l'ordre des axes souhaité, puis de modifier l'ordre des axes. Bien sûr, dans ce cas, vous ne pouvez pas changer le tableau linéaire (LM) lui - même , et en faisant référence à certains indices, nous changerons simplement l'ordre.
Considérez la fonction:
private int GetFlattenedIndexSilent(int[] indices)
{
var res = 0;
for (int i = 0; i < indices.Length; i++)
res += indices[i] * blocks[i];
return res + LinOffset;
}
Comme vous pouvez le voir, si vous permutez les blocs , la visibilité des axes de permutation sera créée. Par conséquent , nous écrirons ceci:
public void Transpose(int axis1, int axis2)
{
(blocks[axis1], blocks[axis2]) = (blocks[axis2], blocks[axis1]);
(Shape[axis1], Shape[axis2]) = (Shape[axis2], Shape[axis1]);
}
Nous changeons simplement les nombres et les longueurs des axes par endroits.
Sous-capteur
Le sous-capteur d'un tenseur à N dimensions est un tenseur à M dimensions (M <N), qui fait partie de l'original. Par exemple, l'élément zéro du tenseur de forme [2 x 3 x 4] est le tenseur de forme [3 x 4]. Nous l'obtenons simplement en changeant.
Imaginons que nous obtenions un sous-capteur à l'index n . Ensuite, le premier élément est le n * blocs [0] + 0 * blocs [1] + 0 * blocs [2] + ... . Autrement dit, le décalage est de n * blocs [0] . En conséquence, sans copier le tenseur d'origine, nous nous souvenons du décalage , créons un nouveau tenseur avec un lien vers nos données, mais avec un décalage. Et vous devrez également jeter l'élément des blocs, à savoir les blocs d'éléments [0], car c'est le premier axe, il n'y aura pas d'appel.
Autres opérations de composition
Tous les autres en découlent déjà.
- SetSubtensor transmettra les éléments au sous-capteur souhaité
- Concat crée un nouveau tenseur, et là il fera avancer des éléments de deux (tandis que la longueur du premier axe est la somme des longueurs des axes des deux tenseurs)
- La pile regroupe un certain nombre de tenseurs en un avec un axe supplémentaire (par exemple, pile ([3 x 4], [3 x 4]) -> [2 x 3 x 4])
- Slice retourne Stack de certains sous-titreurs
Toutes les opérations de composition que j'ai définies se trouvent ici .
Opérations mathématiques
Tout est déjà simple ici.
1) Opérations point par point (c'est-à-dire que pour deux tenseurs, les opérations sont effectuées sur une paire d'éléments correspondants (c'est-à-dire avec les mêmes coordonnées)). L'implémentation est ici (une explication de pourquoi un code aussi laid est ci-dessous).
2) Opérations sur les matrices. Produit, inversion et autres opérations simples, il me semble, ne nécessitent aucune explication. Bien qu'il y ait une histoire à raconter sur le déterminant.
Une histoire du déterminant
3) Opérations sur les vétérans (dot et cross product).
Optimisation
Modèles?
Il n'y a pas de modèles en C # . Par conséquent, vous devez utiliser des béquilles. Certaines personnes ont mis au point une compilation dynamique dans un délégué, par exemple , elle implémente la somme de deux types.
Cependant, je voulais une personnalisation, j'ai donc lancé une interface dont l'utilisateur doit hériter de la structure. Dans ce cas, la primitive est stockée dans le tableau linéaire lui-même et les fonctions d'addition, de différence et autres sont appelées comme
var c = default(TWrapper).Addition(a, b);
Qui est en ligne avant votre méthode. Un exemple de mise en œuvre d'une telle structure .
Indexage
De plus, bien qu'il semble logique d'utiliser des paramètres dans l'indexeur, c'est-à-dire quelque chose comme ceci:
public T this[params int[] indices]
En fait, chaque appel créera un tableau, vous devrez donc créer beaucoup de surcharges . La même chose se produit avec d'autres fonctions qui fonctionnent avec des index.
Des exceptions
J'ai également conduit toutes les exceptions et vérifications dans le bloc #if ALLOW_EXCEPTIONS au cas où vous en auriez vraiment besoin rapidement, mais il n'y a certainement aucun problème avec les index. Il y a une légère augmentation des performances.
En fait, il ne s'agit pas que de micro-optimisation, qui coûtera cher en termes de sécurité. Par exemple, une requête est envoyée à votre tenseur, dans laquelle vous avez déjà, pour vos propres raisons, vérifié l'exactitude des données. Alors pourquoi avez-vous besoin d'un autre chèque? Et ils ne sont pas gratuits, surtout si nous sauvegardons même les opérations arithmétiques inutiles avec des entiers.
Multithreading
Merci Billy, cela s'est avéré très simple et implémenté via Parallel.For.
Bien que le multithreading ne soit pas une panacée, il doit être activé avec précaution. J'ai exécuté un benchmark pour l'ajout ponctuel de tenseurs sur le i7-7700HQ:
où l'axe Y montre le temps (en microsecondes) pour effectuer une opération avec deux tenseurs entiers d'une certaine taille (taille de l'axe X).
Autrement dit, il existe un certain seuil à partir duquel le multithreading a du sens. Afin de ne pas avoir à réfléchir, j'ai créé le drapeau Threading.Auto et bêtement coder en dur les constantes, en commençant par un volume égal auquel vous pouvez activer le multithreading (existe-t-il une méthode automatique plus intelligente?).
Dans le même temps, la bibliothèque n'est toujours pas plus rapide que les matrices de jeu, ou encore plus celles qui sont calculées sur CUDA. Pourquoi? Ceux-ci ont déjà été écrits, mais notre principal élément est un type personnalisé.
Comme ça
Voici une courte note, merci d'avoir lu. Le lien vers le github du projet est ici . Et son utilisateur principal est la bibliothèque d' algèbre symbolique AngouriMath .
Un peu sur nos tenseurs à 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)