コンテンツへスキップ

プログラミング言語へのテンソル記法の導入について

本日は、C言語を例にとって、既存のプログラミング言語でもテンソル記法を可能とする手法について考えてみることといたします。テンソル記法を導入すると、ベクトルや行列の演算(プログラミング言語上では配列の計算)がきわめて簡素に記述されます。

テンソル記法導入の意味

以前のブログで、ゼロから作るディープラーニングを読んだのですが、PythonのNumPy演算である"dot"を用いると、ニューラルネットワークの演算記述が非常に簡単にできることが書かれておりました。

これは良いものを読みました、ということで、私が現在開発中の並列演算記述言語"mhdl"にも同じ機能を取り込もうと、色々と検討いたしました。

dot演算を実現する一つの方法は、行列乗算を行う演算子を新たに導入して、配列に対してこの演算子が作用した場合はdot演算を行う、とすることですが、テンソル記法を導入して和の規約を用いることでも同じ演算を簡素に記述することが可能です。そして、テンソル記法には、行列乗算演算子よりも自由度が高いこと、ソースコードの理解が容易であることなどの利点があります。

テンソル記法は、コンパイラに多少の仕様を追加することで、既存の言語でも導入することができます。そこで、以下、C言語にテンソル記法を導入する方法について、ちょっと考えてみましょう。

テンソルの和の規約とは、項の添え字に同じものが現れた場合、これを全ての番号に対して振って計算してその和をとる、というものです。

次元の宣言

添え字の次元(変化する幅)はあらかじめ決めておく必要がありますので、これを宣言する必要があります。Cであれば、コンパイラディレクティブで、たとえば次のように宣言すればよいでしょう。

#dimension   #i   2

こう宣言することで、テンソルの添え字"#i"は二次元であること、すなわち値0と1をとることを宣言します。テンソルの添え字は多数用いることもありますので、添え字部分は複数記述可能としておくのが良いでしょう。たとえば、次のように宣言することで、#iと#jがそれぞれ0と1の値をとることとします。なお、テンソルの添え字は"#"を含む#iで一つの変数名のように解釈し、名前"i"を他の用途に使えるようにしておきます。

#dimension   #i   #j   2

また、変数名を省略した場合は、全ての変数の次元を指定するものとします。

#dimension 2

このように指定した場合は、すべてのテンソル添え字の次元が2であることを意味します。なお、この宣言を行った場合も、個別の添え字に#dimension宣言をおこなうことを許容し、この場合は個別の宣言を有効とします。

2021.4.25追記:添え字を省略した形の#dimensionディレクティブはまずいかもしれません。こうしてしまうと、テンソル添え字の定義がどこでもなされないことになってしまいますので。結局、#dimensionディレクティブは、テンソル添え字に何を用いるかを指定するとともに、その次元を指定する文であると考えるのがよさそうです。

テンソルの演算:和の規約

このような宣言がなされた後でx = y#i * z#iと記述した場合には、x = y[0] * z[0] + y[1] * z[1]と解釈すればよいのですね。

テンソルは、物理的には共変・反変の二種類が用いられ、それぞれ上付きの添え字と下付きの添え字でこれらを識別しているのですが、単に和の規約を用いるだけなら、あえてこの二種類を区別する必要はありません。

ここでは“#”をテンソルの添え字記号に用いていますが、“#”は、番号記号と呼ばれ、番号を表す添え字には適当ですし、コンパイラに対する指示子であるという意味で、他のコンパイラディレクティブの記述に用いていることとの整合性もとられます。

2021.4.25追記:上付き、下付きを区別するオプションは将来に残しておきたいと思います。“#”は、下付き添え字に用いる記号とし、上付き添え字を区別する場合の記号に“#”の代わりに“^”を用いることにしたいと思います。“^”は、現在のCの規格では、排他論理和を表す二項演算子として用いられていますが、“!”を二項演算子として用いることで排他論理和演算することとします。そして他の論理演算氏と同様、この記号が単独で用いられた場合は、ビットごとの排他論理和、”!!”のように二つ続けて用いられた場合は全体として(0なら偽、それ以外は真)の論理演算とするわけですね。“!”は現在否定を表す単項演算子として用いられておりますが、排他論理和も、演算子に先立つ項が真なら演算子の後の項を否定する(そうでなければ後の項そのまま)という形の、制御付き否定演算とみなすこともできるでしょう。なお、Basicのように“^”をべき乗演算子に使うのであれば、これに代えて“**”をべき乗演算子として使っていただきたいと思います。これは、Fortranなどで行われている記法です。

テンソルの添え字は複数用いることもできます。また、x#i = y#j * z#j#iのように、左辺に用いた場合は次のように解釈します。

x[0] = y[0] * z[0][0] + y[1] * z[1][0],
x[1] = y[0] * z[0][1] + y[1] * z[1][1]

テンソル記法の優れた点は、複雑な演算を簡単に記述できることで、特にベクトルや行列の演算に威力を発揮しそうです。

この程度のことは、すでに試みられていてもよさそうですが、どこかで発表されておりましたでしょうか? gccあたりで何かやっていそうな気がしないでもないのですが、、、

