NArrayの簡単なつかい方

RubyにはNumPyがないってよく言われますが、RubyにもNArrayがあるよ。

   image.png     image.png


NArrayについて

OSSで開発されているRubyの行列計算ライブラリです。開発者は@masa16さんでC言語で実装されています。



  • Numo::NArray


    • 高速

    • 機械学習のRumaleや深層学習のRed Chainerもこれを使用。

    • GPGPUに対応したCumoもある。

    • 単にNArrayといった場合、前身プロジェクトの旧NArrayを意味していることがある。




リファレンスおよび資料

そのほか、公式ドキュメントなど


NArrayの簡単なつかい方

NArrayの世界は広くて、全体を理解するのはちょっとハードです。

なのでこれさえ知っておけば8割ぐらいの用途にカバーできるかも…といったあたり1を目指してまとめます。


NArrayの型

Rubyの配列とちがって、NArrayには型があります。

整数
符号なし整数
小数
複素数

Numo::Int8
Numo::UInt8
Numo::SFloat
Numo::SComplex

Numo::Int16
Numo::UInt16
Numo::DFloat
Numo::DComplex

Numo::Int32
Numo::UInt32

Numo::Int64
Numo::UInt64

使用頻度が高いのは、UInt8 Int32 SFloat DFloat あたりでしょうか。


前準備

以下すべてinclude Numoを行った状態で書きます。

require 'numo/narray'

include Numo

Numo::Int32Int32 って記載するっていう意味です。


NArrayの生成

require 'numo/narray'

include Numo

Int32[1,2,3]

 [1, 2, 3]

Int32.new(3,3).seq

 [[0, 1, 2],

  [3, 4, 5],

  [6, 7, 8]]

a = [[0, 1], [2, 3]]

Int32[*a]

 [[0, 1],

  [2, 3]]

Int32[1..5]

 [1, 2, 3, 4, 5]

Int32[1...5]

  [1, 2, 3, 4]


NArrayの四則演算

require 'numo/narray'

include Numo

a = Int32.new(3,3).seq

 [[0, 1, 2],

  [3, 4, 5],

  [6, 7, 8]]

b = Int32.new(3,3).seq + 1

 [[1, 2, 3],

  [4, 5, 6],

  [7, 8, 9]]

a + b

 [[1, 3, 5],

  [7, 9, 11],

  [13, 15, 17]]

b - a

 [[1, 1, 1],

  [1, 1, 1],

  [1, 1, 1]]

a * b

 [[0, 2, 6],

  [12, 20, 30],

  [42, 56, 72]]

a * 2

 [[0, 2, 4],

  [6, 8, 10],

  [12, 14, 16]]

a * 0.1

 [[0, 0.1, 0.2],

  [0.3, 0.4, 0.5],

  [0.6, 0.7, 0.8]]

  キャストが自動的に行われる


型を変換する

計算中に自動ででキャストされることが多いのですが、明示的にやる場合は、

a = UInt8[1,2,3]

SFloat.cast(a)
# => Numo::SFloat#shape=[3]

とします。ここではUInt8をSFloatに変換している。


行列積

ニューラルネットワークによく出てくる計算

c = Int32.new(2,2).seq

# [[0, 1],
# [2, 3]]

d = Int32.new(2,1).seq

# [[0],
# [1]]

c.dot d

# [[1],
# [3]]

より高速に計算したい場合は、Numo::Linalgを使います。

Numo::Linalg.dot(c,d)

Numo::Linalg.matmul(c,d)
# [[1],
# [3]]


いろいろな行列を生成する

ゼロ行列

x = Int32.zeros(3,3)

 [[0, 0, 0],

  [0, 0, 0],

  [0, 0, 0]]

全要素が1の行列

x = Int32.ones(3,3)

 [[1, 1, 1],

  [1, 1, 1],

  [1, 1, 1]]

全要素が2の行列

z = y.fill 2

 [[2, 2, 2],

  [2, 2, 2],

  [2, 2, 2]]

単位行列

y = Int32.eye(3,3)

 [[1, 0, 0],

  [0, 1, 0],

  [0, 0, 1]]

等間隔の行列

f = DFloat.linspace(-20,20,11)

[-20, -16, -12, -8, -4, 0, 4, 8, 12, 16, 20]


NArrayの情報を取得する

e = DFloat.new(3,4).seq

# 各次元
e.shape # => [3, 4]

# 要素の数
e.size # => 12

# バイトサイズ
e.byte_size # => 96

# 次元数
e.rank # => 2 #e.ndim も同じ


文字列との相互変換

