De alguma forma, eu precisava de tensores (expansões de matriz) para minha projeção. Pesquisei no Google, encontrei uma série de todos os tipos de bibliotecas, por toda parte, e o que você precisa - não. Tive que implementar o plano de cinco dias e implementar o que era necessário. Uma breve nota sobre como trabalhar com tensores e truques de otimização.

Então, o que precisamos?
- Matrizes N-dimensionais (tensores)
- Implementação de um conjunto básico de métodos para trabalhar com um tensor como uma estrutura de dados
- Implementação de um conjunto básico de funções matemáticas (para matrizes e vetores)
- Tipos genéricos, ou seja, qualquer um. E operadores personalizados
E o que já foi escrito antes de nós?
, Towel , :
System.Numerics.Tensor . , , , . , , .
MathSharp, NumSharp, Torch.Net, TensorFlow, /ML-, .
- ,
- Transpose — «» , O(V), V — «».
, . , , , ., ( , )
System.Numerics.Tensor . , , , . , , .
MathSharp, NumSharp, Torch.Net, TensorFlow, /ML-, .
Armazenamento de elemento, transposição, subtensor
Os itens serão armazenados em uma matriz unidimensional. Para obter um elemento de um conjunto de índices, vamos multiplicar os índices por certos coeficientes e adicionar. Ou seja, suponha que temos um tensor [3 x 4 x 5]. Em seguida, precisamos formar um array de três elementos - blocos (ele próprio veio com o nome). Então, o último elemento é 1, o penúltimo é 5 e o primeiro elemento é 5 * 4 = 20. Ou seja, blocos = [20, 5, 1]. Por exemplo, ao acessar pelo índice [1, 0, 4], o índice na matriz será semelhante a 20 * 1 + 5 * 0 + 4 * 1 = 24. Até agora tudo está claro
Transpor
... ou seja, alterando a ordem dos eixos. A maneira tola e fácil é criar um novo array e colocar os elementos em uma nova ordem. Mas geralmente é conveniente transpor, trabalhar com a ordem de eixo desejada e, em seguida, alterar a ordem do eixo de volta. É claro que, neste caso, você não pode alterar a matriz linear (LM) em si e, ao fazer referência a certos índices, simplesmente alteraremos a ordem.
Considere a função:
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 você pode ver, se você trocar os blocos , a visibilidade dos eixos de troca será criada. Portanto , vamos escrever isto:
public void Transpose(int axis1, int axis2)
{
(blocks[axis1], blocks[axis2]) = (blocks[axis2], blocks[axis1]);
(Shape[axis1], Shape[axis2]) = (Shape[axis2], Shape[axis1]);
}
Apenas mudamos os números e comprimentos dos eixos em alguns lugares.
Subtensor
O subtensor de um tensor N-dimensional é um tensor M-dimensional (M <N), que faz parte do original. Por exemplo, o elemento zero do tensor de forma [2 x 3 x 4] é o tensor de forma [3 x 4]. Nós o obtemos apenas mudando.
Vamos imaginar que temos um subtensor no índice n . Em seguida, o primeiro elemento é o n * bloqueia [0] + 0 * bloqueia [1] + 0 * blocos [2] + ... . Ou seja, o deslocamento é n * blocos [0] . Da mesma forma, sem copiar o tensor original, nos lembramos da mudança , criamos um novo tensor com um link para nossos dados, mas com uma mudança. E você também precisará descartar o elemento dos blocos, ou seja, os blocos do elemento [0], porque este é o primeiro eixo, não haverá chamadas para ele.
Outras Operações de Composição
Todos os outros já decorrem destes.
- SetSubtensor irá encaminhar os elementos para o subtensor desejado
- Concat cria um novo tensor, e lá ele irá encaminhar elementos de dois (enquanto o comprimento do primeiro eixo é a soma dos comprimentos dos eixos dos dois tensores)
- A pilha agrupa vários tensores em um com um eixo adicional (por exemplo, pilha ([3 x 4], [3 x 4]) -> [2 x 3 x 4])
- Slice retorna Stack de certas subtensões
Todas as operações de composição que defini podem ser encontradas aqui .
Operações matemáticas
Tudo já é simples aqui.
1) Operações pontuais (ou seja, para dois tensores, as operações são realizadas em um par de elementos correspondentes (ou seja, com as mesmas coordenadas)). A implementação está aqui (uma explicação de por que um código tão feio está abaixo).
2) Operações em matrizes. Produto, inversão e outras operações simples, parece-me, não requerem explicação. Embora haja uma história para contar sobre o determinante.
O Conto do Determinante
3) Operações em veteranos (ponto e produto cruzado).
Otimização
Modelos?
Não existem modelos em C # . Portanto, você deve usar muletas. Algumas pessoas criaram a compilação dinâmica em um delegado, por exemplo , ela implementa a soma de dois tipos.
No entanto, eu queria um customizado, então iniciei uma interface da qual o usuário precisa herdar a estrutura. Neste caso, a primitiva é armazenada na própria matriz linear, e as funções de adição, diferença e outras são chamadas como
var c = default(TWrapper).Addition(a, b);
Que é inline antes do seu método. Um exemplo de implementação de tal estrutura .
Indexando
Além disso, embora pareça lógico usar parâmetros no indexador, ou seja, algo assim:
public T this[params int[] indices]
Na verdade, cada chamada criará um array, portanto, você deve criar muitas sobrecargas . O mesmo acontece com outras funções que trabalham com índices.
Exceções
Eu também direcionei todas as exceções e verificações no bloco #if ALLOW_EXCEPTIONS no caso de você definitivamente precisar dele rapidamente e definitivamente não houver problemas com índices. Há um ligeiro aumento no desempenho.
Na verdade, não se trata apenas de microotimização, que custará muito em termos de segurança. Por exemplo, uma consulta é enviada ao seu tensor, na qual você já, por seus próprios motivos, fez uma verificação da exatidão dos dados. Então por que você precisa de outro cheque? E eles não são gratuitos, especialmente se salvarmos até mesmo operações aritméticas desnecessárias com números inteiros.
Multithreading
Obrigado Billy, acabou sendo muito simples e implementado via Parallel.For.
Embora o multithreading não seja uma panacéia, você deve ativá-lo com cuidado. Eu executei um benchmark para adição pontual de tensores no i7-7700HQ:

Onde o eixo Y mostra o tempo (em microssegundos) para realizar uma única operação com dois tensores inteiros de um certo tamanho (tamanho do eixo X).
Ou seja, há um certo limite a partir do qual o multithreading faz sentido. Para não ter que pensar, criei o sinalizador Threading.Auto e codifiquei estupidamente as constantes, começando com um volume igual ao qual você pode habilitar o multithreading (existe um método automático mais inteligente?).
Ao mesmo tempo, a biblioteca ainda não é mais rápida que as matrizes de jogos, ou mais ainda aquelas que são calculadas em CUDA. Pelo que? Esses já foram escritos, mas nosso principal é um tipo personalizado.
Como isso
Aqui está uma breve nota, obrigado pela leitura. O link para o github do projeto está aqui . E seu principal usuário é a biblioteca de álgebra simbólica AngouriMath .
Um pouco sobre nossos tensores em 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)