多次元配列について

３.３ではNumpyを使って多次元配列の計算を行います。
###3.3.1 多次元配列
多次元配列は簡単にいうと**『数字の集合』**です。
具体的には以下のようなものがあります。

- 数字が
    - ただそこに存在している（０次元配列）
    - 一列に並んだものや長方形状に並んだもの（１、２次元配列）
    - N次元状に並んでいるもの（３次元以上）

![多次元配列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/9e8b50bc-5e35-7666-d042-014890a7f75c.png)

![３次元配列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/c3114bf0-fcdd-05bf-b065-588c04e5d122.png)

それではNumpyを使って多次元配列を作成していきましょう。

**1. １次元配列の作成**

```python:１次元配列
>>> import numpy as np
>>> A = np.array([1,2,3,4])
>>> print(A)
[1,2,3,4]
>>>np.ndim(A)   #配列の次元数を取得
1
>>>A.shape      #配列の形状をshapeというインスタンス変数で取得
(4,)            #タプルになっていることに注意
>>>A.shape[0]
4
```

上の例ではAは１次元の配列であり、４つの要素から構成されていることがわかります。

※ここではA.shapeの結果がタプルになっていることに注意しましょう。
これは１次元配列の場合であっても、多次元配列の場合と同じ統一された結果を返すからです。


```python:タプルになっている理由
(4,3)       #２次元配列の場合　→ タプル
(4,3,2)     #3次元配列の場合　→ タプル

#１次元配列でも統一してタプルで表示
```

**2. 2次元配列の作成**

続いて２次元配列を作成していきます。

```python:２次元配列
>>> B = np.array([[1,2], [3,4], [5,6]])
>>> print(B)
[[1 2]
 [3 4]
 [5 6]
>>> np.ndim(B)
2
>>> np.shape
(3,2)
```

ここでは「３✖️２」の配列であるBを作成しました。

「３✖️２」の配列とは
最初の次元に３つの要素があり、次の次元に要素が２つあるという意味です。

**なお、最初の次元は０番目の次元、次の次元は１番目の次元に対応します。**
（Pythonのインデックスは０から始まります。）

また、２次元配列は以下に示すように**行列**とも呼びます。
![スクリーンショット 2021-08-16 15.11.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/d229c230-952d-da75-8cd4-23cbec823524.png)


###3.3.2 行列の内積

続いて、行列（２次元配列）の内積について説明します。

行列の内積は計算の手順が定義されており、例えば次のように行ないます。

![スクリーンショット 2021-08-16 15.17.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/d20896f0-c5cc-36c3-986e-ce00a236e574.png)

そして、この計算をPythonで実装すると次のようになります。

```Python:Pythonで行列の内積を計算
>>> A = np.array([[1,2], [3,4]])
>>> A.shape
(2,2)
>>> B = np.array([[5,6], [7,8]])
>>> B.shape
(2,2)
>>> np.dot(A,B)      #引数としてNumPy配列を２つ取り、それらの配列の内積を結果として返す関数
array([[19,22],      #結合法則，分配法則は成立するが，交換法則は成立しない
       [43,50]])
```

ここで注意の必要なことがあります。
それは、**行列の積では、被演算子（A,B）の順番が異なると結果も異なる**ということです。

<br>
先ほどは２✖️２の形状の行列についての計算を行いましたが、別の形状の行列についても同様に計算を行うことができます。

```Python:例）2✖️3の行列と3✖️2の行列の積
>>> A = np.array([[1,2,3], [4,5,6]])
>>> A.shape       
(2,3)
>>> B = np.array([[1,2], [3,4], [5,6]]) 
>>> B.shape
(3,2)
>>>np.dot(A,B)
array([[22, 28],
       [49, 64]])
```
2✖️3の行列と3✖️2の行列の積は上のように実装できます。
ここでも注意点があります。それは
**行列の１次元目の要素数（列数）と行列Bの０次元目の要素数（行数）を同じ値にする必要がある**
というものです。

もしこの値が異なれば、行列の計算はできません。
例えば、Pythonで行うとすれば、以下のようなエラーが出力されます。


```Python:2✖️3の行列と2✖️2の行列の積~エラー内容~
>>> C = np.array([[1,2], [3,4]])
>>> C.shape
(2,2)
>>> A.shape
(2,3)
>>> np.dot(A, C)
Traceback (most recent call last):
  File "<stdint>", line 1, in <module>
ValueError: shapes (2,3) and (2,2) not aligned: 3 (dim 1) !=2(dim 0 ) 
#↑行列Aの１次元目と行列Cの０次元目の次元数が一致していないという意味
```

つまり、**多次元配列の積では、２つの行列で対応する次元を一致させる必要がある**ということです。
大切なことなのでもう一度図を用いて説明しておきます。

![スクリーンショット 2021-08-16 17.14.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/270ab8cb-db23-a24b-00b9-4e15119764ba.png)

**計算結果である行列Cは、行列Aの行数と行列Bの列数から構成される**　←これも重要な点です。

なお、Aが２次元の行列で、Bが１次元の配列でも以下に示すように**「対応する次元の要素数を一致させる」**という同じ原則が成り立ちます。

![スクリーンショット 2021-08-16 17.25.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/49b18ff0-730f-3a05-9b69-b97f5e5456a8.png)

Pythonで実装すると次のようになります。


```Python:対応する次元の要素数を一致させる~行列と配列~ 
>>> A = np.array([[1,2], [3,4], [5,6]]) 
>>> A.shape
(3,2)
>>> B =np.array([7,8])
>>> B.shape
(2,)
>>>np.dot(A, B)
array([23, 53, 83])
```

###3.3.3 ニューラルネットワークの内積

それでは次はNumpy行列を使ってニューラルネットワークの実装を行います。
ここでは以下の簡単なニューラルネットワークを対象とします。
※バイアスと活性化関数は省略し、重みだけがあるとする

![スクリーンショット 2021-08-16 17.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/19b3cf11-23ad-7ba8-690b-491417a7a94e.png)

実装に関してはX、W、Yの形状に注意しましょう。
特にXとWの対応する次元の要素数が一致していることが重要な点です。

![スクリーンショット 2021-08-16 17.41.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1747266/fd420111-6b24-0deb-3bcc-132949fd4e23.png)

```python:ニューラルネットワークの実装~簡易版~
>>> X = np.array([1,2])
>>> X.shape
(2,)
>>> W =np.array([[1,3,5], [2,4,6]])
>>> print(W)
[[1 3 5]
 [2 4 6]
>>> W.shape
(2,3)
>>> Y = np.dot(X, W)
>>> print(Y)
[ 5 11 17]
```

np.dot(多次元配列のドット積)を使えば、Yの結果を一度に計算することができます。
これが意味することは、例えYの要素数が１００や１０００だとしても、一度に計算できるということです。

もし使わなければ、Yの要素を一つずつ取り出して計算（or for文を使って計算をしなければならない）のでとても面倒です。

そのため、行列の内積によって一度で計算ができるというテクニックは、実装上とても重要であると言えます。