Aprendizaje profundo desde cero en C++ moderno [2/8]: Programación de tensores

1. Descripción

        Texto para principiantes : este texto requiere conocimientos básicos de programación y una comprensión básica del aprendizaje automático. Los tensores son la forma principal de representar datos en algoritmos de aprendizaje profundo. Son ampliamente utilizados para implementar entradas, salidas, parámetros y estado interno durante la ejecución del algoritmo.

        En esta historia, aprenderemos a usar la API del tensor de funciones para desarrollar nuestro algoritmo de C++. En concreto, hablaremos de:

  • que es tensor
  • Cómo definir tensor en C++
  • Cómo calcular operaciones tensoriales
  • Reducción de tensores y convolución

        Al final de este artículo, implementaremos Softmax como un ejemplo ilustrativo de la aplicación de tensores a algoritmos de aprendizaje profundo.

2. ¿Qué es el tensor?

Los tensores son estructuras de datos en forma de cuadrícula que generalizan el concepto de vectores y matrices con cualquier número de ejes. En el aprendizaje automático, generalmente usamos la palabra " dimensión " en lugar de "eje". El número de dimensiones diferentes de un tensor también se conoce como el rango del tensor :

diferentes tensores de rango

En la práctica, usamos tensores para representar datos en algoritmos y realizar operaciones aritméticas con ellos.

Las operaciones más simples que podemos realizar con tensores son las llamadas operaciones por elementos: dados dos tensores operandos de la misma dimensión, esta operación produce un nuevo tensor de la misma dimensión con el valor de cada coeficiente obtenido de la evaluación binaria del individuo elementos en los operandos:

multiplicación de coeficientes

El ejemplo anterior es una representación gráfica del producto de coeficientes de dos tensores de rango 2. Esta operación aún funciona para dos tensores cualesquiera, ya que tienen las mismas dimensiones.

Al igual que las matrices, podemos usar tensores para realizar otras operaciones más complejas, como el producto matricial, la convolución, la contracción, la reducción e innumerables operaciones geométricas. En esta historia, aprenderemos a usar la API Feature Tensor para realizar algunas de estas operaciones de tensor, centrándonos en las operaciones más importantes para implementar algoritmos de aprendizaje profundo.

3. Cómo declarar y usar tensores en C++

        Como todos sabemos, Eigen es una biblioteca de álgebra lineal ampliamente utilizada para cálculos de matrices. Además del conocido soporte para matrices, Eigen también tiene un módulo de tensor (no compatible).

Si bien la API de Eigen Tensor dice que no es compatible, en realidad es bien compatible con los desarrolladores del marco TensorFlow de Google.

        Podemos definir fácilmente tensores usando rasgos:

#include <iostream>

#include <unsupported/Eigen/CXX11/Tensor>

int main(int, char **)
{

    Eigen::Tensor<int, 3> my_tensor(2, 3, 4);
    my_tensor.setConstant(42);

    std::cout << "my_tensor:\n\n" 
              << my_tensor << "\n\n";

    std::cout << "tensor size is " << my_tensor.size() << "\n\n"; 

    return 0;
}

El Banco

Eigen::Tensor<int, 3> my_tensor(2, 3, 4);

Cree un objeto Tensor y asigne la memoria necesaria para almacenar los enteros. En este ejemplo, es un tensor de rango 3 donde la primera dimensión tiene tamaño 2, la segunda dimensión tiene tamaño 3 y la última dimensión tiene tamaño 4. Podemos expresarlo de la siguiente manera:2x3x4my_tensormy_tensor

Podemos establecer datos de tensor si es necesario:

my_tensor.setValues({
   
   {
   
   {1, 2, 3, 4}, {5, 6, 7, 8}}});

std::cout << "my_tensor:\n\n" << my_tensor << "\n\n";

O use valores aleatorios en su lugar. Por ejemplo, podemos hacer:

Eigen::Tensor<float, 2> kernel(3, 3);
kernel.setRandom();
std::cout << "kernel:\n\n" << kernel << "\n\n";

        Y use este kernel más tarde para realizar la convolución. Cubriremos las circunvoluciones en breve en esta historia. Primero, aprendamos a usar TensorMaps.

