Skip to content

Numo::NArray Overview (Japanese)

Akira Ikeda edited this page Jul 2, 2023 · 2 revisions

このドキュメントはチュートリアルではなく、旧NArrayやnumpy等の数値配列システムを 知っている人向けに、新NArrayの特徴を述べたものです。

Numo::NArrayの型

旧版の NArray では、要素の型をオブジェクトの属性としていました。 Ruby/Numo 版の NArray では、Numo::NArray クラスのサブクラスによって 要素の型を表すという仕様にしています。 例えば、Numo::DFloat クラスは、64ビット浮動小数点数の NArray クラスとなります。

  • 整数

      Numo::Int8
      Numo::Int16
      Numo::Int32
      Numo::Int64
      Numo::UInt8
      Numo::UInt16
      Numo::UInt32
      Numo::UInt64
    
  • 浮動小数点数

      Numo::SFloat
      Numo::DFloat
    
  • 浮動小数点複素数

      Numo::SComplex
      Numo::DComplex
    
  • ビット

      Numo::Bit
    
  • Rubyオブジェクト

      Numo::RObject
    
  • 構造体

      Numo::Struct - 名称変更予定?
    

要素の型を属性でなくクラスにした理由は次の通りです。

  • 「型を表すもの」が、旧NArrayは内部では整数であり、型との対応が明白ではなかったが、 新NArrayではRuby内で一意のクラスであり、型との対応が明白。
  • 旧NArrayでは、型によって異なる演算関数を呼ぶために、メソッド1つ1つに関数マップが必要であったが、 新NArrayでは、Rubyのメソッドディスパッチシステムにより型ごとに処理が分岐されるため、関数マップが不要になる。
  • 旧NArrayでは後から型を追加することが不可能だが、新NArrayでは可能である。

NArray生成

NArray のサブクラスの new メソッドにより、配列のメモリが割り当てられていない NArray オブジェクトを生成します。

$ irb -r numo/narray
irb> a = Numo::DFloat.new(4,5,6)
=> Numo::DFloat#shape=[4,5,6](empty)

irb> a.shape
=> [4, 5, 6]

irb> a.ndim
=> 3

irb> a.size
=> 120

new メソッドの引数には、shape を指定します。 shape とは、多次元配列における各次元のサイズの配列です。 上の例(a.shape == [4,5,6])では、 3次元配列(a.ndim == 3)を表し、 各次元の要素数はそれぞれ 4,5,6 であり、 合計の要素数は a.size == 4*5*6 == 120 となります。

要素の格納

new で生成されたばかりの NArray に対して、要素にアクセスしようとすると例外が上がります。 次のメソッドにより、配列データのメモリを割り当て、値を格納します。

store(another_array)
fill(item)
seq([begin, step]), indgen
logseq(begin, step [,base])
eye([element, offset])
rand([[low,] high])
irb> Numo::DFloat.new(2,4,6).seq
=> Numo::DFloat#shape=[2,4,6]
[[[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]]]

データ生成

初めからデータを持つ NArray オブジェクトを生成する方法として、 次の例に示すメソッドがあります。

irb> Numo::DFloat[1,2,3,5,7,11]
=> Numo::DFloat#shape=[6]
[1, 2, 3, 5, 7, 11]

irb> Numo::DFloat[1..100]
=> Numo::DFloat#shape=[100]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, ...]

irb> Numo::DFloat.cast(Numo::Int32[1,2,3,5,7,11])
=> Numo::DFloat#shape=[6]
[1, 2, 3, 5, 7, 11]

irb> Numo::DFloat.zeros(120)
=> Numo::DFloat#shape=[120]
[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, ...]

irb> Numo::DFloat.ones(2,4,6)
=> Numo::DFloat#shape=[2,4,6]
[[[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]]]

irb> Numo::DFloat.linspace(-5,5,7)
=> Numo::DFloat#shape=[7]
[-5, -3.33333, -1.66667, 0, 1.66667, 3.33333, 5]

irb> Numo::DFloat.logspace(4,0,5,2)
=> Numo::DFloat#shape=[5]
[16, 8, 4, 2, 1]

配列のインデックス

Ruby の Array クラスと同じように、 [] メソッドに要素を参照、[]= メソッドで要素を格納できます。 インデックスが負のときは、(その次元の)要素数を加えた値がインデックスとみなされます。 つまり最後の要素を参照するには -1 を指定します。

