numpyの内積(1) dotとvdotの違い


2023年 09月 07日

内積

Pythonにはnumpyという数学的な処理を行ってくれる拡張モジュールがあり、AIや技術計算、統計処理などを行う場合にはよく使う。

利用するには、importが必要で、通常 as np をつけて、numpyと書く代わりにnpで済ませることが多く、以下の説明でも np を用いることがある。

import numpy as np

内積の計算は、numpy.dot関数を使って次のように行っているのではないだろうか。最後は、要素毎の積の和の計算で、ベタな確認である。

>>> import numpy as np
>>> a = [1,2,3]
>>> b = [4,5,6]
>>> np.dot(a,b)
32
>>> 1*4+2*5+3*6
32

何次元でも可能なので、もうちょっと一般的なのを示す。

>>> c = list(range(1,10)); c
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> d = list(range(11,20)); d
[11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> np.dot(c,d)
735
>>> sum([x*y for x,y in zip(c,d)])
735

複素数

科学技術計算などでは、数は実数とは限らず、複素数のこともある。
とりあえず、複素数の場合の内積をnumpy.dot関数で求めて、検算もしておこう。

虚数単位は数学ではiで表現することが普通だが、Pythonではjを用いる。虚部は実数の直後にjをつけて示す。単独で j と書いた場合は虚数単位と解釈されずエラーとなるので、虚数単位を示すには 1j と書く。

>>> e = [1+2j,3+4j]
>>> f = [5+6j,7+8j]
>>> np.dot(e,f)
(-18+68j)
>>> (1+2j)*(5+6j)+(3+4j)*(7+8j)
(-18+68j)

numpy.vdot関数があり、dotとvdotは同じという説明を見かけるが、そうだろうか。

>>> np.vdot(a,b)
32
>>> np.vdot(c,d)
735
>>> np.vdot(e,f)
(70-8j)

aとb, cとdの内積は一致したが、eとfの内積は異なってしまった。

どうやら、以下のように、eの共役複素数とfのnumpy.dot演算が行われているようである。

>>> (1-2j)*(5+6j)+(3-4j)*(7+8j)
(70-8j)

numpyには、共役複素数を求める関数numpy.conjがあるので、これを使って確認しておこう。

>>> e
[(1+2j), (3+4j)]
>>> np.conj(e)
array([1.-2.j, 3.-4.j])
>>>np.dot(np.conj(e),f)
(70-8j)

eは正確には複素数のリストであるが、np.conjの変換結果はarrayがついているので、配列になっている。

だから、np.dotの第1パラメータは配列で、第2パラメータはリストとタイプが異なっているが、良きに計らってくれて、ちゃんと内積が求まる。

つまり、numpy.vdot関数は、次の内積が計算されているようだ。

$$ \boldsymbol{a\cdot b} = \sum_{i=1}^{n} \overline{a}_{i} b_{i}$$

ここで $\overline{a}_{i}$は複素数$a_{i}$の共役複素数である。

数学の教科書

しかし、複素ベクトルの内積は、線型代数の教科書では

$$ \boldsymbol{a\cdot b} = \sum_{i=1}^{n} a_{i} \overline{b}_{i}$$

となっているはずだ。

これに従って計算すると、

>>> np.dot(e,np.conj(f))
(70+8j)
>>> np.dot(e,np.conj(f)).conj()
(70-8j)

となって、虚部の符号が逆になる。

複素ベクトルの内積が、数学の世界とnumpyの世界で異なっているが、どうしてだろう。

つづく