4. Use Eigen::TensorMap para crear una vista de tensor

A veces, asignamos algunos datos y solo queremos manipularlos usando tensores. Similar a pero en lugar de asignar nuevos datos, es solo una vista de los datos pasados ​​como parámetro. Revisa los siguientes ejemplos:Eigen::TensorMapEigen::Tensor

//an vector with size 12
std::vector<float> storage(4*3);

// filling vector from 1 to 12
std::iota(storage.begin(), storage.end(), 1.);

for (float v: storage) std::cout << v << ','; 
std::cout << "\n\n";

// setting a tensor view with 4 rows and 3 columns
Eigen::TensorMap<Eigen::Tensor<float, 2>> my_tensor_view(storage.data(), 4, 3);

std::cout << "my_tensor_view before update:\n\n" << my_tensor_view << "\n\n";

// updating the vector
storage[4] = -1.;

std::cout << "my_tensor_view after update:\n\n" << my_tensor_view << "\n\n";

// updating the tensor
my_tensor_view(2, 1) = -8;

std::cout << "vector after two updates:\n\n";
for (float v: storage) std::cout << v << ','; 
std::cout << "\n\n";

En este ejemplo, es fácil ver que (de manera predeterminada) los tensores en la API Feature Tensor son col -major . col-major y row-major se refieren a cómo se almacenan los datos de cuadrícula en contenedores lineales (consulte este artículo en Wikipedia ):

fuente

Si bien podemos usar tensores de tamaño de fila, no se recomienda :

Actualmente, solo se admite totalmente el diseño de columna principal predeterminado, por lo que no se recomienda intentar usar un diseño de fila principal en este momento.

Eigen::TensorMapMuy útil porque podemos usarlo para ahorrar memoria, lo cual es crucial para aplicaciones exigentes como algoritmos de aprendizaje profundo.

5. Realizar operaciones unarias y binarias

        La API de Eigen Tensor define operadores aritméticos sobrecargados comunes, lo que hace que la programación con Tensors sea intuitiva y sencilla. Por ejemplo, podemos sumar y restar tensores:

Eigen::Tensor<float, 2> A(2, 3), B(2, 3);
A.setRandom();
B.setRandom();

Eigen::Tensor<float, 2> C = 2.f*A + B.exp();

std::cout << "A is\n\n"<< A << "\n\n";
std::cout << "B is\n\n"<< B << "\n\n";
std::cout << "C is\n\n"<< C << "\n\n";

La API del tensor de características tiene otras funciones relacionadas con los elementos, como , y . Además, podemos usar lo siguiente:.exp()sqrt()log()abs()unaryExpr(fun)

auto cosine = [](float v) {return cos(v);};
Eigen::Tensor<float, 2> D = A.unaryExpr(cosine);
std::cout << "D is\n\n"<< D << "\n\n";

Del mismo modo, podemos usar:binaryExpr

auto fun = [](float a, float b) {return 2.*a + b;};
Eigen::Tensor<float, 2> E = A.binaryExpr(B, fun);
std::cout << "E is\n\n"<< E << "\n\n";

6. Evaluación perezosa y palabra clave automática

Los ingenieros de Google que desarrollaron la API Eigen Tensor siguieron la misma estrategia que en la parte superior de la biblioteca Eigen. Una de estas estrategias, y probablemente la más importante, es cómo se evalúan las expresiones con pereza.

La estrategia de evaluación perezosa implica retrasar la evaluación real de una expresión para que varias expresiones encadenadas se puedan combinar en una sola expresión equivalente optimizada. Entonces, en lugar de evaluar varias expresiones separadas de forma incremental, el código optimizado evalúa solo una expresión, con el objetivo de explotar el rendimiento general resultante.

Por ejemplo, la expresión en realidad no calcula la suma de A y B si sum es un tensor. En efecto, la expresión produce un objeto especial que sabe cómo evaluar. La operación actual solo se realizará cuando este objeto especial se asigne a un tensor actual. En otras palabras, en la siguiente declaración:ABA + BA + BA + B

auto C = A + B;

