1.7. 多次元配列、ベクトル、行列、テンソル

 $2$次元以上の配列あるいは行列を表すのにも、$1$次元配列のときに紹介したndarrayを使う。$2$次元の場合[[]]のように括弧を二重にすることがポイントである。例でみてみよう。

In [1]:
import numpy as np
a = np.array([
    [1, 1], 
    [2, 2]
])
print(a)
 
[[1 1]
 [2 2]]
 

 $3$行目のように]のあとのカンマ(,)が行と行の区切れ目を表している。見やすくするのために3行目と4行目のように改行をしているが、改行しなくてもエラーにはならない。

 $1$次元配列を$2$次元的に扱いたい場合、つまり行ベクトルと列ベクトルの違いを明示的に表したいときも、[[ ]]のように括弧を二重にする。1次元配列、行ベクトル、列ベクトルの違いは下のようになる。

In [2]:
a = np.array([1, 2, 3]) #1次元配列
print(a)
b = np.array([[1, 2, 3]]) #行ベクトル
print(b)
c = np.array([[1], [2], [3]]) #列ベクトル
print(c)
 
[1 2 3]
[[1 2 3]]
[[1]
 [2]
 [3]]
 

 aは、[の数が一重なので1次元配列。bは二重なので行ベクトル、cは二重でそれぞれに,が入っているので列ベクトルになる。行ベクトル、列ベクトルは2次元の行列なので、転置行列なども定義できる。転置行列は.Tを付ければよい。

In [3]:
print(b)
print(b.T) #行ベクトルの転置は列ベクトル
 
[[1 2 3]]
[[1]
 [2]
 [3]]
 

 一方、aは行ベクトルではなく1次元配列なので、.Tで転置しようとしても、

In [4]:
print(a)
print(a.T)
 
[1 2 3]
[1 2 3]
 

のように、エラーは出ないが1次元配列のままで、列ベクトルに転置はされない。

 $1$次元配列のところでみたように、np.arangeを使うと$1$次元配列が生成されるが、$1$次元配列から$2$次元配列(行列)を作りたいときは、.reshape(行数,列数)を付けてサイズを変更することによって実現できる。

In [5]:
c = np.arange(0, 12, 1).reshape(3,4)
print(c)
 
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
 

のように、要素を$12$個もつ配列から、$3 \times 4$の行列を作ることができる。ただし、配列の要素の総数と行列のサイズの積が等しくないとエラーになる。

 

 もちろん行列のサイズを$n \times 1$とすれば、列ベクトルを作ることができる。

In [6]:
c = np.arange(0, 10, 1).reshape(10,1)
print(c)
 
[[0]
 [1]
 [2]
 [3]
 [4]
 [5]
 [6]
 [7]
 [8]
 [9]]
 

 行列のサイズは.shapeを付けると取り出すことができる。

In [7]:
print(c.shape)
print(c.T.shape)
 
(10, 1)
(1, 10)
 

 

多次元配列のaxis

 NumPyの多次元配列にはaxisという概念がある。2次元配列(行列)を例にとると、行がaxis=0、列がaxis=1に対応している。”行”・”列”の順番で0, 1になっているので分かりやすいだろう。通常のグラフのように横軸を$x$軸に、縦軸を$y$軸に対応させた場合、$xy$座標は$(x,y)$のように表記するが、行列の場合、行が縦($y$軸)方向、列が横($x$軸)方向なので、配列の要素としては順番が逆になり、a[y,x]のようになることに注意が必要である。3次元以上になるとややこしくなるので、$xy$ではなくaxis=0, 1, 2, ...で考えることをお勧めする。

 

 

$2$次元配列/行列のスライスに関する注意点

 $1$次元配列のところで、スライスを紹介したが、2次元でも多用する。スライスに関して今後コードを書いていく際に頻発すると思われるエラーやバクの原因になることの多い注意点をここで紹介しておく。

 例えば下のような行列があったとする。

In [8]:
x = np.array([
    [0, 1, 2],
    [3, 4, 5]])
print(x)
 
[[0 1 2]
 [3 4 5]]
 

 この行列の0列目を切り出して「列ベクトル」を作りたいとする。0列目の要素を全部取り出したいので、1次元配列のときにコロン(:)が要素全部を表すことを思い出すと、単純に思い浮かぶのは下のようなコードではないだろうか。

In [9]:
x_0 = x[:, 0]
print(x_0)
 
[0 3]
 

 確かに行列x0列目の要素が取り出されているのだが、結果をよく見ると[が二重になっていないので、列ベクトルではなく1次元配列になっていることが分かる。shapeで形を確認してみても、

In [10]:
print(x_0.shape)
 
(2,)
 

となって、要素数が$2$の1次元配列になってしまっている。このように取り出したい(スライスしたい)部分の要素がスカラーの場合(今の例では0)、取り出された配列の次元性は失われてしまう。これを防いで列ベクトルとして取り出したい場合は、取り出す部分をスカラーではなく1次元配列で指定しないといけない。つまり、スライスしたい要素番号を指定するとき、0ではなくて0:1と書かなければいけないということだ。

In [11]:
x_0 = x[:, 0:1]
print(x_0)
print(x_0.shape)
 
[[0]
 [3]]
(2, 1)
 

 この方法だと確かに列ベクトルになっていて、サイズも$2 \times 1$になっている。この0:1という表現は1を含まないので、結局のところ0番目の列だけを指定していることになるのだが、先に述べたように0のようなスカラーで指定すると次元性が失われてしまうので、元の2次元配列(x)の次元性を保つには、0:1のように配列で指定しなければならない。この事実を知らないと実際にコードを書くときハマるので気を付けてほしい。

 

 

行列の掛け算

数学でよく勉強した行列の掛け算をするには、np.dotを用いる。ベクトル$\boldsymbol{a}$に左から行列$\boldsymbol{M}$をかけるには、

In [12]:
M = np.array([[1, 0, 1],
              [0, 2, 1],
              [0, 0, 3]])
a = np.array([[1], [2], [3]])
c = np.dot(M, a)
print(c)
 
[[4]
 [7]
 [9]]
 

 行列の演算はかける順番が重要であるのと同じで、np.dotの引数の順番を逆にするとサイズが合わないというエラーになる。

In [13]:
c = np.dot(a, M)
 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-13-6b15307e12fc> in <module>
----> 1 c = np.dot(a, M)

<__array_function__ internals> in dot(*args, **kwargs)

ValueError: shapes (3,1) and (3,3) not aligned: 1 (dim 1) != 3 (dim 0)
 

 

配列(テンソル)の演算

 配列の演算で重要なものが三つある。

機能   function名
スカラー積、ドット積(内積)   np.dot
ベクトル積、クロス積(日本語で外積と習うもの)   np.cross
直積   np.outer

例を挙げて使ってみた方がわかりやすいと思うで以下に示す。

 

 まず二つの$1$次元配列a, bを適当に定義し、この二つの配列に対して、np.dot, np.cross, np.outerがどのように演算されるか見てみる。

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

 

np.dot

 

 配列同士をnp.dotした場合、いわゆる内積であるスカラー積ドット積$~\boldsymbol{a} \cdot \boldsymbol{b}$として計算される。

In [15]:
c = np.dot(a,b)
print(c)
 
6
 

 abは両方とも同じサイズのベクトルなので、結果はスカラーとして返される。

 

 

np.cross

np.crossベクトル積クロス積$~\boldsymbol{a} \times \boldsymbol{b}$を表す。大学1年で習った外積はこのクロス積のことである。

In [16]:
c = np.cross(a,b)
print(c)
 
[-5 -2  1]
 

 当然順番を入れ替えるとベクトルの向きが反対になるので、符号が反対になる。

In [17]:
c = np.cross(b,a)
print(c)
 
[ 5  2 -1]
 

 

np.outer

 

 np.outerは’outer’と書いてあるが日本語の外積のことでなく直積$~\boldsymbol{a} \otimes \boldsymbol{b}$のことである(ややこしい!)。直積は、

 
\begin{equation} \boldsymbol{a} \otimes \boldsymbol{b} = \begin{pmatrix} a_0 \\ a_1 \\ a_2 \\ \end{pmatrix} \otimes \begin{pmatrix} b_0 & b_1 & b_2 \end{pmatrix} = \begin{pmatrix} a_0 b_0 & a_0 b_1 & a_0 b_2 \\ a_1 b_0 & a_1 b_1 & a_1 b_2 \\ a_2 b_0 & a_2 b_1 & a_2 b_2 \\ \end{pmatrix} \end{equation}
 

で表されるような演算で、トランプの1から13までの数字と4つのスート(♠、♣、♦、♥)によって52枚のカードが作られる演算も直積である。

In [18]:
c = np.outer(a,b)
print(c)
 
[[ 1 -1  3]
 [-2  2 -6]
 [ 1 -1  3]]
 

このように、$3 \times 1$の1次元配列二つから、$3 \times 3$の行列が作られる。

 なお、1次元配列ではなく、列ベクトル、行ベクトルとして明示しておけば、np.dotでも直積を表すことができる。

In [19]:
a = np.array([[1, 2, 3]])
b = np.array([[2, 2, 3]])
c = np.dot(a.T, b)
print(c)
c = np.outer(a,b)
print(c)
 
[[2 2 3]
 [4 4 6]
 [6 6 9]]
[[2 2 3]
 [4 4 6]
 [6 6 9]]
 

 順番を逆にすれば以下のように内積となる。

In [20]:
c = np.dot(b, a.T)
print(c)
 
[[15]]
 

 結果を見てわかるように、[が二重になっているので、これはスカラーではなく、要素が1個の二次元配列と認識されている。また、以下のように行ベクトル同士を演算させようとするとサイズが合わないのでエラーになる。

In [21]:
c = np.dot(a, b)
print(c)
 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-42f62e060a81> in <module>
----> 1 c = np.dot(a, b)
      2 print(c)

<__array_function__ internals> in dot(*args, **kwargs)

ValueError: shapes (1,3) and (1,3) not aligned: 3 (dim 1) != 1 (dim 0)
 

 扱っている変数が一次元配列なのか、多次元配列なのかによって演算子のふるまいが全くことなることがあり、実際にコードを書くときハマる要因になるので気を付けてほしい。

 

 

行列の要素同士の計算

 行列計算ではなく、同じサイズの二つの行列の要素同士を掛け算するには、*を用いる。

In [22]:
a = np.array([[1, 1],
             [0, 2]])
b = np.array([[1, 0],
             [0, 1]])
c = a*b
print(c)
c = np.dot(a,b)
print(c)
 
[[1 0]
 [0 2]]
[[1 1]
 [0 2]]
 

 当然のことだが行列の要素同士の計算*と、行列の計算np.dotでは結果は全く異なる。

 

 

練習問題

(1.7.1.) $3\times3$の単位行列を作成せよ。

(1.7.2.) 二次元平面において、原点を中心として30°回転させる回転行列$\boldsymbol{R}$を作成せよ。

(1.7.3.) 下記のような、サイズが$100\times100$の三重対角行列$\boldsymbol{A}$を作成せよ。 \begin{equation*} A = \begin{pmatrix} -2 & 1 & & & 1 \\ 1& -2 & 1 & & \\ & & \ddots & \ddots & \\ & & 1 & -2 & 1 \\ 1 & & & 1 & -2 \\ \end{pmatrix} \end{equation*}

 

(1.7.4.) すべての要素が行番号に等しい以下のような$5 \times 7$の行列を作れ。

\begin{equation*} \begin{pmatrix} 0 & 0 & & 0 \\ 1& 1 & &1 \\ \vdots & \vdots & \ddots & \vdots \\ 4 & 4 & & 4 \\ \end{pmatrix} \end{equation*}

 

 

 

解答例

(1.7.1.) np.arrayを使って全部書き出して、

In [23]:
import numpy as np
import matplotlib.pyplot as plt
a = np.array([[1., 0, 0],
              [0, 1, 0],
             [0, 0, 1]])
print(a)
 
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
 

としてもいいが、単位行列を出力するnp.eyeというfunctionを使って、

In [24]:
a = np.eye(3)
print(a)
 
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
 

とするのが楽。

 

 

(1.7.2.) 回転角をthetaとしてラジアンで定義しておく。

In [25]:
theta = 30/360 * 2*np.pi
R = np.array([[np.cos(theta), -np.sin(theta)],
            [np.sin(theta), np.cos(theta)]])
print(R)
 
[[ 0.8660254 -0.5] 
[ 0.5 0.8660254]]
 

 

(1.7.3.) 単位行列をnp.rollして足し合わせると簡単に作れる。

In [26]:
import matplotlib.pyplot as plt
n = 100
eye = np.eye(n)
a = -2 * eye + np.roll(eye, 1, axis=0) + np.roll(eye, -1, axis=0)
 

確認のため、結果を(10,10)成分まで示す。

In [27]:
print(a[:10,:10])
plt.imshow(a[:10,:10])
 
[[-2.  1.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1. -2.  1.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  1. -2.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  1. -2.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1. -2.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  1. -2.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1. -2.  1.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  1. -2.  1.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  1. -2.  1.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  1. -2.]]
Out[27]:
<matplotlib.image.AxesImage at 0x2abcdd04948>
 
 

 このタイプの三重対角行列は離散系の微分演算子に対応するので、数値計算のいろいろな場面で登場する。

 

 

(1.7.3.) np.outerして作ると

In [28]:
a = np.arange(0, 5.)
#b = np.array([1, 1, 1, 1, 1, 1, 1])
b = np.ones([7])
c = np.outer(a, b)
print(c)
 
[[0. 0. 0. 0. 0. 0. 0.]
 [1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2.]
 [3. 3. 3. 3. 3. 3. 3.]
 [4. 4. 4. 4. 4. 4. 4.]]
 

 全部が1の要素を持つ配列は、今の問題では手で入力してもいいが、3行目のようにnp.onesを使う。

 np.meshgridを使って作ってもいいだろう。

In [29]:
c0, c1 = np.meshgrid(b, a)
print(c1)
 
[[0. 0. 0. 0. 0. 0. 0.]
 [1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2.]
 [3. 3. 3. 3. 3. 3. 3.]
 [4. 4. 4. 4. 4. 4. 4.]]
 

 このような列の要素がすべて同じで、列方向に値が比例して増えていく二次元配列は、その転置も含めて、データにオフセットを加えてプロットしたいときによく使う。

 


当サイトのテキスト・画像の無断転載・複製を固く禁じます。
Unauthorized copying and replication of the contents of this site, text and images are strictly prohibited.
© 2019 Go Yusa