カテゴリー
【tensorflowjs入門】生のJavascript配列からTensor型に変換するときに覚えておきたいテクニック集
※ 当ページには【広告/PR】を含む場合があります。
2020/12/31
2022/08/18
今回はtensorflowjsで配列からテンソル型を扱う際に個人に役立った基礎的なテクニックをいくつかまとめておきたいと思います。
tensorflowjsでの開発では配列をテンソル型に変換、テンソルを別のテンソルに変換...などなど独特なテクニックが必要になってきます。
今回は著者が独断で選ぶ、tensorflowjsを使うときに、「これはとても役立ったぞ」というオペレーターとその使用例を厳選して以降で紹介します。
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
配列とテンソルの違い
tensorflowで機械学習のコーディングしていくと、しばしばテンソルだけでは出来ない操作が混じっていて、一旦配列に戻して、配列のメソッドを使ってから再度テンソルに戻すようなことを考える必要があります。
こういうような場面に遭遇するたびに、「配列をテンソル型にする必要があるのか?」なんて内心では思っていますが、そもそもテンソルは代数学の重要で奥深い概念です。
ので、著者のようにテンソルをボンヤリと配列の仲間みたいに使っていると、近年大量に投下されまくっている機械学習分野の最新の論文を正しく理解できない=更に上のステージにいけないように思います。
改めて、テンソルの定義を先程のサイトから拝借させていただくと...
$$V$$と$$W$$の2つの線型空間を考える。 このとき線型空間$$T$$と双線型写像$$\kappa: V \times W \rightarrow T$$の組$$(T, \kappa)$$において、次の性質を満たすものが同型を除き唯一つ存在する。
[性質] 任意の線型空間$$U$$と任意の双線型写像$$\Phi
このとき組$$(T, \kappa)$$を$$V$$と$$W$$のテンソル積、$$T$$の元を
テンソル
...以上がテンソルの定義です。
改めて考えると、テンソルとはとても代数学から与えられる抽象的な意味が背景にありますが、対して配列はあくまでもメモリ空間に配置されたデータの参照という意味以外はないのです。
ではなぜテンソルが多様な分野で使われるのかというと、物理学のアプローチの一つに、既知の複数の物理量から、それとは別の新しい物理量を考えるケースがあります。 その新しい物理量にも線形性が保証されるならば、複数の線型空間の元に対してテンソル積を新しい物理量(テンソル)として取り扱えることになります。
このテンソル(解析)を使う側にとって何が一番嬉しいのかというと、既知の物理量のセットから得られた新しい物理量が唯一に定まるため、その時の任意性を排除して議論することができるようになります。 このテンソル積の仕組みがあることで、新しい物理量として安心して取り扱うことが保証されております。
そう考えるとtensorflowというのは、既知の観測量を幾重にも重なった線形写像に通して新しい意味のある物理量を得ることが目的のアプリケーションですので、確かに
理屈はともかく、配列をテンソル型にしたら、できるだけ配列操作は使わず、テンソル型のクラスメソッドだけを使うように心がけるようになりたいものです。
TensorFlowを動かしながら学ぶ TensorFlowとKerasで動かしながら学ぶ ディープラーニングの仕組み 畳み込みニューラルネットワーク徹底解説
配列 > テンソル型にするときの便利なテクニック集
難しい前置きになりましたがご容赦いただき、本題の配列-テンソル型の変換操作などの個人に使えるテクニックをまとめていきます。
テンソルとスカラーの四則演算
後半になるに従って頭が混乱するといけないので基礎からいきます。
import * as tf from '@tensorflow/tfjs';
//👇1次元配列から1次元テンソル, Shape(形状): [3] に変換
const x = tf.tensor1d([1,2,3]);
//👇実数(Number型)からスカラー型に変換
const a = tf.scalar(4);
//👇テンソルxにスカラーaを足す
x.add(a).print(); // -> [5, 6, 7]
//👇テンソルxにスカラーaを引く
x.sub(a).print(); // -> [-3, -2, -1]
//👇テンソルxにスカラーaを掛ける
x.mul(a).print(); // -> [4, 8, 12]
//👇テンソルxにスカラーaを割る
x.div(a).print(); // -> [0.25, 0.5, 0.75]
単純な四則演算で、計算当然の結果のようにも見えますが、テンソルの線形性として意識して見ると、奥深い操作です。
またtensorflowのテンソルを上手く扱う上で、テンソルの形状(シェイプ,Shape)の表記、テンソル軸(次元数, axis)は常に頭に意識しておく必要があります。
上の例では、
[1,2,3]
[3]
余談で各演算オペレーターの引数には、スカラー型だけでなく、(javascriptの)配列型やNumber型も直接入れることができます。
import * as tf from '@tensorflow/tfjs';
const b = tf.scalar(1/4);
x.mul(b).print(); // -> [0.25, 0.5, 0.75]
x.mul([1/4]).print(); // -> [0.25, 0.5, 0.75]
x.mul(1/4).print(); // -> [0.25, 0.5, 0.75]
このソースコードではプログラムが内部で同じことをやっているのですが、テンソル操作を意識するならきちんと
tf.scalar
stackでテンソル成分の合成
まずは以下のように2次元テンソルを作成してみます。
import * as tf from '@tensorflow/tfjs';
const xarr = [0, 1, 2, 3, 4, 5];
const yarr = [-1, 3, 2, 0, -2, 1];
const x = tf.tensor2d(xarr, [6, 1]); // Shape: [6,1]
const y = tf.tensor2d(yarr, [6, 1]); // Shape: [6,1]
x.print();
// 👇
// [[0],
// [1],
// [2],
// [3],
// [4],
// [5]]
y.print();
// 👇
// [[-1],
// [3 ],
// [2 ],
// [0 ],
// [-2],
// [1 ]]
これら形状[6,1]の2次元テンソル
x
y
import * as tf from '@tensorflow/tfjs';
const xarr = [0, 1, 2, 3, 4, 5];
const yarr = [-1, 3, 2, 0, -2, 1];
const xyarr = xarr.map((e, i) => [e, yarr[i]]);
const xy = tf.tensor2d(xyarr, [6, 2]); // Shape: [6,2]
xy.print();
// 👇
// [[0, -1],
// [1, 3 ],
// [2, 2 ],
// [3, 0 ],
// [4, -2],
// [5, 1 ]]
配列のmapメソッドを使って無理やり配列の要素を切り貼りしているやり方は中々慣れないと面倒です。 こんなことをやらなくても、stackで以下のようにスマートに合成できます。
import * as tf from '@tensorflow/tfjs';
const xarr = [0, 1, 2, 3, 4, 5];
const yarr = [-1, 3, 2, 0, -2, 1];
tf.stack([xarr, yarr], 1).print();
// 👇
// [[0, -1],
// [1, 3 ],
// [2, 2 ],
// [3, 0 ],
// [4, -2],
// [5, 1 ]]
すこしだけ解説すると、stackの第一引数には、同じ形状・dtypeのテンソル型のオブジェクトを合成したい順番に配列で指定します。
第二引数は合成したい軸を指定しますが、合成前の形状([6,1])は軸数2ですので、
axis=0
axis=1
今回は
[6,1] --> [6,2]
axis=1
unstackでテンソルの形状を一つひん剥く
形状を強制的に変えようとすると、tf.reshapeがありますが、結局のところ形状の総積数は変えることができません。
次元数を一つ落として、中身のテンソルの元を配列として得たい場合に使うのが
一例として、形状[4,1,2]の3次元テンソルをunstackで各軸を指定したときの細かな違いを見ていきます。
import * as tf from '@tensorflow/tfjs';
const xarr = [[[5,3]],[[8,1]],[[2,7]],[[3,0]]];
const x= tf.tensor3d(xarr, [4, 1, 2]); // Shape: [4,1,2]
x.print();
// 👇
// [ [[5, 3],],
// [[8, 1],],
// [[2, 7],],
// [[3, 0],] ]
//👇最初の次元をunstack:
// [4,1,2] --> 4 x [1,2]
const xUnstackAlongAxis1 = x.unstack(0);
console.log(xUnstackAlongAxis1.length); // 4(=ひん剥いた次元の要素数と一致)
for (const tensor of xUnstackAlongAxis1) {
tensor.print();
}
// 👇
// [[5, 3],]
// [[8, 1],]
// [[2, 7],]
// [[3, 0],]
//👇2番目の次元をunstack:
// [4,1,2] --> 1 x [4,2]
const xUnstackAlongAxis2 = x.unstack(1);
console.log(xUnstackAlongAxis2.length); // 1(=ひん剥いた次元の要素数と一致)
for (const tensor of xUnstackAlongAxis2) {
tensor.print();
}
// 👇
// [[5, 3],
// [8, 1],
// [2, 7],
// [3, 0]]
//👇3番目の次元をunstack:
// [4,1,2] --> 2 x [4,1]
const xUnstackAlongAxis3 = x.unstack(2);
console.log(xUnstackAlongAxis3.length); // 2(=ひん剥いた次元の要素数と一致)
for (const tensor of xUnstackAlongAxis3) {
tensor.print();
}
// 👇
// [[5],
// [8],
// [2],
// [3]]
// [[3],
// [1],
// [7],
// [0]]
ポイントとしては、元のテンソル形状と比較して、unstackした後のテンソル配列の数が指定して潰した次元の要素数分あるということです。
テンソルの次元を一つづつ落としてくれるので、個人にはreshapeやsqueezeより感覚が掴みやすいと思います。
なお、ややこしいですが、unstackの逆の操作はstackではなく
一次元テンソルの要素を左シフト
次はテンソルの要素を一つづつずらす操作の時に使えるテクニックです。
例として、一次元テンソルの
[1,2,3,4,5]
[2,3,4,5,6]
import * as tf from '@tensorflow/tfjs';
const xarr = [1,2,3,4,5];
const x = tf.tensor1d(xarr);
x.print();
//👇
//[1, 2, 3, 4, 5]
//新たに付け加えるテンソル
const a = tf.tensor1d([6]);
a.print();
//👇
//[6]
//先頭の要素をスライスして、末尾にテンソルを加える
x.slice([1], [-1]).concat([a]).print();
// x.slice([1]).concat([a]).print(); // -1は省略可
//👇
// [2, 3, 4, 5, 6]
ここではテンソルの中身を切り取る
sliceの使い方には注意が必要で、第一引数は切り取り開始の(テンソルの)位置を指定します。
ここで言うと
[1]
[2,3,4,5]
ちなみに二番目の引数は切り取り位置から取り出す残り要素のサイズです。
-1
[-1]
次いでconcatは同次元のテンソルを後ろから結合しているので、
[2, 3, 4, 5, 6]
二次元テンソルの要素を左シフト
先程と同様の操作を2次元に拡張します。
基本的には先程のテクニックと全く同じですが、2次元なので左シフトで指定できる軸が2つとなり考え方もやや複雑です。
sliceの用法がポイントです。
最初のaxisへのシフトは簡単です。
import * as tf from '@tensorflow/tfjs';
// [[1,2],[3,4],[5,6]] から [[3,4],[5, 6],[7, 8]]へ左シフト
const x = tf.tensor2d([[1, 2], [3, 4], [5, 6]]);
const a = tf.tensor2d([[7, 8]]);
x.slice([1], [-1]).concat([a]).print();
//👇
// [[3, 4], [5, 6], [7, 8]]
結果は先程と同じです。
でも、2番目のaxisを左シフトするにはconcatでは難しいので、上の節で紹介したstackを利用します。
import * as tf from '@tensorflow/tfjs';
// [[1,2],[3,4],[5,6]] から [[2,3],[4, 5],[6, 7]]へ左シフト
const x = tf.tensor2d([[1, 2], [3, 4], [5, 6]]);
const b = tf.tensor2d([[3],[5],[7]]);
//ワンライナーで左シフトする
x.slice([0, 1], [-1,-1]).stack([b], 1).squeeze([2]).print();
//👇
// [[2, 3], [4, 5], [6, 7]]
///上のワンライナー操作で個別に分解してみる
//①2番目の軸の最初の要素から切り出し
x.slice([0,1],[-1,-1]).print();
//👇
// [[2],
// [4],
// [6]]
//②テンソルbと2番目の軸で結合
//形状[3,1]と[3,1]の結合は[3,2,1]の拡張として解釈
x.slice([0,1],[-1,-1]).stack([b], 1).print();
//👇
// [[[2],[3]],
// [[4],[5]],
// [[6],[7]]]
//③余分に出た3番目の軸を圧縮し次元を一つ落とす
//[3,2,1] --> [3,2]
x.slice([0,1],[-1,-1]).stack([b], 1).squeeze([2]).print();
//👇
// [[2, 3], [4, 5], [6, 7]]
ここでもsliceの使い方がポイントになります。 使い方がわかると、3次元配列のシフトも作成できると思いますが、そこはご自身の宿題とさせていただきます。
なお、冗長な1次元を潰すのに
x.slice([0,1],[-1,-1]).stack([b],1).unstack(2)[0].print();
unstackはテンソルの配列を返すので、返ってくる配列の最初のオブジェクトを受け取る必要があります。
reshapeで汎用性の高いテンソル変換
reshapeはテンソル形状を自由自在に変形することできますが、操作の自由度が高すぎて、特に低次から高次へのテンソル変換が想像し難いので、人間の頭では使いにくいと思います。
そのためreshapeを多用すると可読性が著しくおちてバグの温床になりやすいので、実際には
tensor1d
tensor6d
ただどうしようもなくなった際の最終奥義として威力を発揮するかもしれません。
import * as tf from '@tensorflow/tfjs';
const x = tf.tensor1d([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
x.print();
//👇 Shape: [16]
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
x.reshape([1, 8, 2]).print();
//👇 [16] --> [1,8,2]
// [[[1 , 2 ],
// [3 , 4 ],
// [5 , 6 ],
// [7 , 8 ],
// [9 , 10],
// [11, 12],
// [13, 14],
// [15, 16]]]
x.reshape([1, 8, 2]).reshape([16, 1]).print();
//👇 [16] --> [1,8,2] --> [16,1]
// [[1 ],
// [2 ],
// [3 ],
// [4 ],
// [5 ],
// [6 ],
// [7 ],
// [8 ],
// [9 ],
// [10],
// [11],
// [12],
// [13],
// [14],
// [15],
// [16]]
x.reshape([1, 8, 2]).reshape([16, 1]).reshape([2, 2, 2, 2]).print();
//👇 [16] --> [1,8,2] --> [16,1] --> [2,2,2,2]
// [[[[1 , 2 ],
// [3 , 4 ]],
// [[5 , 6 ],
// [7 , 8 ]]],
// [[[9 , 10],
// [11, 12]],
// [[13, 14],
// [15, 16]]]]
テンソルの総要素数がテンソル形状の各次元の要素数の積であれば、任意のテンソル形状に何度でも変換することができるなんともハチャメチャなメソッドです。
テンソルを分割するsplit
unstackと同様に返り値はテンソル型の配列になります。
import * as tf from '@tensorflow/tfjs';
const x = tf.tensor2d([1,2,3,4,5,6,7,8,9,10,11,12], [4,3]);
x.print();
// [[1 , 2 , 3 ],
// [4 , 5 , 6 ],
// [7 , 8 , 9 ],
// [10, 11, 12]]
//👇最初の次元で2つのテンソルに分割
const [a, b] = x.split([3,1], 0);
a.print();
// [[1, 2, 3],
// [4, 5, 6],
// [7, 8, 9]]
b.print();
// [[10, 11, 12],]
使い方はsliceに似ていますが、splitは第二引数で指定したaxisの要素で、分割する要素数を配列で指定します。
上の例では、最初の軸(
axis=0
[3,1]
テンソル型からNumber型配列への逆変換
最後にテンソルからNumberの配列に戻すテクニックを紹介します。
特に学習したモデルから予測後の値を取り出すときに利用する場合が多いと思います。
以下の例でdataSyncの操作を試してみます。
import * as tf from '@tensorflow/tfjs';
const x: tf.Scalar = tf.scalar(3);
console.log(x.dataSync()[0]); // 3
console.log(x.dataSync()[0] - 2); // 1 (Number型になっている)
const y: tf.Tensor = tf.tensor1d([4]);
console.log(y.dataSync()[0] - 2); // 2
const arr = [];
const a: tf.Scalar = tf.scalar(1);
const b: tf.Scalar = tf.scalar(2);
array.push(a);
array.push(b);
const values = array.map(t => t.dataSync()[0]);
console.log(values); // [ 1, 2 ]
ちなみにdataSyncと類似のメソッドに
dataSyncの場合には問答無用で1次元配列されてしまいますが、arraySyncはテンソル型に対応したネスト配列を返します。 余程の理由が無い限りは、dataSyncを使う方が楽に値を取り出せると思います。
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
まとめ
今回はjavascript配列とtensorflowjsのテンソル型のやりとりする操作を扱うときに、個人的に便利に使えたテクニック集と題して取りまとめてみました。
まだまだニッチなユーティリティは用意されいますが、今後便利なテクニックを見つけ次第、この記事の内容も更新して行こうかと思います。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー