実践Pytorch

※Pytorchのバージョンが0.4になり大きな変更があったため記事の書き直しを行いました。

#初めに
この記事は深層学習フレームワークの一つであるPytorchによるモデルの定義の方法、学習の方法、自作関数の作り方について備忘録です。

#インストール
Pytorchはcondaやpipで簡単にインストールすることができます。[こちら](http://pytorch.org/)からosやpython,cudaのバージョンを選択すると適したスクリプトが表示されるのでコピペすればインストールが可能です。(cudaやcudnnは別途セットアップが必要)
現状はlinuxかosxのみでwindowsには対応してないようです。(0.4よりwindowsも公式でサポートされました。)

#公式チュートリアル
これからPytorchを使ってみようという人はこの記事よりも[公式のチュートリアル](https://github.com/pytorch/tutorials)が非常にわかりやすいのでそちらを参考にしたほうがいいです。また[example](https://github.com/pytorch/examples)も非常に参考になります。その他[ドキュメント](http://pytorch.org/docs/)や[フォーラム](https://discuss.pytorch.org/)を見ればわからないことはほとんど解決すると思います。

#実践
Pytorchは行列操作は基本的にtorch.Tensorを使います。Torch7のtorch.Tensorと基本的に使い方は同じです。ただ、Torch7と違うのはモデルへの入力がミニバッチでの入力を前提としています。2次元畳み込みの場合Torch7では3次元、4次元どちらの入力でも問題なかったのですが、Pytorchでは4次元が前提となっています。
また、modelを用いた計算時にはtorch.TensorをVariableに変えて用います。
Pytorch0.4以前では以下がimportされていれば基本的なモデルの定義から学習までを行うことができます。

```python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
```

Pytorch0.4以降ではVariableがtorch.Tensorと統合されたため、Variableのimportは必要ありません。

##モデルの定義
Pytorchでは以下のようにTorch7と同じようにモデルを定義することが可能です。

```python:model1.
model = nn.Sequential()
model.add_module('fc1', nn.Linear(10,100))
model.add_module('relu', nn.ReLU())
model.add_module('fc2', nn.Linear(100,10))
```

違いとしては、一つ一つのレイヤーに名前を与えることです。
listに層を入れてnn.Sequential()に突っ込むことも可能です。

```python:model2.py
layer = []
layer.append(nn.Linear(10,100))
layer.append(nn.ReLU())
layer.append(nn.Linear(100,10))
model = nn.Sequential(*layer)
```
また、以下のようにクラスとして定義することも可能です。

```python:model3.py
import torch.nn.functional as F
class Model(nn.Module):
  def __init__(self):
    super(Model,self).__init__()
    self.fc1 = nn.Linear(10,100)
    self.fc2 = nn.Linear(100,10)

  def forward(self,x):
    x = self.fc1(x)
    x = F.relu(x)
    x = self.fc2(x)
    return x
```

chainerを使ったことがある人は馴染みのある定義の方法だと思います。Pytorchではnn.Sequentialを駆使することでmodelの定義の量やforwardの記述量を減らすことが可能です。modelの組み方の参考としてはPytorchのtorchvisionパッケージの[実装例](https://github.com/pytorch/vision/tree/master/torchvision/models)が参考になります。
上記三つを目的に合わせて使い分けたり組み合わせたりしてモデルを組んで行きます。

pytorch使い始めでめちゃくちゃやりがちなミスとして，レイヤーをリストで保持してしまうこと．同じハイパーパラメータのレイヤーを何度も使う際にはfor文を使って定義すれば楽なのですが，学習可能なパラメータをリストで保持してしまうと，モデルのパラメータを呼び出すときにリストで保持しているレイヤーのパラメータはパラメータとして認識されず呼び出されません．そうすると何が起こるかというと，学習の時にパラメータの更新が行われなくってしまいます．以下悪い例です．

```python:list.py
class Model(nn.Module):
  def __init__(self):
    super(Model, self).__init__()
    self.layer = [nn.Linear(10,10) for _ in range(10)]

  def forward(self, x):
    for i in range(len(self.layer)):
      x = self.layer[i](x)
    return x

model = Model()
# model.parameters()で学習パラメータのイテレータを取得できるが，
# listで保持しているとlist内のモジュールのパラメータは取得できない
# optimについては後述
optimize = optim.SGD(model.parameters(), lr=0.1)

```

そのためこのような場合には[nn.ModuleList](https://pytorch.org/docs/stable/nn.html#modulelist)を使って定義します．

```python:modulelist.py
class Model(nn.Module):
  def __init__(self):
    super(Model, self).__init__()
    layer = [nn.Linear(10,10) for _ in range(10)]
    self.layer = nn.ModuleList(layer)

  def forward(self, x):
    for i in range(len(self.layer)):
      x = self.layer[i](x)
    return x

model = Model()
# model.parameters()で学習パラメータのイテレータを取得できるが，
# listで保持しているとlist内のモジュールのパラメータは取得できない
# optimについては後述
optimize = optim.SGD(model.parameters(), lr=0.1)

```

ハマりどころなので要注意です．

##GPUの使用
以下のようにmodelや変数をcudatensorとして定義することでGPU上で計算できます。

```python:gpu.py
import torch

x = torch.randn(10)
y = torch.randn(10)

"""
Pytorch 0.4 以前
x = x.cuda()
y = y.cuda(0) # 引数に数字を入れると数字に対応したidのGPUを利用可能

z = x * y # GPU上で計算が行われる。

z = z.cpu() # cpuへ
"""

"""
Pytorch 0.4 以降
x = x.to('cuda')
y = y.to('cuda:0') # cudaの後に:数字で対応したGPUを使用

z = x * y

z = z.to('cpu') # cpuへ
"""

print(x.is_cuda) # 変数がGPU上にあればTrue
```


##ファインチューニング
Pytorchではtorchvision.modelsを使うことでAlexNet、VGGNet、ResNet、DenseNet、SqueezeNet、GoogleNetが簡単に定義可能で、またこれらの学習済みモデルを簡単に使用することができます。

```python:get_model.py
import torchvision.models as models
alexnet = models.alexnet()
pretrain_alexnet = models.alexnet(pretrained=True) #PretrainedオプションをTrueにすることで学習済みモデルをダウンロード可能
```

また、以下のように出力の次元数を変えることでファインチューニングすることができます。

```python:finetune1.py
resnet = models.resnet50(pretrained=True)
resnet.fc = nn.Liear(2048, 100)
```

また、一部の層のみを使いたい場合は以下のように記述できます。

```python:finetune2.py
resnet = models.resnet50(pretrained=True)
resnet = nn.Sequential(*list(resnet.children())[:-3])
```

スライスを用いて任意の層を取り出すことが可能です。
resnetのGlobal Average PoolingをMax Poolingに変えて出力を10次元にする例を示します。

```python:resnet_finetune.py
class Resnet(nn.Module):
  def __init__(self):
    super(Resnet,self).__init__()
    resnet = models.resnet50(pretrained=True)
    self.resnet = nn.Sequential(*list(resnet.children())[:-2])
    self.maxpool = nn.MaxPool2d(kernel_size=7)
    self.fc = nn.Linear(2048, 10)

  def forward(self,x):
    x = self.resnet(x)
    x = self.maxpool(x)
    x = self.fc(x)
    return x
```

##学習
optimパッケージを使って任意の最適化手法を用いてパラメータの更新を行います。最適化手法のパラメータを設定したら、backward計算をするたびstep()を呼び出すことで更新を行うことができます。

```python:update.py
"""
Pytorch 0.4 以降
if torch.cuda.is_available(): # GPUが利用可能か確認
  device = 'cuda'
else:
  device = 'cpu'
"""
# modelの定義
model = models.resnet18()

"""
Pytorch 0.4 以降
model = model.to(device)
"""
# 最適化手法のパラメータ設定
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# loss関数の定義
criterion = nn.MSELoss()
# 入力と正解を乱数で生成
input = torch.randn(1,3,224,224) # バッチ x チャネル x 高さ x 幅
target = torch.randn(1,1000)

"""
# Pytorch 0.4以前 
# Variableに変えて計算をする
input = Variable(input)
"""

# requires_gradをFalseにすることでその変数に対して勾配の計算を行わないことができる
# デフォルトでFalseになっているため今回のように明示的に書く必要はない
"""
Pytorch 0.4 以前
target = Variable(target, requires_grad=False)
"""

"""
Pytorch 0.4 以降
target.requires_grad = False
"""

# バッチ正規化等、学習時と推論時で振る舞いの違うモジュールの振る舞いを学習時の振る舞いに
# model.eval()で推論時の振る舞いに変更可能
model.train()
# 学習ループ
for i in range(100):
  # 順伝播
  out = model(input)
  # ロスの計算
  loss = criterion(out, target)
  # 勾配の初期化
  optimizer.zero_grad()
  # 勾配の計算
  loss.backward()
  # パラメータの更新
  optimizer.step()
```

###optimを使わない場合
optimを使わない場合、optimizer.step()の部分を以下のように書き換えることでパラメータの更新が可能です。

```python:update_without_optim.py
for param in model.parameters():
    param.data -= learning_rate * param.grad.data
```

###RNNの場合
RNNの場合、以下のようにlossを足していき更新のタイミングでstep()を呼び出すことで逆伝播を行うことが可能です。

```python:rnn_update.py
class RNN(nn.Module):
  def __init__(self, data_size, hidden_size, output_size):
    super(RNN, self).__init__()
    self.hidden_size = hidden_size
    input_size = data_size + hidden_size
    self.i2h = nn.Linear(input_size, hidden_size)
    self.h2o = nn.Linear(hidden_size, output_size)

  def forward(self, data, last_hidden):
    input = torch.cat((data, last_hidden), 1)
    hidden = self.i2h(input)
    output = self.h2o(hidden)
    return hidden, output

RNN = RNN()
# optimizerなど省略
for i in range(10):
  hidden, output = RNN(input, hidden)
  loss += criterion(output, target)
loss.backward()
optimizer.step()
```

上記コードでは10ステップ後にロスを計算してパラメータの更新を行なっています。

#モデルの保存
基本的にTorch7と同様torch.save()で保存できますが、state_dict()を使い学習パラメータのみの保存を行います。

```python:model_save.py
model = models.resnet50(pretrained=True)
# modelの保存
torch.save(model.state_dict(), 'weight.pth')
model2 = models.resnet50()
# パラメータの読み込み
param = torch.load('weight.pth')
model2.load_state_dict(param)
```

モデルだけでくoptimizerも同様にtorch.save()とstate_dict()を使って保存が可能です。

```python:optimizer_save.py
optimizer = optim.SGD(model.parameters(), lr=0.1)
torch.save(optimizer.state_dict(), 'optimizer.pth')

optimizer2 = optim.SGD(model.parameters(), lr=0.1)
optimizer2.load_state_dict(torch.load('optimizer.pth')
```


#Numpyによる自作関数
PytorchはNumpyを用いて簡単にオリジナルレイヤや関数を作ることができます。(Cで書くこともできます)
Functionクラスを継承してforward計算とbackward計算を書くだけです。
例えばReLU関数を自作すると以下のようになります。(relu関数はもともと実装されているので自分で書くことはないです)

```python:relu.py
from torch.autograd import Function

class relu(Function):
  def forward(self,x):
    # torch.Tensorからnumpyへ
    numpy_x = x.numpy()
    result = np.maximum(numpy_x,0)
    # numpyからtorch.Tensorへ
    result = torch.FloatTensor(result)
    # backward計算のためにTensorを保持
    self.save_for_backward(result)
    return result

  def backward(self, grad_output):
    result = self.saved_tensors[0]
    grad_input = grad_output.numpy() * (result.numpy() > 0)
    # 入力に対する勾配を返す
    return torch.FloatTensor(grad_input)
```

学習するパラメータがある場合は学習パラメータにParameterを噛まし、nn.Moduleを継承したクラスを定義する必要があります。入力に対して重みをかけるだけの演算を例として実装します。(x=[1,2,3], w=[0.1,0.2,0.3]とした場合出力が[0.1,0.4,0.9]になる演算。wは学習可能なパラメータ)

```python:elemwise.py
from torch.autograd import Function
from torch.nn.parameter import Parameter
class elemwiseFunction(Function):
  def forward(self, x, w):
    self.save_for_backward(x, w)
    numpy_x = x.numpy()
    numpy_w = w.numpy()
    result = numpy_x*numpy_w
    return torch.FloatTensor(result)

  def backward(self, grad_output):
    input, w = self.saved_tensors
    w_grad = input.numpy() * grad_output
    x_grad = w.numpy() * grad_output
    # 入力に対する勾配と学習パラメータに対する勾配を返す
    return torch.FloatTensor(x_grad), torch.FloatTensor(w_grad)

class elemwise(nn.Module):
  def __init__(self):
    super(elemwise,self).__init__()
    self.w = Parameter(torch.randn(10)

  def forward(self):
    return elemwiseFunction()(x, self.w)

```
上記コードはforwardとbackwardの入出力がtorch.Tensorであればいいので中身はnumpyを用いなくてもいいです。例えばcupyを用いて書くことや、極論その他のライブラリーを使って書くことも可能です。
また、Pytorchには自動微分があるため、上記のような演算においてはnumpyを用いずtorch.Tensorの演算で済ませることでbackwardを書く必要がなくなります。

```python:elemwise_without_backward.py
class elemwise(nn.Module):
  def __init__(self):
    super(elemwise,self).__init__()
    self.w = Parameter(torch.randn(10)

  def forward(self, x):
    return x * self.w
```

　以下、CやC++による自作レイヤーの定義やCUDAカーネルを使った関数を定義する方法の参考となるサイトです。
[Cによる拡張方法の公式チュートリアル](https://pytorch.org/tutorials/advanced/c_extension.html)、[Cによる公式実装例](https://github.com/pytorch/extension-ffi)、[C++による公式実装例](https://github.com/pytorch/extension-cpp)、[C++とcudaカーネルによる拡張の公式チュートリアル](https://pytorch.org/tutorials/advanced/cpp_extension.html)、[Pytorchソースコード](https://github.com/pytorch/pytorch/tree/master/aten/src)
ソースコードではTH,、THS、THC、THCSにtorch.Tensorに関する実装が、THNN、THCUNNにはニューラルネット関連の実装があります。

## その他
以下はNN学習の際に役立つちょっとしたことです。何か思い出すたび追記していければと思っています。

# 計算グラフを作らない方法
pytorchは順伝播時にbackwardするための計算グラフを構築しながら計算を行います。これは推論時には不要でありメモリ節約のため以下のように止めることをお勧めします。

```python:no_grad.py

import torch
x = torch.randn(10)
"""
Pytorch 0.4 以前
"""
from torch.autograd import Variable
x = Variable(x, volatile=True) # volatileオプションをTrueに
y = x**2


"""
Pytorch 0.4 以降
"""
with torch.no_grad():
  # withの中で計算グラフを作りたくない演算を実行
  y = x**2

```

# ロスの平均を計算
学習経過を見るためにロスの平均を算出したい場合があります。その際，単純に各イテレーションのロスを足し合わせていくと計算グラフを作り続けてメモリを食い続けるという問題があるので以下のように記述します。

```python:mean_loss.py
sum_loss = 0

# 学習ループ
for i in range(100):
  """
  順伝播など記述
  """
  loss = loss_function(outputs, targets) # 適当なロス関数でロスを計算

  """
  Pytorch 0.4 以前
  """
  # dataによりVariableからtorch.Tensorへ。さらにgpuからcpuに変えて0番目のインデックスを指定
  sum_loss += loss.data.cpu()[0] 

  """
  Pytorch 0.4 以降
  """
  sum_loss += loss.item()

print("mean loss: ", sum_loss/i) 

```

Pytorch 0.4以前では操作が面倒でしたが0.4以降item()を呼び出すことで簡潔になりました。