CNo el resultado real, sino solo un objeto calculado (realmente un objeto) que sabe cómo calcular. Solo cuando se asigna a un objeto tensor (objeto de tipo , , etc.) se evaluará para proporcionar el valor tensor correcto:A + BEigen::TensorCwiseBinaryOpA + BCEigen::TensorEigen::TensorMapEigen::TensorRef

Eigen::Tensor<...> T = C;
std::cout << "T is " << T << "\n\n";

Por supuesto, esto no tiene sentido para operaciones pequeñas como esta. Sin embargo, este comportamiento es útil para largas cadenas de operaciones en las que se puede optimizar el cálculo antes de la evaluación real. En resumen, como pauta general, en lugar de escribir código como este:A + B

Eigen::Tensor<...> A = ...;
Eigen::Tensor<...> B = ...;
Eigen::Tensor<...> C = B * 0.5f;
Eigen::Tensor<...> D = A + C;
Eigen::Tensor<...> E = D.sqrt();

Deberíamos escribir un código como este:

Eigen::Tensor<...> A = ...;
Eigen::Tensor<...> B = ...;
auto C = B * 0.5f;
auto D = A + C;
Eigen::Tensor<...> E = D.sqrt();

La diferencia es que en el primero, en realidad son objetos, mientras que en el último código, son solo operaciones de cálculo perezoso.CDEigen::Tensor

En la recuperación, es mejor usar el cálculo perezoso para evaluar largas cadenas de operaciones, ya que la cadena se optimizará internamente, lo que en última instancia resultará en una ejecución más rápida.

7. Operaciones geométricas

Las operaciones geométricas producen tensores con diferentes dimensiones y, a veces, tamaños. Ejemplos de estas operaciones incluyen: , , y .reshapepadshufflestridebroadcast

Vale la pena señalar que la API del tensor de características no tiene operaciones. Sin embargo, podemos simularlo usando:transposetransposeshuffle

auto transpose(const Eigen::Tensor<float, 2> &tensor) {
    Eigen::array<int, 2> dims({1, 0});
    return tensor.shuffle(dims);
}

Eigen::Tensor<float, 2> a_tensor(3, 4);
a_tensor.setRandom();

std::cout << "a_tensor is\n\n"<< a_tensor << "\n\n";
std::cout << "a_tensor transpose is\n\n"<< transpose(a_tensor) << "\n\n";

Más adelante, cuando analicemos ejemplos que usan tensores, veremos algunos ejemplos de operaciones geométricas.softmax

8. Reducir

        Una reducción es un caso especial de operación que da como resultado un tensor con una dimensionalidad menor que el tensor original. Un caso intuitivo para la reducción es:sum()maximum()

Eigen::Tensor<float, 3> X(5, 2, 3);
X.setRandom();

std::cout << "X is\n\n"<< X << "\n\n";

std::cout << "X.sum(): " << X.sum() << "\n\n";
std::cout << "X.maximum(): " << X.maximum() << "\n\n";

En el ejemplo anterior, redujimos todos los tamaños una vez. También podemos realizar reducciones a lo largo de ejes específicos. Por ejemplo:

Eigen::array<int, 2> dims({1, 2});

std::cout << "X.sum(dims): " << X.sum(dims) << "\n\n";
std::cout << "X.maximum(dims): " << X.maximum(dims) << "\n\n";

        La API del tensor de características tiene un conjunto de operaciones de reducción preconstruidas como , , , etc. Si alguna de las operaciones preconstruidas no es adecuada para una implementación en particular, podemos proporcionar un funtor personalizado como argumento.prodanyallmeanreduce(dims, reducer)reducer

Nueve, convolución tensorial

        En una historia anterior , aprendimos cómo implementar la convolución 2D usando solo C++ estándar y la matriz de funciones. De hecho, esto es necesario porque no hay una convolución de matriz incorporada en Eigen. Afortunadamente, la API de EigenTensor tiene una función conveniente para realizar la convolución en los objetos de EigenTensor:

Eigen::Tensor<float, 4> input(1, 6, 6, 3);
input.setRandom();

Eigen::Tensor<float, 2> kernel(3, 3);
kernel.setRandom();

Eigen::Tensor<float, 4> output(1, 4, 4, 3);

