[C/C++ 인공지능] Deep Learning from Scratch in C++: Tensor Programming

Let’s have fun by learning how to use the Eigen Tensor API.

source

Tensors are the primary way to represent data in Deep Learning algorithms. They are widely used to implement inputs, outputs, parameters, and internal states during the algorithm execution.

In this story, we are going to learn how to use the Eigen Tensor API to develop our C++ algorithms. Specifically, we shall talk about:

  • What are tensors
  • How to define tensors in C++
  • How to compute tensor operations
  • Tensor Reductions and Convolutions

At the end of this story, we shall implement Softmax as an illustrative example of applying tensors for deep learning algorithms.

Text for beginners: this text requires entry-level programming background and a basic understanding of machine learning.

What is a Tensor?

Tensors are grid-like data structures that generalize the concepts of vectors and matrices for an arbitrary number of axis. In machine learning, we usually use the word “dimension” instead of “axis”. The number of different dimensions of a tensor is also called tensor rank:

different rank tensors

In practice, we use tensors to represent data in our algorithms, performing arithmetic operations with them.

The simpler operation we can perform with tensors are the so-called element-wise operations: given two operand tensors with the same dimensions, the operation results in a new tensor with the same dimensions where the value of each coefficient is obtained from the binary evaluation on the respective elements in the operands:

coefficient wise multiplication

The example above is an illustration of the coefficient-wise product of two 2-rank tensors. This operation is still valid for any two tensors since they have the same dimensions.

Like matrices, we can perform other more sophisticated operations with tensors, such as the matrix-like product, convolutions, contractions, reductions, and a myriad of geometric operations. In this story, we will learn how to use the Eigen Tensor API to perform some of these tensor operations, focusing on the most important for implementing deep learning algorithms.

How to declare and use tensors in C++

As we know, Eigen is a linear algebra library broadly used for matrix computations. In addition to the well know support for matrices, Eigen also has an (unsupported) module for Tensors.

Although the Eigen Tensor API is denoted as unsupported, it is actually well supported by the developers of the Google TensorFlow framework.

We can easily define a tensor using Eigen:

#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;
}

The line

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

creates a tensor object and allocates the required memory to store 2x3x4 ints. In this example,my_tensor is a 3-rank tensor where the size of the first dimension is 2, the size of the second dimension is 3, and the size of the last dimension is 4. We can represent my_tensor as follows:

We can set the tensor data if we want it:

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

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

or use random values instead. For example, we can do:

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

and use this kernel later on to perform convolutions. We will cover convolutions soon in this story. First, let’s learn how to use TensorMaps.

Creating tensor views with Eigen::TensorMap

Sometimes, we have some data allocated and only want to manipulate it using a tensor. Eigen::TensorMap is similar to Eigen::Tensor but, instead of allocating new data, it is only a view of the data passed as the parameter. Check the example below:

//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";

In this example, it is easy to see that (by default) Tensors in Eigen Tensor API are col-major. col-major and row-major refer to the way how the grid data is stored in linear containers (check this article on Wikipedia):

source

Although we can use row-major tensors, it is not recommended:

Only the default column major layout is currently fully supported, and it is therefore not recommended to attempt to use the row major layout at the moment.

Eigen::TensorMap is very useful because we can use it to save memory, which is critical for high-demanding applications such as deep learning algorithms.

Performing unary and binary operations

The Eigen Tensor API defines common arithmetic overload operators, making programming tensors intuitive and straightforward. For example, we can add and subtract tensors:

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";

The Eigen Tensor API has several other element-wise functions like .exp() such as sqrt() , log() , and abs() . In addition, we can use unaryExpr(fun) as follows:

auto cosine = [](float v) {return cos(v);};

Eigen::Tensor<float, 2> D = A.unaryExpr(cosine);

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

Similarly, we can use 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";

Lazy evaluation and the auto keyword

The Google engineers who worked on the Eigen Tensor API followed the same strategies found at the top of the Eigen Library. One of these strategies, and probably the most important one, is the way how expressions are lazily evaluated.

The lazy evaluation strategy consists of retarding the actual evaluation of expressions to combine multiple chained expressions in an optimized equivalent one. Thus, instead of evaluating multiple individual expressions step-by-step, the optimized code evaluates only one expression, aiming to leverage the final overall performance.

For example, if A and B are tensors, the expression A + B does not actually evaluates the sum of A and B. In fact, the expressionA + B results in a special object that knows how to compute A + B . The operation will only be performed when this special object is assigned to an actual tensor. In other words, in the following statement:

auto C = A + B;

C is not the actual result of A + B but just a computation object (an Eigen::TensorCwiseBinaryOp object indeed) that knows how to calculate A + B. Only when C is assigned to a tensor object (an object of typeEigen::TensorEigen::TensorMapEigen::TensorRef, etc) it will be evaluated to provide the proper tensor values:

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