外部のツールと情報をやりとりする時に、文字列へ変換するときがあります。

s = UInt8.ones(2,2).to_string

# => "\x01\x01\x01\x01"
# [[1, 1],
# [1, 1]]

UInt8.from_string(d)
# => Numo::UInt8#shape=[4]
# [1, 1, 1, 1]
# 次元数の情報は含まれないので注意

NArrayの行列(画素)も文字列に変換すれば、TkやGtkなどのGUIツールキットと接続できるので、リアルタイムな何かを作るときに便利です。


Ruby Arrayへの変換

to_aで大丈夫です

a = UInt8[1,2,3]

a.to_a


NMath

sin cos tan log などはここにあります。

f = DFloat.linspace(-20,20,11)

# [-20, -16, -12, -8, -4, 0, 4, 8, 12, 16, 20]

NMath.sin(f)
# [-0.912945, 0.287903, 0.536573, -0.989358, 0.756802, 0, -0.756802, ...]

NMath.cos(f)
# [-0.912945, 0.287903, 0.536573, -0.989358, 0.756802, 0, -0.756802, ...]

NMath.tanh(f)
# [-1, -1, -1, -1, -0.999329, 0, 0.999329, 1, 1, 1, 1]

NMath.log(f)
# [nan, nan, nan, nan, nan, -inf, 1.38629, 2.07944, 2.48491, 2.77259, ...]

NMath.log10(f)
# [nan, nan, nan, nan, nan, -inf, 0.60206, 0.90309, 1.07918, 1.20412, ...]

NMath.sqrt(f)
# [-nan, -nan, -nan, -nan, -nan, 0, 2, 2.82843, 3.4641, 4, 4.47214]


要素を呼び出す

a = Int32.new(10,10).seq

# [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
# [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
# [30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
# [40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
# [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
# [60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
# [70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
# [80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
# [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]]

a[0,true]
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

a[true, 0]
# [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

a[1..3, 2..4]
# [[12, 13, 14],
# [22, 23, 24],
# [32, 33, 34]]

a[1]
# 1

a[-1]
# 99

a.diagonal
# [0, 11, 22, 33, 44, 55, 66, 77, 88, 99]

a.transpose
# [[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
# [1, 11, 21, 31, 41, 51, 61, 71, 81, 91],
# [2, 12, 22, 32, 42, 52, 62, 72, 82, 92],
# [3, 13, 23, 33, 43, 53, 63, 73, 83, 93],
# [4, 14, 24, 34, 44, 54, 64, 74, 84, 94],
# [5, 15, 25, 35, 45, 55, 65, 75, 85, 95],
# [6, 16, 26, 36, 46, 56, 66, 76, 86, 96],
# [7, 17, 27, 37, 47, 57, 67, 77, 87, 97],
# [8, 18, 28, 38, 48, 58, 68, 78, 88, 98],
# [9, 19, 29, 39, 49, 59, 69, 79, 89, 99]]

a.reverse
# [[99, 98, 97, 96, 95, 94, 93, 92, 91, 90],
# [89, 88, 87, 86, 85, 84, 83, 82, 81, 80],
# [79, 78, 77, 76, 75, 74, 73, 72, 71, 70],
# [69, 68, 67, 66, 65, 64, 63, 62, 61, 60],
# [59, 58, 57, 56, 55, 54, 53, 52, 51, 50],
# [49, 48, 47, 46, 45, 44, 43, 42, 41, 40],
# [39, 38, 37, 36, 35, 34, 33, 32, 31, 30],
# [29, 28, 27, 26, 25, 24, 23, 22, 21, 20],
# [19, 18, 17, 16, 15, 14, 13, 12, 11, 10],
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]]

a.reshape(4,25)
# [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...],
# [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, ...],
# [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, ...],
# [75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, ...]

a.max
# 99

a.min
# 0

a.minmax
# [0, 99]

a.sum
# 4950

a.sum 0 # 軸の方向を指定
# [450, 460, 470, 480, 490, 500, 510, 520, 530, 540]

a > 49
# => Numo::Bit#shape=[10,10]
# [[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, 0, 0],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

bit = (a > 49)
bit.where
# => Numo::Int32#shape=[50]
# [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, ...]
# インデックスを返す

a.eq 33
# [[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, 1, 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, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

(a.eq 33).where
# => Numo::Int32#shape=[1]
# [33]

a[a > 90]
# [91, 92, 93, 94, 95, 96, 97, 98, 99]

a = Int32.new(10).seq - 5

# [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]

# 絶対値
a.abs
# a = Int32.new(10).seq - 5
# [5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

