LoginSignup
15
11

Eigen Tensors ドキュメント日本語訳

Last updated at Posted at 2019-02-08

まえがき

Eigen-Unsupportedにテンソルの実装があります.
四則演算やリダクション(maximumとかsumとか),strideやshuffleなどのオペレータが実装されています.
本家と同様,基本的に遅延評価になっているので省メモリで良い感じに走ると思います.
ちょいちょい動きを忘れるので,公式ドキュメントをざっくり日本語訳しておきました.

元表現の完全リスペクト訳は目指しておらず理解しやすさを重視して意訳していますが,英語的表現に引きずられてしまっているかもしれません.適宜追加情報もサイレント挿入しています.
また,よくわからなかったところは英語も併記しておきます.

翻訳当時は元のドキュメントは工事途中のようで,TODOが散見されています.もし更新が見つかったら反映したいと思います.

よければリダクションオペレータを実装してみた体験談もご覧ください.

Eigen Tensors

Tensor(テンソル)は複数次元の配列です.その要素は通常スカラ値ですが,文字列のようなその他のタイプの値もサポートしています.

Tensor Classes

Eigen Tensorsには,複数の種類のTensorクラスが定義されています.これらはEigen名前空間の中に定義されています.

Class Tensor<data_type, rank>

基本的なテンソルのクラスです.テンソルの作成,メモリの生成が行われます.
テンソルのデータ型(float, intなど)と,テンソルのランク(階数)によってテンプレート化されています.
ランクとは,テンソルの次数です.例えば,ランク2のテンソルは,行列です.

このクラスのTensorはリサイズ可能です.
例えば,異なるサイズのテンソルを代入した場合,新しいサイズのテンソルに合致するようにリサイズされます.

Constructor Tensor<data_type, rank>(size0, size1, ...)

Tensorのコンストラクタです.
コンストラクタには,rankで指定した個数の整数を引数に与えます.引数は,各次元におけるサイズです.

// サイズが2,3,4の,ランク3のテンソルを生成する.
// 24個(24 = 2 x 3 x 4)のfloat型の値のメモリを確保する.
Tensor<float, 3> t_3d(2, 3, 4);

// 異なるサイズのテンソルを与えることでリサイズ.ランクは変わらない.
t_3d = Tensor<float, 3>(3, 4, 3);

Constructor Tensor<data_type, rank>(size_array)

サイズを配列で定義する場合のコンストラクタです.この引数に指定する配列はEigen::array<Eigen::Index>型です.
この配列は初期化リストで自動生成できます.

// サイズ5, 7のランク2の文字列型テンソルを作成する.
Tensor<string, 2> t_2d({5, 7});

Class TensorFixedSize<data_type, Sizes<size0, size1, ...>>

コンパイル時にサイズが既知である,固定サイズのテンソルのクラスです.
コンパイラがサイズを知っているため,計算が高速になります.
サイズは変更不可能です.

固定サイズテンソルの中の要素数が少ない場合,stack領域にメモリが確保され,ヒープ領域でメモリ割り当て・メモリ解放が行われなくなります.

// 4 x 3 のfloatがデータ型のテンソルを作る.
TensorFixedSize<float, Sizes<4, 3>> t_4x3;

Class TensorMap<Tensor<data_type, rank>>

コードの他の部分で割り当てられたメモリを流用したテンソルを作成します.
あるメモリ領域をテンソルとみなすような使い方ができます.
このクラス自身が確保,管理するメモリ領域はありません.

自身でメモリを管理していないため,サイズは変更不可能です.

Constructor TensorMap<Tensor<data_type, rank>>(data, size0, size1, ...)

コンストラクタです.
データのストレージへのポインタと,ランク数分のサイズ値を渡します.ストレージは,全要素数より大きいサイズである必要があります.

// スタック領域に割り当てられたストレージを使った整数型TensorのMap
int storage[128];  // 2 x 4 x 2 x 8 = 128
TensorMap<Tensor<int, 4>> t_4d(storage, 2, 4, 2, 8);

// 同じストレージでも,異なるランクのテンソルとしてみなせる.
// ここでも他クラスと同様に,配列でもサイズを渡せる.
TensorMap<Tensor<int, 2>> t_2d(storage, 16, 8);

// 固定サイズテンソルもmapできる.
// 2Dの固定サイズテンソルの1D化.
TensorFixedSize<float, Sizes<4, 3>> t_4x3;
TensorMap<Tensor<float, 1>> t_12(t_4x3.data(), 12);

Class TensorRef

以下のAssigning TensorRefを参照してください.

Accessing Tensor Elements

<data_type> tensor(index0, index1...)

テンソルにおける位置(index0, index1...)の値を返します.
左辺値として使用でき,指定した位置の要素の値を変更できます.
返り値のデータ型はテンソルに依ります.

// 位置(0, 1, 0)にデータを設定する.
Tensor<float, 3> t_3d(2, 3, 4);
t_3d(0, 1, 0) = 12.0f;

// 全要素の値を乱数で初期化する.
for (int i = 0; i < 2; ++i) {
  for (int j = 0; j < 3; ++j) {
    for (int k = 0; k < 4; ++k) {
      t_3d(i, j, k) = ...some random value...;
    }
  }
}

// Tensorの要素を出力する.
for (int i = 0; i < 2; ++i) {
  LOG(INFO) << t_3d(i, 0, 0);
}

TensorLayout

このTensorライブラリは,ColMajor (デフォルト) と RowMajor という,2つのメモリレイアウトを提供しています.

デフォルトのcolumn majorレイアウトは完全サポートされています.
一方で,row majorレイアウトの使用は現段階では推奨されません.
(訳注:2022年5月6日時点で上記記述が外されました.)

テンソルのレイアウトは型として補助的に定義されます.
明示しない場合,column majorとみなされます.

Tensor<float, 3, ColMajor> col_major;  // Tensor<float, 3> と同じ
TensorMap<Tensor<float, 3, RowMajor> > row_major(data, ...);

オペレータ中の全ての引数には,同じレイアウトのものを使用してください.
異なるレイアウトを混ぜた場合,コンパイルエラーとなります.

swap_layout()メソッドにより,テンソルのレイアウトや表現を変更できます.
次元の順番も逆になることに注意してください.

Tensor<float, 2, ColMajor> col_major(2, 4);
Tensor<float, 2, RowMajor> row_major(2, 4);

Tensor<float, 2> col_major_result = col_major;  // レイアウトが合致している例
Tensor<float, 2> col_major_result = row_major;  // コンパイルエラーの例

// シンプルなレイアウトのswap
col_major_result = row_major.swap_layout();
eigen_assert(col_major_result.dimension(0) == 4);
eigen_assert(col_major_result.dimension(1) == 2);

// 次元の順番を保ってswap
array<int, 2> shuffle(1, 0);
col_major_result = row_major.swap_layout().shuffle(shuffle);
eigen_assert(col_major_result.dimension(0) == 2);
eigen_assert(col_major_result.dimension(1) == 4);

Tensor Operations

Eigen Tensor ライブラリは,代数演算(加算減算乗算),ジオメトリ演算(スライス,シャッフル)等の,テンソルにおける演算(Operator)を広く提供しています.
これらの演算はTensorクラスのメソッドとして使用可能で,いくつかは演算子オーバロードされています.
例えば次のコードは2つのテンソルの要素ごとの加算を行っています.

Tensor<float, 3> t1(2, 3, 4);
...t1の値を設定...
Tensor<float, 3> t2(2, 3, 4);
...t2の値を設定...
// t3はt1とt2の要素ごとの和になる
Tensor<float, 3> t3 = t1 + t2;

上記のコードは簡単に見えるのですが,実はt1 + t2は実際にはまだ値の加算は行われていないことが重要です.
この式は,TensorCwiseBinaryOp型の,t1t2を参照するtensor operatorを代わりに生成しています.

この式の値がテンソルt3に代入された時に初めて実際に加算処理が行われます.
技術的には,Tensorクラスにおけるoperator=()のオーバロードにより行われています.

このテンソル計算メカニズムは,遅延評価・最適化を実現でき,高速な処理に繋がります.