Of course, this does not make sense for small operations like A + B . However, this behavior is useful for long operation chains, where the computation can be optimized before being evaluated. In resume, as a general guideline, instead of writing a code like this:

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

we should write a code like this:

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

The difference is that, in the former, C and D are actually Eigen::Tensor objects, whereas in the later code, they are only lazy-computation operations.

In resume, using lazy computations to evaluate a long chain of operations is preferable because the chain will be internally optimized, eventually resulting in faster executions.

Geometric Operations

Geometrics operations result in tensors with different dimensions and, sometimes, sizes. Examples of these operations are: reshape , pad , shuffle , stride , and broadcast .

It is noteworthy that the Eigen Tensor API does not have a transpose operation. We can emulate transpose using shuffle though:

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";

We will see some examples of geometric operations later on when we talk about the softmax example using tensors.

Reductions

Reductions are a special case of operations that results in a tensor with fewer dimensions than the original. Intuitive cases of reductions are sum() and 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";

In the example above, we reduced all the dimensions once. We can also perform reductions along specific axes. For example:

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";

The Eigen Tensor API has a set of pre-built reducing operations, such as prod , any , all , mean , and others. If any of the pre-built operations are not suitable for a specific implementation, we can use reduce(dims, reducer) providing a customreducer functor as a parameter.

Tensor Convolutions

In one of the previous stories, we learned how to implement 2D convolutions using only plain C++ and Eigen Matrices. Indeed, it was necessary because there is no built-in convolution for matrices in Eigen. Luckily, the Eigen Tensor API has a handy function to perform convolutions on Eigen Tensor objects:

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";

Note that we can perform 2D, 3D, 4D, etc, convolutions by controlling the dimensions of the slide in the convolution.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

Softmax with tensors

When programming deep learning models, we use tensors instead of using matrices. It turns out that matrices can represent one or at most two-dimensional grids meanwhile we have higher-dimensional data multi-channel images or batches of registers to handle. That is where tensors come to play.

Let’s consider the following example where we have two batches of registers, each batch having four registers and each register having three values:

We can represent this data as follows:

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";

Now, let’s apply softmax to this data:

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

Softmax is a popular activation function. We covered its implementation using Eigen::Matrixin a previous story. Now, let’s introduce the implementation using Eigen::Tensor instead:

#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;
}

This code outputs:

We will not go into the Softmax details here. Do not hesitate to read again the previous story here on Medium if you need a review of the Softmax algorithm. Now, we are only focused on understanding how to use Eigen Tensors to code our Deep Learning models.

The first thing to note is that the function softmax(z) does not actually calculate the value of softmax for the parameter z . In fact, softmax(z) only mounts a complex object which can compute softmax.

The actual value will be evaluated only when the result of softmax(z) is assigned to a tensor-like object. For example, here:

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

Before this line, everything is only the computation graph of softmax, hopefully optimized. This happened only because we used the keyword auto in the body of softmax(z) . Thus, the Eigen Tensor API can optimize the whole computation of softmax(z) using fewer operations, which improves both processing and memory usage.

Before finishing this story, I would like to point out the tensor.reshape(dims) and tensor.broadcast(bcast) calls:

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) is a special geometric operation that generates another tensor with the same size as the original tensor but with different dimensions. Reshape does not change the order of the data internally in the tensor. For example:

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";

Note that, in this example, the size of X and Y is either 6, although they have very different geometry.

tensor.broadcast(bcast) repeats the tensor as many times as provided in the bcast parameter for each dimension. For example:

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";

Different of reshapebroadcast does not changes the tensor rank (i.e., the number of dimensions) but only increases the size of the dimensions.

Limitations

The Eigen Tensor API docs cite some limitations of which we can be aware:

  • The GPU support was tested and optimized to the float type. Even if we can declare Eigen::Tensor<int,...> tensor; , the usage of non-float tensors is discouraged when using the GPU.
  • The default layout (col-major) is the only one actually supported. We shouldn’t use row-major, at least for now.
  • The max number of dimensions is 250. This size is only achieved when using a C++11-compatible compiler.

Conclusion and Next Steps

Tensors are essential data structures for machine learning programming, allowing us to represent and handle multi-dimensional data as straightforwardly as regular two-dimensional matrices.

In this story, we introduced the Eigen Tensor API and learned how to use tensors relatively easily. We also learned that the Eigen Tensor API has a lazy evaluation mechanism, resulting in an optimized execution in terms of memory and processing time.

To ensure that we understood Eigen Tensor API usage, we covered an example of coding Softmax using tensors.

In the next stories, we will continue developing high-performing deep learning algorithms from scratch using C++ and Eigen, in particular, using the Eigen Tensor API.

Code

You can find the code used in this story in this repository on GitHub.

References

[1] Eigen Tensor API

[2] Eigen Tensor Module

[3] Eigen Gitlab repository, https://gitlab.com/libeigen/eigen

[4] Charu C. Aggarwal, Neural Networks and Deep Learning: A Textbook (2018), Springer

[5] Jason Brownlee, A Gentle Introduction to Tensors for Machine Learning with NumPy

About this series

In this series, we will learn how to code the must-to-know deep learning algorithms such as convolutions, backpropagation, activation functions, optimizers, deep neural networks, and so on using only plain and modern C++.

This story is: Using Eigen Tensor API

Check other stories:

0 — Fundamentals of deep learning programming in Modern C++

1 — Coding 2D convolutions in pure C++

2 — Cost Functions using Lambdas

3 — Implementing Gradient Descent

4 — Activation Functions

… more to come.

 

[출처] https://ai.plainenglish.io/deep-learning-from-scratch-in-c-tensor-programming-83bca6930e96

 

 

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
78 [linux master][리눅스 마스터][국가기술자격] 리눅스 기초 명령어 : 도움말 보기 -man, info, help file 졸리운_곰 2025.02.18 230
77 [linux master][리눅스 마스터][국가기술자격] 우분투에서 NTFS 를 Read/Write로 마운트 (듀얼 부팅 화일 공유) file 졸리운_곰 2024.12.22 279
76 [linux master] 커맨드 라인에서 display 문제로 gui 프로그램이 열리지 않을 때, 졸리운_곰 2024.12.22 341
75 [linux master][리눅스 마스터][국가기술자격] VirtualBox ubuntu ↔ 윈도우 공유 폴더 설정 file 졸리운_곰 2024.12.20 298
74 [linux master][리눅스 마스터][국가기술자격] How to fix sudo after "chmod -R 777 /usr/bin"? /usr/bin/sudo must be owned by uid 0 and have the setuid bit set 졸리운_곰 2024.09.23 754
73 [linux master][리눅스 마스터][국가기술자격] [리눅스/find] 파일찾기 - 허가 거부 제외하고 결과물 출력하기 file 졸리운_곰 2024.07.21 327
72 [linux master][리눅스 마스터][국가기술자격] [리눅스] socat 명령어(proxy) file 졸리운_곰 2024.02.28 349
71 [linux master][리눅스 마스터][국가기술자격] [리눅스] Linux nc 명령어는 무엇이고 어떻게 쓰는 가? 졸리운_곰 2024.02.28 345
70 [linux master][리눅스 마스터][국가기술자격] [리눅스] AWK 명령어 사용법과 여러가지 실용적인 예제 모음 file 졸리운_곰 2024.02.02 300
69 [linux master][리눅스 마스터][국가기술자격] [vi 명령어] vi 명령어 정리 file 졸리운_곰 2024.02.02 284
68 [linux telnet(ssh) 창에서 MSDOS emulation 에뮬레이션 Install dosemu2 dos emulator in Ubuntu and Ubuntu based distributions file 졸리운_곰 2024.01.05 291
67 [linux master][리눅스 마스터][국가기술자격] [Azure] Linux(ubuntu) 우분투 서버 GUI 환경으로 원격 접속하기 - feat. Ubuntu-desktop file 졸리운_곰 2023.11.22 337
66 [linux master][리눅스 마스터][국가기술자격] [Linux] vim(vi) editor 한글 입력시 깨짐 해결방법 file 졸리운_곰 2023.11.22 338
65 [리눅스][linux] 리눅스 서버 다운 원인 분석과 해결 (책) 방안 졸리운_곰 2023.10.03 236
64 [리눅스][linux] Ubuntu/Linux에서 user password를 짧거나 쉬운 password로 변경하는 방법. file 졸리운_곰 2023.01.08 335
63 [linux master][리눅스 마스터][국가기술자격] [GCP 원데이] 서버가 죽는 이유, Message Queue file 졸리운_곰 2023.01.01 225
62 [linux master][리눅스 마스터][국가기술자격] 리눅스 서버 다운 원인 5가지 졸리운_곰 2023.01.01 225
61 [linux master][리눅스 마스터][국가기술자격] Shell In A Box-원격 Linux 서버에 액세스하기위한 웹 기반 SSH 터미널 file 졸리운_곰 2022.11.17 266
60 [linux dev env] [우분투 서버] noVNC 접속 file 졸리운_곰 2022.11.16 341
59 [Linux WSL2] WSL2에서 Ubuntu GUI 프로그램 실행하기 (VcXsrv) file 졸리운_곰 2022.08.20 319
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED