PyTorch超入門　PyTorchの基礎編

# はじめに
　このシリーズは、Pythonの機械学習用フレームワークであるPyTorchの学習のまとめとして記述します。
　今回はPyTorchの基礎中の基礎といった部分のまとめを記事とさせていただきました。伝えるためのものというよりも、要点をおさえて書くようにしているので読み物としては読みづらいかもしれませんが、ポイントの理解などに利用して頂ければと思います。

# PyTorchとは
　先にも述べましたが、PyTorchはPythonの機械学習用フレームワークです。機械学習用フレームワークといえば、Googleが開発したTensorFlowであったり、Kerasあるいはchainerなどがあります。
　個人的主観が入りますが、これらをざっくりと分けると以下の感じかなという印象です。

|フレームワーク|主な特徴|
|:--|:--|
|TensorFlow, Keras|産業向け|
|PyTorch, Chainer|研究向け（カスタマイズ性が高い）|

　また、機械学習を始めてみようと思った際に大半の人がTensorFlowから入ると思いますが、```define and run```というデータ構造の定義と実行を別々に記述するという少し変わったルールがあるため、いまいち馴染めずにフェードアウトする方もいらっしゃるかと思います。
　一方でPyTorchやChainerは```define by run```を採用しているため、普段コーディングしているPythonのコードと同じようにコーディングすることができるため、親しみやすい（好みはありますが）のが特徴です。

　ただし、少しとっつきにくい```define and run```の仕組みは、最適化が容易であったり、先に定義している分計算速度が速いなどのメリットもあるそうです。一長一短あるわけですね。

　それでは、実際にPyTorchの中身について見ていきましょう。


# PyTorchの構成
 PyTorchは以下のような構成になっています。

|構成内容|説明|
|:--|:--|
|torch|メインのネームスペースでTensorや様々な数学関数がこのパッケージに含まれる。**NumPyの構造を模している**|
|torch.autograd|自動微分のための関数が含まれる。自動微分のon/offを制御するコンテキストマネージャのenable_grad/no_gradや独自の微分可能関数を定義する際に使用する基底クラスであるFunctionなどが含まれる|
|torch.nn|ニューラルネットワークを構築するための様々なデータ構造やレイヤーが定義されている。例えばConvolutionやLSTM、ReLUなどの活性化関数やMSELossなどの損失関数も含まれる|
|torch.optim|確率的勾配降下（SGD）を中心としたパラメータ最適化アルゴリズムが実装されている|
|torch.utils.data|SGDの繰り返し計算を回す際のミニバッチを作るためのユーティリティ関数が含まれている|
|torch.onnx|ONNX(Open Neural Network Exchange)[^1]の形式でモデルをエクスポートするために使用する。|

[^1]: 様々な深層学習のフレームワーク間でモデルを共用するためのフォーマット

# Tensorの生成と変換
## Tensorの生成
　Tensorの生成方法はいくつかあります。

```python:Tensorの生成
import numpy as np
import torch

# 入れ子のlistを渡して作成
t = torch.tensor([[1, 2], [3, 4]])

# deviceを指定してGPUにTensorを作成
t = torch.tensor([[1, 2], [3, 4]], device = "cuda:0")

# dtypeを指定して倍精度のTensorを作る
t = torch.tensor([[1, 2], [3, 4]], dtype = "torch.float64")

# 0~9の数値で初期化された1次元のTensor
t = torch.arange(0, 10)

# 全値が0の100x100のTensorを作成し、toメソッドでGPUに転送
t = torch.zeros(100,100).to("cuda:0")

# 100x100のランダムなTensor
t = torch.random(100, 100)
```

## Tensorの変換方法
　TensorはNumPyのndarrayに簡単に変換できます。ただし、GPU上のTensorはそのままでは変換できず、一度CPU上に移す必要があります。

```python:tensorの変換
# numpyメソッドを使用してndarrayに変換
t = torch.tensor([[1, 2], [3, 4]])
nd_arr = t.numpy()

# GPU上のTensorをCPU上に移す
t = torch.tensor([[1, 2], [3, 4]], device = "cuda: 0")
t_cpu = t.to("cpu")
nd_arr = t_cpu.numpy()

# 上記を続けて記入
t = torch.tensor([[1, 2], [3, 4]], device = "cuda: 0")
nd_arr = t.to("cpu").numpy()
```


# Tensorのインデクシング操作
　Tensorでもndarrayと同様ににインデクシング操作[^2]をサポートしています。指定可能な方法は以下。

[^2]: 添字を指定して配列の値を取得したり変更したりすること

* スカラーによる指定
* 添字リスト
* ByteTensorによるマスク配列[^3]での指定

[^3]: 元の配列と同じサイズの配列で、配列内の各要素がTrue/Falseになっているもの。

```python:Tensorのインデクシング操作

t = torch.tensor([[1, 2, 3], [4, 5, 6]])

# スカラーの添字で指定
t[0, 2] # tensor(3)

# スライスで指定
t[:, :2] # tensor([[1, 2], [4, 5]])

# 添字のリストで指定
t[:, [1, 2]]  # tensor([[2, 3], [5, 6]])

# マスク配列を使用して3より大きい部分のみ選択
t[t > 3] # tensor([4, 5, 6])

# [0, 1]要素を100に置換
t[0, 1] = 100  # tensor([[1, 100, 3], [4, 5, 6]])

# スライスを使用した一括代入
t[:, 1] = 200  # tensor([[1, 200, 3], [4, 200, 6]])

# マスク配列を使用して特定条件の要素のみ置換
t[t > 10] = 20  # tensor([[1, 2, 3], [4, 5, 6]])

```