マクロ名への拡張

もしこの拡張を行うのであれば、#defineで定義する名前の先頭にも"#"を付けることを許容していただくと良いですね。たとえば"#define #size 10"のようにする。こうすることで、名前"#size"はコンパイラディレクティブで定義された名前であることが一目でわかります。

この場合、マクロ名とテンソルの添え字との区別がつきにくくなるのですが、「テンソルの添え字はアルファベット一文字とする」などのローカルルールを定めておけば、この区別も容易につけられるでしょう。それでも厭だという人がおられるかもしれませんが、そういう方はこの機能を使わなければよいだけの話、何の問題もありません。

プリプロセッサによる実装

さて、これを現実に実装する手法ですが、プリプロセッサ(プリプリプロセッサ?)を作成して、これによりテンソル記法を含むソースコードを通常のCのソースコードに変換すればよいでしょう。"#k"はCの名前には使用できませんので、これを"_def_k"などに置き換えます。

kの次元数を"_dim_k"などとするのであれば、#dimension   #k   3は#define   _dim_k   3などに変形することとなります。

#dimension 3などと書かれた場合は、#define _dim_ 3などとしておき、#kなどを使用する直前に次のようにすればよいでしょう。

#ifndef _dim_k
#define _dim_k _dim_
#endif

このディレクティブ、_dim_が定義されていた場合のみに意味を持つのですが、次のように記述すべきでしょうかね。このあたりは、CのWizardの方にお任せいたしましょう。

#ifdef _dim_
#ifndef _dim_k
#define _dim_k _dim_
#endif
#endif

あとは、テンソル記法で記述された式をテンソルの演算規則に従うforループに変換するのですが、これには、"for(int   _def_k   =   0;   _def_k   <   _dim_k;   _def_k++){   }"の形に変換することとなります。このforループを用いて、左辺にある添え字に関しては複数の代入を行うこと、右辺の同じ項に表れた同じ添え字に関しては添え字の変化する範囲の値でそれぞれ乗算を行って和をとる形に式を変形します。

先に例をあげたx#i = y#j * z#j#iの場合は、次のようなコードに変換すればよい、ということですね。

for(int _def_i = 0; _def_i < _dim_i; _def_i++)
{
for(int _def_j = 0; _def_j < _dim_j; _def_j++)
{
if(!_def_j) x[_def_i] = y[_def_j] * z[_def_j][_def_i];
else x[_def_i] += y[_def_j] * z[_def_j][_def_i];
}
}

一般的な変換規則

多数の添え字が現れた場合の処理は少々複雑ですが、テンソルの演算規約に従う形に変換プログラムを書き下すことは、さほど難しいことでもないでしょう。

単純には、

・ 左辺に現れたテンソルの添え字:それぞれを振った代入文を複数形成する
・ 右辺にのみ現れたテンソルの添え字:それぞれを変化させた項の和をとる
-  この場合、左辺にはない添え字の全てがゼロの場合は代入文を、
-  そうでない場合は加算代入文を選択実行するよう記述すればよい、

というわけです。

テンソルらしくはありませんが、x = y#iという記述はx = y[0] + y[1]と解釈しましょう。これにより、総和も簡単に計算することができます。


2/8追記:機械的なCソースコードへの変換法則は次のようになります。

(1) 式に含まれるすべてのテンソル添え字に対して、それぞれを次元の幅だけ変化させる多重のforループを記述する

(2) テンソルの添え字は、それぞれを配列のインデックス([_def_i]など)に書き換える

(3) 左辺に含まれていないテンソル添え字のいずれかが0ではない場合、代入演算の代わりに加算代入を行うようif文を形成する

加算がかっこで囲まれて、これに乗算がおこなわれるような複雑な式には対応できませんが、このような場合には“式が複雑すぎる”などのエラーメッセージを出すようにしておけば、さしあたりは十分に実用的だと思うのですが、、、


色々なアイデア

また、dimensionを変数的に扱うこともできるようにしておきましょう。これには、#i.dimension = 2などとするのが良さそうです。プリプロセッサは、これを

unsigned int _def_i;
unsigned int _dim_i = 2;

のように変換するわけですね。

その他、次のような記述も許しておきますと、ソースコードの簡素化に大いに役に立ちそうです。つまりこれで、配列tbl[]に、1度刻みのサイン関数の値がセットされる、というわけです。

#dimension #d 360
double tbl#d = sin(#d * PI / 180);

C言語へのテンソル記法の導入は、現実性も高く、有用ではないかと思うのですが、どうでしょうか。


2017/12/28追記:テンソル表現を使って配列宣言できるようにしておくと便利でしょう。

つまり、次のような記述を許す、ということですね。

#dimension #i 3
double x#i;

この記述はdouble x[3]と同じ意味とします。この記述の利点は、#dimension文のみを変更することで#iの次元を変更できる点です。また、先々x#iと記述されるのに合わせ、xと#iを常に一体のもので扱うことで、間違いが生じるリスクを低下させることができるでしょう。


本文書は、適宜修正を行っております。