🔰

Nimで多次元配列を操作

2023/02/18に公開

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の基本的な使い方

sample01.nim
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型配列値を算術すると、自動で無い部分を算術して埋めてくれます。
その例が下記の結果です。

sample02.nim
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. 配列の並べ替え

sample03.nim
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機能

sample04.nim
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関数には、配列のすべての値を変更可能な外部関数を指定できますので、sincostanなどに値の変更が可能になります。

出力結果

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型のイテレーター

sample05.nim
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