# Tensorの演算
Tensorは四則演算や数学関数、線形代数計算などを行うことができます。ndarrayでもできることですが、特に行列積や特異値分解[^4]などの線形代数計算はGPUが使用可能ということもあって大規模データの場合にはNumPy/SciPyを使用するよりも良いパフォーマンスが多々あります。

[^4]: 特異値分解(Singular Value Decomposition、 SVD)は線形代数でよく使用される計算であり、行列AをUSVのように3つの行列の積に分解する計算。<br />　UとVは直行行列、Sは対角成分のみの正方行列で、最小二乗法を解く際や、行列の近似・圧縮などに利用されます。

## Tensorの四則演算
　Tensorの四則演算の可否は以下の通りです。

||||
|---|---|---|
|Tensor|Tensor(同じ型)|〇|
|Tensor|Tensor(異なる型)|✕|
|Tensor|Pythonのスカラー|〇|
|Tensor|ndarray|✕|

　Tensor同士での四則演算でも同じ型である必要がある点が注意すべきポイントです。
　四則演算を行う際には、ベクトルとスカラーや行列とベクトル間の演算でも自動的に次元が補完されます（[ブロードキャスト](https://docs.scipi.org/doc/numpy-1.13.0/user/basics.broadcasting.html)）。

```python:Tensorの四則演算
# ベクトル
v = torch.tensor([1, 2, 3])
w = torch.tensor([4, 5, 6])

# 行列
m = torch.tensor([[0, 1, 2], [10, 20, 30]])
n = torch.tensor([[3, 4, 5], [40, 50, 60]])

# ベクトル - スカラー
v_pl = v + 10  # tensor([11, 12, 13])
v_mi = v - 10  # tensor([-9, -8, -7])
v_mu = v * 10  # tensor([10, 20, 30])
v_di = v / 10  # tensor([0, 0, 0])

# ベクトル - ベクトル
v_pl = v + w  # tensor([5, 7, 9])
v_mi = v - w  # tensor([-3, -3, -3])
v_mu = v * w  # tensor([4, 10, 18])
v_di = v / w  # tensor([0, 0, 0])

# 行列とベクトル
v_pl = m + v  # tensor([[ 1,  3,  5], [11, 22, 33]])
v_mi = m - v  # tensor([[-1, -1, -1], [ 9, 18, 27]])
v_mu = m * v  # tensor([[ 0,  2,  6], [10, 40, 90]])
v_di = m / v  # tensor([[ 0,  0,  0], [10, 10, 10]])

# 行列と行列
v_pl = m + n  # tensor([[3, 5, 7], [50, 70, 90]])
v_mi = m - n  # tensor([[-3, -3, -3], [-30, -30, -30]])
v_mu = m * n  # tensor([[0, 4, 10], [400, 1000, 1800]])
v_di = m / n  # tensor([[0, 0, 0], [0, 0, 0]])


```
　除算の際に出力が0となる箇所がありますね。お分かりかとは思いますが、演算の結果が小数点以下の値を示しても、tensorの型がintとなっているためです。回避したい場合はあらかじめfloatなどを指定しておきましょう。


# PyTorchで利用可能な数学関数
　PyTorchではTensorに対して様々な数学関数を用意しています。

**Tensorの各値に作用する数学関数**

|関数名|概要|
|:--|:--|
|abs|絶対値|
|sin|正弦|
|cos|余弦|
|exp|指数関数|
|log|対数関数|
|sqrt|平方根|

**集計関数**

|関数|概要|
|:--|:--|
|sum|tensor内の値の合計|
|max|tensor内の最大値|
|min|tensor内の最小値|
|mean|tensor内の値の平均値|
|std|tensor内の値の標準偏差|

# 線形代数の演算子

|関数|概要|
|:--|:--|
|dot|ベクトルの内積|
|mv|行列とベクトルの積|
|mm|行列と行列の積|
|matmul|引数の種類によって自動的にdot、mv、mmを選択して実行|
|gesv|LU分解による連立方程式の解|
|eig, symeig|固有値分解。symeigは対象行列用|
|svd|特異値分解|

　どのような結果が出るのか確認してみましょう（結果はかなり長い値になるので表示していません）。

```python:線形代数の演算
m = torch.randn(100, 10)  # 100x10の行列テストデータを作成
v = torch.randn(10)

# 内積
torch.dot(v, v)

# 行列とベクトルの積
torch.mv(m, v)

# 行列積
torch.mm(m.t(), m)

# 特異値分解
u, s, v = torch.svd(m)
```



# その他よく使う関数

|関数名|概要|
|:--|:--|
|view|Tensorの次元を変更する|
|cat, stack|Tensor同士を結合する|
|transpose|次元を入れ替える|


# 自動微分
　Tensorの持つ```requires_grad```を```True```にすると、自動微分を行うフラグが有効になります。ニューラルネットワークを扱う場合のパラメータやデータはこのフラグがすべて有効になっています。
　```requires_grad```が有効なTensorに様々な演算を積み重ねていくことで、計算グラフが構築され、backwardメソッドを呼ぶことでその情報から自動微分されます。


# おわりに
　今回は主にPyTorchの構成や機械学習を扱う上で必須となるTensorについてまとめました。最後までお読みいただいた皆さん、ありがとうございました。
　次回以降少しずつニューラルネットワークの構築などを行っていこうと思いますので、ご興味ある方はぜひご覧頂ければと思います。

#### 参考
杜世橋(2018年)『[現場で使える！PyTorch開発入門 深層学習モデルの作成とアプリケーションへの実装](https://www.shoeisha.co.jp/book/detail/9784798157184)』