👌

# Pytorchの基礎 forwardとbackwardを理解する

2020/09/27に公開

forwardは一言で言えば順伝搬の処理を定義しています。

ただkerasに比べて複雑に感じる時があります。

# データセットの用意

``````import numpy as np
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
``````

データセットは今回アヤメを利用しようと思います。

``````data = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
``````

バッチサイズは25、隠れ層の次元数も適当に3にしておきます。

``````iris = load_iris()
data = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
y = np.identity(3, dtype=np.int64)[y]
X_train, X_test, y_train, y_test = train_test_split(data.values, y, train_size=0.67, shuffle=True)
print(f"X_train: {X_train.shape}, X_test: {X_test.shape}, y_train: {y_train.shape}, y_test: {y_test.shape}")
X_train = torch.from_numpy(X_train).float()
y_train = torch.from_numpy(y_train).float()
X_test = torch.from_numpy(X_test).float()
y_test = torch.from_numpy(y_test).float()

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 25, X_train.shape[1], 3, y_train.shape[1]
``````

# まずは適当に学習させてみる

``````class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
In the constructor we instantiate two nn.Linear modules and assign them as
member variables.
"""
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)

def forward(self, x):
"""
In the forward function we accept a Tensor of input data and we must return
a Tensor of output data. We can use Modules defined in the constructor as
well as arbitrary operators on Tensors.
"""
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred

model = TwoLayerNet(D_in, H, D_out)

criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)

losses = []
testlosses = []

for t in range(50):
y_pred = model(X_train)

loss = criterion(y_pred, y_train)
losses.append(loss.item())
if t % 10 == 0:
print(t, loss.item())

loss.backward()
optimizer.step()
y_test_pred = model(X_test)
testloss = criterion(y_test_pred, y_test)
testlosses.append(testloss)
plt.plot(losses)
plt.plot(testlosses)
``````

モデルもexampleから適当に見繕いました。
※分類問題なのでクロスエントロピーを利用するのがベストプラクティスかもしれませんが、なぜかうまくいかなかったことと本題から逸れるため、このままMSEで行きます。

# forwardを理解したい

## モデルの中のforwardを理解したい

``````class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
In the constructor we instantiate two nn.Linear modules and assign them as
member variables.
"""
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)

def forward(self, x):
"""
In the forward function we accept a Tensor of input data and we must return
a Tensor of output data. We can use Modules defined in the constructor as
well as arbitrary operators on Tensors.
"""
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
``````

`torch.nn.Linear` は重みとバイアスだけを持つ簡単な結合層です。(Linear — PyTorch 1.6.0 documentation)

``````        self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)
``````

``````    def forward(self, x):
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
``````

forwardはデータxをtensor型で受け取って、3層のレイヤーを通って出力しているだけです。
clampはデータに最小値・最大値を与えてその範囲内にデータを納めます。（torch.clamp — PyTorch 1.6.0 documentation

``````>>> a = torch.randn(4)
>>> a
tensor([-1.7120,  0.1734, -0.0478, -0.0922])
>>> torch.clamp(a, min=-0.5, max=0.5)
tensor([-0.5000,  0.1734, -0.0478, -0.0922])
``````

# なぜforwardに書く必要があるのか？

この辺りがすごく不思議に思えてしまったのです。

ここでニューラルネットワークの基本的な構造を思い出します。

ニューラルネットワークの基礎を学んだなら当たり前のことなのですが、それぞれの層・活性化関数・損失関数も全て入力から出力までを順伝搬する仕組みです。

それぞれをレイヤーとして認識して`nn.Module`クラスを基底として全てに順伝搬を定義しているだけです。

さらに深く理解するため、`backward`の役割を改めて見ておきます。

``````optimizer.zero_grad()
loss.backward()
optimizer.step()
``````

ニューラルネットワークの重みを更新するためには誤差逆伝搬法を利用します。（誤差逆伝搬法が何かについては探せばいくらでも記事が出てきますのでこちらでは割愛）

それでは上のコードが何をしているか確認していきます。
`optimizer.zero_grad()`は一旦「計算した勾配の初期化」だと思っておいて下さい。

lossは損失関数でここではMSE(Mean Square Error)を利用しています。ですが、損失関数と名のついた関数には変わりありません。

``````x = torch.tensor(3.0, requires_grad=True)
``````

`requires_grad`は勾配を自動で計算することを定義する引数です。ここで`True`としておくと、その先にある様々の層の計算に大して、どれくらい寄与するのかその勾配を計算します。

そして次に出力としてy = 2xを定義します。

``````y = 2*x
print(y)
``````

この時y = 2xのxの勾配を出力します。
その値は`x.grad`に入っています。ただし先に勾配を計算する必要があります。

``````y.backward()
``````

です。こうすることで`x.grad`は計算されます。xを定義する時にrequires_grad=Trueとしていないと、このタイミングでエラーが発生します。

``````print(x.grad)
# tensor(3.)
``````

``````z = 5*y
``````

これは合成関数の微分を使って解くと（代入してもさくっと解けてしまいますが）zをxで微分すると15となります。これがzに対するxの勾配です。
あたらためて全て書き直すと以下のようになります。

``````x = torch.tensor(2.0, requires_grad=True)
y = 3*x
z = 5*y
z.backward()
# tensor(15.)
``````

なぜ全て書き直したかというと、この時４行目のz.backwardをy.backwardにすると3が出力されるからです。ぜひ丸っとコピペして試してみてください。
つまりbackwardは`requires_grad=True`とした変数に対して（ここではx）、目的の関数（ここではy, z）に対して微分を行った時の勾配を計算する事ができます。

そしてこの更新に対して注意が必要なのは、計算された重みは追加されていくという事です。つまり`z.backward()`をn実行すると、x.gradはn * 15となります。
そのため`optimizer.zero_grad()`で勾配を初期化する必要が出てくるわけです。

## 最後にoptimizer.step()

これは重みの更新を行っています。
この行をコメントアウトすると学習の結果は以下のようになります。

`optimizer.step()` で学習率と最適化手法に基づいて重みを更新しているわけです。
さらにschedulerなどを設定している時は、学習率の動的な変更もこのタイミングで行われています。

## 損失関数を新しく定義したい時もforward

``````class RMSELoss(nn.Module):
def __init__(self, eps=1e-6):
super().__init__()
self.mse = nn.MSELoss()
self.eps = eps

def forward(self, yhat, y):
loss = torch.sqrt(self.mse(yhat, y) + self.eps)
return loss
``````

これはMSEをRMSEに変更したい時のコードです。
（ちなみにこれを見て理解はできるものの本当に説明できるか不安になり、本記事につながりました）
これを利用し、上記のコードで学習し直すと。

RMSEはMSEの平方根を取る形です。MSEに比べて外れ値に鈍感になる上損失は数字上小さくなるので（単位も異なりますので）学習を回す回数は増えましたが、ちゃんと収束しました。

## 活性化関数を新しく定義したい時もforward

``````class ReLU(Module):
def __init__(self, inplace: bool = False):
super(ReLU, self).__init__()
self.inplace = inplace

def forward(self, input: Tensor) -> Tensor:
return F.relu(input, inplace=self.inplace)
``````

# まとめ

• PyTorchはnn.Moduleクラスを基底とし、順伝搬の処理をforwardの中に書いている。
• さらにnn.Moduleを基底として、それらの入力層・隠れ層・出力層・活性化関数・損失関数などを組み合わせる（つなぎ合わせる）ことでモデルを作成し学習する。