東京にいるけどfukuokaexのYOSUKEです。
最近、エリクサーちゃんで学ぶ Elixirの動画を作成し始めてるので良かったらチャンネル登録お願いします。本格的に始動するのは年明けからの予定です。
さて、兼ねてより興味があったライブラリ、Nxについて勉強して行こうと思います。この記事は自分の為の備忘録でもあります。が、Elixir初学者でも分かりやすいように、なるべく解説をつけながらアウトプット目指します。
Nxの学習ドキュメントを見ながら備忘録として書いていきます。基本的に内容は公式と被ります。僕の深掘りしたい時に少し解説やコードを細かく見ていくくらいの違いしかありません。
Nxの解説
Elixir の主要な数値データ型と構造は、数値プログラミング用に最適化されていません。Nx は、そのギャップを埋めるために構築されたライブラリです。
Elixir Nxは、他のプラットフォームで実装された型付きの多次元データ (テンソルと呼ばれる) にスムーズに統合するための数値計算ライブラリです。このサポートは、これらのテンソルをサポートするコンパイラとライブラリにまで及びます。Nx には 3 つの主要な機能があります。
- Nx では、テンソルは複数の名前付き次元で型指定されたデータを保持します。
- defnとして知られる数値定義は、テンソル対応の演算子と関数を使用したカスタム コードをサポートします。
- autograd または autodiff とも呼ばれる自動微分は、機械学習、シミュレーション、曲線近似、確率モデルなどの一般的な計算シナリオをサポートします。
Tensorの例
Tensorとは事前に定義された形状と型を持つ多次元配列です。
iex()> tensor = 1..4 #[1,2,3,4] と同じ
|> Enum.chunk_every(2)
|> IO.inspect
|> Nx.tensor(names: [:y, :x])
[[1, 2], [3, 4]] # Enum.chunk_every(2)でペアのリストに分けている
#Nx.Tensor<
s64[y: 2][x: 2]
[
[1, 2],
[3, 4]
]
>
- リストのリストとして表示されるデータ[[1, 2], [3, 4]]。
- テンソルの型で、長さ 64 ビットの符号付き整数で、型はs64です。
- 外側の次元が最初にリストされた、左から右へのテンソルの形状。
- 各次元の名前。
簡単にバイナリに戻せます。
iex()> binary = Nx.to_binary tensor
<<1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0,
0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0>>
s64 型のテンソルは、整数ごとに 4 バイトを使用します。確かに、上のデータを見ると、1, 0, 0, 0, 0, 0, 0, 0,と8bit毎に1,2,3,4の値が振り分けられているように見えます。
1..4バイナリは、テンソルを構成する個々のバイトを示すため、テンソルを構成するゼロの間に散在する整数を見ることができます。
すべてのデータが から 0..255の正の数のみを使用する場合、別の型(u8型)でスペースを節約できます。
iex()> Nx.tensor([[1,2], [3,4]], type: :u8) |> Nx.to_binary()
<<1, 2, 3, 4>>
既にバイナリがある場合は、バイナリと型を渡すことで直接テンソルに変換できます。
iex()> Nx.from_binary(<<0, 1, 2>>, :u8)
#Nx.Tensor<
u8[3]
[0, 1, 2]
>
Nxの型について
s64とかu8の型が上記サンプルには出てきました。これは、Nxライブラリで扱う型で以下の種類が定義されているようです。
Nxテンソルは、符号なし整数 (u8、u16、u32、u64)、符号付き整数 (s8、s16、s32、s64)、float (f32、f64)、brain float (bf16)、および複素数 (c64、c128) を保持できます。Tensor は、Google の Accelerated Linear Algebra (XLA) や LibTorch など、Elixir の外部で実装されたバックエンドをサポートします。
@zacky1972 さんから Nxのドキュメント を教えていただいたのですが、こちらにさらに詳しく書かれておりました。Nxの型について詳しく知りたい方はこちらもご覧ください。
ありがとうございます。
Tensorの値の取得
tensorの値が入った変数に[]をつけて何行何列の形式で値を入れるとその行列にある値を取得できます。下の例は、0行、1列目の値を取得している様子。
iex()> tensor[0][1]
#Nx.Tensor<
s64
2
>
1行目だけ取得したい場合は以下のように指定します。
tensor[0]
#Nx.Tensor<
s64[x: 2]
[1, 2]
>
では、1列目の値[2, 4]
を取得したい場合はどうすればいいでしょうか?
[
[1, 2],
[3, 4]
]
以下のように :x
、:y
で指定して取得することができるようになります。
iex()> tensor[x: 1]
#Nx.Tensor<
s64[y: 2]
[2, 4]
>
また、範囲指定もできるようになります。例えば、以下のように4 x 4 のtensorを作ります。
iex()> tensor = 1..16 |> Enum.chunk_every(4) |> IO.inspect |> Nx.tensor(names: [:y, :x])
[[1, 2, 3, 4], [5, 6, 7, 8], '\t\n\v\f', [13, 14, 15, 16]]
#Nx.Tensor<
s64[y: 4][x: 4]
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
]
>
これを 4 x 3 のtensorに範囲指定で取得すると以下のようになります。
iex()> tensor[x: 0..2]
#Nx.Tensor<
s64[y: 4][x: 3]
[
[1, 2, 3],
[5, 6, 7],
[9, 10, 11],
[13, 14, 15]
]
>
組み合わせてこんな範囲指定もできます。
iex()> tensor[x: 1..2][y: 1..2]
#Nx.Tensor<
s64[y: 2][x: 2]
[
[6, 7],
[10, 11]
]
>
さて、今回はここまで。
次回はこの続きを学習していきます。