もちろん,このテンソル演算は入れ子を行います.t1 + t2 * 0.3fという式は実際には次のような演算のツリーで表現されます.

TensorCwiseBinaryOp<scalar_sum>(t1, TensorCwiseUnaryOp<scalar_mul>(t2, 0.3f))

Tensor Operations and C++ "auto"

テンソル演算はテンソル演算子を生成するため,C++のautoキーワードは直感的な意味を持ちません.
次の2行のコードを考えてみます.

Tensor<float, 3> t3 = t1 + t2;
auto t4 = t1 + t2;

一行目は,t1t2の足し算をテンソルt3に代入することで,結果が格納されます.
二行目は,t1t2足し算を計算するであろう,実際はテンソル演算のツリーであるt4の定義です.
実は,t4はテンソルではなく,この要素の値を受け取ることはできません.
t4はTensorCwiseBinaryOpになります.TensorCwiseBinaryOpは値が未評価なので要素アクセス演算は定義できません.)

Tensor<float, 3> t3 = t1 + t2;
cout << t3(0, 0, 0);  // ここで足し算を評価して,その結果の(0,0,0)の値を表示

auto t4 = t1 + t2;
cout << t4(0, 0, 0);  // コンパイルエラー

autoを使う場合,Tensorとして結果を得ることはできません.代わりに,まだ計算されていない式が得られます.
従って,autoは遅延評価したい場合に使用します.

残念ながら,未評価式を格納するための具体的に用意された型はありません.それゆえ,未評価式を保持したい場合はautoを使う必要があります.

テンソル計算の集合を結果として得たい場合,それを確保できるテンソルに代入する必要があります.
それは,通常のTensorでも可能ですし,TensorFixedSizeでも,既存メモリ上のTensorMapでも可能です.
次の三例はすべて問題なく動作します.

auto t4 = t1 + t2;

Tensor<float, 3> result = t4;  // こっちでもok→ result(t4);
cout << result(0, 0, 0);

TensorMap<float, 4> result(<a float* with enough space>, <size0>, ...) = t4;
cout << result(0, 0, 0);

TensorFixedSize<float, Sizes<size0, ...>> result = t4;
cout << result(0, 0, 0);

結果が要るようになる前であれば,演算式を保持しておけます.そして,追加の演算に再利用できます.
演算として式を保持しておいてある限り,計算は発生しません.

// exp((t1 + t2) * 0.2f) の計算方法その1
auto t3 = t1 + t2;
auto t4 = t3 * 0.2f;
auto t5 = t4.exp();
Tensor<float, 3> result = t5;

// 別解.その1より効率的.
Tensor<float, 3> result = ((t1 + t2) * 0.2f).exp();

Controlling When Expression are Evaluated

式が評価されるタイミングをコントロールする方法がいくつかあります.

  • Tensor, TensorFixedSize, TensorMap への代入.
  • eval() メソッドの使用.
  • TensorRefへの代入.

Assigning to a Tensor, TensorFixedSize, or TensorMap.

式を評価する一般的な方法は,Tensorへの代入です.
次の例では,auto定義は中間の値"Operation"を作ります.Tensorではありません.そして式の評価は発生しません.
一方で,Tensorへの結果の代入により,すべての演算が評価されます.

auto t3 = t1 + t2;             // t3 は Operation.
auto t4 = t3 * 0.2f;           // t4 は Operation.
auto t5 = t4.exp();            // t5 は Operation.
Tensor<float, 3> result = t5;  // Operationがこの時計算される.

Operationのランクやサイズを知っているならばTensorの代わりにTensorFixedSizeにOperationを代入できます.
こちらのほうが少し効率的です.

// 結果が 4x4x2 のテンソルだとわかっている
TensorFixedSize<float, Sizes<4, 4, 2>> result = t5;

同様にして,TensorMapへの式の代入は評価を発生させます.
TensorFixedSizeのように,TensorMapはリサイズ不可能なので,代入が行われる式と同じランクとサイズを持っていないければなりません.

Calling eval().

大規模な合成式を評価するとき,「ここの式はその時に評価されるべきである」とEigenに教えたいときがあります.
それはOperation式のevalを呼ぶことで実現できます.

// 前の例はこのようにも書ける
Tensor<float, 3> result = ((t1 + t2) * 0.2f).exp();

// (t1 + t2) の計算を事前に行わせたい場合
Tensor<float, 3> result = ((t1 + t2).eval() * 0.2f).exp();

意味的には,eval()の呼び出しは,適当なサイズの一時テンソルにおける式の値のmaterializingと等価です.
上のコードはこちらと同様の動作をします.

// .eval() はテンソルのサイズを知っている!
TensorFixedSize<float, Sizes<4, 4, 2>> tmp = t1 + t2;
Tensor<float, 3> result = (tmp * 0.2f).exp();

eval()の返り値はOperationそれ自身になります.したがって次のコードは想定の動作をしません.

// t3 は評価Operator.  t3 はまだ評価されない.
auto t3 = (t1 + t2).eval();

// t3 を他の式に使える. まだ評価はされない.
auto t4 = (t3 * 0.2f).exp();

// TensorにOperatorを代入した時に評価される,このとき,
// t3の値を表現するために中間テンソルインスタンスが内部で作られている.
Tensor<float, 3> result = t4;

上記の例でのeval()の呼び出しはパフォーマンスにおける差異はありませんが,他のケースでは大きな意味を持つことがあります.
次の式では,broadcast()X.maximum()の評価を何回も発生させてしまいます.
(訳注:broadcast()は,同テンソルを各次元方向に繰り返しでタイリングする処理.maximum+reshapeの結果をN個タイリングするわけだが,下記コードはこのmaximum+reshapeをN回発生させてしまう.特にmaximum()は重い処理であり,非常によろしくない.maximumの結果を先に用意しておいて,それをbroadcastするなら,maximumの処理は1回だけで済むのでお得.)

Tensor<...> X ...;
Tensor<...> Y = ((X - X.maximum(depth_dim).reshape(dims2d).broadcast(bcast))
                * beta).exp();

maximum()reshape()の間にeval()の呼び出しをはさむことで,maximum()は一回だけ評価され,実行が大きくスピードアップします.

Tensor<...> Y =
  ((X - X.maximum(depth_dim).eval().reshape(dims2d).broadcast(bcast))
  * beta).exp();

次の例では,テンソルYは式と代入の両方に使われています.
この時エイリアシング問題が起こっており,もし右項の計算が先に終了していないと,Yは評価中に逐次的に更新されてしまい,おかしな結果になります.

 Tensor<...> Y ...;
 Y = Y / (Y.sum(depth_dim).reshape(dims2d).broadcast(bcast));

sum()reshape()の間にeval()の呼び出しをはさむことで,Yの更新が行われる前にsumによる加算が行われるようになります.

 Y = Y / (Y.sum(depth_dim).eval().reshape(dims2d).broadcast(bcast));

なお,右辺のi番目の値を左辺に入れる前に計算する必要だけあるので,右辺のすべてをevalにいれる必要はありません.
(直訳:The generated は それを左辺に割り当てる前に 右辺のi番目の値を 計算する必要がある)
Note that an eval around the full right hand side expression is not needed because the generated has to compute the i-th value of the right hand side before assigning it to the left hand side.
(訳注:内部ではテンソルの各要素に注目して計算が行われ,各要素における計算が逐次行われていく.ここで,要素ごとの演算には右辺の計算だけでなく代入演算も含まれているのである.すなわち,0番目の要素の計算が済み,代入が終わった後に1番目の要素の計算が行われることになるが,実はEigen Tensorでは,左辺のYと右辺のYは同じメモリ上の値を参照することになっている.つまり,計算済みの0番目の要素を使って1番目の要素の計算が行われてしまう(sumはそういう計算をする).自己代入的な演算をする場合には,ある要素について計算するときに他の要素を参照する必要がある計算(sumなど)において注意が必要というわけである.周りを参照しない要素ごとの足し算などでは問題がない.ちなみに上記の例では,エイリアシング問題抜きでも,sumをbroadcastする形がこの前の例と同様であり非常によろしくないので,sumに対するeval付与は欠かせない.)

