pyTorch optim SGD徹底解説

2020/1/27 投稿

# 0. この記事の対象者
- pythonを触ったことがあり,実行環境が整っている人
- pyTorchをある程度触ったことがある人
- pyTorchによる機械学習でoptimizer SGDを理解したい人
- pyTorchのoptimizer SGDをNetwork model以外(普通の変数とか)に使いたい人



# 1. はじめに
昨今では機械学習に対してpython言語による研究が主である.なぜならpythonにはデータ分析や計算を高速で行うためのライブラリ(moduleと呼ばれる)がたくさん存在するからだ.
その中でも今回は**pyTorch**と呼ばれるmoduleを使用し,その中の確率的勾配降下法(SGD)を使ったパラメータの更新手法について**プログラムでの扱い方**を解説する.
ただしこの記事は自身のメモのようなもので,あくまで参考程度にしてほしいということと,簡潔に言うために間違った表現や言い回しをしている場合があるかもしれないが,そこはどうかご了承していただきたい.
さらにSGDについての本当に詳しい解説はしないため,そちらに興味がある方は是非自身で学んでほしい.

また,この記事では実際にNetworkを使って学習をしたりはしない.
そちらに興味がある場合は以下のLinkを参照してほしい.

[pyTorchでCNNsを徹底解説](https://qiita.com/mslive/private/8e1f9a8467fff8dfd03c)


# 2. pyTorchのインストール
pyTorchを初めて使用する場合,pythonにはpyTorchがまだインストールされていないためcmdでのインストールをしなければならない.
下記のLinkに飛び,ページの下の方にある「QUICK START LOCALLY」で自身の環境のものを選択し,現れたコマンドをcmd等で入力する(コマンドをコピペして実行で良いはず).

[pytorch 公式サイト](https://pytorch.org/)


# 3. pyTorchに用意されている特殊な型
numpyにはndarrayという型があるようにpyTorchには「**Tensor型**」という型が存在する.
ndarray型のように行列計算などができ,互いにかなり似ているのだが,Tensor型はGPUを使用できるという点で機械学習に優れている.
なぜなら機械学習はかなりの計算量が必要なため計算速度が早いGPUを使用するからだ.
さらに,Tensor型は機械学習のパラメータ更新のための微分が非常に簡単にできる.
これがとても簡単にできることが今回の記事の鍵となるのだ.

Tensor型の操作や説明は下記Linkより参照していただきたい.

[pyTorchのTensor型とは](https://qiita.com/mslive/private/241bfb42d852bb801b96)

微分がどのように実現されているかは下記Linkより参照していただきたい.

[pyTorch backwardできない例まとめ](https://qiita.com/mathlive/items/3dcb46af2e2f0eca559a)


# 4. Stochastic Gradient Descent (SGD)とは
これは確率的勾配降下法と言い,簡単に言うとパラメータの更新手法のことである.
これに関する数学的な解説等はググればいくらでも出てくるのでここであえて説明はしない.


# 5. pyTorchのSGD 
### 5-1. pyTorchのimport
まずはpyTorchを使用できるようにimportをする.
ここからはcmd等ではなくpythonファイルに書き込んでいく.
下記のコードを書くことでmoduleの使用をする.

```ruby:filename.rb
import torch
import torch.optim as optim
```

この2行目の「**import torch.optim as optim**」はSGDを使うために用意するmoduleである.



### 5-2. optim.SGD
まず,SGDの引数を説明する.
使い方は以下のように書く.

```ruby:filename.rb
op = optim.SGD(params, lr=l, momentum=m, dampening=d, weight_decay=w, nesterov=n)
```
以下引数の説明

- params : 更新したいパラメータを渡す.このパラメータは微分可能でないといけない.
- lr : 学習率(learning rate). float型を渡す.
- momentum : モーメンタム. float型を渡す.
- dampening : モーメンタムの勢いを操作する. float型を渡す.
- weight_decay : paramsのL2ノルムを正則化としてどれくらい加えるか. float型を渡す.
- nesterov : nesterov momentumをモーメンタムとして適用するか.True or Falseを渡す.

今回はSGDの挙動を見るために余計な情報であるmomentum, dampening, weight_decay, nestrovは初期値(全部0やFalseとなっている)のまま行う.

### 5-3. SGDの使用
プログラム自体はとても簡単で,まず下準備として計算の例を示す.

```ruby:filename.rb
x = torch.tensor(5.0, requires_grad = True)
c = torch.tensor(3.0, requires_grad = True)
b = torch.tensor(2.0)
d = 4.0
y = c*3*torch.exp(x) + b*x + d
print(y)

------------以下出力---------------
tensor(1349.7185, grad_fn=<AddBackward0>)
```

このプログラムを式で書くと

```math
y = 3 c e^{x} + bx + d
```

で$x = 5$, $c = 3$, $b = 2$, $d = 4$である.
また変数**x**と**c**のみ微分計算ができるように「**requires_grad = True**」としている.
このことから,この二つの変数を更新するためにSGDにパスするのだ.
以下が例である.

```ruby:filename.rb
op = optim.SGD([x,c], lr=1.0)
```

ここで注意するのはSGDの**params**の部分にパスする引数を「**[x,c]**」としているが,このように渡すパラメータをlist「**[  ]**」でかこってあげなければならない.
これはもちろん**パラメータの変数が1つだけの時も同様にする**.
また機械学習などでNetworkを作った時そのmodelのパラメータを入れる場合があるが,その場合はこのカッコはいらない.
少し詳しく言うとparamsはiterationが引数で来ることを期待していて,modelのパラメータはiterationの形になっているからいらないのである.
modelを入れる例は上でも紹介したCNNの解説を見てほしい.

また,この変数**op**が今SGDの機能をもっているのだが,実際に出力してみるとSGDの内容が現れる.

```ruby:filename.rb
print(op)

------------以下出力---------------
SGD (
Parameter Group 0
    dampening: 0
    lr: 1.0
    momentum: 0
    nesterov: False
    weight_decay: 0
)
```



実際のパラメータ微分は以下のようになる.

```ruby:filename.rb
y.backward()
print(x.grad)
print(c.grad)

------------以下出力---------------
tensor(1337.7185)
tensor(445.2395)
```

各変数の微分値は「**変数名.grad**」とすれば閲覧できる.
このように「**y.backward()**」とすると最終的な出力**y**に関する変数の微分が自動で行われる.
それではパラメータ更新は以下のように行う.

```ruby:filename.rb
op.step()
print(x)
print(c)

------------以下出力---------------
tensor(-1332.7185, requires_grad=True)
tensor(-442.2395, requires_grad=True)
```

このように「**op.step()**」で各変数の微分情報を使って更新を行う.
つまり実際の変数である**x**と**c**のメモリとSGDが持っているそれらの値のメモリが一致しているということがわかるのだ.
現条件下での更新式は以下のようになっている.

```math
x \leftarrow x - \eta\frac{\partial y}{\partial x}
```

$\eta$は学習率のことで今は1.0である.
上の式を実際の変数の値と微分の値で書き換えると

```math
-1332.7185 \leftarrow 5.0 - 1.0\times 1337.7185
```

確かにあっているわけだ.


### 5-4. SGDの中身の書き換え
上でSGDの機能を持たせた変数**op**の出力はしたが,パラメータなどの詳しい情報は出てこなかった.
詳細な出力は以下のようにすればよい.
ただし以下のプログラムは上で行った微分計算と更新をする前のデータで行っている(変数xとcは最初のまま).

```ruby:filename.rb
print(op.param_groups)

------------以下出力---------------
[{'params': [tensor(5., requires_grad=True), tensor(3., requires_grad=True)],
  'lr': 1.0,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

このようにすればSGDの詳細なパラメータを見ることができ,それらの情報はlist型「**[ ]**」に入っていることがわかる.
つまり,中身をいじる場合は以下のようにすればよい.

```ruby:filename.rb
op.param_groups[0]['lr'] = 0.1
print(op)

------------以下出力---------------
SGD (
Parameter Group 0
    dampening: 0
    lr: 0.1
    momentum: 0
    nesterov: False
    weight_decay: 0
)
```

今,学習率である**lr**を0.1に書き換えた.
実際に通常の出力をしてみると確かに書き換わったことがわかる.


### 5-5. 更新したい変数の書き換え(SGD側)
上のように,SGDのパラメータ書き換えを使って変数**x**の情報も書き換えてみよう.
まずパラメータは

```ruby:filename.rb
print(op.param_groups[0]['params'])

------------以下出力---------------
[tensor(5., requires_grad=True), tensor(3., requires_grad=True)]
```

である.
さて,これの変数**x**側である0番目の要素を書き換える.

```ruby:filename.rb
op.param_groups[0]['params'][0] = torch.tensor(10., requires_grad=True)
print(op.param_groups)

------------以下出力---------------
[{'params': [tensor(10., requires_grad=True), tensor(3., requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

たしかに書き換わった.
**しかし実際の変数の値を見てみよう.**

```ruby:filename.rb
print(x)

------------以下出力---------------
tensor(5., requires_grad=True)
```

本体は全く書き換わっていないのだ.
実際にbackward()と更新をしてみると

```ruby:filename.rb
y.backward()
op.step()
print(x)
print(x.grad)
print(op.param_groups)

------------以下出力---------------
tensor(5., requires_grad=True)
tensor(1337.7185)
[{'params': [tensor(10., requires_grad=True),
   tensor(-41.5240, requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

学習率lrが0.1になっているため変数cの値は上の例の時と違うことに注意してほしい.
このように変数xの値もSGDが持つxの値も更新されなくなっている.
しかも**x.grad**は変数**x**が5の時の計算となっている.
この原因は「**torch.tensor(10., requires_grad=True)**」を代入する際にメモリがの位置が異なってしまうがために起こってしまうのだ.
変数の値の変更は十分に注意して行うことだ.

### 5-5. 更新したい変数の書き換え(通常の変数側)
上ではSGDの変数情報を書き換えると,変数とSGDのパラメータとが互いに作用しなくなることがわかった.
理由はメモリの不一致であると述べた.
では今度は変数の方を書き換えてみよう.

```ruby:filename.rb
op = optim.SGD([x,c], lr=1.0)
print(op.param_groups)

------------以下出力---------------
[{'params': [tensor(5., requires_grad=True), tensor(3., requires_grad=True)],
  'lr': 1.0,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

この変数**x**に対し

```ruby:filename.rb
x = torch.tensor(10., requires_grad=True)
print(x)

------------以下出力---------------
tensor(10., requires_grad=True)
```

とした.
ここでもう一度変数**op**の詳細を見ると

```ruby:filename.rb
print(op.param_groups)

------------以下出力---------------
[{'params': [tensor(5., requires_grad=True), tensor(3., requires_grad=True)],
  'lr': 1.0,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

パラメータは変わっていない.
もちろんこのままbackward()と更新をすると

```ruby:filename.rb
y.backward()
op.step()
print(x)
print(x.grad)
print(op.param_groups)

------------以下出力---------------
tensor(10., requires_grad=True)
None
[{'params': [tensor(-1332.7185, requires_grad=True),
   tensor(-442.2395, requires_grad=True)],
  'lr': 1.0,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
```

面白いことにメモリの異なった変数**x**にはなにもされておらず,SGDの持つパラメータは更新されている.
結論から言うと,**更新したい変数をむやみに書き換えることは避けるべきである**.


# 6. ひとこと
今回はpyTorchを使用したoptimizerのSGDについて簡単ではあるが説明させていただいた.
意外とSGDをNetwork以外に適応する例はなかったので紹介しておく.
読みづらい点も多かったと思うが読んでいただきありがとうございます.