Eigen::array<int, 2> dims({1, 2});
output = input.convolve(kernel, dims);

std::cout << "input:\n\n" << input << "\n\n";
std::cout << "kernel:\n\n" << kernel << "\n\n";
std::cout << "output:\n\n" << output << "\n\n";

Tenga en cuenta que podemos realizar convoluciones 2D, 3D, 4D, etc. controlando las dimensiones de las diapositivas en la convolución.

10. Máximo suave con tensor

        Cuando programamos modelos de aprendizaje profundo, usamos tensores en lugar de matrices. Resulta que las matrices pueden representar cuadrículas de una o dos dimensiones como máximo, mientras que tenemos imágenes multicanal de datos de mayor dimensión o registros por lotes para procesar. Aquí es donde entran en juego los tensores.

        Consideremos el siguiente ejemplo, donde tenemos dos lotes de registros, cada lote tiene 4 registros y cada registro tiene 3 valores:

        Podemos representar estos datos de la siguiente manera:

Eigen::Tensor<float, 3> input(2, 4, 3);
input.setValues({
    {
   
   {0.1, 1., -2.},{10., 2., 5.},{5., -5., 0.},{2., 3., 2.}},
    {
   
   {100., 1000., -500.},{3., 3., 3.},{-1, 1., -1.},{-11., -0.2, -.1}}
});

std::cout << "input:\n\n" << input << "\n\n";

        Ahora, apliquemos a estos datos:softmax

Eigen::Tensor<float, 3> output = softmax(input);
std::cout << "output:\n\n" << output << "\n\n";

        Softmax es una función de activación popular. Cubrimos su implementación en una historia anterior . Ahora, introduzcamos la implementación:Eigen::MatrixEigen::Tensor

#include <unsupported/Eigen/CXX11/Tensor>

auto softmax(const Eigen::Tensor<float, 3> &z)
{

    auto dimensions = z.dimensions();

    int batches = dimensions.at(0);
    int instances_per_batch = dimensions.at(1);
    int instance_length = dimensions.at(2);

    Eigen::array<int, 1> depth_dim({2});
    auto z_max = z.maximum(depth_dim);

    Eigen::array<int, 3> reshape_dim({batches, instances_per_batch, 1});
    auto max_reshaped = z_max.reshape(reshape_dim);

    Eigen::array<int, 3> bcast({1, 1, instance_length});
    auto max_values = max_reshaped.broadcast(bcast);

    auto diff = z - max_values;

    auto expo = diff.exp();
    auto expo_sums = expo.sum(depth_dim);
    auto sums_reshaped = expo_sums.reshape(reshape_dim);
    auto sums = sums_reshaped.broadcast(bcast);
    auto result = expo / sums;

    return result;
}

        Este código genera:

        No cubriremos Softmax en detalle aquí. Si necesita consultar el algoritmo Softmax, no dude en leer la historia anterior nuevamente en Medium . Por ahora, solo nos centraremos en comprender cómo usar los tensores de características para codificar nuestros modelos de aprendizaje profundo.

        Lo primero a tener en cuenta es que esta función en realidad no calcula el valor softmax de los parámetros. De hecho, solo monte un objeto complejo que pueda calcular el softmax.softmax(z)zsoftmax(z)

        El valor real solo se evalúa cuando el resultado de se asigna a un objeto similar a un tensor. Por ejemplo, aquí:softmax(z)

Eigen::Tensor<float, 3> output = softmax(input);

        Antes de esta línea, todo es solo un gráfico de cálculo de softmax, esperando ser optimizado. Esto sucede solo porque usamos palabras clave en el cuerpo de . Por lo tanto, la API del tensor de características puede optimizar todo el cálculo utilizando menos operaciones, lo que resulta en un mejor procesamiento y uso de la memoria.autosoftmax(z)softmax(z)

        Antes de cerrar esta historia, me gustaría señalar y apelar a:tensor.reshape(dims)tensor.broadcast(bcast)

Eigen::array<int, 3> reshape_dim({batches, instances_per_batch, 1});
auto max_reshaped = z_max.reshape(reshape_dim);