しかしながら,Yのシャッフルに対して式の値の代入する場合,正しい評価を行わせるために右辺全体にeval()呼び出しを追加する必要があります.
(訳注:代入における左辺の要素値の場所に対応する右辺の要素値の場所が異なってしまうのでevalが必要なパターン.)

 Y.shuffle(...) =
  (Y / (Y.sum(depth_dim).eval().reshape(dims2d).broadcast(bcast))).eval();

(訳注:ところで,ここまで怖い情報を聞かされると「とにかくevalだ!」とか思うだろうが,それをすると中間結果のTensorメモリがevalするたびに毎回作られてしまうわけで,非常によろしくない(ライブラリとしてのうま味がなくなる).evalが必要なパターンを認知しておき,適切に使用するのがベター.evalをしなければならないパターンは以下.
①ある要素に複数回アクセスする演算(broadcastやconvolutionなど)があり,かつその前の演算が重いとき.
②左辺と右辺に同じテンソルを使うとき,かつ要素ごとの演算として完結していないとき(畳み込み,行列積や,shuffle,reverseなどの要素の場所をずらすとき).)

Assigning to a TensorRef.

式の値において数個の要素のみアクセスしたい場合,テンソル全体のmaterializingを防ぐためにTensorRefが使えます.

TensorRefはEigenのOperationの小さなラッパークラスです.
式の個々の値へのアクセスが行える,()演算子のオーバーロードを提供しています.
Operationそれ自身は個々の要素へのアクセスを提供していないため,TensorRefは便利です.

// 式に対するTensorRefの作成.式はまだ評価されない.
TensorRef<Tensor<float, 3> > ref = ((t1 + t2) * 0.2f).exp();

// 個々の要素へのアクセスに "ref" を使用.飛び地的に式が評価される.
float at_0 = ref(0, 0, 0);
cout << ref(0, 1, 0);

式の値の部分値を得たい場合にだけTensorRefを使用してください.
TensorRefはアクセスしようとした要素のみを計算します.
ただし全ての値にアクセスしようとする場合は,最初にTensorの結果をmaterializeしたほうが速いです.

いくつかのケースでは,テンソル全体の結果がとても大きくなった場合,TensorRefでアクセスすることによりメモリを節約できます.
ただしすべてのケースにというわけではありません.なので,過信しないでください.

Controlling How Expressions Are Evaluated

テンソルライブラリは縮約や畳み込みといった種々の演算をいくつか実装しています.
実装は環境によって最適化されています.シングルスレッドCPU,マルチスレッドCPU,CUDAを使ったGPU...
追加の実装は後で追加されると思います.

device()を呼び出すことで,どの実装にするか選択できます.
明示しない場合,デフォルトの実装のシングルスレッドCPUが使われます.

デフォルト実装は最近のIntel CPUに最適化されています.SSE, AVX, FMA を使っています.
ARM CPUへのチューニングが進められています.
SSE, AVX, その他の命令を有効にするためのコンパイラオプションが必要です.

例えば,次のコードはデフォルトのシングルスレッドCPU実装による2つのテンソルの足し算を行います.

Tensor<float, 2> a(30, 40);
Tensor<float, 2> b(30, 40);
Tensor<float, 2> c = a + b;

異なる実装を使うには,結果の代入の前にdevice()呼び出しをはさむ必要があります.
C++の技術的な理由によって,結果を格納するテンソルは自分自身で宣言される必要があります.つまり,結果のサイズが分かっている必要があります.
(訳注:Tensor型に異なるサイズのテンソルを代入しようとすると,左辺値のテンソルがリサイズされた上でコピーされるのですが,device()を使う場合はこの暗黙的リサイズ機能を使ってはいけないということ.)

Eigen::Tensor<float, 2> c(30, 40);
c.device(...) = a + b;

device()は演算子=の左辺の最後の呼び出しに付けます.
現在,DefaultDevice, ThreadPoolDevice, GpuDeviceの3つのdeviceを使えます.

Evaluating With the DefaultDevice

こちらはdevice()呼び出しを入れないのと同じです.

DefaultDevice my_device;
c.device(my_device) = a + b;

Evaluating with a Thread Pool

// Eigen ThreadPoolを作成.
Eigen::ThreadPool pool(8 /* number of threads in pool */);

// Eigen ThreadPoolDeviceを作成.
Eigen::ThreadPoolDevice my_device(&pool, 4 /* number of threads to use */);

// 式評価に指定したdeviceが使われる.
Eigen::Tensor<float, 2> c(30, 50);
c.device(my_device) = a.contract(b, dot_product_dims);

Evaluating On GPU

現在はthread pool deviceを使うより少々複雑です.
GPU deviceを作成する必要がありますが,同時に明示的にテンソルのメモリをcudaに割り当てる必要があります.

API Reference

Datatypes

こちらでは,テンソルのメソッドおよびOperationで使われているデータ型の説明を示します.

<Tensor-Type>::Dimensions

整数の配列として振る舞います.int size要素を持ち,個々の値に配列のようにインデックスが振られます.テンソルの次元の表現に使われます.
See dimensions().

<Tensor-Type>::Index

intとして振る舞います.テンソルの次元のインデックスに使われます.
See operator(), dimension(), and size().

<Tensor-Type>::Scalar

テンソルの各要素のデータ型を表します.例えば,
Tensor<float> において, Scalarfloat です.
See setConstant().

<Operation>

本ドキュメント中では,この疑似型を,メソッドの戻り値としてテンソルのOperationが返されることを示すのに使います.
評価後に戻されるOperationの型と次元を示します.

Operationは即時に評価されず,後で評価されます.
例えば,テンソルに代入される時に評価されます.
値へのアクセスの前に,この代入を行う必要があります.
あるいは,TensorRefを通じた要素値へのアクセスが可能です.

Built-in Tensor Methods

テンソルに直に作用するメソッドがあります.
これは,結果の遅延評価を提供するOperatorではありません.
特に記述のない限り,以降の全メソッドはTensor, TensorFixedSize, TensorMap 型に実装されています.

Metadata

int NumDimensions

テンソルの次元数を表す定数です.テンソルの "rank" とも言われます.

Eigen::Tensor<float, 2> a(3, 4);
cout << "Dims " << a.NumDimensions;
=> Dims 2

Dimensions dimensions()

テンソルの次元数を表す配列的オブジェクトを返します.
実際の返り値の型は <Tensor-Type>::``Dimensions です.

Eigen::Tensor<float, 2> a(3, 4);
const Eigen::Tensor<float, 2>::Dimensions& d = a.dimensions();
cout << "Dim size: " << d.size << ", dim 0: " << d[0]
     << ", dim 1: " << d[1];
=> Dim size: 2, dim 0: 3, dim 1: 4

もしC++11コンパイラを使用している場合,autoを使うとシンプルにできます.

const auto& d = a.dimensions();
cout << "Dim size: " << d.size << ", dim 0: " << d[0]
     << ", dim 1: " << d[1];
=> Dim size: 2, dim 0: 3, dim 1: 4

Index dimension(Index n)

テンソルのn番目の次元を返します.実際の返り値のデータ型は <Tensor-Type>::Index ですが,intとして扱えます.

Eigen::Tensor<float, 2> a(3, 4);
int dim1 = a.dimension(1);
cout << "Dim 1: " << dim1;
=> Dim 1: 4

Index size()

テンソルの全要素の数を返します.テンソルの全次元数の掛け算になります.
実際の返り値の型は <Tensor-Type>::Index ですが,intとして扱えます.

Eigen::Tensor<float, 2> a(3, 4);
cout << "Size: " << a.size();
=> Size: 12

Getting Dimensions From An Operation

少数のOperationだけ, dimensions() を直に提供します.(例: TensorReslicingOp
ほとんどのoperatorはOperationが評価されるまで次元数の計算を待ちます.
遅延評価されるOperationの次元数にアクセスしたい場合,TensorRefを使うことで(上記のAssigning to a TensorRefを参照),上記のようにdimensions()dimension() が提供されるようになります.

TensorRefはplainなTensor型もラップできます.
生のテンソルとdeferred operation(例:テンソルのスライス)の両方をテンプレートとするのに便利なイディオムになります.
このケースでは,TensorRefをラップでき,underlying typesに対して次元的にagnosticなままにしておけます.

Constructors

Tensor

指定サイズのテンソルを作成します.
引数の数はテンソルの次元数と同じです.
テンソルの内容は初期化されません.

Eigen::Tensor<float, 2> a(3, 4);
cout << "NumRows: " << a.dimension(0) << " NumCols: " << a.dimension(1) << endl;
=> NumRows: 3 NumCols: 4

TensorFixedSize

指定サイズのテンソルを作成します.Size<>テンプレートパラメータの引数の数はテンソルのランク数分です.テンソルの内容は初期化されません.

Eigen::TensorFixedSize<float, Sizes<3, 4>> a;
cout << "Rank: " << a.rank() << endl;
=> Rank: 2
cout << "NumRows: " << a.dimension(0) << " NumCols: " << a.dimension(1) << endl;
=> NumRows: 3 NumCols: 4

TensorMap

既存のデータ配列をマップするテンソルを作成します.
TensorMapが破棄されるまでデータは解放してはいけません.また,データはテンソルの要素数以上のサイズである必要があります.

float data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
Eigen::TensorMap<Eigen::Tensor<float, 2>> a(data, 3, 4);
cout << "NumRows: " << a.dimension(0) << " NumCols: " << a.dimension(1) << endl;
=> NumRows: 3 NumCols: 4
cout << "a(1, 2): " << a(1, 2) << endl;
=> a(1, 2): 7

Contents Initialization

新しいTensorまたはTensorFixedSizeが作られる際,テンソルの要素を確保するメモリが割り当てられます.初期化は行われません.
同様に,未初期化メモリをマップするTensorMapが作られた際も内容は初期化されません.

テンソルのメモリをの初期化には以下のメソッドを使用します.
直ちにテンソルに反映され,結果のテンソルが返されます.
遅延評価をするOperationではありません.

<Tensor-Type> setConstant(const Scalar& val)

テンソルのすべての値を定数valに設定します.
Scalarはテンソル中のデータの型です.
その型に変換できる値ならどれでも渡せます.

他の呼び出しに連結できるテンソルを返します.

a.setConstant(12.3f);
cout << "Constant: " << endl << a << endl << endl;
=>
Constant:
12.3 12.3 12.3 12.3
12.3 12.3 12.3 12.3
12.3 12.3 12.3 12.3

setConstant() はコピーコンストラクタや operator=() を持つ要素の型がデータ型であるテンソルに使用できます.

Eigen::Tensor<string, 2> a(2, 3);
a.setConstant("yolo");
cout << "String tensor: " << endl << a << endl << endl;
=>
String tensor:
yolo yolo yolo
yolo yolo yolo

<Tensor-Type> setZero()

テンソルをゼロで埋めます.
setConstant(Scalar(0)) と等価です.
他の呼び出しに連結できるテンソルを返します.

a.setZero();
cout << "Zeros: " << endl << a << endl << endl;
=>
Zeros:
0 0 0 0
0 0 0 0
0 0 0 0

<Tensor-Type> setValues({..initializer_list})

std::initializer_listで指定される明示された複数の値でテンソルを埋めます.
初期化リストの型は,テンソルのランクとデータ型に依ります.

テンソルのランクがNであるとき,初期化リストはN回の入れ子である必要があります.
テンソルの最終次元のサイズがPであるとき,最奥の入れ子リストはテンソルの型のP個のスカラを含む必要があります.

例えば, TensorFixedSize<float, 2, 3> において,初期化リストは3つのfloatからなるリストを2つ含む必要があります.

setValues() は他の呼び出しに連携できるテンソルを返します.

Eigen::Tensor<float, 2> a(2, 3);
a.setValues({{0.0f, 1.0f, 2.0f}, {3.0f, 4.0f, 5.0f}});
cout << "a" << endl << a << endl << endl;
=>
a
0 1 2
3 4 5

リストが短すぎる場合,テンソルの対応する要素は変更されません.
これは入れ子の各レベルにおいて有効です.
例えば次のコードはテンソルの最初の行にのみ値を設定します.

Eigen::Tensor<int, 2> a(2, 3);
a.setConstant(1000);
a.setValues({{10, 20, 30}});
cout << "a" << endl << a << endl << endl;
=>
a
10   20   30
1000 1000 1000

<Tensor-Type> setRandom()

テンソルを乱数で埋めます.
他の呼び出しに連結できるテンソルを返します.

a.setRandom();
cout << "Random: " << endl << a << endl << endl;
=>
Random:
  0.680375    0.59688  -0.329554    0.10794
 -0.211234   0.823295   0.536459 -0.0452059
  0.566198  -0.604897  -0.444451   0.257742

setRandom() は,乱数生成器をテンプレート引数として与えることでカスタマイズできます.

a.setRandom<MyRandomGenerator>();

MyRandomGenerator は次のメンバ関数で構成される必要があります. Scalar と Index は <Tensor-Type>::Scalar
<Tensor-Type>::Indexと同じです.

例として,TensorFunctions.h中の struct UniformRandomGenerator を参照してください.

// setRandom()で使うカスタム乱数生成器
struct MyRandomGenerator {
  // デフォルトコンストラクタおよびコピーコンストラクタ.どちらも必要.
  MyRandomGenerator() { }
  MyRandomGenerator(const MyRandomGenerator& ) { }

  // 乱数を返す."element_location" は
  // テンソル中にセットする入力の場所で,典型的には
  // 無視できる.
  Scalar operator()(Eigen::DenseIndex element_location,
                    Eigen::DenseIndex /*unused*/ = 0) const {
    return <randomly generated value of type T>;
  }

  // 上と同様だが一度に複数の乱数を生成.
  typename internal::packet_traits<Scalar>::type packetOp(
      Eigen::DenseIndex packet_location, Eigen::DenseIndex /*unused*/ = 0) const {
    return <a packet of randomly generated values>;
  }
};

テンソルライブラリ中のの2つの乱数生成器を使用できます.

  • UniformRandomGenerator
  • NormalRandomGenerator

Data Access

Tensor, TensorFixedSize, TensorRefクラスはテンソルの要素にアクセスするための次のアクセス演算子を提供します.

const Scalar& operator()(const array<Index, NumIndices>& indices)
const Scalar& operator()(Index firstIndex, IndexTypes... otherIndices)
Scalar& operator()(const array<Index, NumIndices>& indices)
Scalar& operator()(Index firstIndex, IndexTypes... otherIndices)

インデックスの数はテンソルのランクと同一である必要があります.
さらに,これらのアクセサはテンソルの式に利用できません.
テンソルの式の値にアクセスするためには,式を評価済みにしておくかTensorRefでラップします.

Scalar* data() and const Scalar* data() const

テンソルのストレージへのポインタを返します.
テンソルがconstならばポインタもconstです.
これによりデータへの直接アクセスができます.
データのレイアウトはテンソルのレイアウト(ColMajor, RowMajor)に依ります.

このアクセスは特殊ケースにのみ使用されます.
例えば,他のライブラリと併用する場合です.

Scalarはテンソルの中のデータ型です.

Eigen::Tensor<float, 2> a(3, 4);
float* a_data = a.data();
a_data[0] = 123.45f;
cout << "a(0, 0): " << a(0, 0);
=> a(0, 0): 123.45

Tensor Operations

以降の文書中のメソッドは未評価のOperationを返します.
メソッドは連結可能です.メソッドの返り値を他のOperationに適用できます.

Operationの連結はレイジーに評価されます.典型的には,テンソルへの代入時です.
詳細は "Controlling when Expression are Evaluated" を参照してください.

<Operation> constant(const Scalar& val)

元のテンソルと同じ型や次元ですが,全要素の値はvalであるテンソルを返します.
例えば,テンソルから定数値を引いたり,各要素にスカラ値を掛ける等の時に使えます.

Eigen::Tensor<float, 2> a(2, 3);
a.setConstant(1.0f);
Eigen::Tensor<float, 2> b = a + a.constant(2.0f);
Eigen::Tensor<float, 2> c = b * b.constant(0.2f);
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
cout << "c" << endl << c << endl << endl;
=>
a
1 1 1
1 1 1

b
3 3 3
3 3 3

c
0.6 0.6 0.6
0.6 0.6 0.6

<Operation> random()

元のテンソルの型や次元は同じですが,全要素が乱数であるテンソルを返します.

既存のテンソルに乱数を加算する例です.
setRandom()と同様に乱数生成器を設定できます.

Eigen::Tensor<float, 2> a(2, 3);
a.setConstant(1.0f);
Eigen::Tensor<float, 2> b = a + a.random();
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
1 1 1
1 1 1

b
1.68038   1.5662  1.82329
0.788766  1.59688 0.395103

Unary Element Wise Operations

こちらのoperationは単一の入力テンソルを引数に取り,計算適用済みである同じ型や次元のテンソルを返します.全要素に直ちに適用されます.

<Operation> operator-()

要素が元のテンソルの加法的逆元である,元のテンソルと同じ型や次元のテンソルを返します.

Eigen::Tensor<float, 2> a(2, 3);
a.setConstant(1.0f);
Eigen::Tensor<float, 2> b = -a;
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
1 1 1
1 1 1

b
-1 -1 -1
-1 -1 -1

<Operation> sqrt()

要素が元のテンソルの平方根である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> rsqrt()

要素が元のテンソルの逆平方根である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> square()

要素が元のテンソルの二乗である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> inverse()

要素が元のテンソルの乗法的逆元である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> exp()

要素が元のテンソルの指数である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> log()

要素が元のテンソルの対数である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> abs()

要素が元のテンソルの絶対値である,元のテンソルと同じ型や次元のテンソルを返します.

<Operation> arg()

要素が元のテンソルの複素数の位相角である,元のテンソルと同じ次元のテンソルを返します.

<Operation> real()

要素が元のテンソルの複素数の実部である,元のテンソルと同じ次元のテンソルを返します.

<Operation> imag()

要素が元のテンソルの複素数の虚部である,元のテンソルと同じ次元のテンソルを返します.

<Operation> pow(Scalar exponent)

要素が元のテンソルのexponent乗である,元のテンソルと同じ型や次元のテンソルを返します.

exponentの型 Scalar は,テンソルの要素の型と同じです.
例えば,整数のテンソルには整数の指数のみ使えます.

この制限を昇格するのにcast()を使用できます.
例えばこちらは整数テンソルの三乗根を計算します.

Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{0, 1, 8}, {27, 64, 125}});
Eigen::Tensor<double, 2> b = a.cast<double>().pow(1.0 / 3.0);
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
0   1   8
27  64 125