多次元配列では、各次元ごとにインデックスをカンマで区切って与えるか、 1次元のフラットな配列とみなした場合のインデックスを与えることもできます。

irb> a = Numo::DFloat.new(2,4,6).seq
=> Numo::DFloat#shape=[2,4,6]
[[[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]]]

irb> a[1,2,3]
=> 39.0

irb> a[39]
=> 39.0

Numo::NArray の次元の順序は、C-order、つまり後ろの方の次元が速く回ります。 (ちなみに旧版の NArray は Fortran-order)

配列のビュー

[] メソッドの引数に、 Range または Array を与えると、配列のビューを返します。 配列のビューとは、他の配列の一部または全部を参照する「窓」であり、 ビューに対して要素を操作すると、元の配列の要素も書き換えます。

irb> a = Numo::DFloat.new(6).seq
=> Numo::DFloat#shape=[6]
[0, 1, 2, 3, 4, 5]

irb> v = a[1..3]
=> Numo::DFloat(view)#shape=[3]
[1, 2, 3]

irb> v.store([11,12,13])
=> Numo::DFloat(view)#shape=[3]
[11, 12, 13]

irb> a
=> Numo::DFloat#shape=[6]
[0, 11, 12, 13, 4, 5]
# a[1..3] = [11,12,13] と同じ

(その次元の)すべての要素の参照(つまり0..-1)は true で代替できます。 高度な使い方として、残りの次元に対してすべて true を与えたいが次元の個数が不定のとき、 false を与えることもできます。

インデックスが Array オブジェクトの場合は、Array をインデックスの配列と見なし、 そのインデックスが指すビューを返します。

irb a = Numo::DFloat.new(6).seq(1,0.5)
=> Numo::DFloat#shape=[6]
[1, 1.5, 2, 2.5, 3, 3.5]

irb> a[[2,3,5]]
=> Numo::DFloat(view)#shape=[3]
[2, 2.5, 3.5]

[] とほぼ同じメソッドに slice があります。 インデックスが整数の場合の挙動が、[]slice で異なります。 [] の場合、インデックスに整数を与えると、その次元が取り除かれ、次元数が減ります。 (Range/Array で指定した次元は、結果要素数が 1 でも残ります。)

irb> a = Numo::DFloat.new(3,4).seq
=> Numo::DFloat#shape=[3,4]
[[0, 1, 2, 3], 
 [4, 5, 6, 7], 
 [8, 9, 10, 11]]

irb> a[1..2,3]
=> Numo::DFloat(view)#shape=[2]
[7, 11]

一方、slice の場合は、整数を指定した次元でも、要素数 1 の次元として残り、 次元数が変わりません。

irb> a = Numo::DFloat.new(3,4).seq
=> Numo::DFloat#shape=[3,4]
[[0, 1, 2, 3], 
 [4, 5, 6, 7], 
 [8, 9, 10, 11]]

irb> a.slice(1..2,3)
=> Numo::DFloat(view)#shape=[2,1]
[[7], 
 [11]]

配列操作

以下のメソッドは、配列の shape を変更したビューを返します。

flatten([dim0,dim1,...])
transpose([dim0,dim1,...])
expand_dims(dim)
diagonal(offset, [ax1,ax2])

以下のメソッドは、配列の shape を変更したコピーを返します。

reshape

配列演算

NArray には以下の演算メソッドがあります。

+
-
*
/
%
divmod
**
-@
abs

NArray の演算は、基本的に要素ごと(element-wise)の演算です。 以下のルールがあります。

  • Upcast
  • Broadcast
  • coerce
  • inplace

Upcast

a*b などの演算で、 ab で型が異なる場合に、結果の型を決めるルールを Upcast といいます。 Numo::NArray では、基本的には下に書かれた順に、右の方の型に揃えるというルールにしています。

Ruby Numeric -> Numo::Int,UInt -> Numo::Float -> Numo::Complex -> Numo::RObject

Intのサイズや単精度・倍精度で異なる場合は大きい方の型になります。 また、Ruby Float と、Numo::Int32 など整数 NArray との演算では、結果は Numo::DFloat になります。

Upcast ルールを決めているのは、各型クラスネームスペースの UPCAST 定数にアサインされた Hash データです。 例えば Numo::Int32::UPCAST の内容は次のようになります。

