Как реализовать тензорный класс для Кронекер-Продукт

В настоящее время я наткнулся на интересную статью, которая называется Кронекера-Produkt. В то же время я работаю над своей библиотекой нейронных сетей.
Чтобы мой алгоритм работал, мне нужен тензорный класс, где я могу получить произведение двух тензорных операторов с перегруженным * оператором.

Рассмотрим следующий пример / вопросы:

  1. Как эффективно построить / сохранить вложенные матрицы?
  2. Как выполнить произведение двух тензорных?
  3. Как визуализировать тензор c максимально просто?

Мой тензор класса 3, который в настоящее время поддерживает только 3 измерения:

#pragma once

#include <iostream>
#include <sstream>
#include <random>
#include <cmath>
#include <iomanip>

template<typename T>
class tensor {
public:
const unsigned int x, y, z, s;

tensor(unsigned int x, unsigned int y, unsigned int z, T val) : x(x), y(y), z(z), s(x * y * z) {
p_data = new T[s];
for (unsigned int i = 0; i < s; i++) p_data[i] = val;
}

tensor(const tensor<T> & other) : x(other.x), y(other.y), z(other.z), s(other.s) {
p_data = new T[s];
memcpy(p_data, other.get_data(), s * sizeof(T));
}

~tensor() {
delete[] p_data;
p_data = nullptr;
}

T * get_data() {
return p_data;
}

static tensor<T> * random(unsigned int x, unsigned int y, unsigned int z, T val, T min, T max) {
tensor<T> * p_tensor = new tensor<T>(x, y, z, val);

std::random_device rd;
std::mt19937 mt(rd());
std::uniform_real_distribution<T> dist(min, max);

for (unsigned int i = 0; i < p_tensor->s; i++) {
T rnd = dist(mt);
while (abs(rnd) < 0.001) rnd = dist(mt);
p_tensor->get_data()[i] = rnd;
}

return p_tensor;
}

static tensor<T> * from(std::vector<T> * p_data, T val) {
tensor<T> * p_tensor = new tensor<T>(p_data->size(), 1, 1, val);

for (unsigned int i = 0; i < p_tensor->get_x(); i++) p_tensor->set_data(i + 0 * p_tensor->get_x() * + 0 * p_tensor->get_x() * p_tensor->get_y(), p_data->at(i));

return p_tensor;
}

friend std::ostream & operator <<(std::ostream & stream, tensor<T> & tensor) {
stream << "(" << tensor.x << "," << tensor.y << "," << tensor.z << ") Tensor\n";

for (unsigned int i = 0; i < tensor.x; i++) {
for (unsigned int k = 0; k < tensor.z; k++) {
stream << "[";

for (unsigned int j = 0; j < tensor.y; j++) {
stream << std::setw(5) << roundf(tensor(i, j, k) * 1000) / 1000;
if (j + 1 < tensor.y) stream << ",";
}

stream << "]";

}

stream << std::endl;
}

return stream;
}

tensor<T> & operator +(tensor<T> & other) {
tensor<T> result(*this);

return result;
}

tensor<T> & operator -(tensor<T> & other) {
tensor<T> result(*this);

return result;
}

tensor<T> & operator *(tensor<T> & other) {
tensor<T> result(*this);

return result;
}

T & operator ()(unsigned int i, unsigned int j, unsigned int k) {
return p_data[i + (j * x) + (k * x * y)];
}

T & operator ()(unsigned int i) {
return p_data[i];
}

private:
T * p_data = nullptr;
};

int main() {
tensor<double> * p_tensor_input = tensor<double>::random(6, 2, 3, 0.0, 0.0, 1.0);
tensor<double> * p_tensor_weight = tensor<double>::random(2, 6, 3, 0.0, 0.0, 1.0);

std::cout << *p_tensor_input << std::endl;
std::cout << *p_tensor_weight << std::endl;

tensor<double> p_tensor_output = *p_tensor_input + *p_tensor_weight;

return 0;
}

0

Решение

Ваш первый шаг # 2 — и получите его правильно.

После этого оптимизируй.

Начните с контейнера C<T>,

Определите некоторые операции над ним. wrap(T) возвращает C<T> содержащий это T, карта занимает C<T> и функция на T U f(T) и возвращается C<U>, сгладить занимает C<C<U>> и возвращает C<U>,

определять scale( T, C<T> ) который занимает T и C<T> и возвращает C<T> с масштабированными элементами. Ака, скалярное умножение.

template<class T>
C<T> scale( T scalar, C<T> container ) {
return map( container, [&](T t){ return t*scalar; } );
}

Тогда мы имеем:

template<class T>
C<T> tensor( C<T> lhs, C<T> rhs ) {
return flatten( map( lhs, [&](T t) { return scale( t, rhs ); } ) );
}

Ваш тензорный продукт. И да, это может быть вашим реальным кодом. Я бы немного подправил его для эффективности.

(Обратите внимание, что я использовал разные термины, но я в основном описываю монадические операции, используя разные слова.)

После этого протестируйте, оптимизируйте и выполните итерации.

Что касается 3, результат тензорных произведений становится большим и сложным, для большого тензора не существует простой визуализации.

О, и будьте проще и храните данные в std::vector начать.

3

Другие решения

Вот несколько приемов для эффективных векторов, которые я выучил в классе, но они должны быть одинаково хороши для тензора.

Определите пустой конструктор и оператор присваивания. Например

tensor(unsigned int x, unsigned int y, unsigned int z) : x(x), y(y), z(z), s(x * y * z) {
p_data = new T[s];
}

tensor& operator=( tensor const& that ) {
for (int i=0; i<size(); ++i) {
p_data[i] = that(i) ;
}
return *this ;
}

template <typename T>
tensor& operator=( T const& that ) {
for (int i=0; i<size(); ++i) {
p_data[i] = that(i) ;
}
return *this ;
}

Теперь мы можем реализовать такие вещи, как сложение и масштабирование с отложенной оценкой. Например:

template<typename T1, typename T2>
class tensor_sum {
//add value_type to base tensor class for this to work
typedef decltype( typename T1::value_type() + typename T2::value_type() ) value_type ;

//also add function to get size of tensor

value_type operator()( int i, int j, int k ) const {
return t1_(i,j,k) + v2_(i,j,k) ;
}

value_type operator()( int i ) const {
return t1_(i) + v2_(i) ;
}

private:
T1 const& t1_;
T2 const& t2_;
}

template <typename T1, typename T2>
tensor_sum<T1,T2> operator+(T1 const& t1, T2 const& t2 ) {
return vector_sum<T1,T2>(t1,t2) ;
}

Этот tenors_sum ведет себя точно так же, как любой обычный тензор, за исключением того, что нам не нужно выделять память для хранения результата. Таким образом, мы можем сделать что-то вроде этого:

tensor<double> t0(...);
tensor<double> t1(...);
tensor<double> t2(...);
tensor<double> result(...); //define result to be empty, we will fill it later

result = t0 + t1 + 5.0*t2;

Компилятор должен оптимизировать это, чтобы это был всего один цикл, без сохранения промежуточных результатов или изменения оригинальных тензоров. Вы можете сделать то же самое для масштабирования и продукта kronecker. В зависимости от того, что вы хотите сделать с тензорами, это может быть большим преимуществом. Но будьте осторожны, это не всегда лучший вариант.

При реализации продукта kronecker вы должны быть осторожны с порядком расположения вашего цикла, попробуйте пройтись по тензорам в том порядке, в котором они хранятся для эффективности кэша.

1

По вопросам рекламы [email protected]