b
0 1 2
3 4 5

<Operation> operator * (Scalar scale)

与えられたscaleを入力テンソルの全要素に掛けます.

<Operation> cwiseMax(Scalar threshold)

TODO

<Operation> cwiseMin(Scalar threshold)

TODO

<Operation> unaryExpr(const CustomUnaryOp& func)

TODO

Binary Element Wise Operations

こちらのOperationは引数に2つのテンソルを取ります.入力はどちらも同じ型や次元である必要があります.
適用済みの同じ次元のテンソルを返します.また,特に記述がなければ同じ型になります.
operationは要素の組毎に独立に適用されます.

<Operation> operator+(const OtherDerived& other)

入力テンソルの要素毎の足し算を行った,同じ型や次元のテンソルを返します.

<Operation> operator-(const OtherDerived& other)

入力テンソルの要素毎の引き算を行った,同じ型や次元のテンソルを返します.

<Operation> operator*(const OtherDerived& other)

入力テンソルの要素毎の掛け算を行った,同じ型や次元のテンソルを返します.

<Operation> operator/(const OtherDerived& other)

入力テンソルの要素毎の割り算を行った,同じ型や次元のテンソルを返します.
このoperatorは整数型はサポートしません.

<Operation> cwiseMax(const OtherDerived& other)

入力テンソルの要素毎の最大値を取った,同じ型や次元のテンソルを返します.

<Operation> cwiseMin(const OtherDerived& other)

入力テンソルの要素毎の最小値を取った,同じ型や次元のテンソルを返します.

<Operation> Logical operators

以下の論理演算子がサポートされています.

  • operator&&(const OtherDerived& other)
  • operator||(const OtherDerived& other)
  • operator<(const OtherDerived& other)
  • operator<=(const OtherDerived& other)
  • operator>(const OtherDerived& other)
  • operator>=(const OtherDerived& other)
  • operator==(const OtherDerived& other)
  • operator!=(const OtherDerived& other)

これらは全てbool値のテンソルを返します.

Selection (select(const ThenDerived& thenTensor, const ElseDerived& elseTensor)

Selectionは,if-then-elseの操作と等価な三項演算子です.

Tensor<bool, 3> if = ...;
Tensor<float, 3> then = ...;
Tensor<float, 3> else = ...;
Tensor<float, 3> result = if.select(then, else);

3引数は同じ次元でなければなりません.結果も同じ次元です.
ifテンソルはbool型です.
thenelseのテンソルは同じ型である必要があります.
結果のテンソルの型はこれらの型と同じです.

結果の各要素は,ifがtrueであればthenのものになります.そうでなければ,elseのものになります.

Contraction

テンソルの Contraction (縮約)は複数次元における行列積の一般化です.

// ランク2のテンソルはにより行列を作成
Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{1, 2, 3}, {6, 5, 4}});
Eigen::Tensor<int, 2> b(3, 2);
b.setValues({{1, 2}, {4, 5}, {5, 6}});

// 従来の行列積の計算
Eigen::array<Eigen::IndexPair<int>, 1> product_dims = { Eigen::IndexPair<int>(1, 0) };
Eigen::Tensor<int, 2> AB = a.contract(b, product_dims);

// 行列の転置の積の計算
Eigen::array<Eigen::IndexPair<int>, 1> transposed_product_dims = { Eigen::IndexPair<int>(0, 1) };
Eigen::Tensor<int, 2> AtBt = a.contract(b, transposed_product_dims);

// double contractionを用いたスカラ値へのcontraction
// 第一次元は第二次元と同様にcontractionされる.こちらは要素の二乗和の計算の例.
Eigen::array<Eigen::IndexPair<int>, 2> double_contraction_product_dims = { Eigen::IndexPair<int>(0, 0), Eigen::IndexPair<int>(1, 1) };
Eigen::Tensor<int, 0> AdoubleContractedA = a.contract(a, double_contraction_product_dims);

// 他で使えるように,テンソルのcontractionの結果のスカラ値の抽出
int value = AdoubleContractedA(0);

Reduction Operations

Reduction (簡約)は元のテンソルより少ない次元のテンソルを返すoperationです.
元のテンソルからの値のスライスによる reduction operator の適用により結果の値が計算されます.
どちらの次元方向へのスライスを行うかはユーザ側で指定します.

Eigen テンソルライブラリはmaximum()sum()のような定義済みreduction operatorを用意しており,またreducerテンプレートの少々の実装による追加のoperationの定義もできます.

Reduction Dimensions

全てのreduction operatorは整数配列として定義される <TensorType>::Dimensions をただひとつパラメータにとります.
これをreduction次元と呼びます.
値は,入力テンソルのの次元におけるreductionが行われる方向のインデックス配列です.
パラメータ数は多くとも入力テンソルのランクです.
それぞれの要素はテンソルのランク未満で,reductionの次元方向を示します.

入力テンソルの各次元は,実装が重複を排除しない限り,reduction dimensions中で多くとも1回使用されるべきです.
Each dimension of the input tensor should occur at most once in the reduction dimensions as the implementation does not remove duplicates.

reduction次元の順序は結果に影響しません.ただし,昇順に並べた方がコードは高速に実行されます.

例: 一次元のreduction

