Tensor is a recursive array of Tensors, except rank-0 Tensor (scalar)
[Tensor, Tensor, Tensor, Tensor]
| | | |
| | [...] [...]
| [Tensor, Tensor, ...]
[Tensor, Tensor...]
Tensor может быть только of arithmetic type.
Для проверки типов я использую concepts
, а не чистый SFINAE, поэтому target C++20.
Создать 3D tensor можно так, by default tensor of type double:
Tensor tensor(depth, rows, cols); // Tensor<double>, rank-3
You can specify type:
Tensor<int> tensor(depth, rows, cols); // specified tensor of type int
Integer dims only:
Tensor t(2, 2) // Ok, provided integers, creates 2x2 matrix
Tensor t(2., 2.) // Error due to incorrect type double
Как создать scalar tensor:
Tensor sc = scalar(42); // Tensor<int>
Factory function scalar нужна потому что Tensor t(42)
вызовет конструктор dims, который создаст вектор соответствующего размера, а Tensor t = {42}
вызовет initializer_list конструктор, который создаст вектор с одним соответствующим элементом, но не скаляр!
У меня нет copy конструктора принимающего скаляр.
Tensor t = 42
не сработает,
во-первых потому что у меня конструктор explicit,
во-вторых если бы он преобразовывался в Tensor t(42)
, то такой конструктор конфликтовал бы с конструктором dims.
Но, scalar copy assignment operator сработает, если сначала создать пустой Tensor.
Под капотом scalar(42)
работает так:
scalar(T value){
Tensor<T> sc;
sc = value;
return sc;
}
Tensor matrix(2, 2); // Tensor<double>
cout << matrix;
// {{0. 0.},
// {0. 0.}}
// index operator matrix[i] returns tensor object
matrix[0][0] = 42; // scalar copy assignment = , implicit casting to double
double sc = matrix[0][0].value(); // get scalar value from tensor object
cout << matrix;
// {{42. 0.},
// { 0. 0.}}
Создать tensor можно вручную, что реализовано с initializer_list
:
Tensor vector = {1, 2, 3}; // 1D vector
Tensor matrix = { // 2D matrix
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
Тут стоит отметить, что конструктор принимающий dims - explicit
, то есть передавать мы можем только явно, например: Tensor t(rows, cols)
.
Без explicit
с Tensor t = {rows, cols}
вызывал бы конструктор dims, а не initializer_list
.
Проект реализует RAII i.e. Scope-Bound Resource Management для управления выделением и освобождением памяти of raw recursive arrays.
Copy and move constructors:
Tensor vector = {1, 2, 3};
Tensor copied = vector;
Tensor stealed = std::move(vector);
cout << vector; // 0i
Copy and move assignments:
Tensor vector = {1, 2, 3};
Tensor copied(3); // sizes must match
copied = vector;
cout << vector; // tensor<i>: {1i, 2i, 3i}
Tensor stealed(3);
stealed = std::move(vector);
cout << vector; // tensor<i>: 0i
Тензоры одинаковой размерности можно объединять в один тензор на ранк выше:
Tensor v1 = {1, 2, 3};
Tensor v2 = {4, 5, 6};
Tensor v3 = {7, 8, 9};
Tensor copied(v1, v2, v3); // 1 time copy
Tensor stealed(std::move(v1), std::move(v2), std::move(v3)); // 0 copy, steals data
Unary multiplication:
Tensor v1 = {1, 2, 3};
Tensor v2 = {4, 5, 6};
v1 *= v2; // 0 copy, but may be overhead of recursion and
cout << v1; // {4, 10, 18}
cout << v2; // {4, 5, 6}
Binary multiplication:
Tensor v1 = {1, 2, 3};
Tensor v2 = {4, 5, 6};
Tensor mul = v1 * v2; // 1 copy + RVO
cout << v1; // {1, 2, 3}
cout << v2; // {4, 5, 6}
cout << mul; // {4, 10, 18}
// so you have raw array of any dimensionality
int ma[2][3] = {
{1, 2, 3},
{1, 2, 3}
};
// you can pass dims directly like this, as rvalue:
Tensor matrix = from_array({2, 3}, ma); // Tensor<int> - deduction works
// or like this passing dims lvalue:
// std::vector dims = {2, 3};
// Tensor matrix = from_array(dims, ma);
// Это может быть полезно если в рантайме как-то пытаетесь преобразовывать, а не вручную
cout << matrix << endl;
/*
tensor(2)<i>:{
1i 2i 3i
1i 2i 3i
}
*/
std::vector<std::vector<int>> mv = {
{1, 2},
{3, 4}
};
Tensor matrix = from_stl_vector(mv);
cout << matrix << endl;
/*
tensor(2)<i>:{
1i 2i
3i 4i
}
*/
Iterator example:
Tensor tensor = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
};
for(auto & t: tensor){
std::cout << t << std::endl;
}
/*
tensor(1)<i>:{
1i 2i 3i
}
tensor(1)<i>:{
4i 5i 6i
}
tensor(1)<i>:{
7i 8i 9i
}
*/
Tensor может быть не прямоугольным? Это когда nested тензоры могут быть разного size.
Случай непрямоугольности я явно не запращею, но я расчитывал на то, что tensor прямоугольный, поэтому нужно тестить. При передаче размерности в контструктор создается прямоугольный tensor.
Я думаю, что не буду создавать явные проверки или дополнительную логику дополнения до прямоугольности с поиском самой длинной строки и тому подобное.
Просто то, что в shape()
я буду считать размер по первым тензорам... ???
- В случае непрямоугольности копирование работает и не обрезает до прямоугольности.
- Непрямоугольность будет работать и с умножением.
- initializer_list конструкторы работают при передаче непрямоугольных листов
std::vector<std::vector<int>> mv = {
{1, 2, 3},
{3, 4}
};
// Конвертирует непрямоугольные векторы соответсвенно
Tensor matrix = from_stl_vector(mv);
// Копирование
Tensor matrix_copy = matrix; // constructor
matrix_copy = matrix; // assignment
cout << matrix_copy << endl;
// Получается непрямоугольность будет работать и с умножением.
matrix_copy *= matrix; // Unary elementwise
cout << matrix_copy << endl; // elements squared
/*
1 4 9
9 16
*/
Tensor mul = matrix * matrix_copy; // Binary elementwise
cout << mul << endl; // elements cubed
/*
1 8 27
27 64
*/
Не знаю нужны ли эти проверки of buffer overflow. Они удобные, но создают лишний overhead?
Проверка Index out of bounds есть в index operator []
.
Но ни одна внутренняя имплементация функций tensor-а не использует этот operator, а напрямую используется raw array _coeffs
.
Зачем-то я проверяю совпадение _rank
и _size
в assignment operator =
,
я думаю это удобно, хотя можно было просто сделать полное приравнение.
Я проверяю на совпадение size при unary multiplication operator *=
.
Так как имплементация использует raw array, нет проверки на out of bounds,
и несовпадающий _size
может вызывать buffer overflow с одной из сторон.
Заметь что нет superior проверки на совпадения всех размеров вложенных тензоров, то есть не запрещается создавать непрямоугольные тензоры, но расчитывать на это не стоит, тензор сделан с расчетом на прямоугольность размерностей.
Memory Consumption
Tensor tensor(2, 2); // Tensor 2x2
cout << sizeof(tensor) << endl; // 24
std::vector<std::vector<int>> mv = {
{1, 2},
{3, 4}
}; // std::vector 2x2
cout << sizeof(mv) << endl; // 24
Tensor 2x2 - 2D Tensor, который весит 24 bytes. 2D Tensor хранит массив 1D Tensor-ов по 24 bytes. 1D Tensor хранит массив Scalar-Tensor-ов, которые тоже весят 24 byte-а.
Shape = (Row x Col)
1 + Row + Row x Col объектов
Каждый объект весит 24 byte-а:
24 + Row * 24 + Row * Col * 24 bytes.
У std::vector то же самое, только scalar-ы весят 4 или 8 bytes:
24 + Row * 24 + Row * Col * 4/8 bytes
То есть эта имплементация занимает в 3/6 раз больше памяти, чем если пользовать nested std::vector.
Если пользовать raw multidimensional array, то там вообще без overhead-а по memory:
Row * Col * 4/8 bytes
Это связано с тем, что Tensor это массив Tensor-ов.
Лучший подход мог бы быть - "Multidimensional Array with Mappings". В таком подходе память выделяется целиком, в одном непрерывном массиве.
Также я думал о создании рекурсивных template-ов, в таком случае было бы очень эффективно по памяти, но приходилось бы указывать очень длинные типы:
template <typename T = double>
class Tensor { // Base case
public:
T value;
Tensor(){ cout << "Base case" << endl; }
};
template <typename T>
class Tensor<Tensor<T>> {
public:
Tensor * coeffs;
Tensor(){ cout << "Recursive case" << endl; }
Tensor(std::integral auto ... args){
int dims[] = {args...};
rank = sizeof...(args);
for(int i = 0; i < rank; i++){
cout << dims[i] << endl;
}
size = dims[0];
coeffs = new Tensor<T>[size];
}
};
int main(){
Tensor<Tensor<double>> tensor(2); // after calls Base case
// Tensor<Tensor<Tensor<double>>> tensor(2, 2); // after calls Recursive case
}
Tensor-library is licensed under the terms of MPL-2.0, which is simple and straightforward to use, allowing this project to be combined and distributed with any proprietary software, even with static linking. If you modify, only the originally covered files must remain under the same MPL-2.0.
License notice:
SPDX-License-Identifier: MPL-2.0
--------------------------------
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
This file is part of the Tensor-library:
https://github.com/alarxx/Tensor-library
Description: <Description>
Provided “as is”, without warranty of any kind.
Copyright © 2025 Alar Akilbekov. All rights reserved.
Third party copyrights are property of their respective owners.
Если вы собираетесь модифицировать MPL покрытые файлы (IANAL):
- Ваш fork проект = MPL Covered files + Not Covered files (1.7).
- MPL-2.0 is file-based weak copyleft действующий только на Covered файлы, то есть добавленные вами файлы и исполняемые файлы, полученные из объединенения с вашими, могут быть под любой лицензией (3.3).
- (но под copyleft могут подпадать и новые файлы в которых copy-paste-нули код из Covered) (1.7).
- Покрытыми лицензией (Covered) считаются файлы с license notice (e.g. .cpp, .hpp) и любые исполняемые виды этих файлов (e.g. .exe, .a, .so) (1.4).
- You may not remove license notices (3.4), как и в MIT, Apache, BSD (кроме 0BSD) etc.
- При распространении любой Covered файл должен быть доступен, но разрешено личное использование или только внутри организации (3.2).
- Если указан Exhibit B, то производную запрещается лицензировать с GPL.
- Contributor дает лицензию на любое использование конкретной интеллектуальной собственности (patent), которую он реализует в проекте (но не trademarks).
Эти разъяснения условий не меняют и не вносят новые юридические требования к MPL.
Alar Akilbekov - [email protected]
C++:
- Modern C++: https://github.com/federico-busato/Modern-CPP-Programming
- Make: https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
- CMake: https://cmake.org/cmake/help/v3.31/guide/tutorial/index.html
CUDA:
- CUDA C++ Programming Guide. https://docs.nvidia.com/cuda/cuda-c-programming-guide/
Fundamental Deep Learning Books:
- Bishop, C. M., & Nasrabadi, N. M. (2006). Pattern recognition and machine learning (Vol. 4, No. 4, p. 738). New York: springer.
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep learning. The MIT press.
- Raschka, S., Liu, Y., Mirjalili, V., & Dzhulgakov, D. (2022). Machine learning with PyTorch and Scikit-Learn: Develop machine learning and deep learning models with Python. Packt.
- Zhang, A., Lipton, Z. C., Li, M., & Smola, A. J. (2023). Dive into deep learning. Cambridge University Press.
Beginner level books:
- Rashid, T. (2016). Make Your own neural network. CreateSpace Independent Publishing Platform.
- Weidman, S. (2019). Deep learning from scratch: Building with Python from first principles (First edition). O’Reilly Media, Inc.
- Patterson, J., & Gibson, A. (2017). Deep learning: A practitioner’s approach (First edition). O’Reilly.
OpenCV:
- Kaehler, A., & Bradski, G. (2016). Learning OpenCV 3: computer vision in C++ with the OpenCV library. " O'Reilly Media, Inc.".
- Szeliski, R. (2022). Computer vision: algorithms and applications. Springer Nature.
- Прохоренок, Н. А. (2018). OpenCV и Java. Обработка изображений и компьютерное зрение. БХВ-Петербург.
YouTube:
- Евгений Разинков. (2023). Machine Learning (2023, spring). https://www.youtube.com/playlist?list=PL6-BrcpR2C5SCyFvs9Xojv24povpBCI6W
- Евгений Разинков. (2022). Лекции по машинному обучению (осень, 2022). https://www.youtube.com/playlist?list=PL6-BrcpR2C5QYSAfoG8mbQUsI9zPVnlBV
- Евгений Разинков. (2021). Лекции по Advanced Computer Vision (2021). https://www.youtube.com/playlist?list=PL6-BrcpR2C5RV6xfpM7_k5321kJrcKEO0
- Евгений Разинков. (2021). Лекции по Deep Learning. https://www.youtube.com/playlist?list=PL6-BrcpR2C5QrLMaIOstSxZp4RfhveDSP
- Евгений Разинков. (2020). Лекции по компьютерному зрению. https://www.youtube.com/playlist?list=PL6-BrcpR2C5RZnmIWs6x0C2IZK6N9Z98I
- Евгений Разинков. (2019). Лекции по машинному обучению. https://www.youtube.com/playlist?list=PL6-BrcpR2C5RYoCAmC8VQp_rxSh0i_6C6