Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Tensorflow Scalaで遊んだ記録その3(スライス記法)

機械学習で大儲けしたいからTensorflow Scalaを勉強している。その1とその2はこちら。

blog.3qe.us

blog.3qe.us

今回は、テンソルの一部を表現する優れた記法であるスライス記法についてのお話。

オリジナルの(?)PythonのTensorflowにはスライス記法というのがあって、テンソルの一部をインデキシングしたり、切り出したり、切り出した箇所を変更したりできる。

この記法はNumPyがおそらくオリジナル?で、配列っぽく扱えるものの一部を表現するためのデファクトの記法になっている。

以下のページを参考に、テンソルのインデキシング、スライシングをTensorflow Scalaで試してみる。

platanios.org

インデキシング・スライシングに登場する要素は、 ::--- 、そして NewAxis の3つ。

  • ::
    • スライシングを行う。すなわち、特定次元の添字を範囲で絞り込む。
    • 例: Shape(10, 10) の2次元 Tensor t について、 t(2 :: 4, 6 :: 8) を指定すると第1次元の添字を[2, 4]に絞り込み、第2次元の添字を[6, 8]に絞り込んだテンソル(次元は同じ)が得られる。
  • :: 単体で使う場合
    • その次元をノータッチで温存する。(この操作を Full slice と呼ぶ)
    • 例: Shape(2, 2, 2)の3次元 Tensor t について、 t(::, ::, ::) を指定すると何も変化しない(全ての次元をそのまま保つという操作になる)。
    • 例: Shape(2, 2, 2) の3次元 Tensor t について、 t(0, ::, ::) を指定すると第1次元の添字を0に固定したテンソル(2次元)が得られる。
    • 例: Shape(2, 2, 2) の3次元 Tensor t について、 t(0, 1, ::) を指定すると第1・第2次元の添字をそれぞれ0, 1に固定したテンソル(1次元)が得られる。
  • ---
    • 次元をまたいだ連続する :: を省略する記法。正式名称はEllipsis
    • 例: Shape(2, 2, 2) の3次元 Tensor t について、 t(---) を指定すると何も変化しない(t(::, ::, ::)と同じ)。
    • 例: Shape(2, 2, 2) の3次元 Tensor t について、 t(---, 1) を指定すると最初の2次元はそのまま、最後の次元の添字が1に固定され、次元が1つ削減される(Shape(2, 2, 1)になり、2次元相当になる)。
  • NewAxis
    • 成分が1つしかない新たな次元を追加する。
    • 例: Shape(5) の1次元 Tensor t について、 t(::, NewAxis, NewAxis) を指定すると次元が2つ追加され、Shape(5, 1, 1)Tensor になる。
import tensorflow.api.{---, ::}
val t = Tensor.zeros[Int](Shape(4, 2, 3, 8)) // 4 x 2 x 3 x 8 の4次元テンソルを0で初期化する
val t1 = t(::, ::, 1, ::) // 3次元目を1にインデキシングする
println(t1.summarize())
Tensor[Int, [4, 2, 8]]
[[[0, 0, 0, ..., 0, 0, 0],
  [0, 0, 0, ..., 0, 0, 0]],

 [[0, 0, 0, ..., 0, 0, 0],
  [0, 0, 0, ..., 0, 0, 0]],

 [[0, 0, 0, ..., 0, 0, 0],
  [0, 0, 0, ..., 0, 0, 0]],

 [[0, 0, 0, ..., 0, 0, 0],
  [0, 0, 0, ..., 0, 0, 0]]]

インデキシングで切り出されたため、Shapeは4x2x8になって次元が1つ落ちているのがわかる。実際は4x2x1x8になっているのだが、1なので無視されている。

他の切り方を試してみる。

import tensorflow.api._ // n :: m の記法のために必要
val t2 = t(1 :: -2, ---, 2)
println(t2.summarize())
Tensor[Int, [1, 2, 3]]
[[[0, 0, 0],
  [0, 0, 0]]]

最終的にShapeは1 x 2 x 3 x 1になり、表示上は最後の1が削られて1 x 2 x 3になるというわけ(たぶん)。

マイナスインデックスを指定すると、最後の要素から数えていくのだが、1-2は場所は同じになるので実質1を指定するのと同じだ。

NewAxisも試してみよう。

val t3 = Tensor.zeros[Int](Shape(5))
println(t3(::, NewAxis, NewAxis).summarize()
Tensor[Int, [5, 1, 1]]
[[[0]],

 [[0]],

 [[0]],

 [[0]],

 [[0]]]

ニョキッと次元が伸びて、ネストが深まった。これは便利そうだ。

また、Ellipsis の利用制限として、高々1つのEllipsisしかスライス記法には含めることができない(どこが区切りか分からなくなるため)。これはIPv6のアドレッシングと同じだ。

t(---, 1, ---, 2, ---) // これはダメ!!

また、 Ellipsis が与えられない場合は最後の次元に Ellipsis が付いているものとみなされる。

t(1, 2 :: 3) // t(1, 2 :: 3, ---) と等価

これは多次元なテンソルの操作のために便利だ。

Further Reading

さらなるドキュメンテーションは公式マニュアルに用意されている。

http://platanios.org/tensorflow_scala/api/api/org/platanios/tensorflow/api/core/Ellipsis.html

http://platanios.org/tensorflow_scala/api/api/org/platanios/tensorflow/api/core/Slice.html

今日はここまで。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?