// 2次のテンソルの作成
Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{1, 2, 3}, {6, 5, 4}});
// 第二次元方向(1)のreduction...
Eigen::array<int, 1> dims({1 /* dimension to reduce */});
// ...maximum operatorを使用
// 結果は一次元のテンソル
// サイズはaの未reductionである第一次元のと同じ
Eigen::Tensor<int, 1> b = a.maximum(dims);
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
1 2 3
6 5 4

b
3
6

例: 2次元のreduction

Eigen::Tensor<float, 3, Eigen::ColMajor> a(2, 3, 4);
a.setValues({{{0.0f, 1.0f, 2.0f, 3.0f},
              {7.0f, 6.0f, 5.0f, 4.0f},
              {8.0f, 9.0f, 10.0f, 11.0f}},
             {{12.0f, 13.0f, 14.0f, 15.0f},
              {19.0f, 18.0f, 17.0f, 16.0f},
              {20.0f, 21.0f, 22.0f, 23.0f}}});
// テンソルは3次元.前の二つの次元をreductionする.
// 結果はサイズ4(aの最終次元のもの)の一次元テンソル.
// maximum()呼び出しに直にreductionを渡している.
Eigen::Tensor<float, 1, Eigen::ColMajor> b =
    a.maximum(Eigen::array<int, 2>({0, 1}));
cout << "b" << endl << b << endl << endl;
=>
b
20
21
22
23

Reduction along all dimensions

特殊ケースとして, 全ての 次元に対してreductionを実行したい場合,reduction次元を渡しません.結果はスカラで,ゼロ次元テンソルとして表現されます.

Eigen::Tensor<float, 3> a(2, 3, 4);
a.setValues({{{0.0f, 1.0f, 2.0f, 3.0f},
              {7.0f, 6.0f, 5.0f, 4.0f},
              {8.0f, 9.0f, 10.0f, 11.0f}},
             {{12.0f, 13.0f, 14.0f, 15.0f},
              {19.0f, 18.0f, 17.0f, 16.0f},
              {20.0f, 21.0f, 22.0f, 23.0f}}});
// sum() operatorで全次元をreduction
Eigen::Tensor<float, 0> b = a.sum();
cout << "b" << endl << b << endl << endl;
=>
b
276

<Operation> sum(const Dimensions& new_dims)

<Operation> sum()

sum() operatorでテンソルをreduceします.結果の値はreduceされた値の和です.

<Operation> mean(const Dimensions& new_dims)

<Operation> mean()

mean() operatorでテンソルをreduceします.結果の値はreduceされた値の平均です.

<Operation> maximum(const Dimensions& new_dims)

<Operation> maximum()

maximum() operatorでテンソルをreduceします.結果の値はreduceされた値の最大値です.

<Operation> minimum(const Dimensions& new_dims)

<Operation> minimum()

minimum() operatorでテンソルをreduceします.結果の値はreduceされた値の最小値です.

<Operation> prod(const Dimensions& new_dims)

<Operation> prod()

prod() operatorでテンソルをreduceします.結果の値はreduceされた値の積です.

<Operation> all(const Dimensions& new_dims)

<Operation> all()

all() operatorでテンソルをreduceします.
テンソルをboolにキャストし全要素がtrueか調べます.ショートカット無しに全要素に実行するので,顕著に非効率的です.

<Operation> any(const Dimensions& new_dims)

<Operation> any()

any() operatorでテンソルをreduceします.
テンソルをboolにキャストし要素のどれかかがtrueか調べます.
ショートカット無しに全要素に実行するので,顕著に非効率的です.

<Operation> reduce(const Dimensions& new_dims, const Reducer& reducer)

ユーザ定義のreduction operatorでテンソルをreduceします.
reduction operatorの実装の仕方は,TensorFunctors.hの SumReducer を参照してください.

Trace

Trace operation は元のテンソルより少ない次元のテンソルを返します.
各要素が特定次元のリスト"trace次元"における主対角成分の和であるテンソルを返します.
reduction次元と同様に,trace次元はoperationの入力パラメータとして渡され,型は <TensorType>::Dimensionsで,入力パラメータとして渡すのと同様の要求があります.さらに,複数のtrace次元は同じサイズである必要があります.

例: 2次元のtrace

// 3次元のテンソルを作成
Eigen::Tensor<int, 3> a(2, 2, 3);
a.setValues({{{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}}});
// traceが計算される次元を定義
// この例では,0, 1番の次元のtraceのみ計算する
Eigen::array<int, 2> dims({0, 1});
// 出力テンソルはtrace次元 contains all but
Tensor<int, 1> a_trace = a.trace(dims);
cout << "a_trace:" << endl;
cout << a_trace << endl;
=>
a_trace:
11
13
15

<Operation> trace(const Dimensions& new_dims)

<Operation> trace()

特殊ケースとして,operationに何もパラメータを渡されないと,入力テンソルの 全ての 次元へのtraceが返されます.

例: 全次元に対するtrace

// 3次のテンソルを作成.全次元は同じサイズ.
Eigen::Tensor<int, 3> a(3, 3, 3);
a.setValues({{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}},
             {{10, 11, 12}, {13, 14, 15}, {16, 17, 18}},
             {{19, 20, 21}, {22, 23, 24}, {25, 26, 27}}});
// ゼロ次元テンソルが返される
Tensor<int, 0> a_trace = a.trace();
cout<<"a_trace:"<<endl;
cout<<a_trace<<endl;
=>
a_trace:
42

Scan Operations

Scan operationは元のテンソルより少ない次元のテンソルを返します.
指定の軸方向へのinclusive scanを行います.つまり,与えられたreduction次元の軸に沿って全ての要素で走査的に実行されます.
reduction operationが加算に対応する場合,与えられた軸に沿ったテンソルのprefix sumを計算します.

Example:
dd a comment to this line

// 2次のテンソルを作成
Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{1, 2, 3}, {4, 5, 6}});
// 加算による第二次元(1)方向へのスキャン
Eigen::Tensor<int, 2> b = a.cumsum(1);
// 結果は入力と同じサイズのテンソル
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
1 2 3
4 5 6

b
1  3  6
4  9 15

<Operation> cumsum(const Index& axis)

連続エントリの累加によるスキャンを行います.

<Operation> cumprod(const Index& axis)

連続エントリの累積によるスキャンを行います.

Convolutions

<Operation> convolve(const Kernel& kernel, const Dimensions& dims)

入力テンソルの指定した次元における,カーネルによる入力テンソルの畳み込みの出力のテンソルを返します.
畳み込みの部分である出力テンソルの次元のサイズは次式により縮退されます.
output_dim_size = input_dim_size - kernel_dim_size + 1 (requires: input_dim_size >= kernel_dim_size).
畳み込みの部分でない次元のサイズは同じのままです.
畳み込みのパフォーマンスは畳み込みが計算される入力テンソルのストライドの長さに依ります(第一次元がColMajorでは最小のストライドで,RowMajorの場合は最終次元が最小のストライド).

// 第二,第三次元での畳み込みの計算
Tensor<float, 4, DataLayout> input(3, 3, 7, 11);
Tensor<float, 2, DataLayout> kernel(2, 2);
Tensor<float, 4, DataLayout> output(3, 2, 6, 11);
input.setRandom();
kernel.setRandom();

Eigen::array<ptrdiff_t, 2> dims({1, 2});  // Specify second and third dimension for convolution.
output = input.convolve(kernel, dims);

for (int i = 0; i < 3; ++i) {
  for (int j = 0; j < 2; ++j) {
    for (int k = 0; k < 6; ++k) {
      for (int l = 0; l < 11; ++l) {
        const float result = output(i,j,k,l);
        const float expected = input(i,j+0,k+0,l) * kernel(0,0) +
                               input(i,j+1,k+0,l) * kernel(1,0) +
                               input(i,j+0,k+1,l) * kernel(0,1) +
                               input(i,j+1,k+1,l) * kernel(1,1);
        VERIFY_IS_APPROX(result, expected);
      }
    }
  }
}

Geometrical Operations