Eigen::array<int, 3> bcast({1, 1, instance_length});
auto max_values = max_reshaped.broadcast(bcast);

  reshape(dims)es una operación geométrica especial que produce otro tensor del mismo tamaño que el tensor original, pero con dimensiones diferentes. Reformar no cambia el orden de los datos dentro del tensor. Por ejemplo:

Eigen::Tensor<float, 2> X(2, 3);
X.setValues({
   
   {1,2,3},{4,5,6}});

std::cout << "X is\n\n"<< X << "\n\n";

std::cout << "Size of X is "<< X.size() << "\n\n";

Eigen::array<int, 3> new_dims({3,1,2});
Eigen::Tensor<float, 3> Y = X.reshape(new_dims);

std::cout << "Y is\n\n"<< Y << "\n\n";

std::cout << "Size of Y is "<< Y.size() << "\n\n";

Tenga en cuenta que, en este ejemplo, el tamaño de X e Y es 6, aunque tienen una geometría muy diferente.

tensor.broadcast(bcast) repite el tensor tantas veces como se indica en el parámetro para cada dimensión. Por ejemplo:bcast

Eigen::Tensor<float, 2> Z(1,3);
Z.setValues({
   
   {1,2,3}});
Eigen::array<int, 2> bcast({4, 2});
Eigen::Tensor<float, 2> W = Z.broadcast(bcast);

std::cout << "Z is\n\n"<< Z << "\n\n";
std::cout << "W is\n\n"<< W << "\n\n";

De manera diferente, el rango del tensor (es decir, la dimensionalidad) no cambiará, sino que solo aumentará el tamaño de la dimensionalidad.reshapebroadcast

11. Limitaciones

La documentación de la API de Eigen Tensor menciona algunas limitaciones que podemos conocer:

  • La compatibilidad con GPU está probada y optimizada para tipos de punto flotante. Incluso si pudiéramos afirmar eso, se desaconseja el uso de tensores que no sean de punto flotante cuando se usa la GPU.Eigen::Tensor<int,...> tensor;
  • El diseño predeterminado (col-major) es el único realmente compatible. Al menos por ahora no deberíamos usar filas mayores.
  • El número máximo de dimensiones es 250. Este tamaño solo se puede lograr cuando se usa un compilador compatible con C++11.

12. Conclusión y próximos pasos

        Los tensores son la estructura de datos fundamental de la programación de aprendizaje automático, lo que nos permite representar y procesar datos multidimensionales tan directamente como matrices bidimensionales regulares.

En esta historia, presentamos la API de tensores de funciones y aprendimos a usar los tensores con relativa facilidad. También aprendimos que Feature Tensor API tiene un mecanismo de evaluación perezoso que optimiza la ejecución en términos de memoria y tiempo de procesamiento.

Para asegurarnos de que realmente comprendemos el uso de la API Eigen Tensor, analizamos un ejemplo de codificación de Softmax mediante tensores.

En las próximas historias, continuaremos desarrollando algoritmos de aprendizaje profundo de alto rendimiento desde cero usando C++ y Eigen, específicamente usando Eigen Tensor API.

Trece, código github

Puede  encontrar el código utilizado en esta historia en este repositorio en GitHub .

14. Cita

[1]  API de tensor de características

[2]  Módulo de tensor de características

[3] Repositorio propio de Gitlab,  libown / own · GitLab

[4] Charu C. Aggarwal,  Redes neuronales y aprendizaje profundo: un libro de texto  (2018), Springer

[5] Jason Brownlee, una introducción suave a los tensores para el aprendizaje automático con NumPy

Acerca de esta serie

En esta serie , aprenderemos a codificar algoritmos de aprendizaje profundo imprescindibles, como convoluciones, retropropagación, funciones de activación, optimizadores, redes neuronales profundas y más, usando solo C++ simple y moderno.

La historia es: usar la API del tensor de características

Mira otras historias:

0 — Conceptos básicos de la programación moderna de aprendizaje profundo en C++

1 — Codificación de una convolución 2D en C++ puro

2 — Función de costo usando Lambda

3 — Implementando el Descenso de Gradiente

4 — función de activación

...y más próximamente.

Supongo que te gusta

Origin blog.csdn.net/gongdiwudu/article/details/132160022
Recomendado
Clasificación