irb> Numo::Int32::UPCAST
=> {Array=>Numo::Int32, Fixnum=>Numo::Int32, Bignum=>Numo::Int32,
    Float=>Numo::DFloat, Complex=>Numo::DComplex,
    false=>Numo::Int32, Numo::DComplex=>Numo::DComplex,
    Numo::SComplex=>Numo::SComplex, Numo::DFloat=>Numo::DFloat,
    Numo::SFloat=>Numo::SFloat, Numo::Int64=>Numo::Int64,
    Numo::Int32=>Numo::Int32, Numo::UInt64=>Numo::Int64}

ここに含まれない Int8 型の時は、Numo::Int8::UPCAST を見に行く、というようにして決まります。

Broadcast

2つの NArray があり、

a.shape == [2,3,1]
b.shape == [1,3,4]

のとき、

c = a*b
c.shape == [2,3,4]

となります。

つまり、Broadcast とは、 片方の配列のある次元の要素数が 1 であり、 もう一方の配列の対応する次元の要素数が 1 より大きいとき、 要素数1の要素が繰り返し使われるというルールです。 それ以外で次元ごとのの要素数が異なる場合は、例外が上がります。

一方の次元数が少ない場合は、次元が前側(遅く回る側)に拡張され、1 が入ったものとみなされます。 つまり、次の NArray の場合も、結果は上の例と同じになります。

a.shape == [2,3,1]
b.shape ==   [3,4]

coerce

NArray は coerce をサポートしています。したがって、

irb> a = Numo::DFloat.new(5).seq
=> Numo::DFloat#shape=[5]
[0, 1, 2, 3, 4]

irb> 0.5 * a
=> Numo::DFloat#shape=[5]
[0, 0.5, 1, 1.5, 2]

という書き方も可能です。coerce の中で、0.5 の Float が、Upcast で決まる型の NArray オブジェクトに変換され、改めて演算メソッドが呼ばれます。 このとき、0.5 のオブジェクトは 0 次元の NArray に変換されます。 したがって、前出の Broadcast ルールの適用によって、配列に対して値が繰り返し適用されます。

inplace

inplace メソッドは、inplace フラグが立ったビューを返します。 inplace フラグが立った NArray は、inplace に対応したメソッドが 呼ばれたとき、結果を元の配列に上書きします。

irb> a = Numo::DFloat.new(5).seq
=> Numo::DFloat#shape=[5]
[0, 1, 2, 3, 4]

irb> a.inplace + 1
=> Numo::DFloat#shape=[5]
[1, 2, 3, 4, 5]

irb> a
=> Numo::DFloat#shape=[5]
[1, 2, 3, 4, 5]
 # a に上書きされている

inplace を使わない場合、次のように書いても同じ結果が得られます。

a += 1  # a = a + 1 のシンタックスシュガー

しかしこの場合は、演算結果の配列を新たに確保してからそこに書き込むことになります。 一方、inplace の場合は、結果の配列を確保せず、元の配列に上書きするため、 メモリの節約となり、速度も向上します。

条件式とBit型

NArray は、次の条件メソッドを持ちます。

eq
ne
gt (>)
ge (>=)
lt (<)
le (<=)
nearly_eq
isnan
isinf
isfinite

これらは Numo::Bit 配列を返します。 Numo::Bit 配列は、次のメソッドを持ちます。

and  (&)
or   (|)
xor  (^)
not  (~)
count_true
count_false
all?
any?
none?
where

条件式の使い方の例として、次のいずれかの方法で、負数を0に置き換えます。

irb> a = 3 - Numo::DFloat.new(7).seq
=> Numo::DFloat#shape=[7]
[3, 2, 1, 0, -1, -2, -3]

irb> a[a<0] = 0
=> 0

irb> a
=> Numo::DFloat#shape=[7]
[3, 2, 1, 0, 0, 0, 0]


irb> a = 3 - Numo::DFloat.new(7).seq
=> Numo::DFloat#shape=[7]
[3, 2, 1, 0, -1, -2, -3]

irb> a[(a<0).where] = 0
=> 0

irb> a
=> Numo::DFloat#shape=[7]
[3, 2, 1, 0, 0, 0, 0]

統計メソッド

次の統計メソッドが定義されています。

sum
prod
mean
stddev
var
rms
min
min_index
max
max_index
minmax
cumsum
cumprod
sort
sort_index
median

引数に次元番号(左が0番目、複数個指定可)を与えると、 指定した次元の中で統計を求めます。

NMath

sqrt, exp, log, 三角関数などが定義されています。