В настоящее время я наткнулся на интересную статью, которая называется Кронекера-Produkt. В то же время я работаю над своей библиотекой нейронных сетей.
Чтобы мой алгоритм работал, мне нужен тензорный класс, где я могу получить произведение двух тензорных операторов с перегруженным * оператором.
Рассмотрим следующий пример / вопросы:
Мой тензор класса 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;
}
Ваш первый шаг # 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
начать.
Вот несколько приемов для эффективных векторов, которые я выучил в классе, но они должны быть одинаково хороши для тензора.
Определите пустой конструктор и оператор присваивания. Например
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 вы должны быть осторожны с порядком расположения вашего цикла, попробуйте пройтись по тензорам в том порядке, в котором они хранятся для эффективности кэша.