深掘りNxシリーズ ~ tensor を知る ② 算術計算 ~

東京にいるけどfukuokaexのYOSUKEです。
最近、[エリクサーちゃんで学ぶ Elixir](https://youtu.be/_QzrB6tTPwA)の動画を作成し始めてるので良かったらチャンネル登録お願いします。本格的に始動するのは年明けからの予定です。

さて、兼ねてより興味があったライブラリ、Nxについて勉強して行こうと思います。この記事は自分の為の備忘録でもあります。が、Elixir初学者でも分かりやすいように、なるべく解説をつけながらアウトプット目指します。

前回の記事はこちらです。

https://qiita.com/Yoosuke/items/faafc1510bbae99f7036

この回では、Nxを使った、算術計算の方法を見ていきたいと思います。

Nxのライブラリを見ると、様々な関数が用意されていて、単純に四則演算の関数を探すのが大変だったので、まとめておきます。

https://hexdocs.pm/nx/Nx.html#divide/2

## Nxで四則演算に利用できる関数

```elixir
Nx.add #足し算
Nx.subtract #引き算
Nx.multiply #掛け算
Nx.divide #割り算
```
これら、Nxを利用すると、線形代数の計算が簡単にでき、
スカラー、ベクトル、行列の計算が分かりやすく表現されます。
また、ベクトルや行列はNxではNx.tensorと言う構造体で表現されます。

## 線形代数のおさらい

Nxの計算ライブラリを利用する上で線形代数の基礎知識は必要なのでここで少しおさらいしておきます。
とはいえ、あんまり数学的な話しをここでくどくどしても難しいのと、数学的な定義を説明すると、細かく書かざる得ないので、詳しくは専門書に任せるとして、ここでは、プログラミング的に理解しやすい話しとして書いておきます。 その為、線形代数を学びたい人は必ず、専門書を見てください。

まずは、用語の整理から

* 集合：集合とは、いくつかのものの集まりをいう。例えば、次の集まりを果物の集合と表現する。など
    * 果物の集合 A は {りんご、みかん、いちご}
* 要素:集合を形成する一つ一つの値を表現する。例えば、上の例では りんご、みかん、いちごなどは集合の中の要素の1つである。

* ベクトル：ベクトルは1次元配列のようなものをイメージしてください。始点と終点があれば矢印のような線が引けると思います。そういった方向や大きさを示せる表現可能な値だと思ってください。例えば、{0, 1, 2}とした場合、0を視点として、線の長さが2まで引かれた矢印をイメージできます。このベクトルですが、横に表現する事も、縦に表現する事もできます。横に表現したベクトルを行ベクトルといいます。縦に表現したベクトルを列ベクトルといいます。

* 行ベクトル：　行ベクトルは、次のようなイメージです。 これは、プログラミングでも簡単に表現できます。
```elixir
( 1, 2, 3 )
```
* 列ベクトル： 列ベクトルは上記を縦に転値したイメージです。 ただし、これはプログラミングで記載しても縦にはなりません。見た目を縦にしても、プログラム上は行ベクトルと同じ解釈をされますが、人間が解釈する場合には縦書きに意味を持たせて解釈します。
 
```elixir
(1,
 2,
 3)
```

* スカラー：スカラーとは、ベクトルが方向や大きさを示せる表現だとすれば、スカラーは単に量を示せます。
    * 単に単一の数値で表現できます。 例えば、温度は 18度、　身長は170cmなどのように示せます。

* 行列：行列とは、ベクトルを要素にした集合です。2次元配列のように表現できます。例えば、以下のようなイメージです。

```elixir
   [
    [1, 2, 3]
    [2, 3, 4]
    [3, 4, 5]
   ]
```

## Nxの構造体による表現
上の節では、線形代数で扱う集合や値についておさらいしました。それをNxのライブラリを利用してどのような構造体として表現されているか確認しておきます。

### スカラー
スカラーは、単に数値として表現できます。Nx.tensorに数値のみで入力して扱う事もできます。
```elixir
iex()> s = Nx.tensor(2)
#Nx.Tensor<
  s64
  2
>
```

### 行ベクトル
```elixir
iex()> row = Nx.tensor([1, 2, 3])
#Nx.Tensor<
  s64[3]
  [1, 2, 3]
>
```

### 列ベクトル
```elixir
iex()> column = Nx.tensor([[1],[3],[8]])
#Nx.Tensor<
  s64[3][1]
  [
    [1],
    [3],
    [8]
  ]
>
```

### 行列

:names オプションは、テンソルの次元にも名前を付けることができます。
以下は2次元の例

```elixir
iex()> matrix = Nx.tensor([[1,2],[3,4]], names: [:x, :y])
#Nx.Tensor<
  s64[x: 2][y: 2]
  [
    [1, 2],
    [3, 4]
  ]
>
```

では、3次元はどのようにすれば良いでしょうか？ という事で試してみました。
ちゃんと3次元も表現できるようです。

```elixir
matrix = Nx.tensor([[[1,2],[2,3]],[[3,4],[5,6]]], names: [:x, :y, :z])
#Nx.Tensor<
  s64[x: 2][y: 2][z: 2]
  [
    [
      [1, 2],
      [2, 3]
    ],
    [
      [3, 4],
      [5, 6]
    ]
  ]
>
```
## スカラーの計算

Nxを利用したスカラーの計算を見ていきます。

```elixir
# Nx.add #足し算
iex()> Nx.add(1, 2)
#Nx.Tensor<
  s64
  3
>
iex()> s1 = Nx.tensor(2)
#Nx.Tensor<
  s64
  2
>
iex()> s2 = Nx.tensor(3)
#Nx.Tensor<
  s64
  3
>
iex()> Nx.add(s1, s2)
#Nx.Tensor<
  s64
  5
>

# Nx.subtract #引き算
iex()> Nx.subtract(s1, s2) 
#Nx.Tensor<
  s64
  -1
>
iex()> Nx.subtract(3, 2)  
#Nx.Tensor<
  s64
  1
>

# Nx.multiply #掛け算
iex()> Nx.multiply(2,3)
#Nx.Tensor<
  s64
  6
>
iex()> Nx.multiply(s1,s2)
#Nx.Tensor<
  s64
  6
>

# Nx.divide #割り算
iex()> Nx.divide(2,3)    
#Nx.Tensor<
  f32
  0.6666666865348816
>
iex()> Nx.divide(s1,s2)
#Nx.Tensor<
  f32
  0.6666666865348816
>

```

## スカラーとベクトルの計算

```elixir
# 足し算
iex()> Nx.add(s1, row)     
#Nx.Tensor<
  s64[3]
  [3, 4, 5]
>
iex()> Nx.add(s1, column)
#Nx.Tensor<
  s64[3][1]
  [
    [3],
    [5],
    [10]
  ]
> 
# 引き算
iex()> Nx.subtract(s1, row)
#Nx.Tensor<
  s64[3]
  [1, 0, -1]
>
iex()> Nx.subtract(s1, column)
#Nx.Tensor<
  s64[3][1]
  [
    [1],
    [-1],
    [-6]
  ]
>
# 掛け算
iex()> Nx.multiply(s1,row) 
#Nx.Tensor<
  s64[3]
  [2, 4, 6]
>
iex(110)> Nx.multiply(s1,column)
#Nx.Tensor<
  s64[3][1]
  [
    [2],
    [6],
    [16]
  ]
> 
# 割り算
iex()> Nx.divide(s1,row)      
#Nx.Tensor<
  f32[3]
  [2.0, 1.0, 0.6666666865348816]
>
iex()> Nx.divide(s1,column)
#Nx.Tensor<
  f32[3][1]
  [
    [2.0],
    [0.6666666865348816],
    [0.25]
  ]
>
```

## ベクトルとベクトルの計算

ベクトルの計算では、いくつかのパターンを見ていきます。
* 3行ベクトル x 3行ベクトル
* 3列ベクトル x 3列ベクトル
* 3行ベクトル x 3列ベクトル
* 2行ベクトル x 3列ベクトル（要素数が違うベクトルの計算）

まずは下準備に以下を追加で作ります。
```elixir
iex()> row1 = Nx.tensor([2,3,4], names: [:y])
#Nx.Tensor<
  s64[y: 3]
  [2, 3, 4] 
>
iex()> row2 = row2 = Nx.tensor([2,4])           
#Nx.Tensor<
  s64[2]
  [2, 4]
>
iex()> column1 = Nx.tensor([[2],[3],[4]])
#Nx.Tensor<
  s64[3][1]
  [
    [2],
    [3],
    [4]
  ]
>
iex()> column2 = Nx.tensor([[2],[3]])    
#Nx.Tensor<
  s64[2][1]
  [
    [2],
    [3]
  ]
>
```
### 3行ベクトル x 3行ベクトル
```elixir
iex()> Nx.add(row, row1)
#Nx.Tensor<
  s64[y: 3]
  [3, 5, 7] 
>
iex()> Nx.subtract(row, row1)
#Nx.Tensor<
  s64[y: 3]
  [-1, -1, -1]
>
iex()> Nx.multiply(row, row1)
#Nx.Tensor<
  s64[y: 3]
  [2, 6, 12]
>
iex()> Nx.divide(row, row1)
#Nx.Tensor<
  f32[y: 3]
  [0.5, 0.6666666865348816, 0.75]
>
```

### 3列ベクトル x 3列ベクトル
```elixir
iex()> Nx.add(column, column1)
#Nx.Tensor<
  s64[3][1]
  [
    [3],
    [6],
    [12]
  ]
> 
iex()> Nx.subtract(column, column1)
#Nx.Tensor<
  s64[3][1]
  [
    [-1],
    [0],
    [4]
  ]
> 
iex()> Nx.multiply(column, column1)
#Nx.Tensor<
  s64[3][1]
  [
    [2],
    [9],
    [32]
  ]
> 
iex()> Nx.divide(column, column1)
#Nx.Tensor<
  f32[3][1]
  [
    [0.5],
    [1.0],
    [2.0]
  ]
>
```
### 3行ベクトル x 3列ベクトル
```elixir
iex(130)> Nx.add(row, column)         
#Nx.Tensor<
  s64[3][3]
  [
    [2, 3, 4],
    [4, 5, 6],
    [9, 10, 11]
  ]
>
iex()> Nx.subtract(row, column)
#Nx.Tensor<
  s64[3][3]
  [
    [0, 1, 2],
    [-2, -1, 0],
    [-7, -6, -5]
  ]
>
iex()> Nx.multiply(row, column)
#Nx.Tensor<
  s64[3][3]
  [
    [1, 2, 3],
    [3, 6, 9],
    [8, 16, 24]
  ]
>
iex()> Nx.divide(row, column)  
#Nx.Tensor<
  f32[3][3]
  [
    [1.0, 2.0, 3.0],
    [0.3333333432674408, 0.6666666865348816, 1.0],
    [0.125, 0.25, 0.375]
  ]
>
```
### 3行ベクトル x 2列ベクトル（要素数が違うベクトルの計算）

```elixir
iex()> Nx.add(column, row2)     
#Nx.Tensor<
  s64[3][2]
  [
    [3, 5],
    [5, 7],
    [10, 12]
  ]
>
iex()> Nx.add(row2, column)
#Nx.Tensor<
  s64[3][2]
  [
    [3, 5],
    [5, 7],
    [10, 12]
  ]
>
iex()> Nx.subtract(column, row2)
#Nx.Tensor<
  s64[3][2]
  [
    [-1, -3],
    [1, -1],
    [6, 4]
  ]
>
iex()> Nx.subtract(row2, column) 
#Nx.Tensor<
  s64[3][2]
  [
    [1, 3],
    [-1, 1],
    [-6, -4]
  ]
>
iex()> Nx.multiply(column, row2)
#Nx.Tensor<
  s64[3][2]
  [
    [2, 4],
    [6, 12],
    [16, 32]
  ]
>
iex()> Nx.multiply(row2, column)
#Nx.Tensor<
  s64[3][2]
  [
    [2, 4],
    [6, 12],
    [16, 32]
  ]
>
iex()> Nx.divide(column, row2)
#Nx.Tensor<
  f32[3][2]
  [
    [0.5, 0.25], 
    [1.5, 0.75],
    [4.0, 2.0]
  ]
>
iex()> Nx.divide(row2, column)
#Nx.Tensor<
  f32[3][2]
  [
    [2.0, 4.0],
    [0.6666666865348816, 1.3333333730697632], 
    [0.25, 0.5]
  ]
>
```

## 行列

### 行列の準び

```elixir
iex()> left = Nx.tensor([[1.0,2.0,3.0],[2.0, 3.0, 4.0]], names: [:y, :x])
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [1.0, 2.0, 3.0],
    [2.0, 3.0, 4.0]
  ]
>
iex()> right = Nx.tensor([[3.0,4.0,5.0],[6.0, 7.0, 8.0]], names: [:y, :x])
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [3.0, 4.0, 5.0],
    [6.0, 7.0, 8.0]
  ]
>

iex()> mt3x3 = Nx.tensor([[1,2,3],[2,3,4],[3,4,5]])
#Nx.Tensor<
  s64[3][3]
  [
    [1, 2, 3],
    [2, 3, 4],
    [3, 4, 5]
  ]
>

iex()> mt2x2 = Nx.tensor([[1,2],[3,4]])
#Nx.Tensor<
  s64[2][2]
  [
    [1, 2],
    [3, 4]
  ]
> 

iex()> mx3x2 = Nx.tensor([[1,2],[3,4],[5,6]])
#Nx.Tensor<
  s64[3][2]
  [
    [1, 2],
    [3, 4],
    [5, 6]
  ]
>
iex()> mx2x3 = Nx.tensor([[1,2,3],[3,4,5]])      
#Nx.Tensor<
  s64[2][3]
  [
    [1, 2, 3],
    [3, 4, 5]
  ]
>
```
### 行列とベクトルの計算

```elixir
iex()> Nx.add(mt2x2, row2)
#Nx.Tensor<
  s64[2][2]
  [
    [3, 6],
    [5, 8]
  ]
>
iex()> Nx.add(mt2x2, column2)
#Nx.Tensor<
  s64[2][2]
  [
    [3, 4],
    [6, 7]
  ]
>
iex()> Nx.subtract(row2, mt2x2)
#Nx.Tensor<
  s64[2][2]
  [
    [1, 2],
    [-1, 0]
  ]
>
iex()> Nx.subtract(mt2x2, column2)
#Nx.Tensor<
  s64[2][2]
  [
    [-1, 0],
    [0, 1]
  ]
>

iex()> Nx.multiply(right, column2)
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [6.0, 8.0, 10.0],
    [18.0, 21.0, 24.0]
  ]
>
iex()> Nx.multiply(column2, right)
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [6.0, 8.0, 10.0],
    [18.0, 21.0, 24.0]
  ]
>
iex()> Nx.divide(right, column2)  
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [1.5, 2.0, 2.5],
    [2.0, 2.3333332538604736, 2.6666667461395264]
  ]
>
iex()> Nx.divide(column2, right)
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [0.6666666865348816, 0.5, 0.4000000059604645],
    [0.5, 0.4285714328289032, 0.375]
  ]
>
```

### 行列とベクトルの計算 エラーになるパターンとならないパターン
下記の例を見ると、要素の数が違うとエラーになります。

```elixir
mx3x2
#Nx.Tensor<
  s64[3][2]
  [
    [1, 2],
    [3, 4],
    [5, 6]
  ]
>

row
#Nx.Tensor<
  s64[3]
  [1, 2, 3]
>

row2
#Nx.Tensor<
  s64[2]
  [2, 4]
>
# 成功する場合
Nx.add(mx3x2, row2)
#Nx.Tensor<
  s64[3][2]
  [
    [3, 6],
    [5, 8],
    [7, 10]
  ]
>

# エラーが出る場合
Nx.add(mx3x2, row) 
** (ArgumentError) cannot broadcast tensor of dimensions {3, 2} to {3}
    (nx 0.4.1) lib/nx/shape.ex:335: Nx.Shape.binary_broadcast/4
    (nx 0.4.1) lib/nx.ex:3585: Nx.element_wise_bin_op/4

``` 

### 行列と行列 の算術計算

行列同士の和や差は以下のように計算します。
ここでは分かりやすいように同じ要素数のa行列とb行列があったとします。
行列は、2次元配列をイメージして、下はそれぞれ、aの0行0列目,aの0行1列目を表しているとイメージしてください。
※要素の位置を表す数値も2次元配列をイメージすると分かりやすいので0から数えてます。

```tex
a行列
[ 
 [ a00, a01 ],
 [ a10, a11 ]
]
b行列
[ 
 [ b00, b01 ],
 [ b10, b11 ]
]
```

この時、a行列 + b行列の和や差の計算は次のようにします。

```tex
a + b の 和
[
    [a00 + b00, a01 + b01]
    [a10 + b10, a11 + b11]
]
```
差を求める場合も同じです。和と同様に其々、同じ位置の要素同士の計算になります。
この事を理解した上で```Nx.add```や```Nx.substract```の計算を見ていきます。

```elixir
iex()> Nx.add(left, right)   
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [4.0, 6.0, 8.0],
    [8.0, 10.0, 12.0]
  ]
>
```

行列同士の差

```elixir
iex()> Nx.subtract(left, right)
#Nx.Tensor<
  f32[y: 2][x: 3]
  [
    [-2.0, -2.0, -2.0],
    [-4.0, -4.0, -4.0]
  ]
>
```

行列の積と商の計算では例えば、次のような行列が其々あったとします。
```tex
A = [
[a11, a12, a13],
[a21, a22, a23],
[a31, a32, a33],
]

B = [
[b11, b12, b13],
[b21, b22, b23],
[b31, b32, b33]
]
```

すると次のように計算します。
```tex
AB =
[
[a11b11 + a12b21 + a13b31, a11b12 + a12b22 + a13b32, a11b13 + a12b23 + a13b33]
[a21b11 + a22b21 + a23b31, a21b12 + a22b22 + a23b32, a21b13 + a22b23 + a23b33]
[a31b11 + a32b21 + a33b31, a31b12 + a32b22 + a33b32, a31b13 + a32b23 + a33b33]
]
```
ちょっと見ると大変ですが、行ベクトルと列ベクトルにの各成分の積を足し算してます。

これらの計算をNx.multiplyやdivideがしてくれるのでしょうか？
気になります。

```elixir
iex()> t1 = Nx.tensor([[1,0,1],[3,2,0],[1,1,2]])
#Nx.Tensor<
  s64[3][3]
  [
    [1, 0, 1],
    [3, 2, 0],
    [1, 1, 2]
  ]
>

iex()> t2 = Nx.tensor([[0,3,1],[2,0,1],[0,2,0]])
#Nx.Tensor<
  s64[3][3]
  [
    [0, 3, 1],
    [2, 0, 1],
    [0, 2, 0]
  ]
>
```

行列の計算をして、答えはいかになるはずです。

```elixir
[
    [0, 5, 1],
    [4, 9, 5],
    [2, 7, 2]
]
```

結果はどうでしょうか？

```elixir
iex()> Nx.multiply(t1, t2)                  
#Nx.Tensor<
  s64[3][3]
  [
    [0, 0, 1],
    [6, 0, 0],
    [0, 2, 0]
  ]
>
```
なるほど、どうやら行列式の積では無いようです。

単純に各、要素同士を掛け合わせているだけのようです。

次回はこの続きを調べて行こうと思います。

https://qiita.com/Yoosuke/items/a961067b17e20240cea3