b = Int32.new(10).seq

# 足しあわせ
b.cumsum
# [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]


乱数

# 一様な乱数

x = DFloat.new(1000).rand
y = DFloat.new(1000).rand

image.png

# 正規分布

x = DFloat.new(1000).rand_norm
y = DFloat.new(1000).rand_norm
# rand_norm([mu,[sigma]])

image.png

乱数にこだわりたい人はGNU Scientific Libraryをみると、面白い乱数がたくさん搭載されていたりします。

それらをNumo::GSLから呼び出してもよいかもしれません。


統計

a = DFloat.new(100,100).rand_norm(1, 2)

# => Numo::DFloat#shape=[100,100]

# 要素数
a.size
# => 10000

# 平均
a.mean
# => 0.9941970100670163

# 中央値
a.median
# => Numo::DFloat#shape=[]
# 1.0030267444162986

# 分散
a.var
# => 3.9539947182922974

# 標準偏差
a.stddev
# => 1.9884654179271757

# 二乗平均平方根
a.rms
# => 2.223067028599605


ソート

na = Int32[1, 10, 2, 5, 9, 8, 12, 11, 3, 7, 4, 6]

na.sort

#=> Numo::Int32#shape=[12]
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

na.max_index
# => 6

na.min_index
# => 0

na.sort_index
=> Numo::Int32#shape=[12]
[0, 2, 8, 10, 3, 11, 9, 5, 4, 1, 7, 6]


View

NArrayをスライシングした時に、ビューが返ってきます。

a = Int8[1,2,3,4,5]

# [1, 2, 3, 4, 5]
b = a.view
# => Numo::Int8(view)#shape=[5]
# [1, 2, 3, 4, 5]

a[0] = 0
b
# => Numo::Int8(view)#shape=[5]
# [0, 2, 3, 4, 5]

b[1] = 0
a
# => Numo::Int8#shape=[5]
# [0, 0, 3, 4, 5]


結合

a = Int32[1,2,3]

b = Int32[4,5,6]

a.append b
# [1,2,3,4,5,6]

a.concatenate b
# [1,2,3,4,5,6]

a.hstack b

Int32.hstack [a,b]
# [1,2,3,4,5,6]

Int32.dstack [b,b]
# => Numo::Int32#shape=[2,3,2]
# [[[1, 1],
# [2, 2],
# [3, 3]],
# [[4, 4],
# [5, 5],
# [6, 6]]]


保存と読み込み

Marshal.dumpが使えます

a = Int32.new(2,2).seq

# 保存
s = Marshal.dump(a)
File.binwrite("data", s)

# 読み込み
b = Marshal.load(File.read("data"))
a == b
# true


NArrayで高速計算するために


  1. ループを使わない


    • Rubyを使っていると map などのイテレータをよく使います。しかし、NArrayではループを使わずに行列のまま計算します。



  2. 次元の入れ替え



  3. 条件分岐は a[a>0] a[a.eq 0] などBooleanマスクで代用する。


    • 配列のなかの特定の要素にのみ演算を加えたい場合でもifを使わないですみます。




  4. concatenate append hstack vstack hstack などを避ける


    • 上記のメソッドはNArrayのサイズを変更するメソッドです。

    • とっても便利なのですが、実はRubyのコードで新しいNArrayを生成しています。

    • Cumoを併用する場合は避けた方が無難かもしれません。(Cumoが遅くなる原因になる)

    • 逆にCumoを使用する予定が全くないなら、あまり気にしなくてよいかも。




  5. inplace を使う



    • a = a + 10 と書くところを、a.inplace + 10 と記載すると少し速くなります。

    • 上書きするのでメモリ確保の時間がはぶけるとのこと



  6. Numo::Linalg や Cumo を導入する


    • NArrayで完結する行列計算を大量に行う場合、Cumoを導入するとかなりの高速化が期待できます。

    • CUDA環境を作るのは初心者にはハードルが高いです。十分に時間があるときに2台以上PCが使える状況で挑戦した方がよいでしょう。




プロジェクトにフィードバックしよう


  • 明らかに計算結果がおかしい場合は、単純にバグを踏んでる可能性もあります。そういうときはGitHubのページでプロジェクトにフィードバックしましょう。ほかにも困ってるユーザーがいるかも。

  • 感想をブログなどに書いておいてもらえると、他のユーザーの参考になると思います。


付録 Numoファミリーについて

代替がないことも多く重宝する。

この記事は以上です。(この記事は自ブログの記事を改訂したものです。)





  1. 自分が知っている範囲とも