PyTorchの基礎 (1) -Tensorの使い方-

# PyTorch とは

### 概要
Python のオープンソースの機械学習ライブラリ．
PyTorch は **Tensor**（`torch.Tensor`）と呼ばれるクラスを定義しており，多次元配列の保存と演算に利用している．Numpy の配列 Array に似ているが，CUDA が有効な Nvidia の GPU 上での演算も可能になっている．
[参照元] --> [PyTorch - Wikipedia](https://ja.wikipedia.org/wiki/PyTorch)

### 他のライブラリとの違い
機械学習ライブラリは，**Define by Run** と **Define and Run** の2種類に大別される．

 - **Define by Run**
実行しながらネットワークを定義する．
ネットワークを動的に変更できるため，柔軟な設計が可能である．
たとえば，データのサイズによってネットワークを切り替えたり，イテレーションごとに設計変更したりすることも可能．
有名なライブラリは，PyTorch や Chainer など．

 - **Define and Run**
先にネットワークを定義してから実行する．
レゴブロックのようにパーツを組み合わせるだけで簡単にネットワークを構成できる．
簡潔でわかりやすい．
有名なライブラリは，Keras や Tensorflow など．


PyTorch は，実行しながらネットワークを構築する Define by Run の機械学習ライブラリに属する．
このような違いがあるため，これらのライブラリは適材適所で使うのがよさそう．
通常のデータ分析業務では Define and Run の Keras が簡潔であり，詳細な設計が必要な研究や困難なタスクでは Define by Run の PyTorch が優位か．

[参照元] --> [『PyTorch入門』使い方＆Tensorflow, Keras等との違いとは？ - プロクラシスト](https://www.procrasist.com/entry/19-pytorch)

#### PyTorch と Chainer
PyTorch と Chainer の最大の違いは，PyTorch が海外のコミュニティで盛んに使用されるのに対して，Chainer は日本が主流であること．これは，Chainer が日本発の Preferred Networks (PFN)という会社が開発したことに起因する．
しかしながら，2019年12月に PFN が Chainer のメジャーアップデートを終了し PyTorch の研究開発に移行すると発表したことで，2つのライブラリの関係性は大きく一変した．詳しくは[こちら](https://preferred.jp/ja/news/pr20191205/)．
そのため，今後 Define by Run の機械学習ライブラリを使用する場合は PyTorch を選択するのが無難だろう．


# PyTorch 入門
PyTorch の公式チュートリアルを引用しながら，PyTorch の仕様を確認する．


## Tensor の使い方（前編：定義・演算）
[What is PyTorch? -- PyTorch Tutorials 1.4.0 documentation](https://pytorch.org/tutorials/beginner/blitz/tensor_tutorial.html#sphx-glr-beginner-blitz-tensor-tutorial-py)

PyTorch で使用する `Tensor` は Numpy の`ndarray`に似ているが，`Tensor`は計算の高速化のために GPU を用いて計算できる．
以下では，Numpy と比較しつつ PyTorch の`Tensor`の使い方をまとめる．

### ライブラリのインポート
``` python
import torch
import numpy as np
```

### 配列の定義
全ての要素がゼロの配列．

``` python
# Tensor
x_t = torch.zeros(2, 3)
# Numpy
x_n = np.zeros((2,3))

print('Tensor:\n',x_t,'\n')
print('Numpy:\n',x_n)

# ---Output---
#Tensor:
# tensor([[0., 0., 0.],
#        [0., 0., 0.]]) 
#Numpy:
# [[0. 0. 0.]
# [0. 0. 0.]]
```

全ての要素が1の配列．

```python
# Tensor
x_t = torch.ones(2,3)
# Numpy
x_n = np.ones((2,3))
​
print('Tensor:\n',x_t,'\n')
print('Numpy:\n',x_n)

# ---Output---
#Tensor:
# tensor([[1., 1., 1.],
#        [1., 1., 1.]]) 
#Numpy:
# [[1. 1. 1.]
# [1. 1. 1.]]
```

要素の値を指定した配列．

```python
# Tensor
x_t = torch.tensor([[5,3],[10,6]])
# Numpy
x_n = np.array([[5,3],[10,6]])

print('Tensor:\n',x_t,'\n')
print('Numpy:\n',x_n)

# ---Output---
#Tensor:
# tensor([[ 5,  3],
#        [10,  6]]) 
#Numpy:
# [[ 5  3]
# [10  6]]
```

要素の値を乱数で指定した配列．

```python
# Tensor
x_t = torch.rand(2,3)
# Numpy
x_n = np.random.rand(2,3)

print('Tensor:\n',x_t,'\n',x12_t,'\n')
print('Numpy:\n',x_n,'\n',x12_n)

# ---Output---
#Tensor:
# tensor([[0.5266, 0.1276, 0.6704],
#        [0.0412, 0.5800, 0.0312]]) 
# tensor(0.3370) 
#Numpy:
# [[0.08877971 0.51718009 0.99738679]
# [0.35288525 0.68630145 0.73313903]] 
# 0.1799177580940461
```

### 配列の要素の取得
配列の各要素へのアクセスは`x[0,1]`のように行えばよい（これで，配列`x`の1行2列目の要素が取得できる．）

```python
# Tensor
x12_t = x_t[0,1]
# Numpy
x12_n = x_n[0,1]

print('Tensor:\n',x12_t,'\n')
print('Numpy:\n',x12_n)

# ---Output---
#Tensor:
# tensor(0.1276) 
#Numpy:
# 0.5171800941956144
```

ここで注意が必要なのは，配列の要素を取得した際に，Numpy は数値を取得するが，PyTorch では数値ではなく Tensor を取得する点．そのため，PyTorch ではこのように抜き出した配列の要素をそのままスカラー量のように扱うことができない．Numpy と同様に数値を取り出す場合は，`Tensor.item()`を実行する必要がある．

```python
x12_value = x12_t.item()
print(x12_t)
print(x12_value)

# ---Output---
# tensor(0.1276)
# 0.12760692834854126
```

### 四則演算
PyTorch でも Numpy と同じ感覚で四則演算を行うことができる．

``` python
# Tensor
x_t = torch.Tensor([1,2,3])
y_t = torch.Tensor([2,2,2])
add_t = x_t + y_t
sub_t = x_t - y_t
mul_t = x_t * y_t
div_t = x_t / y_t
print('Tensor:\nAddition:\n',add_t,'\nSubtraction:\n',sub_t,
'\nMultiplication:\n',mul_t,'\nDivision:\n',div_t,'\n')

# Numpy
x_n = np.array([1,2,3])
y_n = np.array([2,2,2])
add_n = x_n + y_n
sub_n = x_n - y_n
mul_n = x_n * y_n
div_n = x_n / y_n
print('Numpy:\nAddition:\n',add_n,'\nSubtraction:\n',sub_n,
'\nMultiplication:\n',mul_n,'\nDivision:\n',div_n)

# ---Output---
#Tensor:
#Addition:
# tensor([3., 4., 5.]) 
#Subtraction:
# tensor([-1.,  0.,  1.]) 
#Multiplication:
# tensor([2., 4., 6.]) 
#Division:
# tensor([0.5000, 1.0000, 1.5000]) 
#
#Numpy:
#Addition:
# [3 4 5] 
#Subtraction:
# [-1  0  1] 
#Multiplication:
# [2 4 6] 
#Division:
# [0.5 1.  1.5]
```


## Tensor の使い方（後編：変換・自動微分）

[Autograd: Automatic Differentiation -- PyTorch Tutorials 1.4.0 documentation](https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html)

### 配列の形状操作
配列の形状情報（行数，列数）は`shape`メソッドで取得可能．Numpy と同様の挙動を示す．

```python
# Tensor
x_t = torch.rand(4,3)
row_t = x_t.shape[0]
column_t = x_t.shape[1]
print('Tensor:\n','row: ',row_t,'column: ',column_t)

# Numpy
x_n = np.random.rand(4,3)
row_n = x_n.shape[0]
column_n = x_n.shape[1]
print('Numpy:\n','row: ',row_n,'column: ',column_n)

# ---Output---
#Tensor:
# row:  4 column:  3
#Numpy:
# row:  4 column:  3
```

配列の形状を変更したい場合は，PyTorch では`.view()`，Numpy では`.reshape()`を使うことが多い．ただし，PyTorch の Tensor に対しても，Numpy と同様に`.reshape()`が使用できる．

```python
# Tensor
x_t = torch.rand(4,3)
y_t = x_t.view(12)
z_t = x_t.view(2,-1)
print('Tensor:\n',x_t,'\n',y_t,'\n',z_t,'\n')

# Numpy
x_n = np.random.rand(4,3)
y_n = x_n.reshape(12)
z_n = x_n.reshape([2,-1])
print('Numpy:\n',x_n,'\n',y_n,'\n',z_n)

# ---Output---
#Tensor:
# tensor([[0.5357, 0.2716, 0.2651],
#        [0.6570, 0.0844, 0.9729],
#        [0.4436, 0.9271, 0.4013],
#        [0.8725, 0.2952, 0.1330]]) 
# tensor([0.5357, 0.2716, 0.2651, 0.6570, 0.0844, 0.9729, 0.4436, 0.9271, 0.4013,
#        0.8725, 0.2952, 0.1330]) 
# tensor([[0.5357, 0.2716, 0.2651, 0.6570, 0.0844, 0.9729],
#        [0.4436, 0.9271, 0.4013, 0.8725, 0.2952, 0.1330]]) 
#
#Numpy:
# [[0.02711389 0.24172801 0.01202486]
# [0.59552453 0.49906154 0.81377212]
# [0.24744639 0.58570244 0.26464142]
# [0.14519645 0.03607043 0.46616757]] 
# [0.02711389 0.24172801 0.01202486 0.59552453 0.49906154 0.81377212
# 0.24744639 0.58570244 0.26464142 0.14519645 0.03607043 0.46616757] 
# [[0.02711389 0.24172801 0.01202486 0.59552453 0.49906154 0.81377212]
# [0.24744639 0.58570244 0.26464142 0.14519645 0.03607043 0.46616757]]
```

`.reshape()`を使用した場合．

```python
# Tensor
x_t = torch.rand(4,3)
y_t = x_t.reshape(2,-1)
#y_t = torch.reshape(x_t,[2,-1]) <-- Also works
print('Tensor:\n',y_t,'\n')

# Numpy
x_n = np.random.rand(4,3)
y_n = x_n.reshape(2,-1)
#y_n = np.reshape(x_n,[2,-1]) <-- Also works
print('Numpy:\n',y_n)

# ---Output---
#Tensor:
#tensor([[0.0617, 0.4898, 0.4745, 0.8218, 0.3760, 0.1556],
#        [0.3192, 0.5886, 0.8385, 0.5321, 0.9758, 0.8254]])
#
#Numpy:
#[[0.60080911 0.55132561 0.75930606 0.03275005 0.83148483 0.48780054]
# [0.10971541 0.02317271 0.22571149 0.95286975 0.93045979 0.82358474]]
```

配列の転置は，PyTorch では`.transpose()`または`.t()`で，Numpy では`.transpose()`または`.T`で行う．

```python
# Tensor
x_t = torch.rand(3,2)
xt_t = x_t.transpose(0,1)
#xt_t = torch.transpose(x_t,0,1)
#xt_t = x_t.t()
print('Tensor:\n',x_t,'\n',xt_t)

# Numpy
x_n = np.random.rand(3,2)
xt_n = x_n.transpose()
#xt_n = np.transpose(x_n)
#xt_n = x_n.T
print('Numpy:\n',x_n,'\n',xt_n)
# ---Output---
#Tensor:
# tensor([[0.8743, 0.8418],
#        [0.6551, 0.2240],
#        [0.9447, 0.2824]]) 
# tensor([[0.8743, 0.6551, 0.9447],
#        [0.8418, 0.2240, 0.2824]])
#Numpy:
# [[0.80380702 0.81511741]
# [0.29398279 0.78025418]
# [0.19421487 0.43054298]] 
# [[0.80380702 0.29398279 0.19421487]
# [0.81511741 0.78025418 0.43054298]]
```

### Numpy との変換
- `Tensor` --> `ndarray`

`Tensor`から`ndarray`に変換するときは，`Tensor.numpy()`とすればよい．
なお，変換後の`ndarray`は参照元の`Tensor`の変更の影響を受けない．（`ndarrya`は`Tensor`のコピーになっている．）連動させる場合は，in-place operation（各関数の最後に`_`を付ける．例えば`add_()`．）を使用する必要がある．

```python
a = torch.ones(5)
b = a.numpy()
a = a + 1
print('a = ',a)
print('b = ',b)

# ---Output---
# a =  tensor([2., 2., 2., 2., 2.])
# b =  [1. 1. 1. 1. 1.]

a = torch.ones(5)
b = a.numpy()
a.add_(1)
#torch.add(a,1,out=a) <-- Same operation
print('a = ',a)
print('b = ',b)

# ---Output---
# a =  tensor([2., 2., 2., 2., 2.])
# b =  [2. 2. 2. 2. 2.]
```

- `Tensor` <-- `ndarray`

`ndarray`から`Tensor`に変換するときは，`torch.from_numpy(ndarray)`とすればよい．

```python
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print('a = ',a)
print('b = ',b)
# ---Output---
# a =  [2. 2. 2. 2. 2.]
# b =  tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
```

### CUDA Tensor
`Tensor`は`.to()`メソッドを使うことで，計算領域を移動させることができる．
これにより，`Tensor`を CPU メモリから GPU メモリへ移動し計算を実行することができる．

```python
x = torch.rand(2,3)

if torch.cuda.is_available():
    device = torch.device("cuda")          # a CUDA device object
    y = torch.ones_like(x, device=device)  # directly create a tensor on GPU
    x = x.to(device)                       # or just use strings ``.to("cuda")``
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # ``.to`` can also change dtype together!

# ---Output---
#tensor([[1.1181, 1.1125, 1.3122],
#        [1.1282, 1.5595, 1.4443]], device='cuda:0')
#tensor([[1.1181, 1.1125, 1.3122],
#        [1.1282, 1.5595, 1.4443]], dtype=torch.float64)
```



### 自動微分

`torch.Tensor`の`requires_grad`属性を`True`にすることで，全ての計算履歴を追跡できるようになる．計算が終了した時に`backward()`メソッドを呼び出すことで，自動的に全ての微分を実行してくれる．微分係数は`grad`属性に格納される．
計算履歴の追跡を止めたいときは`detach()`メソッドを呼び出すことで，その先の計算履歴の追跡から切り離される．

個々の`Tensor`は`grad_fn`属性を持っている．この属性は，`Tensor`を作成する`Function`クラスを参照している．（厳密には，ユーザが定義した`Tensor`は`grad_fn`属性を持っておらず，計算結果の`Tensor`に`grad_fn`属性が付与される．）

```python
x = torch.ones(2, 2, requires_grad=True)
print(x)
# ---Output---
#tensor([[1., 1.],
#        [1., 1.]], requires_grad=True)

y = x + 2
print(y)
# ---Output---
#tensor([[3., 3.],
#        [3., 3.]], grad_fn=<AddBackward0>)

print(x.grad_fn)
print(y.grad_fn)
# ---Output---
# None
# <AddBackward0 object at 0x7f2285d93940>]

z = y * y * 3
out = z.mean()
print(z)
print(out)
# ---Output---
#tensor([[27., 27.],
#        [27., 27.]], grad_fn=<MulBackward0>)
#tensor(27., grad_fn=<MeanBackward0>)

print(z.grad_fn)
# ---Output---
#<MulBackward0 object at 0x7f2285d93ac8>

out.backward()
print(x.grad)
# ---Output---
#tensor([[4.5000, 4.5000],
#        [4.5000, 4.5000]])
```

最終結果について，計算を実際に行ってみると，

```math
out = \frac{1}{4} \sum_{i} z_i \\
z_i = y_i \cdot y_i \cdot 3 = 3 \cdot (x_i+2)^2
```
したがって，

```math
\frac{\partial out}{\partial x_i} = \frac{1}{4} \cdot 3 \cdot 2 \cdot (x_i+2) = 4.5
```

となることが確かめられる．


## まとめ
以下にこの記事の要点をまとめる．

- PyTorch は Define by Run の機械学習ライブラリ．
- 高速計算や自動微分が可能な `torch.Tensor` という配列を利用する．これは，Numpy の `numpy.ndaray`とほとんど同じような使い方（定義・演算）ができるほか，簡単に相互変換が行える．
- `torch.Tensor`の`requires_grad`属性を`True`にすることで計算履歴が追跡可能になり，計算終了時に`backward()`メソッドを呼び出すことで自動微分が行われる．これは，ニューラルネットワークの誤差逆伝播法によるパラメータ更新に大いに役立つ．