こちらのoperationは元のテンソルと異なる次元のテンソルを返します.
テンソルのスライスへのアクセス,異なる次元で見る,あるいは追加のデータでテンソルを盛るなどができます.

<Operation> reshape(const Dimensions& new_dims)

入力テンソルの指定した新しい次元に変形されたテンソルのビューを返します.
引数 new_dims はインデックス値の配列です.
結果のテンソルのランクはnew_dimsの要素数と等しいです.

新しい次元のサイズの配列の全要素の積は入力テンソルの要素数と等しいです.

// サイズ1の新しい次元の導入によるテンソルのランクの増大
Tensor<float, 2> input(7, 11);
array<int, 3> three_dims{{7, 11, 1}};
Tensor<float, 3> result = input.reshape(three_dims);

// 2次元の統合によるテンソルのランクの減少
array<int, 1> one_dim{{7 * 11}};
Tensor<float, 1> result = input.reshape(one_dim);

このoperationは入力テンソルのデータを移動しません.したがって変形したテンソルの内容は元のテンソルのデータレイアウトに依ります.

こちらの例では,一つの次元に対しての2D ColMajorテンソルのreshape()で何が起きるかを示します.

Eigen::Tensor<float, 2, Eigen::ColMajor> a(2, 3);
a.setValues({{0.0f, 100.0f, 200.0f}, {300.0f, 400.0f, 500.0f}});
Eigen::array<Eigen::DenseIndex, 1> one_dim({3 * 2});
Eigen::Tensor<float, 1, Eigen::ColMajor> b = a.reshape(one_dim);
cout << "b" << endl << b << endl;
=>
b
  0
300
100
400
200
500

こちらは2D RowMajorの場合です.

Eigen::Tensor<float, 2, Eigen::RowMajor> a(2, 3);
a.setValues({{0.0f, 100.0f, 200.0f}, {300.0f, 400.0f, 500.0f}});
Eigen::array<Eigen::DenseIndex, 1> one_dim({3 * 2});
Eigen::Tensor<float, 1, Eigen::RowMajor> b = a.reshape(one_dim);
cout << "b" << endl << b << endl;
=>
b
  0
100
200
300
400
500

reshapeのoperationは左辺値です.言い換えれば,operatorの代入の左辺に使えます.

前の例は次のようにも書けます.

Eigen::Tensor<float, 2, Eigen::ColMajor> a(2, 3);
a.setValues({{0.0f, 100.0f, 200.0f}, {300.0f, 400.0f, 500.0f}});
Eigen::array<Eigen::DenseIndex, 2> two_dim({2, 3});
Eigen::Tensor<float, 1, Eigen::ColMajor> b(6);
b.reshape(two_dim) = a;
cout << "b" << endl << b << endl;
=>
b
  0
300
100
400
200
500

"b" それ自身は変形せず,代わりにbの変形に対して代入が行われています.

<Operation> shuffle(const Shuffle& shuffle)

次元を指定の順序で並べかえたテンソルのコピーのビューを返します.
引数 shuffle はインデックス配列で,サイズは入力テンソルのランクです.
0, 1, ..., rank-1 の順列である必要があります.
出力テンソルのi次元目は入力テンソルの shuffle[i] 番目の次元のサイズと等しいです.
例えば,

// 一つづつ左にシャッフル
Tensor<float, 3> input(20, 30, 50);
// ... inputには適当な値を入れておく.
Tensor<float, 3> output = input.shuffle({1, 2, 0})

eigen_assert(output.dimension(0) == 30);
eigen_assert(output.dimension(1) == 50);
eigen_assert(output.dimension(2) == 20);

出力テンソルに対するインデックスは入力テンソルのインデックスに対応してシャッフルされます.
例えば,こちらのアサートが通ります.

eigen_assert(output(3, 7, 11) == input(11, 3, 7));

一般的には,このようなアサートが通ります.

eigen_assert(output(..., indices[shuffle[i]], ...) ==
             input(..., indices[i], ...))

shuffle operationは左辺値で,被代入可能です.言い換えれば,代入演算子の左辺に使えます.

この利点を生かした前の例の書き換えです.

// 左に1づつシャッフル
Tensor<float, 3> input(20, 30, 50);
// ... 適当な値を入れておく
Tensor<float, 3> output(30, 50, 20);
output.shuffle({2, 0, 1}) = input;

<Operation> stride(const Strides& strides)

それぞれの次元にてストライド(stride-1個の要素のスキップ)したテンソルのビューを返します.
引数 strides はインデックス配列です.
結果のテンソルの次元は ceil(input_dimensions[i] / strides[i]) です.

例えばこちらは2Dテンソルのstride()の様子です.

Eigen::Tensor<int, 2> a(4, 3);
a.setValues({{0, 100, 200}, {300, 400, 500}, {600, 700, 800}, {900, 1000, 1100}});
Eigen::array<Eigen::DenseIndex, 2> strides({3, 2});
Eigen::Tensor<int, 2> b = a.stride(strides);
cout << "b" << endl << b << endl;
=>
b
   0   200
 900  1100

strideに対してテンソルを代入可能です.

Tensor<float, 3> input(20, 30, 50);
// ... set some values in input.
Tensor<float, 3> output(40, 90, 200);
output.stride({2, 3, 4}) = input;

<Operation> slice(const StartIndices& offsets, const Sizes& extents)

部分テンソルを返します.各次元iにおいて,sliceは入力テンソルの
offset[i] ~ offset[i] + extents[i] の間の要素となるテンソルを返します.

Eigen::Tensor<int, 2> a(4, 3);
a.setValues({{0, 100, 200}, {300, 400, 500},
             {600, 700, 800}, {900, 1000, 1100}});
Eigen::array<int, 2> offsets = {1, 0};
Eigen::array<int, 2> extents = {2, 2};
Eigen::Tensor<int, 1> slice = a.slice(offsets, extents);
cout << "a" << endl << a << endl;
=>
a
   0   100   200
 300   400   500
 600   700   800
 900  1000  1100
cout << "slice" << endl << slice << endl;
=>
slice
 300   400
 600   700

<Operation> chip(const Index offset, const Index dim)

chipはsliceの特別なケースです.
次元 dim における与えられた offset での部分テンソルになります.
次元 dim が消され,結果のテンソルは入力テンソルより少ない次元数となります.

例えば,行列のchipは入力行列の一行か一列になり得ます.

Eigen::Tensor<int, 2> a(4, 3);
a.setValues({{0, 100, 200}, {300, 400, 500},
             {600, 700, 800}, {900, 1000, 1100}});
Eigen::Tensor<int, 1> row_3 = a.chip(2, 0);
Eigen::Tensor<int, 1> col_2 = a.chip(1, 1);
cout << "a" << endl << a << endl;
=>
a
   0   100   200
 300   400   500
 600   700   800
 900  1000  1100
cout << "row_3" << endl << row_3 << endl;
=>
row_3
   600   700   800
cout << "col_2" << endl << col_2 << endl;
=>
col_2
   100   400   700    1000

chip operationは左辺値なので,テンソルのchipに値を代入できます.

Eigen::Tensor<int, 1> a(3);
a.setValues({{100, 200, 300}});
Eigen::Tensor<int, 2> b(2, 3);
b.setZero();
b.chip(0, 0) = a;
cout << "a" << endl << a << endl;
=>
a
 100
 200
 300
cout << "b" << endl << b << endl;
=>
b
   100   200   300
     0     0     0

<Operation> reverse(const ReverseDimensions& reverse)

次元の部分セットにおける逆順である入力テンソルのビューを返します.
引数 reverse は該当の次元における要素の順序を逆にするか否かを示すbool値配列です.
このoperationは入力テンソルの次元を保存します.
例えばこちらは2Dテンソルの第一次元におけるreverse()の様子です.

Eigen::Tensor<int, 2> a(4, 3);
a.setValues({{0, 100, 200}, {300, 400, 500},
            {600, 700, 800}, {900, 1000, 1100}});
Eigen::array<bool, 2> reverse({true, false});
Eigen::Tensor<int, 2> b = a.reverse(reverse);
cout << "a" << endl << a << endl << "b" << endl << b << endl;
=>
a
   0   100   200
 300   400   500
 600   700   800
 900  1000  1100
