Nimで多次元配列を操作
Nim言語を覚えて、まだ3週間ほどの初心者です。
前回、Nim言語用のDataFrameライブラリとしてdatamancerの操作を説明しましたが、
今回は、そのdatamancerの算術部分で使われているarraymancerについて操作説明をしていきます。
arraymancerは、pythonのnumpyに相当する多次元の配列ライブラリです。
numpyよりも速いのですが、日本語の説明サイトがなく、自分で操作しながら覚えました。
環境
私のPC環境はWindowsであるため、Nightly build版で動作させています。
- OS: Windows11
- Nim 1.9.1 (v2.0 RC版 2023/01/29)
- Python 3.9.13
- arraymancer 0.7.19
arraymancerの特徴
- 最大6次元配列まで対応している。
- N次元配列のtensor型で操作が可能
動作確認
1. arraymancerライブラリのインストール
nimble install arraymancer
datamancerライブラリを入れた方なら、既にarraymancerライブラリも含まれていますが、始めての方は上記コマンドでライブラリのインストールが可能です。
2. tensorの基本的な使い方
import std/[math, sequtils, strformat]
import arraymancer
#2次元配列
let seq1 = @[
@[1, 2],
@[3, 4]
]
let array1 = [
[1, 2],
[3, 4]
]
echo fmt"seq1 : {seq1}"
echo fmt"array1 : {array1}"
# echo seq1 * 2 # <- N次元配列のseq型に2を掛けてもエラーになる
# echo array1 * 2 # <- N次元配列のarray型に2を掛けてもエラーになる
# tensor
let tensor1 = @[
@[1, 2],
@[3, 4]
].toTensor
let tensor2 = [
[10, 20],
[30, 40]
].toTensor
echo "--- tensor1, tensor2 2次元配列 ---"
echo tensor1
echo tensor2
echo "--- tensor1 * 2 の値 2次元配列 ---"
echo tensor1 * 2
echo "--- tensor1 + tensor2 2次元配列 ---"
echo tensor1 +. tensor2 # 同一行列か1次元のみ算出可能
echo "--- tensor1 y方向への連結 2次元配列 ---"
echo tensor1.concat(tensor2, axis=0)
echo "--- tensor1 x方向への連結 2次元配列 ---"
echo tensor1.concat(tensor2, axis=1)
# 1~24のシーケンスを作成し、Tensor変換し、3次元配列に変換
let tensor3 = toSeq(1..24).toTensor().reshape(2,3,4) # 引数z, y, x
echo "--- tensor3 3次元配列 ---"
echo tensor3
下記のようにターミナル上から実行
nim r --hints:off .\sample01.nim
出力結果
seq1 : @[@[1, 2], @[3, 4]]
array1 : [[1, 2], [3, 4]]
--- tensor1, tensor2 2次元配列 ---
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|1 2|
|3 4|
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|10 20|
|30 40|
--- tensor1 * 2 の値 2次元配列 ---
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|2 4|
|6 8|
--- tensor1 + tensor2 2次元配列 ---
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|11 22|
|33 44|
--- tensor1 y方向への連結 2次元配列 ---
Tensor[system.int] of shape "[4, 2]" on backend "Cpu"
|1 2|
|3 4|
|10 20|
|30 40|
--- tensor1 x方向への連結 2次元配列 ---
Tensor[system.int] of shape "[2, 4]" on backend "Cpu"
|1 2 10 20|
|3 4 30 40|
--- tensor3 3次元配列 ---
Tensor[system.int] of shape "[2, 3, 4]" on backend "Cpu"
0 1
|1 2 3 4| |13 14 15 16|
|5 6 7 8| |17 18 19 20|
|9 10 11 12| |21 22 23 24|
- 最初のseq型、array型、tensor型の表示結果の違い。
tensor型は表示結果がN次元で表示される事が確認できたと思います。 - 次にseq型array型からtensor型への変更には、
.toTensor
を利用して変換が可能です。 - 次に算術ですが、直接tensor型への算術が可能になります。
- また、tensor型同士の算術も可能ですが、同一次元か1次元のみ可能となります。
-
concat
関数では、y軸方向かx軸方向への連結が可能になります。 - 最後に
toSeq
でseq型の1次元配列を作成し、.toTensor
でtensor型に変換した後、reshape
関数で3次元配列に変換しています。
3. tensor型同士の変わった算術
x軸とy軸に伸びた1次元のtensor型配列値を算術すると、自動で無い部分を算術して埋めてくれます。
その例が下記の結果です。
import std/[math, sequtils, strformat]
import arraymancer
# 下記の配列の算術
# 11 12 13 <- x軸: 1+10, 2+10, 3+10の計算結果
# 21
# 31 この部分は自動算術
# 41
# y軸: 1+10, 1+20, 1+30, 1+40の計算結果
let x_tensor = [1.0, 2.0, 3.0].toTensor().reshape(1,3) # x軸
let y_tensor = [10.0, 20.0, 30.0, 40.0].toTensor().reshape(4,1) # y軸
echo "--- x_tensor +. y_tensor 1次元配列を加算した結果を2次元配列に作成 ---"
echo x_tensor +. y_tensor
echo "--- x_tensor -. y_tensor 1次元配列を減算した結果を2次元配列に作成 ---"
echo x_tensor -. y_tensor
echo "--- x_tensor *. y_tensor 1次元配列を掛算した結果を2次元配列に作成 ---"
echo x_tensor *. y_tensor
echo "--- x_tensor /. y_tensor 1次元配列を割算した結果を2次元配列に作成 ---"
echo x_tensor /. y_tensor
# x_tensor -.= y_tensor # 1次元では置き換えできないみたい 同一列行のみ置き換え可能
# ただし、左辺の変数は変更可能なvarにしないと行けませんヨ
tensor型同士の算術には、+.
、-.
、*.
、/.
を付けて計算が可能。
出力結果
--- x_tensor +. y_tensor 1次元配列を加算した結果を2次元配列に作成 ---
Tensor[system.float] of shape "[4, 3]" on backend "Cpu"
|11 12 13|
|21 22 23|
|31 32 33|
|41 42 43|
--- x_tensor -. y_tensor 1次元配列を減算した結果を2次元配列に作成 ---
Tensor[system.float] of shape "[4, 3]" on backend "Cpu"
|-9 -8 -7|
|-19 -18 -17|
|-29 -28 -27|
|-39 -38 -37|
--- x_tensor *. y_tensor 1次元配列を掛算した結果を2次元配列に作成 ---
Tensor[system.float] of shape "[4, 3]" on backend "Cpu"
|10 20 30|
|20 40 60|
|30 60 90|
|40 80 120|
--- x_tensor /. y_tensor 1次元配列を割算した結果を2次元配列に作成 ---
Tensor[system.float] of shape "[4, 3]" on backend "Cpu"
|0.1 0.2 0.3|
|0.05 0.1 0.15|
|0.0333333 0.0666667 0.1|
|0.025 0.05 0.075|
4. 配列の並べ替え
import std/[math, sequtils, strformat]
import arraymancer
echo "並べ替え"
let tensor1 = @[
@[1, 2],
@[3, 4]
].toTensor
let tensor3 = toSeq(1..24).toTensor().reshape(2,3,4) # 引数z, y, x
echo tensor3.permute(0,2,1) # 並べ替え zはそのまま, y <-> xの並べ替え
# 引数 0, 1, 2は順番を意味するんだと思う
echo tensor1.permute(0) # 並べ替え 2次元の2x2は引数1だが値0の場合は何も起きない
echo tensor1.permute(1) # 並べ替え 2次元の2x2なら引数1でOK
let tensor10 = toSeq(1..6).toTensor().reshape(2,3)
echo tensor10
echo "↓ 並べ替え"
echo tensor10.permute(1,0) # 並べ替え x <-> yの並べ替え
echo tensor10.permute(0,1) # 並べ替え x <-> yの並べ替え何も起きない
permute
関数は、tensor型内部の値を並べ替える事が可能です。
引数は一番右側がx方向、次に、y方向、一番左がz軸方向で、それぞれ、順番を意味しているのだと思います。英語の操作方法を読んでも理解出来なかったので、2次元配列の変数tensor10
に色々引数を変えて確認しました。
出力結果
並べ替え
Tensor[system.int] of shape "[2, 4, 3]" on backend "Cpu"
0 1
|1 5 9| |13 17 21|
|2 6 10| |14 18 22|
|3 7 11| |15 19 23|
|4 8 12| |16 20 24|
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|1 2|
|3 4|
Tensor[system.int] of shape "[2, 2]" on backend "Cpu"
|1 3|
|2 4|
Tensor[system.int] of shape "[2, 3]" on backend "Cpu"
|1 2 3|
|4 5 6|
↓ 並べ替え
Tensor[system.int] of shape "[3, 2]" on backend "Cpu"
|1 4|
|2 5|
|3 6|
Tensor[system.int] of shape "[2, 3]" on backend "Cpu"
|1 2 3|
|4 5 6|
5. tensor型のmap機能
import std/[math, sequtils, strformat]
import arraymancer
# map機能
let map_tensor = [1.0, 2.0, 3.0, 4.0].toTensor().reshape(2,2)
proc sin_chg[T](x: T): T =
sin(x)
echo map_tensor.map(sin_chg) # map機能で内部に変数を設定
# 引数が2つのバージョンをmap2と言う
let map_tensor2 = [1.0, 2.0, 3.0, 4.0].toTensor().reshape(2,2)
proc `**`[T](x, y: T): T = # We create a new power `**` function that works on 2 scalars
pow(x, y)
echo map_tensor.map2(`**`, map_tensor2)
arraymancerにはtensor型を操作する2つのmap関数があります。
map
関数を1つのtensor型に対して変更を行え、map2
は2つのtensor型に対して変更を加える事が可能です。
また、それぞれのmap
関数には、配列のすべての値を変更可能な外部関数を指定できますので、sin
、cos
、tan
などに値の変更が可能になります。
出力結果
Tensor[system.float] of shape "[2, 2]" on backend "Cpu"
|0.841471 0.909297|
|0.14112 -0.756802|
Tensor[system.float] of shape "[2, 2]" on backend "Cpu"
|1 4|
|27 256|
6. tensor型のイテレーター
import arraymancer
let tensor1 = [1.0, 2.0, 3.0, 4.0].toTensor().reshape(2,2)
for i, v in tensor1:
echo i
echo v
tensor型はseq型やarray型と同じで、イテレーターでループさせる事が可能ですが、イテレーターからの戻りが位置と値の両方を返してきます。下記参照。
出力結果
@[0, 0]
1.0
@[0, 1]
2.0
@[1, 0]
3.0
@[1, 1]
4.0
最初のi
変数にN次元の位置情報、v
変数には値を返します。
おわりに
今回は、Nim言語でも広く使われている、arraymancerライブラリの基本操作について説明しました。
作者のサイトでは、python+numpyより数十倍速い結果が出されていましたが、実際使ってみて、それ程差はないかな?とは思っていますが、C言語へコンパイルする分、numpyよりは速いと思います。
Discussion