Tensores para C #. Matrices, vectores, tipos personalizados y relativamente rápido

¡Hola!



De alguna manera necesitaba tensores (expansiones de matriz) para mi proyección. Busqué en Google, encontré una serie de todo tipo de bibliotecas, por todas partes, y lo que necesita, no. Tuve que implementar el plan de cinco días e implementar lo que se necesitaba. Una breve nota sobre cómo trabajar con tensores y trucos de optimización.







Entonces, ¿qué necesitamos?



  1. Matrices N-dimensionales (tensores)
  2. Implementación de un conjunto básico de métodos para trabajar con un tensor como estructura de datos.
  3. Implementación de un conjunto básico de funciones matemáticas (para matrices y vectores)
  4. Tipos genéricos, es decir, cualquiera. Y operadores personalizados


¿Y qué se ha escrito ya antes que nosotros?
, Towel , :



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


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



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



Almacenamiento de elementos, transposición, subtensor



Los elementos se almacenarán en una matriz unidimensional. Para obtener un elemento de un conjunto de índices, multiplicaremos los índices por ciertos coeficientes y sumaremos. Es decir, supongamos que tenemos un tensor [3 x 4 x 5]. Luego necesitamos formar una matriz de tres elementos: bloques (él mismo inventó el nombre). Entonces, el último elemento es 1, el penúltimo es 5 y el primer elemento es 5 * 4 = 20. Es decir, bloques = [20, 5, 1]. Por ejemplo, al acceder por índice [1, 0, 4], el índice de la matriz se verá como 20 * 1 + 5 * 0 + 4 * 1 = 24. Hasta ahora todo está claro



Transponer



... es decir, cambiando el orden de los ejes. La forma sencilla y tonta es crear una nueva matriz y poner los elementos allí en un nuevo orden. Pero a menudo es conveniente transponer, trabajar con el orden de ejes deseado y luego volver a cambiar el orden de los ejes. Por supuesto, en este caso, no puede cambiar la matriz lineal (LM) en sí , y al acceder a ciertos índices, simplemente cambiaremos el orden.



Considere la función:



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


Como puede ver, si intercambia los bloques , se creará la visibilidad de los ejes de intercambio. Por tanto , escribiremos esto:



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


Simplemente cambiamos los números y longitudes de los ejes en algunos lugares.



Subtensor



El subtensor de un tensor N-dimensional es un tensor M-dimensional (M <N), que es parte del original. Por ejemplo, el elemento cero del tensor de forma [2 x 3 x 4] es el tensor de forma [3 x 4]. Lo conseguimos simplemente cambiando.



Imaginemos que obtenemos un subtensor en el índice n . Entonces, el primer elemento es el n * bloques [0] + 0 * bloques [1] + 0 * bloques [2] + ... . Es decir, el desplazamiento es n * bloques [0] . En consecuencia, sin copiar el tensor original, recordamos el cambio , creamos un nuevo tensor con un enlace a nuestros datos, pero con un cambio. Y también necesitará descartar el elemento de los bloques, es decir, los bloques de elementos [0], porque este es el primer eje, no habrá llamadas a él.

Otras operaciones de composición



Todos los demás ya se siguen de estos.



  1. SetSubtensor enviará los elementos al subtensor deseado
  2. Concat crea un nuevo tensor, y allí enviará elementos de dos (mientras que la longitud del primer eje es la suma de las longitudes de los ejes de los dos tensores)
  3. La pila agrupa varios tensores en uno con un eje adicional (por ejemplo, pila ([3 x 4], [3 x 4]) -> [2 x 3 x 4])
  4. Slice devuelve Stack de ciertas subtensiones


Todas las operaciones de composición que he definido se pueden encontrar aquí .



Operaciones matemáticas



Todo ya es simple aquí.



1) Operaciones puntuales (es decir, para dos tensores, las operaciones se realizan en un par de elementos correspondientes (es decir, con las mismas coordenadas)). La implementación está aquí (una explicación de por qué un código tan feo está a continuación).



2) Operaciones sobre matrices. El producto, la inversión y otras operaciones simples, me parece, no requieren explicación. Aunque hay mucho que contar sobre el determinante.



Una historia del determinante
, . — , , O(N!). 3x3 ( ).



, . -: float , int .



, , . , .



, . , , , , . . SafeDivisionWrapper .



: . , . SafeDivision ( , ).



3) Operaciones sobre veteranos (producto puntual y cruzado).



Mejoramiento



¿Plantillas?


No hay plantillas en C # . Por lo tanto, debes usar muletas. Algunas personas han creado una compilación dinámica en un delegado, por ejemplo , implementa la suma de dos tipos.



Sin embargo, quería una personalizada, así que comencé una interfaz desde la cual el usuario necesita heredar la estructura. En este caso, la primitiva se almacena en la propia matriz lineal, y las funciones de suma, diferencia y otras se denominan como



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


Que está incluido antes de su método. Un ejemplo de la implementación de dicha estructura .



Indexación


Además, aunque parece lógico usar params en el indexador, es decir, algo como esto:



public T this[params int[] indices]


De hecho, cada llamada creará una matriz, por lo que tendrá que crear muchas sobrecargas . Lo mismo ocurre con otras funciones que trabajan con índices.



Excepciones


También introduje todas las excepciones y comprobaciones en el bloque #if ALLOW_EXCEPTIONS en caso de que definitivamente lo necesite rápidamente, y definitivamente no hay problemas con los índices. Hay un ligero aumento en el rendimiento.



De hecho, esto no es solo una microoptimización, que costará mucho en términos de seguridad. Por ejemplo, se envía una consulta a su tensor, en la que ya, por sus propios motivos, realizó una verificación de la exactitud de los datos. Entonces, ¿por qué necesita otro cheque? Y no son gratuitos, sobre todo si guardamos incluso operaciones aritméticas innecesarias con enteros.



Multihilo


Gracias Billy, resultó ser muy simple e implementado a través de Parallel.For.



Aunque el multiproceso no es una panacea y debe habilitarse con cuidado. Ejecuté un punto de referencia para la adición puntual de tensores en el i7-7700HQ:







donde el eje Y muestra el tiempo (en microsegundos) para realizar una sola operación con dos tensores enteros de un cierto tamaño (tamaño del eje X).



Es decir, existe un cierto umbral a partir del cual el multiproceso tiene sentido. Para no tener que pensar, hice el indicador Threading.Auto y codifiqué estúpidamente las constantes, comenzando con un volumen igual al que puede habilitar el subproceso múltiple (¿hay un método automático más inteligente?).



Al mismo tiempo, la biblioteca todavía no es más rápida que las matrices de juegos, o incluso más que las que se calculan en CUDA. ¿Para qué? Ya se han escrito, pero lo principal es un tipo personalizado.



Me gusta esto



Aquí hay una breve nota, gracias por leer. El enlace al github del proyecto está aquí . Y su usuario principal es la biblioteca de álgebra simbólica AngouriMath .



Un poco sobre nuestros tensores en 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