b
 900  1000  1100
 600   700   800
 300   400   500
   0   100   200

<Operation> broadcast(const Broadcast& broadcast)

入力が1つから複数回に複製される入力テンソルのビューを返します.
引数 broadcast は各次元において入力テンソルをいくつコピーすべきかを指定します.

Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{0, 100, 200}, {300, 400, 500}});
Eigen::array<int, 2> bcast({3, 2});
Eigen::Tensor<int, 2> b = a.broadcast(bcast);
cout << "a" << endl << a << endl << "b" << endl << b << endl;
=>
a
   0   100   200
 300   400   500
b
   0   100   200    0   100   200
 300   400   500  300   400   500
   0   100   200    0   100   200
 300   400   500  300   400   500
   0   100   200    0   100   200
 300   400   500  300   400   500

<Operation> concatenate(const OtherDerived& other, Axis axis)

TODO

<Operation> pad(const PaddingDimensions& padding)

追加でゼロ詰めされた入力テンソルのビューを返します.

Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{0, 100, 200}, {300, 400, 500}});
Eigen::array<pair<int, int>, 2> paddings;
paddings[0] = make_pair(0, 1);
paddings[1] = make_pair(2, 3);
Eigen::Tensor<int, 2> b = a.pad(paddings);
cout << "a" << endl << a << endl << "b" << endl << b << endl;
=>
a
   0   100   200
 300   400   500
b
   0     0     0    0
   0     0     0    0
   0   100   200    0
 300   400   500    0
   0     0     0    0
   0     0     0    0
   0     0     0    0

<Operation> extract_patches(const PatchDims& patch_dims)

入力テンソルから抽出された要素のパッチのテンソルを返します.
'patch_dims' で指定された次元のパッチとなります.
結果のテンソルは入力テンソルより1以上大きく,各パッチのインデックスに使います.
出力テンソルのパッチインデックスは入力テンソルのデータレイアウトに依存します.
ColMajorにおいてはパッチインデックスは最終次元で,RawMajorでは第一次元です.

例えば,次の入力テンソルを与えたとして,

Eigen::Tensor<float, 2, DataLayout> tensor(3,4);
tensor.setValues({{0.0f, 1.0f, 2.0f, 3.0f},
                  {4.0f, 5.0f, 6.0f, 7.0f},
                  {8.0f, 9.0f, 10.0f, 11.0f}});

cout << "tensor: " << endl << tensor << endl;
=>
tensor:
 0   1   2   3
 4   5   6   7
 8   9  10  11

6つの 2x2 パッチは次のコードによって抽出,インデックスできます.

Eigen::Tensor<float, 3, DataLayout> patch;
Eigen::array<ptrdiff_t, 2> patch_dims;
patch_dims[0] = 2;
patch_dims[1] = 2;
patch = tensor.extract_patches(patch_dims);
for (int k = 0; k < 6; ++k) {
  cout << "patch index: " << k << endl;
  for (int i = 0; i < 2; ++i) {
    for (int j = 0; j < 2; ++j) {
      if (DataLayout == ColMajor) {
        cout << patch(i, j, k) << " ";
      } else {
        cout << patch(k, i, j) << " ";
      }
    }
    cout << endl;
  }
}

このコードはColMajorの場合は次のようになります.

patch index: 0
0 1
4 5
patch index: 1
4 5
8 9
patch index: 2
1 2
5 6
patch index: 3
5 6
9 10
patch index: 4
2 3
6 7
patch index: 5
6 7
10 11

RowMajorの場合はこのようになります.
(NOTE: パッチはColMajorの場合と同じですが,indexが違うようになります.).

patch index: 0
0 1
4 5
patch index: 1
1 2
5 6
patch index: 2
2 3
6 7
patch index: 3
4 5
8 9
patch index: 4
5 6
9 10
patch index: 5
6 7
10 11

<Operation> extract_image_patches(const Index patch_rows, const Index patch_cols, const Index row_stride, const Index col_stride, const PaddingType padding_type)

入力テンソルから抽出された要素の画像パッチのテンソルを返します.
次の順で並んだ次元になります(入力テンソルのデータレイアウト,および追加の次元'N'に依存).

*) ColMajor
1st dimension: channels (of size d)
2nd dimension: rows (of size r)
3rd dimension: columns (of size c)
4th-Nth dimension: time (for video) or batch (for bulk processing).

*) RowMajor (reverse order of ColMajor)
1st-Nth dimension: time (for video) or batch (for bulk processing).
N+1'th dimension: columns (of size c)
N+2'th dimension: rows (of size r)
N+3'th dimension: channels (of size d)

結果のテンソルは入力テンソルより1以上大きく,各パッチのインデックスに使います.
出力テンソルのパッチインデックスは入力テンソルのデータレイアウトに依存します.
ColMajorの場合のパッチインデックスは第四次元で,RowMajorの場合は後ろから四番目の次元です.

例えば,次の次元サイズである入力テンソルが与えられたとして,

*) depth: 2
*) rows: 3
*) columns: 5
*) batch: 7

Tensor<float, 4> tensor(2,3,5,7);
Tensor<float, 4, RowMajor> tensor_row_major = tensor.swap_layout();

2x2 画像パッチは次のコードで抽出およびインデックスできます.

*) 2D patch: ColMajor (patch indexed by second-to-last dimension)

Tensor<float, 5> twod_patch;
twod_patch = tensor.extract_image_patches<2, 2>();
// twod_patch.dimension(0) == 2
// twod_patch.dimension(1) == 2
// twod_patch.dimension(2) == 2
// twod_patch.dimension(3) == 3*5
// twod_patch.dimension(4) == 7

*) 2D patch: RowMajor (patch indexed by the second dimension)

Tensor<float, 5, RowMajor> twod_patch_row_major;
twod_patch_row_major = tensor_row_major.extract_image_patches<2, 2>();
// twod_patch_row_major.dimension(0) == 7
// twod_patch_row_major.dimension(1) == 3*5
// twod_patch_row_major.dimension(2) == 2
// twod_patch_row_major.dimension(3) == 2
// twod_patch_row_major.dimension(4) == 2

Special Operations

<Operation> cast<T>()

元のテンソルと同じ次元で型がTのテンソルを返します.
元のテンソルを型Tに変換した値のテンソルを返します.

Eigen::Tensor<float, 2> a(2, 3);
Eigen::Tensor<int, 2> b = a.cast<int>();

整数のテンソルの要素毎の割り算を行いたいときなどに使えます.
現在,直接の整数テンソルでの割り算はサポートされていませんが,floatへのキャストにより簡単に実行できます.

Eigen::Tensor<int, 2> a(2, 3);
a.setValues({{0, 1, 2}, {3, 4, 5}});
Eigen::Tensor<int, 2> b =
    (a.cast<float>() / a.constant(2).cast<float>()).cast<int>();
cout << "a" << endl << a << endl << endl;
cout << "b" << endl << b << endl << endl;
=>
a
0 1 2
3 4 5

b
0 0 1
1 2 2

<Operation> eval()

TODO

Representation of scalar values

スカラ値はしばしばサイズ1でランク0のテンソルとして表現されます.
例えば
Tensor<T, N>::maximum() は現在 Tensor<T, 0> を返します.同様に,二つの1Dテンソルの(contractionによる)内積は0Dテンソルを返します.

Limitations

  • cxx11をサポートするコンパイラでは,テンソル次元の数は250に限定されています.昔のコンパイラでの制限はたったの5です.
  • IndexList クラスは cxx11 対応コンパイラが必要です.新しいコンパイラが使えない場合は配列のインデックスを使います.
  • GPUでは浮動小数のみテストおよび最適化しています.
  • 複素数および整数値はGPUでは壊れることがわかっています.もし試しに使ってみようとした場合,次のような static assertion failure をトリガすべきでしょう.
    EIGEN_STATIC_ASSERT(packetSize > 1, YOU_MADE_A_PROGRAMMING_MISTAKE)
15
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11