今回は2つの1次元ベクトルの外積を求める関数である、NumPyのnp.outer関数について扱っていきます。

外積・直積(outer)とは

外積といっても高校の数学で習っていたような外積(クロス積)を求めるのではなく、2つのベクトルの要素の全ての掛け合わせるテンソル積のことです。日本語の名称では同じ外積を使用することがありますが、英語では2つのベクトルに直交するようなベクトルをもとめる意味でcross product、今回扱う意味での全ての組み合わせの掛け合わせを求める意味でouterと呼びます。

ここはそのまま関数の名前に反映されているので覚えておいて損はないので覚えておきましょう。

先ほども述べた通り、この関数では引数として指定された2つの1次元ベクトルの要素同士の掛け合わせ全ての組み合わせを2次元配列の要素にして返すものです。行方向には最初に指定されたベクトルの要素が変化していき、列方向には2番目に指定されたベクトルの要素が変化していきます。

例えば、2つの1次元ベクトルa(要素数M)、b(要素数N)があったとします。

a = (a_0, a_1, \ldots, a_{M-2}, a_{M-1}) \\ b = (b_0, b_1, \ldots, b_{N-2}, b_{N-1}) \\

np.outer(a, b)を実行すると、出力されるのはM×Nの2次元配列となります。np.outerの処理は以下のコードと全く一緒です。

a.reshape(-1,1) * b

aを縦ベクトルにしてbと掛け合わせるとブロードキャスティング機能により結果的に全ての組み合わせによる2値の乗算の結果が出力されます。これは以下のような2次元配列が出力されることを意味します。

\left( \begin{array}{cccc} a_0*b_0 & a_0*b_1 & \ldots & a_0*b_{N-1} \\ a_1*b_0 & a_1*b_1 & \ldots & a_1*b_{N-1} \\ \vdots & \vdots & \ddots & \vdots \\ a_{M-1}*b_0 & a_{M-1}*b_1 & \ldots & a_{M-1}*b_{N-1} \\ \end{array} \right)

np.outer

では関数の使い方についてみていきます。np.outer関数のAPIドキュメントは以下の通りです。

numpy.outer(a, b, out=None)

params:

パラメータ名 概要
a array_like
配列に相当するもの(要素数M)
入力する最初のベクトル。こちらが行方向になる。2次元以上の配列が指定されたときは1次元に変換される。
b array_like
配列に相当するもの(要素数N)
入力する2番目のベクトル。こちらが列方向になる。2次元以上の配列が指定されたときは1次元に変換される。
out M×Nの配列(ndarray) (省略可能)初期値None
結果を代入する配列を指定します。

returns:

out[i, j] = a[i] * b[j]
となるようなM×Nの2次元配列を返します。

引数として指定するのはシンプルに外積を求めたい2つのベクトル(1次元でなくともよいが計算の際は1次元に変換される)と結果を格納する配列の3つだけです。3つめの引数outに関しては使われることがほとんど無いためここでの解説は割愛します。

使い方自体は全く難しくないので実際のコードで使い方をみていきましょう。

In [1]: import numpy as np

In [2]: a = np.array([1,2,3,2,1])

In [3]: b = np.array([0, 2, 4, 6, 8, 1]) # 2つの1次元配列を作る。

In [4]: np.outer(a, b) # 外積を求める
Out[4]:
array([[ 0,  2,  4,  6,  8,  1],
       [ 0,  4,  8, 12, 16,  2],
       [ 0,  6, 12, 18, 24,  3],
       [ 0,  4,  8, 12, 16,  2],
       [ 0,  2,  4,  6,  8,  1]])

In [5]: a.shape # それぞれの形状を確認
Out[5]: (5,)

In [6]: b.shape
Out[6]: (6,)

In [7]: np.outer(a, b).shape # ちゃんと(M, N)になっている
Out[7]: (5, 6)

In [8]: np.outer(a, b) == a.reshape(-1,1)*b # ブロードキャストを使って同様の計算もできる。
Out[8]:
array([[ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True]], dtype=bool)

2次元配列を引数として指定すると1次元配列に変換されてから計算されます。

In [11]: b = b.reshape(2, -1)

In [12]: c = np.random.randint(0, 5, size=(2,4))

In [13]: b
Out[13]:
array([[0, 2, 4],
       [6, 8, 1]])

In [14]: c
Out[14]:
array([[1, 4, 4, 0],
       [2, 2, 4, 1]])

In [15]: np.outer(b,c)
Out[15]:
array([[ 0,  0,  0,  0,  0,  0,  0,  0],
       [ 2,  8,  8,  0,  4,  4,  8,  2],
       [ 4, 16, 16,  0,  8,  8, 16,  4],
       [ 6, 24, 24,  0, 12, 12, 24,  6],
       [ 8, 32, 32,  0, 16, 16, 32,  8],
       [ 1,  4,  4,  0,  2,  2,  4,  1]])

In [16]: np.outer(b.ravel(), c.ravel()) # 1次元配列に変換したものを指定しても結果は同じ
Out[16]:
array([[ 0,  0,  0,  0,  0,  0,  0,  0],
       [ 2,  8,  8,  0,  4,  4,  8,  2],
       [ 4, 16, 16,  0,  8,  8, 16,  4],
       [ 6, 24, 24,  0, 12, 12, 24,  6],
       [ 8, 32, 32,  0, 16, 16, 32,  8],
       [ 1,  4,  4,  0,  2,  2,  4,  1]])

参考